今回の記事でブロック崩しを完成させます。
以前までの記事でボールやプレイヤーブロックの移動処理、ボールが壁に当たったときの破壊処理など、ブロック崩しに必要な要素を実装してきました。
前回の記事:
今回は最後にプレイヤーの残機などUIに関係する部分やゲームのクリア・ゲームオーバー判定といったゲーム進行を管理する部分を作成していきましょう!
ブロック崩し ゲームUI作成の下準備
早速UIを作っていきますが、まず下準備をいくつか行っていきます。
ゲームの流れを管理するためのコンポーネントを用意する。
ゲームを作成する上でシーンで行う処理をまとめるコンポーネントがあると便利なのでそれをまず作成します。
この記事ではSceneManagerというファイル名でスクリプトを作成しています。
作成したスクリプトは始めは空でOKです。今回の記事ではこのSceneManagerスクリプトコンポーネントに機能を追加していきます。
1 2 3 4 5 |
// … using UnityEngine; // シーンを管理するコンポーネント class SceneManager : MonoBehaviour { } |
スクリプトを作成したらそのコンポーネントをシーン上のGameObjectにアタッチしてください。
ここではアタッチするGameObject用に空のGameObjectを作成し、SceneManagerと名付けています。
何かボタンを押すまでゲームを開始しないようにする
それでは下準備ができたので、ゲームの流れを作成していきましょう。
初めに再生した時、今の状態ではすぐにボールが動き出すようになっていますが、何かボタンを押すまでボールを動かさないようにしてみましょう!
SceneManagerコンポーネントの内容を次のサンプルコードにしてください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// … using System.Collections; using UnityEngine; public class SceneManager : MonoBehaviour { public Ball Ball; private void Awake() { Ball.enabled = false; } IEnumerator Start() { yield return new WaitUntil(() => Input.GetKeyDown(KeyCode.Space)); Ball.enabled = true; } } |
無事変更が完了しましたら、再生ボタンを押してください。
その際に必ずSceneManagerコンポーネントのBallフィールドにシーン上の「Ball」をアタッチしてください。
スペースボタンを押すまでボールが動かなかったら成功です。
上のサンプルコードでは次のことを行っています。
- Ballコンポーネントをフィールドに追加。
- スクリプト初期化時(Awakeメソッド)でBallコンポーネントを無効化する。
- スペースキーを押すまで待つ。
- スペースキーを押したら、Ballのコンポーネントを有効化する(移動開始)。
Unityではコンポーネントのenabledを有効化(true)するとそのコンポーネントを実行させることができます。
無効化(false)すると停止します。その際はそのコンポーネントが実行したコルーチンも停止しますので、覚えておくと便利でしょう!
ちなみにプレイヤーは動かすことができますが、Ballコンポーネントと同じことをするとプレイヤーの操作もできなくすることができます。できそうな方は実装してみるのもいいでしょう。
AwakeとStartメソッドについて
Unityではコンポーネントを実行し始めた時に2つのメソッドが呼び出されるようになっています。
- Awake: まず初めに実行されるメソッド
- Start: コンポーネントが有効になった時に実行されるメソッド
Awakeは必ずStartメソッドより先に呼び出されます。
今回の記事の場合、StartメソッドでBallコンポーネントを無効化するとスクリプトの実行順番までは保証できないのでSceneManagerのStartメソッドより先にBallコンポーネントのStartメソッドが呼び出されてしまうかもしれません。
そうなると先にボールに力が加わってしまい、ブロック崩しが開始されてしまいます。
そのようなことを避けるためSceneManagerのAwakeメソッドでBallコンポーネントを無効化しています。
もし、ボールが動いてしまう場合はBallコンポーネントの左上にあるチェックを消してみましょう。
チェックを消すとenabledにfalseを設定したのと同じ意味になります。
ボタンを押すまで何かテキストを表示してみる
ボタンを押すまでボールが動かなくなりましたが、それだと少し寂しい感じなので一緒にテキストも表示してみましょう!
まず、テキストを表示するためにメニューからGameObject > 3D Textをクリックし3Dテキストを作成してください。
作成したらテキストを「押したら開始」などと表示したいものに変更してみましょう!
設定後はGameビューを見ながら、ちょうどいい感じにテキストが映るように位置調整してください。シーンにあるMain Cameraの子GameObjectに設定すると安定しやすいです。
(他のGameObjectと被らないように注意してください。)
3Dテキストを作成したら、次はSceneManagerコンポーネントを拡張しましょう!
こちらは上でボールに対して行った処理とは反対に、スペースを押したら非表示になるようにします。
が、テキストの場合はコンポーネントではなく、GameObject自体の有効化を設定します。
GameObjectの有効化を設定する際はGameObject.SetActive
メソッドを利用してください。
enabledとSetActiveとの違いはコンポーネントの有効無効を切り替えるかゲームオブジェクトの有効無効を切り替えるかです。実用的にはコルーチンも止めたいかどうか。
コルーチンを止めたい場合はゲームオブジェクトを無効にします。逆に、コンポーネントを無効に切り替えてもコルーチンの動作は停止しません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// … using System.Collections; using UnityEngine; public class SceneManager : MonoBehaviour { public Ball Ball; public TextMesh StartText; private void Awake() { Ball.enabled = false; } IEnumerator Start() { StartText.gameObject.SetActive(true); yield return new WaitUntil(() => Input.GetKeyDown(KeyCode.Space)); Ball.enabled = true; StartText.gameObject.SetActive(false); } } |
無事スクリプトのコンパイルに成功しましたら、必ずSceneManagerに開始時に表示するテキストをアタッチしてください。
成功すると次の画像のようになります。
プレイヤーの残機を作る
次はプレイヤーの残機を作成してみましょう!
こちらも3Dテキストを利用して簡単に実装してみましょう!
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 |
//SceneManagerに残機用のテキストを追加する // int Lifeフィールドも追加し、開始時に残機用テキストを変更する // … using System.Collections; using UnityEngine; public class SceneManager : MonoBehaviour { public Ball Ball; public TextMesh StartText; public int Life = 3; public TextMesh LifeText; private void Awake() { Ball.enabled = false; } IEnumerator Start() { StartText.gameObject.SetActive(true); LifeText.gameObject.SetActive(false); LifeText.text = $"残機:{Life}"; yield return new WaitUntil(() => Input.GetKeyDown(KeyCode.Space)); Ball.enabled = true; StartText.gameObject.SetActive(false); LifeText.gameObject.SetActive(true); } } |
コンパイルが無事成功したら、開始時のテキストと同じように残機用のテキストをシーンに配置してください。
もちろん忘れずにSceneManagerコンポーネント欄にテキストオブジェクトをアタッチしてください。
ただし、テキストはSceneManagerのスクリプトから設定するのでエディターから設定したテキストは再生時に上書きされるので注意してください。
ここまでうまく実装できたら、再生した時に残機も画面に表示されるようになります。
この記事ではブロック崩しが開始するまで残機表示しないようにしていますが、この辺りはお好みに実装してください。
ボールを落としたら残機が減るようにする
次はボールを落とした時、一緒に残機用のテキストも変更するようにしましょう!
ボールを落としたと判定するには外壁の中の下にあるGameObjectにボールが当たるとちょうどいい感じになるので、そのように実装していきます。
実装内容としては以下のような手順が考えられます。
- BallコンポーネントにOnCollisionEnterメソッドを追加。
- その中で、外壁の下のものに当たったかどうか判定する処理を追加
- 当たっていたら、SceneManagerコンポーネントのLifeを一つ減らすようにする。
基本的には崩れる壁の消す処理と似たような処理をBallコンポーネントに追加していきます。
が、このようにコンポーネントに処理を追加していくと既にある処理と干渉してしまうかもしれません。
またSceneManagerコンポーネントのメンバフィールドを操作するように作ってしまうと、BallスクリプトからSceneManagerコンポーネントのメンバを操作することになります。
そうなるとコンポーネントに依存性が生まれるため使い回すことが難しくなります。
それを避けるため、今回は”何かに当たった時にUnityEventを実行するような汎用的なコンポーネント”を作成し、それを利用してみましょう!
関連記事:UnityC# デリゲートとイベントとUnityActionの使い方
UnityEventを使用するとコンポーネントの間に依存性が生まれにくくなるのでうまく活用していきましょう!
まず新しくスクリプトを作成し、ファイル名はOnHitCollisitionと名付けてください。
スクリプトを作成したら、次のサンプルコードのように実装してください。
内容は以前の記事で崩れる壁にアタッチしたコンポーネントと似ています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// … using UnityEngine; using UnityEngine.Events; public class OnHitCollision : MonoBehaviour { public string Name; public UnityEvent OnEnter; private void OnCollisionEnter(Collision collision) { if(collision.gameObject.name == Name) { OnEnter.Invoke(); } } } |
では次はSceneManagerに残機を変更できるメソッドを追加しましょう!
ChangeLifeメソッドをSceneManagerに追加で定義してください。その際、必ずpublicを指定してください。UnityEventに設定する際に必要になります。
1 2 3 4 5 6 7 8 9 10 11 |
// … public class SceneManager { // … // 残機変更のメソッド public void ChangeLife(int num) { Life += num; LifeText.text = $"残機: { Life}"; } } |
無事変更できたら、次の項目に従って設定を行ってください。この記事では外壁の下の部分のGameObjectを「BottomWall」と名付けるので、それを入力しています。
- ボールにOnHitCollisionコンポーネントをアタッチ
- OnHitCollisionのNameフィールドを外壁の下になるGameObjectと同じ名前に設定
- OnHitCollisionのOnEnterイベントにヒエラルキーのSceneManagerゲームオブジェクトをアタッチし、ChangeLifeメソッドを設定
- ChangeLifeメソッドの引数部分には-1を設定
ここまで実装できたら一度再生してみてください。そうするとボールを落とした時に残機用のテキストも変更されるようになっているはずです。
残機が減ったらボールを始めの位置に戻すようにする
ここまでで残機を減らせるようになりました。
あとは残機が減ったらボールを始めの位置に戻すようにしましょう。
そのためにBallコンポーネントの内容を修正します。
大きな変更点は次の点です。
- 開始時に初期位置を記録している -> _startBallPositionフィールド
- Restartメソッドの追加
- Stopメソッドの追加
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 37 38 39 40 41 42 43 |
using UnityEngine; public class Ball : MonoBehaviour { [Range(0, 1000)] public float Speed = 1000f; private Vector2 Direction = new Vector2(0, 1); private Rigidbody rigidBody; private Vector3 _startBallPosition; void Start() { _startBallPosition = transform.position; //元々あった処理は消してください。 //rigidBody.AddForce(Direction.normalized * Speed * Time.deltaTime, ForceMode.Impulse); } private void Update() { if (Mathf.Abs(rigidBody.velocity.y) <= 1) { rigidBody.velocity += Vector3.up * 5f * (rigidBody.velocity.y >= 0 ? 1 : -1); } } private void OnCollisionEnter(Collision collision) { rigidBody.velocity = rigidBody.velocity.normalized * Speed * Time.deltaTime; } public void Restart() { enabled = true; rigidBody.AddForce(Direction.normalized * Speed * Time.deltaTime, ForceMode.VelocityChange); } public void Stop() { enabled = false; transform.position = _startBallPosition; rigidBody.velocity = Vector3.zero; } } |
RestartメソッドおよびStopメソッドの中でRigidBody.isKinematicを使用しています。
これは以前の記事で簡単に触れましたが、物理エンジンによる制御を有効・無効化するプロパティになります。
今回はボールを開始時に停止した状態にしたいので、念のため使用しています。(また、移動速度も0にしています。)
また、enabledプロパティでBallコンポーネントの実行も再生・停止するようにもしています。
ボール側の準備はできたので、次はSceneManagerコンポーネントに次のサンプルコードを編集・追記してください。
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 37 |
//SceneManagerに残機用のテキストを追加する // int Lifeフィールドも追加し、開始時に残機用テキストを変更する using System.Collections; using UnityEngine; public class SceneManager : MonoBehaviour { public Ball Ball; public TextMesh StartText; public int Life = 3; public TextMesh LifeText; private void Awake() { //Ball.enabled = false; } IEnumerator Start() { StartText.gameObject.SetActive(true); Ball.Stop(); LifeText.gameObject.SetActive(false); LifeText.text = $"残機:{Life}"; yield return new WaitUntil(() => Input.GetKeyDown(KeyCode.Space)); Ball.enabled = true; StartText.gameObject.SetActive(false); LifeText.gameObject.SetActive(true); } public void ChangeLife(int num) { Life += num; LifeText.text = $"Life: { Life}"; StartCoroutine(Start()); } } |
ゲームオーバーを作成する
次は残機が0になったら、ゲームオーバーになるようにしましょう。
ゲームオーバーになったらこれまでと同じくテキストを表示するようにします。
作成手順はここまで行ったテキスト作成と同じになりますので、手順は省略します。
作成したGameObjectの名前はゲームオーバー用だとわかるように「GameOveText」と名付けておきましょう!
GameObjectを作成したら、次はSceneManagerを次のように拡張しましょう。
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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
//SceneManagerにゲームオーバー用のテキストを追加 // 初めは表示しないようにしている // 残機が0以下になったら表示するようにする // また、プレイヤーなどの操作もできなくする。 using System.Collections; using UnityEngine; public class SceneManager : MonoBehaviour { public Ball Ball; public TextMesh StartText; public int Life = 3; public TextMesh LifeText; public TextMesh GameOverText; private void Awake() { //Ball.enabled = false; GameOverText.gameObject.SetActive(false); } IEnumerator Start() { StartText.gameObject.SetActive(true); Ball.Stop(); LifeText.gameObject.SetActive(false); LifeText.text = $"残機:{Life}"; yield return new WaitUntil(() => Input.GetKeyDown(KeyCode.Space)); Ball.enabled = true; StartText.gameObject.SetActive(false); LifeText.gameObject.SetActive(true); } public void ChangeLife(int num) { Life += num; LifeText.text = $"Life: { Life}"; if (Life <= 0) { GameOverText.gameObject.SetActive(true); Ball.gameObject.SetActive(false); } else { StartCoroutine(Start()); } } } |
拡張内容は他のテキストの処理と似たものになります。
注目して欲しい部分は残機が0以下になったか判定し、そうなった時にゲームオーバー用のテキストを表示している部分です。
忘れずにSceneManagerのインスペクターにGameOverオブジェクトをアタッチします。
ここまで無事作業できたら再生してみましょう。
成功すれば次の画像のように残機0になったときにゲームオーバーと表示され、ボールオブジェクトが非表示になります。
ブロックを全て削除したらクリアーできるようにする
最後に、全てのブロックを消した時にはクリアーと表示するようにしましょう!
表示するテキストはこれまでと同じくTextゲームオブジェクトを使って作ります。「GameClearText」というゲームオブジェクトを作り、クリア時に表示する文字列を設定しておきましょう。
こちらもゲームオーバーと同じような処理になりますが、全てのブロックを削除した時にテキストを表示する必要があります。
全てのブロックを削除したかを判定するにはいくつか方法がありますが、基本的な考え方は次のようになります。
- ブロック削除時に
- シーンにある残りのブロックを見て
- なければクリアーとする
これを達成するには色々方法がありますが、この記事ではの親GameObjectを使用して解決する方法について説明しています。
親GameObjectを使用して子GameObjectを調べる方法
ボールがブロックに当たった時にブロックを削除していますが、このタイミングでシーン上にある残りのブロックの数も毎回数えるようにします。
ブロックは以前の記事で「BlockRoot」の子GameObjectにしています。これをSceneManagerスクリプトでTransform BlockRootフィールドとして設定します。
また、フィールドに設定すると同時に、SceneManagerスクリプトに以下のようにCheckBlocksメソッドを追加してください。
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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
using System.Collections; using UnityEngine; public class SceneManager : MonoBehaviour { public Ball Ball; public TextMesh StartText; public int Life = 3; public TextMesh LifeText; public TextMesh GameOverText; public TextMesh GameClearText; public Transform BlockRoot; private void Awake() { //Ball.enabled = false; GameOverText.gameObject.SetActive(false); GameClearText.gameObject.SetActive(false); } IEnumerator Start() { StartText.gameObject.SetActive(true); Ball.Stop(); LifeText.gameObject.SetActive(false); LifeText.text = $"残機:{Life}"; yield return new WaitUntil(() => Input.GetKeyDown(KeyCode.Space)); Ball.enabled = true; StartText.gameObject.SetActive(false); LifeText.gameObject.SetActive(true); } public void ChangeLife(int num) { Life += num; LifeText.text = $"Life: { Life}"; if (Life <= 0) { GameOverText.gameObject.SetActive(true); Ball.gameObject.SetActive(false); } else { StartCoroutine(Start()); } } public void CheckBlocks() { if (BlockRoot.childCount <= 1) { GameClearText.gameObject.SetActive(true); Ball.gameObject.SetActive(false); } } } |
SceneManager.CheckBlocksでクリア判定を行います。
このメソッドの中でTransform型のBlockRootのchildCountを利用して、存在しているブロックの数を数えています。
GameObjectの親子関係はスクリプト上ではGameObject型ではなくTransform型が管理しているので覚えておきましょう!
あとはこのメソッドをブロックとボールが当たる度に呼び出すようにすればOKです。
また、ClearBlocksメソッドはブロックを消すタイミングで呼び出されるので、その時点で消す予定のブロックはまだシーン上にあるとみなされます。そのため、上のサンプルコードの条件式では0ではなく1を使用してchildCountと比較しています。
プレハブを使用する際の注意点
メソッドを呼び出す際は今回の記事で作成したOnHitCollisionコンポーネントを利用します。
崩れるブロックはプレハブを利用しており、かつシーン上にたくさん存在しているので、プレハブにOnHitCollisionコンポーネントをアタッチしてSceneManagerのCheckBlockメソッドを設定すると簡単です。
ですがここで注意点としてOnHitCollisionコンポーネントはUnityEventを利用していますが、プレハブの中にあるUnityEventにはシーン上にあるGameObjectのコンポーネントのメソッドを設定することはできません。
これはUnityEventだけの問題ではなくコンポーネントのフィールド全てについて同じ問題を抱えています。
プレハブの状態だとフィールドがどのシーンのものなのか判別できないため設定できないのです。
もちろん一度シーン上にプレハブを配置したあとなら設定できます。また、同じプレハブの中にあるGameObjectのコンポーネントのメソッドなら問題なく設定できます!
今回の場合で一番簡単な方法としてはUnityエディター上で次の作業を行えばOKです。
- ブロックのプレハブにOnHitCollisionコンポーネントをアタッチする。
- シーン上にあるブロックのプレハブを基にしたGameObjectにSceneManagerのCheckBlockメソッドをアタッチしたOnHitCollisionコンポーネントに設定する。
ですがブロックは数が多いので、今回は手作業ではなくスクリプトを利用して簡単に設定してみましょう!
SceneManagerコンポーネントのAwakeメソッドの中に次のコードを追加してください。
内容としてはBlockRootの子GameObject全てにOnHitCollisionコンポーネントをアタッチしています。
追加した処理の内容は次のようになります。
- Nameフィールド:ボールを表すGameObjectの名前を設定
- OnEnterイベント:SceneManagerのCheckBlocksメソッドを設定する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// … public class SceneManager : MonoBehaviour { private void Awake() { //… foreach (Transform block in BlockRoot) { var onHitCollistion = block.gameObject.AddComponent<OnHitCollision>(); onHitCollistion.Name = Ball.name; onHitCollistion.OnEnter = new UnityEngine.Events.UnityEvent(); onHitCollistion.OnEnter.AddListener(this.CheckBlocks); } } } |
OnEnterイベントはOnHitCollisionクラスで定義されていて、OnCollisionEnterが実行されたときに、nameが一致したら呼び出される処理だったのを思い出してください。その呼び出される処理の中身をSceneManagerから親子関係を使って設定したわけですね。
無事コンパイルできたら、全ての崩れるブロックを消すとクリアーと画面に表示されるようになります。
まとめ
今回の記事ではブロック崩しのゲームの流れを実装していきました。
ゲームの流れと書くと簡単そうですが、実際に作ると意外と難しいものになります。また、適当に作るとゲームを作っていくうちにどんどん複雑化しやすい部分でもあります。
ここで紹介した方法は内容をコンパクトにするようにしているため、このまま拡張するとゲームを作成しづらくなっていってしまうかもしれません。
開発プロジェクトが大きくなっても拡張性を損なわないように、様々な開発ノウハウが考え出されているので、色々と調べてみるのもいいでしょう。
ソフトウェア開発や設計手法、デザインパターン、MVC(Model View Control)などを学んでみると一歩上の開発ができるようになります。
また、残機表示などのUI表示に3DオブジェクトのTextを使用しましたが、UnityにはUI用のコンポーネントも存在しているのでそちらを利用するものOKです。
今回の記事をまとめると次のようになります。
- ゲームの流れを作る時は専用のコンポーネントを作ると便利
- GameObject.SetActive()を使用するとGameObjectの有効・無効を設定できる。
- Component.enabledを設定するとそのComponentを実行したり停止したりできる。
- UnityEventなどデリゲート型を利用すると簡単に処理をつなげることができて便利。
ここまででブロック崩しの解説は終わります。
次からはまた別のゲームを作っていきましょう!
コメント