【Unity】ScriptableObjectにマスタデータを持たせるメリットについて

【Unity】ScriptableObjectにマスタデータを持たせるメリットについて

私がUnityでScriptableObjectを使う場合、敵キャラのパラメータだったり、アイテムデータなどのマスタデータを持たせるために使うことが多いです。

Unity内でのマスタデータの持ち方については種々の方法がありますが、ScriptableObjectを使った場合のメリット、デメリットを考え、他の方法(スクリプト直書き、CSV、XML、JSON、YAML)との比較を行なっていきます。

 

環境

macOS 10.14 Mojave

Unity2018.2.20f1

ScriptableObjectとは

ScriptableObjectとは、特定のゲームオブジェクトに紐付かない共有データを格納するのに使われるクラス、およびそのインスタンスとして作成されるアセットのこと。

概要や作り方については以下の記事に記載したのでそちらも合わせてご覧くださいな。

 

今回はこの共有データを扱う点に注目し、ゲーム内のマスタデータをScriptableObjectで管理する上でのメリット、デメリットを考えます。

マスタデータについて

ここでは、ゲーム内で使用する不変のデータ群をマスタデータと定義します。

例えば敵キャラの名前や能力値、アイテムの名前や値段など、ゲームを実行している間に変わらないデータが該当します。

大元のマスタデータは多くの場合表形式で作成されます。敵キャラのデータであればこんな感じ。

名前 最大HP 攻撃力 防御力 経験値 ゴールド
スライム 4 2 2 1 2
ゴブリン 8 4 2 2 3
ジャイアントマウス 12 5 3 3 5
RPGのイメージ図
RPGのイメージ図

 

ツールはExcel、Numbers、Googleスプレッドシートなどの表計算ソフトを使うことが多いと思います。

ExcelがインストールされていればExcelを使うのが楽です。マクロ使えますもんね。

Macを使っていてExcelをインストールしていないなら、NumbersやGoogleスプレッドシートを使ってデータ作成を行えばOK。

最終的にCSVとして出力できれば、後からJSONに変えたりYAMLに変えたりできるので、チームでのルールに合わせて使うのが一番です。ひとりのチームだったら自分の好みで。

……と、話が逸れましたが、今回はこうした表計算ソフトなどで作られたマスタデータをUnityでどういう形式で持つかという点に焦点を当て、その中でScriptableObjectを使うメリット、デメリットを探ってみたいと思います。

 

データ形式について

作ったマスタデータは加工してUnityで扱うことになります。その時の形式として考えられるのは以下のもの。

  • コードにstaticな定数として直書き
  • ゲームオブジェクトのpublic/[SerializeField]なフィールドに入力
  • CSVファイル
  • XMLファイル
  • JSONファイル
  • YAMLファイル
  • ScriptableObjectのアセットファイル

直書きするケースはそうそう無いかもですが、念の為。

コードにstaticな定数として直書き

定数クラスを作って、そこにマスタデータを記述するやり方です。例えばこんな感じでしょうか。

モンスターの数だけこんな記述が続いていくイメージです。

メリット

うーん……メリット……。

定数を使っているので、ゲームの実行時にファイルから値をロードする処理がいらないことでしょうか。

static readonlyをconstに変えればコンパイル時に数字に置き換えてくれるため、さらに処理が早くなるかも?

ゲームの実行環境のリソースがファミコンレベルです、なんて時にはこうしたギリギリの切り詰めが必要かもしれません。もし30年前にタイムスリップしたら使えるかも。(なおC#はまだ生まれていない模様)

デメリット

ハードコーディングゥ!!

敵キャラが増えるたびに定数をソースコードに追加していく必要がありますし、それぞれ独立した定数なので、敵キャラのコントローラ用スクリプトから定数を参照する時には手動で指定する必要があります。

敵キャラの数だけコントローラ用スクリプトが増えるとか悪夢めいた状況ですね。コントローラを増やさなくても、switch文を使えばコントローラは1個で済むか。行数には目を瞑ろう。

あとはメモリ使用量の問題もあります。staticなクラスはゲーム実行中にずっとメモリ上に居座り続けるため、定数クラスが肥大化するとメモリを圧迫していきます。

コンパイル時に値が決まってしまうため、ゲーム実行中に値を変えることができません。これはゲーム開発中のバランス調整にめっちゃ響きます。値を修正したくなったらゲームを一度止め、値を変更してから再度コンパイル、そしてゲーム実行して確認、と時間がかかってしまいます。辛い。

入力の手間も忘れてはいけません。ソースコードに手動入力する手間はそれはそれは大変なものです。定数を定義する行をExcelのマクロで出力する手段も考えられますが、その能力があれば他のデータ形式の方がオススメです。

ゲームオブジェクトのpublic/[SerializeField]なフィールドに入力

シーン内に作成したゲームオブジェクト、あるいはPrefabとして作成したゲームオブジェクトにスクリプトをアタッチし、publicまたは[SerializeField]で公開しているフィールドに値を入力する方式です。

Inspectorで入力
Inspectorで入力

 

メリット

Unityですぐ使いやすい形でデータを持てるため、Unityエディタでゲームを実行中に値を変えられるのが便利です。こちらも実行時にファイルから値をロードする処理は必要ありません。

Inspectorからの入力なので、RangeAttributeで値の入力範囲を設定できる点も便利です。設定ミスで範囲外の値を入力するリスクを減らすことができますし、管理するデータが少ない場合はこの方法でもいいかもしれません。

デメリット

反面、管理するデータが多くなると手入力の作業が大変です。ExcelとUnityを行き来してコピペ、コピペ、コピペの嵐です。

昔RPGツクールでRPGを作っていた際、敵キャラのステータスをひとつひとつプレステのコントローラで打ち込んでいたのですが、あの時代を思い出すような作業ですね。時間とガッツがないときついです。

 

CSVファイル

CSVはComma Separated Valueの略で、カンマで区切られた値が並んだテキストファイルのこと。

Excelなどの表計算ソフトからCSV形式でデータを出力し、それをUnityにインポートする方法を考えます。

CSVファイルをインポート
CSVファイルをインポート

 

CSVファイルの内容
CSVファイルの内容

 

『マスタデータについて』で紹介した表をCSVにしてみました。Unityではテキストアセットとして扱われ、その中身を取得することができます。

ここでは実行時にCSVからデータをロードする方式を想定しています。

大まかな流れとしては、テキストアセットの読み込み -> System.IO.StringReaderを使って読み込み -> CSVファイルの中身をパース -> 格納用クラスにデータをセット、でしょうか。

メリット

なんと言ってもExcelなどの表計算ソフトからそのままアウトプットできるのが楽ちんです。テキストデータなので扱いやすいのもGood。

受け手側の方で読み込む行を指定することもできるので、ヘッダ行をそのまま残していても読み込む仕組みを作れます。

デメリット

実行時に扱いやすい形式に変換するコストがあります。

敵キャラのデータを読み込む場合は、個々の敵キャラのスクリプトからデータ格納用のクラスへの参照をセットすれば、種族ごとのパラメータを参照する形にできそうです。

戦闘シーンを読み込む時に毎回CSVファイルを読み込むことを考えると、データが大きい場合は時間がかかるかもしれません。

また、テストプレイ中のパラメータ変更については難しそう。

忘れてはならないのがパース時のエスケープ。これについてはどのデータ形式でも考えておいた方がいいけれど、CSVはエスケープしておきたい文字が多いので気にする必要があります。カラムの中にカンマがあると予期せぬところで分割されたりしますものね。

XMLファイル

XMLはExtensible Markup Languageの略語で、マークアップ言語です。

ExcelからXML形式でデータを出力し、それをUnityにインポートする方法を考えます。私は省力化するため、CSVからXMLを生成するWebツールを使いました。普段XMLでデータを扱わないからねぇ。

XMLファイルの内容
XMLファイルの内容

 

値はCSVファイルと一緒です。UnityではXMLファイルもテキストアセットとして読み込めます。

処理内容としては、テキストアセットの読み込み -> XDocumentを使ってパース -> 格納用クラスにデータをセット、となります。流れはCSVファイルの場合と一緒ですね。

メリット

CSVと比べるとデータの中身が分かりやすいです。大量のデータがある時にも、要素名と値の対応が分かりやすいのが便利です。ちゃんとデータを表現したいとなったらやっぱりXMLかな。

あとXML形式だと使い回ししやすいので、Unityの外でもデータを使いたいときに便利です。自分で作ったゲームのwikiを作るときとかにも使えそう。

デメリット

XMLだとファイルサイズが大きくなってしまいがちです。上で使ったCSVファイルの大きさが146バイト、XMLファイルの大きさは639バイトでした。値を要素名のタグで囲んでいることから、どうしても容量は大きくなってしまいます。

Excelから出力する際は、ヘッダをASCIIにしておいた方が無難です。エクスポートする時にはデータ整形用にコードを書いておけば、Excelで編集するときは日本語のヘッダー、XMLで出力するとASCIIのヘッダー、みたいな感じで住み分けできるかも。

読み込み時のコストについてはCSVファイルの場合と共通です。一度ファイル全体を読み込む必要があります。

Unityだけで使うのであれば、わざわざ変換の手間をかけてXMLで出力するメリットはないかも。

 

JSONファイル

JSONはJavaScript Object Notationの略語で、データ記法のひとつ。

ExcelからJSON形式でデータを出力し、それをUnityにインポートする方法を考えます。私は省力化するため、CSVからJSONを生成するWebツールを使いました。この説明文すら省力化です。

ExcelからJSON形式のエクスポートをする際もマクロが必要なんですよね。ちょっと手間はあります。……が、1回書いておけばあとは繰り返し使えるので最初だけ頑張ればOKです。

実際の用途としては、ローカルのマスタデータからファイルを生成するというよりは、サーバ上のマスタデータからクライアントに送信して使うことが多そう。ソシャゲとかではゲーム本体にデータを持たずに、サーバに置かれることが多いですもんね。

JSONファイルの内容
JSONファイルの内容

 

処理内容としては、テキストアセットの読み込み -> JsonUtilityを使ってパース -> 格納用クラスにデータをセット、となります。こちらも流れはCSVファイルの場合と似ています。

JSONUtilityを使う場合は受け皿となる[System.Serializable]なクラスか構造体を先に用意しておく必要があります。値をマッピングする際はC#側のフィールド名とJSONのキー名が一致していることも重要です。

メリット

XMLに比べてデータが軽く、そしてデータの内容が分かりやすいです。CSVとXMLのいいとこ取りのような形式。

特に威力を発揮するのがサーバとの通信。データが軽いのでパフォーマンス面で有利です。サーバ側の処理をJavaScriptで書いているなら相性もいいですし、使いやすいです。

JSONもライブラリのある環境が多いのでパースしやすい印象です。

また、UnityのマニュアルによるとJsonUtilityはよく使用されるJson.NETよりも著しく早いらしいので、実行中の処理コストも低いことからオススメしたい方式です。

デメリット

Excelからのエクスポート時にデータ整形用のマクロを書いたり、Unity側で受け皿用のクラスを準備しておくのが若干手間なくらいでしょうか。

いくら処理速度が早いといってもファイル自体が大きければそれだけメモリの使用量も増えるため、ある程度分割して読み込むなどの処理は必要かもしれません。

YAMLファイル

YAMLはYAML Ain’t Markup Languageの略で、マークアップ言語じゃないよ、という意味。略語の中に略語が含まれる不思議な名前です。

YAMLはJSONのようにデータ記法の一種です。

なんとなく馴染みがないなーなんて思っていましたが、Unityでも使われていました。なんと、Unityのシーンファイルの実体はYAMLで書かれているのです。

Excelから出力する場合はやっぱりマクロでデータ整形する必要があります。

XMLファイルの内容
XMLファイルの内容

 

処理内容としては、テキストアセットの読み込み -> YamlDotNet For Unityを使ってパース -> 格納用クラスにデータをセット、となります。やっぱりこちらも流れはCSVファイルの場合と似ています。

気を付けないといけないのは、Asset StoreからYamlDotNet For Unityをダウンロードしてインポートする必要があること。無料で使えるのでお値段の心配はありません。

また、受け皿として使うクラスでは、フィールドではなくプロパティとして定義する必要があります。getterもsetterも必要ない場合はプロパティ名の後に{ get; set; }だけ記載しておけばOK。プロパティしか認識してくれないのはYamlDotNetの仕様みたいです。

メリット

YAMLファイルの内容が人間から見て分かりやすいことがメリットです。この点はYAMLが推している点ですね。JSONでは書けないコメント文も書けます。

JSONと比べると、キーや文字列に付けていた”ダブルクォート”が必要ないのでファイルサイズも節約可能です。ゲーム実行中にシリアライズしてサーバに送信する、なんて時にもファイルサイズの面で多少有利です。

デメリット

Unityの中で機械的に使うことを考えると、マスタデータからエクスポートした時にコメント文はいらないんですよねぇ……。なのでデータ記法のメリットを活かしきれないのが辛いです。むしろ最初からマスタデータをYAMLで書くぐらいの方がメリットを享受できるかもしれません。

プラグインとしてAsset StoreからYamlDotNet For Unityを持ってこないといけない点もちょっとハードル高いかも。

受け皿として使うクラスでプロパティしか使えないのもちょっとびっくりポイント。

Rails触ってた人なら設定ファイルとかで見かけていると思いますが、慣れないとちょっと面食らうかも。

 

ScriptableObjectのアセットファイル

ふぅ、やっと今回の本題です。

冒頭でも案内しましたが、ScriptableObjectの概要や作り方については下のページをご覧あれ。

ScriptableObjectを継承したクラスからアセットファイルを作成し、そこにマスタデータの内容を設定する方式です。

スライムのパラメータ
スライムのパラメータ

 

Unityのアセットファイル、拡張子は「.asset」ですが、実は中身はYAML形式で記述されています。デフォルトではUnityのアセットファイルはテキスト形式で保存するようになっているためです。

「じゃあYAMLと何が違うの?」と思う方もいるかもしれませんが、大きな違いはゲームの実行中に変換の手間があるかどうかです。単純にYAMLファイルでマスタデータを保持している場合は変換が必要ですが、ScriptableObjectの場合はUnityですぐ使える形になっているためロードするだけでOKです。

さて、ScriptableObjectへの値の設定方法は大きく分けて2つ。Inspectorから入力する方式と、Editor拡張を使ってファイルから自動的に流し込む方式です。

Inspectorから入力する方法は既に述べた方式と共通したメリット・デメリットがあります。ScriptableObjectの強みを生かす場合には、Editor拡張と組み合わせて使うのが一番なので、ここではそのメリット・デメリットを扱います。

メリット

Editor拡張と組み合わせて自動的にScriptableObjectのアセットファイルを作れるのが強みです。作り方によってはExcelファイルをUnityプロジェクトに置いておき、それが即反映されるようにもできます。

そう、そんなEditor拡張がテラシュールブログさんで公開されているんです。神。

Excelの無い環境なら、CSVから読み込むツールを作っておけば自動的にファイル作成ができます。

CSV、XML、JSON、YAMLとの大きな違いは、既にUnityで使いやすい形でデータを持っていること。Resources.Loadなどですぐにロードできるので処理が早いんです。実行中にファイルからデータをロードして、マッピングして……という作業がいらないのが強みです。

好みにもよりますが、例えば敵キャラごとにScriptableObjectのファイルを分けておけば、敵キャラの出現パターンに応じて必要なデータだけ読み込むこともできるので、メモリ使用量も下がります。

ゲームの実行中にScriptableObjectアセットの値を変更できるのも強みです。バランス調整のサイクルがかなり早く回せるようになります。

デメリット

データを自動的に流し込むためにはEditor拡張でスクリプトを書く必要がある点がデメリット。でもゲームの実行中にCSVやJSONからデータを読み込むスクリプトが書けるなら、そんなに難しくありません。マッピングすればいいだけですからね。

ScriptableObject側で変更した値は、当然ながらマスタデータの方には反映されません。なので、ゲームの実行中に値を変えた後、マスタデータの方も編集する必要はあるかもしれません。

Editor拡張を使えばファイルに書き込むこともできるかも……? そのうちちょっとやってみたいです。

サーバから最新のマスタデータを引っ張ってくる点ではちょっと大変です。というのはAssetBundleを使ってリモートサーバからデータをダウンロードすることになるかと思いますが、このAssetBundleがまたしんどいんです。個人で作っているゲームだったらScriptableObjectのアセットデータをアプリにバンドルしちゃった方が楽だったりも。

 

メリットとデメリットを表でまとめると

文章が長くなってしまったので、表で比較してみましょ。定性的な観点から、という建前がありつつも割と主観的に評価を付けています。

◎は「とても良い」、◯は「良い」、△は「苦手」、×は「これはあかん」、-は「評価なし」です。

項目 直書き Inspector入力 CSV XML JSON YAML ScriptableObject
コードの柔軟性 ×
処理速度
ファイルサイズ
データの見やすさ ×
データ送信 × ×
バランス調整 ×
実装難易度

随分とScriptableObjectに甘いような気もしますが、全体的に優秀なのはJSONとScriptableObjectでしょうか。特にサーバからのデータ送信がある場合はJSONが強いです。

YAMLは受け皿のクラスでフィールドではなくプロパティを用意しないといけないことから柔軟性の面で一歩及ばず。ライブラリをAssetStoreから持ってこなきゃいけない点もちょっと惜しいところ。

サーバからのデータ送信についてだと、ScriptableObjectの場合はAssetBundleを使ってサーバからアセットファイルを取得できますが、AssetBundleの実装が辛いという点で△にしました。今後Addressables Systemが1.0版になってアセットファイルのやりとりが簡単になれば◯になるかも。

どれを選んだらいいか

という疑問が出てくるかと思いますが、その答えは「プロジェクトに依存する」です。なんてこった。

定量的な性能比較はまだしていないため信頼性の面ではアレですが、上記の表から各フォーマットの得手不得手を見ながらあなたのゲームの規模と照らし合わせて選択するのがベターです。

マスタデータはそんなに頻繁に変更しないし、ゲームに同梱します! というのであればScriptableObjectの形式で持っておくのが一番便利です。実行時のパフォーマンスもいい感じ。

ソシャゲを作っているので、マスタデータは結構追加されるし、万が一にもユーザ側でいじられたくありません! というのであればJSONを使って都度サーバから暗号化したデータをダウンロードする実装が良さそう。他のユーザに影響が出る部分ではチート対策が大事ですものね。

まとめ

最終的にプロジェクトに合わせて最適なフォーマットを選択しよう! という身も蓋もない結論になりました。

ただその選択の方針として、定性的な観点から各フォーマットとUnityとの相性を検討したので、選ぶための材料はちょっと増え……てるといいなぁ(願望)

個人的にはサーバから頻繁にデータを引っ張るならJSONを使って、そうでない部分はScriptableObjectを使って処理速度を上げていく方針にしています。

アセット作ってます!

CTA-IMAGE

Unityでの開発に役立つアセットを作っています。

3DダンジョンRPGを開発するスピードを200%加速するAssetや、ファンタジーRPGのダンジョンを彩るパーツを取り揃えています。

特に3DダンジョンRPGのゲームを1から作るのは結構時間がかかります。ダンジョン部分の作成はこうしたアセットを使って、開発をブーストさせてみませんか?