每日算法 — 使用java实现状态机:游戏NPC行为建模与状态转换

状态机(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行为树的基础构件。掌握状态机的设计思想,是每一位游戏开发者的必修课。