LoginSignup
51
47

More than 5 years have passed since last update.

Editor拡張のFrameworkを使ってBehaviour Designer のようなAIシステムを作る。

Last updated at Posted at 2016-04-24

概要

BehaviorDesignerのようなEditor拡張のAIシステムを作ってみました。
githubのリンク先は下記になります。Unityちゃんやら、Standard AssetsのCharactersやらが入っているのでちょっと容量が大きいかも。

(追記)
Unityで実行する場合は、BehaviourTreesフォルダの上の階層のフォルダ名をAssetsに変更して、直下にあるmain.unityを再生して下さい。

BehaviourTree Editor

自分の過去記事で、Behaviour TreeのUniRxによる実装と、Unityでノードベースなウィンドウを簡単に作る為の「Simple node editor」の紹介がありますが、今回の記事ではこの2つを組み合わせて実装するところまで書きます。

実現するBehaviourTree

下の図のようなBehaviourTreeAIを作ります。2人のプレイヤーがいるとして以下の4つのアクションを繰り替えし行うイメージです。

  1. HPが2より大きければ敵に向かって進む
  2. 敵との距離が近ければ攻撃する
  3. HPが2以下になったら定位置に戻る。
  4. 回復する

BehaviourTreeUML.png

Behaviour Tree実行部分の作成

Behaviour TreeのUniRxによる実装を見てください。

「Simple node editor」でBehaviourTree Editorを作る。

BehaviourTreeノードクラスを作る

Simple Node Editorはノードエディタを作る為のフレームワークです。NodeクラスをオーバーライドしてBehaviourTreeノードクラスを実装します。このフレームワークだけだと足りない機能があって、BehaviourTreeノードに必要なActionノードにメソッドを設定して、Behaviour Tree実行時にそのメソッドを実行させる事が出来ないので、これを追加します。

やり方としては、下記コードのようにリフレクションを使って、指定したクラスのpublicメソッド名を取得してNodeEditorに表示して選択できるようにし、実行するときには指定のGameObjectにアタッチされているクラスのメソッドのdelegateを作って、それを渡して実行すれば良いです。

BTreeUtil.cs
/// <summary>
/// 指定したクラスの全てのメソッドを取得。
/// </summary>
/// <returns>The methods.</returns>
/// <typeparam name="T">The 1st type parameter.</typeparam>
public static string[] GetMethods<T>(){
//Tを継承したクラスを全て取得
        var clacces = Assembly.GetAssembly(typeof(T)).GetTypes().Where(t => t.IsSubclassOf(typeof(T)));
        return clacces
            .SelectMany(c=>c.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly))
            .Select(p=>p.Name).ToArray();
    }

BTreeUtil.cs
    /// <summary>
    /// 指定のGameObjectの指定のメソッドのDelegateを取得
    /// </summary>
    /// <returns>The action.</returns>
    /// <param name="go">Go.</param>
    /// <param name="type">Type.</param>
    /// <param name="methodName">Method name.</param>
    /// <typeparam name="T">The 1st type parameter.</typeparam>
    public static Action<T> GetAction<T>(GameObject go, Type type, string methodName){
        var clacces = Assembly.GetAssembly(type).GetTypes().Where(t => t.IsSubclassOf(type));
        var query = clacces
            .SelectMany(c=>c.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly));

        Component comp = null;
        foreach(var c in clacces){
            ///Publicメソッドを取得(継承されたメンバは含まない)
            foreach (var m in c.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly))
            {
                if(m.Name == methodName){
                    comp =  go.GetComponent(c.Name);
                }
            }
        }
        var mquery = query.Where(p=>p.Name == methodName);
        var minfo = mquery.FirstOrDefault();
        var result =  (Action<T>)Delegate.CreateDelegate(typeof(Action<T>),comp,minfo);
        return result;
    }

Nodeクラスをどう拡張してごにょごにょしてるかは、本記事のTOPのリンク先のソースコードを参考にしてみてください。
Assets/Node_Editor/Nodes/BehaviourTree/uActionNode.cs

そんなこんなで出来たBehaviourTreeのNodeEditorで構築したAIは下記のような感じになります〜。

BehaviourTreeNode.png

Editorで作成したAIを実行するクラスを作成する。

TOPのプロジェクトの中のAssets/Scripts/BTreeManager.csがそのクラスです。
中身をざっくり説明すると以下のような流れでAIを実行しています。
1. NodeEditorで作成したScriptableObjectをロード
2. 再帰的にNodeTreeを構築。
3. ActionNodeとDicoratorNodeにDelegateを登録
4. NodeTreeを繰り替えし実行

BTreeManager.cs
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using NodeEditorFramework;
using NodeEditorFramework.Utilities;
using System.Linq;
using BehaviourTrees;
using System;
using UniRx;

/// <summary>
/// NodeEditorで作成したScriptableObjectからBehaviourTreeを構築し、実行する
/// </summary>
public class BTreeManager : MonoBehaviour {

    [SerializeField, FilePath]
    string filePath;

    BehaviourTreeInstance node;

    RuntimeNodeEditor runtimeNodeEditor;

    Action m_task;

    uBehaviourTreeNode m_topNode;
    BehaviourTreeBase   m_bTreeBase;


    void Awake(){
        runtimeNodeEditor = GetComponent<RuntimeNodeEditor>();
        if(runtimeNodeEditor!=null){
            runtimeNodeEditor.canvasPath = filePath.Replace (Application.dataPath, "Assets");
        }
    }

    // Use this for initialization
    void Start () {
        m_task = ChangeColorTask;
        var path = filePath.Replace (Application.dataPath, "Assets");
        // Load the NodeCanvas
        var canvas = NodeEditorSaveManager.LoadNodeCanvas (path);
        var startNode = FindStartNode(canvas);
        if(startNode==null || startNode.Outputs[0].connections==null){
            return;
        }
        m_topNode= startNode.Outputs[0].connections[0].body as uBehaviourTreeNode;
        CreateNodes(m_topNode);
    }


    void Update(){
        if(m_task!=null){
            m_task();
        }
    }


    /// <summary>
    /// Creates the nodes.
    /// </summary>
    /// <param name="topNode">Top node.</param>
    void CreateNodes(uBehaviourTreeNode topNode){
        m_bTreeBase = CreateBehaviourTreeBase(topNode);
        node = new BehaviourTreeInstance(m_bTreeBase);
        node.finishRP.Where(p=>p!=BehaviourTreeInstance.NodeState.READY).Subscribe(p=>ResetCoroutineStart());
        node.finishRP.Value = BehaviourTreeInstance.NodeState.READY;
        node.Excute();
    }


    /// <summary>
    /// Changes the color task.
    /// </summary>
    void ChangeColorTask(){
        if(runtimeNodeEditor!=null && runtimeNodeEditor.canvas!=null){
            foreach(var one in runtimeNodeEditor.canvas.nodes){
                var bnode = one as uBehaviourTreeNode;
                if(bnode!=null){
                    //Debug.Log("Excute Node = "+node.nowExcuteUuid);
                    if(bnode.Uuid == node.nowExcuteUuid){
                        bnode.setColorFlag = true;
                    }
                    else {
                        bnode.setColorFlag = false;
                    }
                }
            }
        }
    }

    /// <summary>
    /// 開始ノードを検索する。
    /// </summary>
    /// <returns>The start node.</returns>
    /// <param name="canvas">Canvas.</param>
    Node FindStartNode(NodeCanvas canvas){
        return canvas.nodes.Where(p=>p.GetType() == typeof(uStartNode)).FirstOrDefault();
    }

    /// <summary>
    /// BehaviourTreeを構築する。
    /// </summary>
    /// <returns>The behaviour tree base.</returns>
    /// <param name="node">Node.</param>
    BehaviourTreeBase CreateBehaviourTreeBase(uBehaviourTreeNode bnode){
        var type = Type.GetType("BehaviourTrees."+ bnode.nodeType.ToString());
        var ins =  Activator.CreateInstance(type) as BehaviourTreeBase;

        switch (bnode.nodeType) {
        case uBehaviourTreeNode.NodeType.ActionNode:
            {
                var nodeIns = ins as ActionNode;
                nodeIns.Initialize(BTreeUtil.GetAction<ReactiveProperty<bool>>(gameObject, typeof(BTreeActionBase),bnode.MethodName),bnode.Uuid);
            }
            break;
        case uBehaviourTreeNode.NodeType.DecoratorNode:
            {
                var nodeIns = ins as DecoratorNode;
                var actionNode = bnode.Outputs[0].connections[0].body as uBehaviourTreeNode;
                var bbase = CreateBehaviourTreeBase(actionNode);
                var func = BTreeUtil.GetFunc<BehaviourTreeInstance,ExecutionResult>(gameObject, typeof(BTreeDecoratorFuncBase),bnode.MethodName);
                nodeIns.Initialize(func, bbase,bnode.Uuid);
            }
            break;

        case uBehaviourTreeNode.NodeType.SelectorNode:
            {
                var nodeIns = ins as SelectorNode;
                var list = new List<BehaviourTreeBase>();
                foreach(var one in bnode.Outputs){
                    if(one.connections.Count==0) continue;
                    var childnode = CreateBehaviourTreeBase(one.connections[0].body  as uBehaviourTreeNode);
                    list.Add(childnode);
                }
                nodeIns.Initialize(list.ToArray(),bnode.Uuid);
                list.Clear();
            }
            break;
        case uBehaviourTreeNode.NodeType.SequencerNode:
            {
                var nodeIns = ins as SequencerNode;
                var list = new List<BehaviourTreeBase>();
                foreach(var one in bnode.Outputs){
                    if(one.connections.Count==0) continue;
                    var childnode = CreateBehaviourTreeBase(one.connections[0].body as uBehaviourTreeNode);
                    list.Add(childnode);
                }
                nodeIns.Initialize(list.ToArray(),bnode.Uuid);
                list.Clear();
            }
            break;
        }

        return ins;
    }


    /// <summary>
    /// Resets the coroutine start.
    /// </summary>
    void ResetCoroutineStart(){
        StartCoroutine(ResetCoroutine());
    }


    IEnumerator ResetCoroutine(){
        yield return null;
        node.Delete();
        CreateNodes(m_topNode);
    }
}

実行の様子

Unityちゃんが二人いて、双方とも上記のBTreeManagerがアタッチされており、双方同じAIで動いています。
画面内のBehaviourTreeは青いPlayer1のUnityちゃんのAIと対応しています。
白くハイライトしているノードが、今実行されているノードです。

BehaviourTree-compressor.gif

まとめ

「Simple node editor」を使って比較的簡単に自前のAIシステムが構築できました。
今回の例ではBehaviourTreeでしたが、StateMachineもやろうと思えば出来そうです。

51
47
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
51
47