Unity 6でジャンプで避けるミニゲームを作ろう ― 障害物・スコア・リスタートで完成させる
はじめに
第1回で、動いてジャンプするPlayerを作り、第2回で、ゲームをWebやAndroidに出力する方法を確認しました。今回は、この2つを使って、ジャンプで障害物を避ける小さなゲームを完成させます。
教科書のゲームとは違い、題材も仕様も自分で決めたゲームです。「何を作るか決める → 部品に分ける → 1つずつ作る」という進め方も、あわせて練習しましょう。
シリーズ「Unityで作って、出力して、完成させる」(全3回)
- 第1回:動いてジャンプする(Input System + Rigidbody2D)
- 第2回:WebとAndroidに出力する(Build Profiles)
- 第3回:ジャンプで避けるミニゲームを完成させる(この記事)
今回作るゲーム
- 右から障害物が流れてくる
- スペースキーのジャンプで、障害物を避ける
- 障害物に当たったらゲームオーバー
- 生き残った秒数がスコアになる
- ゲームオーバー後に、スペースキーでリスタートできる

全体の構成
ゲームを、次の部品に分けて作ります。1つの部品に、1つの役割だけを持たせるのがコツです。
| 部品 | スクリプト | 役割 |
|---|---|---|
| Player | PlayerController | 移動とジャンプ(第1回)。障害物に触れたら監督に知らせる |
| Obstacle(Prefab) | ObstacleMover | 左へ動き、しばらくしたら消える |
| ObstacleSpawner | ObstacleSpawner | 一定の間隔で、障害物を出す |
| GameDirector | GameDirector | スコア、ゲームオーバー、リスタートを管理する監督 |
準備
① シーンを保存して、Scene List に登録する
リスタートでは、シーンを読み込み直します。読み込めるのは、Scene List に登録されたシーンだけです。第2回で確認した、Scene List が、ここで必要になります。
- 第1回で作ったシーンを開き、
GameSceneという名前で保存する - File → Build Profiles を開く
- Scene List の Add Open Scenes を押して、
GameSceneを登録する
② Ground を広げる
第1回の Ground は、横幅が短く、障害物が出てくる場所に地面がありません。Ground の Scale を (30, 1, 1) に広げて、画面の外まで届くようにします。
③ Obstacle タグを作る
- Hierarchyで何かのオブジェクトを選び、Inspector上部の Tag の欄を開く
- Add Tag を選び、
Obstacleというタグを追加する
手順1:障害物(Obstacle)を作る
- Square を作り、名前を
Obstacleにする。Scale を (0.8, 0.8, 1) にする - Sprite Renderer の Color を、赤など目立つ色にする
- Box Collider 2D を追加し、Is Trigger にチェックを入れる
- Rigidbody 2D を追加し、Body Type を Kinematic にする
- Tag を
Obstacleにする ObstacleMoverスクリプトを追加する(下のコード)- HierarchyからProjectウィンドウへドラッグして、Prefab にする。Hierarchyに残ったObstacleは削除する
using UnityEngine;
public class ObstacleMover : MonoBehaviour
{
[SerializeField] private float speed = 6.0f;
[SerializeField] private float lifeTime = 5.0f;
private void Start()
{
// 左へ一定の速さで動かす
Rigidbody2D rb = GetComponent<Rigidbody2D>();
rb.linearVelocity = Vector2.left * speed;
// 画面の外へ出たころに自動で消す
Destroy(gameObject, lifeTime);
}
}
Is Trigger は、「ぶつかっても跳ね返らず、触れたことだけを知らせる」設定です。Kinematic の Rigidbody 2D は、重力や力の影響を受けず、linearVelocity を設定すると、その速さで動き続けます。第1回で使った linearVelocity が、ここでも使えます。
Destroy(gameObject, lifeTime) は、第2引数の秒数がたったあとに、オブジェクトを消します。出しっぱなしにすると、画面の外で障害物が増え続けてしまうため、消す処理が必要です。
手順2:障害物を出す(ObstacleSpawner)
- Create Empty で空のオブジェクトを作り、名前を
ObstacleSpawnerにする - Position を (10, -2.1, 0) にする(画面の右の外。地面の上に、障害物の底がそろう高さ)
ObstacleSpawnerスクリプトを追加する(下のコード)- Inspectorの Obstacle Prefab に、手順1で作った Prefab をドラッグする
using UnityEngine;
public class ObstacleSpawner : MonoBehaviour
{
[SerializeField] private GameObject obstaclePrefab;
[SerializeField] private float spawnInterval = 1.5f;
private float timer;
private void Update()
{
timer += Time.deltaTime;
if (timer >= spawnInterval)
{
timer = 0.0f;
Instantiate(obstaclePrefab, transform.position, Quaternion.identity);
}
}
}
timer に、毎フレームの経過時間(Time.deltaTime)を足していき、spawnInterval を超えたら、Prefabから1つ出して、タイマーを0に戻します。
Instantiate は、Prefabのコピーを作る命令です。引数の transform.position は、このスポナー自身の位置です。
手順3:スコアとゲームオーバーの表示を作る
文字の表示には、TextMeshPro を使います。初めて使うときに、必要なリソースのインポートを求められた場合は、インポートしてください。
- Hierarchyで右クリック → UI → Text – TextMeshPro を作る(メニュー名が違う場合は、TextMeshProの文字を作る項目)。Canvas と EventSystem も一緒にできる
- 名前を
ScoreTextにし、Text をScore: 0.0にする。画面の左上に置く - もう1つ作り、名前を
GameOverTextにする。Text をGAME OVERと、改行してPress Space to Restartにし、画面の中央に置く
TextMeshProの標準のフォントには、日本語の文字が入っていません。日本語を入れると、四角で表示されてしまうので、今回は英語で書きます。日本語を表示する方法は、別の機会に扱います。
手順4:監督(GameDirector)を作る
- Create Empty で空のオブジェクトを作り、名前を
GameDirectorにする GameDirectorスクリプトを追加する(下のコード)- Inspectorの Score Text に ScoreText を、Game Over Text に GameOverText を、ドラッグして割り当てる
using TMPro;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.SceneManagement;
public class GameDirector : MonoBehaviour
{
[SerializeField] private TMP_Text scoreText;
[SerializeField] private GameObject gameOverText;
private InputAction jumpAction;
private float score;
private bool isGameOver;
private void Start()
{
// 前回のゲームオーバーで止めた時間を、元に戻す
Time.timeScale = 1.0f;
gameOverText.SetActive(false);
jumpAction = InputSystem.actions.FindAction("Player/Jump");
}
private void Update()
{
if (isGameOver)
{
// ゲームオーバー中は、ジャンプのボタン(スペースキー)でリスタート
if (jumpAction.WasPressedThisFrame())
{
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
}
return;
}
// 生き残った秒数を、そのままスコアにする
score += Time.deltaTime;
scoreText.text = "Score: " + score.ToString("F1");
}
public void GameOver()
{
isGameOver = true;
gameOverText.SetActive(true);
Time.timeScale = 0.0f;
}
}
GameOver() は、public にして、ほかのスクリプトから呼べるようにしています。ゲームオーバーになると、Time.timeScale を 0 にして、ゲームの時間を止めます。
Time.timeScale が 0 のあいだも、Update は動き続けます。そのため、リスタートのためのスペースキーの入力は受け取れます。一方で、FixedUpdate と物理の動きは止まり、Time.deltaTime も 0 になるので、障害物も、スポナーのタイマーも止まります。
止めた時間は、シーンを読み込み直したあとも残ります。そのため、Start で Time.timeScale を 1 に戻しています。ここを忘れると、リスタートしたあとも、ゲームが止まったままになります。
手順5:Player に当たり判定を足す
第1回で作った PlayerController に、次の部分を追記します。既存のコードは、そのままにします。
// 追加:GameDirector への参照
[SerializeField] private GameDirector gameDirector;
// 追加:障害物(Obstacleタグ)に触れたらゲームオーバー
private void OnTriggerEnter2D(Collider2D other)
{
if (other.CompareTag("Obstacle"))
{
gameDirector.GameOver();
}
}
- 上のコードを、
PlayerControllerのクラスの中(IsGrounded()の下など)に書き足す - Inspectorの Game Director に、GameDirector をドラッグして割り当てる
- Player の Tag が
Obstacleになっていないことを確認する(Playerは Untagged のままにする)
OnTriggerEnter2D は、トリガーのColliderに、ほかのオブジェクトが入ったときに呼ばれる関数です。公式マニュアルによると、トリガーのイベントは、どちらかのColliderに Rigidbody 2D が付いているときに送られます。今回は Player が Rigidbody 2D を持っているので、Player のスクリプトで受け取れます。
other.CompareTag("Obstacle") で、触れた相手が障害物かどうかを調べます。
動かしてみる
再生して、次のことを確認しましょう。
- 右から障害物が流れてくる
- スペースキーのジャンプで避けられる
- 当たると GAME OVER が出て、すべてが止まる
- スペースキーを押すと、最初からやり直せる
数字で見る
第1回と同じ設定(Gravity Scale 3、Jump Speed 10)で、今回の障害物(速さ6、大きさ0.8)をジャンプで越えられるかを計算します。
| 項目 | 計算 | 目安 |
|---|---|---|
| Playerが、障害物の高さ(0.8)より上にいる時間 | 2 × √(10² − 2 × 29.4 × 0.8) ÷ 29.4 | 約 0.49 秒 |
| 障害物が、Playerの横を通過するのにかかる時間 | (1 + 0.8) ÷ 6 | 約 0.30 秒 |
| ジャンプのタイミングの余裕 | 0.49 − 0.30 | 約 0.19 秒 |
| 障害物の間隔 | 6 × 1.5 | 9 進む(1.5秒ごとに出る) |
| 障害物が消えるまでに進む距離 | 6 × 5 | 30(画面の外まで十分に届く) |
余裕は、約0.19秒です。難しすぎると感じたら、障害物の速さ(Speed)を下げたり、大きさを小さくしたりしてみましょう。計算は、重力を 9.81 × 3 ≒ 29.4 として行った目安です。
よくあるつまずき
| 症状 | 原因と直し方 |
|---|---|
| 障害物に当たっても、ゲームオーバーにならない | Obstacle の Box Collider 2D の Is Trigger、Rigidbody 2D の有無、Obstacle タグの設定を確認する。Prefab側の設定を変えたか(Hierarchyのコピーだけ変えていないか)も確認する |
| 障害物が下に落ちる | Rigidbody 2D の Body Type が Dynamic になっている。Kinematic にする |
| 障害物が動かない | Obstacle Mover が付いていない、または Prefab を差し替えていない。Body Type が Static でも、速さで動かない |
| ゲームオーバーで NullReferenceException | Player の Game Director、または GameDirector の Score Text / Game Over Text が、割り当てられていない |
| リスタートでエラーが出る | GameScene が Scene List に登録されていない。File → Build Profiles で確認する |
| リスタートしても、ゲームが止まったまま | Start で Time.timeScale を 1 に戻していない |
| 文字が四角で表示される | TextMeshProの標準のフォントは、日本語に対応していない。英語で書く |
| スコアが増えない | Score Text が割り当てられていない、または Canvas が画面に表示されていない |
もう一歩
- 障害物を出す間隔を、
Random.Rangeでランダムにする - 時間がたつほど、障害物の速さを上げて、難しくする
- 最高スコアを、
PlayerPrefsに保存する - 空中に浮かぶ障害物を足して、ジャンプしない避け方も作る
完成したら出力しよう
ゲームが完成したら、第2回の手順で、Web版に出力してみましょう。
- Scene List に、
GameSceneが入っていることを確認する - Build Profiles で、Web のプロファイルを有効にする
- Build And Run などで、ブラウザで動かして確認する
スマホ(Android)で遊ぶには、キーボードの代わりに、タッチ操作への対応が必要です。今回のゲームは、キーボードだけに対応しています。
まとめ
- 作るゲームを、「Player」「障害物」「スポナー」「監督」の部品に分けた
- 障害物は、Is Trigger と Kinematic の Rigidbody 2D で、
linearVelocityで動かした - 当たり判定は
OnTriggerEnter2Dで受け取り、監督に伝えた - ゲームオーバーは
Time.timeScaleで時間を止め、リスタートはSceneManager.LoadSceneで行った - リスタートでは、シーンを Scene List に登録しておく
参考(公式マニュアル)
前の回:第2回:WebとAndroidに出力する(Build Profiles)
これでシリーズは完結です。








ディスカッション
コメント一覧
まだ、コメントがありません