Unity 6でジャンプで避けるミニゲームを作ろう ― 障害物・スコア・リスタートで完成させる

広告

はじめに

第1回で、動いてジャンプするPlayerを作り、第2回で、ゲームをWebやAndroidに出力する方法を確認しました。今回は、この2つを使って、ジャンプで障害物を避ける小さなゲームを完成させます。

教科書のゲームとは違い、題材も仕様も自分で決めたゲームです。「何を作るか決める → 部品に分ける → 1つずつ作る」という進め方も、あわせて練習しましょう。

シリーズ「Unityで作って、出力して、完成させる」(全3回)

今回作るゲーム

  • 右から障害物が流れてくる
  • スペースキーのジャンプで、障害物を避ける
  • 障害物に当たったらゲームオーバー
  • 生き残った秒数がスコアになる
  • ゲームオーバー後に、スペースキーでリスタートできる
ジャンプで障害物を避けるミニゲームの全体像。Space キーでジャンプ、Player・Obstacle・ObstacleSpawner・GameDirector の4つの部品、当たるとゲームオーバーになる流れをまとめた図

全体の構成

ゲームを、次の部品に分けて作ります。1つの部品に、1つの役割だけを持たせるのがコツです。

部品スクリプト役割
PlayerPlayerController移動とジャンプ(第1回)。障害物に触れたら監督に知らせる
Obstacle(Prefab)ObstacleMover左へ動き、しばらくしたら消える
ObstacleSpawnerObstacleSpawner一定の間隔で、障害物を出す
GameDirectorGameDirectorスコア、ゲームオーバー、リスタートを管理する監督

準備

① シーンを保存して、Scene List に登録する

リスタートでは、シーンを読み込み直します。読み込めるのは、Scene List に登録されたシーンだけです。第2回で確認した、Scene List が、ここで必要になります。

  1. 第1回で作ったシーンを開き、GameScene という名前で保存する
  2. File → Build Profiles を開く
  3. Scene List の Add Open Scenes を押して、GameScene を登録する

② Ground を広げる

第1回の Ground は、横幅が短く、障害物が出てくる場所に地面がありません。Ground の Scale を (30, 1, 1) に広げて、画面の外まで届くようにします。

③ Obstacle タグを作る

  1. Hierarchyで何かのオブジェクトを選び、Inspector上部の Tag の欄を開く
  2. Add Tag を選び、Obstacle というタグを追加する

手順1:障害物(Obstacle)を作る

  1. Square を作り、名前を Obstacle にする。Scale を (0.8, 0.8, 1) にする
  2. Sprite Renderer の Color を、赤など目立つ色にする
  3. Box Collider 2D を追加し、Is Trigger にチェックを入れる
  4. Rigidbody 2D を追加し、Body Type を Kinematic にする
  5. Tag を Obstacle にする
  6. ObstacleMover スクリプトを追加する(下のコード)
  7. 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)

  1. Create Empty で空のオブジェクトを作り、名前を ObstacleSpawner にする
  2. Position を (10, -2.1, 0) にする(画面の右の外。地面の上に、障害物の底がそろう高さ)
  3. ObstacleSpawner スクリプトを追加する(下のコード)
  4. 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 を使います。初めて使うときに、必要なリソースのインポートを求められた場合は、インポートしてください。

  1. Hierarchyで右クリック → UI → Text – TextMeshPro を作る(メニュー名が違う場合は、TextMeshProの文字を作る項目)。Canvas と EventSystem も一緒にできる
  2. 名前を ScoreText にし、Text を Score: 0.0 にする。画面の左上に置く
  3. もう1つ作り、名前を GameOverText にする。Text を GAME OVER と、改行して Press Space to Restart にし、画面の中央に置く

TextMeshProの標準のフォントには、日本語の文字が入っていません。日本語を入れると、四角で表示されてしまうので、今回は英語で書きます。日本語を表示する方法は、別の機会に扱います。

手順4:監督(GameDirector)を作る

  1. Create Empty で空のオブジェクトを作り、名前を GameDirector にする
  2. GameDirector スクリプトを追加する(下のコード)
  3. 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();
        }
    }
  1. 上のコードを、PlayerController のクラスの中(IsGrounded() の下など)に書き足す
  2. Inspectorの Game Director に、GameDirector をドラッグして割り当てる
  3. 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.59 進む(1.5秒ごとに出る)
障害物が消えるまでに進む距離6 × 530(画面の外まで十分に届く)

余裕は、約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 でも、速さで動かない
ゲームオーバーで NullReferenceExceptionPlayer の 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)
これでシリーズは完結です。

訪問数 8 回, 今日の訪問数 8回

広告