引言
冰球(Air Hockey)是一款经典的桌上竞技游戏,玩家通过操控球拍击打圆盘,试图将圆盘送入对方球门得分。本文将用Java实现一个带AI的冰球对战系统,核心聚焦于刚体碰撞物理模拟与AI goalie策略决策两大算法模块。读者将学习到弹性碰撞的向量分解、动量守恒在离散时间步的数值实现,以及基于球轨预测的门将拦截策略。
核心算法一:刚体碰撞物理模拟
冰球系统的物理模型包含三个核心对象:冰球(puck)、玩家球拍(mallet)和边界挡板(boundary)。每一帧的物理更新遵循以下流程:
- 位置更新:根据当前速度更新位置
- 摩擦力衰减:模拟冰面阻力使速度逐渐减小
- 碰撞检测:检测冰球与球拍/边界的相交
- 碰撞响应:计算碰撞后的新速度向量
弹性碰撞的向量分解
当两个圆形物体发生碰撞时,将速度向量分解为法向分量(沿碰撞中心连线)和切向分量(垂直于碰撞中心连线):
- 法向分量遵循一维弹性碰撞公式交换
- 切向分量保持不变(忽略旋转摩擦)
设物体A、B的质量分别为 $m_A$、$m_B$,速度向量为 $\vec{v}_A$、$\vec{v}_B$,碰撞方向单位向量为 $\vec{n}$,则:
$$v_{An} = \vec{v}A \cdot \vec{n}, \quad v{Bn} = \vec{v}_B \cdot \vec{n}$$
法向碰撞后的速度(一维弹性碰撞):
$$v’{An} = \frac{(m_A – m_B) v{An} + 2 m_B v_{Bn}}{m_A + m_B}$$
$$v’{Bn} = \frac{(m_B – m_A) v{Bn} + 2 m_A v_{An}}{m_A + m_B}$$
Java实现:物理引擎核心类
import java.awt.*;
import java.awt.geom.*;
/**
* 二维向量工具类,封装向量运算
*/
class Vec2 {
public double x, y;
public Vec2(double x, double y) { this.x = x; this.y = y; }
public Vec2 add(Vec2 v) { return new Vec2(x + v.x, y + v.y); }
public Vec2 sub(Vec2 v) { return new Vec2(x - v.x, y - v.y); }
public Vec2 mul(double s) { return new Vec2(x * s, y * s); }
public double dot(Vec2 v) { return x * v.x + y * v.y; }
public double len() { return Math.sqrt(x * x + y * y); }
public Vec2 normalize() {
double l = len();
return l < 1e-9 ? new Vec2(0, 0) : new Vec2(x / l, y / l);
}
public Vec2 proj(Vec2 onto) {
// 投影到 onto 方向
double ontoLenSq = onto.dot(onto);
if (ontoLenSq < 1e-12) return new Vec2(0, 0);
double scale = dot(onto) / ontoLenSq;
return onto.mul(scale);
}
}
/**
* 圆形刚体:冰球或球拍
*/
class RigidBody {
Vec2 pos; // 位置
Vec2 vel; // 速度
double radius; // 半径
double mass; // 质量
double restitution = 0.98; // 弹性系数(接近1表示几乎完全弹性)
public RigidBody(double x, double y, double r, double m) {
this.pos = new Vec2(x, y);
this.vel = new Vec2(0, 0);
this.radius = r;
this.mass = m;
}
/**
* 更新位置并应用摩擦力
* @param dt 时间步长
* @param friction 摩擦衰减系数(每帧乘以此系数)
*/
public void update(double dt, double friction) {
pos = pos.add(vel.mul(dt));
vel = vel.mul(friction);
// 当速度极小时归零,避免抖动
if (vel.len() < 0.5) vel = new Vec2(0, 0);
}
}
/**
* 碰撞检测与响应处理器
*/
class PhysicsEngine {
private static final double GOAL_WIDTH = 120;
/**
* 处理两个圆形刚体的弹性碰撞
*/
public static void resolveCollision(RigidBody a, RigidBody b) {
Vec2 diff = b.pos.sub(a.pos);
double dist = diff.len();
double minDist = a.radius + b.radius;
if (dist >= minDist || dist < 1e-9) return;
// 碰撞方向单位向量
Vec2 normal = diff.normalize();
// 将速度分解为法向与切向
double vAn = a.vel.dot(normal);
double vBn = b.vel.dot(normal);
// 一维弹性碰撞公式(考虑质量)
double mA = a.mass, mB = b.mass;
double vAnNew = ((mA - mB) * vAn + 2 * mB * vBn) / (mA + mB);
double vBnNew = ((mB - mA) * vBn + 2 * mA * vAn) / (mA + mB);
// 重构速度:切向不变 + 新法向
Vec2 tangent = new Vec2(-normal.y, normal.x);
Vec2 vAt = tangent.mul(a.vel.dot(tangent));
Vec2 vBt = tangent.mul(b.vel.dot(tangent));
a.vel = vAt.add(normal.mul(vAnNew * a.restitution));
b.vel = vBt.add(normal.mul(vBnNew * b.restitution));
// 防止穿透:沿法向推开
double overlap = minDist - dist;
Vec2 correction = normal.mul(overlap / 2.0 + 0.5);
a.pos = a.pos.sub(correction);
b.pos = b.pos.add(correction);
}
/**
* 处理冰球与边界的碰撞
* @param tableWidth 桌面宽度
* @param tableHeight 桌面高度
* @return 0=无得分, 1=玩家得分(AI失球), 2=AI得分(玩家失球)
*/
public static int resolveBoundaryCollision(RigidBody puck,
double tableWidth,
double tableHeight) {
double hw = tableWidth / 2;
double hh = tableHeight / 2;
double r = puck.radius;
// 左右边界(反弹)
if (puck.pos.x < -hw + r) {
puck.pos.x = -hw + r;
puck.vel.x = -puck.vel.x * puck.restitution;
} else if (puck.pos.x > hw - r) {
puck.pos.x = hw - r;
puck.vel.x = -puck.vel.x * puck.restitution;
}
// 上下边界:检测是否进球门
if (puck.pos.y < -hh + r) {
if (Math.abs(puck.pos.x) < GOAL_WIDTH / 2) {
return 1; // 玩家得分
}
puck.pos.y = -hh + r;
puck.vel.y = -puck.vel.y * puck.restitution;
} else if (puck.pos.y > hh - r) {
if (Math.abs(puck.pos.x) < GOAL_WIDTH / 2) {
return 2; // AI得分
}
puck.pos.y = hh - r;
puck.vel.y = -puck.vel.y * puck.restitution;
}
return 0;
}
}
核心算法二:AI门将策略决策
AI球拍的决策核心在于预测冰球轨迹并移动到最佳拦截位置。采用两级策略:
- 轨迹预测:假设冰球当前速度方向不变(忽略未来碰撞),计算冰球到达AI防守线(y = 固定值)时的x坐标
- 位置控制:将AI球拍向预测位置移动,同时限制在合理防守区域内
轨迹预测公式
冰球位置随时间变化:$\vec{p}(t) = \vec{p}_0 + \vec{v} \cdot t$
设AI防守线纵坐标为 $y_{defend}$,当前冰球位置为 $(x_0, y_0)$,速度为 $(v_x, v_y)$,则到达时间:
$$t = \frac{y_{defend} – y_0}{v_y}$$
预测到达位置的x坐标:
$$x_{predict} = x_0 + v_x \cdot t$$
Java实现:AI策略类
/**
* AI goalie策略控制器
*/
class AIGoalie {
private RigidBody aiMallet; // AI球拍
private double defendY; // AI防守线y坐标
private double maxSpeed; // AI最大移动速度
private double tableHalfWidth; // 桌面半宽
public AIGoalie(RigidBody mallet, double defendY,
double maxSpeed, double tableWidth) {
this.aiMallet = mallet;
this.defendY = defendY;
this.maxSpeed = maxSpeed;
this.tableHalfWidth = tableWidth / 2;
}
/**
* 每帧更新AI球拍速度,朝向预测拦截点移动
* @param puck 冰球状态
*/
public void update(RigidBody puck) {
double targetX;
// 只有当冰球向AI方向运动时才积极拦截
if (puck.vel.y > 0) {
// 预测冰球到达防守线时的x坐标
double dy = defendY - puck.pos.y;
double t = dy / puck.vel.y;
targetX = puck.pos.x + puck.vel.x * t;
// 考虑边界反弹一次后的修正(简化处理:限制在桌面范围内)
while (Math.abs(targetX) > tableHalfWidth) {
if (targetX > tableHalfWidth) {
targetX = 2 * tableHalfWidth - targetX;
} else {
targetX = -2 * tableHalfWidth - targetX;
}
}
} else {
// 冰球远离时,回防到球门中央
targetX = 0;
}
// 计算移动方向与速度
double dx = targetX - aiMallet.pos.x;
double moveSpeed = Math.min(maxSpeed, Math.abs(dx) * 5);
if (Math.abs(dx) < 2) {
aiMallet.vel.x = 0;
} else {
aiMallet.vel.x = (dx > 0 ? 1 : -1) * moveSpeed;
}
aiMallet.vel.y = 0; // AI球拍仅在水平方向移动(简化 goalie 模式)
}
/**
* 限制AI球拍在防守半区内
*/
public void clampPosition(double tableHeight) {
double minY = 0;
double maxY = tableHeight / 2 - aiMallet.radius;
aiMallet.pos.y = Math.max(minY, Math.min(maxY, aiMallet.pos.y));
aiMallet.pos.x = Math.max(-tableHalfWidth + aiMallet.radius,
Math.min(tableHalfWidth - aiMallet.radius,
aiMallet.pos.x));
}
}
完整游戏框架与主循环
/**
* 冰球游戏主控类
*/
public class AirHockeyGame {
private static final double TABLE_W = 400;
private static final double TABLE_H = 700;
private static final double PUCK_R = 15;
private static final double MALLET_R = 25;
private static final double DT = 1.0 / 60.0;
private static final double FRICTION = 0.995;
private RigidBody puck;
private RigidBody playerMallet;
private RigidBody aiMallet;
private AIGoalie ai;
private int playerScore = 0;
private int aiScore = 0;
public AirHockeyGame() {
// 冰球质量设为1,球拍质量设为5(球拍几乎不动)
puck = new RigidBody(0, 0, PUCK_R, 1.0);
playerMallet = new RigidBody(0, -200, MALLET_R, 5.0);
aiMallet = new RigidBody(0, 200, MALLET_R, 5.0);
ai = new AIGoalie(aiMallet, 150, 300, TABLE_W);
resetPuck();
}
private void resetPuck() {
puck.pos = new Vec2(0, 0);
puck.vel = new Vec2((Math.random() - 0.5) * 200,
(Math.random() > 0.5 ? 1 : -1) * 200);
}
public void update() {
// 1. 物理更新
puck.update(DT, FRICTION);
playerMallet.update(DT, FRICTION);
aiMallet.update(DT, FRICTION);
// 2. AI决策
ai.update(puck);
ai.clampPosition(TABLE_H);
// 3. 碰撞处理
PhysicsEngine.resolveCollision(puck, playerMallet);
PhysicsEngine.resolveCollision(puck, aiMallet);
int goal = PhysicsEngine.resolveBoundaryCollision(puck, TABLE_W, TABLE_H);
// 4. 得分判定
if (goal == 1) { playerScore++; resetPuck(); }
else if (goal == 2) { aiScore++; resetPuck(); }
// 5. 限制玩家球拍位置
clampPlayerMallet();
}
private void clampPlayerMallet() {
double hw = TABLE_W / 2 - MALLET_R;
double hh = TABLE_H / 2 - MALLET_R;
playerMallet.pos.x = Math.max(-hw, Math.min(hw, playerMallet.pos.x));
playerMallet.pos.y = Math.max(-hh, Math.min(0, playerMallet.pos.y));
}
// Getters for rendering...
public RigidBody getPuck() { return puck; }
public RigidBody getPlayerMallet() { return playerMallet; }
public RigidBody getAiMallet() { return aiMallet; }
public int getPlayerScore() { return playerScore; }
public int getAiScore() { return aiScore; }
}
算法复杂度分析
| 模块 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 碰撞检测 | $O(1)$ | $O(1)$ | 每帧固定检测两组碰撞(puck vs 两个mallets) |
| 碰撞响应 | $O(1)$ | $O(1)$ | 向量运算均为常数时间 |
| AI轨迹预测 | $O(1)$ | $O(1)$ | 线性运动方程直接求解 |
| 整体帧更新 | $O(1)$ | $O(1)$ | 与对象数量无关的常数时间 |
扩展思路
- 多级AI难度:初级AI仅做轨迹预测;中级AI引入击球力度控制;高级AI可预测多次反弹并主动进攻
- 旋转模拟:引入冰球的角速度,碰撞时考虑切向摩擦力产生的旋转效应
- 机器学习增强:使用强化学习(如PPO)训练AI,让AI自主学习最优击球角度和力度
总结
本文实现了一个完整的冰球物理对战系统,核心算法包括:
– 向量分解法处理弹性碰撞,将二维问题转化为一维法向碰撞+切向守恒
– 线性轨迹预测使AI能够提前移动到拦截位置,实现类人反应
– 离散时间步模拟以固定帧率更新物理状态,保证游戏在不同设备上的行为一致性
完整代码可直接扩展为带Swing/JavaFX渲染的完整游戏,物理引擎部分亦可复用于其他圆形刚体碰撞场景。