【Unity 6】AppleCatchのスクリプトを整理しよう ― フォルダ分けとコードの読みやすさ改善
この記事は、北村愛実 著『Unityの教科書 Unity 6完全対応版』(SBクリエイティブ)の第8章サンプル「AppleCatch」をもとにした補足学習記事です。ゲームの作り方やコードの詳しい解説は書籍を参照してください。ここでは「作り終えたあとの整理」だけを扱います。
- 対象: AppleCatch(PC版)を最後まで作り終えた方
- 環境: Unity 6(6000.1.1f1)、Input System 1.14.0
- ゴール: ゲームの動きは変えずに、①フォルダ構成と②コードの読みやすさを整える
動いているコードを書き換えるので、始める前にプロジェクトフォルダごとコピーするか、Gitでコミットしておきましょう。うまくいかなければ、いつでも元に戻せます。
なお、整理(リファクタリング)とは「見た目と中身の構造を整えること」で、ゲームの動作は変わりません。この記事のあとにプレイして、整理前と同じ動きになっていれば成功です。
現状を確認する
教科書どおりに作ると、ProjectウィンドウのAssets直下にスクリプト・モデル・音・プレハブ・シーンが全部並んでいます。
Assets/
├─ BasketController.cs
├─ GameDirector.cs
├─ ItemController.cs
├─ ItemGenerator.cs
├─ applePrefab.prefab
├─ bombPrefab.prefab
├─ apple.fbx / basket.fbx / bomb.fbx / stage.fbx
├─ get_se.mp3 / damage_se.mp3
├─ GameScene.unity
├─ Scenes/ (SampleScene)
├─ Settings/ TextMesh Pro/ TutorialInfo/
└─ InputSystem_Actions.inputactions / Readme.asset
このゲームには4本のスクリプトがあり、それぞれ役割が違います。先に役割をつかんでおくと、整理の辿り着き先が見えやすくなります。

| スクリプト | 役割 | 付いている場所 |
|---|---|---|
| BasketController | カゴの移動と、アイテムとの接触処理 | カゴ |
| GameDirector | 制限時間・ポイント・UI表示、難易度の切り替え | GameDirector |
| ItemGenerator | リンゴと爆弾を一定間隔で生成 | ItemGenerator |
| ItemController | アイテムの落下と、画面外での削除 | リンゴ・爆弾のプレハブ |
この状態だと、素材が増えるほど目的のファイルを探すのが大変になります。スクリプトの中身にも、後から読むと「この数字は何」「この public は本当に必要?」と迷う箇所があります。
手順1: フォルダを作ってファイルを分類する
まずはフォルダ分けです。必ずUnityのProjectウィンドウの中でドラッグ&ドロップして移動してください。Finderやエクスプローラーで動かすと、トラブルの原因になります(理由は次の見出しで説明します)。

- ProjectウィンドウでAssetsを右クリック → Create → Folder で、
Scripts、Prefabs、Models、Audioの4つを作ります(Scenesはすでにあります) - 下の表のとおりに、ファイルをドラッグ&ドロップします
- 移動が終わったら、Consoleウィンドウにエラーが出ていないことを確認します
| 移動するファイル | 移動先 |
|---|---|
| BasketController.cs / GameDirector.cs / ItemController.cs / ItemGenerator.cs | Scripts |
| applePrefab / bombPrefab | Prefabs |
| apple.fbx / basket.fbx / bomb.fbx / stage.fbx | Models |
| get_se.mp3 / damage_se.mp3 | Audio |
| GameScene | Scenes(既存のフォルダ) |
| Settings / TextMesh Pro / TutorialInfo / InputSystem_Actions / Readme | 動かさない |
最後の行のファイルは、Unityやパッケージが自動で作って管理しているものです。中身をよくわからないうちは触らないのが安全です。
整理後は次のようになります。
Assets/
├─ Scripts/ BasketController.cs GameDirector.cs ItemController.cs ItemGenerator.cs
├─ Prefabs/ applePrefab bombPrefab
├─ Models/ apple.fbx basket.fbx bomb.fbx stage.fbx
├─ Audio/ get_se.mp3 damage_se.mp3
├─ Scenes/ GameScene SampleScene
├─ Settings/ TextMesh Pro/ TutorialInfo/
└─ InputSystem_Actions.inputactions / Readme.asset
ここで一度ゲームを再生してみてください。ファイルの場所を変えただけなので、今までどおり動くはずです。
なぜ移動しても壊れないのか
Unityは、ファイルごとに .meta ファイルを作って専用のID(GUID)を記録しています。シーンやプレハブは「ファイルの場所」ではなくこのGUIDでスクリプトやプレハブを参照しているので、Unityの中で移動すれば .meta も一緒に動き、参照は切れません。逆に、Unityの外で .cs だけを動かして .meta を置いていくと、GUIDが変わって「The referenced script on this Behaviour is missing」という警告が出ます。
Gitで管理する場合も、.meta は必ず .cs やフォルダと一緒にコミットします。シーンを移動したときも、Build Profiles の Scene List は同じ仕組みで追従するので、登録し直しは不要です。
手順2: 4本のスクリプトを順に整える
スクリプトは、次の4つのルールだけでずいぶん読みやすくなります。
- 並べる順番を決める: 定数 → Inspectorで設定する値 → 内部の状態 → Start/Updateなど → 他のスクリプトから呼ぶ public メソッド → 自分専用のメソッド
- Inspectorで設定するだけの値は
publicをやめて[SerializeField]にする: 他のスクリプトから勝手に書き換えられなくなります - 意味のある数字には名前をつける:
30.0fや100をconstにします - 長いメソッドは役割ごとに分ける: あわせて、毎フレーム呼ぶ
GetComponentはStartで一度だけにします

注意点があります。変数の名前を変えると、Inspectorに設定済みの値(プレハブや効果音など)が外れてしまいます。ここでは applePrefab や appleSE など、元の名前をそのまま使います。
スクリプト同士が関係しているので、4本とも書き換え終わってから保存・コンパイルしてください。途中でエラーが出ても、全部終えれば消えます。
2-1. ItemController(最もシンプル)
dropSpeedは ItemGenerator から直接代入されていた。[SerializeField]の非公開にし、SetDropSpeedを通して変更する形にする-1.0fに「削除する高さ」という名前をつける
using UnityEngine;
/// <summary>
/// 落ちてくるアイテム(リンゴ・爆弾)の動きを担当する
/// </summary>
public class ItemController : MonoBehaviour
{
const float DestroyY = -1.0f; // この高さより下に落ちたら消す
[SerializeField] float dropSpeed = -0.03f; // 1フレームあたりの落下量(マイナスが下向き)
void Update()
{
transform.Translate(0, this.dropSpeed, 0);
if (transform.position.y < DestroyY)
{
Destroy(gameObject);
}
}
// ItemGeneratorから落下速度を指定するためのメソッド
public void SetDropSpeed(float speed)
{
this.dropSpeed = speed;
}
}
2-2. ItemGenerator
applePrefab/bombPrefabを[SerializeField]にする(名前はそのまま)- Update の中の「アイテムを一つ作る」処理を
SpawnItemメソッドに切り出す if / elseで二回書いていたInstantiateを、三項演算子で一回にまとめる- 生成の高さ
4にSpawnHeightという名前をつける
using UnityEngine;
/// <summary>
/// リンゴと爆弾を一定間隔で生成する。難易度(間隔・速度・爆弾の割合)はGameDirectorが指定する
/// </summary>
public class ItemGenerator : MonoBehaviour
{
const float SpawnHeight = 4.0f; // アイテムを生成する高さ
[SerializeField] GameObject applePrefab;
[SerializeField] 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;
SpawnItem();
}
}
// GameDirectorから難易度を変更するためのメソッド
public void SetParameter(float span, float speed, int ratio)
{
this.span = span;
this.speed = speed;
this.ratio = ratio;
}
// アイテムを一つ生成して、ランダムな位置に置く
void SpawnItem()
{
bool isBomb = Random.Range(1, 11) <= this.ratio;
GameObject item = Instantiate(isBomb ? this.bombPrefab : this.applePrefab);
float x = Random.Range(-1, 2);
float z = Random.Range(-1, 2);
item.transform.position = new Vector3(x, SpawnHeight, z);
item.GetComponent<ItemController>().SetDropSpeed(this.speed);
}
}
2-3. BasketController
appleSE/bombSEを[SerializeField]にするdirectorをGameObjectではなくGameDirector型で持ち、Startで一度だけGetComponentする(接触のたびに探さない)other.gameObject.tag == "Apple"をother.CompareTag("Apple")にする。結果は同じで、Unityに推奨されている書き方- Update の中の「クリック位置へ移動」を
MoveToClickedPositionに切り出す - Unityイベント(
Start/Update/OnTriggerEnter)を上に、自作メソッドを下に並べる
using UnityEngine;
using UnityEngine.InputSystem; // 入力を検知するために必要!!
/// <summary>
/// カゴの移動(クリックした位置へ)と、アイテムとの接触処理を担当する
/// </summary>
public class BasketController : MonoBehaviour
{
[SerializeField] AudioClip appleSE;
[SerializeField] AudioClip bombSE;
AudioSource aud;
GameDirector director;
void Start()
{
Application.targetFrameRate = 60;
this.aud = GetComponent<AudioSource>();
this.director = GameObject.Find("GameDirector").GetComponent<GameDirector>();
}
void Update()
{
if (Mouse.current.leftButton.wasPressedThisFrame)
{
MoveToClickedPosition();
}
}
void OnTriggerEnter(Collider other)
{
if (other.CompareTag("Apple"))
{
this.aud.PlayOneShot(this.appleSE);
this.director.GetApple();
}
else
{
this.aud.PlayOneShot(this.bombSE);
this.director.GetBomb();
}
Destroy(other.gameObject);
}
// クリックした位置(整数座標に丸めて)にカゴを移動する
void MoveToClickedPosition()
{
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);
}
}
}
2-4. GameDirector(最も変更が大きい)
30.0f(制限時間)と100(リンゴの点数)に名前をつけるtimerText/pointText/generatorをGameObjectではなく、この後使う型(TextMeshProUGUI、ItemGenerator)で持ち、Startで一度だけGetComponentする。毎フレームのGetComponentがなくなる- 長い
Updateを「難易度を切り替える」UpdateDifficultyと「画面を更新する」UpdateUIに分ける 0 <= time && time < 4のように、上の条件ですでに否定されている下限のチェックをはずして、time < 4だけにする- 「時間切れ」を
isTimeUpという名前の変数にして、元のとおりtimeを 0 にそろえる前に判定する
using UnityEngine;
using TMPro; // TextMeshProを使う時は忘れないように注意!!
/// <summary>
/// ゲーム全体の進行(制限時間・ポイント・UI表示・難易度の切り替え)を管理する
/// </summary>
public class GameDirector : MonoBehaviour
{
const float TimeLimit = 30.0f; // 制限時間(秒)
const int ApplePoint = 100; // リンゴ1個で増えるポイント
float time = TimeLimit;
int point = 0;
// 他のオブジェクトへの参照(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>();
}
void Update()
{
this.time -= Time.deltaTime;
// 時間切れの判定は、0にそろえる前に行う
bool isTimeUp = this.time < 0;
if (isTimeUp)
{
this.time = 0;
}
UpdateDifficulty(isTimeUp);
UpdateUI();
}
// ItemControllerやBasketControllerから呼ばれる
public void GetApple()
{
this.point += ApplePoint;
}
public void GetBomb()
{
this.point /= 2;
}
// 残り時間に応じて、アイテムの出現間隔・速度・爆弾の割合を変える
void UpdateDifficulty(bool isTimeUp)
{
if (isTimeUp)
{
this.generator.SetParameter(10000.0f, 0, 0); // 実質、生成を止める
}
else if (this.time < 4)
{
this.generator.SetParameter(0.3f, -0.06f, 0);
}
else if (this.time < 12)
{
this.generator.SetParameter(0.5f, -0.05f, 6);
}
else if (this.time < 23)
{
this.generator.SetParameter(0.8f, -0.04f, 4);
}
else if (this.time < 30)
{
this.generator.SetParameter(1.0f, -0.03f, 2);
}
}
// 残り時間とポイントを画面に表示する
void UpdateUI()
{
this.timerText.text = this.time.ToString("F1");
this.pointText.text = this.point.ToString() + " point";
}
}
これで4本とも終わりです。保存してUnityに戻り、Consoleに赤いエラーが出ていないことを確認しましょう。
手順3: 動作確認とつまずきやすい点
ゲームを再生して、整理前と同じ動きになっているかを確認します。

- □ Consoleにエラーや警告(Missing / NullReference)が出ていない
- □ 残り時間が30.0から減っていく
- □ クリックした位置にカゴが移動する
- □ リンゴを取ると効果音が鳴り、100 point ずつ増える
- □ 爆弾を取ると効果音が鳴り、ポイントが半分になる
- □ 残り時間が減るにつれて、アイテムが速く・多くなり、爆弾も増える
- □ 0.0になったあと、新しいアイテムが生成されない
うまくいかないときは、次の表を見てください。
| 症状 | 原因 | 対処 |
|---|---|---|
| 「The referenced script on this Behaviour is missing」が出る | Unityの外でファイルを動かして .meta と分かれた | 元の場所に戻し、Unityの中で移動し直す |
| ItemGenerator で「dropSpeed にアクセスできません」とエラー | ItemController を更新して、ItemGenerator をまだ更新していない | 2-2 のコードに書き換える |
| Inspectorのプレハブや効果音が None になっている | 変数の名前を変えてしまった | applePrefab / bombPrefab / appleSE / bombSE の名前を元に戻す。済んでしまったら、Inspectorで設定し直す |
| NullReferenceException(Start内) | GameObject.Find で探す名前と、シーン上のオブジェクト名が違う | Time / Point / ItemGenerator / GameDirector の綴りを確認する |
| リンゴを取っても爆弾扱いになる | プレハブの Tag が設定されていない、または "Apple" の綴り違い | applePrefab の Tag を Apple にする |
まとめと発展課題
この記事では、ゲームの動きを変えずに次の整理を行いました。
- ファイルを Scripts / Prefabs / Models / Audio / Scenes に分類した(Unityの中で移動すれば
.metaが参照を守ってくれる) - スクリプトの並び順をそろえ、Inspector用の変数を
[SerializeField]にした - 意味のある数字に名前をつけ、長い
Updateを役割ごとのメソッドに分けた - 毎フレームの
GetComponentをStartでの一度きりにした
この種類の整理は、「動くようになった」直後にやっておくと、あとから機能を足すときの迷いが減ります。
発展課題
もう少し整理を進めたい方は、次の課題に挑戦してみてください。
UpdateDifficultyの数字(4・12・23・30 と、各段階のパラメータ)を、配列や構造体にまとめてif / else ifを短くするGameObject.Findをやめて、[SerializeField]でInspectorから参照を設定する形に変える- ゲームが大きくなったときに備えて、Scripts フォルダを役割ごとのサブフォルダに分ける
- 整理前の状態をGitでコミットしてから、ブランチを切って上の課題を試す(うまくいかなければブランチごと捨てられます)
参考書籍
- 北村愛実 著『Unityの教科書 Unity 6完全対応版』 SBクリエイティブ(第8章 AppleCatch)








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