hasht's notes

ゲームAIやUnityの話題

UnityのScriptableObjectでキャラクターのAIを組む

ScriptableObjectとは

インスタンスがアセットとして扱えるクラス。

注意点

バイナリでシリアライズしない方がよい

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がぐちゃぐちゃになって死ぬ。

www.youtube.com

そこで例えばこの動画のように、エージェントが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();
}

参考