引言
合金弹头(Metal Slug)是经典的横版射击游戏,其敌人AI系统虽然看似简单,却蕴含了丰富的算法设计:敌人需要根据玩家位置决策巡逻、追击、攻击和躲避,在复杂地形中寻找可行路径。本文将用Java实现一个简化版的合金弹头敌人AI系统,深入讲解有限状态机(FSM)的行为控制、A*寻路在平台地形中的应用,以及视线检测与射击决策算法。
一、游戏世界建模
1.1 地图与地形表示
使用二维网格表示游戏世界,每个格子可以是空地、墙壁、平台或梯子。
1.2 实体系统设计
游戏中有两类主要实体:玩家(Player)和敌人(Enemy)。敌人AI需要实时感知玩家位置、地形信息和自身状态,做出合理的行动决策。
二、Java核心实现
import java.util.*;
/**
* 地形类型枚举
*/
enum TerrainType {
EMPTY(0), // 空地
WALL(1), // 墙壁(不可通过)
PLATFORM(2),// 平台(可站立)
LADDER(3), // 梯子(可攀爬)
WATER(4); // 水域(减速)
final int value;
TerrainType(int value) { this.value = value; }
}
/**
* 游戏世界坐标点
*/
class GamePoint {
int x, y;
public GamePoint(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof GamePoint)) return false;
GamePoint p = (GamePoint) o;
return x == p.x && y == p.y;
}
@Override
public int hashCode() {
return x * 31 + y;
}
@Override
public String toString() {
return "(" + x + "," + y + ")";
}
public int distanceTo(GamePoint other) {
return Math.abs(x - other.x) + Math.abs(y - other.y);
}
}
/**
* 游戏地图类
*/
public class GameMap {
private final int width;
private final int height;
private final TerrainType[][] grid;
public GameMap(int width, int height) {
this.width = width;
this.height = height;
this.grid = new TerrainType[height][width];
// 默认全部为空地
for (int y = 0; y < height; y++) {
Arrays.fill(grid[y], TerrainType.EMPTY);
}
}
public void setTerrain(int x, int y, TerrainType type) {
if (isValid(x, y)) {
grid[y][x] = type;
}
}
public TerrainType getTerrain(int x, int y) {
if (!isValid(x, y)) return TerrainType.WALL;
return grid[y][x];
}
public boolean isValid(int x, int y) {
return x >= 0 && x < width && y >= 0 && y < height;
}
/**
* 检查点是否可通过(敌人可站立的位置)
* 规则:下方必须有平台或墙壁支撑
*/
public boolean isWalkable(int x, int y) {
if (!isValid(x, y)) return false;
if (grid[y][x] == TerrainType.WALL) return false;
// 下方必须有支撑(平台或墙壁)
if (y + 1 < height && (grid[y + 1][x] == TerrainType.PLATFORM ||
grid[y + 1][x] == TerrainType.WALL)) {
return true;
}
// 或者在梯子上
return grid[y][x] == TerrainType.LADDER;
}
/**
* 检查是否可以跳跃到达(简化:检查目标下方是否有支撑)
*/
public boolean isJumpLanding(int x, int y) {
return isValid(x, y) && isValid(x, y + 1) &&
(grid[y + 1][x] == TerrainType.PLATFORM || grid[y + 1][x] == TerrainType.WALL);
}
public int getWidth() { return width; }
public int getHeight() { return height; }
/**
* 打印地图(调试用)
*/
public void print() {
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
switch (grid[y][x]) {
case WALL: System.out.print("#"); break;
case PLATFORM: System.out.print("="); break;
case LADDER: System.out.print("H"); break;
case WATER: System.out.print("~"); break;
default: System.out.print("."); break;
}
}
System.out.println();
}
}
}
/**
* 玩家类
*/
class Player {
GamePoint position;
int health;
boolean isAlive;
public Player(int x, int y) {
this.position = new GamePoint(x, y);
this.health = 100;
this.isAlive = true;
}
public void moveTo(int x, int y) {
position.x = x;
position.y = y;
}
}
/**
* 敌人AI状态枚举
*/
enum EnemyState {
IDLE("待机"), // 原地巡逻
PATROL("巡逻"), // 在区域内移动
ALERT("警戒"), // 发现异常,准备搜索
CHASE("追击"), // 追踪玩家
ATTACK("攻击"), // 在射程内射击
RETREAT("撤退"), // 血量低时撤退
DEAD("死亡"); // 死亡状态
final String description;
EnemyState(String desc) { this.description = desc; }
}
/**
* 敌人AI类
* 使用有限状态机控制行为
*/
public class EnemyAI {
private GamePoint position;
private int health;
private EnemyState currentState;
private final GameMap map;
private final Player player;
// AI参数
private final int viewRange; // 视野范围
private final int attackRange; // 攻击范围
private final int patrolRange; // 巡逻范围
private final GamePoint spawnPoint; // 出生点
// 状态机相关
private int stateTimer; // 状态持续时间计时器
private GamePoint patrolTarget; // 巡逻目标点
private List<GamePoint> path; // 当前路径
private int pathIndex; // 路径当前索引
private int lastPlayerX; // 上次看到玩家的位置
private int lastPlayerY;
public EnemyAI(int x, int y, GameMap map, Player player,
int viewRange, int attackRange, int patrolRange) {
this.position = new GamePoint(x, y);
this.spawnPoint = new GamePoint(x, y);
this.health = 100;
this.currentState = EnemyState.IDLE;
this.map = map;
this.player = player;
this.viewRange = viewRange;
this.attackRange = attackRange;
this.patrolRange = patrolRange;
this.stateTimer = 0;
this.path = new ArrayList<>();
this.pathIndex = 0;
this.lastPlayerX = -1;
this.lastPlayerY = -1;
}
/**
* 每帧更新AI状态和行为
*/
public void update() {
if (currentState == EnemyState.DEAD) return;
stateTimer++;
// 状态转换决策
EnemyState nextState = decideNextState();
if (nextState != currentState) {
onStateExit(currentState);
currentState = nextState;
onStateEnter(currentState);
}
// 执行当前状态行为
executeCurrentState();
}
/**
* 决策下一状态(状态机核心)
*/
private EnemyState decideNextState() {
if (health <= 0) return EnemyState.DEAD;
if (health < 30 && currentState != EnemyState.RETREAT) return EnemyState.RETREAT;
boolean canSeePlayer = canSeePlayer();
int distToPlayer = position.distanceTo(player.position);
switch (currentState) {
case IDLE:
if (canSeePlayer) return EnemyState.CHASE;
if (stateTimer > 60) return EnemyState.PATROL; // 2秒后切换到巡逻
return EnemyState.IDLE;
case PATROL:
if (canSeePlayer) return EnemyState.CHASE;
if (stateTimer > 180) return EnemyState.IDLE; // 6秒后休息
return EnemyState.PATROL;
case ALERT:
if (canSeePlayer) return EnemyState.CHASE;
if (stateTimer > 120) return EnemyState.PATROL; // 4秒后放弃搜索
return EnemyState.ALERT;
case CHASE:
if (!canSeePlayer && stateTimer > 90) return EnemyState.ALERT;
if (distToPlayer <= attackRange) return EnemyState.ATTACK;
return EnemyState.CHASE;
case ATTACK:
if (!canSeePlayer) return EnemyState.ALERT;
if (distToPlayer > attackRange) return EnemyState.CHASE;
return EnemyState.ATTACK;
case RETREAT:
if (health > 50) return EnemyState.PATROL;
return EnemyState.RETREAT;
default:
return currentState;
}
}
/**
* 进入状态时的初始化
*/
private void onStateEnter(EnemyState state) {
stateTimer = 0;
path.clear();
pathIndex = 0;
switch (state) {
case PATROL:
// 随机选择巡逻目标
patrolTarget = generatePatrolTarget();
path = findPath(position, patrolTarget);
break;
case CHASE:
// 记录玩家位置并开始追击
lastPlayerX = player.position.x;
lastPlayerY = player.position.y;
path = findPath(position, player.position);
break;
case ALERT:
// 移动到上次看到玩家的位置
if (lastPlayerX >= 0) {
path = findPath(position, new GamePoint(lastPlayerX, lastPlayerY));
}
break;
case RETREAT:
// 撤退到出生点
path = findPath(position, spawnPoint);
break;
default:
break;
}
}
private void onStateExit(EnemyState state) {
// 状态退出时的清理工作
}
/**
* 执行当前状态的具体行为
*/
private void executeCurrentState() {
switch (currentState) {
case IDLE:
// 待机:原地左右张望
break;
case PATROL:
followPath();
// 到达目标后选择新目标
if (pathIndex >= path.size()) {
patrolTarget = generatePatrolTarget();
path = findPath(position, patrolTarget);
pathIndex = 0;
}
break;
case CHASE:
// 每30帧重新计算路径(玩家可能移动)
if (stateTimer % 30 == 0) {
path = findPath(position, player.position);
pathIndex = 0;
}
followPath();
break;
case ATTACK:
// 攻击玩家
performAttack();
break;
case RETREAT:
followPath();
break;
default:
break;
}
}
/**
* 沿当前路径移动
*/
private void followPath() {
if (path == null || pathIndex >= path.size()) return;
GamePoint nextStep = path.get(pathIndex);
if (canMoveTo(nextStep.x, nextStep.y)) {
position.x = nextStep.x;
position.y = nextStep.y;
pathIndex++;
}
}
/**
* 检查是否可以移动到目标位置
*/
private boolean canMoveTo(int x, int y) {
return map.isWalkable(x, y);
}
/**
* 执行攻击
*/
private void performAttack() {
// 简化的攻击逻辑:检查射击线是否通畅
if (hasClearShot()) {
System.out.println("Enemy at " + position + " attacks Player at " + player.position);
// 实际游戏中这里会减少玩家血量
}
}
/**
* 视线检测:使用Bresenham直线算法检查是否能直接看到玩家
*/
private boolean canSeePlayer() {
int dist = position.distanceTo(player.position);
if (dist > viewRange) return false;
return hasLineOfSight(position.x, position.y, player.position.x, player.position.y);
}
/**
* Bresenham直线算法检测视线
*/
private boolean hasLineOfSight(int x0, int y0, int x1, int y1) {
int dx = Math.abs(x1 - x0);
int 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.getTerrain(x0, y0) == TerrainType.WALL) {
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 hasClearShot() {
return hasLineOfSight(position.x, position.y, player.position.x, player.position.y);
}
/**
* 生成随机巡逻目标
*/
private GamePoint generatePatrolTarget() {
Random rand = new Random();
int attempts = 0;
while (attempts < 20) {
int nx = spawnPoint.x + rand.nextInt(patrolRange * 2 + 1) - patrolRange;
int ny = spawnPoint.y + rand.nextInt(patrolRange * 2 + 1) - patrolRange;
if (map.isWalkable(nx, ny)) {
return new GamePoint(nx, ny);
}
attempts++;
}
return spawnPoint;
}
/**
* A*寻路算法
*/
private List<GamePoint> findPath(GamePoint start, GamePoint goal) {
PriorityQueue<AStarNode2> openSet = new PriorityQueue<>();
Set<GamePoint> closedSet = new HashSet<>();
Map<GamePoint, Double> gScore = new HashMap<>();
openSet.add(new AStarNode2(start, 0, heuristic(start, goal), null));
gScore.put(start, 0.0);
// 四方向 + 下方向(掉落)+ 上方向(跳跃)
int[][] directions = {{0, 1}, {0, -1}, {-1, 0}, {1, 0}};
while (!openSet.isEmpty()) {
AStarNode2 current = openSet.poll();
if (current.point.equals(goal)) {
return reconstructPath(current);
}
closedSet.add(current.point);
for (int[] dir : directions) {
int nx = current.point.x + dir[0];
int ny = current.point.y + dir[1];
GamePoint neighbor = new GamePoint(nx, ny);
if (closedSet.contains(neighbor)) continue;
if (!canMoveTo(nx, ny)) continue;
double tentativeG = current.g + 1;
if (!gScore.containsKey(neighbor) || tentativeG < gScore.get(neighbor)) {
gScore.put(neighbor, tentativeG);
double h = heuristic(neighbor, goal);
openSet.add(new AStarNode2(neighbor, tentativeG, h, current));
}
}
}
return new ArrayList<>(); // 无路径
}
private double heuristic(GamePoint a, GamePoint b) {
return Math.abs(a.x - b.x) + Math.abs(a.y - b.y);
}
private List<GamePoint> reconstructPath(AStarNode2 endNode) {
List<GamePoint> path = new ArrayList<>();
AStarNode2 current = endNode;
while (current != null) {
path.add(current.point);
current = current.parent;
}
Collections.reverse(path);
return path;
}
// Getters
public GamePoint getPosition() { return position; }
public EnemyState getState() { return currentState; }
public int getHealth() { return health; }
public void takeDamage(int dmg) { health -= dmg; }
}
/**
* A*寻路节点(内部类)
*/
class AStarNode2 implements Comparable<AStarNode2> {
GamePoint point;
double g, h;
AStarNode2 parent;
AStarNode2(GamePoint point, double g, double h, AStarNode2 parent) {
this.point = point;
this.g = g;
this.h = h;
this.parent = parent;
}
double getF() { return g + h; }
@Override
public int compareTo(AStarNode2 other) {
return Double.compare(this.getF(), other.getF());
}
}
三、游戏主循环与测试
/**
* 合金弹头AI演示主程序
*/
public class MetalSlugAIApp {
public static void main(String[] args) {
// 创建地图
GameMap map = new GameMap(20, 10);
// 构建平台地形
for (int x = 0; x < 20; x++) {
map.setTerrain(x, 9, TerrainType.PLATFORM); // 地面
}
// 中层平台
for (int x = 3; x < 8; x++) map.setTerrain(x, 5, TerrainType.PLATFORM);
for (int x = 12; x < 17; x++) map.setTerrain(x, 5, TerrainType.PLATFORM);
// 顶层平台
for (int x = 6; x < 14; x++) map.setTerrain(x, 2, TerrainType.PLATFORM);
// 梯子
map.setTerrain(5, 6, TerrainType.LADDER);
map.setTerrain(5, 7, TerrainType.LADDER);
map.setTerrain(5, 8, TerrainType.LADDER);
map.setTerrain(13, 3, TerrainType.LADDER);
map.setTerrain(13, 4, TerrainType.LADDER);
map.setTerrain(13, 5, TerrainType.LADDER);
map.setTerrain(13, 6, TerrainType.LADDER);
map.setTerrain(13, 7, TerrainType.LADDER);
map.setTerrain(13, 8, TerrainType.LADDER);
System.out.println("游戏地图:");
map.print();
// 创建玩家和敌人
Player player = new Player(2, 8);
EnemyAI enemy = new EnemyAI(17, 8, map, player, 10, 3, 5);
System.out.println("\n===== AI行为模拟 =====");
System.out.println("初始状态:");
System.out.println(" 玩家位置: " + player.position);
System.out.println(" 敌人位置: " + enemy.getPosition());
System.out.println(" 敌人状态: " + enemy.getState().description);
// 模拟游戏循环
System.out.println("\n模拟60帧游戏循环:");
for (int frame = 0; frame < 60; frame++) {
// 玩家移动(模拟)
if (frame == 15) {
player.moveTo(8, 8);
System.out.println("\n[第" + frame + "帧] 玩家移动到 " + player.position);
}
if (frame == 30) {
player.moveTo(10, 4);
System.out.println("\n[第" + frame + "帧] 玩家移动到 " + player.position);
}
if (frame == 45) {
player.moveTo(5, 4);
System.out.println("\n[第" + frame + "帧] 玩家移动到 " + player.position);
}
// 更新敌人AI
EnemyState prevState = enemy.getState();
enemy.update();
// 状态变化时输出信息
if (enemy.getState() != prevState || frame % 15 == 0) {
System.out.println("[第" + frame + "帧] 敌人状态: " + enemy.getState().description +
", 位置: " + enemy.getPosition());
}
}
// 测试视线检测
System.out.println("\n===== 视线检测测试 =====");
player.moveTo(10, 8);
enemy = new EnemyAI(15, 8, map, player, 10, 3, 5);
System.out.println("玩家: " + player.position + ", 敌人: " + enemy.getPosition());
System.out.println("敌人能看到玩家: " + enemy.canSeePlayer());
// 添加墙壁阻挡视线
map.setTerrain(12, 8, TerrainType.WALL);
map.setTerrain(12, 7, TerrainType.WALL);
System.out.println("\n添加墙壁后:");
System.out.println("敌人能看到玩家: " + enemy.canSeePlayer());
}
}
四、算法分析
| 组件 | 算法 | 时间复杂度 | 说明 |
|---|---|---|---|
| 状态机 | 查表切换 | O(1) | 高效的状态决策 |
| 视线检测 | Bresenham直线 | O(d) | d为两点距离 |
| 寻路 | A* | O(b^d) | b为分支因子,d为解深度 |
| 路径跟随 | 顺序执行 | O(1) | 每帧沿路径前进一步 |
五、总结与扩展
本文实现了一个完整的横版射击游戏敌人AI系统,涵盖有限状态机行为控制、A寻路、视线检测等核心算法。状态机使敌人行为逻辑清晰可维护,A确保敌人能智能地在复杂地形中追踪玩家。
扩展方向:
1. 行为树:用行为树替代状态机,实现更复杂的决策逻辑
2. 群体AI:多个敌人之间的协作与包抄策略
3. 学习AI:使用Q-Learning让敌人学习玩家行为模式
4. 完整物理:加入重力、跳跃轨迹预测,实现更真实的平台移动