Skip to content

第09回 UIとシーン遷移 ​

前回: 第08回 GameManagerとゲーム状態 / 次回: 第10回 ビルドと公開

今回の授業内容 ​

  • ゲームルール、表示、Scene遷移の責務を分ける
  • TextMeshProの初期設定と日本語Font Assetの作成
  • GameManagerのイベントと現在値からUIを更新する
  • HUDとResult Panelを役割ごとに構成する
  • Title SceneとScene遷移用コンポーネントを実装する
  • Title、Game、Result、Retryを循環し、状態が初期化されることを確認する

今回の配布コード ​

09.zipは、第08回終了時点のコードへUIとScene遷移を追加した一式です。

配布ファイル一覧
ファイル今回の変更
GameUI.cs撃破数とゲーム結果を表示する
SceneNavigation.csSceneの読み込み、再挑戦、終了を扱う

ゲームルール・表示・Scene遷移の分担 ​

前回ゲームの状態を管理するGameManagerを実装しました。今回はゲーム状態の表示を行うHUDと、ゲーム終了後の結果表示とScene遷移を行う結果パネルのUIを作ります。

ゲームルール・表示・Scene遷移の分担
Enemy / Player
ゲーム内で起きた事実を通知する
GameManager
GameManager
状態と撃破数を通知する
GameUI
GameUI
表示へ反映する
Text / Panel
Button
クリックを受け渡す
SceneNavigation
SceneNavigation
Sceneの読み込みを依頼する
SceneManager
コンポーネント責務持たせない処理
GameManager撃破数と勝敗を決めるTextの書き換え、Scene読み込み
GameUI現在値を画面へ反映する勝敗条件の判定
SceneNavigationSceneを読み込む撃破数やHPの管理

イベント通知と現在値の取得の使い分け ​

イベントは、ある瞬間に起きたことを通知するための仕組みです。
GameUIが有効になる前にイベントが発行されていた場合、その過去の通知は受け取れません。
つまり、初期化にイベントを使えるとは限りません。

ここでの初期化はイベントに頼らず、GameManagerの現在値を直接読み取ってUIへ反映しています。

タイミング方法目的
GameUIの開始時プロパティから現在値を読む初期表示を必ず現在状態にそろえる
開始後に値が変わるときイベントを受け取る毎フレーム確認せず、その瞬間だけ更新する

TextMeshProの初期設定と日本語フォントの準備 ​

TextMeshProとは ​

TextMeshPro(TMP)は高品質な文字表示を行うためのアセットです。
Unityの標準機能ではありますが、フォントを扱うためには初期設定が必要です。
以下の手順でセットアップを済ませてください。

TMP Essential Resourcesをインポートする ​

TMP ImporterでImport TMP Essentialsを選択する画面TMP ImporterでImport TMP Essentialsを選択する画面

TMPを初めて使用するProjectでは、シェーダーや初期設定などの基本リソースをインポートします。

  1. UnityのメニューからWindow > TextMeshPro > Import TMP Essential Resourcesを選択します
  2. Importを押し、表示されたファイルをProjectへ追加します
  3. Assets/TextMesh Proフォルダが作成されたことを確認します

初めてText - TextMeshProを作成したときにTMP Importerが表示された場合は、そこからImport TMP Essentialsを選んでも構いません。

Import TMP Examples & Extras について

Import TMP Examples & Extrasはサンプル集のため今回の制作には必要ありませんが、TMPの使い方を学ぶ上で参考になるサンプルが含まれています。
ぜひ時間のあるときにインポートして、Assets/TextMesh Pro/Examples & Extrasの中身を確認してみてください。

Font Assetの準備 ​

TextMeshPro(TMP)は、文字を表示するためにFont Assetを使用します。

WindowsやMacOSといったOSが標準で扱うフォントは.ttfや.otf形式のフォントファイルです。
フォントの形状を定義するグリフ情報を持つファイルで、OSやアプリケーションが文字を描画する際に参照します。

一方、TMPのFont Assetは、文字を並べるための情報、文字をまとめた画像(Font Atlas)、描画に使うMaterialを結びつけるアセットです。TMPのツールを使い、.ttfや.otf形式のフォントファイルから作成します。

今回はSDF(Signed Distance Field:符号付き距離場)形式を使います。SDFは文字の輪郭までの距離を画像へ記録し、シェーダーがその値から輪郭を描く方式です。画面解像度やUIの拡大率が変わったときにも、輪郭の滑らかさを保ちやすくなります。ただし、元の文字が含まれていることと、表示サイズに合うAtlasの品質は必要です。

日本語フォントからFont Assetを作成する ​

TMPの既定Font Assetに日本語の文字が含まれていない場合、□のような代替文字で表示されることになります。
日本語を表示するには、日本語のグリフを持つフォントファイルからFont Assetを作成しなければなりません。

  1. ゲームへの組み込みが許可された日本語フォントのライセンスを確認します
  2. ProjectウィンドウのAssets内にFontsフォルダを作成します
  3. .ttfまたは.otf形式のフォントファイルをAssets/Fontsへドラッグします
  4. 取り込んだフォントファイルを選択します
  5. Assets > Create > TextMeshPro > Font Asset > SDFを選択します
  6. 作成されたFont Assetを選択し、InspectorのAtlas Population ModeをDynamicにします
  7. Source Font Fileに元のフォントファイルが設定されていることを確認します
  8. Multi Atlas Texturesを有効にし、最初のAtlasが埋まったときに追加のAtlasを使えるようにします

Dynamicは表示に必要になった文字がFont Atlasへ追加されます。
StaticはFont Asset作成時に指定した文字だけがFont Atlasに含まれます。
扱いやすいため、今回の授業ではDynamicでFont Assetを作成します。

Font AssetのDynamicとStatic

Dynamicは、必要になった文字を元のフォントからAtlasへ追加します。文章を変更する実習で扱いやすい一方、初めて使う文字の生成には処理時間がかかり、文字数やAtlasの枚数に応じてメモリ使用量も増えます。元のフォントファイルもゲームに含める必要があります。

Staticは、あらかじめ用意した文字を使います。表示する文章が確定している場合に向いていますが、Atlasにない文字は追加できません。作品の内容に応じて選び、必要ならStaticの本文用フォントとDynamicのフォールバックを組み合わせます。この実習ではDynamicを使い、追加した日本語も表示できるかを確認します。

フォントのライセンスを確認する

WebサイトやOSから取得できるフォントがゲームで利用できるとは限りません。
特にゲームでの利用は再配布にあたると判断され、規約上の禁止事項に含まれる場合があります。
利用条件を確認し、ゲームへの組み込みが許可されたフォントを使用するようにしてください。

フォントに限らず、利用許諾の確認は外部アセットを使用する際の基本です。
必ず確認し、利用条件に従って使用してください。

TMPのTextにFont Assetを設定する ​

作成した日本語Font Assetは、自動ですべてのTextへ適用されるわけではありません。
各TextMeshPro - Text (UI)コンポーネントのFont Assetへ設定します。

今回作成するDefeat Count Text、Result Text、Button内のText、Title SceneのTextには、同じ日本語Font Assetを設定します。
Textに撃破数 0 / 10と入力し、Gameビューで日本語、数字、記号が欠けずに表示されることを確認しましょう。

異なる環境で文字を表示するための確認 ​

フォントファイルをProjectへ取り込み、Font Assetから参照することで、実行するOSに同じフォントがインストールされているかに依存せず文字を用意できます。DynamicではSource Font Fileを残し、実行中に元のフォントから必要な文字を追加します。

元のフォントにもない文字は、Dynamicでも生成できません。表示が□になる場合は、まず使用中のFont Assetと元のフォントの対応文字を確認します。別のフォントで補うときは、そのFont AssetをFallback Font Assetsへ登録します。フォールバック(fallback)は、見つからない文字を別のフォントから探す仕組みです。

  1. 日本語・英数字・句読点を含む短い文を表示します。
  2. Gameビューを16:9、4:3、縦長へ切り替え、文字の欠落と枠からのはみ出しを確かめます。
  3. 対象プラットフォーム向けのビルドでも、同じ文とButtonの文字を確かめます。

SDFは文字の輪郭を描く方式で、UIの配置を自動調整する機能ではありません。異なる画面サイズへの対応には、この後で設定するCanvas ScalerとRectTransformのアンカーも使います。Font Assetの仕様はUnityのFont AssetsとDynamic fonts assetsで確認できます。

Game SceneのUIを作成する ​

UI構造を決める ​

第04回で作成したPlayerとEnemyのHPゲージは、どちらも同じHealthGaugeを使用しています。
機能が実装されているという意味ではこのままでも構わないのですが、通常のゲームではプレイヤーのHPゲージは特別なものとして画面の定位置に表示されるものです。

今回の制作でもプレイヤーのHPゲージは画面の左上に固定して表示することにします。

ゲージCanvasのRender Mode理由
Player HP GaugeScreen Space - Overlay画面の一定位置に表示し、常に確認できるようにする
Enemy HP GaugeWorld SpaceEnemyの頭上に置き、どのEnemyのHPか判別できるようにする

CanvasのRender ModeとHierarchy上の配置によって、画面に固定するUIとゲーム世界に置くUIを使い分けます。

HealthGaugeのスクリプトは共通のままで構いません。
一方、カメラの方を向かせるFaceCameraはWorld Space UIのためのコンポーネントですので、画面に固定するPlayer HP Gaugeには使用しません。

今回作成する画面UIと、Enemy Prefabに残すワールド空間UIは次のように分けます。

Game SceneのUI構造
  • Game UI Canvas(Screen Space - Overlay)
    • HUD
      • Player HP Gauge
      • Defeat Count Text
    • Result Panel
      • Result Text
      • Retry Button
      • Title Button
  • EventSystem
  • Enemy Prefab
    • Enemy UI Canvas(World Space)
      • Enemy HP Gauge

HUDはプレイ中に常時見る情報を、Result Panelは終了時だけ必要な操作をまとめます。
同じUIではありますが、目的がまるで異なるものです。
こうしたUI部品は役割ごとに親を分け、Panel単位で表示を切り替えると管理しやすくなります。

既存のHPゲージはHUDへ移動させない

ここまでの実装では、Player HP GaugeはPlayerの子ObjectとしてPrefab化されています。
Prefabインスタンス内の子Objectを外側のHUDへ直接移動することはできません。

今回は既存のゲージやCanvasを移動・変換せず、Game UI Canvas > HUDの中にPlayer用のScreen Space HP Gaugeを新規作成します。
既存ゲージは削除しても構いませんし、無効化しても構いません。

Game UI CanvasとHUDを作成する ​

  1. Game Sceneを開きます
  2. GameObject > UI > CanvasからCanvasを作成し、Game UI Canvasと名付けます
  3. Game UI Canvasの子に空のGameObjectを作り、HUDと名付けます
  4. HierarchyにEventSystemが1つあることを確認します。なければGameObject > UI > Event Systemから作成します

Game UI Canvasの各コンポーネントを次のように設定します。

コンポーネント項目設定値
CanvasRender ModeScreen Space - Overlay
Canvas ScalerUI Scale ModeScale With Screen Size
Canvas ScalerReference Resolution1920 x 1080
Canvas ScalerMatch0.5

Player HP Gaugeを作成する ​

まず、HPゲージを構成するGameObjectとImageを作成します。

  1. HUDの子に空のUI用GameObjectを作成し、Player HP Gaugeと名付けます
  2. Player HP GaugeへHealthGaugeを追加します
  3. Player HP Gaugeの子に背景用のBackと前景用のFrontのImageを作成します

BackとFrontは同じ大きさに重ね、次のように設定します。

対象項目設定値
Back / FrontImage TypeFilled
Back / FrontFill MethodHorizontal
Back / FrontOriginLeft
HealthGaugeFront作成したFront
HealthGaugeBack作成したBack

FaceCameraはWorld Space UIのためのコンポーネントなので、Player HP Gaugeには追加しません。

画面左上へ配置する ​

Player HP GaugeのRectTransformを次のように調整します。

項目設定
Rotation0
Scale1
Anchor Presets左上
Width / HeightHPゲージとして見やすい大きさ
Pos X / Pos Y左上から適度に余白を空けた位置

Playerへ接続する ​

  1. Scene上のPlayerを選択します
  2. PlayerコンポーネントのHealth Gaugeへ、新しいPlayer HP Gaugeを設定します
  3. HierarchyでPlayerを展開し、子にあるHealth Gauge 3Dを探します
  4. Health Gauge 3DのルートGameObjectを選択し、Inspector上部のチェックを外して無効にします

テンプレートによって名前が異なる場合は、Canvas、Canvas Scaler、FaceCameraを持つPlayer用のWorld Space HP Gaugeを探してください。

PrefabへApplyしない

Scene上に配置したPlayerインスタンスからは、同じSceneのPlayer HP Gaugeを参照できます。ただし、PrefabアセットからScene上のObjectは参照できません。

Health Gaugeの設定はScene上のPlayerに保存し、PrefabへApplyしないでください。

PlayしてHPを変化させ、新しいPlayer HP Gaugeへ反映されることを確認します。

撃破数Textを作成する ​

  1. HUDの子にUI > Text - TextMeshProを作成します
  2. 名前をDefeat Count Textにします

TextMeshProとRectTransformを次のように設定します。

項目設定
Text撃破数 0 / 10
Font Asset準備した日本語Font Asset
Anchor Presets左上
配置Player HP Gaugeと重ならない位置

Result Panelを作成する ​

  1. Game UI Canvasの子にUI > Panelを作り、Result Panelと名付けます
  2. Panelの子にResult Text、Retry Button、Title Buttonを作成します

作成したTextを次のように設定します。

対象TextFont Asset
Result Text結果表示用の仮の文字準備した日本語Font Asset
Retry Button内のTextRETRY準備した日本語Font Asset
Title Button内のTextTITLE準備した日本語Font Asset

PanelはあとでGameUIが非表示にするため、Editor上では表示したまま配置を調整して構いません。
邪魔な場合は適時Result PanelのルートGameObjectを無効にしましょう。

画面比率を変えて配置を確認する ​

16:9のGameビューで画面左上にPlayer HP Gaugeと撃破数を表示した例
16:9の例

4:3のGameビューで画面左上にPlayer HP Gaugeと撃破数を表示した例
4:3の例

縦長のGameビューで画面左上にPlayer HP Gaugeと撃破数を表示した例
縦長の例

Gameビューを16:9、4:3、Free Aspectへ順に切り替え、次の点を確認します。

  • Player HP GaugeとDefeat Count Textが画面左上に表示される
  • UIが画面外へ出ず、左上からの余白が大きく変わらない

最後にPlayし、Player HP Gaugeが画面左上に固定されることを確認します。
Enemy HP Gaugeは、対応するEnemyと一緒に移動していれば問題ありません。

GameUIを実装する ​

Assets/Scripts/GameUI.csを作成します。

cs
using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

public class GameUI : MonoBehaviour
{
    [SerializeField] private GameManager gameManager;
    [SerializeField] private TMP_Text defeatCountText;
    [SerializeField] private GameObject resultPanel;
    [SerializeField] private TMP_Text resultText;
    [SerializeField] private Button retryButton;

GameUIが扱うのは表示だけです。
撃破数のカウントなどのゲームルールには干渉しないことに注目してください。

イベントの購読と解除 ​

cs
private void OnEnable()
{
    gameManager.DefeatCountChanged += OnDefeatCountChanged;
    gameManager.StateChanged += OnStateChanged;
}

private void OnDisable()
{
    gameManager.DefeatCountChanged -= OnDefeatCountChanged;
    gameManager.StateChanged -= OnStateChanged;
}

GameManagerと同様、イベントの購読と解除を対で設定します。

開始時は現在値で初期化する ​

cs
private void Start()
{
    Refresh();
}

private void Refresh()
{
    OnDefeatCountChanged(
        gameManager.DefeatCount,
        gameManager.ClearDefeatCount);
    resultPanel.SetActive(false);
    OnStateChanged(gameManager.State);
}

先述の通り、イベントでの更新は購読前の情報を知ることができません。
開始時はGameManagerが現在持っている値で初期化を行います。

撃破数を文字列に変換する ​

cs
private void OnDefeatCountChanged(int count, int goal)
{
    defeatCountText.text = $"Defeat {count} / {goal}";
}

文字列補間

$"..."は文字列補間です。{}で囲まれた式の値を文字列に変換して埋め込みます。
例えば上のコードはcountとgoalの値を文字列に変換してDefeat 0 / 10のように出力します。

終了状態をResult Panelに表示する ​

cs
private void OnStateChanged(GameState state)
{
    if (state != GameState.GameClear &&
        state != GameState.GameOver)
        return;

    resultPanel.SetActive(true);
    resultText.text = state == GameState.GameClear
        ? "GAME CLEAR"
        : "GAME OVER";
    EventSystem.current.SetSelectedGameObject(
        retryButton.gameObject);
}

GameOverとGameClearの2つの状態をResult Panelに表示します。
Panelを表示した直後にRetry Buttonを選択することで、方向キーやコントローラーからButton操作を始められるようにします。

三項演算子

三項演算子は条件によって2つの値から1つを選ぶ式です。
例えば、このコードは、次のif文と同じ意味です。

cs
// 三項演算子の例
resultText.text = state == GameState.GameClear
    ? "GAME CLEAR"
    : "GAME OVER";

// if文の例
if (state == GameState.GameClear)
    resultText.text = "GAME CLEAR";
else
    resultText.text = "GAME OVER";

GameUIをInspectorで接続する ​

  1. Game UI CanvasへGameUIを追加します
  2. Game ManagerへHierarchyのGame Managerを設定します
  3. Defeat Count Textへ作成したTMP Textを設定します
  4. Result PanelへPanelを設定します
  5. Result TextへPanel内のTMP Textを設定します
  6. Retry ButtonへPanel内のRetry Buttonを設定します
  7. Playして、開始時にPanelが隠れることを確認します
  8. Enemyを倒し、撃破数が更新されることを確認します
  9. ClearとOverで異なる文字が表示されることを確認します

5つの参照欄のいずれかがNoneのままの場合、イベント購読や表示更新でNullReferenceExceptionになります。Consoleの最初の例外から確認します。

Title SceneとScene遷移を作成する ​

Title Sceneを作成する ​

  1. Game Sceneを保存します
  2. File > New Sceneで新しいSceneを作成します
  3. SceneをプロジェクトのScenesフォルダへTitleという名前で保存します
  4. Canvasを作成し、タイトル、ゲーム目的、操作方法などをTextで配置します
  5. 作成した各TextのFont Assetへ、準備した日本語Font Assetを設定します
  6. Start Buttonを作成し、表示をSTARTにします
  7. Start Button内のTextにも同じFont Assetを設定します
  8. 空のGameObjectを作成し、Scene Navigationと名付けます

SceneNavigationを実装する ​

Assets/Scripts/SceneNavigation.csを作成します。

cs
using UnityEngine;
using UnityEngine.SceneManagement;

public class SceneNavigation : MonoBehaviour
{
    public void LoadScene(string sceneName)
    {
        SceneManager.LoadScene(sceneName);
    }

    public void Retry()
    {
        SceneManager.LoadScene(
            SceneManager.GetActiveScene().buildIndex);
    }

    public void Quit()
    {
        Application.Quit();
    }
}

LoadScene()は指定名のScene、Retry()は現在のSceneを読み直します。
Sceneを読み直すとGameObjectと各コンポーネントの状態が作り直されるため、PlayerControllerとEnemySpawnerは初期状態になります。

Scene名を文字列で指定する方法は小規模な教材では分かりやすい一方、名前変更やスペルミスをコンパイル時に検出できません。
注意して扱いましょう。

発展:Sceneのビルドインデックスをenumとして自動生成する

シーン名をenum化できれば、シーンの名前を直接文字列で指定する必要がなくなります。
有効なSceneのビルド順をenumの整数値として生成し、SceneManagerへビルドインデックスを渡す運用であれば、Scene名の変更やスペルミスをコンパイル時に検出できます。

Unityの標準機能では対応できませんが、Editor拡張で自動生成できます。
以下のAssets/Editor/SceneIdGenerator.csを作成してみましょう。

Editorフォルダ内に置かれたスクリプトはUnity Editorでだけ実行されゲーム本体のビルドには含まれないため、ゲームの実行速度や容量に影響しません。
安心して追加してください。

SceneIdGenerator.cscs
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using UnityEditor;

public static class SceneIdGenerator
{
    private const string OutputPath =
        "Assets/Scripts/SceneId.cs";

    [MenuItem("Tools/Generate Scene Id")]
    private static void Generate()
    {
        var usedNames = new HashSet<string>();

        var members = EditorBuildSettings.scenes
            .Where(scene => scene.enabled)
            .Select((scene, buildIndex) =>
            {
                var sceneName =
                    Path.GetFileNameWithoutExtension(scene.path);
                var baseName = ToIdentifier(sceneName);
                var memberName = baseName;
                var suffix = 2;

                while (!usedNames.Add(memberName))
                {
                    memberName = $"{baseName}_{suffix}";
                    suffix++;
                }

                return $"    {memberName} = {buildIndex}";
            });

        var source =
            $"public enum SceneId\n{{\n" +
            $"{string.Join(",\n", members)}\n}}\n";

        File.WriteAllText(OutputPath, source);
        AssetDatabase.Refresh();
    }

    private static string ToIdentifier(string sceneName)
    {
        var builder = new StringBuilder();

        foreach (var character in sceneName)
        {
            if (char.IsWhiteSpace(character))
                builder.Append('_');
            else if (char.IsLetterOrDigit(character) ||
                     character == '_')
                builder.Append(character);
        }

        var identifier = builder.ToString();

        if (string.IsNullOrEmpty(identifier))
            identifier = "Scene";

        if (char.IsDigit(identifier[0]))
            identifier = $"_{identifier}";

        return identifier;
    }
}

Tools > Generate Scene Idを実行すると、現在有効なSceneから次のようなファイルが生成されます。

SceneId.cscs
public enum SceneId
{
    Title = 0,
    Game = 1
}

enumの各値は、ビルド対象に並んだ有効Sceneのインデックスと一致します。
生成時には空白を_へ置き換え、英数字と_以外の記号を削除します。

例えばMain Menu!はMain_Menu、Stage-01はStage01になります。
変換後の名前が重複した場合は、Stage01_2のように連番を加えます。
先頭が数字になる場合は_を加えます。
classやnamespaceなどのC#の予約語はScene名に使用しないでください。

以下のSceneNavigationではenumをintへ変換し、ビルドインデックスとして読み込みます。
Buttonから呼ぶ引数なしのメソッドを用意すれば、InspectorへScene名を直接入力する必要もありません。

SceneNavigation.cscs
private static void LoadScene(SceneId sceneId)
{
    SceneManager.LoadScene((int)sceneId);
}

public void LoadGame()
{
    LoadScene(SceneId.Game);
}

public void LoadTitle()
{
    LoadScene(SceneId.Title);
}

Sceneを追加、削除、改名、並べ替えた場合は、ビルド対象の登録を更新してから生成を再実行します。
生成されたSceneId.csは直接編集しないでください。

Sceneをビルド対象へ登録する ​

Build ProfilesのScene ListへTitleとGameを登録した画面
使用中のUnityのメニューからBuild ProfilesまたはBuild Settingsを開きます。

  1. TitleとGameをScene一覧へ追加します
  2. Titleを先頭、Gameを次に並べます
  3. 両方のSceneが有効になっていることを確認します
  4. Sceneファイル名がTitleとGameであることを確認します

INFO

SceneManagerはビルド対象に登録されていないSceneを読み込めません。
シーン切り替えがうまくいかない場合は、まずビルド対象の登録を確認してください。

Buttonに処理を接続する ​

Title SceneのStart Button ​

Start ButtonのOn ClickにGame Sceneを読み込む処理を設定したInspectorStart ButtonのOn ClickにGame Sceneを読み込む処理を設定したInspector

  1. Scene NavigationへSceneNavigationを追加します
  2. Start ButtonのOn Click()で+を押します
  3. Scene NavigationをObject欄へドラッグします
  4. SceneNavigation > LoadScene(string)を選択します
  5. 引数へGameと入力します

Game SceneのResult Panel ​

  1. Game SceneにもScene Navigationを作り、同じコンポーネントを追加します
  2. Retry ButtonへSceneNavigation.Retry()を設定します
  3. Title ButtonへSceneNavigation.LoadScene(string)を設定し、引数をTitleにします
  4. Game Sceneを保存します

Quit()は任意で追加してください。
Application.Quit()はEditor上では動作しませんが、ビルド版ではアプリケーションを終了できます。

画面遷移を通しで確認する ​

手順期待結果
TitleのSTARTを押すGame Sceneが読み込まれ、移動できる
Enemyを倒す撃破数が1 / 10のように更新される
目標数を倒すGAME CLEARと2つのButtonが表示される
RETRYを押すGame Sceneが初期状態で始まり、時間が進む
Playerを死亡させるGAME OVERが表示される
TITLEを押すTitle Sceneへ戻る
再度STARTを押すGame Sceneをもう一度正常に開始できる

実習:表示とゲームルールの境界を確認する ​

  1. 撃破数の表示をENEMY 1 / 10などへ変更します
  2. 表示変更だけではクリア条件が変わらないことを確認します
  3. GameManagerのClear Defeat Countを変更します
  4. 表示される目標数と実際のクリア条件が同時に変わることを確認します
  5. どの値をGameManagerとGameUIのどちらが所有するか説明します

動作しない場合のチェックリスト ​

Textが更新されない ​

  • GameUIの5つの参照が設定されているか
  • ConsoleにNullReferenceExceptionがないか
  • GameUIが有効でイベントを購読しているか
  • GameManager側の撃破数が増えているか

日本語が□で表示される ​

  • 日本語を収録したフォントファイルからFont Assetを作成したか
  • TMPのFont Assetへ作成した日本語Font Assetを設定したか
  • Font AssetのAtlas Population ModeがDynamicになっているか
  • Source Font Fileに元のフォントファイルが設定されているか

Result Panelが表示されない ​

  • Result PanelへPanel本体を設定したか
  • GameManagerのStateが終了状態へ遷移しているか
  • Panelの親Canvasが有効か
  • Hierarchyに有効なEventSystemが1つあるか

Player HPゲージが表示・更新されない ​

  • Game UI CanvasのRender ModeがScreen Space - Overlayか
  • Player HP GaugeがGame UI Canvas > HUDの子にあるか
  • PlayerインスタンスのHealth Gaugeに画面用ゲージを設定したか
  • HealthGaugeのFrontとBackにImageを設定したか
  • 画面用のPlayer HP GaugeにFaceCameraが付いていないか
  • Player HP GaugeのAnchorとRectTransformが画面外になっていないか

Enemy HPゲージが画面に固定されてしまう ​

  • Enemy HP GaugeをGame UI Canvasへ移動していないか
  • Enemy Prefab内のCanvasがWorld Spaceのままか
  • Enemy Prefab内のFaceCameraが有効か
  • EnemyのHealth Gaugeに、同じEnemy Prefab内のゲージを設定したか

Buttonを押してもSceneが変わらない ​

  • ButtonのOn ClickへScene Navigationを設定したか
  • SceneをBuild ProfilesまたはBuild Settingsへ登録したか
  • TitleとGameの大文字小文字が一致しているか
  • 透明なUIがButtonのRaycastを遮っていないか

Retry後にゲームが止まっている ​

  • Retry先がGame Scene自身になっているか
  • PlayerControllerとEnemySpawnerがSceneの初期状態で有効か
  • GameManagerのInspector参照が設定されているか

完成条件 ​

  • GameManager、GameUI、SceneNavigationの責務を説明できる
  • Player用のScreen Space UIとEnemy用のWorld Space UIを使い分けられる
  • 画面比率を変えてもPlayer HPゲージが画面左上に残る
  • 初期値取得とイベント更新の両方でUIを現在状態へ同期できる
  • Title、Game、ClearまたはOver、Retry、Titleを循環できる
  • HP、撃破数、勝敗、操作方法を画面から判断できる
  • UIの表示変更がゲームルールを直接変更しない

まとめ ​

  • GameManagerを情報源、GameUIを表示担当として分離した
  • 開始時に現在値を取得し、その後はイベントで表示を更新した
  • PlayerのHPゲージはScreen Space、EnemyのHPゲージはWorld Spaceに配置した
  • HUDとResult Panelを用途ごとに整理した
  • Sceneを読み直し、各ゲームプレイコンポーネントを初期状態から再開できるようにした