GameObjectもC#の基礎も入力もUIも、記事は読んだ。なのに、新しいプロジェクトの白紙のSceneを前にすると——何から置けばいいのか分からない。部品の知識と「1本のゲームを完成させる」の間には、思っているより広い川があります。
この記事はその川に橋を架ける 総仕上げ(capstone) です。作るのは 「60秒以内にコインを5枚集める」1画面ゲーム。派手さはありませんが、タイトル・操作・取得・スコア・時間切れ・リザルト・リトライ・ビルドという、 どんなゲームにも必ずある背骨 が全部入っています。素材はCubeとSphereだけ、スクリプトは4本だけ。今日、 最後まで行きましょう。
この記事でわかること
- 白紙のプロジェクトから 遊べるビルド までの一本道
- 4スクリプト構成(PlayerMover / Coin / GameFlow / GameHud)の役割分担
- Triggerで コインを1回だけ 取る
- Title → Playing → Result → リトライ の状態管理
- スコアと残り時間の UI表示
- ビルドしてEditorの外で確認 するスモークテスト
動作確認環境: Unity 2022.3 LTS / Unity 6・Input System有効
今日つくるもの:完成図から始める
迷子にならないよう、先にゴールを見ておきます。画面はこうなります——床の上をプレイヤー(Capsule)が走り、コイン(黄色いSphere)が5枚。画面の上にスコアと残り時間。タイトルで「Start」を押すと60秒のカウントダウンが始まり、全部集めるかタイムアップでリザルトへ。「Retry」でもう一度。
スクリプトは4本で、役割を1つずつ持ちます。この「責務で分ける」感覚は C#クラス入門 で学んだ通りです。
| スクリプト | 役割 | それ以外はやらない |
|---|---|---|
PlayerMover | 入力を読んで動く | スコアも時間も知らない |
Coin | 取られたら消えて通知する | 何枚目かは知らない |
GameFlow | 進行のルール(状態・枚数・時間) | 画面表示はしない |
GameHud | 表示だけ | ルールの判断はしない |
準備:SceneとPrefabの最小構成
新規3Dプロジェクト(Input Systemが有効なテンプレート)を作り、Sceneに次を置きます。見た目づくりに時間を使わないのがコツです——主役は「完成させる体験」です。

- Ground: 3D Object > Plane。Scaleを (2, 1, 2) 程度に
- Player: 3D Object > Capsule。名前を
Playerに。 Rigidbody を追加し、Constraints > Freeze Rotation の X/Y/Zすべてにチェック(転がって倒れるのを防ぐ) - Coin: 3D Object > Sphere。Scale (0.5, 0.5, 0.5)、黄色いマテリアルを付け、Sphere Colliderの Is Trigger をオン。名前を
Coinに - CoinをProjectウィンドウへドラッグして Prefab化 し、Scene上に5枚ばらまく(Prefabの記事 の出番です)
- GameFlow: 空のGameObject。進行管理スクリプトの置き場所
- Canvas: UI > Text - TextMeshPro を作るとCanvasとEventSystemが自動で入ります(初回はTMPのImportダイアログが出るのでImport)
確認: Hierarchyに Ground / Player / Coin×5 / GameFlow / Canvas が並んでいて、再生してもエラーが出なければOKです。
PlayerMover:動かす
1本目のスクリプトです。キーボード(WASD/矢印)とゲームパッドの左スティックを読み、Updateで読んでFixedUpdateで物理に適用 の型どおりに動かします。
// PlayerMover.cs — Playerに付ける
using UnityEngine;
using UnityEngine.InputSystem;
[RequireComponent(typeof(Rigidbody))]
public class PlayerMover : MonoBehaviour
{
[SerializeField] private float moveSpeed = 6f;
private Rigidbody rb;
private Vector2 input;
void Awake()
{
rb = GetComponent<Rigidbody>();
}
void Update()
{
// 入力はUpdateで読む(キーボードとパッドの両対応)
input = Vector2.zero;
if (Keyboard.current != null)
{
if (Keyboard.current.wKey.isPressed || Keyboard.current.upArrowKey.isPressed) input.y += 1f;
if (Keyboard.current.sKey.isPressed || Keyboard.current.downArrowKey.isPressed) input.y -= 1f;
if (Keyboard.current.dKey.isPressed || Keyboard.current.rightArrowKey.isPressed) input.x += 1f;
if (Keyboard.current.aKey.isPressed || Keyboard.current.leftArrowKey.isPressed) input.x -= 1f;
}
if (Gamepad.current != null && input == Vector2.zero)
{
input = Gamepad.current.leftStick.ReadValue();
}
// 斜め移動が速くなりすぎないように長さを1までに抑える
input = Vector2.ClampMagnitude(input, 1f);
}
void FixedUpdate()
{
// 物理への適用はFixedUpdateで
Vector3 velocity = new Vector3(input.x, 0f, input.y) * moveSpeed;
velocity.y = rb.linearVelocity.y; // 重力はそのまま
rb.linearVelocity = velocity;
}
}
確認: 再生してWASDでカプセルが床の上を走ればOK。ゲームパッドがあればスティックでも動きます。
Coin:1回だけ取れる
2本目。コインは「プレイヤーが触れたら、1回だけ通知して消える」だけの部品です。Triggerの基礎 の実戦投入と、C#イベント による疎結合な通知がここで効きます。

// Coin.cs — CoinのPrefabに付ける
using System;
using UnityEngine;
public class Coin : MonoBehaviour
{
// 「コインが取られた」という全体通知(GameFlowが聞く)
public static event Action OnCollected;
private bool collected; // 二重取得ガード
void Update()
{
// ちょっとした存在感:くるくる回す
transform.Rotate(0f, 180f * Time.deltaTime, 0f);
}
void OnTriggerEnter(Collider other)
{
if (collected) return; // 同フレームの多重ヒットを弾く
if (!other.CompareTag("Player")) return; // プレイヤー以外は無視
collected = true;
OnCollected?.Invoke();
Destroy(gameObject);
}
}
Playerオブジェクトの Tagを Player に設定 するのを忘れずに(Inspector上 部のTagドロップダウン)。collected フラグは「Colliderが複数あっても、通知は1回だけ」の保険です——この「1回だけ」問題を深く掘ると ダメージ処理の記事 につながります。
確認: 再生してコインに突っ込むと、コインが消えます。まだ数は出ませんが、Consoleにエラーが出ないこと。
GameFlow:Title・Playing・Resultを回す
3本目が進行の心臓部です。ゲームの状態を Title → Playing → Result の3つで管理し、コインの枚数と残り時間のルールを握ります。この形は GameManagerパターン の最小版です。

// GameFlow.cs — 空のGameObject「GameFlow」に付ける
using UnityEngine;
using UnityEngine.SceneManagement;
public class GameFlow : MonoBehaviour
{
public enum State { Title, Playing, Result }
[SerializeField] private float timeLimit = 60f;
[SerializeField] private int targetCoins = 5;
[SerializeField] private PlayerMover player;
public State Current { get; private set; } = State.Title;
public int Coins { get; private set; }
public float TimeLeft { get; private set; }
public bool IsCleared { get; private set; }
void OnEnable()
{
Coin.OnCollected += HandleCoinCollected;
}
void OnDisable()
{
// staticイベントはSceneをまたいで残るので、解除は必須
Coin.OnCollected -= HandleCoinCollected;
}
void Start()
{
TimeLeft = timeLimit;
player.enabled = false; // タイトル中は動けない
}
void Update()
{
if (Current != State.Playing) return;
TimeLeft -= Time.deltaTime;
if (TimeLeft <= 0f)
{
TimeLeft = 0f;
FinishGame(cleared: false); // 時間切れ
}
}
public void StartGame() // タイトルのStartボタンから呼ぶ
{
Current = State.Playing;
player.enabled = true;
}
public void Retry() // リザルトのRetryボタンから呼ぶ
{
// Sceneごと読み直す=スコアもコインも全部が初期状態に戻る
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
}
void HandleCoinCollected()
{
if (Current != State.Playing) return; // プレイ中以外は数えない
Coins++;
if (Coins >= targetCoins)
{
FinishGame(cleared: true); // 全部集めた
}
}
void FinishGame(bool cleared)
{
IsCleared = cleared;
Current = State.Result;
player.enabled = false; // リザルト中は動けない
}
}
読みどころは3つあります。 開始前・終了後はプレイヤーを enabled = false で止める(Title中に走り回れない)。 プレイ中以外はコインを数えない(HandleCoinCollected の先頭ガード)。そして リトライはScene再読み込み——タイマーもコインも全部が確実に初期化されるので、「リトライ3回目でタイマーが2倍速になる」ような残留バグが原理的に起きません。その代償が OnDisable での購読解除で、これを忘れるとSceneを読み直すたびに古い購読が積み重なります。
GameHud:スコアと残り時間を見せる
最後の1本は表示係です。Canvasの下に次を作ります。
- ScoreText・TimeText(TextMeshPro): 画面の上に横並び
- TitlePanel: 半透明のPanel+「Start」Button
- ResultPanel: 半透明のPanel+結果表示用Text+「Retry」Button(最初は非表示でOK。スクリプトが制御します)
// GameHud.cs — Canvasに付ける
using TMPro;
using UnityEngine;
public class GameHud : MonoBehaviour
{
[SerializeField] private GameFlow gameFlow;
[SerializeField] private TextMeshProUGUI scoreText;
[SerializeField] private TextMeshProUGUI timeText;
[SerializeField] private GameObject titlePanel;
[SerializeField] private GameObject resultPanel;
[SerializeField] private TextMeshProUGUI resultText;
void Update()
{
scoreText.text = "コイン " + gameFlow.Coins + "/5";
timeText.text = "残り " + Mathf.CeilToInt(gameFlow.TimeLeft) + "秒";
titlePanel.SetActive(gameFlow.Current == GameFlow.State.Title);
resultPanel.SetActive(gameFlow.Current == GameFlow.State.Result);
if (gameFlow.Current == GameFlow.State.Result)
{
resultText.text = gameFlow.IsCleared ? "クリア!" : "時間切れ…";
}
}
}
InspectorでGameFlowと各UIパーツをつなぎ、 StartボタンのOnClickに GameFlow.StartGame、Retryボタンに GameFlow.Retry を割り当てます。仕上げに、Coinの OnTriggerEnter に AudioSource.PlayClipAtPoint(pickupSound, transform.position); を1行足して取得音を鳴らすと、急にゲームらしくなります(好きなフリー効果音を [SerializeField] private AudioClip pickupSound; で受けてください)。

確認: Title「Start」→ 60秒カウントが動く → コインを取るたびスコアが増える → 5枚で「クリア!」/放置で「時間切れ…」→「Retry」でタイトルから再挑戦。この一巡が回れば、ゲームは完成です。
ビルドして、友達に渡す
Editorの中で動くものは、まだ「プロジェクト」です。 Editorの外で動いて初めて「ゲーム」になります。
- File > Build Settings(Unity 6では File > Build Profiles)を開く
- Add Open Scenes で今のSceneを登録する(これを忘れると空のビルドができます)
- PlatformがWindows/Macになっていることを確認して Build。出力フォルダを選ぶ
- できた実行ファイルを起動して、 エディタなしで一巡遊ぶ——Title→Playing→クリアと時間切れの両方→Retry。これが スモークテスト です

そして最後の仕上げが、 ビルドのフォルダをzipにして、友達に渡す ことです。自分以外の人間が、あなたの説明なしで遊べたら——おめでとうございます。あなたは「Unityの機能を知っている人」から「ゲームを完成させた人」になりました。この差は、思っているよりずっと大きいです。
もしビルドで画面が真っ暗なら、Scenes In Buildへの登録忘れがほぼ犯人です。Editorでは動くのにビルドで音が出ない場合は、AudioClipの割り当て漏れを疑ってください。
おまけ:改造チャレンジ4選
完成したゲームは、最高の実験場です。1つ選んで、自分の手で壊して直してみてください。
- 危険物を置く: 触れるとタイムが5秒減る赤いSphereを追加。作り方はCoinの複製+通知の中身違いです。本格的なダメージ処理は 体力・ダメージの記事 へ
- ハイスコアを残す: クリアタイムを PlayerPrefs で保存して、タイトルに「ベスト記録」を表示
- コインを増やして配置を変える:
targetCoinsとPrefabの配置だけでレベルデザインが体験できます。取りにくい場所の1枚が、ゲームを面白くします - WebGLでブラウザ配信: ビルド先をWebGLにすると、URLひとつで遊んでもらえます。ビルド設定の詳細は ビルド設定の記事 へ
まとめ
- ゲームの背骨は Title → Playing → Result → リトライ の一巡。どんな大作もこの骨格の肉付けです
- スクリプトは 役割で4本に分ける:動く係・取られる係・ルール係・表示係
- コインの「1回だけ」は フラグ1つ から。リトライは Scene再読み込み が最も確実(staticイベントの解除だけ忘れずに)
- ビルドしてEditorの外で一巡遊ぶ までが完成。zipにして人に渡せたら卒業です
次に作りたいものは、もう頭に浮かんでいるはずです。2Dのアクション? セーブのあるRPG?——このコインゲームに使った背骨は、そのままどれにでも流用できます。さて、次は何を完成させますか?