【GameMaker Studio2】ゲームデータの管理方法

この記事では、GameMakerでのゲームデータの管理方法について書きます。

# ゲームデータの定義
なお、ここでの「ゲームデータ」の定義としては以下のものです。

* プレイヤーのジャンプ力や攻撃力などのステータスパラメータ
* アイテムパラメータ
* 敵の体力やスコア

などなど……。
これらのパラメータをGMLに直接記述しても良いですが、パラメータ数が増えてきたときに、１つにまとまっていたほうが値を調整しやすくなります。

なお、ここで紹介する方法は外部データとなるので、"datafiles" フォルダ (Included Files) に格納することを想定しています。

# datafilesフォルダについて
まずは `datafiles` フォルダについてですが、このフォルダには GameMaker の標準リソースとしてサポートしていないデータを含めることができます。

このフォルダは、プロジェクト内の "datafiles" フォルダで、Asset Brower からも `Included Files` を選ぶことでアクセスすることができます。

![includedfiles.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/7801a36e-8ec0-7f3f-cfa8-e2b5d691d1f0.gif)

# 利用するデータ種別

GameMakerはバイナリデータとテキストファイルを読み込むことが可能なので、どのようなデータでも扱うことができます。

ただバイナリデータはバージョン管理しにくい（差分が取りにくい）ので、できればテキストデータにした方がよいと思います。（ただチート対策としてデータを暗号化する場合にはバイナリデータであるほうが、比較的難読化しやすいです）

テキストデータとした場合、GameMakerのゲームデータは CSV で管理するのが扱いやすいです。CSVであれば 
`load_csv()` で読み込みを簡単に行える関数が用意されているためです。

(INIファイルという選択もありますが、こちらは "datafiles" には入れられなさそうなので除外します。おそらく設定ファイル、もしくはセーブ用という印象です）

## CSVファイルの読み込み方法

CSVファイルは `load_csv()` で読み込むと、ds_grid のデータ型を返すので、`ds_grid_*()` を使ってデータにアクセスすることができます。

例として以下のようなCSVファイルを読み込むとします。

```csv:test.csv
id,name,hp,mp,str
1,hero,100,20,8
2,fighter,150,0,12
3,wizard,80,120,4
```

1行目には各パラメータの名称、2行目からデータの定義となります。

GMLでの読み込み例です。

```js
// "test.csv" を読み込む
var grid = load_csv("test.csv");

// ds_grid_height() で行の数を取得できる
for(var j = 0; j < ds_grid_height(grid); j++) {
  var str = "";
  // ds_grid_width() で列の数を取得できる
  for(var i = 0; i < ds_grid_width(grid); i++) {
    str += grid[# i, j] + ", "		
  }
  show_debug_message(str);
}
```

これを実行すると、Outputウィンドウに以下のように出力されます

```
id, name, hp, mp, str, 
1, hero, 100, 20, 8, 
2, fighter, 150, 0, 12, 
3, wizard, 80, 120, 4, 
```

注意点として__CSVファイルに「空行」が含まれている__と、そこも行とみなしてしまうことです。
よく起きる問題としては、

```csv:test.csv
id,name,hp,mp,str
1,hero,100,20,8
2,fighter,150,0,12
3,wizard,80,120,4
                        ←←←ここに空行がある
```

というCSVにしてしまうことです。
このCSVを先程のGMLの方法で読み込むとエラーとなります。

`load_csv()` の場合、空行には ""(空文字) が入るようになっているので、以下のように__行の先頭が空文字だったら無効なデータとして扱う__、という方法で対処しても良いかしれません。

```js
var grid = load_csv("test.csv");

for(var j = 0; j < ds_grid_height(grid); j++) {
  var str = "";
  if(grid[# 0, j] == "") {
    continue; // 行の先頭が空文字の場合はその行は処理しない
  }
  for(var i = 0; i < ds_grid_width(grid); i++) {
    str += grid[# i, j] + ", "
  }
  show_debug_message(str);
}
```

### CSVエディターについて
CSVはテキストエディタで編集することもできますが、専用のエディターを使った方が編集しやすいです。例えばWindowsでは 「Cassava Editor」が使いやすかった印象があります(最近は macOS に環境を移行したので使っていませんが……）

* [「Cassava Editor」表計算やマクロ機能を備えるCSVエディター - 窓の杜](https://forest.watch.impress.co.jp/library/software/cassavaedit/)

あと、macOS環境であれば VSCode の "Edit CSV" という拡張が良さそうな感じでした。

![Extension__Edit_csv_—_adv.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/fcc600bc-4de7-e847-a069-c133c92459c7.png)

![csvedit.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/c914df25-25c3-a9ca-1b03-e73a7baf0be3.gif)

### ExcelをCSVに変換して使う
ゲームデータをExcelで編集できるようにすると、行に色を付けたり、項目の移動やコピーが楽にできたり、数式が使えたりして色々と便利です。

GameMakerはExcelを直接読み込むことはできませんが、Excel上からCSV形式で保存する、または VBA で CSVに変換する、PythonやRubyなどのスクリプト言語でコンバートする、など色々な方法で変換することができるので、興味のある人はやってみてもいいかもしれません。
（GMLを使いこなせる力があれば、たぶんそんなに難しくはないはずです）

# JSONをゲームデータとして扱う
ややマニアックな方法となりますが、JSONをゲームデータとして扱うのも場合によってはありです。

JSONの良いところは、データ構造をとても柔軟に定義できるところです。
例えば CSVではデータ構造が2次元の表形式に限られますが、JSONの場合は項目の下にリストやオブジェクトを含めることができます。
![castledb.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/552f4b3d-5861-a4be-1cde-291e106993a6.gif)
上の画像では、項目 "choices" にリスト構造でデータを持たせることで、表形式では定義できなかったデータ構造にしています。
もしこのようなデータ構造を CSVで実現すると、"choices[0]" / "choices[1]" / "choices[2]" ... というように横に項目を増やすことになります。

ただ、JSONのような柔軟なデータ構造は自由度が高いぶん扱いが難しいかもしれません。
また、JSONはテキストデータであるものの、CSVよりもかなり読みにくいというデメリットがあります。そのためJSONのテキストファイルを直接編集するのは結構厳しいと思います。

なお画像で紹介したエディタは [CastleDB](http://castledb.org) という静的なデータベースで、JSONでデータを保持しています。ただマイナーすぎて情報が少ないツールなので、自力で試行錯誤してデータ構造を解析しなければなりませんが……。

過去に私が Haxeを使っていたときに書いたドキュメントがあるので、もし使いたい場合は参考になるかもしれません。

* [HaxeでのCastleDBの使い方 - HaxeFlixel Wiki](http://haxeflixel.2dgames.jp/index.php?CastleDB%2FHaxe)

# セーブデータについて
JSONについて書いたので、セーブデータについても書いておきます。

## JSONをセーブデータとして使用する
JSONに編集可能なゲームパラメータを定義して使うのは難易度が高いですが、セーブ用として扱うぶんには難しくなく、複雑なパラメータを扱うゲーム（RPGやシミュレーターなど）の保存データとして扱いやすいかと思います。

以下、JSONのセーブ処理とロード処理の例です。

```js:セーブ処理
// ds_mapを生成
var map = ds_map_create();

// パラメータを設定
map[? "name"] = "主人公";
map[? "hp"] = 100;
map[? "money"] = 1980;

// JSON文字列に変換
var str = json_encode(map);

// "save.json" に書き込み
var file = file_text_open_write("save.json");
file_text_write_string(file, str);
file_text_close(file);

// ds_mapを消しておく
ds_map_destroy(map);
```

上記コードを実行すると、"save.json" として以下の内容のテキストが保存されます

```json:save.json
{ "money": 1980.0, "hp": 100.0, "name": "主人公" }
```

なお各環境でのファイルの保存場所は以下のとおりです。 

* __Windows__: %LOCALAPPDATA%/[ゲームプロジェクト名]/
* __macOS__: %HOME/Library/Application Support/com.yoyogames.macyoyorunner/game/

ただ、Windowsは手元に環境がないため未確認です。

保存した "save.json" をロードする処理は以下のように書きます。

```js:ロード処理
// "save.json" を読み込み
var file = file_text_open_read("save.json");
// テキスト読み込み
var str = file_text_read_string(file);
// ファイルを閉じる
file_text_close(file);

// JSON (ds_map) に変換
var data = json_decode(str);

// パラメータを表示
show_debug_message(data[? "name"]);
show_debug_message(data[? "hp"]);
show_debug_message(data[? "money"]);
```

先程の "save.json" を読み込んだ場合の実行結果は以下のとおりです。

```
主人公
100
1980
```

## INIファイルにデータ構造を保存
JSONに変換せずに直接 ds_map をテキストで保存する方法があります。
INIファイルでds_mapを保存する方法は以下のとおりです。

```js:INIファイル保存
// パラメータを ds_map に保存
var map = ds_map_create();
map[? "name"] = "主人公";
map[? "hp"] = 100;
map[? "money"] = 1980;

// "save.ini" に保存
ini_open("save.ini");

// ds_mapを文字列として書き込み
ini_write_string("player", "param", ds_map_write(map));

// INIファイルを閉じる
ini_close();

// ds_mapを削除
ds_map_destroy(map);
```

上記コードを実行すると以下の INIファイルが保存されます。

```ini:save.ini
[player]
param="930100000300000001000000050000006D6F6E6579000000000000000000F09E400100000002000000687000000000000000000000594001000000040000006E616D650100000009000000E4B8BBE4BABAE585AC"
```

暗号のような文字列が作られましたが、これは GameMaker での ds_map のデータ構造を表現した文字列となります。

これを読み込むコードは以下のとおりです。

```js:INIファイル読み込み
// INIファイルを開く
ini_open("save.ini");

// "player" セクションの "param" キーの値を取り出す
var str = ini_read_string("player", "param", "");

// INIファイルを閉じる
ini_close();

// ds_mapを生成
var data = ds_map_create();

// ds_map文字列表現から読み取り可能な形式に変換.
ds_map_read(data, str);

show_debug_message(data[? "name"]);
show_debug_message(data[? "hp"]);
show_debug_message(data[? "money"]);

// ds_map を破棄
ds_map_destroy(data);
```

実行結果は以下のとおりです。

```
主人公
100
1980
```

`ds_map_write()` / `ds_map_read()` を使うメリットとしては、お手軽に変換できて、保存した文字列が難読化されているということです。

チート対策として使えるほど強力な難読化ではありません（GameMakerでの変換方法を理解していれば簡単に復元できてしまいます）が、簡単にセーブデータを書き換えられてほしくない場合には、ひとまずこの形式で保存すれば問題ないと思います。

それに対してJSONで保存する場合は、ひとまず読むことのできるテキストファイルなので、開発中であればパラメータを書き換えてデバッグしやすくなるメリットがあります。

なお、文字列で保存できるデータ構造は ds_map だけではなく、他の形式も可能です。

* __ds_stack__(スタック): `ds_stack_write()` / `ds_stack_read()`
* __ds_queue__(キュー): `ds_queue_write()` / `ds_queue_read()`
* __ds_list__(リスト): `ds_list_write()` / `ds_list_read()`
* __ds_map__(マップ): `ds_map_write()` / `ds_map_read()`
* __ds_priority__(優先度付きキュー): `ds_priority_write)` / `ds_priority_read()`
* __ds_grid__(グリッド): `ds_grid_write()` / `ds_grid_read()`
 
# まとめ
この記事のまとめとしては以下のとおりです

* 外部データを読み込む場合は `datafiles` に配置する
* 外部データは CSVが扱いやすい（または ExcelからCSVに変換)
* セーブデータは INIファイルまたはJSONが扱いやすい
    * INIファイルに データ構造を文字列として保存することもできる

# 補足: game_save()について
セーブについて、GameMakerには `game_save()` というゲームの状態をまとめて保存してくれる便利な関数があるのですが、こういった関数はゲームエンジンの実装に依存した作りとなってしまうので、個人的には使っていないです。

念のためドキュメントを読むと、
> NOTE: This function is very limited and it is designed for the beginner to get a save system up and running quickly, but more advanced users may prefer to code their own system using the File functions, due to the fact that the game will not save any of the dynamic resources like data structures, surfaces, added backgrounds and sprites etc..

>注：この機能は非常に制限されており、初心者が保存システムをすばやく起動して実行できるように設計されていますが、ゲームが保存されないため、上級ユーザーはファイル機能を使用して独自のシステムをコーディングすることをお勧めします 
(保存されないもの: データ構造、サーフェス、追加された背景やスプライトなどの動的リソース)

というように、保存されない情報もあるので、使い方には注意が必要そうです。