行为树详解(4)——节点参数配置化

【分析】

行为树是否足够灵活强大依赖于足够丰富的各类条件节点和动作节点,在实现这些节点时,不可避免的,节点本身需要有一些参数供配置。

这些参数可以分为静态的固定值的参数以及动态读取设置的参数。

静态参数直接设置为Public即可,动态参数的难点在于可能需要获取不同模块的数据。(这里认为数据是这些模块的某个属性或字段)

节点是通用的,不可能在获取模块A的数据时在Update中调用A方法,在获取模块B的数据时调用B方法,这里需要一个统一的调用方式。

获取属性值的关键在于获取实例类和属性名,在代码上就可以通过实例类.属性名获取到属性值

因此,动态参数的配置内容为类名和字段名,需要通过代码自动生成[实例类.属性名]的调用,这必须要依赖反射。

理论上我们可以通过类A.类B.类C这样生成嵌套的调用。但这样通过程序去实现是成本较高,出现Bug容易导致错乱。

需要通过流程做规范,最多通过某个类就可以获取到值。

例如,在Unity中,脚本挂在GameObject上,可以通过统一的GetComponet获取;有个属性系统,可以通过一个的单例获取等等,这于具体的项目相关。这里我们用GetComponet

为了实现动态的设置和获取值,我们需要将这些动态值做封装,设置Getter和Setter

【行为树数据初始化】

参数配置是整个行为树配置的一部分,在此之前,我们需要补充实现前文缺省的初始化数据部分

我们需要有一个结构来描述行为树的配置数据,这个结构是用于运行时初始化数据的,可以将编辑数据和运行时数据结构分隔开来。

数据显而易见的可以分为三个层次:行为树的数据、节点的数据、参数的数据

比较难解决的是最下层的参数数据,这在很多配置数据中是通用的,其主要问题是参数类型、数量、值等各不相同,如何做一个统一的描述?

方式一是采用Json描述不同节点的参数,每个参数各自解析,便于任意扩展,可以用Unity提供的JsonUtility.ToJson和JsonUtility.FromJson做编码和解析。更进一步的,可以看到这和网络协议的编码和解码类似,在追求性能和效率时可以考虑用ProtoBuffer

方式二是使用固定的结构来存储所有类型数据,每个参数各自解析。

行为树节点基本参数是可以做明确的,这里采用方式二

【生命周期】

行为树需要初始化读取配置数据,节点也需要有初始化

行为树在Update中执行,轮询所有节点,节点需要有Update方法。行为树本身也不一定需要每帧都执行,可以根据实际情况定期执行等

大多数节点只在Update中执行即可,有些节点自身包含参数数据,需要重置。

有些特殊的节点在首次执行时,有特殊的逻辑处理,需要有特殊的Start方法

行为树销毁时,节点数据也需要销毁

【代码实现】

配置数据结构

cs 复制代码
    #region BTConfigData
    public class BehaviourTreeData:ScriptableObject
    {
        public string btName;
        public UpdateType updateType;
        public float updateTime;
        public List<NodeInfo> nodes = new List<NodeInfo>();
    }

    [Serializable]
    public class NodeInfo
    {
        public int nodeId;
        public string nodeName;
        public string nodeType;
        public List<NodeStaticParams> staticParams = new List<NodeStaticParams>();
        public List<NodeDynamicParams> dynamicParams = new List<NodeDynamicParams>();
        public List<int> subNodes = new List<int>();
    }

    [Serializable]
    public class NodeStaticParams
    {
        public string paramType;
        public string paramName;
        public List<int> intParams;
        public List<string> strParams;

        //节点类型是有限的,直接对每个类型做编码解码
        public void Encode(string name,bool value)
        {
            paramType = "bool";
            paramName = name;
            intParams = new List<int>();
            intParams.Add(value ? 1 : 0);
        }

        public void Encode(string name,int value)
        {
            paramType = "int";
            paramName = name;
            intParams = new List<int>();
            intParams.Add(value);
        }

        public void Encode(string name, float value)
        {
            paramType = "float";
            paramName = name;
            intParams = new List<int>();
            intParams.Add((int)value*1000);
        }

        public void Encode(string name, string value)
        {
            paramType = "string";
            paramName = name;
            strParams = new List<string>();
            strParams.Add(value);
        }

        public void Encode(string name, Comparison value)
        {
            paramType = "comparison";
            paramName = name;
            intParams = new List<int>();
            intParams.Add((int)value);
        }

        public void Encode(string name, List<int> value)
        {
            paramType = "listint";
            paramName = name;
            intParams = new List<int>();
            intParams.AddRange(value);
        }

        public void Encode(string name, List<float> value)
        {
            paramType = "listfloat";
            paramName = name;
            intParams = value.Select(x=>(int)(x*1000)).ToList();
        }

        public void Encode(string name, List<string> value)
        {
            paramType = "liststring";
            paramName = name;
            strParams = new List<string>();
            strParams.AddRange(value);
        }

        public object Decode()
        {
            object value = null;
            switch (paramType)
            {
                case "bool": value = intParams[0] > 1;break;
                case "float": value = ((float)intParams[0]) / 1000; break;
                case "string": value = strParams[0]; break;
                case "int": value = intParams[0]; break;
                case "listint": value = new List<int>(intParams); break;
                case "listfloat":value = intParams.Select(x=>((float)x)/1000).ToList(); break;
                case "liststring":value = new List<string>(strParams); break;
                case "comparison":value = (Comparison)intParams[0];break;
                default:
                    break;
            }
            return value;
        }
    }

    [Serializable]
    public class NodeDynamicParams
    {
        public string paramType;
        public string paramName;

        public string classType;
        public string fieldName;
        public bool isProperty;//最好字段都是属性
    }

    public class DynamicParams
    {
        public string paramName;

        public virtual void Init(BehaviorTree bt, NodeDynamicParams param)
        {

        }
    }
    public class DynamicParams<T>: DynamicParams
    {
        protected Func<T> getter;

        protected Action<T> setter;

        public T Value {
            get 
            {
                if(getter != null)
                {
                    return getter();
                }
                return default(T);
            }
            set 
            {
                if(setter != null)
                {
                    setter.Invoke(value);
                }
            }
        }

        public override void Init(BehaviorTree bt, NodeDynamicParams param)
        {
            var classType = Type.GetType(param.classType);
            var classIntance = bt.owner.GetComponent(classType);
            
            if(param.isProperty)
            {
                var property = classIntance.GetType().GetProperty(param.fieldName);

                var getMethod = property.GetGetMethod(true);
                var getter = (Func<T>)Delegate.CreateDelegate(typeof(Func<T>), classIntance, getMethod);
                this.getter = getter;

                var setMethod = property.GetSetMethod(true);
                var setter = (Action<T>)Delegate.CreateDelegate(typeof(Action<T>), classIntance, setMethod);
                this.setter = setter;
            }
            else
            {
                var fieldInfo = classIntance.GetType().GetField(param.fieldName);
                Func<T> getter = () =>
                {
                    return (T)fieldInfo.GetValue(classIntance);
                };
                this.getter = getter;

                Action<T> setter = (value) =>
                {
                    fieldInfo.SetValue(classIntance, value);
                };
                this.setter = setter;
            }
        }

    }

    #endregion

行为树

cs 复制代码
    public class BehaviorTree
    {
        public string btName;
        public int btId;
        public UpdateType updateType;
        public float updateTime;
        private float curTime;
        public GameObject owner;
        private Node rootNode;

        private List<Node> allNodes = new List<Node>();
        private Dictionary<int,Node> id2Node = new Dictionary<int,Node>();
        private Dictionary<int,NodeInfo> id2NodeInfo = new Dictionary<int,NodeInfo>();


        public static Func<string, BehaviourTreeData> loadFunc = null;
        public void Init(GameObject owner, string path, Func<string, BehaviourTreeData> loadFunc)
        {
            this.owner = owner;
            //这里省略部分边界情况检查的逻辑,
            if (!string.IsNullOrEmpty(path))
            {
                var data = loadFunc?.Invoke(path);
                btId = owner.GetInstanceID();
                updateType = data.updateType; 
                updateTime = data.updateTime;
                //通常初始化有两种写法,一种是在管理者中完成管理者自身以及被管理者的数据初始化;另一种是管理者完成自身的初始化后将被数据传递给被管理者,被管理者自身完成初始化
                //第二种方式更加灵活可变,且不需要关注行为树结构是怎么样的,只需每个节点做好自身的初始化
                //在第二种方式下涉及如何将数据递归传递的问题,为统一获取,将数据记录在管理者上,被管理者根据Id从管理者中获取,初始化后可选择将数据释放
                foreach (var item in data.nodes)
                {
                    id2NodeInfo[item.nodeId] = item;
                }
                var rootData = data.nodes[0];//默认第一个是rootNode数据
                rootNode = new RootNode();
                rootNode.Init(rootData, this);
                id2NodeInfo.Clear();//也可以保留
            }
            
        }

        public void Update(float time)
        {
            if(updateType == UpdateType.EveryFrame)
            {
                Update();
            }
            else if(updateType == UpdateType.FixedTime)
            {
                curTime += time;
                if(curTime>updateTime)
                {
                    curTime = 0;
                    Update();
                }
            }
        }

        public NodeStatus Update()
        {
            
            var status = rootNode.Update();
            if(status != NodeStatus.Running)
            {
                rootNode.End();
            }
            return status;
        }

        public void Destroy()
        {
            foreach (var item in allNodes)
            {
                item.Destroy();
            }
            allNodes.Clear();
            id2Node.Clear();
            id2NodeInfo.Clear();
        }

        public NodeInfo GetNodeInfo(int nodeId)
        {
            return id2NodeInfo[nodeId];
        }

        public void AddNode(Node node)
        {
            allNodes.Add(node);
            id2Node[node.nodeId] = node;
        }
    }

部分节点

cs 复制代码
    public class RootNode:Node 
    {
        public Node subNode;

        protected override void OnInit(NodeInfo nodeInfo)
        {
            if(nodeInfo.subNodes.Count > 0)
            {
                var subNodeInfo = owner.GetNodeInfo(nodeInfo.subNodes[0]);
                Type type = Type.GetType(subNodeInfo.nodeType);//根据完整类名,例如反射创建类及其实例
                subNode = (Node)Activator.CreateInstance(type);
                subNode.Init(subNodeInfo, owner);
            }
        }

        protected override NodeStatus OnUpdate()
        {
            return subNode.Update();
        }
    }


 public class ControlNode:Node 
 { 
     public List<Node> subNodes;

     public int curSubIndex;

     protected override void OnInit(NodeInfo nodeInfo)
     {
         foreach (var item in nodeInfo.subNodes)
         {
             var subNodeInfo = owner.GetNodeInfo(item);
             Type type = Type.GetType(subNodeInfo.nodeType);//根据完整类名,例如反射创建类及其实例
             var node = (Node)Activator.CreateInstance(type);
             subNodes.Add(node);
             node.Init(subNodeInfo, owner);
         }
         foreach (var item in nodeInfo.staticParams)
         {
             var field = this.GetType().GetField(item.paramName);
             //这里类型有限,直接用对每个类型做判断实现类型转换
             field.SetValue(this, item.Decode());
         }
         foreach (var item in nodeInfo.dynamicParams)
         {
             var paramtype = Type.GetType(item.paramType);
             var paramInstance = (DynamicParams)Activator.CreateInstance(paramtype);
             paramInstance.Init(owner, item);
             var fieldInfo = GetType().GetField(item.paramName);
             fieldInfo.SetValue(this, paramInstance);
         }
     }

    public class BoolCompareSelectorNode:ControlNode
    {
        public bool targetValue;
        public DynamicParams<bool> curValue;

        public bool targetValues { get; set; }

        public bool GetBool(bool a)
        {
            return a;
        }

        protected override NodeStatus OnUpdate()
        {
            curSubIndex = targetValue == curValue.Value ? 0 : 1;
            var status = subNodes[curSubIndex].Update();
            return status;
        }
    }

    public class IntCompareSelectorNode : ControlNode
    {
        public int targetValue;
        public Comparison comparison;
        public DynamicParams<int> curValue;
        protected override NodeStatus OnUpdate()
        {
            bool res = true;
            switch(comparison)
            {
                case Comparison.SmallerThan: res = curValue.Value<targetValue; break;
                case Comparison.GreaterThan: res = curValue.Value >targetValue; break;
                case Comparison.Equal: res = curValue.Value == targetValue;break;
                case Comparison.SmallerThanOrEqual: res = curValue.Value <= targetValue; break;
                case Comparison.GreaterThanOrEqual: res = curValue.Value >= targetValue; break;
            }
            curSubIndex = res?0:1;
            var status = subNodes[curSubIndex].Update();
            return status;
        }
    }

    public class FloatCompareSelectorNode : ControlNode
    {
        public float targetValue;
        public Comparison comparison;
        public DynamicParams<float> curValue;
        protected override NodeStatus OnUpdate()
        {
            bool res = true;
            switch (comparison)
            {
                case Comparison.SmallerThan: res = curValue.Value < targetValue; break;
                case Comparison.GreaterThan: res = curValue.Value > targetValue; break;
                case Comparison.Equal: res = curValue.Value == targetValue; break;
                case Comparison.SmallerThanOrEqual: res = curValue.Value <= targetValue; break;
                case Comparison.GreaterThanOrEqual: res = curValue.Value >= targetValue; break;
            }
            curSubIndex = res ? 0 : 1;
            var status = subNodes[curSubIndex].Update();
            return status;
        }
    }

    public class IntRangeSelectorNode:ControlNode
    {
        public List<int> intRange;
        public DynamicParams<int> curValue;

        protected override NodeStatus OnUpdate()
        {
            for (int i = 0;i<intRange.Count;i++)
            {
                if (curValue.Value < intRange[i])
                {
                    curSubIndex = i;
                    break;
                }
            }
            var status = subNodes[curSubIndex].Update();
            return status;
        }
    }

    public class FloatRangeSelectorNode : ControlNode
    {
        public List<float> intRange;
        public DynamicParams<float> curValue;

        protected override NodeStatus OnUpdate()
        {
            for (int i = 0; i < intRange.Count; i++)
            {
                if (curValue.Value < intRange[i])
                {
                    curSubIndex = i;
                    break;
                }
            }
            var status = subNodes[curSubIndex].Update();
            return status;
        }
    }


    public class ActionNode:Node
    {
        private bool start;
        protected override NodeStatus OnUpdate()
        {
            if(!start)
            {
                Start();
                start = true;                    
            }
            return base.OnUpdate();
        }

        private void Start()
        {
            OnStart();
        }

        protected virtual void OnStart()
        {

        }

        protected override void OnEnd()
        {
            start = false;
        }
    }
相关推荐
Flamesky23 天前
MMORPG技能管线设计经验总结
行为树·可视化·rpg·skill·mmo·战斗系统·技能编辑器·技能管线·mmorpg·arpg
蔗理苦5 个月前
2024-07-22 Unity AI行为树1 —— 框架介绍
unity·c#·游戏引擎·行为树·游戏ai
大风吹~~~~~10 个月前
行为树入门:BehaviorTree.CPP Groot2练习(叶子节点)(2)
行为树
Sarapines Programmer10 个月前
【Oracle】玩转Oracle数据库(二):体系结构、存储结构与各类参数
数据库·oracle·性能优化·体系结构·存储结构·oracle数据库·参数配置
寸_铁10 个月前
【Go-Zero】Error: only one service expected goctl一键转换生成rpc服务错误解决方案
后端·rpc·golang·goland·goctl·参数配置
薛动静1 年前
行为树(Behavior Trees)
行为树
姚家湾1 年前
行为树(BEHAVIOR TREES)及其工业应用
行为树