Help us understand the problem. What is going on with this article?

Unityでノードエディタを使ってBehaviourTreeを作る

追記2020-03-20

UnityのExperimental機能ですが、GraphViewを検討するのはどうでしょうか。

はじめに

UnityのGUIツールでBehaviourTreeを作成したいので、ノードエディタxNodeを試してみます。

今回のコードを下記に置きました。
https://github.com/jnhtt/xNode-BehaviourTree/

修正

2018/11/6

  • BtSequencerを修正しました。
    • BtSeauencer.Initの子ノードの取得方法が間違っていました。
  • BtDebugを変更しました。
    • 戻り値をSuccessに変更しました。

環境

  • MacBook Pro(15-inch, 2017)
  • Unity 2018.2.2f1
  • xNode(xNode_1.5.unitypackage)

ノードエディタの選択

MITライセンスで実装がシンプルなxNodeを選びました。
今回は使わなかったですが、Node_Editor_Frameworkというものもあります。

お金に余裕がある人はUnity AssetstoreでArborやBoltも選択肢になると思います。

BehaviourTreeについて

このあたりの説明をみて実装しました。
解説は下記に任せます。

xNodeでツリーを作る準備

名前空間は、AI.BtGraphとします。
下記は、作成する基本的なノードです。

ノード 説明 使い方
BtStart BehaviourTreeの開始ノード グラフに1つ
BtAction アクションノードのベースクラス 継承して具体的な処理を記述
BtDecorator デコレーションノードのベースクラス 継承して具体的な条件を記述
BtSelector 条件チェックして子ノードを1つだけ実行。優先度順にチェック 直後にDecoratorノードを付けて分岐
BtSequencer 順番に子ノードを実行。途中で失敗したら終わる

xNodeを取得

xNodeからcloneするか、releaseにあるパッケージをインポートします。

NodeGraph作成

Create > xNode > NodeGraph C# Scriptを選びます。
C#ファイル名をBtGraphとします。

NodeGraphの全ノードからBtStartノードを探して、見つかったら実行します。
BtStartノードは、後で説明します。

BtGraph.cs
using UnityEngine;
using XNode;

namespace AI.BtGraph
{
    // fileName : 新規のBehaviourTreeのScriptableObjectのファイル名
    // menuName : メニューAssets/Create/AI/BtGraphを選択したらNewBTGraphを作成する
    [CreateAssetMenu(fileName = "NewBTGraph", menuName = "AI/BtGraph")]
    public class BtGraph : NodeGraph
    {
        // BtStartノードを取得.
        // BtStartノードは必ずあるものとする.
        public BtStart GetStartNode()
        {
            BtStart startNode = null;
            foreach (var node in nodes) {
                startNode = node as BtStart;
                if (startNode != null) {
                    return startNode;
                }
            }
            return null;
        }

        // DataにBtGraph内のノードでアクセスしたいデータを入れておく想定.
        public void Exec(Data data)
        {
            var startNode = GetStartNode();
            if (startNode != null) {
                startNode.Exec(data);
            }
        }
    }
}

Node作成

ノードエディタでノードをつなぐためのAttributeをノードに付けます。
すると、ノードエディタでノードを接続できるようになります。

//前への接続.
[Input] public BtKnobEmpty inputKnob;
//次への接続.
[Output] public BtKnobEmpty outputKnob;

ファイルの配置をBaseNodesとNodesに分けました。
GUIから作成可能なノードをNodesに入れ、GUIから作らせないノードをBaseNodesに入れました。
Action/Decoratorノードは継承して使うのでBaseNodesに入れます。

スクリーンショット 2018-11-02 0.42.08.png

BtNode

BehaviourTreeのベースノード。

BtNode.cs
using System;
using XNode;

namespace AI.BtGraph.Base
{
    public class BtNode : Node
    {
        public int Priority;

        public BtNodeType NodeType { get; protected set; }

        public BtNode(BtNodeType nodeType)
        {
            NodeType = nodeType;
        }

        public virtual BtResult Exec(Data data)
        {
            return BtResult.Success;
        }

        public virtual BtNode GetNext()
        {
            var port = GetOutputPort("outputKnob");
            BtNode next = null;
            if (port != null && port.IsConnected) {
                next = port.Connection.node as BtNode;
            }
            return next;
        }

        public virtual void Setup()
        { }
    }
}

BtStartノード

BtGraphの開始ノードです。
前への接続を持たず、次への接続だけを持ちます。
BtStartノードがないとBtGraphは実行しません。

BtStart.cs
namespace AI.BtGraph
{
    public class BtStart : Base.BtNode
    {
        //次への接続.
        [Output(ShowBackingValue.Never, ConnectionType.Override)] public BtKnobEmpty outputKnob;

        public BtStart() : base(BtNodeType.Start)
        {}

        public override BtResult Exec(Data data)
        {
            var next = GetNext();
            if (next != null) {
                return next.Exec(data);
            }
            return BtResult.Failure;
        }
    }
}

BtActionノード

行動を行うノードです。
木構造の葉なので、次への接続を持ちません。
具体的な処理は継承先で実装します。

BtAction.cs
using UnityEngine;

namespace AI.BtGraph.Base
{
    public class BtAction : BtNode
    {
        //前への接続.
        [Input(ShowBackingValue.Never, ConnectionType.Override)] public BtKnobEmpty inputKnob;

        public BtAction() : base(BtNodeType.Action)
        {}
    }
}

BtApproach

moverがgoalに向かって移動するアクションノードです。

BtApproach.cs
using UnityEngine;
using AI.BtGraph.Base;

namespace AI.BtGraph
{
    public class BtApproach : BtAction
    {
        public override BtResult Exec(Data data)
        {
            Vector3 dir = data.goal.CachedTransform.position - data.mover.CachedTransform.position;
            data.mover.CachedTransform.position += dir.normalized * 4f * Time.deltaTime;
            return BtResult.Success;
        }
    }
}

BtAround

moverがgoalを中心に回転するアクションノードです。
右左の回転を設定できます。

BtAround.cs
using UnityEngine;
using AI.BtGraph.Base;

namespace AI.BtGraph
{
    public class BtAround : BtAction
    {
        public enum Rot
        {
            Right = 0,
            Left,
        }
        public Rot rot;

        public override BtResult Exec(Data data)
        {
            float angle = rot == Rot.Right ? 30f : -30f;
            data.mover.CachedTransform.RotateAround(data.goal.CachedTransform.position, Vector3.up, angle * Time.deltaTime);
            return BtResult.Success;
        }
    }
}

BtDecoratorノード

条件判定して正なら次のノードへ、偽なら前のノードに遷移します。
具体的な条件は継承先で実装します。

BtDecorator.cs
using UnityEngine;

namespace AI.BtGraph.Base
{
    public class BtDecorator : BtNode
    {
        //前への接続.
        [Input] public BtKnobEmpty inputKnob;
        //次への接続.
        [Output] public BtKnobEmpty outputKnob;

        public BtDecorator() : base(BtNodeType.Decorator)
        { }

        public override BtResult Exec(Data data)
        {
            if (Branch(data)) {
                Base.BtNode btNode = GetNext();
                if (btNode != null) {
                    return btNode.Exec(data);
                }
                return BtResult.Success;
            }
            return BtResult.Failure;
        }

        public virtual bool Branch(Data data)
        {
            return true;
        }
    }
}

BtCheckDistance

moverとgoalの距離とdistanceに設定した値をconditionで比較するノード。

例:moverとgoalの距離が5より大きい時は正

Condition Greater
distance 5
BtCheckDistance.cs
using UnityEngine;
using AI.BtGraph.Base;

namespace AI.BtGraph
{
    public class BtCheckDistance : BtDecorator
    {
        public enum Condition {
            Equal = 0,
            Less,
            LessEqual,
            Greater,
            GreaterEqual,
        }

        public Condition condition;
        public float distance;

        public override bool Branch(Data data)
        {
            float dist = Vector3.Distance(data.goal.CachedTransform.position, data.mover.CachedTransform.position);
            switch (condition) {
                case Condition.Equal:
                    return Mathf.Approximately(dist, distance);
                case Condition.Less:
                    return dist < distance;
                case Condition.LessEqual:
                    return dist <= distance;
                case Condition.Greater:
                    return dist > distance;
                case Condition.GreaterEqual:
                    return dist >= distance;
            }
            return false;
        }
    }
}

BtSelectorノード

条件チェックして子ノードを1つだけ実行します。
優先度順にチェックする。

BtSelector.cs
using System.Collections.Generic;
using UnityEngine;

namespace AI.BtGraph
{
    public class BtSelector : Base.BtNode
    {
        [Input] public BtKnobEmpty inputKnob;
        [Output] public BtKnobEmpty outputKnob;

        protected List<Base.BtNode> btNodeList;

        public BtSelector() : base(BtNodeType.Selector)
        {
            btNodeList = new List<Base.BtNode>();
        }

        protected override void Init()
        {
            base.Init();

            btNodeList.Clear();
            Base.BtNode btNode = null;
            foreach (var port in Outputs) {
                if (port.IsConnected) {
                    int cnt = port.ConnectionCount;
                    for (int i = 0; i < cnt; ++i) {
                        var p = port.GetConnection(i);
                        if (p != null) {
                            btNode = p.node as Base.BtNode;
                            if (btNode != null) {
                                btNodeList.Add(btNode);
                            }
                        }
                    }
                }
            }
            // 降順:priorityの値が大きい順に実行.
            btNodeList.Sort((a, b) => b.Priority - a.Priority);
        }

        public override BtResult Exec(Data data)
        {
            foreach (var node in btNodeList) {
                if (node.Exec(data) == BtResult.Success) {
                    return BtResult.Success;
                }
            }
            return BtResult.Failure;
        }
    }
}

BtSequencerノード

順番に子ノードを実行します。
途中で失敗したら終わります。

BtSequencer.cs
using System.Collections.Generic;
using UnityEngine;

namespace AI.BtGraph
{
    public class BtSequencer : Base.BtNode
    {
        [Input] BtKnobEmpty inputKnob;
        [Output] BtKnobEmpty outpuKnob;

        protected List<Base.BtNode> btNodeList;

        public BtSequencer() : base(BtNodeType.Sequencer)
        {
            btNodeList = new List<Base.BtNode>();
        }

        protected override void Init()
        {
            base.Init();

            btNodeList.Clear();
            Base.BtNode btNode = null;
            foreach (var port in Outputs) {
                if (port.IsConnected) {
                    int cnt = port.ConnectionCount;
                    for (int i = 0; i < cnt; ++i) {
                        var p = port.GetConnection(i);
                        if (p != null) {
                            btNode = p.node as Base.BtNode;
                            if (btNode != null) {
                                btNodeList.Add(btNode);
                            }
                        }
                    }
                }
            }
            // 降順:priorityの値が大きい順に実行.
            btNodeList.Sort((a, b) => b.Priority - a.Priority);
        }

        public override BtResult Exec(Data data)
        {
            foreach (var node in btNodeList) {
                if (node.Exec(data) == BtResult.Failure) {
                    return BtResult.Failure;
                }
            }
            return BtResult.Success;
        }
    }
}

BtDebug

デバッグ用のメッセージを出すノードです。

BtDebug.cs
using UnityEngine;

namespace AI.BtGraph
{
    public class BtDebug : Base.BtNode
    {
        [Input] public BtKnobEmpty inputKnob;
        [Output] public BtKnobEmpty outputKnob;

        public string message;

        public BtDebug() : base(BtNodeType.Debug)
        {}

        public override BtResult Exec(Data data)
        {
            UnityEngine.Debug.Log(message);
            var next = GetNext();
            if (next != null)
            {
                return next.Exec(data);
            }
            return BtResult.Success;
        }
    }
}

ノードエディタでBehaviourTree作成

ScriptableObjectの作成

Projectウィンドウのデータを作成したいフォルダで下記の操作を行います。
右クリック > Create > AI > BtGraph
作成したNewBtGraphをダブルクリックするとウィンドウが表示されます。

ウィンドウで右クリックし、ノードを選択して作成します。
右クリック > AI > Bt Graph > 各ノード

データ作成上の注意

これらを守らないと意図通りに動かせないと思うので注意が必要です。

  • BtStartノードを作成する。
    • BtStartが開始ノードになる。
  • BtSelector/BtSequencerはPriorityを設定して実行順を決める。
    • 値が大きいものから実行する。

テスト

下画像のようなツリーを作成します。
下記のような挙動になります。

  • goalまでの距離が5になるまで近く
  • goalを中心にと時計回りに周る

スクリーンショット 2018-11-02 1.15.34.png

さいごに

xNodeでノードを簡単に作成できました。気になる点は、ファイルサイズです。少し大きい印象を受けました。
Asset SerializationをForce Textの設定にして、ノード10個の.assetファイルのサイズが12KBもあることです。5個追加すると15KBになりました。

BehaviourTreeについては、ノードの組み合わせと値の調整でいろんな挙動を作れそうです。
簡単なサンプルなので、Actionノードに記述する処理の粒度を把握できませんでした。複雑な処理を実装してみないと理解できないかもしれません。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした