状态机(Finite State Machine,FSM)是游戏开发中管理角色行为的核心模式。一个NPC可能在”待机”、”巡逻”、”追击”、”攻击”、”死亡”等状态之间切换,每种状态有独立的逻辑和转换规则。本文将用 Java 实现一个完整的状态机框架,并构建一个具备简单AI的守卫NPC demo。
状态机的核心概念
状态机由四个基本要素组成:
- 状态(State):系统在某一刻的具体情况,如
IDLE、PATROL、CHASE、ATTACK、DEAD。 - 事件(Event):触发状态转换的信号,如
SEE_PLAYER、LOSE_PLAYER、IN_RANGE、HP_ZERO。 - 转换(Transition):从一个状态到另一个状态的规则。
- 动作(Action):进入或退出状态时执行的行为。
基础状态机实现
首先定义状态接口和具体状态:
/**
* 状态接口:每个状态需实现进入、执行、退出三个生命周期方法
*/
public interface State {
void enter(NPC npc);
void execute(NPC npc);
void exit(NPC npc);
}
/**
* 枚举所有可能的状态,便于管理和扩展
*/
public enum StateType {
IDLE, PATROL, CHASE, ATTACK, DEAD
}
/**
* 待机状态:原地站立,定时环顾四周
*/
public class IdleState implements State {
private long lastLookAroundTime = 0;
@Override
public void enter(NPC npc) {
npc.setVelocity(0, 0);
System.out.println(npc.getName() + " 进入待机状态");
}
@Override
public void execute(NPC npc) {
long now = System.currentTimeMillis();
// 每3秒环顾一次,检测玩家
if (now - lastLookAroundTime > 3000) {
lastLookAroundTime = now;
if (npc.canSeePlayer()) {
npc.changeState(StateType.CHASE);
} else if (npc.shouldPatrol()) {
npc.changeState(StateType.PATROL);
}
}
}
@Override
public void exit(NPC npc) {
System.out.println(npc.getName() + " 离开待机状态");
}
}
/**
* 巡逻状态:按预设路径移动
*/
public class PatrolState implements State {
@Override
public void enter(NPC npc) {
System.out.println(npc.getName() + " 开始巡逻");
npc.setPatrolTarget();
}
@Override
public void execute(NPC npc) {
npc.moveTowardsPatrolPoint();
// 巡逻中如果看到玩家,切换到追击
if (npc.canSeePlayer()) {
npc.changeState(StateType.CHASE);
return;
}
// 到达巡逻点后短暂待机
if (npc.reachedPatrolPoint()) {
npc.changeState(StateType.IDLE);
}
}
@Override
public void exit(NPC npc) {
System.out.println(npc.getName() + " 停止巡逻");
}
}
/**
* 追击状态:朝玩家位置快速移动
*/
public class ChaseState implements State {
@Override
public void enter(NPC npc) {
System.out.println(npc.getName() + " 发现玩家!开始追击!");
npc.setSpeed(npc.getRunSpeed());
}
@Override
public void execute(NPC npc) {
npc.moveTowardsPlayer();
// 如果玩家进入攻击范围,切换攻击
if (npc.isPlayerInAttackRange()) {
npc.changeState(StateType.ATTACK);
return;
}
// 如果丢失玩家视野,返回待机
if (!npc.canSeePlayer()) {
npc.changeState(StateType.IDLE);
return;
}
}
@Override
public void exit(NPC npc) {
npc.setSpeed(npc.getWalkSpeed());
}
}
/**
* 攻击状态:对玩家造成伤害
*/
public class AttackState implements State {
private long lastAttackTime = 0;
private static final long ATTACK_COOLDOWN = 1000; // 1秒攻击间隔
@Override
public void enter(NPC npc) {
System.out.println(npc.getName() + " 进入攻击状态!");
}
@Override
public void execute(NPC npc) {
long now = System.currentTimeMillis();
if (now - lastAttackTime >= ATTACK_COOLDOWN) {
lastAttackTime = now;
npc.attackPlayer();
}
// 玩家跑出攻击范围则追击
if (!npc.isPlayerInAttackRange()) {
npc.changeState(StateType.CHASE);
return;
}
// 玩家死亡后待机
if (npc.isPlayerDead()) {
npc.changeState(StateType.IDLE);
}
}
@Override
public void exit(NPC npc) {
System.out.println(npc.getName() + " 停止攻击");
}
}
/**
* 死亡状态:不再响应任何事件
*/
public class DeadState implements State {
@Override
public void enter(NPC npc) {
System.out.println(npc.getName() + " 已死亡");
npc.setVelocity(0, 0);
}
@Override
public void execute(NPC npc) {
// 死亡状态下无任何行为
}
@Override
public void exit(NPC npc) {
// 死亡后不可复活
}
}
NPC 实体类
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
public class NPC {
private final String name;
private State currentState;
private final Map<StateType, State> stateMap = new HashMap<>();
private int hp = 100;
private double x, y; // 当前位置
private double vx, vy; // 速度向量
private double patrolTargetX, patrolTargetY;
private final double walkSpeed = 1.0;
private final double runSpeed = 3.0;
private double currentSpeed = walkSpeed;
private final double viewRange = 10.0;
private final double attackRange = 2.0;
private static final Random random = new Random();
// 模拟玩家位置和状态
private double playerX = 15.0, playerY = 15.0;
private int playerHp = 100;
public NPC(String name, double startX, double startY) {
this.name = name;
this.x = startX;
this.y = startY;
// 注册所有状态
stateMap.put(StateType.IDLE, new IdleState());
stateMap.put(StateType.PATROL, new PatrolState());
stateMap.put(StateType.CHASE, new ChaseState());
stateMap.put(StateType.ATTACK, new AttackState());
stateMap.put(StateType.DEAD, new DeadState());
// 初始状态为待机
changeState(StateType.IDLE);
}
public void update() {
if (currentState != null && hp > 0) {
currentState.execute(this);
}
// 更新位置
x += vx * currentSpeed;
y += vy * currentSpeed;
}
public void changeState(StateType newStateType) {
if (currentState != null) {
currentState.exit(this);
}
currentState = stateMap.get(newStateType);
if (currentState != null) {
currentState.enter(this);
}
}
public void takeDamage(int damage) {
hp -= damage;
System.out.println(name + " 受到 " + damage + " 点伤害,剩余 HP: " + hp);
if (hp <= 0) {
changeState(StateType.DEAD);
}
}
// 视线检测:简单计算与玩家的距离
public boolean canSeePlayer() {
double dist = Math.hypot(playerX - x, playerY - y);
return dist <= viewRange;
}
public boolean isPlayerInAttackRange() {
double dist = Math.hypot(playerX - x, playerY - y);
return dist <= attackRange;
}
public boolean isPlayerDead() {
return playerHp <= 0;
}
public void moveTowardsPlayer() {
double dx = playerX - x;
double dy = playerY - y;
double dist = Math.hypot(dx, dy);
if (dist > 0) {
vx = dx / dist;
vy = dy / dist;
}
}
public void setPatrolTarget() {
patrolTargetX = x + (random.nextDouble() - 0.5) * 8;
patrolTargetY = y + (random.nextDouble() - 0.5) * 8;
}
public void moveTowardsPatrolPoint() {
double dx = patrolTargetX - x;
double dy = patrolTargetY - y;
double dist = Math.hypot(dx, dy);
if (dist > 0.1) {
vx = dx / dist;
vy = dy / dist;
} else {
vx = 0;
vy = 0;
}
}
public boolean reachedPatrolPoint() {
return Math.hypot(patrolTargetX - x, patrolTargetY - y) < 0.5;
}
public boolean shouldPatrol() {
return random.nextDouble() < 0.3; // 30%概率决定巡逻
}
public void attackPlayer() {
int damage = 10 + random.nextInt(10);
playerHp -= damage;
System.out.println(name + " 对玩家造成 " + damage + " 点伤害!玩家剩余 HP: " + playerHp);
}
// Getters and Setters
public String getName() { return name; }
public void setVelocity(double vx, double vy) { this.vx = vx; this.vy = vy; }
public void setSpeed(double speed) { this.currentSpeed = speed; }
public double getWalkSpeed() { return walkSpeed; }
public double getRunSpeed() { return runSpeed; }
public double getX() { return x; }
public double getY() { return y; }
public static void main(String[] args) throws InterruptedException {
NPC guard = new NPC("守卫队长", 0, 0);
// 模拟游戏主循环,每200ms更新一次
for (int i = 0; i < 50; i++) {
guard.update();
Thread.sleep(200);
// 模拟玩家靠近守卫
if (i == 10) {
guard.playerX = 3; guard.playerY = 3;
System.out.println("=== 玩家靠近守卫 ===");
}
// 模拟玩家被击杀
if (guard.playerHp <= 0) {
System.out.println("=== 玩家阵亡 ===");
break;
}
}
}
}
状态转换表与可视化
为了更清晰地理解状态流转,我们整理出如下状态转换矩阵:
| 当前状态 | 触发条件 | 下一状态 |
|---|---|---|
| IDLE | 发现玩家 | CHASE |
| IDLE | 巡逻定时器到期 | PATROL |
| PATROL | 发现玩家 | CHASE |
| PATROL | 到达巡逻点 | IDLE |
| CHASE | 进入攻击范围 | ATTACK |
| CHASE | 丢失视野 | IDLE |
| ATTACK | 玩家逃出范围 | CHASE |
| ATTACK | 玩家死亡 | IDLE |
| 任意 | HP <= 0 | DEAD |
进阶:分层状态机
当NPC行为变得复杂时,单一状态机可能产生大量重复代码。此时可以引入分层状态机(Hierarchical State Machine):将”移动”抽象为父状态,其子状态包含PATROL和CHASE,共享移动逻辑。
/**
* 抽象父状态:封装移动逻辑
*/
public abstract class MovementState implements State {
protected void move(NPC npc, double targetX, double targetY, double speed) {
double dx = targetX - npc.getX();
double dy = targetY - npc.getY();
double dist = Math.hypot(dx, dy);
if (dist > 0) {
npc.setVelocity((dx / dist) * speed, (dy / dist) * speed);
}
}
}
复杂度分析
| 指标 | 时间复杂度 | 说明 |
|---|---|---|
| 状态更新 | (O(1)) | 每次 update 只执行当前状态的逻辑 |
| 状态切换 | (O(1)) | HashMap 查询 + 两次方法调用 |
| 内存占用 | (O(s)) | s 为状态数量,与NPC行为复杂度无关 |
总结
状态机模式将复杂的NPC行为拆解为离散的状态和转换规则,代码结构清晰、易于调试和扩展。本文实现的守卫NPC demo 展示了从待机到死亡的全生命周期管理。在实际游戏引擎(如Unity、Unreal)中,状态机更是动画系统、AI行为树的基础构件。掌握状态机的设计思想,是每一位游戏开发者的必修课。