引言
魂斗罗(Contra)是经典的横版卷轴射击游戏,玩家控制角色在关卡中前进并消灭敌人。本文用Java实现魂斗罗风格的敌人AI系统,核心算法为有限状态机(Finite State Machine, FSM),配合行为树分层决策,让敌人能够根据玩家位置、距离和自身状态做出巡逻、警戒、追击、攻击、撤退等智能行为。
游戏场景与实体定义
游戏场景为横向滚动的2D平面。核心实体包括玩家(Player)和多种敌人(Enemy)。敌人AI需要感知玩家位置、地形障碍、友军状态等信息,并选择合适的行动策略。
核心算法设计
1. 有限状态机(FSM)
每个敌人有明确的状态集合和转移条件:
- 巡逻(Patrol):在固定区域内来回移动
- 警戒(Alert):发现玩家但距离较远,保持观察
- 追击(Chase):主动向玩家靠近
- 攻击(Attack):进入射击范围,进行攻击
- 撤退(Retreat):生命值过低,寻找掩护
- 死亡(Dead):生命值归零
状态转移由感知条件触发,如玩家距离、视野检测、生命值变化等。
2. A*寻路
在追击和撤退状态下,敌人需要绕过障碍物接近或远离玩家。使用A*算法在网格地图上搜索最短路径。
3. 行为分层
将简单FSM与行为树结合:高层用FSM确定总体策略,低层用行为树实现具体动作序列(如”移动-瞄准-射击-躲避”)。
Java完整实现
import java.util.*;
/**
* 魂斗罗敌人AI系统
* 实现有限状态机 + A*寻路 + 分层行为决策
*/
public class ContraEnemyAI {
// 游戏地图尺寸
public static final int MAP_WIDTH = 100;
public static final int MAP_HEIGHT = 20;
// 感知参数
public static final double VIEW_DISTANCE = 15.0;
public static final double ATTACK_RANGE = 8.0;
public static final double RETREAT_HP_RATIO = 0.3;
private char[][] map; // 地图: '.'空地 '#'障碍
private Player player;
private List<Enemy> enemies;
public ContraEnemyAI() {
this.map = new char[MAP_HEIGHT][MAP_WIDTH];
initMap();
this.player = new Player(10, 10);
this.enemies = new ArrayList<>();
enemies.add(new Enemy(30, 10, EnemyType.SOLDIER));
enemies.add(new Enemy(50, 12, EnemyType.SNIPER));
enemies.add(new Enemy(70, 8, EnemyType.TURRET));
}
void initMap() {
for (int y = 0; y < MAP_HEIGHT; y++) {
Arrays.fill(map[y], '.');
}
// 生成一些障碍物
for (int x = 20; x < 25; x++) map[15][x] = '#';
for (int x = 40; x < 45; x++) map[12][x] = '#';
for (int x = 60; x < 70; x++) map[14][x] = '#';
}
enum EnemyType { SOLDIER, SNIPER, TURRET }
enum State { PATROL, ALERT, CHASE, ATTACK, RETREAT, DEAD }
static class Player {
int x, y;
int hp = 100;
Player(int x, int y) { this.x = x; this.y = y; }
}
static class Enemy {
int x, y; // 当前位置
int hp, maxHp; // 生命值
EnemyType type; // 敌人类型
State state; // 当前状态
int patrolStart, patrolEnd; // 巡逻范围
int patrolDir = 1; // 巡逻方向
List<Node> path; // 当前路径
int pathIndex; // 路径索引
int attackCooldown; // 攻击冷却
int stateTimer; // 状态持续时间
public Enemy(int x, int y, EnemyType type) {
this.x = x; this.y = y; this.type = type;
this.maxHp = type == EnemyType.SOLDIER ? 50 :
type == EnemyType.SNIPER ? 30 : 80;
this.hp = maxHp;
this.state = State.PATROL;
this.patrolStart = x - 5;
this.patrolEnd = x + 5;
this.path = new ArrayList<>();
}
}
/**
* 每帧更新所有敌人AI
*/
public void update() {
for (Enemy e : enemies) {
if (e.state == State.DEAD) continue;
senseAndDecide(e);
executeBehavior(e);
}
}
/**
* 感知环境并决定状态转移
*/
private void senseAndDecide(Enemy e) {
double dist = Math.sqrt(Math.pow(e.x - player.x, 2)
+ Math.pow(e.y - player.y, 2));
boolean canSee = hasLineOfSight(e.x, e.y, player.x, player.y);
double hpRatio = (double) e.hp / e.maxHp;
State newState = e.state;
switch (e.state) {
case PATROL:
if (canSee && dist < VIEW_DISTANCE) {
newState = dist < ATTACK_RANGE ? State.ATTACK : State.ALERT;
}
break;
case ALERT:
if (!canSee || dist > VIEW_DISTANCE * 1.2) {
newState = State.PATROL;
} else if (dist < ATTACK_RANGE) {
newState = State.ATTACK;
} else if (e.stateTimer > 30) { // 警戒3秒后追击
newState = State.CHASE;
}
break;
case CHASE:
if (!canSee || dist > VIEW_DISTANCE * 1.5) {
newState = State.PATROL;
} else if (dist < ATTACK_RANGE) {
newState = State.ATTACK;
} else if (hpRatio < RETREAT_HP_RATIO) {
newState = State.RETREAT;
}
break;
case ATTACK:
if (dist > ATTACK_RANGE * 1.2) {
newState = canSee ? State.CHASE : State.ALERT;
} else if (hpRatio < RETREAT_HP_RATIO) {
newState = State.RETREAT;
}
break;
case RETREAT:
if (dist > VIEW_DISTANCE || hpRatio > 0.6) {
newState = State.PATROL;
} else if (canSee && dist < ATTACK_RANGE && hpRatio > 0.5) {
newState = State.ATTACK;
}
break;
}
if (newState != e.state) {
e.state = newState;
e.stateTimer = 0;
e.path.clear();
System.out.printf("敌人(%d,%d) 状态切换 -> %s%n", e.x, e.y, newState);
} else {
e.stateTimer++;
}
}
/**
* 执行当前状态对应的行为
*/
private void executeBehavior(Enemy e) {
switch (e.state) {
case PATROL:
doPatrol(e);
break;
case ALERT:
doAlert(e);
break;
case CHASE:
doChase(e);
break;
case ATTACK:
doAttack(e);
break;
case RETREAT:
doRetreat(e);
break;
}
if (e.attackCooldown > 0) e.attackCooldown--;
}
/**
* 巡逻行为:在固定范围内来回移动
*/
private void doPatrol(Enemy e) {
if (e.x >= e.patrolEnd) e.patrolDir = -1;
if (e.x <= e.patrolStart) e.patrolDir = 1;
int nx = e.x + e.patrolDir;
if (isWalkable(nx, e.y)) {
e.x = nx;
} else {
e.patrolDir *= -1;
}
}
/**
* 警戒行为:面向玩家但保持原地
*/
private void doAlert(Enemy e) {
// 警戒状态下原地待命,偶尔微调位置
if (e.stateTimer % 20 == 0) {
int dx = Integer.compare(player.x, e.x);
int nx = e.x + dx;
if (isWalkable(nx, e.y)) e.x = nx;
}
}
/**
* 追击行为:用A*寻路接近玩家
*/
private void doChase(Enemy e) {
if (e.path.isEmpty() || e.pathIndex >= e.path.size()
|| e.stateTimer % 10 == 0) {
e.path = aStar(e.x, e.y, player.x, player.y);
e.pathIndex = 0;
}
if (e.pathIndex < e.path.size()) {
Node next = e.path.get(e.pathIndex++);
if (isWalkable(next.x, next.y)) {
e.x = next.x;
e.y = next.y;
}
}
}
/**
* 攻击行为:瞄准并射击
*/
private void doAttack(Enemy e) {
// 根据类型调整射击策略
if (e.attackCooldown <= 0) {
fireAtPlayer(e);
e.attackCooldown = e.type == EnemyType.SNIPER ? 20 :
e.type == EnemyType.TURRET ? 5 : 10;
}
// 攻击时小幅移动,避免成为固定靶
if (e.stateTimer % 15 == 0) {
int dy = (e.stateTimer / 15) % 2 == 0 ? -1 : 1;
if (isWalkable(e.x, e.y + dy)) e.y += dy;
}
}
/**
* 撤退行为:远离玩家并寻找掩护
*/
private void doRetreat(Enemy e) {
// 寻找远离玩家的方向
int dx = Integer.compare(e.x, player.x);
int bestX = e.x, bestY = e.y;
double bestDist = 0;
for (int[] dir : new int[][]{{dx,0},{dx,-1},{dx,1},{0,-1},{0,1}}) {
int nx = e.x + dir[0];
int ny = e.y + dir[1];
if (!isWalkable(nx, ny)) continue;
double d = Math.sqrt(Math.pow(nx - player.x, 2)
+ Math.pow(ny - player.y, 2));
if (d > bestDist) {
bestDist = d;
bestX = nx; bestY = ny;
}
}
e.x = bestX; e.y = bestY;
}
/**
* A*寻路算法
*/
private List<Node> aStar(int sx, int sy, int tx, int ty) {
PriorityQueue<Node> open = new PriorityQueue<>(
Comparator.comparingInt(n -> n.f));
boolean[][] closed = new boolean[MAP_HEIGHT][MAP_WIDTH];
Node[][] parent = new Node[MAP_HEIGHT][MAP_WIDTH];
Node start = new Node(sx, sy, 0,
Math.abs(tx - sx) + Math.abs(ty - sy));
open.add(start);
while (!open.isEmpty()) {
Node cur = open.poll();
if (cur.x == tx && cur.y == ty) break;
if (closed[cur.y][cur.x]) continue;
closed[cur.y][cur.x] = true;
for (int[] d : new int[][]{{0,1},{0,-1},{1,0},{-1,0}}) {
int nx = cur.x + d[0], ny = cur.y + d[1];
if (nx < 0 || nx >= MAP_WIDTH || ny < 0 || ny >= MAP_HEIGHT)
continue;
if (closed[ny][nx] || !isWalkable(nx, ny)) continue;
int g = cur.g + 1;
int h = Math.abs(tx - nx) + Math.abs(ty - ny);
Node neighbor = new Node(nx, ny, g, h);
if (parent[ny][nx] == null || g < parent[ny][nx].g) {
parent[ny][nx] = cur;
open.add(neighbor);
}
}
}
// 回溯路径
List<Node> path = new ArrayList<>();
Node cur = parent[ty][tx];
while (cur != null && !(cur.x == sx && cur.y == sy)) {
path.add(0, cur);
cur = parent[cur.y][cur.x];
}
return path;
}
static class Node {
int x, y, f, g;
Node(int x, int y, int g, int h) {
this.x = x; this.y = y; this.g = g; this.f = g + h;
}
}
/**
* 视线检测:Bresenham直线算法检查视线是否被阻挡
*/
private boolean hasLineOfSight(int x0, int y0, int x1, int y1) {
int dx = Math.abs(x1 - x0), dy = Math.abs(y1 - y0);
int sx = x0 < x1 ? 1 : -1;
int sy = y0 < y1 ? 1 : -1;
int err = dx - dy;
while (true) {
if (map[y0][x0] == '#') return false;
if (x0 == x1 && y0 == y1) break;
int e2 = 2 * err;
if (e2 > -dy) { err -= dy; x0 += sx; }
if (e2 < dx) { err += dx; y0 += sy; }
}
return true;
}
private boolean isWalkable(int x, int y) {
return x >= 0 && x < MAP_WIDTH && y >= 0 && y < MAP_HEIGHT
&& map[y][x] != '#';
}
private void fireAtPlayer(Enemy e) {
double hitChance = e.type == EnemyType.SNIPER ? 0.9 :
e.type == EnemyType.TURRET ? 0.7 : 0.5;
if (Math.random() < hitChance) {
player.hp -= e.type == EnemyType.TURRET ? 15 :
e.type == EnemyType.SNIPER ? 25 : 10;
System.out.printf("敌人(%s)击中玩家!玩家HP:%d%n", e.type, player.hp);
} else {
System.out.printf("敌人(%s)射击未命中%n", e.type);
}
}
public void printMap() {
char[][] display = new char[MAP_HEIGHT][MAP_WIDTH];
for (int y = 0; y < MAP_HEIGHT; y++) {
System.arraycopy(map[y], 0, display[y], 0, MAP_WIDTH);
}
display[player.y][player.x] = 'P';
for (Enemy e : enemies) {
if (e.state != State.DEAD) {
display[e.y][e.x] = e.type == EnemyType.SOLDIER ? 'S' :
e.type == EnemyType.SNIPER ? 'N' : 'T';
}
}
for (int y = 0; y < MAP_HEIGHT; y++) {
System.out.println(new String(display[y]));
}
System.out.println();
}
public static void main(String[] args) throws InterruptedException {
ContraEnemyAI game = new ContraEnemyAI();
System.out.println("=== 魂斗罗敌人AI模拟 ===");
game.printMap();
for (int i = 0; i < 50; i++) {
// 模拟玩家向右移动
if (i % 3 == 0 && game.isWalkable(game.player.x + 1, game.player.y)) {
game.player.x++;
}
game.update();
if (i % 5 == 0) game.printMap();
if (game.player.hp <= 0) {
System.out.println("玩家阵亡!");
break;
}
Thread.sleep(200);
}
}
}
算法亮点解析
状态机设计的层次性
FSM将复杂AI行为分解为离散状态,每个状态职责单一,转移条件明确。这种设计让AI行为可预测、可调试,也方便后续扩展新状态(如”呼叫支援”、”投掷手雷”等)。
A*与FSM的协作
A*算法仅在需要路径规划时调用(CHASE和RETREAT状态),避免了每帧都进行全图搜索的开销。路径缓存机制(path + pathIndex)进一步减少重复计算。
差异化敌人类型
三种敌人类型共享同一FSM框架,但通过不同参数(生命值、射击冷却、命中率)体现差异化:狙击手精准但脆弱,炮塔笨重但火力强,普通士兵均衡。这种设计模式便于扩展更多敌人类型。
复杂度分析
- 时间复杂度:每帧O(E × (A + LOS)),其中E为敌人数量。A最坏O(W×H log(W×H)),W、H为地图尺寸;LOS检测为O(max(W,H))。实际运行中由于路径缓存,平均开销远低于理论上限。
- 空间复杂度:O(W×H)用于A*的辅助数组。
扩展方向
- 行为树:将FSM的每个状态替换为子行为树,实现更细粒度的动作编排
- 群体AI:引入 flocking 算法,让多个敌人形成包抄阵型
- 学习机制:用Q-Learning让敌人从与玩家的交战中学习最优策略
- 分层路径规划:高层用导航网格(NavMesh)规划粗略路径,低层用局部避障处理动态障碍
总结
魂斗罗敌人AI展示了有限状态机在实时游戏中的应用价值。通过清晰的状态划分和转移条件,配合A*寻路和视线检测,敌人能够表现出巡逻、警戒、追击、攻击、撤退等多种战术行为,为横版射击游戏提供了可扩展的AI框架。