引言
气球大战(Balloon Fight)是任天堂于1984年推出的经典街机游戏,玩家控制一个绑着气球的角色,通过拍打翅膀产生浮力在空中飞行,同时躲避敌人并戳破对方的气球。本文用 Java 实现气球大战的物理引擎与敌人 AI,核心聚焦于物理浮力模拟与AI 追击策略,让敌人能够智能追踪玩家并选择最佳攻击时机。
游戏场景建模
游戏场景为一个有边界的二维空间,包含以下元素:
– 玩家(Player):绑着两个气球,通过拍打产生向上的推力,受重力影响下落
– 敌人(Enemy):同样绑着气球,AI 控制其飞行轨迹,目标是接近玩家并戳破玩家气球
– 云朵(Cloud):悬浮障碍物,碰撞后会弹开
– 水域(Water):屏幕底部,落入其中会损失生命
– 气泡(Bubble):从水中浮出,戳破可获得奖励
物理模型需要模拟:重力、空气阻力、拍打的脉冲推力、以及碰撞反弹。
核心算法设计
物理浮力模拟
气球大战的飞行物理是游戏的核心。角色的运动遵循以下规则:
– 重力:每帧施加向下的恒定加速度 g
– 拍打推力:按下按钮时施加向上的脉冲力 flap_force
– 空气阻力:与速度成正比,方向与速度相反
– 最大速度限制:垂直和水平速度均有上限
运动学方程(每帧更新):
v_y = v_y + g - flap_force * isFlapping - drag * v_y
v_x = v_x + input_x * accel - drag * v_x
x = x + v_x
y = y + v_y
其中 drag 为空气阻力系数,isFlapping 为是否正在拍打(0 或 1)。
AI 追击策略
敌人 AI 采用预测性追击策略:
1. 目标选择:始终将玩家作为追击目标
2. 轨迹预测:根据玩家当前速度和加速度,预测玩家未来短时间内的位置
3. 最优拦截点:计算敌人到达预测位置的最优路径
4. 高度控制:利用拍打时机控制垂直位置,保持在玩家同一高度或略高(便于俯冲攻击)
5. 攻击时机:当与玩家的距离小于攻击阈值且相对速度方向有利时发动攻击
更精细的 AI 还会考虑:
– 避障:规划路径时绕开云朵
– 边界回避:避免飞出屏幕边界
– 协同追击:多个敌人时采用包围策略
Java 实现
import java.util.*;
/**
* 气球大战:物理浮力模拟与AI追击策略
*/
public class BalloonFightAI {
// 物理常量
static final double GRAVITY = 0.15;
static final double FLAP_FORCE = 3.5;
static final double DRAG = 0.05;
static final double H_ACCEL = 0.3;
static final double MAX_H_SPEED = 3.0;
static final double MAX_V_SPEED = 4.0;
static final double ATTACK_RANGE = 25.0;
static final int WIDTH = 256;
static final int HEIGHT = 240;
/**
* 游戏实体基类
*/
static class Entity {
double x, y;
double vx, vy;
boolean alive = true;
void update() {
x += vx;
y += vy;
// 边界环绕(左右边界)
if (x < 0) x += WIDTH;
if (x >= WIDTH) x -= WIDTH;
// 底部边界(水域)
if (y > HEIGHT - 20) {
y = HEIGHT - 20;
vy = -vy * 0.5; // 水面反弹
}
// 顶部边界
if (y < 0) {
y = 0;
vy = 0;
}
}
}
/**
* 玩家
*/
static class Player extends Entity {
boolean isFlapping = false;
int balloons = 2;
void applyPhysics() {
// 重力
vy += GRAVITY;
// 拍打推力
if (isFlapping) {
vy -= FLAP_FORCE;
isFlapping = false; // 每帧只拍一次
}
// 空气阻力
vx -= DRAG * vx;
vy -= DRAG * vy;
// 速度限制
vx = Math.max(-MAX_H_SPEED, Math.min(MAX_H_SPEED, vx));
vy = Math.max(-MAX_V_SPEED, Math.min(MAX_V_SPEED, vy));
}
}
/**
* 敌人 AI
*/
static class Enemy extends Entity {
int balloons = 2;
double targetX, targetY; // 当前追击目标位置
int predictionFrames = 15; // 预测玩家未来15帧的位置
double aggression = 0.8; // 攻击性系数(0-1)
/**
* AI 主控制逻辑:决定每帧的拍打和水平移动
*/
void aiControl(Player player, List<Cloud> clouds) {
// 1. 预测玩家未来位置
predictPlayerPosition(player);
// 2. 计算与目标的相对位置
double dx = wrapDistance(x, targetX);
double dy = targetY - y;
double dist = Math.hypot(dx, dy);
// 3. 水平方向控制:朝向目标移动
if (Math.abs(dx) > 5) {
vx += Math.signum(dx) * H_ACCEL;
} else {
vx *= 0.9; // 接近目标时减速
}
// 4. 垂直方向控制:通过拍打维持理想高度
double idealHeight = computeIdealHeight(player, dy, dist);
double heightError = idealHeight - y;
// 只有在需要向上时才拍打
if (heightError < -10 && vy > -MAX_V_SPEED * 0.7) {
flap();
}
// 5. 攻击决策
if (dist < ATTACK_RANGE && isGoodAttackAngle(dx, dy, player)) {
performAttack();
}
// 6. 避障:检测前方云朵
avoidClouds(clouds);
// 7. 应用物理
vy += GRAVITY;
vy -= DRAG * vy;
vx -= DRAG * vx;
vx = Math.max(-MAX_H_SPEED, Math.min(MAX_H_SPEED, vx));
vy = Math.max(-MAX_V_SPEED, Math.min(MAX_V_SPEED, vy));
}
/**
* 预测玩家未来位置(简单线性预测)
*/
void predictPlayerPosition(Player player) {
// 假设玩家保持当前速度
targetX = player.x + player.vx * predictionFrames;
targetY = player.y + player.vy * predictionFrames;
// 处理边界环绕
if (targetX < 0) targetX += WIDTH;
if (targetX >= WIDTH) targetX -= WIDTH;
// 重力影响下的垂直位置修正
double predictedVy = player.vy;
for (int i = 0; i < predictionFrames; i++) {
predictedVy += GRAVITY * 0.5; // 假设玩家偶尔拍打
predictedVy -= DRAG * predictedVy;
}
targetY = player.y + (player.vy + predictedVy) * predictionFrames / 2;
}
/**
* 计算理想的追击高度
*/
double computeIdealHeight(Player player, double dy, double dist) {
// 基础策略:保持在玩家略上方,便于俯冲攻击
double baseHeight = player.y - 20 * aggression;
// 距离较远时,提前占据拦截位置
if (dist > 80) {
baseHeight = player.y + dy * 0.3;
}
// 避免飞得太高浪费能量
if (baseHeight < 30) baseHeight = 30;
if (baseHeight > HEIGHT - 60) baseHeight = HEIGHT - 60;
return baseHeight;
}
/**
* 判断是否是好攻击角度
*/
boolean isGoodAttackAngle(double dx, double dy, Player player) {
// 理想情况:从上方或侧方接近,相对速度方向有利于碰撞
double relVx = vx - player.vx;
double relVy = vy - player.vy;
// 计算相对速度朝向目标的角度
double speedToward = -(relVx * dx + relVy * dy) / Math.hypot(dx, dy);
// 如果正在远离玩家,不是好时机
if (speedToward < 0) return false;
// 从上方俯冲时攻击成功率更高
if (dy < 0 && Math.abs(dx) < 15) return true;
// 侧面对冲时也可以攻击
return Math.abs(dx) < 10 && Math.abs(dy) < 15;
}
void performAttack() {
// 攻击时略微加速冲向玩家
vx *= 1.1;
vy *= 1.1;
}
void flap() {
vy -= FLAP_FORCE;
}
/**
* 云朵避障
*/
void avoidClouds(List<Cloud> clouds) {
for (Cloud c : clouds) {
double cdx = wrapDistance(x, c.x);
double cdy = c.y - y;
double cdist = Math.hypot(cdx, cdy);
if (cdist < c.radius + 30) {
// 预测碰撞,提前避让
if (Math.abs(cdx) < Math.abs(cdy)) {
// 主要水平避让
vx -= Math.signum(cdx) * H_ACCEL * 2;
} else {
// 主要垂直避让
if (cdy > 0) flap();
else vy += GRAVITY * 2;
}
}
}
}
}
/**
* 云朵障碍物
*/
static class Cloud {
double x, y;
double radius;
Cloud(double x, double y, double r) {
this.x = x; this.y = y; this.radius = r;
}
}
/**
* 处理屏幕环绕距离
*/
static double wrapDistance(double x1, double x2) {
double d = x2 - x1;
if (d > WIDTH / 2) d -= WIDTH;
if (d < -WIDTH / 2) d += WIDTH;
return d;
}
// 主程序:演示物理模拟和AI追击
public static void main(String[] args) {
Player player = new Player();
player.x = 100; player.y = 100;
player.vx = 1.5; player.vy = -0.5;
Enemy enemy = new Enemy();
enemy.x = 200; enemy.y = 150;
List<Cloud> clouds = Arrays.asList(
new Cloud(150, 80, 20),
new Cloud(80, 180, 25)
);
System.out.println("=== 气球大战 AI 追击模拟 ===");
System.out.println("初始状态:");
System.out.printf("玩家: (%.1f, %.1f) 速度(%.2f, %.2f)%n", player.x, player.y, player.vx, player.vy);
System.out.printf("敌人: (%.1f, %.1f) 速度(%.2f, %.2f)%n", enemy.x, enemy.y, enemy.vx, enemy.vy);
// 模拟 60 帧
for (int frame = 0; frame < 60; frame++) {
// 玩家控制(模拟持续向右上方飞行)
if (frame % 8 == 0) player.isFlapping = true;
player.applyPhysics();
player.update();
// 敌人 AI 控制
enemy.aiControl(player, clouds);
enemy.update();
double dist = Math.hypot(wrapDistance(enemy.x, player.x), enemy.y - player.y);
if (frame % 15 == 0 || dist < ATTACK_RANGE) {
System.out.printf("帧 %2d: 玩家(%.1f, %.1f) 敌人(%.1f, %.1f) 距离=%.1f%n",
frame, player.x, player.y, enemy.x, enemy.y, dist);
}
if (dist < ATTACK_RANGE) {
System.out.println("*** 敌人进入攻击范围!***");
}
}
}
}
算法解析
浮力物理的核心参数
气球大战的飞行手感由几个关键参数决定:
– GRAVITY = 0.15:较小的重力使角色能长时间悬浮,符合”气球”主题
– FLAP_FORCE = 3.5:单次拍打的向上冲量,约为重力的 23 倍,能产生明显的上升
– DRAG = 0.05:空气阻力使速度自然衰减,避免无限加速
– MAX_V_SPEED = 4.0:垂直速度上限防止下落过快
这些参数的组合创造了一个”浮力感”:角色不是自由落体,而是像在水中一样有缓冲感。
预测性追击的优势
相比简单的”朝当前位置移动”,预测性追击能显著提升 AI 的威胁性:
– 如果玩家正在水平飞行,AI 会拦截其前方
– 如果玩家正在下落,AI 会提前降低高度等待
– 预测帧数 predictionFrames = 15 约等于 0.25 秒(按 60fps),是一个平衡值:太短则反应迟钝,太长则预测不准
高度控制策略
敌人的高度控制是一个 PID 控制的简化版:
– 误差 = 理想高度 – 当前高度
– 当误差 < -10(即当前太低)时拍打
– 不主动减速下降,依靠重力自然下落
这种”只在需要时拍打”的策略模拟了人类玩家的操作习惯,既节能又有效。
复杂度分析
| 操作 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 物理更新(每实体) | O(1) | O(1) |
| 玩家位置预测 | O(1) | O(1) |
| 避障检测 | O(C),C 为云朵数 | O(1) |
| 每帧完整更新 | O(N + C),N 为实体数 | O(1) |
气球大战的 AI 计算量极小,完全可以在 8 位机(如 NES)的 1.79MHz CPU 上实时运行。其优雅之处在于用简单的物理规则和启发式策略就能产生智能的行为。
扩展思路
- 群体协同:多个敌人时引入蜂群算法(Boids),实现分离、对齐、聚合三个规则的平衡
- 学习玩家模式:记录玩家的移动习惯(偏好高度、拍打频率),调整预测模型
- 风场模拟:加入随机风力和湍流,使飞行轨迹更不可预测
- 神经网络控制:用 NEAT(神经进化)训练敌人 AI,发现人类想不到的飞行技巧
总结
本文通过 Java 实现了气球大战的物理引擎与敌人 AI:
– 物理浮力模拟:重力+脉冲推力+空气阻力的组合创造出独特的气球飞行手感
– 预测性追击:通过线性预测玩家未来位置,实现拦截式追击
– 高度 PID 控制:只在需要时拍打,模拟人类玩家的操作策略
读者可以从中学习到:物理引擎的基础实现、目标预测与拦截算法、以及简单的游戏 AI 行为树设计。