前回ではゲームオーバーを実装しました。様々なものができてきたので、今回は音をつけていきたいと思います。最後までついて来て頂ければ↑のようなゲームを作る事ができます。
この記事は本のように順を追って解説しています。この記事は途中のページになります。
この記事を見ていて、現在の状況がわからない場合や忘れてしまった事などが出てきたら↓のリンクから目次ページへ飛べますので立ち戻って見てください。
<アセットストアで素材を集めよう>
さて、今までタイトルシーンとステージ1を作成してきましたが、今のところ音に関するものが全くなかったのでそろそろ実装していきたいと思います。
実装する前にアセットストアを使ってとりあえず仮の素材を集めようと思います。
アセットストアというのはUnityのマーケット的な場所で、色々な人がゲーム制作に使える素材や便利な機能などを販売しています。
販売と言っていますが無料のものもあるので、今回は無料のものをダウンロードしてこようと思います。
アセットストアを使うには注意点みたいなものが多々存在するので、一旦無料のものでどんな感じか試してみるといいと思います。まぁ、今回は仮のBGMとSEをダウンロードして試しに当てはめてみるみたいな感じです。
詳しく知りたい方は↓の記事で解説しています。
アセットストアを開くには上部メニューのWindow>Asset Storeから開きます。
するとAsset Storeのウィンドウが開きます。
サインインされてない方は右上のアイコンからサインインしてください。
右側のオーディオにチェックを入れるとオーディオに絞り込みされます。
Sort byのところをPopularity(人気順)から、Price(Low to High)(安い順)に変更します。
するとFREEのものが並ぶと思います。その中のものを適当に押してください。
するとそのアセットの説明が表示されます。
内容プレビューのボタンを押すと中身をみる事ができます。
これでなんのファイルが入っているかがわかります。
ダウンロードをクリックすると
ダウンロードがインポートに変わると思います。
インポートをクリックすると
中身をプロジェクトに入れる選択をする事ができます。チェックが入っているものがプロジェクトに入ります。右下のImportを押すとプロジェクトにインポートされます。
これでBGMになりそうなものとSEになりそうなものをダウンロードしてきてください。
<BGMを設定しよう>
さて、素材をダウンロードしてきたところでBGMを設定していきましょう。
BGMの設定は簡単です。
どのシーンでもいいのでカラのゲームオブジェクトを作成し、Add ComponentでAudio Sourceと検索してください。
これをくっつけると↓のような感じになります。
これのPlay On AwakeとLoopにチェックを入れます。
Play On Awakeというのはインスタンス化された瞬間から再生をするという意味になります。
Loopは読んで字のごとく曲をループさせます。
その後、AudioClipと書いてあるところの◎をクリックすると
今プロジェクトにあるオーディオファイルを選択する事ができます。
そのシーンで再生したい曲を選べばBGMとして再生する事ができるようになります。
これを各シーンで同じことをすればBGMの設定完了です。
簡単でしたね。
音の大きさはVolumeのところをいじってもらえればと思います。
<SEを設定しよう>
SEを再生するにはちょっと工夫が必要です。
BGMで設置したAudioSourceは要するに音源です。
SEは色々な実装方法があるのですが、今回は音が何個も重なって鳴るのが困るので2つ目の音が鳴った時、1つ目の音を上書きするようにします。
要はSEの音源を1つにします。
複数設置するような設計にしても構いません。そこは音をどう扱うのか皆さん次第ですね
音源を1つにするならばゲームマネージャーをくっつけてあるゲームオブジェクトにAudio Sourceをくっつけましょう。
Audio SourceのPlay On Awakeを外しましょう。
AudioClipはつけなくてもOKです。
ゲームマネージャーのプレハブの変更を全部のシーンでするのはめんどくさいのでApply Allを押してOverrideしましょう。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GManager : MonoBehaviour
{
public static GManager instance = null;
[Header("スコア")] public int score;
[Header("現在のステージ")] public int stageNum;
[Header("現在の復帰位置")] public int continueNum;
[Header("現在の残機")] public int heartNum;
[Header("デフォルトの残機")] public int defaultHeartNum;
[HideInInspector] public bool isGameOver = false;
private AudioSource audioSource = null;
private void Awake()
{
if (instance == null)
{
instance = this;
DontDestroyOnLoad(this.gameObject);
}
else
{
Destroy(this.gameObject);
}
}
private void Start()
{
audioSource = GetComponent<AudioSource>();
}
/// <summary>
/// 残機を1つ増やす
/// </summary>
public void AddHeartNum()
{
if (heartNum < 99)
{
++heartNum;
}
}
/// <summary>
/// 残機を1つ減らす
/// </summary>
public void SubHeartNum()
{
if (heartNum > 0)
{
--heartNum;
}
else
{
isGameOver = true;
}
}
/// <summary>
/// 最初から始める時の処理
/// </summary>
public void RetryGame()
{
isGameOver = false;
heartNum = defaultHeartNum;
score = 0;
stageNum = 1;
continueNum = 0;
}
/// <summary>
/// SEを鳴らす
/// </summary>
public void PlaySE(AudioClip clip)
{
if (audioSource != null)
{
audioSource.PlayOneShot(clip);
}
else
{
Debug.Log("オーディオソースが設定されていません");
}
}
}
とりあえず、くっつけたAudioSourceのインスタンスを捕まえます。
private AudioSource audioSource = null; private void Start() { audioSource = GetComponent<AudioSource>(); }
AudioSourceから音を鳴らすのをスクリプトで行うには↓のようにします。
/// <summary>
/// SEを鳴らす
/// </summary>
public void PlaySE(AudioClip clip)
{
if (audioSource != null)
{
audioSource.PlayOneShot(clip);
}
else
{
Debug.Log("オーディオソースが設定されていません");
}
}
AudioClipはAudioSourceのインスペクターで設定したやつです。
AudioSourceは音源でAudioClipがSEのファイルになります。
PlayOneShotは1回だけ音を再生します。
後は音を鳴らしたいタイミングでこのメソッドを呼べばOKです。
これで、ゲームマネージャーを通してどこからでも音を鳴らす事が可能となったので、今まで作成してきたスクリプトに音の処理を追加します。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player : MonoBehaviour
{
#region//インスペクターで設定する
[Header("移動速度")] public float speed;
[Header("重力")] public float gravity;
[Header("ジャンプ速度")] public float jumpSpeed;
[Header("ジャンプする高さ")] public float jumpHeight;
[Header("ジャンプする長さ")] public float jumpLimitTime;
[Header("接地判定")] public GroundCheck ground;
[Header("天井判定")] public GroundCheck head;
[Header("ダッシュの速さ表現")] public AnimationCurve dashCurve;
[Header("ジャンプの速さ表現")] public AnimationCurve jumpCurve;
[Header("踏みつけ判定の高さの割合(%)")] public float stepOnRate;
[Header("ジャンプする時に鳴らすSE")] public AudioClip jumpSE; //New!
[Header("やられた鳴らすSE")] public AudioClip downSE; //New
[Header("コンティニュー時に鳴らすSE")] public AudioClip continueSE; //New
#endregion
#region//プライベート変数
private Animator anim = null;
private Rigidbody2D rb = null;
private CapsuleCollider2D capcol = null;
private SpriteRenderer sr = null;
private bool isGround = false;
private bool isJump = false;
private bool isHead = false;
private bool isRun = false;
private bool isDown = false;
private bool isOtherJump = false;
private bool isContinue = false;
private bool nonDownAnim = false;
private float jumpPos = 0.0f;
private float otherJumpHeight = 0.0f;
private float dashTime = 0.0f;
private float jumpTime = 0.0f;
private float beforeKey = 0.0f;
private float continueTime = 0.0f;
private float blinkTime = 0.0f;
private string enemyTag = "Enemy";
private string deadAreaTag = "DeadArea";
private string hitAreaTag = "HitArea";
#endregion
void Start()
{
//コンポーネントのインスタンスを捕まえる
anim = GetComponent<Animator>();
rb = GetComponent<Rigidbody2D>();
capcol = GetComponent<CapsuleCollider2D>();
sr = GetComponent<SpriteRenderer>();
}
private void Update()
{
if (isContinue)
{
//明滅 ついている時に戻る
if (blinkTime > 0.2f)
{
sr.enabled = true;
blinkTime = 0.0f;
}
//明滅 消えているとき
else if (blinkTime > 0.1f)
{
sr.enabled = false;
}
//明滅 ついているとき
else
{
sr.enabled = true;
}
//1秒たったら明滅終わり
if (continueTime > 1.0f)
{
isContinue = false;
blinkTime = 0.0f;
continueTime = 0.0f;
sr.enabled = true;
}
else
{
blinkTime += Time.deltaTime;
continueTime += Time.deltaTime;
}
}
}
void FixedUpdate()
{
if (!isDown && !GManager.instance.isGameOver)
{
//接地判定を得る
isGround = ground.IsGround();
isHead = head.IsGround();
//各種座標軸の速度を求める
float xSpeed = GetXSpeed();
float ySpeed = GetYSpeed();
//アニメーションを適用
SetAnimation();
//移動速度を設定
rb.velocity = new Vector2(xSpeed, ySpeed);
}
else
{
rb.velocity = new Vector2(0, -gravity);
}
}
/// <summary>
/// Y成分で必要な計算をし、速度を返す。
/// </summary>
/// <returns>Y軸の速さ</returns>
private float GetYSpeed()
{
float verticalKey = Input.GetAxis("Vertical");
float ySpeed = -gravity;
//何かを踏んだ際のジャンプ
if (isOtherJump)
{
//現在の高さが飛べる高さより下か
bool canHeight = jumpPos + otherJumpHeight > transform.position.y;
//ジャンプ時間が長くなりすぎてないか
bool canTime = jumpLimitTime > jumpTime;
if (canHeight && canTime && !isHead)
{
ySpeed = jumpSpeed;
jumpTime += Time.deltaTime;
}
else
{
isOtherJump = false;
jumpTime = 0.0f;
}
}
//地面にいるとき
else if (isGround)
{
if (verticalKey > 0)
{
if(!isJump)
{
GManager.instance.PlaySE(jumpSE); //New!
}
ySpeed = jumpSpeed;
jumpPos = transform.position.y; //ジャンプした位置を記録する
isJump = true;
jumpTime = 0.0f;
}
else
{
isJump = false;
}
}
//ジャンプ中
else if (isJump)
{
//上方向キーを押しているか
bool pushUpKey = verticalKey > 0;
//現在の高さが飛べる高さより下か
bool canHeight = jumpPos + jumpHeight > transform.position.y;
//ジャンプ時間が長くなりすぎてないか
bool canTime = jumpLimitTime > jumpTime;
if (pushUpKey && canHeight && canTime && !isHead)
{
ySpeed = jumpSpeed;
jumpTime += Time.deltaTime;
}
else
{
isJump = false;
jumpTime = 0.0f;
}
}
if (isJump || isOtherJump)
{
ySpeed *= jumpCurve.Evaluate(jumpTime);
}
return ySpeed;
}
/// <summary>
/// X成分で必要な計算をし、速度を返す。
/// </summary>
/// <returns>X軸の速さ</returns>
private float GetXSpeed()
{
float horizontalKey = Input.GetAxis("Horizontal");
float xSpeed = 0.0f;
if (horizontalKey > 0)
{
transform.localScale = new Vector3(1, 1, 1);
isRun = true;
dashTime += Time.deltaTime;
xSpeed = speed;
}
else if (horizontalKey < 0)
{
transform.localScale = new Vector3(-1, 1, 1);
isRun = true;
dashTime += Time.deltaTime;
xSpeed = -speed;
}
else
{
isRun = false;
xSpeed = 0.0f;
dashTime = 0.0f;
}
//前回の入力からダッシュの反転を判断して速度を変える
if (horizontalKey > 0 && beforeKey < 0)
{
dashTime = 0.0f;
}
else if (horizontalKey < 0 && beforeKey > 0)
{
dashTime = 0.0f;
}
xSpeed *= dashCurve.Evaluate(dashTime);
beforeKey = horizontalKey;
return xSpeed;
}
/// <summary>
/// アニメーションを設定する
/// </summary>
private void SetAnimation()
{
anim.SetBool("jump", isJump || isOtherJump);
anim.SetBool("ground", isGround);
anim.SetBool("run", isRun);
}
/// <summary>
/// コンティニュー待機状態か
/// </summary>
/// <returns></returns>
public bool IsContinueWaiting()
{
if (GManager.instance.isGameOver)
{
return false;
}
else
{
return IsDownAnimEnd() || nonDownAnim;
}
}
//ダウンアニメーションが完了しているかどうか
private bool IsDownAnimEnd()
{
if (isDown && anim != null)
{
AnimatorStateInfo currentState = anim.GetCurrentAnimatorStateInfo(0);
if (currentState.IsName("player_down"))
{
if (currentState.normalizedTime >= 1)
{
return true;
}
}
}
return false;
}
/// <summary>
/// コンティニューする
/// </summary>
public void ContinuePlayer()
{
GManager.instance.PlaySE(continueSE); //New!
isDown = false;
anim.Play("player_stand");
isJump = false;
isOtherJump = false;
isRun = false;
isContinue = true;
nonDownAnim = false;
}
//やられた時の処理
private void ReceiveDamage(bool downAnim)
{
if (isDown)
{
return;
}
else
{
if (downAnim)
{
anim.Play("player_down");
}
else
{
nonDownAnim = true;
}
isDown = true;
GManager.instance.PlaySE(downSE); //New!
GManager.instance.SubHeartNum();
}
}
#region//接触判定
private void OnCollisionEnter2D(Collision2D collision)
{
if (collision.collider.tag == enemyTag)
{
//踏みつけ判定になる高さ
float stepOnHeight = (capcol.size.y * (stepOnRate / 100f));
//踏みつけ判定のワールド座標
float judgePos = transform.position.y - (capcol.size.y / 2f) + stepOnHeight;
foreach (ContactPoint2D p in collision.contacts)
{
if (p.point.y < judgePos)
{
ObjectCollision o = collision.gameObject.GetComponent<ObjectCollision>();
if (o != null)
{
otherJumpHeight = o.boundHeight; //踏んづけたものから跳ねる高さを取得する
o.playerStepOn = true; //踏んづけたものに対して踏んづけた事を通知する
jumpPos = transform.position.y; //ジャンプした位置を記録する
isOtherJump = true;
isJump = false;
jumpTime = 0.0f;
}
else
{
Debug.Log("ObjectCollisionが付いてないよ!");
}
}
else
{
ReceiveDamage(true);
break;
}
}
}
}
private void OnTriggerEnter2D(Collider2D collision)
{
if(collision.tag == deadAreaTag)
{
ReceiveDamage(false);
}
else if(collision.tag == hitAreaTag)
{
ReceiveDamage(true);
}
}
#endregion
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ScoreItem : MonoBehaviour
{
[Header("加算するスコア")] public int myScore;
[Header("プレイヤーの判定")] public PlayerTriggerCheck playerCheck;
[Header("アイテム取得時に鳴らすSE")] public AudioClip itemSE;
// Update is called once per frame
void Update()
{
//プレイヤーが判定内に入ったら
if (playerCheck.isOn)
{
if(GManager.instance != null)
{
GManager.instance.score += myScore;
GManager.instance.PlaySE(itemSE);
Destroy(this.gameObject);
}
}
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class StageCtrl : MonoBehaviour
{
[Header("プレイヤーゲームオブジェクト")] public GameObject playerObj;
[Header("コンティニュー位置")] public GameObject[] continuePoint;
[Header("ゲームオーバー")] public GameObject gameOverObj;
[Header("フェード")] public FadeImage fade;
[Header("ゲームオーバー時に鳴らすSE")] public AudioClip gameOverSE; //New!
[Header("リトライ時に鳴らすSE")] public AudioClip retrySE; //New!
private Player p;
private int nextStageNum;
private bool startFade = false;
private bool doGameOver = false;
private bool retryGame = false;
private bool doSceneChange = false;
// Start is called before the first frame update
void Start()
{
if (playerObj != null && continuePoint != null && continuePoint.Length > 0 && gameOverObj != null && fade != null)
{
gameOverObj.SetActive(false);
playerObj.transform.position = continuePoint[0].transform.position;
p = playerObj.GetComponent<Player>();
if (p == null)
{
Debug.Log("プレイヤーじゃない物がアタッチされているよ!");
}
}
else
{
Debug.Log("設定が足りてないよ!");
}
}
// Update is called once per frame
void Update()
{
//ゲームオーバー時の処理
if (GManager.instance.isGameOver && !doGameOver)
{
gameOverObj.SetActive(true);
GManager.instance.PlaySE(gameOverSE); //New!
doGameOver = true;
}
//プレイヤーがやられた時の処理
else if (p != null && p.IsContinueWaiting() && !doGameOver)
{
if (continuePoint.Length > GManager.instance.continueNum)
{
playerObj.transform.position = continuePoint[GManager.instance.continueNum].transform.position;
p.ContinuePlayer();
}
else
{
Debug.Log("コンティニューポイントの設定が足りてないよ!");
}
}
//ステージを切り替える
if (fade != null && startFade && !doSceneChange)
{
if (fade.IsFadeOutComplete())
{
//ゲームリトライ
if (retryGame)
{
GManager.instance.RetryGame();
}
//次のステージ
else
{
GManager.instance.stageNum = nextStageNum;
}
SceneManager.LoadScene("stage" + nextStageNum);
doSceneChange = true;
}
}
}
/// <summary>
/// 最初から始める
/// </summary>
public void Retry()
{
GManager.instance.PlaySE(retrySE); //New!
ChangeScene(1); //最初のステージに戻るので1
retryGame = true;
}
/// <summary>
/// ステージを切り替えます。
/// </summary>
/// <param name="num">ステージ番号</param>
public void ChangeScene(int num)
{
if (fade != null)
{
nextStageNum = num;
fade.StartFadeOut();
startFade = true;
}
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine.SceneManagement;
using UnityEngine;
public class Title : MonoBehaviour
{
[Header("フェード")] public FadeImage fade;
[Header("ゲームスタート時に鳴らすSE")] public AudioClip startSE;
private bool firstPush = false;
private bool goNextScene = false;
/// <summary>
/// スタートボタンを押されたら呼ばれる
/// </summary>
public void PressStart()
{
Debug.Log("Press Start!");
if (!firstPush)
{
GManager.instance.PlaySE(startSE);
fade.StartFadeOut();
firstPush = true;
}
}
void Update()
{
if (!goNextScene && fade.IsFadeOutComplete())
{
SceneManager.LoadScene("stage1");
goNextScene = true;
}
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Enemy_Zako1 : MonoBehaviour
{
#region//インスペクターで設定する
[Header("加算スコア")] public int myScore;
[Header("移動速度")] public float speed;
[Header("重力")] public float gravity;
[Header("画面外でも行動する")] public bool nonVisibleAct;
[Header("接触判定")] public EnemyCollisionCheck checkCollision;
[Header("やられた時に鳴らすSE")] public AudioClip deadSE;
#endregion
#region//プライベート変数
private Rigidbody2D rb = null;
private SpriteRenderer sr = null;
private Animator anim = null;
private ObjectCollision oc = null;
private BoxCollider2D col = null;
private bool rightTleftF = false;
private bool isDead = false;
#endregion
// Start is called before the first frame update
void Start()
{
rb = GetComponent<Rigidbody2D>();
sr = GetComponent<SpriteRenderer>();
anim = GetComponent<Animator>();
oc = GetComponent<ObjectCollision>();
col = GetComponent<BoxCollider2D>();
}
void FixedUpdate()
{
if (!oc.playerStepOn)
{
if (sr.isVisible || nonVisibleAct)
{
if (checkCollision.isOn)
{
rightTleftF = !rightTleftF;
}
int xVector = -1;
if (rightTleftF)
{
xVector = 1;
transform.localScale = new Vector3(-1, 1, 1);
}
else
{
transform.localScale = new Vector3(1, 1, 1);
}
rb.velocity = new Vector2(xVector * speed, -gravity);
}
else
{
rb.Sleep();
}
}
else
{
if (!isDead)
{
anim.Play("dead");
rb.velocity = new Vector2(0, -gravity);
isDead = true;
col.enabled = false;
if (GManager.instance != null)
{
GManager.instance.PlaySE(deadSE);
GManager.instance.score += myScore;
}
Destroy(gameObject, 3f);
}
else
{
transform.Rotate(new Vector3(0, 0, 5));
}
}
}
}
各種スクリプトに音を鳴らす処理を追加しました。どれも音を鳴らしたいタイミングで
GManager.instance.PlaySE(オーディオクリップ);
としているだけです。これで音を鳴らすことができます。
プレイヤーのスクリプトの部分だけ、ジャンプした時、1フレームで接地判定から抜けるかどうかわからないので、2重に鳴らないようにしています
if (!isJump)
{
GManager.instance.PlaySE(jumpSE); //New!
}
後は各種インスペクターで鳴らしたいSEを設定すればOKです。
音も今回、仮ですので、とりあえず鳴ることがテストできたらOKかなと思います。
何かうまくいかない事があった場合は↓の記事を参考にしてみてください
最低限↓の動画の要件を満たしていない質問は受けかねるので、ご理解ください。
また、筆者も間違えることはありますので、何か間違っている点などありましたら、動画コメント欄にでも書いていただけるとありがたいです。