Unity本格入門:いきのこバトルで学ぶタイトル・セーブデータ
題材・出典: 技術評論社刊『作って学べる Unity本格入門[Unity 6対応版]』(賀好 昭仁 著)に基づく学習補助の解説です。書籍の代替提供を目的とせず、コード掲載は学習上必要な範囲(必要最小限)にとどめます。利用条件は書籍記載(P.4〜5)および出版社サポート情報に従ってください。本シリーズ目次(書籍・著作の注記)
対象読者:Unity と C# のコードを追いながら読める方
このブログでは、いきのこバトル(IkinokoBattle)の タイトル画面・ゲームオーバー・ハイスコア保存に関わるコードを読み解きながら、UIボタンとセーブデータの考え方を学びます。
Unity教科書_Unity6対応(教科書サンプル連載)とは別シリーズです。
参考(任意):100日Unityマスターロードマップ の Phase 3〜4 の時期や、ClimbCloudゲーム入門 で SceneManager に触れたあとに読むと、用語のつながりを掴みやすいです。教科書側の Chapter 6 ではシーン切り替えの入口だけを扱っていますが、本記事では uGUI のボタン・シーン間のデータの渡し方・端末に残るセーブまで踏み込みます。
記事の目次
この記事はやや長めです。目的に合わせてジャンプして読み分けてください。
おすすめの読み方
セクション一覧
- 題材の範囲
- いきのこバトル全体像(記事化候補・参考)
- プロジェクトの構成(今回のファイル)
- クラス関係(この記事で登場する範囲)
- UMLで整理する(シーケンス・状態)
- コードを全部見てみよう
- ポイント解説(①〜⑦)
- コードの流れを整理しよう
- 自分でカスタマイズしてみよう!
- まとめ
ポイント一覧
- ①
Textでハイスコアを表示する - ②
Button.onClickでシーンを切り替える - ③
SaveDataとシングルトンInstance - ④
JsonUtilityとPlayerPrefsでセーブする - ⑤
SavableSingletonBaseがキーを一意にする理由 - ⑥
staticでシーン間にスコアを渡す - ⑦ ハイスコア更新と
Save()
題材の範囲
今回の記事では、ゲーム本体(移動・戦闘・UIの大半)は扱いません。タイトルシーンでハイスコアを表示してゲームへ進む流れと、ゲームオーバー後にスコアを保存する流れに絞って読みます。説明の切り方は、Unity教科書シリーズと同様に1機能塊=1記事にしています。
補足:ここで扱うコードは uGUI の UnityEngine.UI.Text を使っています。教科書シリーズの多くの記事では TextMeshProUGUI が出てきますが、画面に文字を出すという点は同じです。新規プロジェクトでは TextMeshPro の利用が推奨されることも多いので、慣れたら読み替えてみてください。

説明(学習のヒント):タイトルでハイスコアを見てゲームへ進み、ゲームオーバー後にスコアを端末へ残す流れのイメージです。記事では uGUI のボタン・シーン切り替え・JsonUtility と PlayerPrefs をコードで追います。
いきのこバトル全体像(記事化候補・参考)
シーンは次の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 の一部だけ登場します(コードでは引用します)。
クラス関係(この記事で登場する範囲)
説明(学習のヒント):継承は |>、参照(SaveData.Instance)は点線。タイトルとゲームオーバーは MonoBehaviour、セーブデータは純粋な C# クラスです。
UMLで整理する(シーケンス・状態)
クラス図に加え、画面間のデータの流れとシーンの切り替わりを UML 風に表します。
シーケンス(タイトル〜ハイスコア表示〜セーブ)
Mermaid では参加者 ID に Title が使えないため、TitleSceneController は TSC としています。
説明(学習のヒント):上から時系列で読みます。矢印は「誰が誰に頼むか」。-->> は結果が返るイメージです。
状態(シーンの遷移)
説明(学習のヒント):丸はシーンの「状態」、矢印は 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);
}
}
}メインシーンからゲームオーバーへ(参照)
スコアは GameOverSceneController の static フィールドに代入してから、シーンを読み込んでいます。
// 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.HighScoreSaveData はクラス名を SavableSingletonBase<SaveData> に渡して継承しています。どこからでも SaveData.Instance というひとつの共有オブジェクトとしてアクセスできる、いわゆるシングルトンの形です(MonoBehaviour ではなく、純粋な C# クラスとしてのシングルトン)。
ポイント④:JsonUtility と PlayerPrefs でセーブする
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()で端末へ書き出します。 isHighScoreUpdatedMessageはGameObjectなので、SetActive(true/false)で見た目のオンオフを切り替えています。
コードの流れを整理しよう
説明(学習のヒント):左から右へ読みます。箱はシーンや保存先、ラベル付きの矢印は「そのとき何をしているか」です。
流れの文章での整理
- タイトル:
SaveData.Instanceで保存済みのハイスコアを読み、Textに表示する。 - スタートボタン:
SceneManager.LoadScene("MainScene")で本編へ。 - 本編終了時(抜粋):
GameOverSceneController.Scoreに今回のスコアを入れ、GameOverSceneを読み込む。 - ゲームオーバー:static の
Scoreと、保存されていたハイスコアを比較し、更新ならSave()。
自分でカスタマイズしてみよう!
挑戦①:ハイスコアの横にラベルをつける
highScoreText に代入する文字列を、"Best: " + SaveData.Instance.HighScore のようにしてみましょう。
挑戦②:タイトルから別シーンへ
LoadScene の引数を変えると、誤って真っ暗なシーンに飛ぶこともあります。Build Settings のシーン一覧と名前が一致しているか確認する習慣をつけましょう。
挑戦③:SaveData に項目を足す
SaveData に public 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月