拳皇(The King of Fighters)是经典格斗游戏系列,其核心魅力在于精准的连招系统与帧级判定。本文用Java实现一个简化的拳皇AI对战引擎,讲解有限状态机(FSM)管理角色状态、帧数据表驱动攻击判定,以及贪心策略选择最优连招路径。
核心设计思路
格斗游戏的核心是”帧”——每1/60秒为一帧。每个动作(站立、轻拳、重拳、跳跃)都有对应的启动帧、活跃帧、恢复帧。AI需要在一帧内完成:读取双方状态 → 评估最优动作 → 执行输入。我们将整个系统拆分为三层:
- 状态管理层:FSM管理角色的站立、蹲下、跳跃、受击、硬直等状态;
- 帧数据层:每个招式用
MoveFrameData记录伤害、命中框、无敌帧; - 决策层:基于当前帧差(Frame Advantage)计算最优连招路径。
完整Java实现
import java.util.*;
/**
* 拳皇AI对战引擎核心框架
* 包含:有限状态机、帧数据表、连招图、贪心决策器
*/
public class KofEngine {
// ==================== 状态定义 ====================
enum State {
IDLE, CROUCH, JUMP_START, JUMP_UP, JUMP_DOWN,
ATTACK_STARTUP, ATTACK_ACTIVE, ATTACK_RECOVERY,
HITSTUN, BLOCKSTUN, KNOCKDOWN
}
// ==================== 帧数据结构 ====================
static class MoveFrameData {
String name; // 招式名称
int startupFrames; // 启动帧(可被打断)
int activeFrames; // 活跃帧(产生命中框)
int recoveryFrames; // 恢复帧(硬直)
int damage; // 伤害值
int hitstunFrames; // 对手受击硬直
int blockstunFrames; // 对手防御硬直
int range; // 攻击距离
boolean isLow; // 是否下段攻击
boolean isOverhead; // 是否中段攻击
MoveFrameData(String name, int su, int ac, int re, int dmg,
int hs, int bs, int range, boolean low, boolean oh) {
this.name = name;
this.startupFrames = su;
this.activeFrames = ac;
this.recoveryFrames = re;
this.damage = dmg;
this.hitstunFrames = hs;
this.blockstunFrames = bs;
this.range = range;
this.isLow = low;
this.isOverhead = oh;
}
// 总帧数
int totalFrames() { return startupFrames + activeFrames + recoveryFrames; }
// 帧优势 = 对手硬直 - 自身恢复帧
int frameAdvantageOnHit() { return hitstunFrames - recoveryFrames; }
int frameAdvantageOnBlock() { return blockstunFrames - recoveryFrames; }
}
// ==================== 角色实体 ====================
static class Fighter {
String name;
int hp = 1000;
int x; // 位置
int y; // 0=地面, >0=空中
State state = State.IDLE;
int stateTimer = 0; // 当前状态剩余帧数
boolean facingRight = true;
MoveFrameData currentMove = null;
// 输入缓冲区:记录未来N帧的预输入
Deque<String> inputBuffer = new ArrayDeque<>();
Fighter(String name, int x) {
this.name = name;
this.x = x;
}
boolean isIdle() {
return state == State.IDLE || state == State.CROUCH;
}
boolean isInHitstun() {
return state == State.HITSTUN || state == State.BLOCKSTUN;
}
boolean isAirborne() {
return state == State.JUMP_UP || state == State.JUMP_DOWN;
}
}
// ==================== 全局招式库 ====================
static final Map<String, MoveFrameData> MOVE_DB = new HashMap<>();
static {
// 轻拳:快、短、安全
MOVE_DB.put("5LP", new MoveFrameData("5LP", 4, 2, 6, 30, 12, 9, 35, false, false));
// 重拳:慢、长、高伤
MOVE_DB.put("5HP", new MoveFrameData("5HP", 8, 3, 18, 90, 20, 15, 55, false, false));
// 蹲下轻脚:下段
MOVE_DB.put("2LK", new MoveFrameData("2LK", 5, 2, 8, 40, 14, 10, 40, true, false));
// 跳跃重拳:空中攻击
MOVE_DB.put("jHP", new MoveFrameData("jHP", 6, 6, 0, 100, 18, 12, 50, false, true));
// 升龙拳:对空无敌
MOVE_DB.put("DP", new MoveFrameData("DP", 3, 12, 25, 140, 24, 18, 45, false, false));
// 波动拳:远程
MOVE_DB.put("Hadoken", new MoveFrameData("Hadoken", 13, 0, 32, 60, 16, 12, 200, false, false));
}
// ==================== 连招图(Combo Graph) ====================
// key: 当前招式 -> value: 可接续的招式列表(需满足帧优势条件)
static final Map<String, List<String>> COMBO_GRAPH = new HashMap<>();
static {
COMBO_GRAPH.put("5LP", Arrays.asList("5LP", "5HP", "2LK", "DP"));
COMBO_GRAPH.put("2LK", Arrays.asList("5LP", "DP"));
COMBO_GRAPH.put("5HP", Arrays.asList("DP", "Hadoken"));
COMBO_GRAPH.put("DP", Arrays.asList("Hadoken"));
COMBO_GRAPH.put("jHP", Arrays.asList("5LP", "2LK", "5HP"));
}
// ==================== AI决策器 ====================
static class KofAI {
/**
* 主决策入口:每帧调用一次
* @param self 己方角色
* @param enemy 敌方角色
* @return 本帧输入指令
*/
String decide(Fighter self, Fighter enemy) {
int distance = Math.abs(self.x - enemy.x);
// 1. 防御优先:如果对手处于活跃帧且攻击能打到我们
if (isEnemyAttacking(enemy) && willHit(enemy, self)) {
if (enemy.currentMove.isLow && self.state != State.CROUCH) {
return "CROUCH_BLOCK"; // 下段必须蹲防
}
if (enemy.currentMove.isOverhead && self.state == State.CROUCH) {
return "STAND_BLOCK"; // 中段必须站防
}
return "BLOCK";
}
// 2. 惩罚对手收招硬直
if (enemy.state == State.ATTACK_RECOVERY && distance <= 55) {
// 贪心选择伤害最高的可命中招式
String punish = selectBestPunish(self, enemy, distance);
if (punish != null) return punish;
}
// 3. 连招延续:如果当前招式命中,查找下一招
if (self.currentMove != null && self.state == State.ATTACK_RECOVERY) {
int fa = self.currentMove.frameAdvantageOnHit();
if (fa >= 0) { // 有帧优势才能连
String next = selectComboFollowup(self, enemy, distance, fa);
if (next != null) return next;
}
}
// 4. 起攻:根据距离选择最优起手
if (self.isIdle()) {
return selectOpening(self, enemy, distance);
}
// 5. 默认保持防御或待机
return "NEUTRAL";
}
// 判断敌方是否处于攻击活跃帧
boolean isEnemyAttacking(Fighter enemy) {
return enemy.state == State.ATTACK_ACTIVE;
}
// 简化判定:攻击距离是否覆盖双方间距
boolean willHit(Fighter attacker, Fighter defender) {
int dist = Math.abs(attacker.x - defender.x);
return dist <= attacker.currentMove.range;
}
// 贪心选择惩罚招式:在对手硬直内,选伤害/帧数比最高的招式
String selectBestPunish(Fighter self, Fighter enemy, int distance) {
String best = null;
double bestScore = -1;
for (Map.Entry<String, MoveFrameData> e : MOVE_DB.entrySet()) {
MoveFrameData m = e.getValue();
if (m.range < distance) continue; // 打不到
if (m.startupFrames > enemy.stateTimer) continue; // 出手太慢
double score = (double) m.damage / m.totalFrames();
if (score > bestScore) {
bestScore = score;
best = e.getKey();
}
}
return best;
}
// 选择连招下一招:在帧优势允许的情况下,最大化期望伤害
String selectComboFollowup(Fighter self, Fighter enemy, int distance, int frameAdvantage) {
List<String> candidates = COMBO_GRAPH.getOrDefault(self.currentMove.name, Collections.emptyList());
String best = null;
int bestDmg = -1;
for (String moveName : candidates) {
MoveFrameData m = MOVE_DB.get(moveName);
if (m == null) continue;
if (m.range < distance) continue;
// 需要启动帧 ≤ 帧优势 + 对手受击硬直(简化判定)
if (m.startupFrames > frameAdvantage + 6) continue;
if (m.damage > bestDmg) {
bestDmg = m.damage;
best = moveName;
}
}
return best;
}
// 起攻选择:近距离轻拳压制,中距离波动拳,远距离前冲
String selectOpening(Fighter self, Fighter enemy, int distance) {
if (distance <= 40) return "5LP"; // 轻拳起攻
if (distance <= 100) return "2LK"; // 蹲下轻脚起攻
if (distance <= 180) return "Hadoken"; // 波动拳牵制
return "FORWARD_DASH"; // 前冲拉近距离
}
}
// ==================== 主循环与模拟器 ====================
static class BattleSimulator {
Fighter p1 = new Fighter("Kyo", 100);
Fighter p2 = new Fighter("Iori", 300);
KofAI ai = new KofAI();
int frame = 0;
void runOneFrame() {
frame++;
// AI决策
String cmd1 = ai.decide(p1, p2);
String cmd2 = ai.decide(p2, p1); // 简单镜像AI
// 执行指令(简化版)
applyCommand(p1, cmd1, p2);
applyCommand(p2, cmd2, p1);
// 更新状态机
updateState(p1);
updateState(p2);
// 命中检测
checkHit(p1, p2);
checkHit(p2, p1);
}
void applyCommand(Fighter f, String cmd, Fighter enemy) {
if (!f.isIdle() && !f.isInHitstun()) return; // 非可控状态忽略输入
switch (cmd) {
case "5LP": case "5HP": case "2LK": case "DP": case "Hadoken":
f.currentMove = MOVE_DB.get(cmd);
f.state = State.ATTACK_STARTUP;
f.stateTimer = f.currentMove.startupFrames;
break;
case "CROUCH_BLOCK":
f.state = State.CROUCH;
break;
case "STAND_BLOCK":
case "BLOCK":
f.state = State.IDLE; // 简化:防御即站立
break;
case "FORWARD_DASH":
f.x += (f.facingRight ? 1 : -1) * 15;
break;
}
}
void updateState(Fighter f) {
if (f.stateTimer > 0) f.stateTimer--;
if (f.stateTimer == 0) {
switch (f.state) {
case ATTACK_STARTUP:
f.state = State.ATTACK_ACTIVE;
f.stateTimer = f.currentMove.activeFrames;
break;
case ATTACK_ACTIVE:
f.state = State.ATTACK_RECOVERY;
f.stateTimer = f.currentMove.recoveryFrames;
break;
case ATTACK_RECOVERY:
case HITSTUN:
case BLOCKSTUN:
f.state = State.IDLE;
f.currentMove = null;
break;
case JUMP_START:
f.state = State.JUMP_UP;
f.stateTimer = 20;
break;
case JUMP_UP:
f.state = State.JUMP_DOWN;
f.stateTimer = 20;
break;
case JUMP_DOWN:
f.state = State.IDLE;
f.y = 0;
break;
default:
break;
}
}
}
void checkHit(Fighter attacker, Fighter defender) {
if (attacker.state != State.ATTACK_ACTIVE) return;
if (defender.state == State.HITSTUN || defender.state == State.BLOCKSTUN) return;
int dist = Math.abs(attacker.x - defender.x);
if (dist > attacker.currentMove.range) return;
// 简化防御判定
boolean blocking = (attacker.currentMove.isLow && defender.state == State.CROUCH)
|| (!attacker.currentMove.isLow && defender.state == State.IDLE);
if (blocking) {
defender.state = State.BLOCKSTUN;
defender.stateTimer = attacker.currentMove.blockstunFrames;
System.out.printf("[%s] %s 防御了 %s%n", attacker.name, defender.name, attacker.currentMove.name);
} else {
defender.hp -= attacker.currentMove.damage;
defender.state = State.HITSTUN;
defender.stateTimer = attacker.currentMove.hitstunFrames;
System.out.printf("[%s] %s 命中 %s,造成 %d 伤害,敌方剩余 HP=%d%n",
attacker.name, attacker.currentMove.name, defender.name,
attacker.currentMove.damage, defender.hp);
}
}
}
public static void main(String[] args) {
BattleSimulator sim = new BattleSimulator();
System.out.println("=== 拳皇AI对战模拟开始 ===");
for (int i = 0; i < 300; i++) {
sim.runOneFrame();
if (sim.p1.hp <= 0 || sim.p2.hp <= 0) break;
}
System.out.println("=== 对战结束 ===");
System.out.printf("P1 %s HP=%d%n", sim.p1.name, sim.p1.hp);
System.out.printf("P2 %s HP=%d%n", sim.p2.name, sim.p2.hp);
}
}
复杂度分析
| 模块 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 状态机更新 | O(1) | O(1) | 每帧固定状态转移 |
| AI决策(贪心) | O(M) | O(1) | M为招式库大小,本例M=6 |
| 命中检测 | O(1) | O(1) | 双角色两两判定 |
| 连招图查询 | O(C) | O(C) | C为单节点出边数 |
实战中对战双方各6个招式,每帧AI决策仅需数十次整数比较,在现代CPU上可轻松达到60FPS的帧级决策。
扩展方向
- 深度学习AI:用神经网络替代贪心决策,通过自我对弈训练策略网络;
- 帧精准输入:引入输入缓冲(Input Buffer)与指令预读,实现1帧目押;
- 完整碰撞系统:用AABB或OBB替代距离判定,支持蹲下、跳跃的不同受击框。
拳皇的竞技深度源于”帧”级别的信息差。通过状态机+帧数据+贪心决策的三层架构,我们搭建了一个可扩展的格斗AI原型,读者可在此基础上继续探索更复杂的博弈策略。