現場レベルのゲーム制作が、すべてここで学べます。
2D倉庫番ゲーム制作講座もいよいよ今回で最終回(第5回)を迎えます!
前回までで、プレイヤーが箱を押し、ゴールへ運んでクリア音を鳴らすという「ゲームの骨組み」が完成しました。

しかし、今の状態ではConsole(コンソール)画面を見ないとクリアしたことがわからず、一度クリアすると再プレイもできません。
また、ギミックとなる箱とゴールを1つずつしかまだ置けない状態です。
そこで今回はゲームを「しっかりと遊べる形」に仕上げていきます。
TextMeshProを使った華やかな「GOAL!!」の文字表示、一瞬で最初からやり直せる「リトライボタン」の実装。
さらに応用編として箱やゴールが何個あっても自動でクリア判定を行う「複数オブジェクトへの対応」や、ゲーム開発には付き物の「バグ修正」まで一気にマスターしていきます。
それでは、この倉庫番ゲームを完成させましょう!
ゲームクリア画面を表示させよう
今は箱をGoalに乗せてクリアしたとき、歓声の音が鳴り、ConsoleにClear!と表示されるだけです。
ですが、このままだと画面上ではクリアしたことがわかりづらいです。
プレイヤーの操作も止まるので、人によっては「ゲームが止まったのかな?」と勘違いしてしまうかもしれません。
そこで今回は、クリアしたときに画面上にGOAL!!という文字を表示してみましょう。
クリアした文字を用意しよう
まずは、UIでGOAL!!という文字を用意します。Hierarchyウィンドウで右クリックして、UI(Canvas) > Text – TextMeshProを選択しましょう。

すると、初めてTextMeshProを使う場合は、TMP Importerというウィンドウが開きます。

このウィンドウが表示されたら、Import TMP Essentialsを選択しましょう。

TMP Essentialsは、TextMeshProを使うために必要な基本データです。
下にあるImport TMP Examples & Extrasは、サンプルや追加素材なので今回はインポートしなくて大丈夫です。
今回は講座で使う文字表示ができればよいので、上のImport TMP Essentialsのみをインポートします。
インポートが終わったら、TMP Importerのウィンドウは閉じて大丈夫です。

その後、CanvasとTextが作成されます。

作成されたTextの名前は、分かりやすくGoalUIに変更しておきましょう。


Canvasとは?
ここで、Canvasについて簡単に説明しておきます。
Canvasとは、UnityでUIを表示するための場所です。たとえば、今回のようなクリア文字。他にはスコア、ボタン、タイトル画面、HPバー、メニュー画面などは基本的にCanvasの中に配置して作成します。
通常のプレイヤーや壁、箱などは、ゲームの世界の中にあるオブジェクトです。
一方で、UIはゲーム画面の手前に表示される文字やボタンです。そのUIを表示するための土台になるのがCanvasです。今回作成したGoalUIもCanvasの中に作成されています。
つまり、Canvasは「UIを表示するための大きな紙」のようなものだと考えると分かりやすいです。その紙の上にTextやButtonなどのUIを置いていくイメージです。
Canvasの設定をしよう
次に、Canvasの設定を変更します。HierarchyからCanvasを選択しましょう。

InspectorにあるCanvas Scalerを確認します。その中にあるUI Scale Modeを、Scale With Screen Sizeに変更しましょう。

これは、画面サイズや解像度が変わっても、UIの大きさや位置が崩れにくくなる設定です。
この設定をしていないと、ゲーム画面のサイズによってUIが思った位置からずれてしまうことがあります。
初心者向けの2Dゲームでは、UIを作るときにまずこの設定をしておくと安心です。
(よりしっかりと画面サイズや解像度対応させたい場合は以下の参考記事を読んでみてください)

GoalUIの見た目を整えよう
次に、HierarchyからGoalUIを選択しましょう。

まず、Textの文字をGOAL!!に変更します。

次に、位置とサイズを調整します。Rect TransformのPositionをすべて0に設定しましょう。

次に、WidthとHeightを設定します。Width = 500、Height = 150にします。

さらに、Font Sizeを125に設定しましょう。

次に、Alignmentを中央にします。横方向も縦方向も中央揃えにすることで、GOAL!!の文字が画面中央に表示されます。

また、Goalの表示なので、文字の色はゴールらしく黄色にしてみましょう。

ただし、単色の文字だけだと背景によっては少し見づらかったり、物足りなく見えたりします。
そこで、TextMeshProの設定の下の方にあるOutlineをオンにします。Outlineの太さは0.25に設定してみましょう。

これで、文字の外側にフチが付いて、見やすくなります。

↑ゲーム画面はこんな感じになってると思います。
最後に、Inspectorの左上にあるチェックボックスを外して、GoalUIを非アクティブにしておきます。

このチェックボックスを外すと、Gameビュー上ではGoalUIが表示されなくなります。
「非アクティブって何?」と思うかもしれませんが、こちらはあとでスクリプトから表示・非表示を切り替えるために使う際に説明するので一旦気にしなくて大丈夫です。
スクリプトからゲームクリア画面の表示を制御しよう
ここまでできたら、スクリプトからGoalUIを表示できるようにしていきます。
ProjectウィンドウからAssets > ScriptsフォルダにあるPlayerController2Dをダブルクリックで開き、以下のように追記しましょう。
|
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 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 |
using UnityEngine; using UnityEngine.Audio; public class PlayerController2D : MonoBehaviour { public bool isGameCleared = false; [Header("UI")] public GameObject goalUI; [Header("Sound Effects")] private AudioSource audioSource; public AudioClip moveSound; public AudioClip blockedSound; public AudioClip pushBoxSound; public AudioClip clearSound; [Header("Player Sprites")] public Sprite upSprite; public Sprite downSprite; private SpriteRenderer spriteRenderer; private Sprite defaultSprite; // Start is called once before the first execution of Update after the MonoBehaviour is created void Start() { audioSource = GetComponent<AudioSource>(); spriteRenderer = GetComponent<SpriteRenderer>(); defaultSprite = spriteRenderer.sprite; } // Update is called once per frame void Update() { if (isGameCleared) { goalUI.SetActive(true); return; } if (Input.GetKeyDown(KeyCode.UpArrow)) { spriteRenderer.sprite = upSprite; spriteRenderer.flipX = false; float nextPlayerX = transform.position.x; float nextPlayerY = transform.position.y + 1; Vector2 nextPlayerPosition = new Vector2(nextPlayerX, nextPlayerY); if (IsWall(nextPlayerPosition)) { audioSource.PlayOneShot(blockedSound); return; } GameObject boxObject = GetBox(nextPlayerPosition); if (boxObject != null) { float nextBoxX = boxObject.transform.position.x; float nextBoxY = boxObject.transform.position.y + 1; Vector2 nextBoxPosition = new Vector2(nextBoxX, nextBoxY); if (IsWall(nextBoxPosition)) { audioSource.PlayOneShot(blockedSound); return; } boxObject.transform.position = nextBoxPosition; transform.position = nextPlayerPosition; if (IsBoxOnGoal(nextBoxPosition)) { isGameCleared = true; Debug.Log("Clear!"); audioSource.PlayOneShot(clearSound); } audioSource.PlayOneShot(pushBoxSound); return; } transform.position = nextPlayerPosition; audioSource.PlayOneShot(moveSound); } if (Input.GetKeyDown(KeyCode.DownArrow)) { spriteRenderer.sprite = downSprite; spriteRenderer.flipX = false; float nextPlayerX = transform.position.x; float nextPlayerY = transform.position.y - 1; Vector2 nextPlayerPosition = new Vector2(nextPlayerX, nextPlayerY); if (IsWall(nextPlayerPosition)) { audioSource.PlayOneShot(blockedSound); return; } GameObject boxObject = GetBox(nextPlayerPosition); if (boxObject != null) { float nextBoxX = boxObject.transform.position.x; float nextBoxY = boxObject.transform.position.y - 1; Vector2 nextBoxPosition = new Vector2(nextBoxX, nextBoxY); if (IsWall(nextBoxPosition)) { audioSource.PlayOneShot(blockedSound); return; } boxObject.transform.position = nextBoxPosition; transform.position = nextPlayerPosition; if (IsBoxOnGoal(nextBoxPosition)) { isGameCleared = true; Debug.Log("Clear!"); audioSource.PlayOneShot(clearSound); } audioSource.PlayOneShot(pushBoxSound); return; } transform.position = nextPlayerPosition; audioSource.PlayOneShot(moveSound); } if (Input.GetKeyDown(KeyCode.RightArrow)) { spriteRenderer.sprite = defaultSprite; spriteRenderer.flipX = false; float nextPlayerX = transform.position.x + 1; float nextPlayerY = transform.position.y; Vector2 nextPlayerPosition = new Vector2(nextPlayerX, nextPlayerY); if (IsWall(nextPlayerPosition)) { audioSource.PlayOneShot(blockedSound); return; } GameObject boxObject = GetBox(nextPlayerPosition); if (boxObject != null) { float nextBoxX = boxObject.transform.position.x + 1; float nextBoxY = boxObject.transform.position.y; Vector2 nextBoxPosition = new Vector2(nextBoxX, nextBoxY); if (IsWall(nextBoxPosition)) { audioSource.PlayOneShot(blockedSound); return; } boxObject.transform.position = nextBoxPosition; transform.position = nextPlayerPosition; if (IsBoxOnGoal(nextBoxPosition)) { isGameCleared = true; Debug.Log("Clear!"); audioSource.PlayOneShot(clearSound); } audioSource.PlayOneShot(pushBoxSound); return; } transform.position = nextPlayerPosition; audioSource.PlayOneShot(moveSound); } if (Input.GetKeyDown(KeyCode.LeftArrow)) { spriteRenderer.sprite = defaultSprite; spriteRenderer.flipX = true; float nextPlayerX = transform.position.x - 1; float nextPlayerY = transform.position.y; Vector2 nextPlayerPosition = new Vector2(nextPlayerX, nextPlayerY); if (IsWall(nextPlayerPosition)) { audioSource.PlayOneShot(blockedSound); return; } GameObject boxObject = GetBox(nextPlayerPosition); if (boxObject != null) { float nextBoxX = boxObject.transform.position.x - 1; float nextBoxY = boxObject.transform.position.y; Vector2 nextBoxPosition = new Vector2(nextBoxX, nextBoxY); if (IsWall(nextBoxPosition)) { audioSource.PlayOneShot(blockedSound); return; } boxObject.transform.position = nextBoxPosition; transform.position = nextPlayerPosition; if (IsBoxOnGoal(nextBoxPosition)) { isGameCleared = true; Debug.Log("Clear!"); audioSource.PlayOneShot(clearSound); } audioSource.PlayOneShot(pushBoxSound); return; } transform.position = nextPlayerPosition; audioSource.PlayOneShot(moveSound); } } private bool IsWall(Vector2 checkPosition) { Collider2D[] hitColliders = Physics2D.OverlapPointAll(checkPosition); for (int i = 0; i < hitColliders.Length; i++) { if (hitColliders[i].CompareTag("Wall")) { return true; } } return false; } private GameObject GetBox(Vector2 checkPosition) { Collider2D[] hitColliders = Physics2D.OverlapPointAll(checkPosition); for (int i = 0; i < hitColliders.Length; i++) { if (hitColliders[i].CompareTag("Box")) { return hitColliders[i].gameObject; } } return null; } private bool IsBoxOnGoal(Vector2 boxPosition) { Collider2D[] hitColliders = Physics2D.OverlapPointAll(boxPosition); for (int i = 0; i < hitColliders.Length; i++) { if (hitColliders[i].CompareTag("Goal")) { return true; } } return false; } } |
今回追加したのは、次の2つです。
|
1 2 |
[Header("UI")] public GameObject goalUI; |
こちらは、クリアしたときに表示するGoalUIをスクリプトから操作するための変数です。
public GameObject goalUI;と書くことで、UnityのInspectorからGoalUIをアタッチできるようになります。
GameObjectというのは、Unity上にあるオブジェクト全般を扱うための型です。今回であれば、Hierarchyに作成したGoalUIをこの変数に入れて、スクリプトから表示・非表示を切り替えられるようにしています。
次に追加したのがこちらです。
|
1 2 3 4 5 |
if (isGameCleared) { goalUI.SetActive(true); return; } |
これは、ゲームをクリアしたあとにGoalUIを表示する処理です。
isGameClearedがtrueになったら、つまりクリアしたら、goalUI.SetActive(true);でGoalUIを表示します。そのあとreturnで処理を終了するので、クリア後はプレイヤー操作の処理に進まなくなります。
アクティブオブジェクトとは?
ここで、アクティブオブジェクトについて説明しておきます。
Unityのオブジェクトには、アクティブと非アクティブという状態があります。
アクティブとは、そのオブジェクトが有効になっている状態です。アクティブなオブジェクトは、ゲーム画面に表示され、スクリプトが動き、コンポーネントに設定してあるColliderが判定に使われたりします。
反対に、非アクティブとは、そのオブジェクトが無効になっている状態です。非アクティブのオブジェクトは基本的にゲーム画面に表示されません。また、そのオブジェクトに付いているスクリプトやColliderなども動かなくなります。
Inspectorの左上にあるチェックボックスがオンになっていると、そのオブジェクトはアクティブです。

チェックボックスを外すと、そのオブジェクトは非アクティブになります。

今回のGoalUIは、ゲーム開始時には表示したくありません。
最初からGOAL!!と表示されていたら、まだクリアしていないのにクリアしたように見えてしまいます。
そのため、最初はInspector左上のチェックボックスを外して非アクティブにしておきます。
そして、クリアしたタイミングでスクリプトからアクティブにします。それを行っているのが、次の処理です。
|
1 |
goalUI.SetActive(true); |
SetActive(true)は、そのオブジェクトをアクティブにする処理です。つまり、非表示だったGoalUIを表示するということです。
反対に、
|
1 |
goalUI.SetActive(false); |
と書くと、そのオブジェクトを非アクティブにできます。つまり、表示されていたUIを非表示にすることができます。
UIだけではなく、敵、アイテム、エフェクト、説明パネルなど、さまざまなオブジェクトの表示や動作を切り替えるときにも使えます。
たとえば、アイテムを取ったらそのアイテムを消す場合は、
|
1 |
itemObject.SetActive(false); |
のように書くこともあります。ただし、非アクティブにしたオブジェクトは画面から消えるだけで、完全に削除されたわけではありません。
また必要になったときに
|
1 |
itemObject.SetActive(true); |
とすれば、もう一度表示できます。
この点がオブジェクトを完全に消すDestroyとは違うところです。
Destroyはオブジェクトそのものを削除します。
|
1 |
Destroy(itemObject); |
一方で、SetActive(false)は一時的に無効にするだけです。
そのため、あとでまた表示したいUIやオブジェクトには、SetActiveを使うと便利です。
今回のGoalUIも、最初は非アクティブにしておき、クリアしたらアクティブにする使い方をしています。
スクリプトを確認しよう
ここまでできたら、Ctrl + Sで保存してUnityに戻りましょう。Unityに戻ったら、HierarchyからPlayerを選択します。

InspectorにあるPlayerController2Dを確認すると、Goal UIという項目が追加されています。

ここで、Inspectorを確認したときに、少し気になることがあるかもしれません。本来であれば、スクリプト上では次のように書いています。
|
1 2 3 4 5 6 7 8 9 |
[Header("UI")] public GameObject goalUI; [Header("Sound Effects")] private AudioSource audioSource; public AudioClip moveSound; public AudioClip blockedSound; public AudioClip pushBoxSound; public AudioClip clearSound; |
InspectorではUIという見出しの下にGoal UIが表示され、Sound Effectsという見出しの下に音の設定項目が表示されてほしいところです。しかし、UIの項目の中に音の設定まで続いて表示されています。

これは、[Header(“Sound Effects”)]のすぐ下にある
|
1 2 3 4 5 6 |
[Header("Sound Effects")] private AudioSource audioSource; public AudioClip moveSound; public AudioClip blockedSound; public AudioClip pushBoxSound; public AudioClip clearSound; |
こちらがprivateになっているためです。privateの変数は、基本的にInspectorには表示されません。
Headerは、その下にあるInspectorへ表示される項目に対して見出しを付けるためのものです。
しかし、[Header(“Sound Effects”)]の直下にあるprivate AudioSource audioSource;はInspectorに表示されないため、見出しの表示もされません。
この場合は、private AudioSource audioSource;をInspectorに表示したい項目の間に置かないようにするとわかりやすくなります。
次のように書き換えましょう。
|
1 2 3 4 5 6 |
[Header("Sound Effects")] public AudioClip moveSound; public AudioClip blockedSound; public AudioClip pushBoxSound; public AudioClip clearSound; private AudioSource audioSource; |
このように、ゲーム開発では思わぬ表示の違いや、予期していなかった不具合が起きることがあります(まあ今回に関しては表示上の問題だけで不具合というほどのものではないですが)。
ですが、不具合には必ず原因があります。
最初は焦ってしまうかもしれませんが、落ち着いてコードを見直したり、設定を確認したり、調べたり、AIに聞いてみたりすることで少しずつ解決できるようになります。
エラーや不具合が出ること自体はゲーム開発ではよくあることです。
むしろ、その原因を探して直していくことでUnityやプログラムへの理解が深まっていきます。
書き換えが終わったら、Ctrl + Sで保存するのを忘れずに、Unityに戻りましょう。Unityに戻ったら、HierarchyからPlayerを選択します。

Goal UIの欄に、HierarchyにあるGoalUIをドラッグ&ドロップしましょう。

これで、スクリプトからGoalUIを操作できるようになります。
ここまでできたら再生して確認してみましょう。箱をGoalの上に押して、クリアしてみます。

クリアしたときに画面中央にGOAL!!と表示されれば成功です。これでConsoleを見なくても、ゲーム画面上でクリアしたことが分かるようになりました。
再プレイできるようにしよう
現状では、クリア後にプレイヤーを操作できないようにしています。
そのため、一度クリアするともう一度遊ぶことができません。
そこで、次はリプレイボタンを追加してもう一度最初から遊べるようにしていきましょう。
仕組みはとてもシンプルです。リプレイボタンを押したら、現在のシーンを再ロードします。
シーンの再ロードとは?
シーンの再ロードとは、今開いているシーンをもう一度読み込み直すことです。
たとえば現状だと、クリア後に操作できなくなった場合、一度Unityの再生を止めてもう一度再生ボタンを押すと最初から遊べるようになると思います。
シーンの再ロードは、それに近いことをスクリプトから行うイメージです。
ただし、厳密にはUnityの再生を止めて再スタートしているわけではありません。
ゲームの中で今のシーン自体をもう一度読み込み直しています。
つまり、プレイヤーの位置、箱の位置、クリア状態などを、シーン開始時の状態に戻すということです。
今回はこの仕組みを使ってRetryボタンを押したら最初からやり直せるようにします。
ゲームのリトライボタンを作成しよう
まずは、Retryボタンを用意します。
HierarchyでCanvasを右クリックして、UI(Canvas) > Button – TextMeshProを選択しましょう。
作成されたボタンの名前は、わかりやすくReplayButtonに変更しておきましょう。

次に、作成されたReplayButtonの子オブジェクトを確認します。子オブジェクトにTextがあると思います。
今回は、今作ったReplayButtonにこちらで用意したボタン画像を設定します。
その画像にRetryという文字が入っているので、このTextは必要ありません。ReplayButtonの子オブジェクトにあるTextを選択して削除しましょう。

次に、ReplayButtonを選択します。

InspectorのRect Transformにあるアンカープリセットから、top – centerを選択しましょう。

アンカーとは?
ここで、アンカーについて簡単に説明しておきます。
アンカーとは、UIを画面のどこにくっつけておくかを決める目印のようなものです。
たとえば、紙にシールを貼るところを想像してみてください。シールを「紙の左上にくっつける」と決めておけば、紙の大きさが少し変わっても、そのシールは左上を基準にして位置を保とうとします。

UnityのUIもこれと同じです。
画面サイズは、プレイする環境によって変わることがあります。パソコンの画面、ゲームビューのサイズ、フルスクリーン、ウィンドウ表示などによって、横長になったり、少し小さくなったりします。
そのときに、UIがどこを基準に置かれるのかを決めるのがアンカーです。

今回のRetryボタンは、画面の上の中央に置きたいので、top – centerを選びます。
これは、「画面の上の真ん中を基準にして、このボタンを置いてください」という設定です。もし左上に固定したい場合はtop – left、画面中央に置きたい場合はmiddle – centerのように、UIを置きたい場所に合わせてアンカーを選びます。
アンカーを正しく設定しておくと、画面サイズが変わってもUIの位置が大きく崩れにくくなります。
アンカーを設定できたら、Rect Transformを調整します。以下のように設定しましょう。

次に、ボタン画像を設定します。
Projectウィンドウから、Assets内にある倉庫番ゲーム画像素材フォルダを開きましょう。その中に「リトライ」というボタン画像があると思います。

ReplayButtonを選択した状態で、InspectorのImageコンポーネントを確認します。Source Imageの欄に、「リトライ」画像をドラッグ&ドロップして設定しましょう。

これで、Retryボタンの下準備は完了です。

スクリプトから制御しよう
次に、Retryボタンを押したときに、シーンを再ロードできるようにします。ProjectウィンドウからAssets > ScriptsフォルダにあるPlayerController2Dをダブルクリックで開き、以下のように追記します。
|
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 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 |
using UnityEngine; using UnityEngine.Audio; using UnityEngine.SceneManagement; public class PlayerController2D : MonoBehaviour { public bool isGameCleared = false; [Header("UI")] public GameObject goalUI; [Header("Sound Effects")] public AudioClip moveSound; public AudioClip blockedSound; public AudioClip pushBoxSound; public AudioClip clearSound; private AudioSource audioSource; [Header("Player Sprites")] public Sprite upSprite; public Sprite downSprite; private SpriteRenderer spriteRenderer; private Sprite defaultSprite; // Start is called once before the first execution of Update after the MonoBehaviour is created void Start() { audioSource = GetComponent<AudioSource>(); spriteRenderer = GetComponent<SpriteRenderer>(); defaultSprite = spriteRenderer.sprite; } // Update is called once per frame void Update() { if (isGameCleared) { goalUI.SetActive(true); return; } if (Input.GetKeyDown(KeyCode.UpArrow)) { spriteRenderer.sprite = upSprite; spriteRenderer.flipX = false; float nextPlayerX = transform.position.x; float nextPlayerY = transform.position.y + 1; Vector2 nextPlayerPosition = new Vector2(nextPlayerX, nextPlayerY); if (IsWall(nextPlayerPosition)) { audioSource.PlayOneShot(blockedSound); return; } GameObject boxObject = GetBox(nextPlayerPosition); if (boxObject != null) { float nextBoxX = boxObject.transform.position.x; float nextBoxY = boxObject.transform.position.y + 1; Vector2 nextBoxPosition = new Vector2(nextBoxX, nextBoxY); if (IsWall(nextBoxPosition)) { audioSource.PlayOneShot(blockedSound); return; } boxObject.transform.position = nextBoxPosition; transform.position = nextPlayerPosition; if (IsBoxOnGoal(nextBoxPosition)) { isGameCleared = true; Debug.Log("Clear!"); audioSource.PlayOneShot(clearSound); } audioSource.PlayOneShot(pushBoxSound); return; } transform.position = nextPlayerPosition; audioSource.PlayOneShot(moveSound); } if (Input.GetKeyDown(KeyCode.DownArrow)) { spriteRenderer.sprite = downSprite; spriteRenderer.flipX = false; float nextPlayerX = transform.position.x; float nextPlayerY = transform.position.y - 1; Vector2 nextPlayerPosition = new Vector2(nextPlayerX, nextPlayerY); if (IsWall(nextPlayerPosition)) { audioSource.PlayOneShot(blockedSound); return; } GameObject boxObject = GetBox(nextPlayerPosition); if (boxObject != null) { float nextBoxX = boxObject.transform.position.x; float nextBoxY = boxObject.transform.position.y - 1; Vector2 nextBoxPosition = new Vector2(nextBoxX, nextBoxY); if (IsWall(nextBoxPosition)) { audioSource.PlayOneShot(blockedSound); return; } boxObject.transform.position = nextBoxPosition; transform.position = nextPlayerPosition; if (IsBoxOnGoal(nextBoxPosition)) { isGameCleared = true; Debug.Log("Clear!"); audioSource.PlayOneShot(clearSound); } audioSource.PlayOneShot(pushBoxSound); return; } transform.position = nextPlayerPosition; audioSource.PlayOneShot(moveSound); } if (Input.GetKeyDown(KeyCode.RightArrow)) { spriteRenderer.sprite = defaultSprite; spriteRenderer.flipX = false; float nextPlayerX = transform.position.x + 1; float nextPlayerY = transform.position.y; Vector2 nextPlayerPosition = new Vector2(nextPlayerX, nextPlayerY); if (IsWall(nextPlayerPosition)) { audioSource.PlayOneShot(blockedSound); return; } GameObject boxObject = GetBox(nextPlayerPosition); if (boxObject != null) { float nextBoxX = boxObject.transform.position.x + 1; float nextBoxY = boxObject.transform.position.y; Vector2 nextBoxPosition = new Vector2(nextBoxX, nextBoxY); if (IsWall(nextBoxPosition)) { audioSource.PlayOneShot(blockedSound); return; } boxObject.transform.position = nextBoxPosition; transform.position = nextPlayerPosition; if (IsBoxOnGoal(nextBoxPosition)) { isGameCleared = true; Debug.Log("Clear!"); audioSource.PlayOneShot(clearSound); } audioSource.PlayOneShot(pushBoxSound); return; } transform.position = nextPlayerPosition; audioSource.PlayOneShot(moveSound); } if (Input.GetKeyDown(KeyCode.LeftArrow)) { spriteRenderer.sprite = defaultSprite; spriteRenderer.flipX = true; float nextPlayerX = transform.position.x - 1; float nextPlayerY = transform.position.y; Vector2 nextPlayerPosition = new Vector2(nextPlayerX, nextPlayerY); if (IsWall(nextPlayerPosition)) { audioSource.PlayOneShot(blockedSound); return; } GameObject boxObject = GetBox(nextPlayerPosition); if (boxObject != null) { float nextBoxX = boxObject.transform.position.x - 1; float nextBoxY = boxObject.transform.position.y; Vector2 nextBoxPosition = new Vector2(nextBoxX, nextBoxY); if (IsWall(nextBoxPosition)) { audioSource.PlayOneShot(blockedSound); return; } boxObject.transform.position = nextBoxPosition; transform.position = nextPlayerPosition; if (IsBoxOnGoal(nextBoxPosition)) { isGameCleared = true; Debug.Log("Clear!"); audioSource.PlayOneShot(clearSound); } audioSource.PlayOneShot(pushBoxSound); return; } transform.position = nextPlayerPosition; audioSource.PlayOneShot(moveSound); } } private bool IsWall(Vector2 checkPosition) { Collider2D[] hitColliders = Physics2D.OverlapPointAll(checkPosition); for (int i = 0; i < hitColliders.Length; i++) { if (hitColliders[i].CompareTag("Wall")) { return true; } } return false; } private GameObject GetBox(Vector2 checkPosition) { Collider2D[] hitColliders = Physics2D.OverlapPointAll(checkPosition); for (int i = 0; i < hitColliders.Length; i++) { if (hitColliders[i].CompareTag("Box")) { return hitColliders[i].gameObject; } } return null; } private bool IsBoxOnGoal(Vector2 boxPosition) { Collider2D[] hitColliders = Physics2D.OverlapPointAll(boxPosition); for (int i = 0; i < hitColliders.Length; i++) { if (hitColliders[i].CompareTag("Goal")) { return true; } } return false; } public void ReplayGame() { SceneManager.LoadScene(SceneManager.GetActiveScene().name); } } |
今回追加したプログラムは次の2つです。まず以下の行を追加しました。
|
1 |
using UnityEngine.SceneManagement; |
これは、シーンの読み込み・取得・変更などを行うUnityの機能であるSceneManagerを使うために必要なものです。
次に、スクリプトの一番下にこちらの関数を追加しました。
|
1 2 3 4 |
public void ReplayGame() { SceneManager.LoadScene(SceneManager.GetActiveScene().name); } |
このReplayGame関数が呼び出されると、現在のシーンを再ロードします。中身を順番に見ていきましょう。
|
1 |
SceneManager.LoadScene |
これは、指定したシーンを読み込む処理です。
今遊んでいるシーンをもう一度読み込みたいので現在のシーン名を指定します。
現在のシーンを取得しているのが以下の部分です。
|
1 |
SceneManager.GetActiveScene() |
GetActiveScene()は、現在アクティブになっているシーンを取得する処理です。
ここでいうアクティブなシーンとは、今ゲームで使われているシーンのことです。
ただし、LoadSceneでは、シーンそのものではなくシーン名を指定する必要があります。
そこで、最後に.nameを付けています。
|
1 |
SceneManager.GetActiveScene().name |
これで、現在のシーン名を取得できます。つまり、全体としては、
|
1 |
SceneManager.LoadScene(SceneManager.GetActiveScene().name); |
で、「今開いているシーンと同じ名前のシーンを、もう一度読み込む」という意味になります。
これでシーンが最初の状態に戻ります。
プレイヤーの位置、箱の位置、GoalUIの表示、クリア状態などもシーン開始時の状態に戻るので、もう一度遊べるようになります。
では、このReplayGame()はどこから呼び出されるのでしょうか?
ここからUnityエディタでReplayGame()を呼び出す設定を行います。
ボタンにReplayGameを設定しよう
スクリプトを修正できたら、Ctrl + Sで保存してUnityに戻りましょう。Unityに戻ったら、HierarchyからReplayButtonを選択します。

InspectorにあるButtonコンポーネントを確認しましょう。その中に、On Click()という項目があります。これは、そのボタンがクリックされたときに実行する処理を設定する場所です。

まず、On Click()の右下にある+ボタンをクリックしましょう。

すると、新しい設定欄が追加されます。None (Object)と書かれている欄に、HierarchyにあるPlayerをドラッグ&ドロップします。

これで、このボタンからPlayerについているスクリプトを呼び出せるようになります。
次に、右側にある関数選択のメニューをクリックします。
その中からPlayerController2D > ReplayGame()を選択しましょう。

これでReplayButtonをクリックしたときに、ReplayGame()が呼び出されるようになります。
ここまでできたら、再生して確認してみましょう。
プレイヤーを動かしたり、箱を押したりしてシーンの状態を変えてみます。
その後でRetryボタンを押してみましょう。

プレイヤーや箱の位置が最初の状態に戻れば成功です。
また、クリアしたあとにRetryボタンを押してもう一度最初から遊べるかも確認しておきましょう。
これでシーンをリロードして再プレイできるようになりました。
応用編!複数Box・複数Goalに対応しよう
ここからは応用編です。これまでは、箱が1つ、Goalも1つある前提で作ってきました。
しかし、倉庫番では箱やGoalが複数あるステージもよくあります。
そこで、ここからは箱とGoalがシーン上に何個あっても対応できるようにしていきましょう。
ただし、「箱が2個ならこの処理」、「箱が3個ならこの処理」のように、数を決め打ちする方法では作りません。
箱が何個あっても、すべての箱がGoalに乗っているかを自動で調べる処理を作ります。
ちゃんとどんな状況でも対応できるように処理を一般化する形で作るわけですね。
GoalとBoxを増やそう
まずは、シーン上にGoalとBoxを増やしてみましょう。HierarchyでGoalを選択し、Ctrl + Dで複製します。

複製したGoalを選択して、Inspectorから位置を調整しましょう。

次に、HierarchyでBoxを選択し、こちらもCtrl + Dで複製します。

複製したBoxを選択して、Inspectorから位置を調整しましょう。

自分でマップを作成している場合は、必ずクリアできる位置にBoxとGoalを配置しましょう。Boxを壁際に置きすぎたり、Goalに届かない場所に置いたりすると、ゲームをクリアできなくなってしまいます。
AI研究の最前線の題材にもなっているなんてとても奥深いゲームですね。
「クリア可能性の担保」と「人間にとっての面白さ(難易度)の調整」の両立を自動生成で行うのは今のLLMや強化学習アプローチを用いても難しいのが現状です。
スクリプトから制御しよう
次に、複数のBoxとGoalに対応できるように、クリア判定を変更していきます。
ProjectウィンドウからAssets > ScriptsフォルダにあるPlayerController2Dをダブルクリックで開きましょう。
これまでのクリア判定では、箱を押したあとに次のように書いていました。
|
1 2 3 4 5 6 |
if (IsBoxOnGoal(nextBoxPosition)) { isGameCleared = true; Debug.Log("Clear!"); audioSource.PlayOneShot(clearSound); } |
これは、今押した箱がGoalに乗ったかどうかだけを調べる処理です。
箱が1つだけならこの方法でも問題ありません。
しかし、箱が複数ある場合は1つの箱がGoalに乗っただけではまだクリアできません。
すべてのBoxがGoalに乗ったときクリアにしたいので、次のように変更します。
|
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 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 |
using UnityEngine; using UnityEngine.Audio; using UnityEngine.SceneManagement; public class PlayerController2D : MonoBehaviour { public bool isGameCleared = false; [Header("UI")] public GameObject goalUI; [Header("Sound Effects")] public AudioClip moveSound; public AudioClip blockedSound; public AudioClip pushBoxSound; public AudioClip clearSound; private AudioSource audioSource; [Header("Player Sprites")] public Sprite upSprite; public Sprite downSprite; private SpriteRenderer spriteRenderer; private Sprite defaultSprite; // Start is called once before the first execution of Update after the MonoBehaviour is created void Start() { audioSource = GetComponent<AudioSource>(); spriteRenderer = GetComponent<SpriteRenderer>(); defaultSprite = spriteRenderer.sprite; } // Update is called once per frame void Update() { if (isGameCleared) { goalUI.SetActive(true); return; } if (Input.GetKeyDown(KeyCode.UpArrow)) { spriteRenderer.sprite = upSprite; spriteRenderer.flipX = false; float nextPlayerX = transform.position.x; float nextPlayerY = transform.position.y + 1; Vector2 nextPlayerPosition = new Vector2(nextPlayerX, nextPlayerY); if (IsWall(nextPlayerPosition)) { audioSource.PlayOneShot(blockedSound); return; } GameObject boxObject = GetBox(nextPlayerPosition); if (boxObject != null) { float nextBoxX = boxObject.transform.position.x; float nextBoxY = boxObject.transform.position.y + 1; Vector2 nextBoxPosition = new Vector2(nextBoxX, nextBoxY); if (IsWall(nextBoxPosition)) { audioSource.PlayOneShot(blockedSound); return; } boxObject.transform.position = nextBoxPosition; transform.position = nextPlayerPosition; if (AreAllBoxesOnGoals()) { isGameCleared = true; Debug.Log("Clear!"); audioSource.PlayOneShot(clearSound); } audioSource.PlayOneShot(pushBoxSound); return; } transform.position = nextPlayerPosition; audioSource.PlayOneShot(moveSound); } if (Input.GetKeyDown(KeyCode.DownArrow)) { spriteRenderer.sprite = downSprite; spriteRenderer.flipX = false; float nextPlayerX = transform.position.x; float nextPlayerY = transform.position.y - 1; Vector2 nextPlayerPosition = new Vector2(nextPlayerX, nextPlayerY); if (IsWall(nextPlayerPosition)) { audioSource.PlayOneShot(blockedSound); return; } GameObject boxObject = GetBox(nextPlayerPosition); if (boxObject != null) { float nextBoxX = boxObject.transform.position.x; float nextBoxY = boxObject.transform.position.y - 1; Vector2 nextBoxPosition = new Vector2(nextBoxX, nextBoxY); if (IsWall(nextBoxPosition)) { audioSource.PlayOneShot(blockedSound); return; } boxObject.transform.position = nextBoxPosition; transform.position = nextPlayerPosition; if (AreAllBoxesOnGoals()) { isGameCleared = true; Debug.Log("Clear!"); audioSource.PlayOneShot(clearSound); } audioSource.PlayOneShot(pushBoxSound); return; } transform.position = nextPlayerPosition; audioSource.PlayOneShot(moveSound); } if (Input.GetKeyDown(KeyCode.RightArrow)) { spriteRenderer.sprite = defaultSprite; spriteRenderer.flipX = false; float nextPlayerX = transform.position.x + 1; float nextPlayerY = transform.position.y; Vector2 nextPlayerPosition = new Vector2(nextPlayerX, nextPlayerY); if (IsWall(nextPlayerPosition)) { audioSource.PlayOneShot(blockedSound); return; } GameObject boxObject = GetBox(nextPlayerPosition); if (boxObject != null) { float nextBoxX = boxObject.transform.position.x + 1; float nextBoxY = boxObject.transform.position.y; Vector2 nextBoxPosition = new Vector2(nextBoxX, nextBoxY); if (IsWall(nextBoxPosition)) { audioSource.PlayOneShot(blockedSound); return; } boxObject.transform.position = nextBoxPosition; transform.position = nextPlayerPosition; if (AreAllBoxesOnGoals()) { isGameCleared = true; Debug.Log("Clear!"); audioSource.PlayOneShot(clearSound); } audioSource.PlayOneShot(pushBoxSound); return; } transform.position = nextPlayerPosition; audioSource.PlayOneShot(moveSound); } if (Input.GetKeyDown(KeyCode.LeftArrow)) { spriteRenderer.sprite = defaultSprite; spriteRenderer.flipX = true; float nextPlayerX = transform.position.x - 1; float nextPlayerY = transform.position.y; Vector2 nextPlayerPosition = new Vector2(nextPlayerX, nextPlayerY); if (IsWall(nextPlayerPosition)) { audioSource.PlayOneShot(blockedSound); return; } GameObject boxObject = GetBox(nextPlayerPosition); if (boxObject != null) { float nextBoxX = boxObject.transform.position.x - 1; float nextBoxY = boxObject.transform.position.y; Vector2 nextBoxPosition = new Vector2(nextBoxX, nextBoxY); if (IsWall(nextBoxPosition)) { audioSource.PlayOneShot(blockedSound); return; } boxObject.transform.position = nextBoxPosition; transform.position = nextPlayerPosition; if (AreAllBoxesOnGoals()) { isGameCleared = true; Debug.Log("Clear!"); audioSource.PlayOneShot(clearSound); } audioSource.PlayOneShot(pushBoxSound); return; } transform.position = nextPlayerPosition; audioSource.PlayOneShot(moveSound); } } private bool IsWall(Vector2 checkPosition) { Collider2D[] hitColliders = Physics2D.OverlapPointAll(checkPosition); for (int i = 0; i < hitColliders.Length; i++) { if (hitColliders[i].CompareTag("Wall")) { return true; } } return false; } private GameObject GetBox(Vector2 checkPosition) { Collider2D[] hitColliders = Physics2D.OverlapPointAll(checkPosition); for (int i = 0; i < hitColliders.Length; i++) { if (hitColliders[i].CompareTag("Box")) { return hitColliders[i].gameObject; } } return null; } private bool AreAllBoxesOnGoals() { GameObject[] boxes = GameObject.FindGameObjectsWithTag("Box"); Vector2[] boxPositions = new Vector2[boxes.Length]; for (int i = 0; i < boxes.Length; i++) { boxPositions[i] = boxes[i].transform.position; } for (int i = 0; i < boxPositions.Length; i++) { if (!IsBoxOnGoal(boxPositions[i])) { return false; } } return true; } private bool IsBoxOnGoal(Vector2 boxPosition) { Collider2D[] hitColliders = Physics2D.OverlapPointAll(boxPosition); for (int i = 0; i < hitColliders.Length; i++) { if (hitColliders[i].CompareTag("Goal")) { return true; } } return false; } public void ReplayGame() { SceneManager.LoadScene(SceneManager.GetActiveScene().name); } } |
IsBoxOnGoalではなく、AreAllBoxesOnGoalsという関数を使うようにします。
|
1 2 3 4 5 6 |
if (AreAllBoxesOnGoals()) { isGameCleared = true; Debug.Log("Clear!"); audioSource.PlayOneShot(clearSound); } |
AreAllBoxesOnGoalsは、「すべてのBoxがGoalに乗っているか」を調べるための関数です。
その関数が以下の部分です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
private bool AreAllBoxesOnGoals() { GameObject[] boxes = GameObject.FindGameObjectsWithTag("Box"); Vector2[] boxPositions = new Vector2[boxes.Length]; for (int i = 0; i < boxes.Length; i++) { boxPositions[i] = boxes[i].transform.position; } for (int i = 0; i < boxPositions.Length; i++) { if (!IsBoxOnGoal(boxPositions[i])) { return false; } } return true; } |
この関数が何をしているのかを順番に見ていきましょう。
まずはこちらです。
|
1 |
GameObject[] boxes = GameObject.FindGameObjectsWithTag("Box"); |
これは、シーン上にあるBoxタグの付いたオブジェクトをすべて取得しています。
箱が1個なら1個分、箱が2個なら2個分、箱が10個なら10個分取得されます。
そして、取得した箱はboxesという配列に入ります。
配列というのは同じ種類のデータをまとめて入れておく箱のようなものです。
今回はシーン上にあるBoxたちをまとめてboxesに入れています。
次にこちらです。
|
1 |
for (int i = 0; i < boxes.Length; i++) |
取得したBoxを1つずつ確認するためのfor文です。boxes.Lengthには、取得したBoxの数が入っています。
たとえばBoxが2個あれば、boxes.Lengthは2になります。Boxが5個あれば、boxes.Lengthは5になります。
つまり、このfor文ではシーン上にあるBoxの数だけ処理を繰り返しています。
次にこちらです。
|
1 2 3 4 |
for (int i = 0; i < boxes.Length; i++) { boxPositions[i] = boxes[i].transform.position; } |
今確認しているBoxの位置を取得しています。boxes[i]には、今チェックしているBoxが入っています。
そのBoxのtransform.positionを見ることで、その箱が今どこにあるのかを取得できます。その位置をboxPositionsに入れています。
次にこちらです。
|
1 2 3 4 5 6 7 |
for (int i = 0; i < boxPositions.Length; i++) { if (!IsBoxOnGoal(boxPositions[i])) { return false; } } |
ここがとても大事です。IsBoxOnGoal(boxPositions[i])は、その箱がGoalの上に乗っているかどうかを調べる関数です。Goalの上に乗っていればtrue、乗っていなければfalseになります。
もし1つでもGoalに乗っていないBoxがあった場合、まだクリアではありません。
|
1 |
return false; |
そのため、このreturn文のfalseでゲームクリアできてないという情報を返しています。
最後にこちらです。
|
1 |
return true; |
これは、すべてのBoxを調べ終わった後に実行されます。for文の中で1つでもGoalに乗っていないBoxがあれば、途中でreturn false;されます。
つまり、この最後のreturn true;までたどり着いたということは、すべてのBoxがGoalに乗っていたということです。
だからここでtrueを返します。
流れをまとめると、次のようになります。
- シーン上のBoxをすべて取得する
- Boxの場所を取得する
- そのBoxがGoalに乗っているか調べる
- 1つでもGoalに乗っていないBoxがあれば、falseを返して終了(まだクリアではない)
- すべてのBoxがGoalに乗っている→trueを返して終了(ゲームクリア)
こうした処理にすることでBoxとGoalが何個あっても対応できるようになります。
スクリプトを確認しよう
ここまでできたら、Ctrl + Sで保存してUnityに戻りましょう。Unityに戻ったら、再生して確認します。BoxをそれぞれGoalの上まで動かしてみましょう。
1つのBoxだけをGoalに乗せてもまだクリアにならないことを確認してください。

すべてのBoxをGoalに乗せたときに、GOAL!!が表示され、クリア音が鳴り、操作が止まれば成功です。
これで、複数Box・複数Goalに対応できるようになりました。
ここまでで、複数のBoxと複数のGoalに対応できるようになりました。ただし、実はここには1つ抜け穴があります。
試しに箱を別の箱に向かって押してみましょう。

すると、なんと箱同士が重なって、1つになったように見えてしまいます。
これまでは箱の奥に壁があったときは押せないようにしていましたが、箱の奥に別の箱があったときの対策をまだ入れていなかったためです。
このバグは修正しないといけないですね。
箱が奥にあった時、箱を押せないようにしよう
ProjectウィンドウからAssets > ScriptsフォルダにあるPlayerController2Dをダブルクリックします。
箱を押す処理の中にハイライトになっている処理を追加します。
|
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 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 |
using UnityEngine; using UnityEngine.Audio; using UnityEngine.SceneManagement; public class PlayerController2D : MonoBehaviour { public bool isGameCleared = false; [Header("UI")] public GameObject goalUI; [Header("Sound Effects")] public AudioClip moveSound; public AudioClip blockedSound; public AudioClip pushBoxSound; public AudioClip clearSound; private AudioSource audioSource; [Header("Player Sprites")] public Sprite upSprite; public Sprite downSprite; private SpriteRenderer spriteRenderer; private Sprite defaultSprite; // Start is called once before the first execution of Update after the MonoBehaviour is created void Start() { audioSource = GetComponent<AudioSource>(); spriteRenderer = GetComponent<SpriteRenderer>(); defaultSprite = spriteRenderer.sprite; } // Update is called once per frame void Update() { if (isGameCleared) { goalUI.SetActive(true); return; } if (Input.GetKeyDown(KeyCode.UpArrow)) { spriteRenderer.sprite = upSprite; spriteRenderer.flipX = false; float nextPlayerX = transform.position.x; float nextPlayerY = transform.position.y + 1; Vector2 nextPlayerPosition = new Vector2(nextPlayerX, nextPlayerY); if (IsWall(nextPlayerPosition)) { audioSource.PlayOneShot(blockedSound); return; } GameObject boxObject = GetBox(nextPlayerPosition); if (boxObject != null) { float nextBoxX = boxObject.transform.position.x; float nextBoxY = boxObject.transform.position.y + 1; Vector2 nextBoxPosition = new Vector2(nextBoxX, nextBoxY); if (IsWall(nextBoxPosition)) { audioSource.PlayOneShot(blockedSound); return; } if (GetBox(nextBoxPosition) != null) { audioSource.PlayOneShot(blockedSound); return; } boxObject.transform.position = nextBoxPosition; transform.position = nextPlayerPosition; if (AreAllBoxesOnGoals()) { isGameCleared = true; Debug.Log("Clear!"); audioSource.PlayOneShot(clearSound); } audioSource.PlayOneShot(pushBoxSound); return; } transform.position = nextPlayerPosition; audioSource.PlayOneShot(moveSound); } if (Input.GetKeyDown(KeyCode.DownArrow)) { spriteRenderer.sprite = downSprite; spriteRenderer.flipX = false; float nextPlayerX = transform.position.x; float nextPlayerY = transform.position.y - 1; Vector2 nextPlayerPosition = new Vector2(nextPlayerX, nextPlayerY); if (IsWall(nextPlayerPosition)) { audioSource.PlayOneShot(blockedSound); return; } GameObject boxObject = GetBox(nextPlayerPosition); if (boxObject != null) { float nextBoxX = boxObject.transform.position.x; float nextBoxY = boxObject.transform.position.y - 1; Vector2 nextBoxPosition = new Vector2(nextBoxX, nextBoxY); if (IsWall(nextBoxPosition)) { audioSource.PlayOneShot(blockedSound); return; } if (GetBox(nextBoxPosition) != null) { audioSource.PlayOneShot(blockedSound); return; } boxObject.transform.position = nextBoxPosition; transform.position = nextPlayerPosition; if (AreAllBoxesOnGoals()) { isGameCleared = true; Debug.Log("Clear!"); audioSource.PlayOneShot(clearSound); } audioSource.PlayOneShot(pushBoxSound); return; } transform.position = nextPlayerPosition; audioSource.PlayOneShot(moveSound); } if (Input.GetKeyDown(KeyCode.RightArrow)) { spriteRenderer.sprite = defaultSprite; spriteRenderer.flipX = false; float nextPlayerX = transform.position.x + 1; float nextPlayerY = transform.position.y; Vector2 nextPlayerPosition = new Vector2(nextPlayerX, nextPlayerY); if (IsWall(nextPlayerPosition)) { audioSource.PlayOneShot(blockedSound); return; } GameObject boxObject = GetBox(nextPlayerPosition); if (boxObject != null) { float nextBoxX = boxObject.transform.position.x + 1; float nextBoxY = boxObject.transform.position.y; Vector2 nextBoxPosition = new Vector2(nextBoxX, nextBoxY); if (IsWall(nextBoxPosition)) { audioSource.PlayOneShot(blockedSound); return; } if (GetBox(nextBoxPosition) != null) { audioSource.PlayOneShot(blockedSound); return; } boxObject.transform.position = nextBoxPosition; transform.position = nextPlayerPosition; if (AreAllBoxesOnGoals()) { isGameCleared = true; Debug.Log("Clear!"); audioSource.PlayOneShot(clearSound); } audioSource.PlayOneShot(pushBoxSound); return; } transform.position = nextPlayerPosition; audioSource.PlayOneShot(moveSound); } if (Input.GetKeyDown(KeyCode.LeftArrow)) { spriteRenderer.sprite = defaultSprite; spriteRenderer.flipX = true; float nextPlayerX = transform.position.x - 1; float nextPlayerY = transform.position.y; Vector2 nextPlayerPosition = new Vector2(nextPlayerX, nextPlayerY); if (IsWall(nextPlayerPosition)) { audioSource.PlayOneShot(blockedSound); return; } GameObject boxObject = GetBox(nextPlayerPosition); if (boxObject != null) { float nextBoxX = boxObject.transform.position.x - 1; float nextBoxY = boxObject.transform.position.y; Vector2 nextBoxPosition = new Vector2(nextBoxX, nextBoxY); if (IsWall(nextBoxPosition)) { audioSource.PlayOneShot(blockedSound); return; } if (GetBox(nextBoxPosition) != null) { audioSource.PlayOneShot(blockedSound); return; } boxObject.transform.position = nextBoxPosition; transform.position = nextPlayerPosition; if (AreAllBoxesOnGoals()) { isGameCleared = true; Debug.Log("Clear!"); audioSource.PlayOneShot(clearSound); } audioSource.PlayOneShot(pushBoxSound); return; } transform.position = nextPlayerPosition; audioSource.PlayOneShot(moveSound); } } private bool IsWall(Vector2 checkPosition) { Collider2D[] hitColliders = Physics2D.OverlapPointAll(checkPosition); for (int i = 0; i < hitColliders.Length; i++) { if (hitColliders[i].CompareTag("Wall")) { return true; } } return false; } private GameObject GetBox(Vector2 checkPosition) { Collider2D[] hitColliders = Physics2D.OverlapPointAll(checkPosition); for (int i = 0; i < hitColliders.Length; i++) { if (hitColliders[i].CompareTag("Box")) { return hitColliders[i].gameObject; } } return null; } private bool AreAllBoxesOnGoals() { GameObject[] boxes = GameObject.FindGameObjectsWithTag("Box"); Vector2[] boxPositions = new Vector2[boxes.Length]; for (int i = 0; i < boxes.Length; i++) { boxPositions[i] = boxes[i].transform.position; } for (int i = 0; i < boxPositions.Length; i++) { if (!IsBoxOnGoal(boxPositions[i])) { return false; } } return true; } private bool IsBoxOnGoal(Vector2 boxPosition) { Collider2D[] hitColliders = Physics2D.OverlapPointAll(boxPosition); for (int i = 0; i < hitColliders.Length; i++) { if (hitColliders[i].CompareTag("Goal")) { return true; } } return false; } public void ReplayGame() { SceneManager.LoadScene(SceneManager.GetActiveScene().name); } } |
今回追加したのはこちらです。
|
1 2 3 4 5 |
if (GetBox(nextBoxPosition) != null) { audioSource.PlayOneShot(blockedSound); return; } |
箱の奥に壁があったときの処理と考え方はほとんど同じです。箱の奥に壁がある場合は、次のようにして押せないようにしていました。
|
1 2 3 4 5 |
if (IsWall(nextBoxPosition)) { audioSource.PlayOneShot(blockedSound); return; } |
この処理は、箱の移動先に壁があるかどうかを調べ、壁があればブロック音を鳴らして処理を終了するというものでした。
|
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 |
if (boxObject != null) { float nextBoxX = boxObject.transform.position.x; float nextBoxY = boxObject.transform.position.y + 1; Vector2 nextBoxPosition = new Vector2(nextBoxX, nextBoxY); if (IsWall(nextBoxPosition)) { audioSource.PlayOneShot(blockedSound); return; } if (GetBox(nextBoxPosition) != null) { audioSource.PlayOneShot(blockedSound); return; } boxObject.transform.position = nextBoxPosition; transform.position = nextPlayerPosition; if (AreAllBoxesOnGoals()) { isGameCleared = true; Debug.Log("Clear!"); audioSource.PlayOneShot(clearSound); } audioSource.PlayOneShot(pushBoxSound); return; } |
今回もやっていることは同じです。ただし、調べる対象が壁ではなく箱になっています。
|
1 |
GetBox(nextBoxPosition) |
この処理で、箱の移動先に別の箱があるかどうかを調べています。もし箱の移動先に別の箱があった場合、GetBox(nextBoxPosition)はその箱のゲームオブジェクトを返します。
つまり、
|
1 |
if (GetBox(nextBoxPosition) != null) |
と書くことで、「箱の移動先に別の箱がある場合」という条件になります。
その場合は、
|
1 2 |
audioSource.PlayOneShot(blockedSound); return; |
こちらでブロック音を鳴らして処理を終了します。ここでreturnすることで、その下にある箱を移動させる処理まで進まなくなります。
スクリプトを確認しよう
ここまでできたら、Ctrl + Sで保存してUnityに戻りましょう。Unityに戻ったら、再生して確認します。箱を別の箱に向かって押してみましょう。

箱同士が重ならず、ブロック音が鳴って動かなければ成功です。
これで箱の奥に壁がある場合だけでなく、箱の奥に別の箱がある場合も押せなくなりました。
今回のプロジェクトファイル
ここまでの操作を行ったプロジェクトファイルを用意しました。
最後に

これで倉庫番ゲームの講座は終了です。ここまでお疲れさまでした。
今回は、Unityを使って2Dの倉庫番ミニゲームを作成していきました。
プレイヤーを動かすところから始まり、壁にぶつかったら止まる処理、箱を押す処理、箱の奥に壁や箱があるときは押せない処理、Goalによるクリア判定、クリア表示、リトライボタン、そして複数Box・複数Goalへの対応まで実装しました。
ここまで作ることができれば、倉庫番ゲームとしての基本的な仕組みはかなり完成に近いと思います。
もちろん、まだまだ改良の余地はあります。
例えば、ステージを複数作ったり、手数をカウントしたり、クリアタイムを表示したり、クリア演出を追加したりすることもできます。
今回学んだことを活かせばそういった機能も少しずつ実装できるようになるはずです。
今回の実装をいきなり
”プレイヤーが箱を押す、箱の奥に壁があれば押せない、箱の奥に別の箱があれば押せない、すべての箱がGoalに乗ったらクリア・・・”
このように聞くと、最初はとても難しい処理に感じるかもしれません。
開発に慣れている人でも、一気に全部を考えようとすると頭が痛くなってしまいます。
だからこそ、丁寧に一つずつ実装していくことが大切です。
- まずはマップを用意する。
- 次にプレイヤーを動かせるようにする。
- その次に壁にぶつかったら止まるようにする。
- さらに箱を押せるようにする。
- そのあとで箱の奥に壁や箱がある場合は押せないようにする。
このように一つずつ分けて作っていくと、最初は難しそうに見えた処理も意外とシンプルに考えられるようになります。
今回の倉庫番も特別に難しいアルゴリズムを使っているわけではありません。やっていることは、かなりシンプルです。
大切なのは一気に完成させようとしないことです。
やりたいことを小さく分けて、ひとつずつ確認しながら作っていくこと。それを意識するだけで、作れるものは大きく変わっていきます。
今後の開発でも、ぜひこの「丁寧に分けて考える」ということを意識してみてください。
今回完成させた倉庫番ゲームは、ゲーム開発で最も重要とされる「条件分岐」「コンポーネントの制御」「データの管理」の基礎がすべて詰まっています。
ここで得たスキルと「丁寧に分けて考える」方法は今後あなたがどんなゲームを作る際にも必ず強力な武器になってくれるはずです。
次はさらに本格的なゲーム制作に挑戦していきましょう!
今回でUnityに多少慣れてきたかと思うので
なども次のステップにお勧めです。
ここまで読んでいただきありがとうございました。これからもUnity入門の森でゲーム制作を楽しんでいきましょう!もくもくUnity!
現場レベルのゲーム制作が、すべてここで学べます。





コメント