每日算法 — 使用java实现魂斗罗:横版射击敌人AI与有限状态机决策

引言

魂斗罗(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*的辅助数组。

扩展方向

  1. 行为树:将FSM的每个状态替换为子行为树,实现更细粒度的动作编排
  2. 群体AI:引入 flocking 算法,让多个敌人形成包抄阵型
  3. 学习机制:用Q-Learning让敌人从与玩家的交战中学习最优策略
  4. 分层路径规划:高层用导航网格(NavMesh)规划粗略路径,低层用局部避障处理动态障碍

总结

魂斗罗敌人AI展示了有限状态机在实时游戏中的应用价值。通过清晰的状态划分和转移条件,配合A*寻路和视线检测,敌人能够表现出巡逻、警戒、追击、攻击、撤退等多种战术行为,为横版射击游戏提供了可扩展的AI框架。