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

Unity入門:リンゴをキャッチしてスコアを稼げ!AppleCatchゲームを作りながら学ぼう!

対象読者:Unity学び始めの方・C#をこれから覚えたい方 このブログでは、実際のAppleCatchゲームのコードを読み解きながら、Unityの3Dレイキャスト・タグ判定・難易度設計・サウンド再生を学びます。


記事の目次

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

おすすめの読み方

セクション一覧

ポイント一覧


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

今回題材にするのは、上から降ってくるリンゴをバスケットでキャッチしてスコアを稼ぐ3Dアクションゲームです。

  • 🖱️ マウスでクリックした場所にバスケットが瞬時に移動する
  • 🍎 リンゴを取ると +100ポイント
  • 💣 ボムを取るとスコアが半分になる!
  • ⏱️ 30秒の制限時間内でスコアを競う
  • 📈 残り時間に応じてリンゴの速度・間隔・ボム出現率が変化する

落ちてくるリンゴをバスケットでキャッチする様子(イメージ)

たった 4つのスクリプト(合計約100行) でこれが実現できます。3Dレイキャスト・タグ判定・サウンド・時間管理・動的難易度設計という実践的な要素が一気に学べます!


プロジェクトの構成

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

Assets/
  ├── BasketController.cs   # バスケットの移動とアイテム取得を制御
  ├── GameDirector.cs       # タイマー・スコア・難易度を管理
  ├── ItemController.cs     # リンゴ・ボムの落下と削除を制御
  ├── ItemGenerator.cs      # リンゴ・ボムをランダムに生成
  ├── applePrefab           # リンゴのプレハブ(Tag: Apple)
  └── bombPrefab            # ボムのプレハブ(Tag: Bomb)

4つのスクリプトが明確に役割分担しています。「バスケット操作」「ゲーム全体管理」「アイテム落下」「アイテム生成」——それぞれが独立しており、修正やカスタマイズがしやすい設計です。


コードを全部見てみよう

BasketController.cs(バスケットの制御)

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

public class BasketController : MonoBehaviour
{
    public AudioClip appleSE;
    public AudioClip bombSE;
    AudioSource aud;
    GameObject director;

    void Start()
    {
        Application.targetFrameRate = 60;
        this.aud = GetComponent<AudioSource>();
        this.director = GameObject.Find("GameDirector");
    }

    void OnTriggerEnter(Collider other)
    {
        if (other.gameObject.tag == "Apple")
        {
            this.aud.PlayOneShot(this.appleSE);
            this.director.GetComponent<GameDirector>().GetApple();
        }
        else
        {
            this.aud.PlayOneShot(this.bombSE);
            this.director.GetComponent<GameDirector>().GetBomb();
        }
        Destroy(other.gameObject);
    }

    void Update()
    {
        if (Mouse.current.leftButton.wasPressedThisFrame)
        {
            Ray ray = Camera.main.ScreenPointToRay(Mouse.current.position.value);
            RaycastHit hit;
            if (Physics.Raycast(ray, out hit, Mathf.Infinity))
            {
                float x = Mathf.RoundToInt(hit.point.x);
                float z = Mathf.RoundToInt(hit.point.z);
                transform.position = new Vector3(x, 0, z);
            }
        }
    }
}

GameDirector.cs(ゲームの管理)

using UnityEngine;
using TMPro;    // TextMeshProを使う時は忘れないように注意!!

public class GameDirector : MonoBehaviour
{
    GameObject timerText;
    GameObject pointText;
    float time = 30.0f;
    int point = 0;
    GameObject generator;

    public void GetApple()
    {
        this.point += 100;
    }

    public void GetBomb()
    {
        this.point /= 2;
    }

    void Start()
    {
        this.timerText = GameObject.Find("Time");
        this.pointText = GameObject.Find("Point");
        this.generator = GameObject.Find("ItemGenerator");
    }

    void Update()
    {
        this.time -= Time.deltaTime;

        if (this.time < 0)
        {
            this.time = 0;
            this.generator.GetComponent<ItemGenerator>().SetParameter(10000.0f, 0, 0);
        }
        else if (0 <= this.time && this.time < 4)
        {
            this.generator.GetComponent<ItemGenerator>().SetParameter(0.3f, -0.06f, 0);
        }
        else if (4 <= this.time && this.time < 12)
        {
            this.generator.GetComponent<ItemGenerator>().SetParameter(0.5f, -0.05f, 6);
        }
        else if (12 <= this.time && this.time < 23)
        {
            this.generator.GetComponent<ItemGenerator>().SetParameter(0.8f, -0.04f, 4);
        }
        else if (23 <= this.time && this.time < 30)
        {
            this.generator.GetComponent<ItemGenerator>().SetParameter(1.0f, -0.03f, 2);
        }

        this.timerText.GetComponent<TextMeshProUGUI>().text = this.time.ToString("F1");
        this.pointText.GetComponent<TextMeshProUGUI>().text = this.point.ToString() + " point";
    }
}

ItemController.cs(アイテムの落下)

using UnityEngine;

public class ItemController : MonoBehaviour
{
    public float dropSpeed = -0.03f;

    void Update()
    {
        transform.Translate(0, this.dropSpeed, 0);
        if (transform.position.y < -1.0f)
        {
            Destroy(gameObject);
        }
    }
}

ItemGenerator.cs(アイテムの生成)

using UnityEngine;

public class ItemGenerator : MonoBehaviour
{
    public GameObject applePrefab;
    public GameObject bombPrefab;
    float span = 1.0f;
    float delta = 0;
    int ratio = 2;
    float speed = -0.03f;

    public void SetParameter(float span, float speed, int ratio)
    {
        this.span = span;
        this.speed = speed;
        this.ratio = ratio;
    }

    void Update()
    {
        this.delta += Time.deltaTime;
        if (this.delta > this.span)
        {
            this.delta = 0;
            GameObject item;
            int dice = Random.Range(1, 11);
            if (dice <= this.ratio)
            {
                item = Instantiate(bombPrefab);
            }
            else
            {
                item = Instantiate(applePrefab);
            }
            float x = Random.Range(-1, 2);
            float z = Random.Range(-1, 2);
            item.transform.position = new Vector3(x, 4, z);
            item.GetComponent<ItemController>().dropSpeed = this.speed;
        }
    }
}

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


ポイント①:Physics.Raycast() で3D空間のクリック位置を取得する

Ray ray = Camera.main.ScreenPointToRay(Mouse.current.position.value);
RaycastHit hit;
if (Physics.Raycast(ray, out hit, Mathf.Infinity))
{
    float x = Mathf.RoundToInt(hit.point.x);
    float z = Mathf.RoundToInt(hit.point.z);
    transform.position = new Vector3(x, 0, z);
}

chapter7では ray.direction を使って「方向」を取り出しましたが、今回は Physics.Raycast() で光線が3D空間のどこに当たったかを取得しています。

ScreenPointToRay()  → クリックから光線(Ray)を作る
Physics.Raycast()   → 光線がどこかに当たったかを調べる
hit.point           → 当たった3D座標(Vector3)
用途 使う技術
クリックした「方向」に飛ばす(発射) ray.direction
クリックした「場所」を特定する(移動・配置) Physics.Raycast() + hit.point

out hit は「関数の外に結果を書き出す」という書き方です。RaycastHit 型の変数 hit に当たった情報(座標・法線・オブジェクト名など)が格納されます。


ポイント②:Mathf.RoundToInt() でグリッドにスナップする

float x = Mathf.RoundToInt(hit.point.x);
float z = Mathf.RoundToInt(hit.point.z);
transform.position = new Vector3(x, 0, z);

hit.point.x はクリックした場所のX座標ですが、小数点以下の細かい値(例:1.73f)になります。Mathf.RoundToInt() で整数に丸めることで、バスケットが整数座標(-1、0、1、2…)にピタッとはまるグリッドスナップを実現しています。

クリック座標 x = 1.73f → RoundToInt → 2
クリック座標 x = 0.32f → RoundToInt → 0
クリック座標 x = -0.6f → RoundToInt → -1
メソッド 動作 例(1.7の場合)
Mathf.RoundToInt() 四捨五入して整数に 2
Mathf.FloorToInt() 切り捨てて整数に 1
Mathf.CeilToInt() 切り上げて整数に 2

ポイント③:tag でオブジェクトの種類を判別する

void OnTriggerEnter(Collider other)
{
    if (other.gameObject.tag == "Apple")
    {
        this.aud.PlayOneShot(this.appleSE);
        this.director.GetComponent<GameDirector>().GetApple();
    }
    else
    {
        this.aud.PlayOneShot(this.bombSE);
        this.director.GetComponent<GameDirector>().GetBomb();
    }
    Destroy(other.gameObject);
}

gameObject.tag はUnityエディターでGameObjectに設定した「タグ」文字列です。リンゴのプレハブには "Apple"、ボムのプレハブには "Bomb"(または "Apple" 以外)のタグを設定しておきます。

バスケットに何かが触れた(OnTriggerEnter)
  └─ タグが "Apple"
       ├─ YES → りんごSE再生 + GetApple()
       └─ NO  → ボムSE再生 + GetBomb()
  └─ 触れたオブジェクトを Destroy

GetComponent<>() で取得したコンポーネントのプロパティで判別するよりも、タグを使う方がシンプルで高速です。複数の種類のオブジェクトを区別するときに便利なテクニックです。


ポイント④:AudioSource.PlayOneShot() で効果音を再生する

public AudioClip appleSE;
public AudioClip bombSE;
AudioSource aud;

void Start()
{
    this.aud = GetComponent<AudioSource>();
}

this.aud.PlayOneShot(this.appleSE);

効果音の再生には AudioSource コンポーネントを使います。PlayOneShot() は指定した AudioClip今すぐ1回再生するメソッドです。

メソッド 特徴
AudioSource.Play() アタッチされたAudioClipを再生。重ねて呼ぶと最初からやり直し
AudioSource.PlayOneShot(clip) 指定のClipを重ね掛け再生できる

PlayOneShot() は短時間に何度も呼んでも重ね掛けで再生されるため、連続してアイテムを取った時でも効果音が途切れません。public AudioClip appleSEpublic にすることで、Inspectorから音声ファイルをドラッグ&ドロップして設定できます。


ポイント⑤:タイマーで難易度を段階的に変化させる

this.time -= Time.deltaTime;

if (this.time < 0)
{
    this.generator.GetComponent<ItemGenerator>().SetParameter(10000.0f, 0, 0);
}
else if (0 <= this.time && this.time < 4)
{
    this.generator.GetComponent<ItemGenerator>().SetParameter(0.3f, -0.06f, 0);
}
else if (4 <= this.time && this.time < 12)
{
    this.generator.GetComponent<ItemGenerator>().SetParameter(0.5f, -0.05f, 6);
}
// ...(以下省略)

残り時間に応じてアイテムの生成パラメータを変えることで、ゲームが進むにつれて難しくなる仕組みを実現しています。

残り時間 生成間隔(span) 落下速度(speed) ボム率(ratio/10)
23〜30秒 1.0秒 遅い(-0.03) 20%
12〜23秒 0.8秒 やや速い(-0.04) 40%
4〜12秒 0.5秒 速い(-0.05) 60%
0〜4秒 0.3秒 最速(-0.06) 0%(ラストチャンス!)
終了後 10000秒 止まる 0%(生成停止)

終了直前(残り4秒未満)はボム率が0になり、最後にリンゴを稼ぐチャンスになっています。単純な「一定速度」ではなく、ゲームデザインとしての緩急がコードで表現されています。


ポイント⑥:SetParameter() で外部からパラメータを一括変更する

// ItemGenerator.cs
public void SetParameter(float span, float speed, int ratio)
{
    this.span = span;
    this.speed = speed;
    this.ratio = ratio;
}

GameDirector から ItemGenerator の3つのパラメータを一度に更新するための公開メソッドです。GameDirector が直接 ItemGenerator のフィールドを触るのではなく、メソッドを介して変更するのが重要です。

GameDirector(管理役)
  └─ SetParameter(0.5f, -0.05f, 6) を呼ぶ

  ItemGenerator(実行役)
    └─ span=0.5、speed=-0.05、ratio=6 に更新

このパターンによって ItemGenerator の内部構造(フィールド名など)が変わっても、SetParameter の引数さえ合っていれば GameDirector のコードは修正不要です。


ポイント⑦:確率でボムとリンゴを出し分ける

int dice = Random.Range(1, 11);  // 1〜10のランダム整数
if (dice <= this.ratio)
{
    item = Instantiate(bombPrefab);  // ratio/10 の確率でボム
}
else
{
    item = Instantiate(applePrefab); // 残りはリンゴ
}

Random.Range(1, 11) で1〜10の整数をサイコロのように振り、ratio 以下ならボムを生成します。

ratio = 2 の時: dice が 12 → ボム(20%)、310 → リンゴ(80%
ratio = 6 の時: dice が 16 → ボム(60%)、710 → リンゴ(40%
ratio = 0 の時: dice が 10 → 絶対ボムにならない(0%

ratio の値を変えるだけで出現確率を簡単に調整できる、シンプルで汎用的な確率設計です。


ポイント⑧:生成時にコンポーネントのパラメータを注入する

item.GetComponent<ItemController>().dropSpeed = this.speed;

Instantiate() でアイテムを生成した直後に、そのアイテムの ItemControllerdropSpeed を上書きしています。

Instantiate(applePrefab)           → プレハブのデフォルト値(-0.03f)で生成
.GetComponent<ItemController>()    → 生成したアイテムのコンポーネントを取得
.dropSpeed = this.speed            → GameDirector が指定した速度に書き換える

これにより、1種類のプレハブを使いながら「速度だけが異なる複数のアイテム」を動的に生成できます。ゲームが進むほど速いアイテムが落ちてくる、という体験はこの仕組みで実現しています。


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

【BasketController】          【GameDirector】
バスケットにアタッチ          空のGameObjectにアタッチ
   ↓                              ↓
クリック座標を Raycast で取得   タイマーのカウントダウン
グリッドスナップして移動        スコアの計算(GetApple/GetBomb)
アイテムとの衝突を検知          残り時間でSetParameterを呼び分け
タグでリンゴ・ボムを判別        タイマー・スコアをUI表示
効果音を再生

【ItemController】           【ItemGenerator】
各アイテムにアタッチ          空のGameObjectにアタッチ
   ↓                              ↓
毎フレーム下に移動            Time.deltaTimeで生成タイミング管理
画面外で自己破棄              確率でリンゴ/ボムを選択
dropSpeedで速さを可変         Instantiate して位置・速度を設定

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

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

① クリックされた?
   └─ YES → ScreenPointToRay → Physics.Raycast
            → hit.point の x・z を RoundToInt で整数化
            → transform.position でバスケットをグリッド移動

② 何かが触れた時(OnTriggerEnter)
   └─ タグが "Apple"
        ├─ YES → appleSE 再生 + GameDirector.GetApple()
        └─ NO  → bombSE 再生 + GameDirector.GetBomb()
   └─ Destroy(other.gameObject)

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

① time -= Time.deltaTime(カウントダウン)
② 残り時間に応じて ItemGenerator.SetParameter() を呼ぶ
③ タイマーとスコアをUIに表示

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

① delta += Time.deltaTime
② delta > span ?
   └─ YES → delta=0 → サイコロを振る
            → ボム or リンゴを Instantiate
            → ランダム座標に配置
            → dropSpeed を設定

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

挑戦①:ボムのペナルティを変える

public void GetBomb()
{
    this.point /= 2;  // -= 50 にするとマイナス50ポイントに
}

挑戦②:制限時間を変える

float time = 30.0f;  // 60.0f にすると1分間のゲームに

挑戦③:スコアに応じてボーナスを追加する

public void GetApple()
{
    this.point += 100;
    if (this.point >= 1000)
    {
        this.point += 50; // 1000点以上なら50ポイントボーナス!
    }
}

挑戦④:時間切れをUIで表示する

if (this.time < 0)
{
    this.time = 0;
    this.timerText.GetComponent<TextMeshProUGUI>().text = "TIME UP!";
    this.generator.GetComponent<ItemGenerator>().SetParameter(10000.0f, 0, 0);
}

まとめ

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

  • Physics.Raycast() で光線が3D空間の「どこに当たったか」(座標)を取得できる
  • Mathf.RoundToInt() で小数を整数に丸めてグリッドスナップを実現できる
  • gameObject.tag でオブジェクトの種類をシンプルに判別できる
  • AudioSource.PlayOneShot(clip) で重ね掛けに対応した効果音を再生できる
  • Time.deltaTime を引き続けてタイマーを実装し、残り時間で難易度を段階的に変化させられる
  • SetParameter() のような公開メソッドを使うと、外部スクリプトから複数パラメータを一括更新できる
  • Random.Range(1, 11) とratioの比較でシンプルな確率制御ができる
  • Instantiate() 直後にコンポーネントの値を書き換えることで、同じプレハブから異なる挙動のオブジェクトを生成できる

4つのスクリプトが連携することで、タイマー・スコア・確率・動的難易度という本格的なゲームシステムが完成しました。「動く→コードを読む→カスタマイズする」 のサイクルを繰り返すことが、Unity上達の近道です。

次のステップとして、スコアランキング機能や、バースト(連続キャッチ)ボーナスなどを追加してみましょう!


最終更新:2026年4月