“合成大西瓜”是一款曾风靡全网的休闲益智小游戏。游戏的核心机制是:玩家从顶部投放不同大小的圆形水果,水果受重力下落并在底部堆叠;当两个相同类型的水果接触时,会合并为更大一级的水果。本文用Java实现该游戏的核心物理引擎,重点讲解圆形刚体的碰撞检测与响应、空间划分加速,以及基于贪心策略的合并时机决策算法。
一、游戏物理模型
1.1 水果状态表示
每个水果由以下属性描述:
- 位置 ((x, y))、半径 (r)、质量 (m \propto r^2)
- 速度 ((v_x, v_y))、角速度 (\omega)
- 类型等级 (level \in {0, 1, …, 10}),等级越高半径越大
1.2 运动方程
水果在重力作用下下落,同时受底部边界和其他水果的碰撞约束:
[\ddot{x} = 0, \quad \ddot{y} = -g]
当水果间发生碰撞时,采用基于冲量的碰撞响应模型。对于两个质量分别为 (m_1, m_2)、恢复系数为 (e) 的圆形物体,沿碰撞法线方向的相对速度在碰撞后变为 (-e) 倍碰撞前相对速度。
1.3 合并条件
两个水果合并需同时满足:
- 圆心距离 (d < r_1 + r_2 + \epsilon)(接触或轻微重叠)
- 类型等级相同:(level_1 = level_2)
- 两者均处于”稳定期”(碰撞后经过一段冷却时间,防止连续合并)
合并后新水果的等级为 (level + 1),半径按预设比例增长。
二、核心算法设计
2.1 圆形碰撞检测
最直接的方法是 (O(n^2)) 两两检测,但当水果数量达到数十个时效率低下。本文采用均匀网格空间划分(Uniform Grid Spatial Hashing):将游戏区域划分为固定大小的网格单元,每个水果根据其位置映射到一个或多个网格中。碰撞检测时,只需检查同一网格及相邻网格中的水果对,平均复杂度降至 (O(n))。
2.2 基于冲量的碰撞响应
对于碰撞对 ((A, B)):
- 计算碰撞法线:(\vec{n} = (B – A) / |B – A|)
- 计算相对速度在法线方向的分量:(v_{rel} = (\vec{v}_B – \vec{v}_A) \cdot \vec{n})
- 若 (v_{rel} > 0)(正在分离),跳过
- 计算冲量大小:(j = -(1+e) v_{rel} / (1/m_A + 1/m_B))
- 应用冲量:(\vec{v}_A -= j/m_A \cdot \vec{n}),(\vec{v}_B += j/m_B \cdot \vec{n})
2.3 位置修正
为避免水果相互穿透导致的数值不稳定性,每次碰撞响应后进行位置修正:将两水果沿碰撞法线方向推开,推开距离与穿透深度成正比。
2.4 贪心合并策略
AI自动玩”合成大西瓜”时,需要在每个回合选择最佳投放位置和水果类型。本文实现一个简化版贪心策略:
- 空间评估:扫描底部区域,找到”最大连续空档”
- 对齐投放:将水果投放在与底部最大水果相同的水平位置,以增加碰撞合并概率
- 高度惩罚:避免在已堆叠较高区域投放,防止提前触顶
三、完整Java实现
import java.util.*;
/**
* 合成大西瓜物理引擎与贪心策略
* 运用圆形碰撞检测、冲量响应与空间划分加速
*/
public class BigWatermelon {
// 物理常量
private static final double G = 9.8; // 重力加速度
private static final double DT = 0.016; // 时间步长 (60FPS)
private static final double RESTITUTION = 0.3; // 恢复系数(非弹性碰撞)
private static final double FRICTION = 0.98; // 速度衰减
private static final double MERGE_COOLDOWN = 0.3; // 合并冷却时间(s)
private static final double BOUNDS_WIDTH = 6.0; // 游戏区域宽度
private static final double BOUNDS_HEIGHT = 8.0; // 游戏区域高度
// 水果半径配置(等级0到10)
private static final double[] RADII = {
0.15, 0.20, 0.25, 0.30, 0.35,
0.40, 0.50, 0.60, 0.75, 0.90, 1.10
};
/** 水果实体 */
static class Fruit {
double x, y; // 位置
double vx, vy; // 速度
int level; // 类型等级
double radius; // 半径
double mass; // 质量
double cooldown; // 合并冷却计时
boolean merged; // 是否已被合并
int id; // 唯一标识
static int nextId = 0;
Fruit(double x, double y, int level) {
this.x = x; this.y = y;
this.vx = 0; this.vy = 0;
this.level = level;
this.radius = RADII[level];
this.mass = radius * radius; // 质量 ∝ 面积
this.cooldown = MERGE_COOLDOWN;
this.merged = false;
this.id = nextId++;
}
}
private final List<Fruit> fruits;
private final Random random;
public BigWatermelon() {
this.fruits = new ArrayList<>();
this.random = new Random(42);
}
/**
* 空间划分:均匀网格哈希
* 网格大小取最大水果直径
*/
private Map<Long, List<Fruit>> buildSpatialGrid() {
Map<Long, List<Fruit>> grid = new HashMap<>();
double cellSize = RADII[RADII.length - 1] * 2.5;
for (Fruit f : fruits) {
if (f.merged) continue;
long gx = (long) Math.floor(f.x / cellSize);
long gy = (long) Math.floor(f.y / cellSize);
long key = (gx << 32) ^ (gy & 0xffffffffL);
grid.computeIfAbsent(key, k -> new ArrayList<>()).add(f);
}
return grid;
}
/**
* 检测并处理所有碰撞对
*/
private void resolveCollisions() {
Map<Long, List<Fruit>> grid = buildSpatialGrid();
double cellSize = RADII[RADII.length - 1] * 2.5;
Set<String> processed = new HashSet<>();
for (Map.Entry<Long, List<Fruit>> entry : grid.entrySet()) {
long key = entry.getKey();
int gx = (int) (key >> 32);
int gy = (int) (key & 0xffffffffL);
// 检查当前格及8邻域
for (int dx = -1; dx <= 1; dx++) {
for (int dy = -1; dy <= 1; dy++) {
long nKey = (((long)(gx + dx)) << 32) ^ (((long)(gy + dy)) & 0xffffffffL);
List<Fruit> neighbors = grid.get(nKey);
if (neighbors == null) continue;
for (Fruit a : entry.getValue()) {
for (Fruit b : neighbors) {
if (a.id >= b.id) continue;
String pairKey = a.id + "-" + b.id;
if (processed.contains(pairKey)) continue;
processed.add(pairKey);
checkAndResolve(a, b);
}
}
}
}
}
}
private void checkAndResolve(Fruit a, Fruit b) {
if (a.merged || b.merged) return;
double dx = b.x - a.x;
double dy = b.y - a.y;
double dist = Math.hypot(dx, dy);
double minDist = a.radius + b.radius;
if (dist >= minDist || dist < 1e-9) return;
// 碰撞法线
double nx = dx / dist;
double ny = dy / dist;
// 相对速度
double dvx = b.vx - a.vx;
double dvy = b.vy - a.vy;
double vRel = dvx * nx + dvy * ny;
if (vRel > 0) return; // 正在分离
// 冲量响应
double invMassA = 1.0 / a.mass;
double invMassB = 1.0 / b.mass;
double j = -(1 + RESTITUTION) * vRel / (invMassA + invMassB);
a.vx -= j * invMassA * nx;
a.vy -= j * invMassA * ny;
b.vx += j * invMassB * nx;
b.vy += j * invMassB * ny;
// 位置修正(防止穿透)
double overlap = minDist - dist;
double correction = overlap * 0.5;
a.x -= correction * nx;
a.y -= correction * ny;
b.x += correction * nx;
b.y += correction * ny;
// 尝试合并
tryMerge(a, b);
}
private void tryMerge(Fruit a, Fruit b) {
if (a.level != b.level) return;
if (a.level >= RADII.length - 1) return; // 已达最大等级
if (a.cooldown > 0 || b.cooldown > 0) return;
a.merged = true;
b.merged = true;
// 新水果在碰撞中点生成
double mx = (a.x + b.x) / 2;
double my = (a.y + b.y) / 2;
Fruit merged = new Fruit(mx, my, a.level + 1);
// 继承部分动量
merged.vx = (a.vx * a.mass + b.vx * b.mass) / (a.mass + b.mass);
merged.vy = (a.vy * a.mass + b.vy * b.mass) / (a.mass + b.mass);
fruits.add(merged);
}
/**
* 单步物理更新
*/
public void step() {
// 更新位置和速度
for (Fruit f : fruits) {
if (f.merged) continue;
f.vy -= G * DT;
f.x += f.vx * DT;
f.y += f.vy * DT;
f.cooldown = Math.max(0, f.cooldown - DT);
// 边界约束
if (f.x - f.radius < -BOUNDS_WIDTH / 2) {
f.x = -BOUNDS_WIDTH / 2 + f.radius;
f.vx = Math.abs(f.vx) * RESTITUTION;
}
if (f.x + f.radius > BOUNDS_WIDTH / 2) {
f.x = BOUNDS_WIDTH / 2 - f.radius;
f.vx = -Math.abs(f.vx) * RESTITUTION;
}
if (f.y - f.radius < 0) {
f.y = f.radius;
f.vy = Math.abs(f.vy) * RESTITUTION;
}
// 顶部溢出检测
if (f.y + f.radius > BOUNDS_HEIGHT) {
f.vy *= 0.5;
}
// 摩擦衰减
f.vx *= FRICTION;
f.vy *= FRICTION;
}
// 碰撞检测与响应(迭代多次增加稳定性)
for (int i = 0; i < 3; i++) {
resolveCollisions();
}
// 清理已合并水果
fruits.removeIf(f -> f.merged);
}
/**
* 投放新水果
*/
public void dropFruit(double x, int level) {
fruits.add(new Fruit(x, BOUNDS_HEIGHT - 0.2, level));
}
/**
* 贪心策略:选择最佳投放位置
* 策略:寻找底部最大空档的中心,或对齐同等级水果
*/
public double greedyDropPosition(int level) {
// 统计底部各区域的水果分布
int bins = 20;
double binWidth = BOUNDS_WIDTH / bins;
double[] heights = new double[bins];
for (Fruit f : fruits) {
int bin = (int) ((f.x + BOUNDS_WIDTH / 2) / binWidth);
bin = Math.max(0, Math.min(bins - 1, bin));
heights[bin] = Math.max(heights[bin], f.y + f.radius);
}
// 寻找最低区域(最大空档)
int bestBin = bins / 2;
double minHeight = Double.MAX_VALUE;
for (int i = 0; i < bins; i++) {
// 加上中心偏好和高度惩罚
double score = heights[i] + Math.abs(i - bins / 2) * 0.1;
if (score < minHeight) {
minHeight = score;
bestBin = i;
}
}
// 检查是否有同等级水果可以对齐
for (Fruit f : fruits) {
if (f.level == level && f.y < BOUNDS_HEIGHT * 0.5) {
int bin = (int) ((f.x + BOUNDS_WIDTH / 2) / binWidth);
if (bin >= 0 && bin < bins && heights[bin] < heights[bestBin] + 1.0) {
return f.x; // 对齐投放
}
}
}
return -BOUNDS_WIDTH / 2 + (bestBin + 0.5) * binWidth;
}
/**
* 模拟一局游戏
*/
public void simulateGame(int turns) {
System.out.println("开始模拟 " + turns + " 回合...\n");
for (int i = 0; i < turns; i++) {
int level = random.nextInt(3); // 投放0-2级水果
double pos = greedyDropPosition(level);
dropFruit(pos, level);
// 物理模拟直到静止
for (int s = 0; s < 120; s++) step();
if ((i + 1) % 10 == 0) {
printStats(i + 1);
}
}
printStats(turns);
}
public void printStats(int turn) {
int count = 0;
int maxLevel = 0;
double maxHeight = 0;
for (Fruit f : fruits) {
count++;
maxLevel = Math.max(maxLevel, f.level);
maxHeight = Math.max(maxHeight, f.y + f.radius);
}
System.out.println("回合 " + turn + ": 水果数=" + count
+ ", 最高等级=" + maxLevel
+ "(" + RADII[maxLevel] + "m)"
+ ", 堆叠高度=" + String.format("%.2f", maxHeight) + "m");
}
// ==================== 主程序 ====================
public static void main(String[] args) {
System.out.println("===== 合成大西瓜物理引擎与贪心策略 =====\n");
BigWatermelon game = new BigWatermelon();
game.simulateGame(50);
System.out.println("\n【性能对比】碰撞检测优化效果");
int[] counts = {10, 50, 100, 200};
for (int n : counts) {
BigWatermelon test = new BigWatermelon();
for (int i = 0; i < n; i++) {
test.dropFruit(test.random.nextDouble() * 4 - 2, test.random.nextInt(4));
}
long start = System.nanoTime();
for (int s = 0; s < 60; s++) test.step();
long elapsed = System.nanoTime() - start;
System.out.println(" 水果数=" + n + ": 60帧模拟耗时="
+ String.format("%.2f", elapsed / 1e6) + "ms");
}
}
}
四、复杂度分析
| 模块 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 空间划分建表 | (O(n)) | (O(n)) | 每个水果映射到网格 |
| 碰撞检测 | 平均 (O(n)),最坏 (O(n^2)) | (O(n)) | 取决于空间分布均匀性 |
| 冲量响应 | (O(1)) | (O(1)) | 每对碰撞固定计算 |
| 物理步进 | (O(n + c)) | (O(n)) | (c)为碰撞对数 |
| 贪心策略 | (O(n)) | (O(b)) | (b)为区域划分数 |
空间划分将碰撞检测从 (O(n^2)) 降低到接近 (O(n)),在水果密集场景下加速效果尤为显著。
五、扩展思考
- 四叉树动态空间划分:当水果分布极不均匀时,均匀网格效率下降。可采用四叉树根据物体密度自适应划分空间,进一步减少无效检测。
- 蒙特卡洛树搜索(MCTS):将”合成大西瓜”建模为序列决策问题,用MCTS评估每个投放位置的长远收益,超越局部贪心策略。
- 连续碰撞检测(CCD):高速运动的小水果可能在单帧内穿透其他水果,采用基于时间细分的CCD可确保碰撞不遗漏。
“合成大西瓜”的物理引擎涵盖了刚体动力学、碰撞检测和启发式决策三大核心主题,是构建完整2D物理系统的优秀练手项目。