U
タブ 05モバイルへ書き出す7 レッスン — タップで開く
04 / 07 · 10 分
モバイルへ書き出す

進行状況を保存する

ここまでのプロジェクト

Assets/

Scenes/

Arena.unity

Knight

Scripts/

CharacterStats.csEquipment.csSaveData.csSaveSystem.csGameSession.cs

すでにあるものこのレッスンで追加

スマホのゲームはしょっちゅう中断されます。電話がかかってきたり、通知が来たり、OS がメモリを空けようとアプリを落としたりします。アプリを閉じただけで進行度が失われるゲームに、人は戻ってきません。

アニメーション図

素の C# クラスを JSON にして persistentDataPath に書き出し、ゲームを起動したら読み戻します。

PlayerPrefs か、ファイルか

✓ PlayerPrefs

型は三つだけ(int、float、string)。構造は持てず、registry や plist に保存されます。音量スライダーや Invert Y には最適ですが、ゲーム状態を置く場所ではありません。

✓ JSON file

オブジェクトのグラフ全体を一つのファイルにまとめます。中身を確認でき、バックアップも version 管理もできます。本物のセーブデータとはこのことです。

ステップ 1 — データだけの class

セーブ用クラスが保持するのは値だけで、MonoBehaviour は保持しません。GameObject は serialise できませんから、その代わりとして作り直しに必要な情報を書き残しておいてください。

SaveData.cs
using System;
using System.Collections.Generic;
using UnityEngine;

[Serializable]
public class SaveData
{
    public int version = 1;              // bump this when the shape changes

    public int level = 1;
    public float health = 100f;
    public int gold;
    public float playTimeSeconds;

    public string sceneName = "Arena";
    public Vector3 playerPosition;       // Unity serialises Vector3 fine

    public List<string> ownedArtifacts = new List<string>();
    public List<string> equippedArtifacts = new List<string>();

    public string savedAtUtc = "";
}

ステップ 2 — 書き出して読み込む

SaveSystem.cs
using System;
using System.IO;
using UnityEngine;

public static class SaveSystem
{
    // the ONLY folder you are allowed to write to on iOS and Android
    private static string Path => System.IO.Path.Combine(Application.persistentDataPath, "save.json");

    public static bool Exists => File.Exists(Path);

    public static void Save(SaveData data)
    {
        data.savedAtUtc = DateTime.UtcNow.ToString("o");

        string json = JsonUtility.ToJson(data, prettyPrint: true);
        string temp = Path + ".tmp";

        // if the phone dies mid-write, the old save is still intact
        File.WriteAllText(temp, json);
        File.Copy(temp, Path, overwrite: true);
        File.Delete(temp);

        Debug.Log("Saved to " + Path);
    }

    public static SaveData Load()
    {
        if (!Exists) return new SaveData();          // first run

        try
        {
            string json = File.ReadAllText(Path);
            SaveData data = JsonUtility.FromJson<SaveData>(json);
            return Migrate(data);
        }
        catch (Exception e)
        {
            // a corrupt save must never brick the game
            Debug.LogError("Save file unreadable, starting fresh: " + e.Message);
            File.Move(Path, Path + ".broken");
            return new SaveData();
        }
    }

    public static void Delete()
    {
        if (Exists) File.Delete(Path);
    }

    /// old saves must keep working after you ship an update
    private static SaveData Migrate(SaveData data)
    {
        if (data.version < 1)
        {
            data.gold = 0;
            data.version = 1;
        }
        return data;
    }
}
  • Application.persistentDataPath はスマホ上で書き込みできる唯一のフォルダです。Application.dataPath はアプリバンドルの中にあり、読み取り専用です。

  • 一時ファイルに書いてから入れ替えるのは、書き込みの途中で OS にアプリを落とされても、セーブデータが壊れないための工夫です。

  • Load は必ず try/catch で包んでください。ファイルが壊れてゲームを開けない人は、★1 つのレビューを残していきます。

  • version フィールドは今まったくコストになりませんが、1.3 でフィールドを追加した日に必ず役立ちます。

ステップ 3 — 適切なタイミングで保存する

毎フレーム保存するのはダメですが、プレイヤーがボタンを押したときだけ保存するのも同じくダメです——スマホではプレイヤーにそんな余裕はありません。

GameSession.cs
using UnityEngine;

public class GameSession : MonoBehaviour
{
    public static GameSession Instance { get; private set; }
    public SaveData Data { get; private set; }

    [SerializeField] private CharacterStats playerStats;   // the Knight's
    [SerializeField] private Transform player;

    void Awake()
    {
        if (Instance != null) { Destroy(gameObject); return; }
        Instance = this;
        DontDestroyOnLoad(gameObject);

        Data = SaveSystem.Load();
        Apply(Data);
    }

    void Update() => Data.playTimeSeconds += Time.deltaTime;

    void Collect()
    {
        Data.health = playerStats.currentHealth;
        Data.playerPosition = player.position;
        Data.sceneName = UnityEngine.SceneManagement.SceneManager.GetActiveScene().name;
    }

    void Apply(SaveData d)
    {
        playerStats.currentHealth = d.health;
        if (d.playerPosition != Vector3.zero) player.position = d.playerPosition;
    }

    /// fires when the app goes to the background — THIS is the important one on mobile
    void OnApplicationPause(bool paused)
    {
        if (paused) { Collect(); SaveSystem.Save(Data); }
    }

    /// desktop and a graceful exit
    void OnApplicationQuit()
    {
        Collect();
        SaveSystem.Save(Data);
    }
}

自動保存を連発せず、チェックポイントで保存

  • プレイヤーが新しいエリアに入ったとき、戦闘が終わったとき、買い物をしたとき、アプリがバックグラウンドに回ったときに保存します。

  • 小さな JSON ファイルを 1 回書くのは 1 ミリ秒ほどです。それでも 1 秒に 60 回書けば、フラッシュメモリを確実に摩耗させます。

  • 「保存しました」の目印を 0.5 秒だけ表示してください。保存しているのが見えないゲームを、プレイヤーは信用してくれません。

ファイルを見つけてデバッグする

terminal
# print the path from inside the game
Debug.Log(Application.persistentDataPath);

# Android — pull the save off the device
adb shell run-as com.yourstudio.yourgame cat files/save.json

# Editor paths
# macOS   ~/Library/Application Support/CompanyName/ProductName/
# Windows %USERPROFILE%\AppData\LocalLow\CompanyName\ProductName\