【Unity】セーブ機能の作り方-データ改造ができないシステム

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

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

ゲーム開発においてプレイヤーが途中でゲームを終了し、
次回に続きから再開できるセーブ機能は非常に重要です。

しかし、単なるセーブ・ロード機能だけでなく、データ改造を防止するシステムを実装することで、
開発者はゲームの秩序を保つ必要があります。

また、ノベルゲームでは、クイックセーブ・ロード(Q.save,Q.Load)という機能があり、
セーブをして、ロードすると、その場面に戻ります。

クイックセーブ・ロード(Quick save,Quick Load)についてまとめています。

この記事でも言及していますが、
ゲームを起動しているときはデータを記憶して進んだり戻ったりすることができます。

しかし、再生ボタンを押してゲームを終了してもう一度スタートすると、
データは保存されずに初期化される問題があります。

本記事では、Unityで改造されにくいセーブ機能の作り方について解説します。
これから紹介する手法を用いれば、安全で効率的なセーブシステムを構築できます。

本記事は次の人におすすめ
  • Unityでセーブ機能を初めて実装する開発者
  • データ改造を防止したいと考えている方
  • PlayerPrefs以外のセーブ手法を探している方
  • 軽量かつ効率的なセーブシステムに興味がある方
  • 安全なセーブ機能を用いたゲームを作りたい方
Udemyで学習する
スポンサーリンク

セーブ・ロード

ゲームを攻略するには長時間要するので、プレイヤーの進行状況を保存し、
再開するセーブとロードはコンピュータゲームに必要不可欠です。

また、ミスをしてゲームオーバーになった場合、セーブしたポイントからロードすることで、
その状況からやり直すことができます。
このようにプレイヤーにリトライの機会を与えることができます。

この性質を持つためRPGやアドベンチャーゲーム、シミュレーションゲームなど、
長時間のプレイが必要となるゲームで特に重要です。

特に、ノベルゲームのようなストーリー重視のゲームは、
プレイヤーの選択や決定がストーリーの進行に影響を与えることがあります。

セーブとロードを使用することで異なるストーリー展開を探索し、
複数のエンディングを体験することができます。

これらのように、セーブとロードはゲームやアプリケーションのプレイ体験を向上させ、
プレイヤーにより良い制御と楽しみを提供する不可欠な機能です。

Unityにおけるセーブ機能

最近のゲームは自動でセーブされるので、自らセーブすることはあまりありません。
しかし、開発者側に立つと、セーブ機能を開発・導入しなければなりません。

Unityでは、「PlayerPrefs」・「JsonUtility」・「Easy Save」といった
3つのセーブ機能があります。

主な機能と使用する場面
  • PlayerPrefs: シンプルなハイスコアや設定の保存に最適。
  • JsonUtility: データ構造が複雑なゲームのセーブに向いている。
  • Easy Save: 暗号化機能が求められる場合や、アプリ全体でセーブデータを管理したいときに最適。

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.Save();

// データの削除
// 全てデータ
PlayerPrefs.DeleteAll();
// HighScoreというキーのデータを削除
PlayerPrefs.DeleteKey("HighScore");

データの書き込みのタイミングはアプリ終了時、あるいは”PlayerPrefs.Save();”の実行時です。
一見便利ですが、個人的には使用はおすすめしません。

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操作や大規模なデータの処理には適していません。

メリット・デメリット
  • メリット: 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サーバー上の情報を得ることができます。

UnityとPHPを連携する方法を紹介しています。

セーブデータをサーバーに保管することは、ローカルデバイスにデータを保存する方法よりも
データの安全性や同期性を高める重要な手段です。

特にオンラインゲームや、複数のデバイス間で進行状況を共有(クロスプラットフォーム)したい場合に有効です。

Unityでマルチプレイを実装する方法を紹介しています。

ローカルでのセーブ保存に対するメリットとデメリットを紹介します。

メリット
  • セキュリティ: ローカルデバイスに保存する場合と比べ、データ改造のリスクが大幅に低減される。
  • データの同期: プレイヤーが異なるデバイスでゲームをプレイする際、同じ進行状況を反映できる。
  • バックアップ: ローカルデバイスの破損や紛失があっても、サーバーに保存されたデータを使って復元できる。
メリット
  • コスト: サーバーの維持や通信のためのコストが発生する。また、クラウドサービスを利用する場合、料金プランに依存する。
  • 通信遅延: サーバーとの通信が必要なため、ネットワークの状態によってはデータの保存やロードに時間がかかることがある。
  • 実装の複雑さ: サーバーとクライアント間の通信を適切に設計・実装する必要があり、ローカルセーブよりも高度なスキルが要求される。

REST APIやWebSocketなどを使用し、クライアントとサーバー間でデータを送受信して、実装できます。

自作する暗号化するセーブ・ロード機能

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();
        }
    }
}

UdemyでUnityを学習

Udemyは、オンデマンド式の学習講座です。
趣味から実務まで使えるおすすめの講座を紹介します。

\Unityの学習から収益化の方法はこちらから!/



解説

  • 名前空間

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は保存するクラスです。
これら変数を改ざんして、異常な数値を作成するチート行為は不可能です。

要するに、データを削除することしかやりようがありません。

セーブデータの保存先

セーブデータがどこに保存されているか疑問に思われているでしょう。

公式ページにパスの場所は言及されています。

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);

まとめ

Unityでは、再生ボタンを押してゲームを終了してもう一度スタートすると、
データは保存されずに初期化されてしまいます。

セーブ機能はゲーム体験を支える重要な要素です。
特に改造されにくいバイナリ形式を用いることで、ゲームの公平性を保つことができます。

他にも、PlayerPrefs、JsonUtility、Easy Saveといった様々な手法が提供されており、
目的に応じて選択することが重要です。

色々な方法でセーブ・ロードできますが、
特にバイナリ形式で外部でデータを保存することをおすすめします。

しかし、BinaryFormatterのセキュリティに関して安全性を保障することはできません。
オフラインでのセーブデータを作成する程度の規模であれば一切問題ありません。

最近のゲームは、ネットワークを使って、
サーバーにアクセスしてデータベース上に保存することが多いです。

膨大なセーブデータでセキュリティーを懸念する場合は、
DBサーバーを使ったセーブ・ロードを実装してください。

お借りした素材

立ち絵:イラストや

Udemyで学習する

この記事を書いた人

プロフィール

アリッシア

                 

大学4年間で何か胸を張れるスキルを身に着けたくて当サイト運営を始めました。
現在、大学院に進学するか就職するか迷いながら勉強しています。
詳しいプロフィールはこちら

Contact icon

contact

X icon

X

Instagram icon

Instagram

Note icon

Note

スポンサーリンク
Unity
フォローする