Unity 6でAppleCatchを作る② ― アイテムを降らせて、得点・時間とレベルデザインで仕上げる(PC版)
この記事は、北村愛実 著『Unityの教科書 Unity 6完全対応版』(SBクリエイティブ)で扱われているAppleCatch(第8章)制作を題材に、授業でつまずきやすい点や、コードの意味を補足するために作成した学習記事です。コードおよび制作手順の詳細は、書籍もあわせてご参照ください。
前回は、クリックしたマスへバスケットを動かし、落ちてくるリンゴや爆弾をキャッチして効果音を鳴らすところまで作りました。
Unity 6でAppleCatchを作る① ― クリックしたマスへ動かして、リンゴをキャッチする(PC版)
今回は、リンゴと爆弾をランダムに降らせる工場、得点と残り時間のUI、そして時間がたつほどゲームが難しくなるレベルデザインを作って、AppleCatchを完成させます。
| 回 | 内容 |
|---|---|
| 前回(①) | ステージの準備/クリックしたマスへバスケットを動かす(Raycast)/アイテムを落とす/キャッチして音を鳴らす |
| 今回(②) | アイテムをランダムに降らせる/得点と残り時間のUI/時間に合わせて難しくする(レベルデザイン) |
この記事で作るもの
- 3×3のマスのどこかに、リンゴや爆弾がランダムに降ってくる
- リンゴをキャッチすると+100点、爆弾をキャッチすると得点が半分になる
- 制限時間は30秒。時間がたつほど、たくさん・速く・爆弾多めに降ってくる
1. アイテム工場を作る(ItemGenerator)
前回、動作確認のためにシーンに置いたリンゴと爆弾は、Hierarchyから削除しておきます(Prefabは残しておきます)。
空のオブジェクトを作って名前を ItemGenerator にし、同じ名前のスクリプトを作って付けます。
ステップ1:1秒ごとに、ランダムな場所へ降らせる
using UnityEngine;
public class ItemGenerator : MonoBehaviour
{
public GameObject applePrefab;
public GameObject bombPrefab;
float span = 1.0f; // 何秒ごとに作るか
float delta = 0; // 前に作ってからの経過時間
int ratio = 2; // 爆弾が出る割合(10のうちいくつ)
float speed = -0.03f; // 落ちる速さ
void Update()
{
this.delta += Time.deltaTime;
if (this.delta > this.span)
{
this.delta = 0;
GameObject item;
// 1〜10のサイコロを振って、ratio以下なら爆弾
int dice = Random.Range(1, 11);
if (dice <= this.ratio)
{
item = Instantiate(bombPrefab);
}
else
{
item = Instantiate(applePrefab);
}
// x と z を -1、0、1 のどれかにして、マスの真上に置く
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;
}
}
}
保存したら、ItemGeneratorのInspectorで Apple Prefab に applePrefab、Bomb Prefab に bombPrefab をドラッグ&ドロップします。
「delta に経過時間をためて、span をこえたら作る」という形は、CatEscapeの矢の工場と同じです。
Unity 6でCatEscapeを作る① ― 猫の移動と、Prefabで矢を降らせる仕組み(PC版)

サイコロで「爆弾の割合」を決める
Random.Range に整数を渡すと、最後の数は含まれません。CatEscapeで矢の位置を決めたときにも出てきた決まりです。
| 書き方 | 出てくる値 | 使い道 |
|---|---|---|
Random.Range(1, 11) | 1〜10のどれか | 10面のサイコロ |
Random.Range(-1, 2) | -1、0、1のどれか | 3×3のマスの位置(xとz) |
サイコロの目が ratio 以下なら爆弾にしているので、ratio が2なら「1か2が出たら爆弾」で、爆弾の確率は10分の2(20%)です。ratio を変えるだけで、爆弾の出やすさを調整できます。
x と z を -1、0、1 にしているのは、前回バスケットが移動できるようにした3×3のマスと、ちょうど同じ位置にするためです。
2. 得点と残り時間のUIを作る
Hierarchyの + → UI → Text – TextMeshPro でテキストを2つ作ります。初めてTextMeshProを使うときは、「TMP Importer」のウィンドウが出るので、Import TMP Essentials を押してから閉じます。
| 名前 | アンカー | Pos X / Pos Y | Width / Height | 最初の文字 |
|---|---|---|---|---|
Time | 右上 | -170 / -70 | 250 / 100 | 30.0 |
Point | 右上 | -270 / -180 | 450 / 100 | 0 point |
文字の大きさ(Font Size)は、サンプルでは大きめの84にしています。名前の Time と Point は、このあとスクリプトから GameObject.Find で探すので、1文字も違わないように付けましょう。
3. 監督を作る(GameDirector)
空のオブジェクトを作って名前を GameDirector にし、同じ名前のスクリプトを作って付けます。まずは、得点と残り時間を表示するところまで作ります。
ステップ2:残り時間と得点を表示する
using UnityEngine;
using TMPro; // TextMeshProを使う時は忘れないように注意!!
public class GameDirector : MonoBehaviour
{
GameObject timerText;
GameObject pointText;
float time = 30.0f;
int point = 0;
// リンゴをキャッチしたとき
public void GetApple()
{
this.point += 100;
}
// 爆弾をキャッチしたとき
public void GetBomb()
{
this.point /= 2;
}
void Start()
{
this.timerText = GameObject.Find("Time");
this.pointText = GameObject.Find("Point");
}
void Update()
{
this.time -= Time.deltaTime;
if (this.time < 0)
{
this.time = 0;
}
this.timerText.GetComponent<TextMeshProUGUI>().text = this.time.ToString("F1");
this.pointText.GetComponent<TextMeshProUGUI>().text = this.point.ToString() + " point";
}
}
| 書き方 | 意味 |
|---|---|
using TMPro; | TextMeshProを使うための1行。書き忘れると TextMeshProUGUI が見つからないエラーになる |
this.time.ToString("F1") | 小数点以下1けたの文字にする(例:23.456 → “23.5") |
this.point /= 2; | point = point / 2; と同じ。得点を半分にする |
point は int(整数)なので、割り算の結果の小数点以下は切り捨てられます。たとえば、300点で爆弾を取ると150点、もう一度取ると75点、さらに取ると37点(37.5ではない)になります。
4. バスケットから監督に知らせる
前回 Debug.Log にしておいた部分を、監督のメソッドを呼ぶ形に書き換えます。CatEscapeで、矢から監督の DecreaseHp を呼んだのと同じ仕組みです。
Unity 6でCatEscapeを作る② ― 当たり判定とHPゲージ、スクリプト同士の連携(PC版)
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);
}
}
}
}
再生すると、リンゴを取るたびに100点ずつ増え、爆弾を取ると半分になります。残り時間は0.0で止まります。これで「ゲーム」としては遊べる形になりました。
5. レベルデザイン ― 時間がたつほど難しくする
今のままだと、30秒間ずっと同じペースでアイテムが降ってきます。最後まで同じだと、だんだん退屈になってしまいます。そこで、残り時間に合わせて、降ってくる間隔・速さ・爆弾の割合を変えるようにします。このように、ゲームの難しさや手ごたえを調整することをレベルデザインといいます。
ステップ3:工場に「設定を変える窓口」を作る
ItemGeneratorに、外から設定を変えるための SetParameter メソッドを追加します。
// 外から、間隔・速さ・爆弾の割合をまとめて変える
public void SetParameter(float span, float speed, int ratio)
{
this.span = span;
this.speed = speed;
this.ratio = ratio;
}
引数の名前(span)と変数の名前(this.span)が同じなので、this. を付けて「このスクリプトの変数」のほうを指定しています。
ステップ4:監督が、残り時間に合わせて設定を変える(完成)
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";
}
}

数字で見るレベルデザイン
それぞれの時間帯の設定を、表にまとめてみます。「落ちるまでの時間」は、y = 4 からカゴの口(y = 0.55 あたり)まで、60fpsで落ちたときの目安です。
| 残り時間 | 間隔(span) | 落ちるまで | 爆弾の確率 | 降ってくる数(目安) |
|---|---|---|---|---|
| 30〜23秒 | 1.0秒 | 約1.9秒 | 20% | 約7個 |
| 23〜12秒 | 0.8秒 | 約1.4秒 | 40% | 約14個 |
| 12〜4秒 | 0.5秒 | 約1.2秒 | 60% | 約16個 |
| 4〜0秒 | 0.3秒 | 約1.0秒 | 0% | 約13個 |
| 0秒(終了) | 10000秒 | ― | ― | もう降らない |
時間がたつほど、間隔は短く、落ちる速さは速く、爆弾は多くなっていきます。ところが、最後の4秒だけは爆弾が0%で、リンゴが次々に降ってきます。最後に一気に得点を取り返せる「ボーナスタイム」です。
ずっと難しくなるだけだと、途中で爆弾を取ってしまった人はやる気をなくしてしまいます。最後に逆転のチャンスを用意しておくことで、最後まで集中して遊べるようになっています。数値を変えて、自分なりのレベルデザインを試してみましょう。
時間切れのときに間隔を10000秒にしているのは、「実際にはもう二度と作らない」ようにするための工夫です。すでに落ちているアイテムは、そのまま落ちて消えます。
6. もう一歩:GetComponent を毎フレーム呼ばない
完成版の GameDirector では、Update の中で毎フレーム GetComponent を呼んでいます。動きは正しいのですが、同じものを何度も探すのは少しもったいない書き方です。
CatEscapeで GameObject.Find を Start で1回だけ呼んだのと同じように、GetComponent の結果も Start で取っておくことができます。
TextMeshProUGUI timerText;
TextMeshProUGUI pointText;
ItemGenerator generator;
void Start()
{
this.timerText = GameObject.Find("Time").GetComponent<TextMeshProUGUI>();
this.pointText = GameObject.Find("Point").GetComponent<TextMeshProUGUI>();
this.generator = GameObject.Find("ItemGenerator").GetComponent<ItemGenerator>();
}
// Update の中では、次のように短く書ける
// this.generator.SetParameter(1.0f, -0.03f, 2);
// this.timerText.text = this.time.ToString("F1");
変数の型を GameObject ではなく、使いたいコンポーネントの型(TextMeshProUGUI、ItemGenerator)にしておくのがポイントです。教科書のコードで動きを確かめたら、こうした書き換えにも挑戦してみましょう。
7. よくあるつまずき
TextMeshProUGUI が見つからないエラーが出る
error CS0246: The type or namespace name 'TextMeshProUGUI' could not be found (are you missing a using directive or an assembly reference?)
using TMPro; を書き忘れています。スクリプトの一番上に追加しましょう。
NullReferenceException が出る
GameObject.Find("Time")などの名前と、Hierarchyのオブジェクトの名前が違う(大文字・小文字、空白にも注意)- UIを「Legacy」のTextで作ってしまい、
TextMeshProUGUIが付いていない GameDirectorやItemGeneratorという名前の空のオブジェクトがない、またはスクリプトが付いていない
アイテムが降ってこない
- ItemGeneratorの Apple Prefab/Bomb Prefab 欄が空のまま(UnassignedReferenceException が出る)
- Prefabに
ItemControllerが付いていない(GetComponent<ItemController>()で NullReferenceException が出る)
得点が増えない
BasketControllerの OnTriggerEnter が前回の Debug.Log のままになっていないか、director を Start で探しているかを確認しましょう。
まとめ
Random.Range(1, 11)で1〜10のサイコロを作り、ratio以下なら爆弾にすると、出る割合を調整できる- 整数の
Random.Range(-1, 2)は -1、0、1 のどれか。3×3のマスの位置にぴったり合う - TextMeshProを使うときは
using TMPro;を忘れずに。ToString("F1")で小数点以下1けたにできる - 工場に
SetParameterという窓口を作り、監督が残り時間に合わせて呼ぶことで、レベルデザインができる - 難しくするだけでなく、最後に「ボーナスタイム」を用意するなど、遊ぶ人の気持ちを考えて数値を決めるのがレベルデザイン
これで、教科書の5つのゲーム(ルーレット、SwipeCar、CatEscape、ClimbCloud、Igaguri)に続いて、AppleCatchも完成です。2Dでも3Dでも、「物理」「Prefabと工場」「スクリプトどうしの連携」「UI」という同じ考え方の組み合わせでゲームができていることを、ぜひ振り返ってみてください。
このシリーズの記事一覧(全10本)は、Unity 6 ゲーム制作 補足解説シリーズまとめからご覧いただけます。
参考書籍
北村愛実 著
『Unityの教科書 Unity 6完全対応版』
SBクリエイティブ
書籍サポートページ(サンプルファイル)
本記事では、書籍で扱われているAppleCatch(第8章)制作を題材に、初学者向けの補足説明やUnity 6での学習上のポイントを加えて解説しています。











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