はじめに
AI(ここではゲーム等のNPC的な意味)を作る際、リアリティを追求しようとすればする程、様々な状態が必要となり、真っ当にStateパターンなんかで実装すると大量のStateクラスを量産する羽目になります。
大した処理もしてないのにコード量とクラス数ばかり増えて嫌になったので、ゲーム業界でよく使われていると噂のBehaviourTree(ここではBehaviorDesigner)を用いた実装を試してみました。
その中で学んだちょっとしたコツ的なものを残しておこうと思います。
BehaviorDesignerの使い方自体は、これまでに色々な方が書いて下さっているので、そちらを参照してください。
BehaviorDesignerでイベントを受け取ってTaskを実行する
普通に作っていればBehaviorDesignerのみでゲームを作りきるという事は無く、AIの一部をBehaviorDesignerで作って、基本的なロジックはC#スクリプトで作っていく事になると思います。
そうなると当然C#側で発行したイベントをBehaviorDesignerで受け取って処理をしたくなります。
簡単な例として「Spaceキーを押下した回数をLogで表示する」というのをBehaviorDesignerとC#スクリプトで作ってみます。
(当然このような簡単な処理はBehaviorDesignerだけで完結する話ですが、良い例が思い浮かばなかったので)
using UnityEngine;
using UniRx;
using System;
namespace BehaviorTreeTest
{
public class InputEventHandler : MonoBehaviour
{
public IObservable<int> OnKeyDown { get { return OnKeyDownSubject; } }
private Subject<int> OnKeyDownSubject = new Subject<int>();
private int m_Count = 0;
void Update()
{
if( Input.GetKeyDown( KeyCode.Space ) )
{
OnKeyDownSubject.OnNext( m_Count );
++m_Count;
}
}
}
}
using UnityEngine;
using BehaviorDesigner.Runtime;
using UniRx;
namespace BehaviorTreeTest
{
public class InputEventPresenter : MonoBehaviour
{
[SerializeField] BehaviorTree m_BehaviourTree = null;
[SerializeField] InputEventHandler m_Handler = null;
private void Start()
{
m_Handler.OnKeyDown
.Subscribe( ( count ) => m_BehaviourTree.SendEvent<object>( "OnKeyDown", count ) )
.AddTo( this );
}
}
}
InputEventHandler側でBehaviorDesignerにイベントを飛ばさない理由は、実際の実装でそんな設計をすると、BehaviorDesignerへの依存が強くなりすぎて将来的に確実に死ぬからです。
(BehaviorDesignerの自作Taskを作る際にも同様に、BehaviorDesignerのActionを継承したクラス(Task)とロジックを分離しないと使いまわしが効かないので、分離したほうがよいでしょう)
BehaviorDesigner側は下記のような感じで作ります。
はい。簡単ですね。これでSpaceキーを押下した回数をLogで表示できます。
C#側はBehaviorDesignerにイベントを送るだけなら実質1行です。
InputEventPresenter.csの「m_BehaviourTree.SendEvent<object>( "OnKeyDown", count )」この部分だけです。簡単!
1点だけ注意してほしいのは、SendEvent<object>の部分です。
BehaviorDesignerのHasReceivedEventは下記のようになっており、System.Objectを受け取る実装となっているため、「SendEvent<int>」とするとイベントを受け取ってくれません。
public override void OnStart()
{
// Let the behavior tree know that we are interested in receiving the event specified
if (!registered) {
Owner.RegisterEvent(eventName.Value, ReceivedEvent);
Owner.RegisterEvent<object>(eventName.Value, ReceivedEvent);
Owner.RegisterEvent<object, object>(eventName.Value, ReceivedEvent);
Owner.RegisterEvent<object, object, object>(eventName.Value, ReceivedEvent);
registered = true;
}
}