Unity中状态机与行为树的简单实现

前言

状态机与行为树是两大热门AI实现方式,这里简单贴出我的实现,仅作为参考,简单讲解原理,多的就不做解释,其他的AI实现方式可以使用GOAP或ML_Agent

状态机

状态机由三部分组成:状态、转换、条件。

状态表示对象当前的行为模式,如待机、追击、攻击。每个状态有进入、持续、退出三个阶段,用于处理动画切换、物理参数变更等副作用。

转换定义状态间的迁移路径。不同于硬编码的if-else,转换与状态解耦,形成有向图结构。运行期从当前状态出发,遍历所有出边,检查条件决定是否迁移。

条件是转换的守卫,返回布尔值。将条件抽象为独立对象,可实现复用(如"距离小于X"被多个转换共用)与组合(与、或、非)。

数据驱动体现在:状态机资产(SO)描述图结构,运行实例持有当前状态与上下文数据。同一资产可被多个敌人共享,各自独立推进。

复制代码
//状态
public abstract class BaseState : ScriptableObject 
{
    public StateType Type;
    public virtual void Enter(BaseStateValue value){}
    public virtual void Exit(BaseStateValue value){}
    public virtual void OnUpdate(BaseStateValue value) {}
} 
//条件
public abstract class BaseCondition :ScriptableObject 
 {
     public abstract bool CanTranslate(BaseStateValue value);
 } 
 //状态机
 public abstract class BaseStateMachine : MonoBehaviour 
 {
     [SerializeField]
     protected BaseStateMachineAsset _stateMachineAsset;
     protected BaseState _currentState;
     public BaseStateValue _currentStateValue;
     protected virtual void Start()
     {
         CreateStateValue();
         Initialize();
     }
     protected void Update()
     {
         _currentState.OnUpdate(_currentStateValue);
         var list = _stateMachineAsset.GetTranslations(_currentState.Type);
         if (list == null) return;
         
         foreach (var t in list)
         {
             if(t.Condition.CanTranslate(_currentStateValue))
             {
                 _currentState.Exit(_currentStateValue);
                 _currentState = t.To;
                 _currentState.Enter(_currentStateValue);
                 break;
             }
         }
     }
     protected void FixedUpdate()
     {
         if(_currentState is IPhysicsState physics)
         {
             physics.OnFixedUpdate(_currentStateValue);
         }
     }
     protected abstract void CreateStateValue();
     protected void Initialize()
     {
         if (_stateMachineAsset == null)
         {
             Debug.LogError("资源为空");
         }
         if(_stateMachineAsset.InitialState==null)
         {
             Debug.LogError("初始状态为空");
         }
         else
         {
             _currentState = _stateMachineAsset.InitialState;
             _currentState.Enter(_currentStateValue);
         }
     }
     public BaseStateValue GetStateValue()=> _currentStateValue;
 }
 //状态机配置
 public class BaseStateMachineAsset : ScriptableObject 
 {
     public List<BaseTranslation> Translations;
     public BaseState InitialState;
     private Dictionary<StateType, List<BaseTranslation>> _map;
     public void OnValidate()
     {
         if(Translations != null&&Translations.Count>0)
         {
             BuildMap();
         }
     }
     public IReadOnlyList<BaseTranslation> GetTranslations(StateType type)
     {
         if(_map == null)
         {
             return null;
         }
         if(_map.TryGetValue(type,out var res))
         {
             return res.AsReadOnly();
         }
         return null;
     }
     private void BuildMap()
     {
         _map = new Dictionary<StateType, List<BaseTranslation>>();

         foreach (var translation in Translations)
         {
             if(!_map.ContainsKey(translation.From.Type))
             {
                 _map[translation.From.Type] = new List<BaseTranslation>();
             }
             _map[translation.From.Type].Add(translation);
         }
         foreach (var translations in _map.Values)
         {
             translations.Sort((a,b)=>b.Weight.CompareTo(a.Weight));
         }
     }
 }
 //上下文(数据容器)
public class BaseContext
{
    public GameObject Self;
    public bool IsHurt;
    public bool IsDead;
    public BaseContext(GameObject gameObject)
    {
        Self = gameObject;
        IsHurt = false;
        IsDead = false;
    }
}
//转换
 public abstract  class BaseTranslation : ScriptableObject 
 {
     public BaseState From;
     public BaseState To;
     public BaseCondition Condition;
     public int Weight;      //权重
 }

行为树

行为树采用树形结构组织行为,节点按功能分为三类:

组合节点控制子节点执行方式。Sequence顺序执行,子节点失败则中断;Selector选择执行,子节点成功则中断。这种隐式优先级避免了状态机中显式转换条件的膨胀。

修饰节点包装单个子节点,修改其返回值或执行逻辑。如Invert反转结果,Repeat循环执行,Cooldown限制频率。修饰器可嵌套,实现复杂控制流而不新增节点类型。

叶节点执行具体逻辑,包括条件判断(Condition)与动作执行(Action)。条件节点不改变世界状态,仅返回评估结果;动作节点可能持续多帧,返回Running表示未完成。

行为树每帧从根节点开始深度遍历,根据节点返回值决定遍历路径。这种"重新评估"机制天然支持反应式行为:高优先级条件(如受击)插入树前端,立即打断当前行为。

由于行为树的优势是可视化,而可视化需要用到GraphView编辑窗口,这里只贴出基础实现,项目中使用需要借助插件或者升级到Unity6

复制代码
 //返回节点状态
 public enum NodeState
 {
     Success,
     Failure,
     Running
 }
 //基础节点
 public abstract class BTNode
{
    public string Name;
    public abstract NodeState Execute();
}
//叶子节点
public abstract class ActionNode : BTNode
{
    protected Func<NodeState> _action;
    protected ActionNode(string name,Func<NodeState> act)
    {
        Name = name;
        _action = act;
    }
    public override NodeState Execute()
    {
        return _action();
    }
    
    public abstract class CompositeNode : BTNode
{
    protected List<BTNode> _children=new List<BTNode>();
    protected int currentIndex = 0;
    public void AddChild(BTNode child)
    {
        _children.Add(child);
    }
}
}
public class Selector : CompositeNode
{
    public override NodeState Execute()
    {
        for (int i = 0; i < _children.Count; i++)
        {
            var state = _children[i].Execute();
            if(state==NodeState.Success
                ||state==NodeState.Running)
            {
                return state;
            }
        }
        return NodeState.Failure;
    }
}
 public class Sequence : CompositeNode
 {
     public override NodeState Execute()
     {
         for (int i = 0; i < _children.Count; i++)
         {
             var state = _children[i].Execute();
             if(state==NodeState.Failure||
                 state==NodeState.Running)
             {
                 return state;
             }
         }
         return NodeState.Success;
     }
 }
 // 修饰器基类
public abstract class DecoratorNode : BTNode
{
    protected BTNode _child;
    
    public void SetChild(BTNode child) => _child = child;
}

// 反转结果
public class Inverter : DecoratorNode
{
    public override NodeState Execute()
    {
        var state = _child.Execute();
        return state switch
        {
            NodeState.Success => NodeState.Failure,
            NodeState.Failure => NodeState.Success,
            _ => NodeState.Running
        };
    }
}

// 重复执行N次
public class Repeater : DecoratorNode
{
    private int _repeatCount;
    private int _currentCount;
    
    public Repeater(int count)
    {
        _repeatCount = count;
        _currentCount = 0;
    }
    
    public override NodeState Execute()
    {
        while (_currentCount < _repeatCount)
        {
            var state = _child.Execute();
            if (state == NodeState.Running) return NodeState.Running;
            _currentCount++;
        }
        _currentCount = 0;
        return NodeState.Success;
    }
}

// 冷却时间
public class Cooldown : DecoratorNode
{
    private float _duration;
    private float _lastExecuteTime = float.MinValue;
    
    public Cooldown(float duration) => _duration = duration;
    
    public override NodeState Execute()
    {
        if (Time.time - _lastExecuteTime < _duration)
            return NodeState.Failure;
        
        var state = _child.Execute();
        if (state != NodeState.Running)
            _lastExecuteTime = Time.time;
        
        return state;
    }
}

// 行为树运行器
public class BehaviorTreeRunner : MonoBehaviour
{
    [SerializeField] private BTNode _root;
    
    void Update() => _root?.Execute();
    
    public void SetRoot(BTNode root) => _root = root;
}

结语

有什么不明白的欢迎在评论区讨论,年关将近,本文有些仓促

相关推荐
云上空13 小时前
腾讯云使用对象存储托管并分享WebGL小游戏(unity3d)(需要域名)
unity·腾讯云·webgl·游戏开发·对象存储·网页托管
小贺儿开发15 小时前
Unity3D VR党史主题展馆
unity·人机交互·vr·urp·展馆·党史
TopGames15 小时前
Unity实现10万人同屏动态避障和导航寻路系统 支持3D地形
unity·性能优化·游戏引擎
在路上看风景18 小时前
01. GUIContent
unity
TrudgeCarrot1 天前
unity打包使用SPB管线出现 DontSava错误解决
unity·游戏引擎·dontsave
3D霸霸1 天前
unity 创建URP新场景
unity·游戏引擎
玉梅小洋2 天前
Unity 2D游戏开发 Ruby‘s Adventure 1:课程介绍和资源导入
游戏·unity·游戏引擎·游戏程序·ruby
托洛夫斯基扎沙耶2 天前
Unity可视化工具链基础
unity·编辑器·游戏引擎
浅陌sss2 天前
检查Unity对象要始终使用 != null而不是?.
unity