さて、ゲームのシステム部分の制作もそろそろ終わりが見えてきました。最後にセーブ・ロード処理を作ってシステム部分の制作を締めくくることにしましょう。
それで今までのチュートリアル作品ではセーブ・ロード処理にPlayerPrefsを使っていましたが、実はPlayerPrefsにはいろいろな問題があります。そのため、今回は処理を自作することにします。といっても理屈さえわかればそれほど難しくないので、プログラムを追って理解できるようにちょっと頑張ってみましょう。
なぜPlayerPrefsではダメなのか
ではまず、なぜPlayerPrefsはダメなのかという話からすると…PlayerPrefsには次のような問題点があります。
- 保存できるデータの型が限られている
- 処理が遅い
- 複数のデータを一括で保存できない
- Windows用ゲームの場合、なぜか保存先がレジストリ
PlayerPrefsは手軽に使えるのが利点ですが、こんなに問題があるようでは自作ゲームには使いたくありませんよね。とはいえUnity側はなかなかまともなセーブ・ロード処理を作ってくれませんし、ちゃんとしたアセットは有料なのでこうなったら自作するしかありません。
代替案:JSONを使ってセーブファイルを読み書きする
そこでPlayerPrefsの代わりとして、JSONというデータ記述方法を使ってクラスをファイルに読み書きする方法をとります。詳しい話は私が運営する姉妹サイトのほうに書いたのでそちらを参照してください。
【Unity】JSONを使ったセーブ・ロード処理 | くろくまそふと
セーブ・ロード処理を行うC#スクリプト
それでは実際のプログラムを見てみましょう。今回実装するセーブ・ロード処理は2つのクラスで構成されます。
SaveData.cs
一つ目は実際にセーブファイルに書き込まれるクラスです。
[System.Serializable]
public class SaveData
{
public bool[] stageFlags;
}
今回のゲームでは、あるステージに挑戦できるかどうかだけを記録できれば良いのでコードはすごくシンプルです。
SaveManager.cs
次に二つ目は実際にセーブ・ロード処理を行うクラスです。
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using UnityEngine;
public class SaveManager : SingletonMonoBehaviour<SaveManager>
{
string filePath;
SaveData save;
MoveSceneManager moveSceneManager;
void Awake()
{
if (this != Instance)
{
Destroy(gameObject);
return;
}
DontDestroyOnLoad(gameObject);
filePath = Application.persistentDataPath + "/" + "savedata.json";
save = new SaveData();
moveSceneManager = GetComponent<MoveSceneManager>();
if (Debug.isDebugBuild)
{
CreateDebugData();
}
else
{
if (File.Exists(filePath))
{
Load();
}
else
{
//初回起動時の設定
//ステージ1だけフラグを立てて選択可能にする
save.stageFlags = new bool[moveSceneManager.NumOfStage];
save.stageFlags[1] = true;
Save();
}
}
}
public void SetFlag(int stageIndex)
{
stageIndex = Mathf.Clamp(stageIndex, 0, moveSceneManager.NumOfStage - 1);
save.stageFlags[stageIndex] = true;
}
public bool GetFlag(int stageIndex)
{
stageIndex = Mathf.Clamp(stageIndex, 0, moveSceneManager.NumOfStage - 1);
return save.stageFlags[stageIndex];
}
public void Save()
{
string json = JsonUtility.ToJson(save);
StreamWriter streamWriter = new StreamWriter(filePath);
streamWriter.Write(json);
streamWriter.Flush();
streamWriter.Close();
}
public void Load()
{
if (File.Exists(filePath))
{
StreamReader streamReader;
streamReader = new StreamReader(filePath);
string data = streamReader.ReadToEnd();
streamReader.Close();
save = JsonUtility.FromJson<SaveData>(data);
}
}
public void InitSaveData()
{
save = new SaveData();
}
void CreateDebugData()
{
save.stageFlags = Enumerable.Repeat(true, moveSceneManager.NumOfStage).ToArray();
}
}
このクラスの肝はSave()メソッドとLoad()メソッドです。全体のコードは少し長めですがやっていること自体はシンプルで、まずSaveDataクラスのインスタンスを作り、その内容をLoad()メソッドで書き換えたりSave()メソッドでファイルに書き出したりします。
UnityでJSONを扱いたいときはJsonUtilityというのを使えば簡単に実装できます。セーブのときはSaveDataクラスのインスタンスをJsonUtilityでJSON化して、それをC#の機能でセーブファイルとして書き出します。逆にロードの時はC#の機能でセーブファイルを読み込んで、JsonUtilityでクラスに戻してSaveDataクラスのインスタンスを書き換えるというような処理を行います。
ともかく見慣れない部分が多いと思うので初見だと理解するのが大変だと思いますが、適宜ググって「ここはこういう意味なんだな」と腑に落ちるようにしてください。きちんと理解することでゲーム作りの力がいっそう養われることでしょう。
※注意:
今回のプログラムではセーブファイルの暗号化は行っていません。今回のゲームでは暗号化するまでもないので処理を省きましたが、ゲームによっては暗号化しないとまずい場合があるのでご注意ください。
GameManager.csの変更
あとは必要な場面でセーブ・ロード処理を呼び出すだけです。ステージをクリアしたときにセーブ処理が呼び出されるようにGameManager.csを変更します。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityStandardAssets.Characters.ThirdPerson;
[RequireComponent(typeof(MoveSceneManager))]
[RequireComponent(typeof(SaveManager))]
[RequireComponent(typeof(SoundManager))]
[DefaultExecutionOrder(-5)]
public class GameManager : SingletonMonoBehaviour<GameManager>
{
//省略
public void StageClear()
{
countDown = false;
character.allowInput = false;
if(clearSeName.Length > 0)
{
int randIndex = Random.Range(0, clearSeName.Length);
soundManager.PlaySeByName(clearSeName[randIndex]);
}
canvas = Instantiate(clearCanvasPrefab, transform.position, Quaternion.identity).GetComponent<Canvas>();
nextButton = GameObject.Find("NextStageButton").GetComponent<Button>();
titleButton = GameObject.Find("TitleButton").GetComponent<Button>();
//次のステージのフラグが立っていなければ立ててセーブ
if (moveSceneManager.CurrentStageNum < moveSceneManager.NumOfStage - 1) { bool flag = saveManager.GetFlag(moveSceneManager.CurrentStageNum + 1); if (!flag) { saveManager.SetFlag(moveSceneManager.CurrentStageNum + 1); saveManager.Save(); } } if (moveSceneManager.CurrentStageNum >= moveSceneManager.NumOfStage - 1)
{
nextButton.interactable = false;
}
else
{
nextButton.onClick.AddListener(() => moveSceneManager.MoveToStage(moveSceneManager.CurrentStageNum + 1));
}
titleButton.onClick.AddListener(() => moveSceneManager.MoveToStage(0));
}
//省略
次のページ→敵キャラクターの作り方1:アニメーションの設定
