学習記事一覧 · Unity教科書_Unity6対応

Unity入門:矢をよけろ!CatEscapeゲームを作りながら学ぼう!

対象読者:Unity学び始めの方・C#をこれから覚えたい方 このブログでは、実際のCatEscapeゲームのコードを読み解きながら、Unityの基本を学びます。


記事の目次

この記事はやや長めです。目的に合わせてジャンプして読み分けてください。

おすすめの読み方

セクション一覧

ポイント一覧


このゲームで何ができるの?

今回題材にするのは、上から降ってくる矢をよけながら生き延びる2Dアクションゲームです。

  • ⌨️ 左右の矢印キーでキャラクター(猫)を動かす
  • 🏹 矢が画面上からランダムな位置に降ってくる
  • ❤️ 矢に当たるたびにHPゲージが減っていく

上から降る矢をよけながら猫を動かす様子(イメージ)

たった 4つのスクリプト(合計約70行) でこれが実現できます。「プレハブ」「衝突判定」「UIゲージ」など、実践的なゲーム開発の要素が凝縮されています。一緒に読み解いていきましょう!


プロジェクトの構成

このプロジェクトのファイル構成はこうなっています。

Assets/
  ├── PlayerController.cs   # プレイヤー(猫)の操作スクリプト
  ├── ArrowGenerator.cs     # 矢を生成するスクリプト
  ├── ArrowController.cs    # 矢の動きと衝突判定スクリプト
  ├── GameDirector.cs       # HPゲージを管理するスクリプト
  ├── LButton.png           # 左ボタン画像
  ├── RButton.png           # 右ボタン画像
  └── Scenes/
        └── SampleScene.unity   # ゲームのシーン

4つのスクリプトが役割ごとに分担しているのがポイントです。「動かす」「生成する」「当たり判定をする」「管理する」——それぞれの責務が明確に分かれています。


コードを全部見てみよう

PlayerController.cs(プレイヤーの操作)

using UnityEngine;
using UnityEngine.InputSystem;  // 入力を検知するために必要!!

public class PlayerController : MonoBehaviour
{
    void Start()
    {
        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」動かす
        }
    }
}

ArrowGenerator.cs(矢の生成)

using UnityEngine;

public class ArrowGenerator : MonoBehaviour
{
    public GameObject arrowPrefab;
    float span = 1.0f;
    float delta = 0;

    void Update()
    {
        this.delta += Time.deltaTime;
        if (this.delta > this.span)
        {
            this.delta = 0;
            GameObject go = Instantiate(arrowPrefab);
            int px = Random.Range(-6, 7);
            go.transform.position = new Vector3(px, 7, 0);
        }
    }
}

ArrowController.cs(矢の動きと衝突判定)

using UnityEngine;

public class ArrowController : MonoBehaviour
{
    GameObject player;

    void Start()
    {
        this.player = GameObject.Find("player_0");
    }

    void Update()
    {
        // フレームごとに等速で落下させる
        transform.Translate(0, -0.1f, 0);

        // 画面外に出たらオブジェクトを破棄する
        if (transform.position.y < -5.0f)
        {
            Destroy(gameObject);
        }

        // 当たり判定
        Vector2 p1 = transform.position;                // 矢の中心座標
        Vector2 p2 = this.player.transform.position;    // プレイヤの中心座標
        Vector2 dir = p1 - p2;
        float d = dir.magnitude;
        float r1 = 0.5f;    // 矢の半径
        float r2 = 1.0f;    // プレイヤの半径

        if (d < r1 + r2)
        {
            // 監督スクリプトにプレイヤと衝突したことを伝える
            GameObject director = GameObject.Find("GameDirector");
            director.GetComponent<GameDirector>().DecreaseHp();

            // 衝突した場合は矢を消す
            Destroy(gameObject);
        }
    }
}

GameDirector.cs(HPゲージの管理)

using UnityEngine;
using UnityEngine.UI;   // UIを使うので忘れずに追加

public class GameDirector : MonoBehaviour
{
    GameObject hpGauge;

    void Start()
    {
        this.hpGauge = GameObject.Find("hpGauge");
    }

    public void DecreaseHp()
    {
        this.hpGauge.GetComponent<Image>().fillAmount -= 0.1f;
    }
}

この4つのスクリプトには、合計7つの学びポイントがあります。順番に見ていきましょう!


ポイント①:キーボード入力で瞬時に移動する

if (Keyboard.current.leftArrowKey.wasPressedThisFrame)
{
    transform.Translate(-3, 0, 0);  // 左に「3」動かす
}

Unity の新しい Input System では Keyboard.current でキーボードの状態にアクセスできます。wasPressedThisFrame は「そのフレームにキーを押した瞬間だけ true」になるプロパティです。

プロパティ いつ true になる?
wasPressedThisFrame 押した瞬間のフレームだけ
isPressed 押している間ずっと
wasReleasedThisFrame 離した瞬間のフレームだけ

今回は「押すたびに3マス移動」という設計なので、wasPressedThisFrame がぴったりです。isPressed を使うと押しっぱなしで高速移動になってしまいます。

💡 Application.targetFrameRate = 60Start() に書くことで、フレームレートを60FPSに固定しています。ゲームの動作速度を一定に保つ基本テクニックです。


ポイント②:Time.deltaTime でフレームレートによらない時間管理をする

float span = 1.0f;
float delta = 0;

void Update()
{
    this.delta += Time.deltaTime;
    if (this.delta > this.span)
    {
        this.delta = 0;
        // 矢を生成する
    }
}

Time.deltaTime は「前のフレームから今のフレームまでにかかった秒数」です。これを毎フレーム足し続けることで、経過時間を計測できます。

60FPSの場合: Time.deltaTime ≈ 0.0167
30FPSの場合: Time.deltaTime ≈ 0.0333

span = 1.0f と比較しているので、フレームレートに関係なく約1秒ごとに矢が生成されます。Update() が呼ばれる回数(FPS)に依存しないのが重要なポイントです。

💡 delta はフィールドに定義されています。Update() 内でローカル変数にすると、毎フレームリセットされてしまいます。フレームをまたいで値を保持したい場合はフィールドに書くのが基本です。


ポイント③:プレハブから Instantiate() でオブジェクトを量産する

public GameObject arrowPrefab;

GameObject go = Instantiate(arrowPrefab);
int px = Random.Range(-6, 7);
go.transform.position = new Vector3(px, 7, 0);

プレハブ(Prefab) は、GameObjectのテンプレートです。Instantiate() でプレハブをコピーしてシーンに追加できます。

プレハブ(設計図)
    └─ Instantiate() → 矢オブジェクト①(シーンに追加)
    └─ Instantiate() → 矢オブジェクト②(シーンに追加)
    └─ Instantiate() → 矢オブジェクト③(シーンに追加)

Random.Range(-6, 7) は「-6以上7未満」の整数をランダムに返します。これを X 座標に使うことで、矢が毎回バラバラな位置から降ってきます。

メソッド 返す値の範囲
Random.Range(int min, int max) min以上 max未満の整数
Random.Range(float min, float max) min以上 max以下の小数

⚠️ 整数版の Random.Rangemax を含みません。Random.Range(-6, 7) で -6〜6 の13通りになります。


ポイント④:Destroy() でオブジェクトを削除してメモリを節約する

// 画面外に出たらオブジェクトを破棄する
if (transform.position.y < -5.0f)
{
    Destroy(gameObject);
}

Instantiate() でオブジェクトを量産し続けると、画面外に消えたあとも残り続けてメモリを圧迫します。Destroy(gameObject) でそのオブジェクト自身を削除することで、不要になったオブジェクトを消してメモリを節約できます。

矢が y < -5.0f を下回ったとき(画面の下側に消えたとき)に削除しています。プレイヤーに当たったときも同様に削除しています。

生成 → 落下 → 当たった or 画面外に出た → Destroy で削除

このパターンは、弾丸・エフェクト・アイテムなど大量に生成・消滅を繰り返すオブジェクトでよく使われます。


ポイント⑤:円と円の距離で当たり判定をする

Vector2 p1 = transform.position;                // 矢の中心座標
Vector2 p2 = this.player.transform.position;    // プレイヤの中心座標
Vector2 dir = p1 - p2;
float d = dir.magnitude;
float r1 = 0.5f;    // 矢の半径
float r2 = 1.0f;    // プレイヤの半径

if (d < r1 + r2)
{
    // 衝突!
}

2つの円が重なっているかどうかは、中心間の距離 < 2つの半径の合計 で判定できます。

距離 d = 矢とプレイヤの中心間の距離
半径の合計 = r1 + r2 = 0.5 + 1.0 = 1.5

d < 1.5 → 衝突している!
d >= 1.5 → 衝突していない

Vector2 の差(p1 - p2)を計算すると2点間のベクトルが求まり、その .magnitude(大きさ)が距離になります。Unity には Physics2D による自動の衝突判定もありますが、このように自分で計算することで仕組みを理解できます。

💡 UnityのBuilt-in当たり判定(Collider2D + OnTrigger)を使う方法もあります。手動実装と比べてどちらが適しているかは、ゲームの複雑さや要件によります。


ポイント⑥:スクリプト間でメソッドを呼び出して連携する

// ArrowController.cs 内
GameObject director = GameObject.Find("GameDirector");
director.GetComponent<GameDirector>().DecreaseHp();
// GameDirector.cs 内
public void DecreaseHp()
{
    this.hpGauge.GetComponent<Image>().fillAmount -= 0.1f;
}

矢がプレイヤーに当たった時、矢スクリプトから直接HPゲージを操作するのではなく、**「ゲームを管理するスクリプト(GameDirector)のメソッドを呼び出す」**という設計になっています。

ArrowController(矢)
    └─ "当たった!" → GameDirector.DecreaseHp() を呼ぶ
                           └─ HPゲージを減らす

この設計の利点は「HPをどう減らすか」という処理を GameDirector に集約できることです。ダメージ量の変更や演出の追加も GameDirector だけ修正すれば済みます。

GetComponent<GameDirector>() は、GameObjectにアタッチされている指定コンポーネントを取得するメソッドです。


ポイント⑦:Image.fillAmount でHPゲージを表現する

this.hpGauge.GetComponent<Image>().fillAmount -= 0.1f;

UnityのUIの Image コンポーネントには fillAmount というプロパティがあります。

fillAmount の値 ゲージの見た目
1.0f 100%(満タン)
0.5f 50%
0.0f 0%(空)

矢に当たるたびに 0.1f ずつ減るので、10回当たるとゲージが空になる設計です。Image の Image TypeFilled に設定しておくことで、この機能が使えます。

💡 fillAmount0.0f1.0f の範囲で指定します。範囲外の値を代入しても自動的にクランプ(制限)されます。


4つのスクリプトの役割分担

各スクリプトの責務を整理しましょう。

【PlayerController】        【ArrowGenerator】
プレイヤーにアタッチ        空のGameObjectにアタッチ
   ↓                            ↓
キー入力を検知              時間を計測して矢を生成
プレイヤーを移動            ランダムな位置に配置

【ArrowController】         【GameDirector】
矢のプレハブにアタッチ      空のGameObjectにアタッチ
   ↓                            ↓
矢を毎フレーム落下          HPゲージを管理
衝突判定を実行              DecreaseHp() を公開
不要になったら自己破棄

「自分のこと(動き・入力)は自分で管理」「ゲーム全体の状態は GameDirector が一元管理」という分担が、コードを読みやすく保つ鍵です。


コードの流れを整理しよう

【PlayerController の毎フレーム処理フロー】

① 左矢印キーが押された?
   └─ YES → 左に3移動
② 右矢印キーが押された?
   └─ YES → 右に3移動

【ArrowGenerator の毎フレーム処理フロー】

① delta += Time.deltaTime(経過時間を積算)
② delta が span(1秒)を超えた?
   └─ YES → delta をリセット
            → arrowPrefab を Instantiate
            → ランダムなX座標(-66)、Y=7 に配置

【ArrowController の毎フレーム処理フロー】

① 下方向に 0.1f 移動
② Y座標 < -5.0f
   └─ YES → Destroy(画面外に出た)
③ プレイヤーとの距離を計算
④ 距離 < 1.5f(半径の合計)?
   └─ YES → GameDirector.DecreaseHp() を呼ぶ
            → Destroy(矢を消す)

自分でカスタマイズしてみよう!

挑戦①:矢の降るスピードを変える

// ArrowController.cs の落下量を変える
transform.Translate(0, -0.1f, 0);  // -0.2f にすると2倍速!

挑戦②:矢の生成間隔を変える

// ArrowGenerator.cs の span を変える
float span = 1.0f;  // 0.5f にすると2倍の頻度で生成!

挑戦③:ダメージ量を変える

// GameDirector.cs の減少量を変える
this.hpGauge.GetComponent<Image>().fillAmount -= 0.1f;  // 0.2f にすると5回でゲームオーバー

挑戦④:矢が当たったらスコアをカウントダウンする

// GameDirector.cs に当たった回数を追加してみよう
int hitCount = 0;

public void DecreaseHp()
{
    this.hitCount++;
    this.hpGauge.GetComponent<Image>().fillAmount -= 0.1f;
}

挑戦⑤:HPがゼロになったらゲームオーバーを表示する

public void DecreaseHp()
{
    this.hpGauge.GetComponent<Image>().fillAmount -= 0.1f;
    if (this.hpGauge.GetComponent<Image>().fillAmount <= 0)
    {
        // ゲームオーバー処理をここに書く
    }
}

まとめ

今回のCatEscapeゲームから学んだことを整理しましょう。

  • Keyboard.current.〇〇.wasPressedThisFrame で押した瞬間のキー入力を検知できる
  • Application.targetFrameRate = 60 でフレームレートを固定してゲームの速度を安定させる
  • Time.deltaTime を積算することでFPSに依存しない時間計測ができる
  • Instantiate() でプレハブからオブジェクトを量産し、Destroy() で不要になったら削除する
  • Random.Range() でランダムな値を生成できる(整数版は max を含まない点に注意)
  • 2つの円の中心間距離と半径の合計を比べることで衝突判定を実装できる
  • GetComponent<GameDirector>().メソッド名() でスクリプト間の連携ができる
  • Image.fillAmount を 0.0〜1.0 で操作するとHPゲージを表現できる
  • 「操作・生成・判定・管理」を4つのスクリプトに分担することでコードが整理しやすくなる

4つのスクリプトが連携することで、入力→移動→生成→落下→衝突→HP減少という一連のゲームフローが完成しました。「動く→コードを読む→カスタマイズする」 のサイクルを繰り返すことが、Unity上達の近道です。

次は、HPがゼロになったときの「ゲームオーバー画面」や、よけた矢の数をカウントする「スコア機能」を追加してみましょう!


最終更新:2026年4月