Unity 6でCatEscapeを作る① ― 猫の移動と、Prefabで矢を降らせる仕組み(PC版)

広告

落ちてくる矢を、猫が左右に動いてよける――今回からは「CatEscape」というゲームを作ります。

これまでのルーレットやSwipeCarは、画面の中のオブジェクトを1つ動かすゲームでした。CatEscapeでは、ゲームの途中で矢がどんどん生まれて、落ちて、消えていきます。この「たくさん作って、いらなくなったら消す」仕組みが、今回のいちばんの学びどころです。

CatEscapeは内容が多いので、2回に分けて解説します。

回内容
今回(①)猫を矢印キーで動かす/Prefabで矢を作る/矢を一定間隔で降らせる/画面の外に出た矢を消す
次回(②)矢と猫の当たり判定/HPゲージを減らす/スクリプト同士で連絡を取り合う

この記事で作るもの

  • 画面の下に猫、背景に空の画像を置く
  • 左右の矢印キーを押すと、猫が左右に動く
  • 画面の上から、1秒ごとに矢が1本ずつ降ってくる
  • 矢が出てくる横の位置は毎回ランダム
  • 画面の下まで落ちた矢は、自動で消える
項目内容
UnityUnity 6(サンプルは6000.1で作成)
テンプレートUniversal 2D(URP)
入力Input System(UnityEngine.InputSystem)
操作PC版:キーボードの左右の矢印キー

今回作るスクリプトは3つです。

スクリプト付ける場所役割
PlayerController猫(player)矢印キーで猫を左右に動かす
ArrowController矢(arrowPrefab)矢を落とし、画面の外に出たら消す
ArrowGenerator空のオブジェクト(ArrowGenerator)1秒ごとに矢を作って、ランダムな位置に置く

1. シーンを準備する

使う画像は、猫(player.png)、矢(arrow.png)、背景(background.png)の3枚です。Projectウィンドウに取り込み、猫と背景をSceneビューに配置します。矢はあとでPrefabにするので、まだ置かなくて大丈夫です。

オブジェクトPositionScaleOrder in Layer
背景(background)(0, 0, 0)(4.5, 2, 1)0
猫(player)(0, -3.6, 0)(1, 1, 1)1

ポイントはOrder in Layerです。2Dの画像は、この数字が大きいものほど手前に表示されます。背景を0、猫を1にしておくと、猫が背景の後ろに隠れてしまうことを防げます。Order in Layerは、InspectorのSprite Rendererの中にあります。

画面の各ウィンドウ(Scene・Hierarchy・Inspectorなど)の役割があいまいな場合は、先に次の記事を読んでおくと迷いにくくなります。

Unity 6の画面構成を理解する ― Game・Scene・Hierarchy・Inspector・Project

2. 猫を矢印キーで動かす(PlayerController)

Projectウィンドウで Create → Scripting → MonoBehaviour Script を選んで PlayerController を作り、猫にドラッグ&ドロップして付けます。

using UnityEngine;
using UnityEngine.InputSystem;   // Keyboard を使うために必要

public class PlayerController : MonoBehaviour
{
    void Start()
    {
        // Update が1秒間に60回を目安に呼ばれるようにする
        Application.targetFrameRate = 60;
    }

    void Update()
    {
        // 左矢印キーを押した瞬間
        if (Keyboard.current.leftArrowKey.wasPressedThisFrame)
        {
            transform.Translate(-3, 0, 0);   // 左に3動かす
        }

        // 右矢印キーを押した瞬間
        if (Keyboard.current.rightArrowKey.wasPressedThisFrame)
        {
            transform.Translate(3, 0, 0);    // 右に3動かす
        }
    }
}

ルーレットやSwipeCarでは Mouse.current でマウスを調べましたが、今回は Keyboard.current でキーボードを調べます。書き方の形はそっくりです。

書き方調べる入力
Mouse.current.leftButtonマウスの左ボタン
Keyboard.current.leftArrowKeyキーボードの左矢印キー
Keyboard.current.rightArrowKeyキーボードの右矢印キー
Keyboard.current.spaceKeyスペースキー(参考)

wasPressedThisFrame は「押した瞬間の1フレームだけ true」なので、キーを1回押すごとに3ずつ動きます。押しっぱなしで動き続けてほしい場合は、「押している間ずっと true」になる isPressed を使います。

書き方true になるタイミング向いている動き
wasPressedThisFrame押した瞬間だけ1マスずつ動く(今回)
isPressed押している間ずっとなめらかに動き続ける
wasReleasedThisFrame離した瞬間だけSwipeCarのスワイプ終了

キー入力でなめらかに動かす方法や、ネットでよく見る Input.GetKeyDown との違いは、次の記事で解説しています。

Unity 6でキー入力による移動を実装する ― Input Systemの基本

Unityの入力方法が教科書とネットで違う?― Input ManagerとInput Systemの違い

3. 矢を落とす(ArrowController)

次は矢です。まず、矢の画像を1本だけシーンにドラッグして配置し、Order in Layerを1にします。そして、次のスクリプト ArrowController を作って矢に付けます。

using UnityEngine;

public class ArrowController : MonoBehaviour
{
    void Update()
    {
        // 毎フレーム、下に0.1ずつ動かす
        transform.Translate(0, -0.1f, 0);

        // 画面の下の外に出たら、この矢を消す
        if (transform.position.y < -5.0f)
        {
            Destroy(gameObject);
        }
    }
}

Translate(0, -0.1f, 0) のY方向がマイナスなので、矢は下へ落ちていきます。SwipeCarと違って減速はしないので、ずっと同じ速さで落ち続けます。

Destroy で「いらなくなった矢」を消す

カメラの表示範囲は、上下が y = 5 〜 -5 です。transform.position.y が -5 より小さくなった矢は、もう画面に映っていません。そのまま放っておくと、見えない矢がどんどんたまって、パソコンの処理が重くなってしまいます。

そこで Destroy(gameObject) で矢そのものを消します。gameObject は「このスクリプトが付いているオブジェクト自身」を表します。

書き方消えるもの
Destroy(gameObject)矢のオブジェクトごと消える(今回はこちら)
Destroy(this)このスクリプトだけが外れる。矢は画面に残ったまま止まる

this と書いてしまうと、スクリプトだけが消えて、止まった矢がその場に残ります。よくある間違いなので注意しましょう。

4. 矢をPrefabにする

矢は1秒ごとに何本も作りたいので、今置いた矢をPrefab(プレハブ)にします。Prefabは、オブジェクトの「型」や「設計図」のようなものです。クッキーの型が1つあれば同じ形のクッキーを何枚でも作れるように、Prefabが1つあれば同じ矢をいくつでも作れます。

  1. Hierarchyの矢の名前を arrowPrefab に変える
  2. Hierarchyの矢を、Projectウィンドウへドラッグ&ドロップする(青い箱のアイコンのPrefabができる)
  3. Hierarchyに残っている矢は、もう使わないので削除する

Prefabには、画像だけでなく付けたスクリプト(ArrowController)やOrder in Layerの設定もまるごと保存されます。だから、Prefabから作った矢は、どれも最初から「落ちて、画面外で消える」動きをします。

シーンに置いたオブジェクトPrefab
ある場所Hierarchy(シーンの中)Projectウィンドウ(ファイル)
たとえると焼き上がったクッキークッキーの型
数作った数だけある1つあればよい
Prefabは矢の型:シーンの矢をProjectへドラッグしてPrefabを作り、Instantiateで何本でも作る

5. 1秒ごとに矢を降らせる(ArrowGenerator)

最後に、Prefabから矢を作る「矢の工場」を用意します。Hierarchyで Create Empty を選んで空のオブジェクトを作り、名前を ArrowGenerator にします。そこに次のスクリプトを付けます。

using UnityEngine;

public class ArrowGenerator : MonoBehaviour
{
    public GameObject arrowPrefab;   // Inspectorで矢のPrefabを設定する
    float span = 1.0f;               // 矢を作る間隔(秒)
    float delta = 0;                 // 前回矢を作ってからの経過時間(秒)

    void Update()
    {
        // 経過時間をためていく
        delta += Time.deltaTime;

        // 1秒たったら矢を1本作る
        if (delta > span)
        {
            delta = 0;

            // Prefabから矢を作る
            GameObject go = Instantiate(arrowPrefab);

            // 横の位置をランダムにして、画面の上に置く
            int px = Random.Range(-6, 7);
            go.transform.position = new Vector3(px, 7, 0);
        }
    }
}

スクリプトを保存したら、ArrowGeneratorを選んでInspectorを見てください。Arrow Prefab という欄ができているので、Projectウィンドウの arrowPrefab をドラッグ&ドロップして設定します。

Time.deltaTime で「1秒」を数える

Time.deltaTime は「前のフレームから何秒たったか」です。60fpsなら1フレームあたり約0.0167秒なので、これを毎フレーム delta に足していくと、ちょうどストップウォッチのように経過時間がたまっていきます。delta が1秒を超えたら矢を作り、delta を0に戻して、また数え直します。

フレームdelta(60fpsの場合)矢を作る?
10.0167
20.0333
……
約60約1.01秒を超えたフレームで矢を作り、delta を0に戻す
Time.deltaTimeでdeltaをため、1秒を超えたら矢を作って0に戻すタイマーの仕組み

Instantiate でPrefabから矢を作る

Instantiate(arrowPrefab) は、Prefab(型)から新しいオブジェクトを1つ作ってシーンに置く命令です。作ったオブジェクトは go という変数で受け取れるので、そのあと go.transform.position で置く場所を決めています。

Random.Range の範囲に注意

Random.Range(-6, 7) は、-6 から 6 までの整数をランダムに返します。7は含まれません。整数版の Random.Range は「最後の数字は含まない」というルールがあるためです。

書き方返ってくる値
Random.Range(-6, 7)-6, -5, …, 5, 6 のどれか(整数。7は出ない)
Random.Range(0, 3)0, 1, 2 のどれか(3は出ない)
Random.Range(-6f, 6f)-6.0 〜 6.0 の小数(float版は両端を含む)

整数版で「-6から6まで」がほしいときは、最後の数字を1つ大きく書く、と覚えておきましょう。y = 7 は画面の上端(y = 5)より少し上なので、矢は画面の外から降ってくるように見えます。

6. 「フレーム」と「秒」がまざっている

ここで、ArrowControllerとArrowGeneratorを見比べてみてください。実は、2つのスクリプトは時間の数え方が違います。

スクリプト書き方基準
ArrowControllertransform.Translate(0, -0.1f, 0)1フレームごとに0.1落ちる
ArrowGeneratordelta += Time.deltaTime1秒ごとに1本作る

矢は y = 7 から y = -5 まで、12進むと消えます。1フレームで0.1なので、消えるまでに120フレームかかります。60fpsなら2秒、144fpsなら約0.8秒です。一方、矢が出てくる間隔はどの環境でも1秒のままです。つまり、フレームレートが高いパソコンほど、矢の落ちる速さだけが速くなるのです。

フレーム単位の移動は60fpsと144fpsで落ちる速さが変わり、Time.deltaTimeを掛けるとどの環境でも同じ速さになる

PlayerControllerの Application.targetFrameRate = 60; は、この差が出にくいようにフレームレートをそろえる役割もしています。ルーレットの記事でも同じ話をしました。

Unity 6でルーレットを作る ― クリックで回して、だんだん止める仕組み(PC版)

矢の落下も「秒」で考えたい場合は、次のように Time.deltaTime を掛けます。「1秒に6進む」という書き方になるので、どのフレームレートでも同じ速さで落ちます(60fps × 0.1 = 1秒に6)。

// 1秒間に6ずつ落ちる(フレームレートに左右されない)
transform.Translate(0, -6f * Time.deltaTime, 0);

7. よくあるつまずき

矢が出てこず、UnassignedReferenceException が出る

UnassignedReferenceException: The variable arrowPrefab of ArrowGenerator has not been assigned.

ArrowGeneratorのInspectorで、Arrow Prefab 欄が空(None)のままです。Projectウィンドウの arrowPrefab をドラッグ&ドロップしてください。Hierarchyの矢ではなく、ProjectウィンドウのPrefabを入れるのがポイントです(Hierarchyの矢は削除済みのはずです)。

Keyboard が見つからないというエラーが出る

error CS0103: The name 'Keyboard' does not exist in the current context

PlayerControllerの先頭に using UnityEngine.InputSystem; がありません。

その他のチェックポイント

  • 矢や猫が見えない → Order in Layerを確認する(背景0、猫と矢は1)
  • 矢が途中で止まって画面に残る → Destroy(this) になっていないか確認する
  • Hierarchyに「arrowPrefab(Clone)」がたまり続ける → Destroyの条件(y < -5.0f)を確認する
  • キーを押しても猫が動かない → Gameビューをクリックして選択状態にしてから押す

8. もう一歩:猫を画面の外に出さない

今のままだと、右矢印キーを何回も押すと、猫が画面の外へ出ていってしまいます。Mathf.Clamp を使うと、値を決まった範囲におさめられます。PlayerControllerの Update の最後に、次の2行を足してみましょう。

// X座標を -6 〜 6 の範囲におさめる
float x = Mathf.Clamp(transform.position.x, -6f, 6f);
transform.position = new Vector3(x, transform.position.y, 0);

Mathf.Clamp(値, 最小, 最大) は、値が最小より小さければ最小を、最大より大きければ最大を返します。矢が出てくる範囲(-6〜6)と同じにしておくと、ゲームのバランスもちょうどよくなります。

まとめ

  • Keyboard.current.leftArrowKey.wasPressedThisFrame で、キーを押した瞬間を調べられる
  • Destroy(gameObject) で、いらなくなったオブジェクトを消せる(this と間違えない)
  • Prefabはオブジェクトの「型」。スクリプトや設定もまるごと保存される
  • Instantiate(Prefab) で、型から新しいオブジェクトを作れる
  • Time.deltaTime を足していくと、経過時間(秒)を数えられる
  • 整数版の Random.Range(最小, 最大) は、最大の数字を含まない

次回は、矢と猫の当たり判定と、当たったときにHPゲージを減らす仕組みを作ります。CatEscapeの全体の構造は、次の記事でも解析しています。

猫が逃げるゲームの解析例(学習目的)

続きの②はこちらです。

Unity 6でCatEscapeを作る② ― 当たり判定とHPゲージ、スクリプト同士の連携(PC版)

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

広告