在游戏开发领域,有限状态机(Finite State Machine,简称FSM)是构建角色AI最基础也最实用的工具之一。作为一名从事游戏开发多年的程序员,我几乎在每个项目中都会用到FSM。它的魅力在于能将复杂的行为逻辑分解为离散的状态和转换,让AI行为变得清晰可控。
想象一下你在设计一个敌人AI,它需要根据玩家行为做出不同反应:当玩家远离时巡逻,发现玩家时追击,距离足够近时攻击,受伤时逃跑...如果用一堆if-else来实现,代码很快就会变成难以维护的"面条式"代码。而FSM通过将每个行为封装成独立状态,通过明确的转换条件连接,让代码结构保持清晰。
FSM特别适合模拟具有明确行为模式的游戏实体,比如:
一个典型的FSM包含三个核心部分:
状态(States):对象可能处于的离散行为模式。比如吃豆人幽灵的"追逐"、"散射"、"恐惧"状态。
转换(Transitions):定义状态之间如何切换的条件规则。比如"从散射状态切换到追逐状态的条件是散射计时器归零"。
行为(Actions):进入状态、退出状态和处于状态期间执行的具体行为。比如进入"恐惧"状态时改变颜色,在"恐惧"状态下随机移动等。
在Unity中实现FSM时,通常会使用枚举定义状态,用switch-case处理不同状态的逻辑,用变量记录状态计时器等元数据。
让我们深入分析《吃豆人》中红幽灵(Blinky)的AI实现,这是游戏史上最经典的FSM应用之一。红幽灵的行为看似简单,但设计精妙,对后来的游戏AI产生了深远影响。
红幽灵的FSM包含以下几个关键状态:
散射状态(Scatter):幽灵会移动到地图的特定角落(通常是右上角)。这个状态让幽灵有规律地远离吃豆人,给玩家喘息机会。
追逐状态(Chase):幽灵会直接朝向吃豆人当前位置移动。红幽灵的独特之处在于它采用"直接追逐"策略,比其他幽灵更具攻击性。
恐惧状态(Frightened):当吃豆人吃掉能量豆时,幽灵会变成蓝色并试图逃离吃豆人。此时玩家可以反过来吃掉幽灵。
被吃状态(Eaten):当幽灵被吃后,会变成眼睛返回重生点。这个状态展示了FSM如何处理异常行为。
离开鬼屋状态(LeavingHouse):游戏开始时幽灵从鬼屋中央出发的状态。
进入鬼屋状态(EnteringHouse):被吃后返回鬼屋的状态。
状态之间的转换不是随机的,而是由精心设计的条件触发:
散射→追逐:当散射计时器归零时转换。在经典吃豆人中,这个时间通常是7秒。
追逐→散射:当追逐计时器归零时转换。通常持续约20秒。
正常→恐惧:当吃豆人吃掉能量豆时,所有幽灵立即转换到此状态。
恐惧→正常:恐惧状态持续约10秒,时间到或幽灵被吃时转换。
恐惧→被吃:当幽灵与吃豆人碰撞且处于恐惧状态时转换。
这些转换条件确保了幽灵行为既有规律可循,又能对玩家行为做出响应。
让我们看看如何在Unity中实现这样的FSM。以下是核心代码结构:
csharp复制public class PacManGhostFSM : MonoBehaviour
{
public enum GhostState
{
Scatter, // 散射状态
Chase, // 追逐状态
Frightened, // 恐惧状态
Eaten, // 被吃状态
LeavingHouse, // 离开鬼屋状态
EnteringHouse // 进入鬼屋状态
}
[SerializeField]
private GhostState currentState = GhostState.Scatter;
private void Update()
{
UpdateStateTimers();
UpdateStateMachine();
UpdateMovement();
UpdateAnimations();
}
private void UpdateStateMachine()
{
switch (currentState)
{
case GhostState.LeavingHouse:
HandleLeavingHouseState();
break;
case GhostState.Scatter:
HandleScatterState();
break;
// 其他状态处理...
}
}
private void ChangeState(GhostState newState)
{
if (currentState == newState) return;
GhostState previousState = currentState;
currentState = newState;
// 状态转换时的初始化逻辑
switch (newState)
{
case GhostState.Scatter:
// 重置散射计时器等
break;
// 其他状态初始化...
}
Debug.Log($"状态变化: {previousState} -> {newState}");
}
}
这个框架清晰地分离了状态定义、状态更新和状态转换逻辑。每个状态有独立的处理方法,使代码易于理解和扩展。
要实现一个健壮的游戏FSM,需要考虑许多细节问题。下面分享我在实际项目中的一些经验。
游戏中的状态转换往往与时间相关。比如吃豆人幽灵的散射和追逐状态会定期切换。实现这种定时行为需要精心设计计时器系统:
csharp复制[System.Serializable]
public class GhostStateTimers
{
public float scatterTimer;
public float chaseTimer;
public float frightenedTimer;
public float stateChangeCooldown; // 状态转换冷却,防止频繁切换
}
private void UpdateStateTimers()
{
switch (currentState)
{
case GhostState.Scatter:
stateTimers.scatterTimer -= Time.deltaTime;
if (stateTimers.scatterTimer <= 0)
{
ChangeState(GhostState.Chase);
}
break;
// 其他计时器更新...
}
}
提示:使用[System.Serializable]让计时器配置显示在Inspector中,方便设计师调整平衡性而无需修改代码。
很多时候我们需要在状态转换时执行特定操作。例如幽灵进入恐惧状态时要改变颜色,播放音效等:
csharp复制private void ChangeToNormalMode()
{
currentMode = GhostMode.Normal;
isVulnerable = false;
// 恢复正常颜色
ghostSpriteRenderer.color = behaviorSettings.normalColor;
// 如果不是被吃或进入鬼屋状态,切换到散射状态
if (currentState != GhostState.Eaten && currentState != GhostState.EnteringHouse)
{
ChangeState(GhostState.Scatter);
}
}
这种显式的状态转换处理让行为变化更加清晰可控。
幽灵的移动是FSM的重要组成部分。不同状态下,幽灵会采用不同的路径计算策略:
csharp复制private void HandleChaseState()
{
if (pacManTransform == null) return;
Vector2 pacManPosition = pacManTransform.position;
currentTargetPosition = pacManPosition;
// 红幽灵的特殊行为:近距离时预测玩家移动
float distanceToPacMan = Vector2.Distance(transform.position, pacManPosition);
if (distanceToPacMan < 5.0f)
{
Rigidbody2D pacManRb = pacManTransform.GetComponent<Rigidbody2D>();
if (pacManRb != null)
{
Vector2 predictedPosition = pacManPosition + pacManRb.velocity.normalized * 3.0f;
currentTargetPosition = predictedPosition;
}
}
if (ShouldRecalculatePath())
{
CalculatePathToTarget(currentTargetPosition);
}
}
FSM不仅要控制行为逻辑,还需要管理相应的视觉表现。在Unity中,这通常通过Animator Controller实现:
csharp复制private void UpdateGhostAppearance()
{
switch (currentMode)
{
case GhostMode.Normal:
ghostSpriteRenderer.color = behaviorSettings.normalColor;
break;
case GhostMode.Frightened:
ghostSpriteRenderer.color = behaviorSettings.frightenedColor;
break;
case GhostMode.Eaten:
ghostSpriteRenderer.color = behaviorSettings.eatenColor;
break;
}
// 更新动画参数
if (ghostAnimator != null)
{
ghostAnimator.SetBool("IsFrightened", currentMode == GhostMode.Frightened);
ghostAnimator.SetBool("IsEaten", currentMode == GhostMode.Eaten);
// 根据移动方向更新动画
Vector2 currentDirection = movementController.CurrentDirection;
ghostAnimator.SetFloat("MoveX", currentDirection.x);
ghostAnimator.SetFloat("MoveY", currentDirection.y);
}
}
这种将逻辑状态与视觉表现紧密绑定的设计,确保了玩家能直观理解游戏实体的当前行为。
除了吃豆人这类简单AI,FSM也能支撑相当复杂的行为。让我们看看如何在第一人称射击游戏中应用FSM。
《雷神之锤II》中的怪物AI通常包含以下状态:
空闲状态(Idle):怪物处于待机模式,可能播放呼吸动画。
巡逻状态(Patrol):在预设路径点间移动。
追逐状态(Chase):发现玩家后追击。
攻击状态(Attack):达到攻击距离后发动攻击。
受伤状态(Pain):受到攻击时的短暂硬直。
死亡状态(Dead):生命值归零后的处理。
射击游戏中的FSM通常需要复杂的感知系统来决定状态转换:
csharp复制private void UpdateSenses()
{
// 视觉检测
if (Time.time - lastSightCheckTime > sightCheckInterval)
{
lastSightCheckTime = Time.time;
playerInSight = CheckPlayerVisibility();
if (playerInSight)
{
lastKnownPlayerPosition = playerTarget.position;
playerLastSeenTime = Time.time;
if (currentState == MonsterState.Idle ||
currentState == MonsterState.Patrol)
{
ChangeState(MonsterState.Chase);
}
}
}
// 听觉检测
if (Vector3.Distance(transform.position, playerTarget.position) < hearingRange)
{
// 根据声音强度决定是否警觉
}
}
private bool CheckPlayerVisibility()
{
RaycastHit hit;
Vector3 direction = playerTarget.position - transform.position;
if (Physics.Raycast(transform.position, direction, out hit, stats.detectionRange))
{
return hit.transform == playerTarget;
}
return false;
}
通过FSM可以方便地实现怪物类型的差异化:
csharp复制public enum MonsterType
{
Grunt, // 基础士兵,简单行为
Enforcer, // 强化士兵,会寻找掩体
Berserker, // 近战狂战士,高攻击性
Flyer, // 飞行单位,三维移动
Tank // 重型单位,高生命值
}
private void UpdateBehaviorBasedOnType()
{
switch (monsterType)
{
case MonsterType.Grunt:
// 基础追逐和射击行为
break;
case MonsterType.Enforcer:
// 会寻找掩体,从侧面包抄
if (ShouldTakeCover())
{
FindCoverPosition();
}
break;
case MonsterType.Berserker:
// 近战攻击,冲锋行为
if (CanChargeAtPlayer())
{
StartChargeAttack();
}
break;
}
}
这种设计让不同类型的怪物共享相同的FSM框架,但通过状态内的差异化处理实现丰富的行为变化。
在实际项目中使用FSM时,会遇到各种边界情况和设计挑战。以下是几个常见问题及解决方法。
随着需求增加,状态数量可能急剧增长,导致FSM难以维护。解决方案包括:
层次化状态机(HFSM):将相关状态分组,形成层次结构。比如"移动"大状态包含"行走"、"跑步"、"冲刺"等子状态。
行为树结合:对复杂决策逻辑使用行为树,仍用FSM管理高层状态。
状态分割:将大状态拆分为多个组件,比如分离移动逻辑和攻击逻辑。
当转换条件涉及多个因素时,代码可能变得混乱。可以考虑:
csharp复制[System.Serializable]
public class StateTransition
{
public GhostState fromState;
public GhostState toState;
public Func<bool> condition;
}
public List<StateTransition> transitions = new List<StateTransition>();
private void CheckTransitions()
{
foreach (var transition in transitions)
{
if (transition.fromState == currentState && transition.condition())
{
ChangeState(transition.toState);
break;
}
}
}
某些事件(如玩家吃能量豆)需要影响所有幽灵状态。可以通过事件系统解耦:
csharp复制// 事件定义
public static event Action OnPowerPelletEaten;
// 事件触发(玩家脚本中)
GameEvents.OnPowerPelletEaten?.Invoke();
// 事件监听(幽灵脚本中)
private void OnEnable()
{
GameEvents.OnPowerPelletEaten += OnPowerPelletEaten;
}
private void OnDisable()
{
GameEvents.OnPowerPelletEaten -= OnPowerPelletEaten;
}
private void OnPowerPelletEaten()
{
if (currentState != GhostState.Eaten && currentState != GhostState.EnteringHouse)
{
SetFrightenedMode();
}
}
复杂的FSM需要良好的调试支持:
状态日志:记录状态变化历史,便于追踪问题。
编辑器可视化:在Scene视图显示当前状态和目标信息。
csharp复制private void OnDrawGizmosSelected()
{
// 绘制当前目标位置
Gizmos.color = Color.red;
Gizmos.DrawWireSphere(currentTargetPosition, 0.3f);
// 绘制状态信息
#if UNITY_EDITOR
string stateInfo = $"状态: {currentState}\n模式: {currentMode}";
UnityEditor.Handles.Label(transform.position + Vector3.up, stateInfo);
#endif
}
在大型游戏中,可能有数百个AI实体同时运行FSM。这时性能优化就变得至关重要。
不是所有AI都需要每帧更新:
csharp复制private void Update()
{
// 根据距离玩家远近决定更新频率
float distanceToPlayer = Vector3.Distance(transform.position, playerPosition);
float updateInterval = Mathf.Lerp(0.1f, 1.0f, distanceToPlayer / 50f);
if (Time.time - lastUpdateTime > updateInterval)
{
lastUpdateTime = Time.time;
UpdateFSM();
}
}
同类AI可以共享状态逻辑实例:
csharp复制public static class GhostBehaviorLibrary
{
public static void ExecuteScatterBehavior(Ghost ghost)
{
// 共享的散射行为逻辑
}
// 其他共享行为...
}
// 在具体幽灵脚本中
private void HandleScatterState()
{
GhostBehaviorLibrary.ExecuteScatterBehavior(this);
}
将路径查找等昂贵操作分散到多帧:
csharp复制private IEnumerator CalculatePathAsync(Vector3 target)
{
// 在后台线程执行路径计算
var path = PathFinder.FindPath(transform.position, target);
// 返回到主线程应用结果
yield return new WaitForEndOfFrame();
currentPath = path;
}
对于大量简单AI,可以考虑使用结构体而非类来表示状态:
csharp复制public struct GhostStateData
{
public GhostState state;
public float timer;
public Vector2 target;
}
private GhostStateData currentStateData;
这种优化在需要实例化数千个简单AI时(如RTS游戏的小兵)特别有效。
虽然FSM是经典AI技术,但现代游戏开发中它仍在不断演进,与其他技术结合产生新的可能性。
行为树适合处理复杂决策逻辑,而FSM擅长管理明确的状态流程。两者结合可以发挥各自优势:
csharp复制// 行为树节点示例:决定是否追击玩家
public class ChaseDecision : BTNode
{
public override BTStatus Execute()
{
float distance = Vector3.Distance(ai.transform.position, player.position);
if (distance < chaseThreshold && CanSeePlayer())
{
fsm.ChangeState(State.Chase);
return BTStatus.Success;
}
return BTStatus.Failure;
}
}
实用AI通过评分系统选择最佳行为,与FSM结合可以实现更动态的状态转换:
csharp复制private void EvaluateBestState()
{
var options = new Dictionary<GhostState, float>();
// 计算各状态的效用分数
options[GhostState.Scatter] = CalculateScatterScore();
options[GhostState.Chase] = CalculateChaseScore();
options[GhostState.Ambush] = CalculateAmbushScore();
// 选择最高分状态
var bestState = options.Aggregate((x, y) => x.Value > y.Value ? x : y).Key;
if (bestState != currentState)
{
ChangeState(bestState);
}
}
在一些前沿应用中,FSM可以作为机器学习AI的"安全网",确保AI行为不会完全失控:
csharp复制private void Update()
{
if (useMLDecision)
{
// 使用机器学习模型决定行为
nextAction = mlModel.DecideNextAction();
// 但用FSM约束可执行的行为范围
if (IsActionAllowedInCurrentState(nextAction))
{
ExecuteAction(nextAction);
}
}
else
{
// 回退到传统FSM
UpdateFSM();
}
}
在多人游戏中,AI状态需要在客户端间同步。FSM因其离散特性非常适合网络同步:
csharp复制[Command]
private void CmdChangeState(GhostState newState)
{
currentState = newState;
RpcOnStateChanged(newState);
}
[ClientRpc]
private void RpcOnStateChanged(GhostState newState)
{
if (isServer) return;
currentState = newState;
// 客户端本地处理状态转换效果
}
这种设计确保了所有玩家看到的AI行为一致,同时最小化了网络数据传输量。
根据我在多个游戏项目中的经验,以下是设计高质量FSM的一些实用建议:
良好的命名能极大提高FSM代码的可读性:
csharp复制public enum EnemyState
{
Patrolling,
Chasing,
Attacking,
TakingCover,
Fleeing
}
private bool ShouldStartChasing()
{
return CanSeePlayer() && !isInjured;
}
private void OnPlayerEnteredDetectionRange()
{
if (currentState == EnemyState.Patrolling)
{
ChangeState(EnemyState.Chasing);
}
}
每个状态应该尽可能独立,避免直接依赖其他状态的内部细节。可以通过共享数据对象传递必要信息:
csharp复制public class GhostSharedData
{
public Vector2 playerPosition;
public bool isPowerPelletActive;
public float gameTime;
// 其他共享数据...
}
private void HandleChaseState()
{
Vector2 target = sharedData.playerPosition;
// 使用共享数据而非直接引用其他状态
}
为每个状态实现ToString()或GetDebugInfo()方法,便于开发时监控:
csharp复制public override string ToString()
{
return $"{currentState} (Timer: {stateTimer:F1}) Target: {currentTarget}";
}
将状态参数(如持续时间、移动速度等)定义为ScriptableObject,方便非程序员调整:
csharp复制[CreateAssetMenu]
public class GhostStateConfig : ScriptableObject
{
public float scatterDuration = 7f;
public float chaseDuration = 20f;
public float frightenedDuration = 10f;
public float scatterSpeed = 3.5f;
// 其他可配置参数...
}
// 在MonoBehaviour中使用
public GhostStateConfig config;
private void InitializeState()
{
stateTimers.scatterTimer = config.scatterDuration;
// 其他初始化...
}
记录状态变化历史有助于调试复杂问题:
csharp复制private struct StateHistoryEntry
{
public GhostState state;
public float time;
public string reason;
}
private Queue<StateHistoryEntry> stateHistory = new Queue<StateHistoryEntry>(20);
private void ChangeState(GhostState newState, string reason = "")
{
stateHistory.Enqueue(new StateHistoryEntry
{
state = currentState,
time = Time.time,
reason = reason
});
if (stateHistory.Count > 20)
{
stateHistory.Dequeue();
}
// 正常状态转换逻辑...
}
虽然核心概念相同,但FSM在不同类型游戏中的实现会有所侧重。以下是几种常见游戏类型的FSM特点。
典型应用:
特点:
csharp复制public enum BossState
{
Phase1,
Phase2,
Phase3,
Enraged,
DeathSequence
}
private void UpdateBossFSM()
{
switch (currentState)
{
case BossState.Phase1:
if (health < 0.7f) ChangeState(BossState.Phase2);
break;
case BossState.Phase2:
if (health < 0.3f) ChangeState(BossState.Enraged);
break;
// 其他阶段...
}
}
典型应用:
特点:
csharp复制public class NPCStateMachine : MonoBehaviour, ISaveable
{
public NPCRelationshipState relationship;
public NPCConversationState conversation;
public NPCBehaviorState behavior;
public void OnDialogueChoiceSelected(DialogueChoice choice)
{
// 根据玩家对话选择改变状态
if (choice.leadsToQuest)
{
behavior = NPCBehaviorState.QuestGiver;
}
}
public object CaptureState()
{
return new SaveData
{
relationship = this.relationship,
conversation = this.conversation,
behavior = this.behavior
};
}
public void RestoreState(object state)
{
var saveData = (SaveData)state;
// 恢复状态...
}
}
典型应用:
特点:
csharp复制public class RTSUnitFSM : MonoBehaviour
{
public enum UnitState
{
Idle,
Moving,
Attacking,
Gathering,
Building
}
private void UpdateUnitState()
{
if (currentOrder != null)
{
switch (currentOrder.type)
{
case OrderType.Move:
ChangeState(UnitState.Moving);
break;
case OrderType.Attack:
ChangeState(UnitState.Attacking);
break;
// 其他命令类型...
}
}
}
}
典型应用:
特点:
csharp复制public class SoccerPlayerAI : MonoBehaviour
{
private void UpdatePlayerState()
{
if (HasBall())
{
// 根据场上情况决定带球、传球或射门
float shootScore = CalculateShootOpportunity();
float passScore = CalculatePassOpportunity();
if (shootScore > 0.8f && shootScore > passScore)
{
ChangeState(PlayerState.Shooting);
}
else if (passScore > 0.7f)
{
ChangeState(PlayerState.Passing);
}
else
{
ChangeState(PlayerState.Dribbling);
}
}
else
{
// 无球时的防守或跑位逻辑
}
}
}
虽然FSM非常实用,但在某些复杂场景下可能需要更高级的AI架构。了解这些技术如何与FSM协同工作很有价值。
行为树通过树状结构组织AI决策,比FSM更适合处理复杂、多层的决策逻辑。
与FSM的结合方式:
csharp复制// 行为树中的攻击决策节点
public class AttackDecision : BTNode
{
public override BTStatus Execute()
{
if (enemyFSM.CanAttackPlayer())
{
enemyFSM.ChangeState(EnemyState.Attacking);
return BTStatus.Success;
}
return BTStatus.Failure;
}
}
实用AI通过为每个可能行为计算效用分数,选择最高分行为执行。
与FSM的结合方式:
csharp复制private void EvaluateBestState()
{
var stateScores = new Dictionary<EnemyState, float>();
// 计算各状态的效用分数
stateScores[EnemyState.Patrol] = CalculatePatrolScore();
stateScores[EnemyState.Chase] = CalculateChaseScore();
stateScores[EnemyState.Rest] = CalculateRestScore();
// 选择最高分状态
var bestState = stateScores.OrderByDescending(pair => pair.Value).First().Key;
if (bestState != currentState)
{
ChangeState(bestState);
}
}
GOAP让AI通过规划序列动作来实现目标,适合需要多步计划的复杂行为。
与FSM的结合方式:
csharp复制public class GoapActionState : State
{
private Queue<GoapAction> currentPlan;
public override void Enter()
{
// 获取GOAP规划器生成的行动序列
currentPlan = goapPlanner.Plan(currentGoal);
}
public override void Update()
{
if (currentPlan.Count > 0)
{
var currentAction = currentPlan.Peek();
if (currentAction.IsDone())
{
currentPlan.Dequeue();
}
else
{
currentAction.Execute();
}
}
else
{
fsm.ChangeState(EnemyState.Idle);
}
}
}
现代游戏开始尝试使用机器学习技术(如强化学习)训练AI。
与FSM的结合方式:
csharp复制private void Update()
{
if (useMLDecisionMaking)
{
// 使用训练好的模型决定行为
var action = mlModel.DecideAction(observation);
// 但通过FSM确保行为安全合理
if (IsActionAllowed(action))
{
ExecuteAction(action);
}
}
else
{
// 回退到传统FSM
UpdateFSM();
}
}
在实际项目中,我通常会根据游戏类型和AI复杂度选择合适的架构组合。对于大多数游戏场景,FSM加上一些扩展(如分层状态机)已经足够强大。