第一版本 AI 的目标很简单:查找目标、移动向目标、攻击目标。

1. 行为树

行为树与决策树的主要区别在于:行为树不仅具备决策功能,还承担控制行为(逻辑)的职责。一种简单粗暴的做法是通过决策树找出解决方案,再交由状态机去执行。

纯行为树除了决策之外,还需要执行逻辑——也就是动作节点的执行状态,例如 Running、Complete 等。这些状态的意义在于:帧结束后下一帧可以继续从当前节点执行,而无需从根节点重新遍历。这涉及树节点的状态保存、恢复与跳转,在设计上存在一定难度。状态机本身是有向图结构,较为直观;但改为树形结构后,设计复杂度会大幅提升。因此,另一种做法是用决策树处理复杂逻辑,再通过状态机执行——这样决策树自身就不需要考虑行为之间的跳转和保留问题,适合"行为本身不复杂、但跳转关系很复杂"的场景。

以 FPS 游戏的机器人为例:行为本身很简单,不过是前后左右移动和开枪;复杂的是决策层——何时开枪、何时前进。将简单操作委托给状态或目标函数来完成,可以简化设计,让精力集中在决策层的设计上。

行为树中游戏逻辑的核心集中在叶子节点(条件节点和行为节点),其余部分可固化为框架。

2. 行为树代码实现

代码分为三个部分:

  • BehaviorTree.cs(代码框架)
  • Actions.cs(动作叶子节点)
  • Conditions.cs(条件叶子节点)

3. BehaviorTree.cs 框架部分

框架第一版的实现要点:每次从根节点开始遍历,寻找行为并执行。

/*
                * Author:  caoshanshan
                * Email:   me@dreamyouxi.com
                using Behavior Tree to peocess  AI
                 * 行为树框架
                 */
                using UnityEngine;
                using System.Collections;
                
                namespace BehaviorTree
                {
                    //----------------行为树框架部分
                    /* public enum ActionNodeState
                     {//行为节点状态
                         Running,//运行中,该状态下父节点会直接运行该节点逻辑,
                         Looping,//循环,因为行为节点都是条件导出, 该状态会 重新 执行该层级的 比如用于持续条件评定,
                         Complete,//完成, 父节点可进入下个环节
                         //  Failure,//执行失败,父节点进入下个环节
                         UnKnown,//默认状态,什么都不知道
                     }*/
                    public enum NodeType
                    {
                        Condition,//条件节点
                        Action, // 行为节点
                
                        Selector,//选择节点   从子节点选择一个执行
                        Sequence,//序列节点   从子节点依次执行 一般是条件 和动作的组合
                        Parallel,//并行节点   执行所有节点
                
                        UnKnown,
                    }
                    public class NodeBase : Model // Model for 事件系统 和 生命周期管理协议
                    {// 所有节点 基类
                        public override void OnEnter()
                        {
                
                        }
                        public override void OnExit()
                        {
                
                        }
                        public override void OnEvent(int type, object userData)
                        {
                
                        }
                        public sealed override void UpdateMS() { }
                        public sealed override void Update() { }
                
                        public void AddChild(NodeBase node)
                        {
                            if (IsConflict(node))
                            {
                                return;
                            }
                            node.parent = this;
                            children.Add(node);
                        }
                        public void RemoveChild(NodeBase node)
                        {
                            node.parent = null;
                            children.Remove(node);
                        }
                        public virtual bool Visit(Entity target)
                        {//条件节点不需要 携带参数,
                            return false;
                        }
                        public NodeBase parent = null;//父节点
                        protected ArrayList children = new ArrayList();
                
                        //----------helper function
                        public bool IsConflict(NodeBase other)
                        {// 节点间 是否冲突
                            // 比如 选择节点的子节点中 不应该有条件节点
                            return false;
                        }
                        public bool HasParent()
                        {
                            return parent != null;
                        }
                        public NodeType GetNodeType()
                        {
                            return type;
                        }
                        protected
                        NodeType type = NodeType.UnKnown;
                        protected Entity _host = null;
                    }
                
                    public class ActionBase : NodeBase
                    {//行为节点基类
                        public ActionBase()
                        {
                            this.type = NodeType.Action;
                        }
                    }
                    public class ConditionBase : NodeBase
                    {//条件节点基类
                        public ConditionBase()
                        {
                            this.type = NodeType.Condition;
                        }
                    }
                
                    //-----------------------------------------------------------------控制节点
                    public class ControllBase : NodeBase
                    {
                
                    }
                    public class Selector : ControllBase
                    {//选择节点
                        public Selector()
                        {
                            this.type = NodeType.Selector;
                        }
                        public override bool Visit(Entity target)
                        {//从子节点选择一个 执行
                            foreach (NodeBase node in children)
                            {
                                NodeType child_type = node.GetNodeType();
                                if (child_type == NodeType.Condition)
                                {//子节点是条件节点, 条件评定
                                    return false;// 选择节点中不应该存在 条件节点
                                }
                                else
                                {//选择一个节点即可
                                    if (node.Visit(target))
                                    {
                                        return true;
                                    }
                                }
                            }
                            return false;
                        }
                    }
                    public class Sequence : ControllBase
                    {//序列节点
                        public Sequence()
                        {
                            this.type = NodeType.Sequence;
                        }
                        public override bool Visit(Entity target)
                        {//一个返回false 即 返回false
                            foreach (NodeBase node in children)
                            {
                                if (node.Visit(target) == false)
                                {
                                    return false;
                                }
                            }
                            return true;
                        }
                    }
                    public class Parallel : ControllBase
                    {//并行节点 所有节点都返回true 才返回true
                        public Parallel()
                        {
                            this.type = NodeType.Parallel;
                        }
                        public override bool Visit(Entity target)
                        {//从子节点选择一个 执行 都返回false 才返回false 否则返回true
                            if (children.Count <= 0) return false;
                            bool ret = true;
                            foreach (NodeBase node in children)
                            {
                                if (node.Visit(target) == false)
                                {
                                    ret = false;
                                }
                            }
                            return ret;
                        }
                    }
                }

4. Actions.cs(动作节点部分)

/*
                * Author:  caoshanshan
                * Email:   me@dreamyouxi.com
                  Behavior Tree  's Actions
                 * 行为树游戏逻辑部分
                 */
                using UnityEngine;
                using System.Collections;
                
                namespace BehaviorTree.Action
                {
                    //--------------------------------------------------游戏逻辑实际的  Action
                    public class SearchNearestTarget : ActionBase
                    {//寻找最近的玩家作为目标
                        public override bool Visit(Entity target)
                        {
                            Enemy host = target as Enemy;
                
                            if (host == null) return false;
                
                            float minDis = float.MaxValue;
                            Entity t = null;
                            foreach (Entity h in HeroMgr.ins.GetHeros())
                            {//找出一个最近的玩家 作为锁定目标
                                if (h.IsMaxTarget())
                                {
                                    continue;
                                }
                                float dis = h.ClaculateDistance(host.x, host.y);
                                if (dis < minDis)
                                {
                                    t = h;
                                    minDis = dis;
                                }
                            }
                
                            if (t != null)
                            {
                                host.target = t;
                                return true;
                            }
                
                            return false;
                        }
                    }
                
                
                    public class MoveToTarget : ActionBase
                    {
                        public override bool Visit(Entity target)
                        {
                            Enemy host = target as Enemy;
                            if (host == null) return false;
                            host.dir = (int)Utils.GetAngle(host.pos, host.target.pos);//委托给Run状态去做
                            return true;
                        }
                    }
                    public class AttackTarget : ActionBase
                    {
                        public override bool Visit(Entity target)
                        {
                            target.atk = true;
                            return true;
                        }
                    }
                
                
                }

5. Conditions.cs(条件部分)

/*
                * Author:  caoshanshan
                * Email:   me@dreamyouxi.com
                  Behavior Tree  's Conditions
                 * 行为树游戏逻辑部分
                 */
                using UnityEngine;
                using System.Collections;
                
                namespace BehaviorTree.Condition
                {
                    //--------------------------------------------------游戏逻辑实际的 Condition
                    public class TargetHasNotInAtkRange : ConditionBase
                    {//目标不在否在攻击范围内
                        public override bool Visit(Entity target)
                        {
                            Enemy host = target as Enemy;
                            if (host == null) return false;
                            if (host.target == null) return false;
                            if (host.atk_range > host.target.ClaculateDistance(host))
                            {//范围内
                                return false;
                            }
                            return true;
                        }
                    }
                    public class IsCDMax : ConditionBase
                    {//CD是否结束
                        public override bool Visit(Entity target)
                        {
                            Enemy host = target as Enemy;
                            if (host == null) return false;
                            if (host.target == null) return false;
                            if (host.cd.IsMax())
                            {
                                host.cd.Reset();
                                return true;
                            }
                            return false;
                        }
                    }
                    public class NotTargetOrDie : ConditionBase
                    {//没有目标或者死亡
                        public override bool Visit(Entity target)
                        {
                            Enemy host = target as Enemy;
                            if (host == null) return true;
                            if (host.target == null) return true;
                            if (host.target.isDie) return true;
                            return false;
                        }
                    }
                }

6. 接入 Enemy.cs

暂时手动输入树结构:

 private void InitBehaviorTree()
                    {
                        bt_root = new BehaviorTree.Parallel();
                
                        {
                            var bt_target = new BehaviorTree.Sequence();
                            bt_target.AddChild(new BehaviorTree.Condition.NotTargetOrDie());
                            bt_target.AddChild(new BehaviorTree.Action.SearchNearestTarget());
                            bt_root.AddChild(bt_target);
                        }
                        {
                            var bt_selector = new BehaviorTree.Selector();
                            var bt_sequence1 = new BehaviorTree.Sequence();
                            var bt_sequence2 = new BehaviorTree.Sequence();
                            bt_selector.AddChild(bt_sequence1);
                            bt_selector.AddChild(bt_sequence2);
                
                            bt_sequence1.AddChild(new BehaviorTree.Condition.TargetHasNotInAtkRange());
                            bt_sequence1.AddChild(new BehaviorTree.Action.MoveToTarget());
                
                            bt_sequence2.AddChild(new BehaviorTree.Condition.IsCDMax());
                            bt_sequence2.AddChild(new BehaviorTree.Action.AttackTarget());
                            bt_root.AddChild(bt_selector);
                        }
                
                    }
                    public virtual void AI_UpdateMSWithAI()
                    {
                        bt_root.Visit(this);
                    }

接入时采用简单粗暴的方式:每次从根节点开始遍历。

下篇将对 AI 本身的复杂度进行丰富。

源代码:https://git.oschina.net/dreamyouxi/XYGame