ボタンを押すと、改造できないセーブ・ロードをする。
Unityで作るノベルゲーム第8回は、Unityで改造されないセーブ・ロードを実装します。
- 第0回:「画面サイズ変更、ボタンとテキスト作成、UIの操作 」
- 第1回:「Excel(表計算ソフト)でCSVを作成して読み込む」
- 第2回:「画面クリックした回数だけCSVのテキストを表示する」
- 第3回:「CSVのテキストをスクリプトでUITextを操作、画面に表示する」
- 第4回:「ボタンクリックで画面クリック判定しない方法」
- 第5回:「CSVでテキスト、画像、音をパスで認識して呼び起こす方法」
- 第6回:「Auto機能の実装」
- 第7回:「クイックセーブ・ロードの実装」
- 第8回: 【本記事】
前回第7回のノベルゲームのシステム開発は、クイックセーブ・ロードを実装しました。
本製作は、ゲームを起動しているときは、データを記憶して進んだり戻ったりすることはできます。
しかし、再生ボタンを押してゲームを終了してもう一度スタートすると、データは保存されずに初期化される問題点があります。
前回紹介したように、「PlayerPrefs」や「JsonUtility」、アセット「Easy Save」を使った方法でUnityのゲームデータを保存する方法がありますが、これらは改ざんされたり、保存先が好ましくなかったり、1万円程度だったりとデメリットが目立ちます。
本記事では、これら3つを使わないで、セーブ・ロードを自作で開発し導入する方法を紹介します。
参考:「SAVE & LOAD SYSTEM in Unity」
立ち絵:イラストや
- ノベルゲームの作り方を知りたい。
- 数多のセーブ・ロードする方法を知りたい。
- 改造されないセーブとロードの作り方を知りたい。
セーブ・ロード
ゲームを攻略するには長時間要するので、プレイヤーの進行状況を保存し、再開するセーブとロードはコンピュータゲームに必要不可欠です。
また、ミスをしてゲームオーバーになった場合、セーブしたポイントからロードすることで、その状況からやり直すことができます。このようにプレイヤーにリトライの機会を与えることができます。
さらに、ノベルゲームのようなストーリー重視のゲームは、プレイヤーの選択や決定がストーリーの進行に影響を与えることがあります。セーブとロードを使用することで、異なるストーリー展開を探索し、複数のエンディングを体験することができます。
これらのように、セーブとロードはゲームやアプリケーションのプレイ体験を向上させ、プレイヤーにより良い制御と楽しみを提供する不可欠な機能です。
Unityにおけるセーブ機能
最近のゲームは自動でセーブされるので、自らセーブすることはあまりありません。
しかし、開発者側に立つと、セーブ機能を開発・導入しなければなりません。
Unityでは、前述したように主に3つのセーブ機能があります。
PlayerPrefs
PlayerPrefsは、Unityゲームエンジンで提供されている機能です。
PlayerPrefsを使用すると、ゲーム内のデータを永続的に保存および読み込むことができます。
要するに、ゲームを終了してからもう一度再生してもデータが保存されることになります。
保存できる変数は、int型・float型・string型の3つです。
オフラインでデータを管理できて、データベースを必要としません。
// データの保存
PlayerPrefs.SetInt("HighScore", 10000);
PlayerPrefs.SetFloat("Volume", 0.5f);
PlayerPrefs.SetString("PlayerName", "John");
// データの読み込み
int highScore = PlayerPrefs.GetInt("HighScore");
float volume = PlayerPrefs.GetFloat("Volume");
string playerName = PlayerPrefs.GetString("PlayerName");
// データの削除
// 全てデータ
PlayerPrefs.DeleteAll();
// HighScoreというキーのデータを削除
PlayerPrefs.DeleteKey("HighScore");
一見便利ですが、個人的には使用はおすすめしません。
Windowsの場合、レジストリにデータが保存されます。
マイクロソフトサポートが警告をしているように、レジストリエディターをむやみに編集するととりかえしのつかない状態になるので、できるだけセーブデータをここに保存したくありません。
レジストリ エディターを使用する場合は、注意が必要です。 レジストリを誤って編集すると、オペレーティング システムを完全に再インストールする必要がある重大な問題が発生し、データが失われる可能性があります。 非公式のソースによって提案される編集は避ける。 保護を強化するには、正式に公開された Microsoft ドキュメントに基づいて編集を行う前に、レジストリをバックアップしてください。 その後、問題が発生した場合に復元できます。 詳細については、 でレジストリをバックアップおよび復元する方法に関するページをWindows。
Windows 10 でレジストリ エディターを開く方法 – Microsoft サポート
また、PlayerPrefsはデータが比較的小さなサイズであることを前提としており、セキュリティを必要とする場合や、大量のデータを保存する必要がある場合は、別のシステムを検討してください。
JsonUtility
JsonUtilityは、Unityエンジン内でJSONデータのシリアル化(オブジェクトからJSON文字列への変換)およびデシリアル化(JSON文字列からオブジェクトへの変換)を行うための便利なクラスです。
JSON(JavaScript Object Notation)は、データをテキストベースで表現するための軽量で一般的なフォーマットであり、Unityプロジェクト内でのデータの保存、読み込み、交換に広く使用されます。
- スクリプトからJSON文字列への変換
[System.Serializable]
public class PlayerData
{
public string playerName;
public int playerScore;
}
// PlayerDataオブジェクトを作成
PlayerData player = new PlayerData();
player.playerName = "John";
player.playerScore = 10000;
string json = JsonUtility.ToJson(player);
- JSON文字列からスクリプトへの変換
PlayerData loadedPlayer = JsonUtility.FromJson<PlayerData>(json);
JsonUtilityはUnityのシリアライズされたクラス([System.Serializable]属性が付いているクラス)に適用されます。
シリアライズされたクラスは、公開されているフィールドに基づいてJSONデータが作成または読み込まれますが、Unityのコンポーネントクラスなど、一部のクラスはシリアライズできない場合があります。
JsonUtilityはシンプルで使いやすい方法でJSONデータの処理ができますが、高度なJSON操作や大規模なデータの処理には適していません。
Easy Save
有料アセットですが、Unityでセーブ・ロードをするならば、「Easy Save」一択です。
本アセットは、暗号化、圧縮、クラウドストレージ、スプレッドシート、バックアップなどの機能が搭載させていて、簡単にセーブデータを保存できます。
2011年からの販売実績があり、「PC、Mac、Linux、Windows Universal、iOS、tvOS、Android、Oculus、Steam、WebGL」と互換性が高いです。
サーバーにセーブデータを保管する
最近のゲームは、ネットワークの要素は欠かせないものです。
- ユーザーごとのセーブデータ管理
- ガチャ・ログインボーナスの処理
- マルチプレイの実行
- 外部サービス(SNS)の連携
セーブデータは、スマホやPC内に保存されるのではなく、主に、ゲームを配信している企業が保有しているサーバーのデータベース上に保存されます。
この時ユーザーは、PHPやJavaのような言語を使ったAPIサーバーが仲介として、MySQL(SQLベース)のようなDBサーバーにアクセスしています。
例えば、ユーザーがゲームを起動すれば、ID:1234番とAPIサーバーに送り、APIサーバーがDBサーバーから1234番のデータを探してユーザーに渡しているイメージです。
APIサーバーは使用していませんが、以下の記事からUnityでもPHPと連携してDBサーバー上の情報を得ることができます。
自作する暗号化するセーブ・ロード機能
3つの方法でセーブ・ロードする方法を紹介しました。
どれも機能としては充実していますが、改ざんされることがあり、個人開発者としてはお金がかかるのは使用を躊躇うことでしょう。
ここからは、改造されないセーブ・ロードを作成していきます。
ソースコード
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using UnityEngine;
[System.Serializable]
public class SaveData
{
public int MyInt;
}
public class SaveManager : MonoBehaviour
{
public static SaveManager instance;
public SaveData saveData1;
private string saveFileName1 = "Data.alicia";
void Start()
{
saveData1 = LoadDataFromFile(saveFileName1);
Debug.Log("saveData1.MyInt: " + saveData1.MyInt);
}
public void SaveData1()
{
saveData1.MyInt = csvcontroler.i;
Debug.Log("Button clicked. Saving " + saveData1.MyInt + " to file.");
SaveDataToFile(saveData1, saveFileName1);
}
public void LoadData1()
{
saveData1 = LoadDataFromFile(saveFileName1);
csvcontroler.i = saveData1.MyInt;
Debug.Log("saveData1.MyInt: " + saveData1.MyInt);
}
private void SaveDataToFile(SaveData data, string fileName)
{
string filePath = Application.persistentDataPath + "/" + fileName;
FileStream fileStream = new FileStream(filePath, FileMode.Create);
BinaryFormatter bf = new BinaryFormatter();
bf.Serialize(fileStream, data);
fileStream.Close();
// Debug.Log("Save data saved to " + filePath);
}
private SaveData LoadDataFromFile(string fileName)
{
string filePath = Application.persistentDataPath + "/" + fileName;
if (File.Exists(filePath))
{
FileStream fileStream = new FileStream(filePath, FileMode.Open);
BinaryFormatter bf = new BinaryFormatter();
SaveData data = (SaveData)bf.Deserialize(fileStream);
fileStream.Close();
// Debug.Log("Save data loaded from " + filePath);
return data;
}
else
{
Debug.Log("Save file not found.");
return new SaveData();
}
}
}
思い通りのゲームが作れない
Unityでゲーム開発しているけど完成しない。
技術的な壁や知識不足が原因で、思い描いたゲームを実現するのは難しいです。
しかし、Udemyは動画で実践的なゲーム開発を解説していて、
購入した講座は再生・停止・スキップなどが可能なオンデマンド形式なので、
専門的な内容を自分のペースで学習できます。
Unityの機能を網羅したいや作りたいゲームがある人はUdemy学習を取り入れましょう。
数多くある講座の中から特におすすめな講座を3つ紹介します。
初夏のセール開催中!(5月23日まで)
対象のコースが1300円から。
Unityのはじめの一歩としておすすめ。開発例に物理挙動やアニメーションを使用しているので、今後の開発が円滑になる。
トランプを題材にした講座。カードゲームやボードゲーム開発に応用可能
UnityエンジンのインストールやC#の文法に加えて、App StoreとGoogle Playにゲームをリリース方法を解説。
解説
初めに名前空間を定義します。
「using System.IO;」は、ファイルとデータストリームの読み書きを可能にして、ファイルとディレクトリに対するサポートを提供します。
「using System.Runtime.Serialization.Formatters.Binary;」は、オブジェクト、または接続されているオブジェクトをバイナリ形式でシリアル化(直列化)および逆シリアル化(デシリアライズ)します。
BinaryFormatterでのデータを改造することはできませんが、セキュリティに関して安全性を保障することはできません。
BinaryFormatter クラス
BinaryFormatter
は安全ではなく、セキュリティで保護することはできません。 詳細については、「BinaryFormatter セキュリティ ガイド」を参照してください。
特に逆シリアル化の脆弱性は懸念するべきで、攻撃者が脆弱性を突くと、サービス拒否 (DoS)、情報漏えい、またはアプリを遠隔で操作する恐れが発生する可能性があります。
しかしながら、本製作は、あくまでオフラインでも稼働できるセーブデータの管理です。
自分のPC内で解決することなので、セキュリティ面での不安感は否定的に考えてください。
これらは、C#ライブラリで提供される名前空間「Using System.;」の一種で、一般的なC#機能やクラスにアクセスできるようにします。
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using UnityEngine;
続いて、SaveData クラスを作成します。
[System.Serializable] 属性でマークして、シリアライズ可能なデータを保持します。
本スクリプトでは、int型のMyInt メンバー変数を持っています。
[System.Serializable]
public class SaveData
{
public int MyInt;
}
SaveManager クラスは、セーブとロードの機能を提供します。
「instance」 はシングルトンパターンを実装するために使用され、他のスクリプトからこのクラスへのアクセスを可能にします。
「saveData1」 はセーブデータを保持します。
「saveFileName1」 はprivateなstring型の変数で、セーブファイルの名前(Data.alicia)を指定します。
セーブファイルの名前は自分の好きなように設定できます。もちろん、拡張子も自分の好きなように作れます。
public static SaveManager instance;
public SaveData saveData1;
private string saveFileName1 = "Data.alicia";
Startメソッドは、ゲームが開始されたときに呼び出され、セーブデータをファイルから読み込みます。また、読み込んだデータの MyInt値をデバッグログに表示します。
void Start()
{
saveData1 = LoadDataFromFile(saveFileName1);
Debug.Log("saveData1.MyInt: " + saveData1.MyInt);
}
SaveData1メソッドは、ボタンがクリックされたときに呼び出され、csvcontrolerクラスのint型変数 「i」 を saveData1.MyInt に設定し、セーブデータをファイルに保存します。
LoadData1 メソッドは、SaveData1メソッドの逆をします。
ボタンがクリックされたときに呼び出され、セーブデータをファイルから読み込み、csvcontrolerクラスの「i」に読み込んだ値を設定します。読み込んだデータの MyInt値をデバッグログに表示します。
public void SaveData1()
{
saveData1.MyInt = csvcontroler.i;
Debug.Log("Button clicked. Saving " + saveData1.MyInt + " to file.");
SaveDataToFile(saveData1, saveFileName1);
}
public void LoadData1()
{
saveData1 = LoadDataFromFile(saveFileName1);
csvcontroler.i = saveData1.MyInt;
Debug.Log("saveData1.MyInt: " + saveData1.MyInt);
}
SaveDataToFileメソッドは、SaveDataオブジェクトをバイナリ形式でファイルに保存する役割を果たします。
「Application.persistentDataPath」は、Unityアプリケーションが実行されているプラットフォームに依存するデータ保存用のパスを返します。このパスに指定した「fileName」を追加して、ファイルの保存先のフルパスを取得します。
指定されたファイルパスでファイルストリームを作成します。
「FileStream」を用いることで、同期および非同期の読み取り操作と書き込み操作をサポートします。
「FileMode.Create」は、ファイルが存在しない場合に新しいファイルを作成し、既存のファイルがある場合は上書きします。
バイナリ形式でデータをシリアライズ・デシリアライズするためのバイナリフォーマッターを作成して、指定されたデータオブジェクト (data) をファイルストリームにシリアライズし、バイナリデータとして保存します。
ファイルストリームを閉じてファイルの保存が完了します。
private void SaveDataToFile(SaveData data, string fileName)
{
string filePath = Application.persistentDataPath + "/" + fileName;
FileStream fileStream = new FileStream(filePath, FileMode.Create);
BinaryFormatter bf = new BinaryFormatter();
bf.Serialize(fileStream, data);
fileStream.Close();
// Debug.Log("Save data saved to " + filePath);
}
LoadDataFromFile メソッドは、指定されたファイルからセーブデータを読み込む役割を果たします。
保存されたセーブデータが格納されているファイルのフルパスを取得します。
指定されたファイルパスが存在するかどうかを確認し、ファイルが存在する場合は、セーブデータが読み込みます。
セーブデータが存在しない場合、新しい SaveDataオブジェクトを作成して返します。
ファイルストリームを作成し、指定されたファイルを開きます。「FileMode.Open」は既存のファイルを読み込むモードです。
バイナリフォーマッターを作成して、ファイルストリームからデータをデシリアライズし、SaveData オブジェクトとして読み込みます。
ファイルストリームを閉じて、ファイルの読み込みが完了します。
読み込んだセーブデータを呼び出し元に返します。
private SaveData LoadDataFromFile(string fileName)
{
string filePath = Application.persistentDataPath + "/" + fileName;
if (File.Exists(filePath))
{
FileStream fileStream = new FileStream(filePath, FileMode.Open);
BinaryFormatter bf = new BinaryFormatter();
SaveData data = (SaveData)bf.Deserialize(fileStream);
fileStream.Close();
// Debug.Log("Save data loaded from " + filePath);
return data;
}
else
{
Debug.Log("Save file not found.");
return new SaveData();
}
}
実演
スクリプトが完成しましたら、Hierarchyウィンドウにアタッチしてください。
その後、セーブ・ロードするUIボタンを「onclick」にアタッチしてください。
アタッチが完了しましたら、再生ボタンを押してください。
冒頭で紹介した動作ができれば成功です。
データの改造
出力したファイル「Data.alicia」をメモ帳で開くと、次のように出力されます。
MyIntは、保存する変数で、SaveDataは、保存するクラスです。
これら変数を改ざんして、異常な数値を作成するチート行為は不可能です。
要するに、データを削除することしかやりようがありません。
まとめ
本製作では、Unityで改造されないセーブ・ロード機能を自作する方法を紹介しました。
再生ボタンを押してゲームを終了してもう一度スタートすると、データは保存されずに初期化されてしまいます。
色々な方法でセーブ・ロードできますが、バイナリ形式で外部でデータを保存することをおすすめします。
しかし、BinaryFormatterのセキュリティに関して安全性を保障することはできません。
オフラインでのセーブデータを作成する程度の規模であれば一切問題ありません。
最近のゲームは、ネットワークを使って、サーバーにアクセスしてデータベース上に保存することが多いです。
膨大なセーブデータでセキュリティーを懸念する場合は、DBサーバーを使ったセーブ・ロードを実装してください。
セーブデータの保存先
セーブデータがどこに保存されているか疑問に思われているでしょう。
公式ページにパスの場所は言及されています。
Windows Store Apps: Application.persistentDataPath points to %userprofile%\AppData\Local\Packages\<productname>\LocalState.
iOS: Application.persistentDataPath points to /var/mobile/Containers/Data/Application/<guid>/Documents.
Android: Application.persistentDataPath points to /storage/emulated/0/Android/data/<packagename>/files on most devices (some older phones might point to location on SD card if present), the path is resolved using android.content.Context.getExternalFilesDir.
Application-persistentDataPath – Unity スクリプトリファレンス
もしも、見つからない場合は、スクリプトでコメント文としていましたが、「filePath」がデータの変数から探してください。
Debug.Log()を使用すれば、consoleウィンドウにパス(所在地)を得ることができます。
Debug.Log(filePath);