学習記事一覧 · Unity本格入門

Unity本格入門:いきのこバトルで学ぶタイトル・セーブデータ

題材・出典: 技術評論社刊『作って学べる Unity本格入門[Unity 6対応版]』(賀好 昭仁 著)に基づく学習補助の解説です。書籍の代替提供を目的とせず、コード掲載は学習上必要な範囲(必要最小限)にとどめます。利用条件は書籍記載(P.4〜5)および出版社サポート情報に従ってください。本シリーズ目次(書籍・著作の注記)

対象読者:Unity と C# のコードを追いながら読める方
このブログでは、いきのこバトル(IkinokoBattle)の タイトル画面・ゲームオーバー・ハイスコア保存に関わるコードを読み解きながら、UIボタンとセーブデータの考え方を学びます。
Unity教科書_Unity6対応(教科書サンプル連載)とは別シリーズです。

参考(任意)100日Unityマスターロードマップ の Phase 3〜4 の時期や、ClimbCloudゲーム入門SceneManager に触れたあとに読むと、用語のつながりを掴みやすいです。教科書側の Chapter 6 ではシーン切り替えの入口だけを扱っていますが、本記事では uGUI のボタンシーン間のデータの渡し方端末に残るセーブまで踏み込みます。


記事の目次

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

おすすめの読み方

セクション一覧

ポイント一覧


題材の範囲

今回の記事では、ゲーム本体(移動・戦闘・UIの大半)は扱いません。タイトルシーンでハイスコアを表示してゲームへ進む流れと、ゲームオーバー後にスコアを保存する流れに絞って読みます。説明の切り方は、Unity教科書シリーズと同様に1機能塊=1記事にしています。

補足:ここで扱うコードは uGUI の UnityEngine.UI.Text を使っています。教科書シリーズの多くの記事では TextMeshProUGUI が出てきますが、画面に文字を出すという点は同じです。新規プロジェクトでは TextMeshPro の利用が推奨されることも多いので、慣れたら読み替えてみてください。

タイトル画面・ハイスコア表示・シーン遷移とセーブのイメージ

説明(学習のヒント):タイトルでハイスコアを見てゲームへ進み、ゲームオーバー後にスコアを端末へ残す流れのイメージです。記事では uGUI のボタンシーン切り替えJsonUtilityPlayerPrefs をコードで追います。


いきのこバトル全体像(記事化候補・参考)

シーンは次の3つです。

シーン 主な役割
TitleScene ハイスコア表示、ゲーム開始ボタン
MainScene 本編(プレイヤー・敵・アイテム・UI など)
GameOverScene スコア表示、ハイスコア更新メッセージ

今回の記事は、上表のうえ二行と、Main から GameOver に渡るスコアの受け渡しに焦点を当てています。

記事を増やすときの候補として、スクリプトをフォルダごとに整理すると次のようになります(全体の並びは Unity本格入門の目次 も参照してください)。

単位(候補) 含まれる主なスクリプト 教科書シリーズとの差分の例
メインゲームの土台 MainSceneController, PlayerStatus, RoundLight DOTween・昼夜・SingletonMonoBehaviourInSceneBase
プレイヤー操作・戦闘 PlayerController, PlayerAttack, ThrowAxe 新 Input System(PlayerAction
敵・スポーン Spawner, SpawnerManager, EnemyMove, MobStatus など Instantiate の応用、AI の素
UI(メニュー・レシピ) Menu, RecipeDialog, ItemsDialog, LifeGauge など Canvas/ダイアログの重なり
オーディオ AudioManager シングルトン、Resources からの再生

プロジェクトの構成(今回のファイル)

今回読むスクリプトは次のとおりです(Assets/IkinokoBattle/Scripts/ 以下)。

Title/
  TitleSceneController.cs   # タイトルでハイスコア表示
  StartButton.cs            # ボタンで MainScene へ
Common/
  SavableSingletonBase.cs   # JSON セーブの共通基盤
  SaveData.cs               # ハイスコア本体
GameOver/
  GameOverSceneController.cs  # スコア表示とハイスコア更新

メインシーンからゲームオーバーへスコアを渡している箇所は MainSceneController.cs の一部だけ登場します(コードでは引用します)。


クラス関係(この記事で登場する範囲)

classDiagram direction TB class MonoBehaviour class SavableSingletonBase class SaveData { +int HighScore } class TitleSceneController class StartButton class GameOverSceneController { +int Score } SavableSingletonBase <|-- SaveData MonoBehaviour <|-- TitleSceneController MonoBehaviour <|-- StartButton MonoBehaviour <|-- GameOverSceneController TitleSceneController ..> SaveData : "Instance" GameOverSceneController ..> SaveData : "Instance"

説明(学習のヒント):継承は |>、参照(SaveData.Instance)は点線。タイトルとゲームオーバーは MonoBehaviour、セーブデータは純粋な C# クラスです。


UMLで整理する(シーケンス・状態)

クラス図に加え、画面間のデータの流れシーンの切り替わりを UML 風に表します。

シーケンス(タイトル〜ハイスコア表示〜セーブ)

Mermaid では参加者 ID に Title が使えないため、TitleSceneControllerTSC としています。

sequenceDiagram actor Player participant TSC as TitleSceneController participant SD as SaveData participant GOC as GameOverSceneController participant PP as PlayerPrefs Player->>TSC: 起動 TSC->>SD: HighScore 参照 SD-->>TSC: 表示用の値 Note over GOC,SD: 本編終了後 GOC->>SD: Save 呼び出し SD->>PP: JSON を保存

説明(学習のヒント):上から時系列で読みます。矢印は「誰が誰に頼むか」。-->> は結果が返るイメージです。

状態(シーンの遷移)

stateDiagram-v2 [*] --> TitleScene TitleScene --> MainScene MainScene --> GameOverScene GameOverScene --> [*]

説明(学習のヒント):丸はシーンの「状態」、矢印は LoadScene などで移れる流れです。* は開始・終了のイメージです。


コードを全部見てみよう

TitleSceneController.cs

using UnityEngine;
using UnityEngine.UI;
public class TitleSceneController : MonoBehaviour
{
    [SerializeField] private Text highScoreText;
    private void Start()
    {
        highScoreText.text = "" + SaveData.Instance.HighScore;
    }
}

StartButton.cs

using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
[RequireComponent(typeof(Button))]
public class StartButton : MonoBehaviour
{
    private void Start()
    {
        var button = GetComponent<Button>();
        // ボタンを押下した時のリスナーを設定
        button.onClick.AddListener(() =>
        {
            // シーン遷移の際にはSceneManagerを使用する
            SceneManager.LoadScene("MainScene");
        });
    }
}

SaveData.cs

/// <summary>
/// セーブデータクラス
/// </summary>
public class SaveData : SavableSingletonBase<SaveData>
{
    /// <summary>
    /// ハイスコア
    /// </summary>
    public int HighScore;
}

SavableSingletonBase.cs(全文)

セーブの仕組みの核心はこの基底クラスです。以下は省略していません。

using UnityEngine;
using System;
using System.IO;
using System.Security.Cryptography;
/// <summary>
/// PlayerPrefまたはファイルにデータを保存できるクラス
/// このクラスを継承すると、Save()メソッドを呼ぶことでpublicまたは[SerializeField]にしたフィールドをJSONに保存できる
/// </summary>
public abstract class SavableSingletonBase<T> where T : SavableSingletonBase<T>, new()
{
    private static T _instance;
    private bool _isLoaded;
    /// <summary>
    /// シリアライズしたJSONをPlayerPrefに保存するか、ファイルに保存するかの設定。継承したクラスでtrue/falseを指定可能にしている
    /// </summary>
    protected virtual bool IsSaveToPlayerPref => true;
    public static T Instance
    {
        get
        {
            if (null != _instance) return _instance;
            string json;
            _instance = new T();
            // インスタンス生成時にデータを自動ロードする
            if (_instance.IsSaveToPlayerPref)
            {
                json = PlayerPrefs.GetString(SaveKey);
            }
            else
            {
                json = File.Exists(SavePath) ? File.ReadAllText(SavePath) : "";
            }
            if (string.IsNullOrEmpty(json) || !LoadFromJson(json))
            {
                _instance._isLoaded = true;
            }
            return _instance;
        }
    }
    /// <summary>
    /// データをJSONにシリアライズ
    /// </summary>
    protected virtual string SerializedData => JsonUtility.ToJson(this);
    private static string SavePath => $"{Application.persistentDataPath}/{SaveKey}";
    private static string SaveKey
    {
        get
        {
            // クラス名のハッシュ値を生成している
            var provider = new SHA1CryptoServiceProvider();
            var hash = provider.ComputeHash(
                System.Text.Encoding.ASCII.GetBytes(typeof(T).FullName ?? throw new InvalidOperationException()));
            return BitConverter.ToString(hash);
        }
    }
    /// <summary>
    /// JSONデータからデータを復元します。
    /// </summary>
    public static bool LoadFromJson(string json)
    {
        try
        {
            _instance = JsonUtility.FromJson<T>(json);
            _instance._isLoaded = true;
            return true;
        }
        catch (Exception e)
        {
            Debug.LogWarning(e.ToString());
            return false;
        }
    }
    /// <summary>
    /// データを保存します。
    /// </summary>
    public void Save()
    {
        if (!_isLoaded) return;
        if (IsSaveToPlayerPref)
        {
            PlayerPrefs.SetString(SaveKey, SerializedData);
            PlayerPrefs.Save();
        }
        else
        {
            var path = SavePath;
            File.WriteAllText(path, SerializedData);
#if UNITY_IOS
            UnityEngine.iOS.Device.SetNoBackupFlag(path);
#endif
        }
    }
    /// <summary>
    /// データをリセットします。
    /// </summary>
    public void Reset()
    {
        _instance = null;
    }
    /// <summary>
    /// データを削除します。
    /// </summary>
    public void Delete()
    {
        if (IsSaveToPlayerPref)
        {
            PlayerPrefs.DeleteKey(SaveKey);
            PlayerPrefs.Save();
        }
        else
        {
            if (File.Exists(SavePath))
            {
                File.Delete(SavePath);
            }
        }
        _instance = null;
    }
}

GameOverSceneController.cs

using UnityEngine;
using UnityEngine.UI;
public class GameOverSceneController : MonoBehaviour
{
    public static int Score;
    [SerializeField] private Text previousHighScoreText;
    [SerializeField] private Text scoreText;
    [SerializeField] private GameObject isHighScoreUpdatedMessage;
    private void Start()
    {
        var previousHighScore = SaveData.Instance.HighScore;
        previousHighScoreText.text = "" + previousHighScore;
        scoreText.text = "" + Score;
        if (previousHighScore < Score)
        {
            // スコアが前のハイスコアよりも高ければ、ハイスコアとして記録
            isHighScoreUpdatedMessage.SetActive(true);
            SaveData.Instance.HighScore = Score;
            SaveData.Instance.Save();
        }
        else
        {
            isHighScoreUpdatedMessage.SetActive(false);
        }
    }
}

メインシーンからゲームオーバーへ(参照)

スコアは GameOverSceneControllerstatic フィールドに代入してから、シーンを読み込んでいます。

// MainSceneController.cs より(抜粋)
GameOverSceneController.Score = MinutesInGame;
SceneManager.LoadScene("GameOverScene");

ポイント①:Text でハイスコアを表示する

[SerializeField] private Text highScoreText;
private void Start()
{
    highScoreText.text = "" + SaveData.Instance.HighScore;
}
  • [SerializeField] により、プライベートフィールドでも Inspector から Text コンポーネントを割り当てられます。
  • "" + 数値 は数値を文字列に変換する簡単な書き方です(ToString() でも同じ)。

ポイント②:Button.onClick でシーンを切り替える

var button = GetComponent<Button>();
button.onClick.AddListener(() =>
{
    SceneManager.LoadScene("MainScene");
});

ClimbCloudゲーム入門 では SceneManager.LoadScene() だけを見ました。ここでは UI の Button が押されたときに呼ぶために、onClick.AddListenerラムダ式 () => { ... } を渡しています。

[RequireComponent(typeof(Button))] により、このスクリプトを付けるオブジェクトには必ず Button が付きます。GetComponent<Button>() で取得してからリスナーを登録する、という定番の形です。

注意"MainScene"Build Settings に登録されているシーン名と一致している必要があります。


ポイント③:SaveData とシングルトン Instance

public class SaveData : SavableSingletonBase<SaveData>
{
    public int HighScore;
}
SaveData.Instance.HighScore

SaveData はクラス名を SavableSingletonBase<SaveData> に渡して継承しています。どこからでも SaveData.Instance というひとつの共有オブジェクトとしてアクセスできる、いわゆるシングルトンの形です(MonoBehaviour ではなく、純粋な C# クラスとしてのシングルトン)。


ポイント④:JsonUtilityPlayerPrefs でセーブする

protected virtual string SerializedData => JsonUtility.ToJson(this);
PlayerPrefs.SetString(SaveKey, SerializedData);
PlayerPrefs.Save();
  • JsonUtility.ToJson(this) で、public なフィールドHighScore など)を JSON 文字列にします。
  • その文字列を PlayerPrefs に保存しています。PlayerPrefsキーと文字列のペアを端末に残す Unity の仕組みで、小さなセーブに向きます。

Load 側では PlayerPrefs.GetString(SaveKey) で取り出し、JsonUtility.FromJson<T> でオブジェクトに戻しています。


ポイント⑤:SavableSingletonBase がキーを一意にする理由

private static string SaveKey
{
    get
    {
        var provider = new SHA1CryptoServiceProvider();
        var hash = provider.ComputeHash(
            System.Text.Encoding.ASCII.GetBytes(typeof(T).FullName ?? throw new InvalidOperationException()));
        return BitConverter.ToString(hash);
    }
}

SaveData のようなセーブ用クラスが増えてもPlayerPrefs のキーが衝突しないように、型名からハッシュ文字列を作ってキーにしている、という設計です。型ごとに別の保存領域になるイメージです。


ポイント⑥:static でシーン間にスコアを渡す

public static int Score;
GameOverSceneController.Score = MinutesInGame;
SceneManager.LoadScene("GameOverScene");

シーンをまたぐと、通常はヒエラルキー上のオブジェクトは破棄されます。ここでは クラス名に紐づく static 変数に一時的にスコアを書き、ゲームオーバーシーンの Start で読む、というシンプルな受け渡しをしています。

より大きなゲームでは、ScriptableObject や専用のマネージャーで渡すことも多いですが、数値ひとつならこの方法でも十分よく使われます。


ポイント⑦:ハイスコア更新と Save()

if (previousHighScore < Score)
{
    isHighScoreUpdatedMessage.SetActive(true);
    SaveData.Instance.HighScore = Score;
    SaveData.Instance.Save();
}
else
{
    isHighScoreUpdatedMessage.SetActive(false);
}
  • 今回のスコアが保存されていたハイスコアより大きいときだけ、メッセージを表示し、メモリ上の HighScore を更新してから Save() で端末へ書き出します。
  • isHighScoreUpdatedMessageGameObject なので、SetActive(true/false) で見た目のオンオフを切り替えています。

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

flowchart LR title[TitleScene] main[MainScene] over[GameOverScene] storage["PlayerPrefs JSON"] title -->|"StartButton LoadScene"| main main -->|"Score を static に代入して LoadScene"| over over -->|"SaveData.Save ハイスコア"| storage title -->|"SaveData.Instance 表示"| storage

説明(学習のヒント):左から右へ読みます。箱はシーンや保存先、ラベル付きの矢印は「そのとき何をしているか」です。

流れの文章での整理

  1. タイトルSaveData.Instance で保存済みのハイスコアを読み、Text に表示する。
  2. スタートボタンSceneManager.LoadScene("MainScene") で本編へ。
  3. 本編終了時(抜粋):GameOverSceneController.Score に今回のスコアを入れ、GameOverScene を読み込む。
  4. ゲームオーバー:static の Score と、保存されていたハイスコアを比較し、更新なら Save()

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

挑戦①:ハイスコアの横にラベルをつける

highScoreText に代入する文字列を、"Best: " + SaveData.Instance.HighScore のようにしてみましょう。

挑戦②:タイトルから別シーンへ

LoadScene の引数を変えると、誤って真っ暗なシーンに飛ぶこともあります。Build Settings のシーン一覧と名前が一致しているか確認する習慣をつけましょう。

挑戦③:SaveData に項目を足す

SaveDatapublic int PlayCount; のようなフィールドを追加し、ゲームオーバーで PlayCount++ して Save() してみましょう。JSON に新しいキーが含まれることを Console やデバッグ表示で確かめられます。


まとめ

この記事では、次のことを整理しました。

  • uGUI の Text で数値を画面に出す([SerializeField] で割り当て)
  • Button.onClick.AddListener でボタンから SceneManager.LoadScene を呼ぶ
  • SaveData + SavableSingletonBase で、どこからでも同じセーブデータにアクセスする
  • JsonUtility + PlayerPrefs で JSON として永続化する
  • static 変数で、シーンをまたいでスコアを一時的に渡す
  • ハイスコアが更新されたときだけ Save() する

Unity教科書シリーズの ClimbCloudゲーム入門 で学んだシーン遷移から一歩進んで、UI とセーブのつながりを掴めたら成功です。続きは 第2回:メインシーンの時間と昼夜 です。メインシーンのその他の要素は 目次 の記事一覧も参照してください。


最終更新:2026年4月