はじめに
ゲーム開発において、セーブファイルの管理は非常に重要です。
ですが、セーブファイル作成って結構実装めんどくさいんですよね。管理も大変だしエラーもよくわからなかったりするし...。
そこで、これらの問題を解決するために「SSave
」というアセットを紹介しようと思います。
自分も愛用しており、セーブファイルの作成はこのアセットに頼りっきりです。
SSaveとは
SSave
はGMS2向けのシンプルなセーブファイルシステムです。
Itch.ioやGameMaker Marketplaceでダウンロード可能です。
恐ろしいことに無料です。
こちらは解説のGitHub github.com
SSave
はセーブデータの作成を容易かつ安全に行うための多くの機能を備えています。
以下に、SSave
の主な特徴を挙げます。
型付きの値:
保存されたデータの誤った型から発生するゲームクラッシュなどの問題を防げます。複数のセーブファイルタイプ:
メインのセーブファイルとは別に、設定ファイルなどの異なるタイプのファイルを作成できます。複数バージョンのセーブファイル:
複数のセーブスロットを持つことができ、セーブファイルタイプの異なるバージョンを管理可能です。データの自動同期:
古いセーブデータも新しいデータに自動的に同期されるため、
セーブファイルに値が必ず存在することを保証できます。ファイルの保護:
ユーザーによるファイルの改ざんを防ぐため、ファイルのエンコードや暗号化が可能です。
これらの操作は内部的に処理されるため、保護されたファイルをロードする際に追加作業は不要です。
これを聞くだけでも、かなり有能だ!という印象を受けますね。
SSaveを用いたセーブデータ管理の実装
次はセーブデータの管理と保護をどのように実装するかを、サンプルコードを用いながら解説します。
1. SSaveを継承したクラスの定義
まずSSave
を継承したconstructor
構造体を定義し、保存するデータの構造を設定します。
サンプルでは下記のConfig
構造体とSaveFile
構造体を定義しています
// 設定ファイル用の構造体を定義 // この構造体は "config" という名前でセーブデータを扱う function ConfigFile() : SSave("config") constructor { // セーブスロットのインデックスを登録 // 型: 実数 (REAL)、初期値: 0 // 例: プレイヤーが選択したセーブスロット番号を管理する add_value("saveIndex", SSAVE_TYPE.REAL, 0); }
上記のセーブデータでは使用するセーブデータのスロット番号をセーブしています。
マ◯オ64とかでセーブデータが4つあるようなイメージです。
// ゲームのセーブデータ用の構造体を定義 // この構造体は "save" という名前でセーブデータを扱う function SaveFile() : SSave("save") constructor { // プレイヤー名を登録 // 型: 文字列 (STRING)、初期値: 空文字列 ("") // 例: プレイヤーが設定した名前を記録 add_value("name", SSAVE_TYPE.STRING, ""); // プレイ時間を登録 // 型: 実数 (REAL)、初期値: 0 // 例: ゲームプレイの累計時間を記録(秒単位や分単位など用途に応じて) add_value("playtime", SSAVE_TYPE.REAL, 0); // プレイヤーのX座標を登録 // 型: 実数 (REAL)、初期値: 512 // 例: マップ上でのプレイヤーの位置を管理 (X軸) add_value("x", SSAVE_TYPE.REAL, 512); // プレイヤーのY座標を登録 // 型: 実数 (REAL)、初期値: 300 // 例: マップ上でのプレイヤーの位置を管理 (Y軸) add_value("y", SSAVE_TYPE.REAL, 300); }
上記のセーブデータでは主にプレイヤー情報をセーブしています。
SSave
機能を使うにはこのように、SSave
構造体を継承した構造体を用意することが必要です。
2. データの保存と取得方法
次に先ほど定義した構造体を使用してデータを保存し、取得する方法について
SSave
のサンプルを用いて説明します。
データの保存
// セーブデータを保存する処理を定義 Save = function() { // obj_ssave_demo_player インスタンスに対して以下の処理を実行 with (obj_ssave_demo_player) { // 設定データを取得 // ConfigFileからセーブスロット番号 (saveIndex) を取得するために使用 // static を使うことで処理を効率化し、ConfigFileの再取得を防ぐ static config = ssave_get(ConfigFile); // セーブデータを取得または作成し、以下のデータを更新 ssave_get(SaveFile, config.get("saveIndex")) .set("x", x) // プレイヤーの現在のx座標を保存 .set("y", y) // プレイヤーの現在のy座標を保存 .set("name", name) // プレイヤーの名前を保存 .set("playtime", playtime) // プレイ時間を保存 .save(); // 上記の更新内容をセーブデータに保存 } } // プレイヤーオブジェクトを作成 // obj_ssave_demo_player のインスタンスを (0, 0) の位置に生成 // ゲーム内でプレイヤーのデータを管理するためのオブジェクト instance_create_depth(0, 0, 0, obj_ssave_demo_player);
このコードはプレイヤーデータをセーブする処理を定義し、
セーブデータにプレイヤーの状態(位置や名前、プレイ時間など)を保存します。
Save処理
セーブデータにプレイヤーの現在の状態を登録して保存します:
ConfigFile
から現在のセーブスロット番号(saveIndex
)を取得- 指定されたスロットに対応するセーブデータ(
SaveFile
)を取得し、以下のデータを更新x
,y
: プレイヤーの座標name
: プレイヤー名playtime
: プレイ時間
.save()
メソッドで、更新内容を実際にファイルに書き込み
全体の流れ
- プレイヤーオブジェクトを (0, 0) の位置に生成。
Save
関数を呼び出すとConfigFile
を基にセーブスロットを選び、対応するSaveFile
にプレイヤーデータ(位置、名前、プレイ時間など)を保存。
3. データの取得
// プレイヤーデータをロードする処理を定義 // この関数は、セーブデータからプレイヤーの状態を取得して設定します Load = function() { // 設定データを取得 // ConfigFileから設定値をロードします。セーブスロットのインデックス(saveIndex)を取得するために使用。 // static を使うことで処理を効率化し、ConfigFileの再取得を防ぐ static config = ssave_get(ConfigFile); // セーブデータを取得 // 設定データ (config) の saveIndex に基づいて、対応するセーブデータを取得します。 var _save = ssave_get(SaveFile, config.get("saveIndex")); // セーブデータからプレイヤーの位置 (x, y) を取得して設定 x = _save.get("x"); y = _save.get("y"); // セーブデータからプレイ時間を取得して設定 playtime = _save.get("playtime"); // セーブデータからプレイヤー名を取得 var _name = _save.get("name"); // プレイヤー名が空の場合、名前を入力するようプレイヤーに促す // デフォルト値として "Geoffrey" を表示 while (_name == "") _name = get_string("Enter name for this save file:", "Geoffrey"); // 入力または取得した名前をプレイヤー名に設定 name = _name; } // プレイヤー名を初期化(デフォルトは空文字列) name = ""; // プレイ時間を初期化(デフォルトは0) playtime = 0; // プレイヤーデータをロード Load();
こちらのコードではセーブデータからプレイヤーの状態(位置、名前、プレイ時間など)をロードしています。
Load処理
- プレイヤーの状態をセーブデータから取得し、現在のプレイヤーオブジェクトに設定します。
処理の流れ:
- ConfigFileの取得:
ConfigFile
からセーブスロット番号(saveIndex
)を取得
- セーブデータのロード:
SaveFile
を利用して指定されたスロット番号のデータをロード
- セーブデータからの値の取得:
.get()
を使って以下の値を取得:x
,y
: プレイヤーの位置playtime
: プレイ時間name
: プレイヤー名
- 名前の入力処理:
- セーブデータにプレイヤー名がない場合、
get_string
を使ってユーザーに名前を入力させます。 - デフォルト名として
"Geoffrey"
を使用。
- セーブデータにプレイヤー名がない場合、
仕様
セーブスロット管理:
ConfigFile
に保存されたsaveIndex
を基に、対応するセーブスロットのデータをロードします。- 複数のセーブデータを扱う場合に便利です。
.get()
の利用:- セーブデータから特定の項目を取得し、プレイヤーオブジェクトに適用しています。
x
,y
,playtime
は即座に設定されますが、name
は空の場合に入力を促す仕様です。
データ保護レベルの設定
SSave
ではデータの保護レベルを選択することができます。
これにより、データの改ざんや不正アクセスを防ぎます。
各保護レベルの解説
下記3つのタイプが用意されています。
enum SSAVE_PROTECTION { NONE, // Save data is stored in plaintext json - good if you don't care about tampering ENCODE, // Save data is stored in base64 encoded json - good if you want *most* players to not know how to tamper ENCRYPT, // Save data is encrypted with a key - good if you want *most* players to be unable to tamper. This is NOT secure enough for sensitive data }
1. SSAVE_PROTECTION.NONE
- 内容: 保護なし。データはそのまま保存される
- 用途: 機密性のないデータに適している
- メリット: 読み書きが速く、簡単に実装できる、デバッグ等に用いれる
- デメリット: データ改ざんのリスクが高い
2. SSAVE_PROTECTION.ENCODE
- 内容: データがエンコードされ、人間に読みにくい形式で保存される
- 用途: 軽度のデータ保護が必要な場合に適している
- メリット: データ構造を隠すことで、改ざん防止効果がある
- デメリット: エンコードの解除が可能であり、完全な保護にはならない
3. SSAVE_PROTECTION.ENCRYPT
- 内容: データが暗号化され、復号キーなしでは読めない形式で保存される
- 用途: 高度なセキュリティが必要なデータに適している
- メリット: データの保護が非常に強力で、改ざんや不正アクセスを防げる
- デメリット: 処理が少し重くなる可能性があるが、
SSave
が内部処理を行うため、実装は簡単
設定方法
主に2つの方法で設定できます。
デフォルト値を変更もできますが、内部の処理に手をいれるのであまりオススメしません。
constructorで設定
constructor
でSSave
を継承する際、第二引数に設定することが出来ます。
// 暗号化を使用する設定 function SaveFile() : SSave("save", SSAVE_PROTECTION.ENCRYPT) constructor { add_value("name", SSAVE_TYPE.STRING, ""); add_value("x", SSAVE_TYPE.REAL, 0); add_value("y", SSAVE_TYPE.REAL, 0); }
set_protection メソッドを使用して後から設定
初期化後、set_protection
関数を呼び出すことで設定ができます。
// 各スロットの保護設定 ssave_get(SaveFile, "1").set_protection(SSAVE_PROTECTION.NONE); ssave_get(SaveFile, "2").set_protection(SSAVE_PROTECTION.ENCODE); ssave_get(SaveFile, "3").set_protection(SSAVE_PROTECTION.ENCRYPT);
まとめ
SSave
を利用することでGMS2でのセーブデータ管理が簡単かつ安全に行えます。
定義した構造体を通じてデータの構造を明確にし、内部メソッドを使用してデータの保存と取得を簡単に実現できます。
また、データ保護レベルを設定することでセーブデータの安全性を確保することができます。
これにより、ゲームの信頼性とプレイヤーの安心感を向上させることが可能です。
個人的には非常にオススメのアセットですので、ぜひ使ってみてください!