ささみ雑記帳

少し長くなりそうな色々を書き留めておくためのノート。

ScriptableObjectにステージ配置データを保存、復元する仕組みの話

ブログ名を少し短くしました。(自分で読みにくかった…)

ステージ読込周りで、ステージ構成データみたいなScriptableObjectを割とよくつくるので、今回はその話です。

ステージの配置パーツをつくる

まず、ステージ内に配置する対象の「キャラ」「アイテム」といった個々のパーツは、

  • このコンポーネントが予め設定されている
  • こういうデータ初期化が必ずされている

みたいにある程度決まった形に制限されたパーツであって欲しいので、それぞれprefabとして作ることがほとんどです。
ここまでは多くの人の共通見解だと思います。

ステージの配置パーツをどう持たせるか問題

次に、これらの配置パーツを複数組み合わせて固有のステージデータを作ることを考えます。
まず思いつくのは、

  • キャラやアイテムを多数配置した、1ステージにつき1つのprefabをつくり、それをInstantiateする
  • キャラやアイテムを多数配置した、1ステージにつき1つのシーンをつくり、それをシーン読込する

辺りの統合型…100%自由に配置しちゃうぜ!方式だと思います。

このアプローチで進める場合、キャラやアイテムという単位で一度固めたprefabを複製しまくって配置する、といったワークフローになるのかな…と思うのですが、これをやると数が増えたり、パーツprefabに後で変更が加わったりした場合、問題が起こりそうです。

  • 複製したキャラ1000体に、実はこのコンポーネントが共通で必要だったので、手動で付け直さないといけない…
  • 複製したキャラ1000体のうち1体だけ、誤って必須コンポーネント外しちゃった。ごめんねてへぺろ

みたいな。後日発覚する拡張問題、ヒューマンエラーなどは普通に起こりうる話で。

パーツprefab+配置のみの情報でロードするアプローチの検討

「キャラ」「アイテム」といった配置パーツは、後日更新があり得るものなので、ある時点で完成品としてコピーするのではなく、通常通りのprefab(複製元)として扱い、自動生成する仕組みが欲しくなってきます。 実行時ではなく、ステージデータの作成中(非実行時)にです。

そこで、エディタ拡張でボタンを押すとprefabとして生成する風なアプローチを考えてみました。
こんな感じのです。

  • キャラやアイテムの配置情報だけをステージ配置データ(マスターデータ)として持つ
  • ステージ配置データの持たせ方はScriptableObject
  • インスペクタをエディタ拡張して、"Save"を押したらHierarchyからステージを読み取って位置を抽出、保存
  • インスペクタをエディタ拡張して、"Load"を押したら配置データにprefabをInstantiateして復元

下準備:ステージ配置パーツprefabの用意

これを実際やってみます。 ステージ配置周りの仕様は、ザックリ以下のように決めました。

  • 複数種類ある前提で試すため、CharacterとItemの2つのprefabをつくる
  • Resourcesフォルダ内にprefabを入れてそこから読む
  • Hierarchyウィンドウ側(実際に配置した際)は、"Stage"という空のGameObjectをつくってその下に配置
  • prefab化したGameObjectには、種類を識別するためStageCharacter, StageItemスクリプトを貼り付ける

Resourcesの下にこう入れておいて
f:id:sasanon:20181122025304p:plain

こんな風に生成されるイメージじゃ。
f:id:sasanon:20181122025309p:plain

配置データのオブジェクト名は、キャラ名やモンスター名、アイテム名が設定されると考えられるので、貼り付けた固有スクリプトで識別します。

StageCharacter, StageItemスクリプトについても、実際にはガッツリ固有処理が入る想定ですが、今回はそこは本筋ではないので、

using UnityEngine; public class StageCharacter : MonoBehaviour {}

using UnityEngine; public class StageItem: MonoBehaviour {}

これで。

ステージ配置データを定義

ScriptableObjectをステージ配置データ型DataStageをつくります。
gist.github.com クラスというよりも構造体のような状態。
DataCharacterとDataItemという子クラスの配列を持たせています。

4行目に書かれている通り、"Stage Data"という名前でアセットとして生成できるようになります。

f:id:sasanon:20181122031331g:plain

ステージ配置データの拡張エディタ処理を定義

Editorフォルダをつくり、その下にDataStage型の拡張エディタ用クラスをつくって、

f:id:sasanon:20181122031620p:plain

インスペクタの拡張処理を書き書き。

gist.github.com

Hierarchy上から”Stage”という名前になっているオブジェクトを探し出し、
その子階層にある"StageCharacter"や"StageItem"コンポーネント一覧を取得。

インスペクタには、まずSave/Loadボタンを表示します。

  • Saveボタンで、配置パーツ種別毎にTransformパラメータとオブジェクト名を保存。
  • Loadボタンで、配置パーツ種別毎にStage配下の当該種別をクリア、再生成して位置を設定。

してます。(ボタン処理は確認ダイアログを出したほうが良いです。その辺簡易版です…)
最後にDrawDefaultInspector();でデフォルトのインスペクタを表示しています。

実際に動かしてみます。
f:id:sasanon:20181122033115g:plain

Saveで配置情報が配列に保存され、Hierarchyから消しても、Loadでprefab再生成されます。

ステージ配置データできた!

そんな感じで、ステージ配置データを保存、復元する仕組みができました。

  • 複製したキャラ1000体に、実はこのコンポーネントが共通で必要だったので、手動で付け直さないといけない…

 →普通にprefabにコンポーネント付けて再Loadしたら直るよ

 →スクリプトで自動生成なので、こういうヒューマンエラーは防止できるよ

ゲームフローから実際に読み込む場合は、Load部分をそのままメソッド化して、ステージ配置データのアセットを参照、Loadメソッドを呼び出せば良さげですね。

今回やらなかったけどできそうなステージ設定とか

  • 固定扱いのステージオブジェクト
    今回のつくりでは、"StageCharacter"や"StageItem"コンポーネントが設定されたもののみを追加削除対象とするため、何も設定しなければ固定オブジェクトとして残り続けます。マップ地形は固定で…みたいなハイブリッドも問題なくできそうです。

  • 複数から選択扱いのステージオブジェクト
    StageData型は普通にクラスなので、当然ながら配置情報以外も設定できます。例えばenumでマップ種別を定義して、予め用意したマップ種別の中からひとつだけをGameObject.SetActive(true);で有効化、みたいなこともできそうです。