レベル・所持アイテム・現在位置・クリア済みステージ——PlayerPrefs の「int/float/string 3種だけ」では、ゲームの進行状況を丸ごと保存するのはすぐに限界が来ます。「アイテムのListってどうやって保存するの?」で手が止まった経験はないでしょうか。
その答えが、JSON形式へのシリアライズです。保存したいデータを1つのC#クラスにまとめ、JsonUtilityでJSON文字列に変換してファイルに書く——この形を1回作れば、フィールドを足すだけでセーブ項目を増やせます。
- シリアライズ: C#のオブジェクト(メモリ上のデータ)を、ファイルに保存できる形式(JSON文字列)に変換すること。
- デシリアライズ: JSON文字列を、元のC#オブジェクトの形に復元すること。
この記事では、Unity組み込みのJsonUtilityを使って、ゲームデータをJSONファイルとして保存・ロードする基本形と、タイトル画面の「つづきから」への組み込みまでを解説します。
この記事でわかること
- シリアライズ/デシリアライズ——C#オブジェクト⇄JSON文字列の往復
[System.Serializable]とpublicフィールドの原則JsonUtility.ToJson/FromJson+Fileクラスでのファイル保存・読込- セーブ先の正解——
Application.persistentDataPathJsonUtilityの制限(Dictionary不可など)と回避策
Step 1: 保存するデータクラスを定義する
まず、セーブしたいデータ構造を定義したC#のクラスを作成します。重要なのは、このクラスに [System.Serializable] 属性を付けることです。この属性がないクラスは、JsonUtilityでシリアライズすることができません。

using System.Collections.Generic;
// この属性が必須!
[System.Serializable]
public class GameData
{
public int level;
public float currentHealth;
public Vector3 playerPosition;
public List<string> inventoryItems;
// コンストラクタで初期値を設定
public GameData()
{
this.level = 1;
this.currentHealth = 100f;
this.playerPosition = Vector3.zero;
this.inventoryItems = new List<string>();
}
}
注意点:
- シリアライズ対象となるのは、
publicなフィールドのみです。privateなフィールドやプロパティは無視されます。 JsonUtilityは、多次元配列やDictionaryなど、一部の複雑な型を直接シリアライズできないという制限があります。Listはサポートされています。

Step 2: データをJSONにシリアライズして保存する
次に、ゲーム内の現在の状態をGameDataクラスのインスタンスに格納し、それをJsonUtility.ToJson()でJSON文字列に変換してファイルに書き出します。
using UnityEngine;
using System.IO; // ファイル操作のために必要
public class SaveLoadManager : MonoBehaviour
{
private string saveFilePath;
private GameData gameData;
void Awake()
{
// セーブファイルのパスを決定
// Application.persistentDataPathは、各プラットフォームで安全に書き込みが許可されている永続的なディレクトリを指す
saveFilePath = Path.Combine(Application.persistentDataPath, "gamedata.json");
gameData = new GameData();
}
public void SaveGame()
{
// --- ここで現在のゲーム状態をgameDataオブジェクトに反映させる ---
// 例:
// gameData.level = FindFirstObjectByType<GameManager>().currentLevel;
// gameData.playerPosition = FindFirstObjectByType<PlayerController>().transform.position;
// ----------------------------------------------------------
// GameDataオブジェクトをJSON文字列に変換
// 第2引数をtrueにすると、人間が読みやすいように整形(pretty print)される
string json = JsonUtility.ToJson(gameData, true);
// JSON文字列をファイルに書き込む
File.WriteAllText(saveFilePath, json);
Debug.Log("Save successful! Path: " + saveFilePath);
}
}
Application.persistentDataPathは、ユーザーのドキュメントフォルダ内など、アンインストールしても消えない安全な場所を指すため、セーブデータの保存場所として最適です。

Step 3: JSONファイルを読み込んでデシリアライズする
ゲーム開始時やロードボタンが押された時に、保存したJSONファイルを読み込み、JsonUtility.FromJson()でGameDataオブジェクトに復元します。
// SaveLoadManagerクラスの続き
public void LoadGame()
{
// セーブファイルが存在するか確認
if (File.Exists(saveFilePath))
{
// ファイルからJSON文字列を読み込む
string json = File.ReadAllText(saveFilePath);
// JSON文字列からGameDataオブジェクトに復元
gameData = JsonUtility.FromJson<GameData>(json);
// --- ここで復元したデータをゲームに反映させる ---
// 例:
// FindFirstObjectByType<GameManager>().currentLevel = gameData.level;
// FindFirstObjectByType<PlayerController>().transform.position = gameData.playerPosition;
// --------------------------------------------------
Debug.Log("Load successful!");
}
else
{
Debug.LogWarning("Save file not found. Starting new game.");
// 新しいゲームを開始するための処理
gameData = new GameData();
}
}
FromJson<T>()メソッドは、指定した型Tのオブジェクトを生成し、JSONデータに基づいてそのフィールドを上書きします。
実践:タイトル画面の「つづきから」を組む
RPGの宿屋でセーブ、アクションのチェックポイント通過で自動セーブ、ローグライトの「中断セーブ」——形は違っても、プレイヤーが最初に触れるのは タイトル画面の「つづきから」 です。Step 1〜3のセーブ&ロードを、この定番UIに接続してみましょう。
まず「いつセーブするか」を決めます。定番は次の3つで、どれも中身はSaveGame()を呼ぶだけです。

タイトル画面側は、セーブファイルの有無で「つづきから」ボタンの状態を切り替えるのがポイントです。

using UnityEngine;
using UnityEngine.UI;
using System.IO;
public class TitleMenu : MonoBehaviour
{
[SerializeField] private Button continueButton;
private string saveFilePath;
void Start()
{
saveFilePath = Path.Combine(Application.persistentDataPath, "gamedata.json");
// セーブファイルがなければ「つづきから」を押せなくする
continueButton.interactable = File.Exists(saveFilePath);
}
// 「はじめから」ボタン: セーブを作り直して開始
public void OnNewGame()
{
var newData = new GameData(); // コンストラクタの初期値で開始
File.WriteAllText(saveFilePath, JsonUtility.ToJson(newData, true));
// → ゲームシーンへ遷移
}
// 「つづきから」ボタン: ロードして開始
public void OnContinue()
{
string json = File.ReadAllText(saveFilePath);
GameData data = JsonUtility.FromJson<GameData>(json);
// → dataをGameManager等に渡してゲームシーンへ遷移
}
}
ポイントは2つです。
- 「つづきから」は
interactableで制御: セーブがないのに押せてしまうと、その先でFile.ReadAllTextが例外を投げます。File.Existsでの事前チェックを、ボタンの見た目(グレーアウト)に直結させるのがユーザーにも優しい形です。 - ロードしたデータの「受け渡し」を設計する:
FromJsonで復元したGameDataを、シーンをまたいでゲーム本編に渡す必要があります。シーン遷移 と DontDestroyOnLoad(または ScriptableObject)を組み合わせるのが定番です。
おまけ:先に知っておくと良いこと
- 複数セーブスロット: ファイル名を
save_slot1.jsonのように変えるだけでスロット制になります。スロット選択UIには、各ファイルの更新日時(File.GetLastWriteTime)を添えると親切です。 - Dictionaryの回避策:
JsonUtilityはDictionaryを直接扱えません。 「キーのListと値のListを別々に持つ」 か、キーと値をペアにした[Serializable]クラスのListにするのが定番の回避策です。 - セーブデータの後方互換: アップデートでフィールドを追加した場合、古いセーブデータにはその項目がありません。
FromJsonは存在しない項目をコンストラクタの初期値のままにするので、新フィールドには必ず安全な初期値を設定しておきましょう。 - より高機能なライブラリ: 型の制限が厳しいと感じたら、
Newtonsoft Json.NET(Unity公式パッケージあり)がDictionaryやプロパティにも対応しています。まずはJsonUtilityで始めて、必要になったら乗り換えるのがおすすめです。 - 設定だけなら: 音量など単純なオプション値のためにファイルセーブは大げさです。PlayerPrefsの記事 との使い分けを意識してください。
まとめ
JsonUtilityを使ったセーブ&ロードシステムは、PlayerPrefsよりもはるかに柔軟で拡張性の高いデータ管理を可能にします。
- 保存したいデータをまとめたクラスを作成し、
[System.Serializable]属性を付ける(保存されるのはpublicフィールドだけ)。 - セーブ時:
JsonUtility.ToJson()でJSON文字列に変換し、File.WriteAllText()でファイルに保存する。 - ロード時:
File.ReadAllText()で読み込み、JsonUtility.FromJson<T>()で復元する。 - 保存場所は
Application.persistentDataPathが安全で確実。 - タイトル画面では
File.Existsで「つづきから」の活性を切り替える。
この基本的な仕組みを応用すれば、複数のセーブスロットを管理したり、セーブデータを暗号化してチートを防いだりといった、より高度な機能も実装していくことができます。