每日算法 — 使用java实现坦克大战:人工势场路径规划与有限状态机敌人AI

坦克大战(Battle City)是街机与FC时代最具代表性的射击游戏之一。玩家在矩形地图中操控坦克摧毁敌方基地,同时躲避敌方坦克的追击。本文用Java实现一个带AI的简化版坦克大战,核心讲解人工势场法(Artificial Potential Field)如何融合吸引力与排斥力,在布满障碍的栅格地图中引导坦克智能移动;以及有限状态机(Finite State Machine, FSM)如何管理敌人坦克在巡逻、追击、逃跑、绕后四种行为状态之间的平滑切换。全文代码可直接运行,适合作为路径规划与游戏AI的入门实践。

一、游戏世界建模与核心数据结构

1.1 为什么选人工势场而非A*

A寻路在静态地图中表现优异,但在坦克大战这类动态对抗环境中存在明显短板:敌方坦克、飞行中的子弹、不断变化的战局都意味着最优路径可能在几帧之内失效。人工势场法的优势在于连续响应*——每一帧都根据当前态势重新计算合力方向,天然适应动态障碍物。它的核心思想来自物理学:目标位置产生吸引力,障碍物和敌人产生排斥力,坦克沿合力方向移动即可。

1.2 栅格地图与方向枚举

地图采用二维整数数组表示,每个格子有明确的类型。坦克和子弹的位置用连续的浮点坐标记录,但在碰撞检测和势场计算时映射到栅格。

import java.util.*;

/**
 * 地图格子类型枚举
 * EMPTY: 空地,可自由移动
 * BRICK: 砖墙,可被子弹摧毁
 * STEEL: 钢墙,不可摧毁
 * WATER: 水域,不可通行
 * BASE: 玩家基地,游戏胜利条件
 */
enum CellType {
    EMPTY(0), BRICK(1), STEEL(2), WATER(3), BASE(4);
    final int code;
    CellType(int code) { this.code = code; }
}

/**
 * 移动方向枚举
 * 每个方向包含x/y轴增量与角度值,便于统一处理移动与旋转
 */
enum Direction {
    UP(0, -1, 0),
    RIGHT(1, 0, 90),
    DOWN(0, 1, 180),
    LEFT(-1, 0, 270);

    final int dx, dy, angle;
    Direction(int dx, int dy, int angle) {
        this.dx = dx;
        this.dy = dy;
        this.angle = angle;
    }

    /**
     * 获取当前方向的反方向,用于逃跑或子弹反弹判断
     */
    Direction opposite() {
        return values()[(ordinal() + 2) % 4];
    }
}

/**
 * 二维向量工具类
 * 人工势场的核心计算全部建立在向量运算之上
 */
record Vector2D(double x, double y) {
    Vector2D add(Vector2D other) {
        return new Vector2D(x + other.x, y + other.y);
    }
    Vector2D scale(double s) {
        return new Vector2D(x * s, y * s);
    }
    double length() {
        return Math.sqrt(x * x + y * y);
    }
    Vector2D normalize() {
        double len = length();
        if (len < 1e-6) return new Vector2D(0, 0);
        return new Vector2D(x / len, y / len);
    }
    double distanceTo(Vector2D other) {
        double dx = x - other.x, dy = y - other.y;
        return Math.sqrt(dx * dx + dy * dy);
    }
}

1.3 坦克实体定义

坦克包含位置、方向、速度、状态等核心属性。Tank类同时服务于玩家和AI敌人,区别在于AI坦克额外持有一个状态机实例。

/**
 * 坦克实体基类
 * position: 中心点坐标(像素)
 * direction: 当前朝向
 * speed: 每秒移动的像素数
 * cooldown: 射击冷却计时器(秒)
 */
class Tank {
    Vector2D position;
    Direction direction;
    double speed = 80.0;      // 像素/秒
    double cooldown = 0.0;    // 剩余冷却时间
    boolean alive = true;

    Tank(Vector2D position, Direction direction) {
        this.position = position;
        this.direction = direction;
    }

    /**
     * 获取坦克占据的矩形边界,用于碰撞检测
     * 坦克尺寸统一为 32x32 像素
     */
    double left()   { return position.x() - 16; }
    double right()  { return position.x() + 16; }
    double top()    { return position.y() - 16; }
    double bottom() { return position.y() + 16; }

    /**
     * 尝试沿当前方向移动指定时间步长
     * 实际是否移动由外部碰撞检测决定
     */
    Vector2D nextPosition(double deltaTime) {
        return new Vector2D(
            position.x() + direction.dx * speed * deltaTime,
            position.y() + direction.dy * speed * deltaTime
        );
    }
}

二、射线投射碰撞检测

2.1 为什么用射线投射而非矩形碰撞

坦克大战中的子弹是高速飞行的小物体,若采用逐帧矩形碰撞检测,可能出现穿墙问题——子弹一帧在墙前,下一帧已到墙后,中间从未与墙体矩形相交。射线投射(Ray Casting)从子弹起点向终点发射一条射线,检测射线与地图障碍的交点,从根本上解决高速穿透问题。

2.2 DDA算法实现栅格射线投射

DDA(Digital Differential Analyzer)是经典的栅格射线遍历算法,沿射线方向逐步访问穿过的每个格子,直到遇到障碍或超出射程。时间复杂度为O(射线穿过的格子数),在小型地图中几乎常数时间。

/**
 * 射线投射结果
 * hit: 是否击中障碍
 * hitPoint: 击中的精确坐标
 * hitCell: 击中的格子坐标
 * distance: 发射点到击中点的距离
 */
record RaycastResult(boolean hit, Vector2D hitPoint, Vector2D hitCell, double distance) {}

/**
 * 射线投射器
 * 使用DDA算法在栅格地图中高效遍历射线经过的格子
 */
class Raycaster {
    final int[][] map;       // 地图数据
    final int cellSize;      // 每个格子的像素尺寸(如32)

    Raycaster(int[][] map, int cellSize) {
        this.map = map;
        this.cellSize = cellSize;
    }

    /**
     * 从origin沿direction方向发射射线,最大射程maxDistance
     * 返回射线最先击中的障碍信息;若无障碍则返回hit=false
     */
    RaycastResult cast(Vector2D origin, Vector2D direction, double maxDistance) {
        // 标准化方向
        Vector2D dir = direction.normalize();
        double dx = dir.x(), dy = dir.y();

        // 当前所在栅格坐标
        int mapX = (int)(origin.x() / cellSize);
        int mapY = (int)(origin.y() / cellSize);

        // deltaDist: 射线在x或y方向每穿过一个格子的距离
        double deltaDistX = Math.abs(1.0 / dx);
        double deltaDistY = Math.abs(1.0 / dy);

        // step: 射线在x/y方向的步进方向(+1或-1)
        int stepX = dx > 0 ? 1 : -1;
        int stepY = dy > 0 ? 1 : -1;

        // sideDist: 射线到达下一个垂直/水平栅格线的距离
        double sideDistX = dx > 0
            ? (mapX + 1 - origin.x() / cellSize) * deltaDistX
            : (origin.x() / cellSize - mapX) * deltaDistX;
        double sideDistY = dy > 0
            ? (mapY + 1 - origin.y() / cellSize) * deltaDistY
            : (origin.y() / cellSize - mapY) * deltaDistY;

        boolean hit = false;
        int side = 0; // 0=x面, 1=y面
        double perpWallDist = 0;

        while (!hit && perpWallDist < maxDistance / cellSize) {
            if (sideDistX < sideDistY) {
                sideDistX += deltaDistX;
                mapX += stepX;
                side = 0;
            } else {
                sideDistY += deltaDistY;
                mapY += stepY;
                side = 1;
            }

            // 越界检查
            if (mapX < 0 || mapX >= map[0].length || mapY < 0 || mapY >= map.length) {
                break;
            }

            // 遇到非空地即为命中
            if (map[mapY][mapX] != CellType.EMPTY.code && map[mapY][mapX] != CellType.BASE.code) {
                hit = true;
            }
        }

        if (!hit) {
            return new RaycastResult(false, null, null, maxDistance);
        }

        // 计算击中点的世界坐标
        if (side == 0) {
            perpWallDist = sideDistX - deltaDistX;
        } else {
            perpWallDist = sideDistY - deltaDistY;
        }
        double wallX = origin.x() + dx * perpWallDist * cellSize;
        double wallY = origin.y() + dy * perpWallDist * cellSize;

        return new RaycastResult(true, new Vector2D(wallX, wallY),
            new Vector2D(mapX, mapY), perpWallDist * cellSize);
    }
}

三、人工势场路径规划

3.1 势场构建原理

人工势场由两部分叠加而成:

  • 吸引力(Attractive Force):目标位置(如玩家基地或玩家坦克)对AI坦克产生吸引力,距离越远吸引力越强,遵循公式 F_att = k_att * (goal - pos)
  • 排斥力(Repulsive Force):障碍物、墙体和其他坦克对AI坦克产生排斥力,距离越近排斥力急剧增大,遵循公式 F_rep = k_rep * (1/d²) * (pos - obstacle).normalize(),其中d为到障碍的距离。

合力方向即为AI坦克本帧的理想移动方向。

3.2 势场计算器实现

/**
 * 人工势场路径规划器
 * 为AI坦克计算每一帧的期望移动方向
 */
class PotentialFieldPlanner {
    double kAtt = 2.0;       // 吸引力系数
    double kRep = 8000.0;    // 排斥力系数
    double d0 = 80.0;        // 排斥力影响半径(像素)
    double cellSize = 32.0;

    /**
     * 计算坦克在当前局势下受到的合力
     * @param self 当前AI坦克
     * @param target 目标(玩家坦克或基地)
     * @param obstacles 地图中所有障碍物的中心坐标列表
     * @param otherTanks 其他敌方坦克(避免拥挤)
     */
    Vector2D computeForce(Tank self, Tank target,
                          List<Vector2D> obstacles, List<Tank> otherTanks) {
        Vector2D pos = self.position;
        Vector2D force = new Vector2D(0, 0);

        // 1. 计算目标吸引力
        Vector2D toGoal = new Vector2D(
            target.position.x() - pos.x(),
            target.position.y() - pos.y()
        );
        double distGoal = toGoal.length();
        if (distGoal > 1.0) {
            // 吸引力与距离成正比,避免过近时震荡
            force = force.add(toGoal.normalize().scale(kAtt));
        }

        // 2. 计算障碍物排斥力
        for (Vector2D obs : obstacles) {
            Vector2D diff = new Vector2D(pos.x() - obs.x(), pos.y() - obs.y());
            double d = diff.length();
            if (d < d0 && d > 1.0) {
                // 距离越近排斥力越强,d→0时趋于无穷
                double magnitude = kRep / (d * d);
                force = force.add(diff.normalize().scale(magnitude));
            }
        }

        // 3. 计算其他坦克的排斥力(社交距离)
        for (Tank other : otherTanks) {
            if (other == self || !other.alive) continue;
            Vector2D diff = new Vector2D(pos.x() - other.position.x(),
                                         pos.y() - other.position.y());
            double d = diff.length();
            if (d < d0 * 0.8 && d > 1.0) {
                double magnitude = (kRep * 0.5) / (d * d);
                force = force.add(diff.normalize().scale(magnitude));
            }
        }

        return force;
    }

    /**
     * 将合力向量映射到最近的四个正方向之一
     * 坦克只能沿上下左右移动,因此需要离散化
     */
    Direction forceToDirection(Vector2D force) {
        double fx = force.x(), fy = force.y();
        double absX = Math.abs(fx), absY = Math.abs(fy);

        if (absX > absY) {
            return fx > 0 ? Direction.RIGHT : Direction.LEFT;
        } else {
            return fy > 0 ? Direction.DOWN : Direction.UP;
        }
    }
}

3.3 局部最小值与逃逸策略

人工势场的经典缺陷是局部最小值——当吸引力与排斥力恰好抵消时,坦克会在某一点附近震荡不前。本文采用简单的逃逸策略:当坦克连续2秒位置变化小于5像素时,随机选择一个新方向持续移动1秒,打破平衡。

/**
 * 逃逸检测器
 * 监测坦克是否陷入局部最小值
 */
class LocalMinimaEscaper {
    Vector2D lastPosition;
    double stuckTimer = 0;
    double escapeTimer = 0;
    boolean escaping = false;

    void update(Tank tank, double deltaTime) {
        if (lastPosition == null) {
            lastPosition = tank.position;
            return;
        }

        double moved = tank.position.distanceTo(lastPosition);
        if (moved < 5.0) {
            stuckTimer += deltaTime;
        } else {
            stuckTimer = 0;
            escaping = false;
        }

        // 陷入局部最小值超过2秒,启动逃逸
        if (stuckTimer > 2.0 && !escaping) {
            escaping = true;
            escapeTimer = 1.0;
            stuckTimer = 0;
        }

        if (escaping) {
            escapeTimer -= deltaTime;
            if (escapeTimer <= 0) escaping = false;
        }

        lastPosition = tank.position;
    }

    boolean isEscaping() { return escaping; }
}

四、有限状态机敌人AI

4.1 状态设计

敌人坦克的FSM包含四个状态,每个状态有独立的决策逻辑:

  • PATROL(巡逻):在出生点周围随机移动,扫描玩家位置。若发现玩家进入视野,切换到CHASE。
  • CHASE(追击):使用人工势场向玩家移动,保持射击。若玩家被墙体遮挡或自身血量过低,切换。
  • FLEE(逃跑):沿玩家反方向移动,寻找掩体。持续3秒后或血量恢复后切换回PATROL。
  • FLANK(绕后):当玩家躲在墙体后直射无效时,沿侧向移动尝试绕过墙体攻击侧面。

4.2 状态机实现

/**
 * 敌人AI状态枚举
 */
enum AIState { PATROL, CHASE, FLEE, FLANK }

/**
 * 有限状态机驱动的敌人坦克
 * 继承Tank并扩展AI决策能力
 */
class EnemyTank extends Tank {
    AIState state = AIState.PATROL;
    double stateTimer = 0;            // 当前状态持续时间
    double patrolChangeTimer = 0;     // 巡逻方向更换计时
    double health = 100;
    final Vector2D spawnPoint;        // 出生点,用于巡逻范围限制
    final PotentialFieldPlanner planner;
    final LocalMinimaEscaper escaper;
    final Random random = new Random();

    EnemyTank(Vector2D position, PotentialFieldPlanner planner) {
        super(position, Direction.DOWN);
        this.spawnPoint = position;
        this.planner = planner;
        this.escaper = new LocalMinimaEscaper();
    }

    /**
     * AI决策主循环,每帧调用一次
     * @param player 玩家坦克
     * @param map 地图数据
     * @param raycaster 射线投射器
     * @param obstacles 障碍物坐标列表
     * @param allies 友方AI坦克列表
     * @param deltaTime 时间步长
     */
    void think(Tank player, int[][] map, Raycaster raycaster,
               List<Vector2D> obstacles, List<EnemyTank> allies, double deltaTime) {
        stateTimer += deltaTime;
        escaper.update(this, deltaTime);

        // 1. 状态转移判断
        boolean canSeePlayer = canSee(player, raycaster);
        transitionState(canSeePlayer, player, deltaTime);

        // 2. 根据当前状态执行行为
        switch (state) {
            case PATROL  -> doPatrol(deltaTime);
            case CHASE   -> doChase(player, obstacles, allies, deltaTime);
            case FLEE    -> doFlee(player, obstacles, deltaTime);
            case FLANK   -> doFlank(player, obstacles, allies, deltaTime);
        }
    }

    /**
     * 使用射线投射判断是否能直接看到玩家
     * 若射线从本坦克中心到玩家中心未击中墙体,则视野畅通
     */
    boolean canSee(Tank player, Raycaster raycaster) {
        Vector2D toPlayer = new Vector2D(
            player.position.x() - position.x(),
            player.position.y() - position.y()
        );
        RaycastResult result = raycaster.cast(position, toPlayer, 800);
        if (!result.hit()) return true; // 无障碍
        // 比较击中点与玩家距离:若玩家比障碍更近,则能看到
        double hitDist = result.distance();
        double playerDist = position.distanceTo(player.position);
        return playerDist < hitDist - 10; // 留10像素容差
    }

    /**
     * 状态转移逻辑
     */
    void transitionState(boolean canSeePlayer, Tank player, double deltaTime) {
        AIState old = state;

        switch (state) {
            case PATROL -> {
                if (canSeePlayer) state = AIState.CHASE;
                else if (health < 30 && position.distanceTo(player.position) < 200)
                    state = AIState.FLEE;
            }
            case CHASE -> {
                if (!canSeePlayer && stateTimer > 1.5)
                    state = AIState.FLANK; // 丢失视野,尝试绕后
                else if (health < 25)
                    state = AIState.FLEE;
            }
            case FLEE -> {
                if (stateTimer > 3.0 || health > 60)
                    state = AIState.PATROL;
            }
            case FLANK -> {
                if (canSeePlayer) state = AIState.CHASE;
                else if (stateTimer > 4.0) state = AIState.PATROL; // 绕后超时
            }
        }

        if (state != old) stateTimer = 0; // 重置状态计时器
    }

    /**
     * 巡逻行为:在出生点周围随机移动
     */
    void doPatrol(double deltaTime) {
        patrolChangeTimer -= deltaTime;
        if (patrolChangeTimer <= 0 || escaper.isEscaping()) {
            // 随机选择方向,优先朝向出生点
            Direction[] dirs = Direction.values();
            direction = dirs[random.nextInt(dirs.length)];
            patrolChangeTimer = 1.5 + random.nextDouble() * 2.0;
        }
        position = nextPosition(deltaTime);
    }

    /**
     * 追击行为:人工势场引导 + 持续射击
     */
    void doChase(Tank player, List<Vector2D> obstacles,
                 List<EnemyTank> allies, double deltaTime) {
        Vector2D force = planner.computeForce(this, player, obstacles, allies);
        if (escaper.isEscaping()) {
            direction = Direction.values()[random.nextInt(4)];
        } else {
            direction = planner.forceToDirection(force);
        }
        position = nextPosition(deltaTime);
        cooldown -= deltaTime;
    }

    /**
     * 逃跑行为:沿玩家反方向移动
     */
    void doFlee(Tank player, List<Vector2D> obstacles, double deltaTime) {
        Vector2D away = new Vector2D(
            position.x() - player.position.x(),
            position.y() - player.position.y()
        );
        direction = planner.forceToDirection(away);
        position = nextPosition(deltaTime);
    }

    /**
     * 绕后行为:沿玩家侧向移动,试图获得射击角度
     * 通过将吸引力旋转90度实现侧向迫近
     */
    void doFlank(Tank player, List<Vector2D> obstacles,
                 List<EnemyTank> allies, double deltaTime) {
        Vector2D toPlayer = new Vector2D(
            player.position.x() - position.x(),
            player.position.y() - position.y()
        );
        // 将向量顺时针旋转90度获得侧向吸引力
        Vector2D flankForce = new Vector2D(toPlayer.y(), -toPlayer.x());
        Vector2D baseForce = planner.computeForce(this, player, obstacles, allies);

        // 侧向力与基础势场叠加,权重各占50%
        Vector2D combined = flankForce.normalize().scale(1.0)
            .add(baseForce.normalize().scale(1.0));
        direction = planner.forceToDirection(combined);
        position = nextPosition(deltaTime);
    }
}

五、游戏引擎主循环与渲染

5.1 简化版控制台渲染

为了代码可直接运行且不依赖图形库,本文提供一个基于ASCII字符的渲染器。在实际项目中,将render()中的System.out.print替换为JavaFX或LibGDX的绘制调用即可。

/**
 * 游戏主引擎
 * 负责地图初始化、实体管理和主循环更新
 */
class GameEngine {
    final int width, height;
    final int[][] map;
    final int cellSize = 32;
    final Raycaster raycaster;
    final PotentialFieldPlanner planner;

    Tank player;
    List<EnemyTank> enemies = new ArrayList<>();
    List<Bullet> bullets = new ArrayList<>();

    GameEngine(int width, int height) {
        this.width = width;
        this.height = height;
        this.map = new int[height][width];
        this.raycaster = new Raycaster(map, cellSize);
        this.planner = new PotentialFieldPlanner();
        initMap();
    }

    /**
     * 初始化简单地图:外围钢墙,内部随机砖墙
     */
    void initMap() {
        for (int y = 0; y < height; y++) {
            for (int x = 0; x < width; x++) {
                if (x == 0 || x == width - 1 || y == 0 || y == height - 1)
                    map[y][x] = CellType.STEEL.code;
                else if (Math.random() < 0.15 && x > 3 && x < width - 4)
                    map[y][x] = CellType.BRICK.code;
                else
                    map[y][x] = CellType.EMPTY.code;
            }
        }
        // 玩家基地位于底部中央
        map[height - 2][width / 2] = CellType.BASE.code;
    }

    /**
     * 更新一帧游戏逻辑
     */
    void update(double deltaTime) {
        List<Vector2D> obstacles = collectObstacles();

        // 更新所有敌人AI
        for (EnemyTank e : enemies) {
            if (!e.alive) continue;
            e.think(player, map, raycaster, obstacles, enemies, deltaTime);
            clampPosition(e);
        }

        // 更新子弹
        for (Bullet b : new ArrayList<>(bullets)) {
            b.update(deltaTime);
            if (b.outOfBounds(width * cellSize, height * cellSize) || b.hit) {
                bullets.remove(b);
            }
        }

        // 碰撞检测:子弹 vs 坦克 vs 墙体
        resolveCollisions();
    }

    /**
     * 收集地图中所有非空障碍物的中心坐标
     */
    List<Vector2D> collectObstacles() {
        List<Vector2D> list = new ArrayList<>();
        for (int y = 0; y < height; y++) {
            for (int x = 0; x < width; x++) {
                if (map[y][x] != CellType.EMPTY.code) {
                    list.add(new Vector2D(x * cellSize + cellSize / 2.0,
                                          y * cellSize + cellSize / 2.0));
                }
            }
        }
        return list;
    }

    /**
     * 将坦克位置限制在地图范围内
     */
    void clampPosition(Tank t) {
        double maxX = width * cellSize - 16;
        double maxY = height * cellSize - 16;
        double x = Math.max(16, Math.min(maxX, t.position.x()));
        double y = Math.max(16, Math.min(maxY, t.position.y()));
        t.position = new Vector2D(x, y);
    }

    /**
     * 碰撞检测与伤害结算
     */
    void resolveCollisions() {
        for (Bullet b : bullets) {
            if (b.hit) continue;
            // 检测子弹与墙体
            int mx = (int)(b.position.x() / cellSize);
            int my = (int)(b.position.y() / cellSize);
            if (my >= 0 && my < height && mx >= 0 && mx < width
                && map[my][mx] == CellType.BRICK.code) {
                map[my][mx] = CellType.EMPTY.code; // 砖墙可被摧毁
                b.hit = true;
            }
            // 检测子弹与坦克
            if (!b.fromEnemy && player.alive && intersects(b, player)) {
                player.alive = false; b.hit = true;
            }
            for (EnemyTank e : enemies) {
                if (b.fromEnemy == (e == b.owner)) continue;
                if (e.alive && intersects(b, e)) {
                    e.health -= 50;
                    if (e.health <= 0) e.alive = false;
                    b.hit = true;
                }
            }
        }
    }

    boolean intersects(Bullet b, Tank t) {
        return b.position.x() > t.left() && b.position.x() < t.right()
            && b.position.y() > t.top() && b.position.y() < t.bottom();
    }

    /**
     * 控制台渲染(ASCII艺术)
     */
    void render() {
        char[][] screen = new char[height][width];
        for (int y = 0; y < height; y++) {
            for (int x = 0; x < width; x++) {
                screen[y][x] = switch (map[y][x]) {
                    case 1 -> '#'; // 砖墙
                    case 2 -> '@'; // 钢墙
                    case 3 -> '~'; // 水
                    case 4 -> 'B'; // 基地
                    default -> '.';
                };
            }
        }
        // 渲染玩家
        if (player.alive) {
            int px = (int)(player.position.x() / cellSize);
            int py = (int)(player.position.y() / cellSize);
            if (py >= 0 && py < height && px >= 0 && px < width) screen[py][px] = 'P';
        }
        // 渲染敌人
        for (EnemyTank e : enemies) {
            if (!e.alive) continue;
            int ex = (int)(e.position.x() / cellSize);
            int ey = (int)(e.position.y() / cellSize);
            if (ey >= 0 && ey < height && ex >= 0 && ex < width) screen[ey][ex] = 'E';
        }
        // 输出
        StringBuilder sb = new StringBuilder();
        for (char[] row : screen) {
            sb.append(new String(row)).append('\n');
        }
        System.out.print("\033[H\033[2J"); // 清屏
        System.out.println(sb);
    }
}

5.2 子弹实体

/**
 * 子弹实体
 * 沿固定方向匀速飞行,速度远高于坦克
 */
class Bullet {
    Vector2D position;
    Direction direction;
    double speed = 300.0;
    boolean hit = false;
    boolean fromEnemy;
    Tank owner;

    Bullet(Vector2D position, Direction direction, boolean fromEnemy, Tank owner) {
        this.position = position;
        this.direction = direction;
        this.fromEnemy = fromEnemy;
        this.owner = owner;
    }

    void update(double deltaTime) {
        position = new Vector2D(
            position.x() + direction.dx * speed * deltaTime,
            position.y() + direction.dy * speed * deltaTime
        );
    }

    boolean outOfBounds(double maxX, double maxY) {
        return position.x() < 0 || position.x() > maxX
            || position.y() < 0 || position.y() > maxY;
    }
}

六、启动器与复杂度分析

6.1 主入口

/**
 * 坦克大战AI演示程序入口
 * 在控制台中以ASCII形式展示AI坦克的巡逻、追击与绕后行为
 */
public class TankBattleAI {
    public static void main(String[] args) throws InterruptedException {
        GameEngine engine = new GameEngine(20, 15);

        // 初始化玩家(底部中央偏左)
        engine.player = new Tank(
            new Vector2D(320, 400), Direction.UP);

        // 初始化3个AI敌人
        for (int i = 0; i < 3; i++) {
            engine.enemies.add(new EnemyTank(
                new Vector2D(100 + i * 200, 80),
                engine.planner
            ));
        }

        // 主循环:每秒30帧
        double deltaTime = 1.0 / 30.0;
        while (engine.player.alive) {
            engine.update(deltaTime);
            engine.render();
            Thread.sleep((long)(deltaTime * 1000));
        }
        System.out.println("Game Over!");
    }
}

6.2 算法复杂度总结

模块 时间复杂度 空间复杂度 关键瓶颈
射线投射(DDA) O(L),L为射线穿过格子数 O(1) 远距离投射
人工势场计算 O(N),N为障碍物数量 O(1) 障碍物过多时排斥力计算
状态机决策 O(1) O(1) 状态数量固定为4
整体帧更新 O(E·N),E为敌人数 O(M²),M为地图尺寸 敌人数>20时势场计算累加

人工势场的核心优势是实时性动态适应性,无需像A*那样预计算完整路径;代价是可能陷入局部最小值,需配合逃逸策略使用。在10个以下敌人、100×100以内栅格的地图中,单帧计算耗时远低于1毫秒,完全满足实时游戏需求。

七、扩展方向

  • 群体智能:引入多AI协同,部分坦克正面吸引火力,另一部分侧翼包抄,可用势场中的”编队吸引力”实现。
  • 学习增强:在FSM状态转移中引入Q-Learning,让AI自动学习何时追击、何时逃跑,替代手工编写的阈值判断。
  • 子弹预测:在CHASE状态下,不仅朝玩家当前位置移动,而是计算玩家未来0.5秒后的预测位置作为势场目标,提升命中率。

通过将人工势场与有限状态机结合,我们在不依赖复杂图搜索的前提下,为坦克大战构建了一套反应灵敏、行为丰富的AI系统。希望这段代码能为你的游戏AI学习提供一块合格的垫脚石。