今回の記事はUnityで3Dエンドレスランゲームを制作する講座の第4回目です。
前回は3Dランで走るフィールドの作成、プレイヤーキャラクターの移動・ジャンプ処理や障害物との当たり判定の設定を行いました。
前回の記事:
今回はまず音楽と効果音を簡易的に設定し、地形の無限生成スクリプトの実装、そして得点スコアシステムを開発します。今回で3D障害物無限ランゲームのゲームシステムが一通り完成します。
3Dランゲームに音楽を付けてみよう
最初にダウンロードしておいた音楽系のアセットを使ってBGMを付けてみましょう。
まず、Hierarchyに空のオブジェクトを作り、名前を “LevelControls”とします。
Inspector画面で、Add Component → Audio Sourceをクリックします。
プロジェクトフォルダからオーディオファイルを探し、Level Controlsを再度選択します。
Inspector画面のAudioSourceコンポ手のAudioClipフィールドにオーディオファイルをドラッグ&ドロップします。
Optionの「Play on awake」と「Loop」が有効になっていることを確認します。
これでゲーム開始と同時に指定したAudioClipのBGMが流れます。
このコンポーネントでは、その他のオプションを使用して音量などを自由にカスタマイズできます。
Unityの音響設定や応用についてより詳しく知りたい方は関連記事を参照してみてください。
関連記事:【Unity入門】汎用サウンドマネージャー(Sound Manager)の作り方 前編
3Dランゲームの進行を管理するゲームマネージャーを作成
Unityではそのゲームシステムの一般的な機能をコントロールするオブジェクトを設置することができます。このオブジェクトは”ゲームマネージャー”と呼ばれます。
シーンに新しい空のオブジェクトを作り、GameManagerと名付けます。
今回の障害物3Dランゲームではプレイヤーキャラクターは障害物を避けながらアイテムを収集し、収集したアイテムの数をゲームスコアとします。
ゲームマネージャーはゲームの進行を管理するオブジェクトのため、今回はゲームのスコアシステムとアイテム収集操作を処理します。
新しいScriptsフォルダで新しいC#スクリプトを作成し、名前をGameManagerとしましょう。
作成したスクリプトはGameManagerオブジェクトのインスペクタにドラッグ&ドロップします。
スクリプトのアイコンが歯車になっていることに気付きましたか?UnityではこのGameManagerと名付けたスクリプトを他のスクリプトと区別して簡単に見つけられるようにしています。
アイテム収集システムの実装
3Dランゲームの収集用アイテムを作成していきましょう。
インポートした環境アセットからお好みのプレハブを選びます。サンプルゲームでは青い花のプレハブを選んでみました。
このプレハブをScene画面にドラッグ&ドロップします。
続いて、プロジェクトフォルダ「Scripts」で新規スクリプトを作成 → 右クリック → 作成 → C#スクリプトを新規作成します。
スクリプト名はCollectableなど特徴的な名前をつけ、作ったスクリプトを開きましょう。
スクリプトの中身を以下の通りにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Collectable : MonoBehaviour { public int rotateSpeed = 1; // Start is called before the first frame update void Start() { } // Update is called once per frame void Update() { transform.Rotate(0, rotateSpeed, 0, Space.World); } void OnTriggerEnter(Collider other) { if (other.gameObject.CompareTag("Player")) { Destroy(gameObject); } } } |
スクリプトの解説を行っていきます。
まず最初に新しい変数を一つ宣言します。アイテムオブジェクトの回転速度になります。ゲームの見栄えをよくするために使います。
1 |
public int rotateSpeed = 1; |
そしてUpdate() の下に新しいメソッドを作成しました。
1 2 3 4 5 6 7 |
void OnTriggerEnter(Collider other) { if (other.gameObject.CompareTag("Player")) { Destroy(gameObject); } } |
Playerがこのオブジェクトに衝突するとそのオブジェクトは破壊されます。
そしてUpdate()メソッドに以下の処理を追加します。
1 2 3 4 |
void Update() { transform.Rotate(0, rotateSpeed, 0, Space.World); } |
毎フレームこのスクリプトがアタッチされたオブジェクトは「rotateSpeed」の回転速度で、Y軸まわりに回転します(Unityのワールド座標空間ではY軸が鉛直上向き方向を、Z軸が奥行き方向を表します)。
Ctrl+Sでスクリプトを保存し、Unityエディタに戻ります。
Collectable.csを選択し、先ほど収集アイテム用に作成したオブジェクトのインスペクタにスクリプトをドラッグ&ドロップします。
次に、当たり判定を設定するためにメッシュコライダーをこのオブジェクトに追加します。
衝突が検出されるように、コライダーのConvexパラメータとIsTriggerパラメータを有効にしてください。
このようにアセットにメッシュがない場合は、コライダーを追加して、オブジェクトと同じような形に当たり判定となるメッシュ領域のサイズを調整します(球、カプセル、ボックスコライダーなどいくつかの選択肢があります)。
また、Characterオブジェクトのタグを “Player “に変更します。当たり判定に用いるのでこの操作も忘れずに行ってください。Characterプレハブを選択し、Inspectorの上のTagリストを選択し、”Player “タグを選択します。
これでこのアイテムにプレイヤーが触れたらこのアイテムが消えるようになりました。
最後に、Collectable.csを付けたオブジェクトの名前をCollectableに変更し、Prefabsフォルダにドラッグ&ドロップします。Original Prefabとして保存してOKです。
プレハブにしたことでこのアイテムを再利用できるようになりました。後ほどこのアイテムを別のスクリプトから生成して使います。
アイテム取得時の効果音の設定
ここでアイテム取得時の効果音を設定します。
BGMの設定箇所で行ったようにAudio SourceをCollectableプレハブに追加してアイテム取得時に効果音を鳴らしたいところです。しかし、Trigger関数でCollectableをDestroyしてしまうのでCollectableオブジェクトでは効果音を再生できません。
代わりに、Audio Sourceをシーン内で消えずに存在しているオブジェクトであるゲームマネージャーに追加します。
アイテム取得時にプレイヤーが獲得するポイントの処理もこのタイミングで行います。スコア処理は後ほど実装します。
まずGameManagerオブジェクトを選択し、Add Component → Audio Sourceを選択します。
アイテム取得時に鳴らしたい効果音をアセットから選択し、GameManagerのインスペクタのAudio Clipフィールドにドラッグ&ドロップします。
そして「Play on awake」オプションをオフにします(Play on awakeがオンだとゲーム開始時にこの効果音が鳴ってしまいます)。
お好みで他のオプションで音源の音量などを自由にカスタマイズしてOKです。
Collectable.csスクリプトをもう一度開き、以下のように修正します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Collectable : MonoBehaviour { public int rotateSpeed = 1; public GameManager gameManager; void Start() { gameManager = GameObject.Find("GameManager").GetComponent<GameManager>(); } // Update is called once per frame void Update() { transform.Rotate(0, rotateSpeed, 0, Space.World); } void OnTriggerEnter(Collider other) { if (other.gameObject.CompareTag("Player")) { Destroy(gameObject); } } } |
この変更により、Collectableオブジェクトは起動すると同時にGameManagerオブジェクトを見つけ、GameManagerコンポーネントを変数gameManagerに代入するようになりました。
上記の変更を加えたらCtrl+Sでスクリプトを保存し、”Game Manager “スクリプトを開きましょう。
GameManagerスクリプトでは効果音のための変数を宣言し、サウンド再生(とのちのスコア計算)を行うメソッドを一つ作成します。また、Startメソッドで、GameManagerオブジェクトにアタッチしたAudioSource(効果音の音源)を取得します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
using System.Collections; using System.Collections.Generic; using UnityEngine; public class GameManager : MonoBehaviour { public AudioSource audioSource; void Start() { audioSource = this.GetComponent<AudioSource>(); } public void UpdateScore() { audioSource.Play(); } } |
変更を加えたらCtrl+Sでスクリプトを保存し、もう一度”Collectable “スクリプトを開きます。
Collectableを破棄する前に、UpdateScore関数を呼び出すようにします。これで効果音が鳴ってからCollectableオブジェクトが消えるようになりました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Collectable : MonoBehaviour { public int rotateSpeed = 1; public GameManager gameManager; void Start() { gameManager = GameObject.Find("GameManager").GetComponent<GameManager>(); } // Update is called once per frame void Update() { transform.Rotate(0, rotateSpeed, 0, Space.World); } void OnTriggerEnter(Collider other) { if (other.gameObject.CompareTag("Player")) { gameManager.UpdateScore(); Destroy(gameObject); } } } |
Ctrl+Sでスクリプトを保存し、Unityエディターに戻ります。
Collectableオブジェクトのプレハブをいくつか真ん中の島の子オブジェクトに追加します。Ctrl+Dを押すことで、Hierarchy内のオブジェクトを複製することもできます。
複製したCollectableプレハブはプレイヤーが移動可能な前方領域であれば好きに配置してOKです。
Ctrl+Sで定期的にシーンを保存しましょう。
そしてUnityエディタで再生ボタンを押して一度ゲームをプレイしてテストしてみてください。
(↑早送りのgif動画のため音は出ませんが実装の一例です)
プレイヤーキャラクターがアイテムを取得したら効果音が鳴ってアイテムが消えるようになりましたか?
Skyboxを変更して背景色を変えてみよう
SkyboxとはUnityの空の色を設定できる機能です。開発中はいつでも背景のSkyBoxを追加・変更できます。
インポートしたSkyboxから、好きなマテリアルをシーン内の空きスペースにドラッグ&ドロップしてください。
Customizable Skybox > Stylized Sky > Materials > Night1を選択してみた結果が以下の図です。
また、メニューのWindow → Rendering → Lighting → EnvironmentタブSkybox Materialからでもスカイボックスを変更できます。
ランダムに地形を生成して無限ランゲームを実装する
いよいよ障害物3Dランゲームを無限ランゲームに変更していきます。
Hierarchyで空のオブジェクトを作成し、Level という名前を付けます。そのTransformをリセットします。
すべての島を選択し、「Level」オブジェクト内にドラッグ&ドロップします。
Levelを選択し、Ctrl+Dを3回押してコピーペーストします。
Levelの名前を “StartingLevel”に変更し、Prefabsフォルダにドラッグしてプレハブにします。
複製したLevelsをZ軸上で移動させていきましょう。それぞれのLevelsが終了した後に繋がるように配置します。
サンプルゲームではStartingLevelオブジェクトのTransform PositionのZを0、Level(1)のZを12、Level(2)のZを24、Level(3)のZを36に設定してください。以下の画像はその一例です。
ステージ構成を凝りたい場合は先ほどプレハブにしたLevel(1)、Level(2)、Level(3)の構造をそれぞれ変更してみると良いでしょう。
配置したら各Levelの中にあるIslandのアセットに配置した障害物や花のアイテムの位置を好きなようにカスタマイズしてください。
特に、実際にランゲームで走る地域になる真ん中の島のオブジェクト配置は変化を入れておきましょう。そして障害物となるオブジェクトにはすべてコライダーをつけましょう。
花のアイテムはプレイヤーが取りやすい場所、あるいは取りにくい場所に配置していきます。このアイテム配置がゲームコースになるので、プレイして面白いと感じる配置になると良いですね。
また、よりゲームの臨場感を高めるためにカメラの位置を変更してみるのも良いでしょう。
次に、ランゲームが進行していくとLevelオブジェクトをランダムに生成するようにしていきます。
Scripts フォルダに新しい C# スクリプトを作成し、”GenerateLevels” という名前を付けます。
Hierarchyで LevelControls オブジェクトを選択し、今作ったC#スクリプトをインスペクター画面にドラッグ&ドロップします。
スクリプトを開き、いくつかの文を追加します:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
using System.Collections; using System.Collections.Generic; using UnityEngine; public class GenerateLevels : MonoBehaviour { public GameObject[] level; public int zPos = 12; public bool creatingLevel = false; public int lvlNum; // Start is called before the first frame update void Start() { } // Update is called once per frame void Update() { if (!creatingLevel) { creatingLevel = true; StartCoroutine(GenerateLvl()); } } IEnumerator GenerateLvl() { lvlNum = Random.Range(0, 4); // 0, 1, 2, 3 Instantiate(level[lvlNum], new Vector3(0, 0, zPos), Quaternion.identity); zPos += 12; yield return new WaitForSeconds(3); creatingLevel = false; } } |
最初に4つの変数を追加します。
1 2 3 4 |
public GameObject[] level; public int zPos = 12; public bool creatingLevel = false; public int lvlNum; |
Levelは、Levelオブジェクトを格納するための配列です。インスペクタ画面で後ほど入力します。
zPosは生成するLevelオブジェクトのZ軸の位置です。
creatingLevelは、常にレベルを生成しないようにするためのものです。
lvlNumは、生成するLevelオブジェクトの番号を表します。
Update() の下に IEnumeratorから始まるコルーチンのメソッドを記述しています:
1 2 3 4 5 6 7 8 |
IEnumerator GenerateLvl() { lvlNum = Random.Range(0, 3); Instantiate(level[lvlNum], new Vector3(0, 0, zPos), Quaternion.identity); zPos += 12; yield return new WaitForSeconds(3); creatingLevel = false; } |
このコルーチンが呼び出されると3秒ごとにlevel配列からランダムな数字のLevelオブジェクトが生成されます。
Update() メソッドからこのコルーチンを呼び出します:
1 2 3 4 5 6 7 8 |
void Update() { if (!creatingLevel) { creatingLevel = true; StartCoroutine(GenerateLvl()); } } |
Ctrl+Sでスクリプトを保存し、Unityエディターに戻ります。
インスペクタ画面のGenerate Levelコンポーネントに先ほど宣言したpublic変数が表示されているはずです。
Levelの配列を展開し、+を4回押して4つの項目を追加します。
まずはStarting LevelプレハブをElement 0に設定します。次にStartingLevelを除く3つのLevelオブジェクトを選択し順番にElement 1~3にドラッグ&ドロップします。
ヒエラルキーではなく、PrefabsフォルダからStarting Levelを追加してください。
Unityエディターで再生ボタンを押して修正した部分の動作テストをしてください。キャラクターが走る先に新しい地形がランダムに生成されたでしょうか?
(↑ゲーム進行とともにScene画面に地形が自動生成されていることがわかります)
適宜キャラクターのスピードやLevelオブジェクトを生成するタイミングを変更してください。
プレイヤーの背後にあるオブジェクトを消去して動作を軽くする
さて、ここまででおわかりのようにこのゲームをプレイするとキャラクターが走り去った後方には役に立たないグラフィック・オブジェクトがたくさん出しっぱなしになりますね。
これはゲームの実行速度を遅くしてしまう処理落ちの原因になります。
プレイヤーの後ろに「掃き出し」の壁を追加することでこの問題を解決していきます。
ヒエラルキー画面で3DのCubeオブジェクトを作成し、Characterオブジェクトの子オブジェクトになるようにドラッグ&ドロップします。
名前を「Sweeper」とし、Transformの数値をリセットします。
プレイヤーの後ろにSweeperを移動させ、大きな薄い壁になるようにサイズを調整します。
そしてコライダーだけが必要なので、MeshRendererを右クリックしてコンポーネントを削除する。
Box Colliderの Is Trigger チェックボックスを有効にします。
Scriptsフォルダに新しいC#スクリプトを作成し、名前を「Sweeper」として開いてください。
Start()メソッドとUpdate()メソッドを削除し、新しいメソッドを書いていきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Sweeper : MonoBehaviour { public void OnTriggerEnter(Collider collision) { if (collision.gameObject.CompareTag("Level")) { Destroy(collision.gameObject); } } } |
ここで追加したOnTriggerEnterメソッドは、”Level “タグを持つものとの衝突を検出します。
Ctrl+Sでスクリプトを保存し、Unityエディターに戻ります。
そしてSweeperオブジェクトのインスペクタにこのスクリプトをドラッグ&ドロップします。
Characterオブジェクトを選択し、プレハブに変更を上書き反映させます。
また、ここからは各Levelオブジェクトにコライダを追加して、”Level “タグを作成して割り当てを行いSweeperオブジェクトとの当たり判定を実装します。
Starting Levelを選択して、インスペクタでAdd Component → Box Colliderを選択します。
「Edit Collider」ボタンを押して、Starting Levelオブジェクトを緑色の四角で囲みます。
地面の表面と相互作用するように、高すぎない位置になってることを確認してください。
このコンポーネントをコピーし、新しいコンポーネントとして他のLevelオブジェクトにも貼り付けます。
また、StartingLevelを選択し、インスペクタの一番上にあるTagパラメータを見つけます。
それをクリックすると、ゲームに存在するすべてのタグが表示されます。Add Tagをクリックします。
新しい項目を追加し、Levelと名付けて保存します。
次に、ヒエラルキー画面内の全てのLevelオブジェクトを選択し、そのタグを「Level」に変更する。
Starting Levelプレハブへの変更をOverrideで上書きします。
Unityエディターでゲームをプレイして変更を確認してみてください。
(↑Levelタグが付いたオブジェクトはSweeperオブジェクトに触れると消滅していることがわかります)
Ctrl+Sで定期的にシーンを保存することを忘れないでください。
追記:バグの改善 GenerateLevelsのLevelオブジェクトの消滅を防ぐ
さて、実際にここでSweeperのテストプレイを長時間進めていくと一つのバグが発生することに気付いたでしょうか?
実は、LevelControlsオブジェクトに配置されているLevelリストに登録されている4つのうち、StartingLevel以外の3つは今はプレハブではなく、ヒエラルキー上に存在するゲームオブジェクトです。
そのため、長時間テストプレイすると必ずSweeperの効果でLevel(1)~(3)のオブジェクトはDestroyされリストから消えてしまいます。ゲームオブジェクトが消えるのでLevelリストに登録されている参照先もなくなってしまいます。
この状態でゲームを続けるとGenerateLevelsスクリプトにより生成する地形がなくなってしまい、いずれプレイヤーが走れるフィールドが生成されなくなります(StartingLevelオブジェクトだけは生成されます)。
このバグを除去するためにLevel(1)、Level(2)、Level(3)も全てプレハブにして同様にPrefabsフォルダに保存しましょう。
その後、GenerateLevelsコンポーネントに改めてプレハブを登録しなおしてください。
さて、再びテストプレイしてみましょう。
これで本当に長時間プレイが可能な無限ラン部分のゲームシステムが仕上がりました。
Canvas UIによる得点システムの実装
ここからはプレイヤーが花のアイテムを取得した数を画面に表示させるUIを作ります。得点を表示するスコアシステムですね。
そのためにHierarchy画面で右クリック > UI > Canvasを作成します。
名前は変更してもいいですがここではCanvasという名前のままにしておきます。
ゲームプレイ時の画面サイズ調整(解像度の違い)に簡易的に対応するために、UI Scale ModeをScale With Screen Sizeに変更し、合わせて画面サイズとして想定する値を設定するのがベストです。
(なお、より汎用的な解像度調節のやり方に関しては「Unityの画面のアスペクト比と解像度を自動変換 全スマホ・複数解像度に対応させる」の記事を参照してください)
CanvasができたらHierarchy上でCanvasオブジェクトを右クリックし、テキストボックスを追加します。
TMP(Text Mesh Pro)を使用するためのプロンプトが表示されます。「Import TPM Essentials」を選択し必要なものをインポートし、ポップアップを閉じます。
このテキストボックスを左右の隅に移動し、Canvasとの相対位置を固定します。数値は以下の画像の通り。
続いてText Mesh Proコンポーネントの設定を行います。
入力欄には “Score: 0 “と記入します。追加のフォントをインポートした場合はFont Assetを変更し、より立体的な表示にしたい場合はMaterial Presetを変更します。
画面解像度に応じてテキストサイズを変更するためAuto Sizeにチェックを入れ、お好みで色を変更します。
そして、折りたたまれないようにWrappingをDisabled(無効)にします。
次に、”GameManager “スクリプトを開き、以下のように内容を修正します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
using System.Collections; using System.Collections.Generic; using UnityEngine; using TMPro; public class GameManager : MonoBehaviour { public AudioSource audioSource; public int score; public TextMeshProUGUI scoreText; void Start() { audioSource = this.GetComponent<AudioSource>(); } public void UpdateScore() { audioSource.Play(); score++; scoreText.text = "Score: " + score; } } |
まずはTextMeshProの名前空間を他の名前空間の下に追加します。これでTextMeshProをスクリプト内で扱えるようになります。
1 2 3 4 |
using System.Collections; using System.Collections.Generic; using UnityEngine; using TMPro; |
新しい変数を2つ宣言します。ひとつはスコア用、もうひとつはテキストUIオブジェクト用です。
1 2 3 |
public AudioSource audioSource; public int score; public TextMeshProUGUI scoreText; |
UpdateScoreメソッドで、スコアを増やし、UI用テキストにスコアを表示する。
1 2 3 4 5 6 |
public void UpdateScore() { audioSource.Play(); score++; scoreText.text = "Score: " + score; } |
基本的にテキストは毎回変更しますが、”Score: ” の部分は変更しません。
Ctrl+Sでスクリプトを保存し、Unityエディタに戻りましょう。
HierarchyでCanvasオブジェクトを展開し、Text(TMP)の子オブジェクトを表示します。
HierarchyでGame Managerオブジェクトを選択し、Game ManagerコンポーネントのscoreTextフィールドにテキストオブジェクトをドラッグ&ドロップします。
一度ゲームプレイして変更が反映されているか確認してみましょう。
うまくいきましたね!
・・・あれ?Scoreが5から6ではなく、7になり、そのまま9になっていますね?
これはなぜでしょうか?
実は今Level(1)~Level(3)オブジェクトが配置されているZの位置が12,24,36ですがLevelControlsの地形生成の初期位置が12になっていました。
そのため実は今は二つの地形がダブって生成されてしまっています。StartingLevelやLevel(1)~(3)の地形を変更していたらもっと早く気づいていたかもしれませんね。
この問題を解決するにはLevelControlsのGenerateLevelsコンポーネントのZPosの数値を48に変更する。もしくはLevel(1)~(3)の初期オブジェクトを削除してしまうことです。
今回は視覚的に先が見えてる方が見栄えが良いのでZPosを48にして対応させました。
さて、再びテストプレイを行ってみましょう。
今度はちゃんとスコアが1ずつ増加していますね!これでようやく3Dランゲームの基本システムが出来上がりました。
まとめ
今回は3D無限ランゲームの軸となる地形の無限生成やスコアシステム等の実装を行いました。
こまめなテストプレイを行うことで開発中のバグを早期に取り除く重要性も体験できましたね。
いよいよ次で全5回の3D無限ランゲームの作り方講座の最後です。
次回はゲームオーバー処理を作成して実際にゲームをビルドしてPCやアンドロイドスマホで遊べるようにしていきましょう。
次の記事:
コメント