【Unity】バイナリー形式の改造されないセーブ・ロードを実装

当サイトで紹介する商品・サービス等の外部リンクは、アフィリエイト広告を含む場合があります。
スポンサーリンク
本記事を読むと以下の実行ができます

ボタンを押すと、改造できないセーブ・ロードをする。

Unityで作るノベルゲーム第8回は、Unityで改造されないセーブ・ロードを実装します。

前回第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」と互換性が高いです。

>>Easy Save 使用方法

サーバーにセーブデータを保管する

最近のゲームは、ネットワークの要素は欠かせないものです。

ネットワークの処理
  • ユーザーごとのセーブデータ管理
  • ガチャ・ログインボーナスの処理
  • マルチプレイの実行
  • 外部サービス(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は動画で実践的なゲーム開発を解説していて、
購入した講座は再生・停止・スキップなどが可能なオンデマンド形式なので、
専門的な内容を自分のペースで学習できます。

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 AppsApplication.persistentDataPath points to %userprofile%\AppData\Local\Packages\<productname>\LocalState.

iOSApplication.persistentDataPath points to /var/mobile/Containers/Data/Application/<guid>/Documents.

AndroidApplication.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);
タイトルとURLをコピーしました