現場レベルのゲーム制作が、すべてここで学べます。
倉庫番ゲーム制作講座の第4回となる今回はいよいよゲームの核となる「箱を押す処理」と「ゴールによるクリア判定」を実装していきます!
前回はプレイヤーを動かして壁で止める仕組みを作りましたが、今の状態ではまだ箱にぶつかっても素通りするか動かせないままです。
前回の記事:

今回は倉庫番らしい動きを作るために次の内容を実装していきます。
- 箱を押す処理を作る
- 箱を押せない条件を作る
- Goalを作ってクリア判定を作る
- 箱がゴールに乗った瞬間に歓声が響く「クリア判定」を作る
前回学んだ「自作関数」や「当たり判定(Collider)」の知識を応用しながら、実際にゲームとして楽しく遊べる形へと仕上げていきましょう!
箱を押せるようにしよう
まずは、箱を押せるようにするにあたって、どのような流れで処理を作るのかを確認しておきましょう。
倉庫番では、プレイヤーが箱に向かって移動したとき、箱も同じ方向に1マス動きます。そのためには、まず任意の矢印キーが押されたときに、プレイヤーの移動先に箱があるかどうかを調べる必要があります。

これは前編で作成した、壁があるかどうかを調べる処理と考え方はほとんど同じです。前編では、壁があるかどうかを調べるために、次のような関数を作成しました。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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; } |
この処理では、指定した場所にあるCollider2Dを取得し、その中にWallタグが付いているオブジェクトがあるかどうかを調べています。箱の場合も、考え方は同じです。違うのは、調べるタグ名がWallではなくBoxになることです。
ただし、壁と箱では処理の内容が少し違います。
壁の場合は、移動先に壁があれば、その時点でプレイヤーを動かさずに処理を終了すればよいだけでした。しかし箱の場合は、移動先に箱があったときに、箱を動かす処理とプレイヤーを動かす処理の両方が必要になります。
また、今回は箱を押したときの音も鳴らせるようにしてみましょう。
スクリプトから実装してみよう
まずは、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 |
using UnityEngine; using UnityEngine.Audio; public class PlayerController2D : MonoBehaviour { [Header("Sound Effects")] private AudioSource audioSource; public AudioClip moveSound; public AudioClip blockedSound; public AudioClip pushBoxSound; [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 (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); boxObject.transform.position = nextBoxPosition; transform.position = nextPlayerPosition; 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); boxObject.transform.position = nextBoxPosition; transform.position = nextPlayerPosition; 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); boxObject.transform.position = nextBoxPosition; transform.position = nextPlayerPosition; 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); boxObject.transform.position = nextBoxPosition; transform.position = nextPlayerPosition; 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; } } |
まず、箱を押したときに鳴らす効果音用のAudioClipを追加しました。
|
1 |
public AudioClip pushBoxSound; |
前編では、プレイヤーが移動したときの音と、壁にぶつかったときの音を用意しました。今回はそれに加えて、箱を押したとき専用の音を用意しています(実際の音声データの紐づけは後ほどUnityエディタで行います)。
次に、それぞれの移動処理の中に、箱があるかどうかを確認する処理を追加しました。上下左右で考え方は同じなので、ここでは上に移動する場合を例に確認していきます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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); boxObject.transform.position = nextBoxPosition; transform.position = nextPlayerPosition; audioSource.PlayOneShot(pushBoxSound); return; } |
こちらが、実際に箱を移動させている処理です。まず、次の部分を見てみましょう。
|
1 |
GameObject boxObject = GetBox(nextPlayerPosition); |
これは、プレイヤーが次に移動しようとしている場所に、箱があるかどうかを調べています。もし箱が見つかった場合、その箱のゲームオブジェクトがboxObjectに入ります。GetBox(nextPlayerPosition)は、前編で作成したIsWallとかなり似た処理です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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; } |
基本的な流れはIsWallと同じです。指定した位置にあるCollider2Dを調べて、その中にBoxタグが付いているオブジェクトがあるかどうかを確認しています。
ただし、IsWallとは戻り値が違います。
IsWallの場合は、壁があるかどうかだけ分かればよかったので、trueかfalseを返していました。
|
1 |
if (IsWall(nextPlayerPosition)) |
このように、if文の条件として使っていました。
一方で、今回は箱が見つかった場合、その箱自体を動かす必要があります。そのため、ただ「箱がある・ない」を返すだけではなく、見つけた箱のゲームオブジェクトを返しています。
|
1 |
return hitColliders[i].gameObject; |
そして、箱が見つからなかった場合は、
|
1 |
return null; |
を返しています。
nullやif文の書き方を確認しよう
ここで、今回出てきたnullやif文の書き方について少し補足しておきます。
まず、次の処理を見てみましょう。
|
1 |
if (boxObject != null) |
nullとは、「何も入っていない」という意味です。今回の処理では、プレイヤーの移動先に箱があれば、boxObjectに箱のゲームオブジェクトが入ります。反対に、移動先に箱がなかった場合、boxObjectには何も入りません。この「何も入っていない状態」がnullです。
|
1 |
if (boxObject != null) |
なのでこちらは、boxObjectがnullではない、つまり、箱が見つかった場合という意味になります。!=は「等しくない」という意味です。反対に、もし
|
1 |
if (boxObject == null) |
と書いた場合は、「boxObjectがnullであるなら」という意味になります。
==は「同じかどうか」を調べるときに使います。
!=は「同じではないかどうか」を調べるときに使います。
今回の処理では、箱が見つかったときだけ箱を動かしたいので、
|
1 |
if (boxObject != null) |
と書いています。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
if (boxObject != null) { float nextBoxX = boxObject.transform.position.x; float nextBoxY = boxObject.transform.position.y + 1; Vector2 nextBoxPosition = new Vector2(nextBoxX, nextBoxY); boxObject.transform.position = nextBoxPosition; transform.position = nextPlayerPosition; audioSource.PlayOneShot(pushBoxSound); return; } |
次に、if文の中身を見ていきましょう。
|
1 2 3 4 |
float nextBoxX = boxObject.transform.position.x; float nextBoxY = boxObject.transform.position.y + 1; Vector2 nextBoxPosition = new Vector2(nextBoxX, nextBoxY); |
ここでは箱の移動先を作っています。考え方としてはプレイヤーの移動先を作ったときと同じです。
|
1 |
float nextBoxY = boxObject.transform.position.y + 1; |
上に移動する場合、箱も上に1マス動かしたいので、箱の現在位置のY座標に+ 1した座標を用意します。これが移動先の候補となる座標ですね。
|
1 |
Vector2 nextBoxPosition = new Vector2(nextBoxX, nextBoxY); |
そして、X座標とY座標をまとめて、nextBoxPositionという変数に保存しています。
このnextBoxPositionが箱の移動先になります。
次に、こちらの処理を見てみましょう。
|
1 2 3 4 5 |
boxObject.transform.position = nextBoxPosition; transform.position = nextPlayerPosition; audioSource.PlayOneShot(pushBoxSound); return; |
ここでは、箱とプレイヤーを実際に移動させています。
|
1 |
boxObject.transform.position = nextBoxPosition; |
この処理で、箱を1マス先に移動させています。
|
1 |
transform.position = nextPlayerPosition; |
この処理で、プレイヤーを箱が元々あった場所に移動させています。つまり、プレイヤーが箱を押して、箱と一緒に1マス進むような動きになります。
|
1 |
audioSource.PlayOneShot(pushBoxSound); |
箱を押したときの音を鳴らしています。
|
1 |
return; |
最後に、こちらで処理を終了しています。このreturnも重要です。if文の後には箱がなかった場合にプレイヤーだけを移動させる処理があります。
|
1 2 |
transform.position = nextPlayerPosition; audioSource.PlayOneShot(moveSound); |
もし箱を押したあとにこの処理まで実行されてしまうと、プレイヤーの移動処理が重複して2歩動く形になってしまいます。さらに、箱を押した音と通常の移動音が両方鳴ってしまいます。
そこで、箱を押す処理が終わったらreturnで処理を終了し、その下の通常移動処理に進まないようにしています。
このように考えると、箱を押す処理自体はとてもシンプルです。
- プレイヤーの移動先を調べる
- 移動先が壁なら動かない
- 移動先に箱があるか調べる
- 箱があれば、箱を1マス先に動かす
- プレイヤーも1マス動かす
- 箱を押した音を鳴らす
- 処理を終了する
このようなプログラムの流れになっています。
作成したスクリプトを試してみよう
スクリプトを書けたら、CTRL + Sで保存後、Unity側で箱の設定を行いましょう。今回のスクリプトでは、次の設定が必要です。
- Box Collider 2Dを付ける
- Boxタグを設定する
- 箱を押したときの効果音を設定する
まずは、箱を押したときに鳴らす効果音を用意します。今回も、以下のリンクから「本を閉じる2.mp3」をダウンロードしましょう。
ダウンロードできたら、ProjectウィンドウのAssets > Soundsフォルダに追加します。

次に、箱の設定をしていきます。ProjectウィンドウからAssets > Prefabsフォルダを開き、Boxを選択してダブルクリックして開きましょう。

Inspectorが表示されたら、Add Componentをクリックします。その中から、Physics 2D > Box Collider 2Dを選択して追加します。

これで、スクリプトから箱の位置を判定できるようになります。次に、Boxタグを設定します。Inspector上部にあるTagの項目をクリックします。

Add Tag…を選択します。

Tagsの一覧に移動したら、+ボタンを押して、新しくBoxというタグを作成しましょう。

タグを作成しただけでは、まだBoxには設定されていません。もう一度、Assets > PrefabsフォルダのBoxを選択。

InspectorのTagからBoxを選びます。

ここでタグ名の大文字・小文字に注意してください。スクリプトでは、
|
1 |
CompareTag("Box") |
と書いているので、タグ名も必ずBoxにします。
次に、プレイヤーのPlayerController2Dに、箱を押したときの音を設定します。HierarchyからPlayerを選択。

InspectorにあるPlayerController2Dを確認します。Push Box Soundの欄に、先ほど追加した「本を閉じる2.mp3」をドラッグ&ドロップしましょう。

ここまでできたら、一度再生して確認してみます。矢印キーでプレイヤーを動かし、箱に向かって移動してみましょう。

プレイヤーが箱を押して、箱が1マス動けば成功です。ただし、今の状態ではまだ問題があります。箱の奥に壁があっても、そのまま箱を押せてしまいます。
これでは箱が壁にめり込んでしまいます。
奥に壁がある時、箱を押せないようにしよう
前の章では、プレイヤーが箱を押せるようにしました。
ただし、今のままだと少し問題があります。箱の奥に壁があっても、そのまま箱を押せてしまうため、箱が壁にめり込んでしまいます。

倉庫番で、箱の奥に壁がある場合、その箱を押すことはできません。
そこで箱の移動先に壁がある場合、箱もプレイヤーも動かないようにしていきます。
考え方はとても簡単です。
前回、プレイヤーが壁にぶつかったときに止まる処理を作りました。そのときは、次のように書きました。
|
1 2 3 4 5 |
if (IsWall(nextPlayerPosition)) { audioSource.PlayOneShot(blockedSound); return; } |
これは、プレイヤーの移動先であるnextPlayerPositionに壁があるかどうかを調べる処理です。壁があった場合は、ブロック音を鳴らしてreturnで処理を終了しています。
箱の場合も考え方は同じです。
プレイヤーの移動先ではなく箱の移動先を調べればよいだけです。
つまり、nextPlayerPositionではなく、nextBoxPositionを使って、箱の移動先に壁があるかどうかを確認します。では、実際にスクリプトに追加してみましょう。
スクリプトを書いてみよう
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 |
using UnityEngine; using UnityEngine.Audio; public class PlayerController2D : MonoBehaviour { [Header("Sound Effects")] private AudioSource audioSource; public AudioClip moveSound; public AudioClip blockedSound; public AudioClip pushBoxSound; [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 (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; 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; 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; 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; 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; } } |
今回追加した処理は、次の部分です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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; audioSource.PlayOneShot(pushBoxSound); return; } |
この処理を、各方向の箱を押す処理の中に追加しました。たとえば、上に箱を押す場合は、まず箱の移動先を作っています。
|
1 2 3 4 |
float nextBoxX = boxObject.transform.position.x; float nextBoxY = boxObject.transform.position.y + 1; Vector2 nextBoxPosition = new Vector2(nextBoxX, nextBoxY); |
このnextBoxPositionが、箱を押したときに箱が移動する予定の場所です。そして、その場所に壁があるかどうかを調べています。
|
1 |
if (IsWall(nextBoxPosition)) |
もし箱の移動先に壁があった場合は、次の処理が実行されます。
|
1 2 3 4 5 |
if (IsWall(nextBoxPosition)) { audioSource.PlayOneShot(blockedSound); return; } |
まず、壁にぶつかったときと同じように、ブロック音を鳴らします。そしてreturnで処理を終了します。
ここで処理を終了することで、この下にある箱を移動させる処理が実行されなくなります。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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; audioSource.PlayOneShot(pushBoxSound); return; } |
つまり、箱の移動先に壁がある場合、箱もプレイヤーも動かないということです。
再びプログラムの流れを整理すると、次のようになっています。
- プレイヤーの移動先を調べる
- プレイヤーの移動先に壁があれば止まる
- プレイヤーの移動先に箱があるか調べる
- 箱があった場合、箱の移動先を作る
- 箱の移動先に壁があるか調べる
- 壁があれば、ブロック音を鳴らして処理を終了する
- 壁がなければ、箱とプレイヤーを移動させる
追加したのは短い処理ですが、これだけで箱が壁にめり込まなくなります。
スクリプトを確認しよう
スクリプトを追加できたら、Ctrl + Sで保存しましょう。保存できたらUnityに戻ります。今回は、Unity側で新しく設定する項目はありません。再生ボタンを押して、箱を壁に向かって押してみましょう。

箱の奥に壁があるとき、箱が動かず、壁にめり込まなければ成功です。
Goalを作ってクリア判定を作ろう
ここからはゲームをクリアできるようにしていきましょう。
今回は箱がGoalの上に乗ったらゲームクリアになる処理を作ります。
考え方はとてもシンプルです。前回、移動先に箱があるかどうかを調べるために、次のような関数を作りました。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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; } |
この関数では、指定した場所にあるオブジェクトを調べて、Boxタグが付いているオブジェクトがあれば、その箱を取得していました。
実は、Goalの判定もこれとかなり似た考え方で作ることができます。違うのは、調べるタグをBoxではなくGoalにすることです。
どういうことかというと、箱を押したあと、箱の移動先の位置を関数に渡します。
そして、その場所にGoalタグが付いたオブジェクトがあるかどうかを調べます。もし箱の移動先にGoalがあれば、箱がGoalの上に乗ったということになります。つまり、クリアです。
今回は、箱がGoalの上に乗ったら、次の処理を行うようにします。
- UnityエディタのConsole画面にClear!と表示する
- クリア音を再生する
- クリア後はプレイヤーを操作できないようにする
スクリプトから制御しよう
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 |
using UnityEngine; using UnityEngine.Audio; public class PlayerController2D : MonoBehaviour { public bool isGameCleared = false; [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) { 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 |
public bool isGameCleared = false; |
こちらは、ゲームをクリアしたかどうかを記録するための変数です。最初はまだクリアしていないので、falseにしています。箱がGoalに乗ってクリアしたら、この変数をtrueに変更します。
|
1 |
public AudioClip clearSound; |
こちらは、クリアしたときに鳴らす効果音です。
次に、Updateの先頭に追加した処理を見てみましょう。
|
1 2 3 4 |
if (isGameCleared) { return; } |
これは、ゲームクリアの場合にこれ以上処理を進めないようにするためのものです。
isGameClearedがtrueになってゲームをクリアすると、このif文の中に入ります。
そして、returnで処理を終了します。
この処理をUpdateの先頭に書いているので、クリア後はその下にある矢印キーの入力処理まで進まなくなります。
そのため、クリア後にプレイヤーを動かせなくなります。もしこの処理がないと、クリアしたあともプレイヤーが動けてしまいます。
ゲームとしては少し不自然なので今回はクリア後に操作を止めるようにしています。
次に、箱を押したあとに追加したこちらの処理を見てみましょう。
|
1 2 3 4 5 6 |
if (IsBoxOnGoal(nextBoxPosition)) { isGameCleared = true; Debug.Log("Clear!"); audioSource.PlayOneShot(clearSound); } |
まずは、if文の条件になっているこちらから確認します。
|
1 |
if (IsBoxOnGoal(nextBoxPosition)) |
ここでは、IsBoxOnGoalという関数を呼び出しています。
この関数には箱の移動先であるnextBoxPositionを渡しています。つまり、「箱が移動した先にGoalがあるかどうか」を調べているということです。
その関数が以下の部分です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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; } |
見てわかる通り、IsWallやGetBoxとやっていることはほとんど同じです。
まず、以下の処理で箱の移動先にあるCollider2Dをすべて取得しています。
|
1 |
Collider2D[] hitColliders = Physics2D.OverlapPointAll(boxPosition); |
その後、for文を使って取得したCollider2Dを1つずつ確認しています。
|
1 |
for (int i = 0; i < hitColliders.Length; i++) |
そして、その中にGoalタグが付いているオブジェクトがあるかどうかを調べています。
|
1 2 3 4 |
if (hitColliders[i].CompareTag("Goal")) { return true; } |
もしGoalタグが付いているオブジェクトが見つかった場合は、箱とGoalが重なったということになります。そのため、
|
1 |
return true; |
を返します。反対に、最後まで調べてもGoalタグが見つからなかった場合、Goalの上には乗っていないということです。その場合は、
|
1 |
return false; |
を返します。
では、もう一度こちらのif文に戻ってみましょう。
|
1 2 3 4 5 6 |
if (IsBoxOnGoal(nextBoxPosition)) { isGameCleared = true; Debug.Log("Clear!"); audioSource.PlayOneShot(clearSound); } |
IsBoxOnGoal(nextBoxPosition)がtrueだった場合、つまり箱がGoalの上に乗った場合、この中の処理が実行されます。
|
1 |
isGameCleared = true; |
まず、isGameClearedをTrueにしてゲームクリアしたことを記録します。これにより、次のフレームからUpdateの先頭にある処理で止まるようになります。
|
1 2 3 4 |
if (isGameCleared) { return; } |
つまり、クリア後はプレイヤーの操作ができなくなります。
|
1 |
Debug.Log("Clear!"); |
ここではConsoleにClear!と表示しています。クリア判定が正しく動いているか確認するためのものです。
|
1 |
audioSource.PlayOneShot(clearSound); |
最後にゲームクリア音を再生しています。箱をGoalに乗せたときにクリア音が鳴るようになります。
スクリプトを確認しよう
スクリプトを書けたら、Ctrl + Sで保存しましょう。保存できたらUnityに戻ります。
今回はUnity側で設定する項目が3つあります。
- GoalにBox Collider 2Dを追加する
- GoalにGoalタグを設定する
- Playerにクリア音を設定する
まずGoalにColliderを追加します。
ProjectウィンドウからAssets > Prefabsフォルダを開き、Goalを選択しましょう。

InspectorからAdd Componentをクリックします。その中から、Physics 2D > Box Collider 2Dを選択して追加します。

これで、スクリプトからGoalの位置を判定できるようになります。
次に、Goalタグを設定します。Inspector上部にあるTagをクリックします。

Add Tag…を選択します。

Tagsの一覧に移動したら、+ボタンを押して、新しくGoalというタグを作成しましょう。

タグを作成しただけでは、まだGoalには設定されていません。もう一度、Assets > PrefabsフォルダのGoalを選択。

InspectorのTagからGoalを選びます。

次に、クリア音を追加します。以下のリンクから「歓声と拍手1.mp3」をダウンロードしましょう。
ダウンロードできたら、ProjectウィンドウのAssets > Soundsフォルダにドラッグ&ドロップして追加します。

最後に、Playerを選択します。

InspectorにあるPlayerController2Dの中に、Clear Soundという項目が追加されています。

そこに、先ほど追加した「歓声と拍手1.mp3」をドラッグ&ドロップしましょう。

ここまでできたら、再生して確認します。箱を押してGoalの上に乗せてみましょう。


箱がGoalに乗ったときに、ConsoleにClear!と表示され、クリア音が鳴り、プレイヤーが操作できなくなれば成功です。
Goalの表示順を調整しよう
ここまで設定すると、箱をGoalに乗せてクリア判定ができるようになります。
ただし、実際に再生してみると少し気になる部分があると思います。
プレイヤーがGoalの上を通過したときや箱をGoalの上に重ねたときに、Goalがプレイヤーや箱の上に表示されてしまう場合があります。

倉庫番では、Goalは床に描かれている目印のようなものなので、プレイヤーや箱よりも後ろに表示されている方が自然です。そこで、Goalの表示順を下げて、プレイヤーや箱の後ろに表示されるようにしましょう。
ProjectウィンドウからAssets > Prefabsフォルダを開き、Goalを選択します。

InspectorにあるSprite Rendererを確認しましょう。その中にあるOrder in Layerを-1に設定します。

プレイヤー、壁、箱のOrder in Layerが0になっているので、Goalを-1にすることでGoalの方が後ろに表示されます。
これで箱やプレイヤーとGoalが重なったときにGoalが一番背面に表示されるようになります。
設定できたら、再生して確認してみましょう。

プレイヤーや箱がGoalの上に重なったとき、Goalが後ろに表示されていれば成功です。
今回はここまでです。
今回のプロジェクトファイル
ここまでの操作を行ったプロジェクトファイルを用意しました。
まとめ
今回は以下のような機能を実装しました。
-
箱を押す処理の追加: 新しい自作関数 GetBox を作成し、移動先に箱がある場合は「箱を1マス先へ」、「プレイヤーを箱のいた場所へ」同時に移動させ専用の効果音を鳴らしました。
-
「null」を使った条件分岐: 「箱があるかないか(何も入っていない状態=null)」を判別する != null の記述を学び、プログラムを制御しました。
-
奥の壁の押し込み防止: 箱の移動先に対しても IsWall を実行することで、箱が壁にめり込んでしまう現象を防ぎました。
-
ゴール判定とクリア処理: IsBoxOnGoal 関数によって箱とゴールの重なりを検知し、クリア時に操作をロックして歓声を鳴らすゲームループを作りました。
-
表示順(Order in Layer)の調整: ゴールがプレイヤーや箱の背面へ自然に隠れるよう、2Dレイヤーの描画優先度を整えました。
プレイヤーを動かし、箱を押し、ゴールへ運んでゲームがクリアとなる。これで倉庫番パズルゲームとしての最小限のルールと仕組みが、あなたの手で見事に完成しました!
自分で書いたコードによって、画像や音が連動して「ゲームのルール」に変わっていくおもしろさを体感できたのではないでしょうか。
今回で基本システムはバッチリ完成しました。
ですが、まだクリアした際の画面上での変化がわかりづらい点や箱やゴールを1つずつしか配置できない問題が残っています。
最終回となる次回はこれらの処理を実装し、どんな倉庫番ステージでも自由自在に作れるシステムにアップデートしていきましょう。
次の記事:

現場レベルのゲーム制作が、すべてここで学べます。






コメント