Unity游戏开发实战:有限状态机FSM实现NPC 4种行为状态切换
在游戏开发中,NPC(非玩家角色)的行为逻辑往往需要处理多种状态之间的切换。想象一下,你正在开发一款开放世界RPG游戏,游戏中的守卫NPC需要在巡逻、追击入侵者、攻击和逃跑四种行为之间智能切换。如果直接用if-else嵌套来实现,代码很快就会变得难以维护。这时候,有限状态机(FSM)就派上用场了。
1. 有限状态机基础概念
有限状态机(Finite State Machine,简称FSM)是一种数学模型,用于表示对象在其生命周期内可能处于的有限状态集合,以及在这些状态之间转换的规则。在游戏开发中,FSM特别适合用来管理角色行为。
FSM的核心要素:
- 状态(State):对象所处的特定行为模式,如"巡逻"、"追击"
- 转移(Transition):从一个状态切换到另一个状态的条件
- 动作(Action):进入、执行或退出状态时执行的操作
// FSM基本结构伪代码 public class FSM { private Dictionary<StateEnum, StateBase> states; private StateBase currentState; public void TransitionTo(StateEnum newState) { currentState?.OnExit(); currentState = states[newState]; currentState.OnEnter(); } public void Update() { currentState?.OnExecute(); } }2. Unity中实现FSM的架构设计
在Unity中实现FSM,我们需要设计一个清晰的架构。以下是推荐的类结构:
类结构设计:
StateMachine:状态机管理器StateBase:所有状态的基类- 具体状态类(如
PatrolState、ChaseState等)
// 状态枚举定义 public enum NPCState { Patrol, Chase, Attack, Flee } // 状态基类 public abstract class NPCStateBase { protected NPCController controller; protected NPCState stateType; public NPCStateBase(NPCController controller) { this.controller = controller; } public abstract void OnEnter(); public abstract void OnUpdate(); public abstract void OnExit(); public NPCState StateType => stateType; }3. NPC四种行为状态实现
3.1 巡逻状态实现
巡逻是NPC的基础状态,通常表现为在特定路径或区域内移动。
public class PatrolState : NPCStateBase { private Transform[] waypoints; private int currentWaypointIndex = 0; private float waitTimer = 0f; public PatrolState(NPCController controller) : base(controller) { stateType = NPCState.Patrol; waypoints = controller.PatrolWaypoints; } public override void OnEnter() { controller.NavAgent.speed = controller.PatrolSpeed; MoveToNextWaypoint(); } public override void OnUpdate() { if (controller.NavAgent.remainingDistance <= controller.NavAgent.stoppingDistance) { waitTimer += Time.deltaTime; if (waitTimer >= controller.WaypointWaitTime) { waitTimer = 0f; MoveToNextWaypoint(); } } // 检查是否发现玩家 if (controller.CanSeePlayer()) { controller.TransitionToState(NPCState.Chase); } } private void MoveToNextWaypoint() { if (waypoints.Length == 0) return; currentWaypointIndex = (currentWaypointIndex + 1) % waypoints.Length; controller.NavAgent.SetDestination(waypoints[currentWaypointIndex].position); } public override void OnExit() { // 清理工作 } }3.2 追击状态实现
当NPC发现玩家时,从巡逻状态切换到追击状态。
public class ChaseState : NPCStateBase { private Transform playerTransform; private float lastSeenTime; public ChaseState(NPCController controller) : base(controller) { stateType = NPCState.Chase; playerTransform = GameObject.FindGameObjectWithTag("Player").transform; } public override void OnEnter() { controller.NavAgent.speed = controller.ChaseSpeed; controller.NavAgent.stoppingDistance = controller.AttackRange * 0.9f; lastSeenTime = Time.time; } public override void OnUpdate() { if (controller.CanSeePlayer()) { lastSeenTime = Time.time; controller.NavAgent.SetDestination(playerTransform.position); // 检查是否进入攻击范围 float distanceToPlayer = Vector3.Distance( controller.transform.position, playerTransform.position); if (distanceToPlayer <= controller.AttackRange) { controller.TransitionToState(NPCState.Attack); } } else if (Time.time - lastSeenTime > controller.ChaseDuration) { // 丢失玩家视线超过一定时间返回巡逻 controller.TransitionToState(NPCState.Patrol); } // 检查是否需要逃跑(如生命值过低) if (controller.HealthPercent < 0.3f) { controller.TransitionToState(NPCState.Flee); } } public override void OnExit() { // 可能播放停止追击的动画 } }3.3 攻击状态实现
当NPC接近玩家到攻击范围时,切换到攻击状态。
public class AttackState : NPCStateBase { private Transform playerTransform; private float attackCooldown; public AttackState(NPCController controller) : base(controller) { stateType = NPCState.Attack; playerTransform = GameObject.FindGameObjectWithTag("Player").transform; } public override void OnEnter() { controller.NavAgent.isStopped = true; attackCooldown = 0f; } public override void OnUpdate() { // 面向玩家 Vector3 direction = (playerTransform.position - controller.transform.position).normalized; Quaternion lookRotation = Quaternion.LookRotation(new Vector3(direction.x, 0, direction.z)); controller.transform.rotation = Quaternion.Slerp( controller.transform.rotation, lookRotation, Time.deltaTime * controller.RotationSpeed); // 攻击逻辑 attackCooldown -= Time.deltaTime; if (attackCooldown <= 0f) { PerformAttack(); attackCooldown = controller.AttackInterval; } // 检查玩家是否超出攻击范围 float distance = Vector3.Distance( controller.transform.position, playerTransform.position); if (distance > controller.AttackRange * 1.2f) { controller.NavAgent.isStopped = false; controller.TransitionToState(NPCState.Chase); } // 检查是否需要逃跑 if (controller.HealthPercent < 0.3f) { controller.TransitionToState(NPCState.Flee); } } private void PerformAttack() { // 播放攻击动画 controller.Animator.SetTrigger("Attack"); // 实际伤害计算逻辑 if (Vector3.Distance(controller.transform.position, playerTransform.position) <= controller.AttackRange) { // 对玩家造成伤害 playerTransform.GetComponent<PlayerHealth>().TakeDamage(controller.AttackDamage); } } public override void OnExit() { controller.NavAgent.isStopped = false; } }3.4 逃跑状态实现
当NPC生命值过低时,会切换到逃跑状态寻找安全位置。
public class FleeState : NPCStateBase { private Transform safePoint; private float fleeTimer; public FleeState(NPCController controller) : base(controller) { stateType = NPCState.Flee; safePoint = GameObject.FindGameObjectWithTag("SafePoint").transform; } public override void OnEnter() { controller.NavAgent.speed = controller.FleeSpeed; controller.NavAgent.SetDestination(safePoint.position); fleeTimer = 0f; } public override void OnUpdate() { fleeTimer += Time.deltaTime; // 到达安全点或逃跑时间过长 if (controller.NavAgent.remainingDistance <= controller.NavAgent.stoppingDistance || fleeTimer >= controller.MaxFleeTime) { // 生命值恢复后返回巡逻 if (controller.HealthPercent > 0.6f) { controller.TransitionToState(NPCState.Patrol); } } } public override void OnExit() { // 可能播放恢复正常状态的动画 } }4. 状态机管理与集成
4.1 状态机控制器实现
状态机控制器负责管理所有状态和状态转换。
public class NPCController : MonoBehaviour { [Header("Movement Settings")] public float PatrolSpeed = 3.5f; public float ChaseSpeed = 5f; public float FleeSpeed = 6f; public float RotationSpeed = 5f; [Header("Combat Settings")] public float AttackRange = 2f; public float AttackDamage = 10f; public float AttackInterval = 2f; [Header("AI Settings")] public float ChaseDuration = 5f; public float MaxFleeTime = 10f; public float WaypointWaitTime = 2f; public Transform[] PatrolWaypoints; [Header("Health")] public float HealthPercent => currentHealth / maxHealth; private float maxHealth = 100f; private float currentHealth = 100f; // 组件引用 private NavMeshAgent navAgent; private Animator animator; // 状态机相关 private Dictionary<NPCState, NPCStateBase> states; private NPCStateBase currentState; private void Awake() { navAgent = GetComponent<NavMeshAgent>(); animator = GetComponent<Animator>(); // 初始化状态 states = new Dictionary<NPCState, NPCStateBase> { { NPCState.Patrol, new PatrolState(this) }, { NPCState.Chase, new ChaseState(this) }, { NPCState.Attack, new AttackState(this) }, { NPCState.Flee, new FleeState(this) } }; } private void Start() { TransitionToState(NPCState.Patrol); } private void Update() { currentState?.OnUpdate(); } public void TransitionToState(NPCState newState) { if (states.TryGetValue(newState, out var state)) { currentState?.OnExit(); currentState = state; currentState.OnEnter(); } } public bool CanSeePlayer() { // 实现玩家检测逻辑 // 包括视线检测、距离检测等 return false; // 简化示例 } // 其他辅助方法和属性... }4.2 状态转换条件优化
为了使状态转换更加灵活,我们可以将转换条件抽象出来:
// 状态转换条件类 [System.Serializable] public class StateTransition { public NPCState FromState; public NPCState ToState; public System.Func<bool> Condition; public int Priority; } // 在NPCController中添加 private List<StateTransition> transitions; private void InitializeTransitions() { transitions = new List<StateTransition> { new StateTransition { FromState = NPCState.Patrol, ToState = NPCState.Chase, Condition = () => CanSeePlayer(), Priority = 10 }, new StateTransition { FromState = NPCState.Chase, ToState = NPCState.Attack, Condition = () => { var player = GameObject.FindGameObjectWithTag("Player"); return player != null && Vector3.Distance(transform.position, player.transform.position) <= AttackRange; }, Priority = 20 }, // 其他转换条件... }; } private void CheckTransitions() { var currentStateType = currentState?.StateType ?? NPCState.Patrol; var applicableTransitions = transitions .Where(t => t.FromState == currentStateType && t.Condition()) .OrderByDescending(t => t.Priority); foreach (var transition in applicableTransitions) { TransitionToState(transition.ToState); break; // 只执行最高优先级的转换 } }5. 高级技巧与优化
5.1 使用ScriptableObject管理状态配置
为了更方便地调整NPC行为参数,可以使用ScriptableObject:
[CreateAssetMenu(menuName = "AI/NPC Config")] public class NPCConfig : ScriptableObject { [Header("Movement")] public float PatrolSpeed = 3.5f; public float ChaseSpeed = 5f; public float FleeSpeed = 6f; [Header("Combat")] public float AttackRange = 2f; public float AttackDamage = 10f; public float AttackInterval = 2f; [Header("AI Behavior")] public float ChaseDuration = 5f; public float MaxFleeTime = 10f; public float WaypointWaitTime = 2f; } // 在NPCController中引用 public class NPCController : MonoBehaviour { public NPCConfig config; // 使用config中的值替代硬编码 public float PatrolSpeed => config.PatrolSpeed; // 其他属性... }5.2 动画状态机与行为状态机同步
确保动画状态与行为状态同步:
// 在每个状态的OnEnter中添加动画触发 public override void OnEnter() { controller.Animator.SetInteger("State", (int)stateType); // 其他初始化... } // 在Animator Controller中设置对应的状态参数5.3 性能优化建议
- 减少每帧计算:将一些昂贵的计算(如物理检测)分散到多帧执行
- 对象池管理:对于大量NPC,使用对象池管理状态机实例
- 事件驱动:使用事件系统减少状态机轮询
// 示例:事件驱动的状态转换 public class NPCController : MonoBehaviour { // ... private void OnEnable() { HealthSystem.OnLowHealth += HandleLowHealth; } private void OnDisable() { HealthSystem.OnLowHealth -= HandleLowHealth; } private void HandleLowHealth() { if (currentState.StateType != NPCState.Flee) { TransitionToState(NPCState.Flee); } } }通过以上实现,我们建立了一个灵活、可维护的NPC行为系统。这种基于有限状态机的设计不仅适用于NPC AI,也可以应用于游戏中的各种状态管理场景,如UI流程、游戏状态管理等。