UnityのScriptableObjectでキャラクターのAIを組む
ScriptableObjectとは
インスタンスがアセットとして扱えるクラス。
- 継承して使う
- インスタンスがアセットとして保存できる(シリアライズ可能なプロパティだけ)
- AssetDatabase.CreateAssetで保存
- Resources.LoadやAssetDatabase.LoadAssetAtPathでロード
注意点
バイナリでシリアライズしない方がよい
Asset SerializationをForce Textにする。 バージョン管理する場合は特に。
インスタンスは共有される
何回ロードしても同じものへの参照が返される。
ポリモーフィズムには対応していない
「Unityのシリアライゼーション | Unity Japan Official Blog」を参照。
例えばメンバをインターフェースや抽象クラスで持たせたりできない。シリアライザの都合上可変サイズにできないのだろうか?
これは正直どうすればいいかよく分からなくて、自前で更にシリアライズ・デシリアライズ的な処理をしなければならない感じが辛い。(ISerializationCallbackReceiver
やリフレクション芸で一見上手く動くようにすることはできる)
AIを組む
ゲームのAIを組む場合、行動ロジックをScriptableObjectとして組み、パラメータの違いによるいろいろなバリエーションをアセットとして保存しておくのが便利。
using UnityEngine; using UnityEditor; public class CharacterAI : ScriptableObject { [SerializeField] private float _waitTime; [SerializeField] private float _moveTime; // ここがアレなので次も読んでね private bool _isWaiting = true; private float _elapsed = 0; public CharacterAI(float waitTime, float moveTime) { _waitTime = waitTime; _moveTime = moveTime; } public void Update(float dt) { _elapsed += dt; var duration = (_isWaiting ? _waitTime : _moveTime); if (_elapsed > duration) { _isWaiting = !_isWaiting; _elapsed -= duration; } if (_isWaiting) { Wait(); } else { Move(); } } // なんらかの実装をする private void Wait(); private void Move(); }
単純かつロジックがハードすぎるが、new CharacterAI(0.5f, 1f)
とかnew CharacterAI(1f, 0.5f)
とかいろいろなAIを作成して、アセットとして保存しておくことができる。
エディタ拡張でGUIからそういうAIの作成とか編集とかを補助するとよい。
状態を持たせる
インスタンスが共有される都合で、エージェント固有の状態を持たせることができない。
つまり上のAIを複数のエージェントに与えると、_isWaiting
や_elapsed
がぐちゃぐちゃになって死ぬ。
そこで例えばこの動画のように、エージェントがContextを持ち、実行ごとにロジックに渡すようにする。
using UnityEngine; using UnityEditor; public class CharacterContext { public bool isWaiting { get; set; } public float elapsed { get; set; } } public class CharacterAI : ScriptableObject { [SerializeField] private float _waitTime; [SerializeField] private float _moveTime; public CharacterAI(float waitTime, float moveTime) { _waitTime = waitTime; _moveTime = moveTime; } public void Update(float dt, CharacterContext context) { context.elapsed += dt; var duration = (context.isWaiting ? _waitTime : _moveTime); if (context.elapsed > duration) { context.isWaiting = !context.isWaiting; context.elapsed -= duration; } if (context.isWaiting) { Wait(); } else { Move(); } } // なんらかの実装をする private void Wait(); private void Move(); }