游戏背景与问题建模
跳一跳是微信推出的一款极简休闲游戏,玩家通过按压屏幕控制小人从一个平台跳跃到下一个平台。按压时间越长,跳跃距离越远。核心挑战在于:如何根据当前平台与目标平台的距离,精确计算出所需的按压时长。
本文将跳一跳抽象为物理抛物线问题,用Java实现一个完整的跳跃模拟与最优落点计算系统,包含物理引擎、距离估计和贪心策略决策。
核心算法设计
1. 物理抛物线模型
假设小人起跳时获得水平初速度 $v_x$ 和垂直初速度 $v_y$,在重力加速度 $g$ 作用下做抛物线运动:
- 水平位移:$x(t) = v_x \cdot t$
- 垂直位移:$y(t) = v_y \cdot t – \frac{1}{2} g t^2$
小人落回起跳高度时,$y(t) = 0$,解得飞行时间 $T = \frac{2v_y}{g}$,水平射程 $R = v_x \cdot T = \frac{2v_x v_y}{g}$。
2. 按压时长与初速度映射
游戏中按压时长 $\Delta t$ 与初速度呈线性关系。设按压时长映射系数为 $k$,则:
– $v_x = k \cdot \Delta t \cdot \cos\theta$
– $v_y = k \cdot \Delta t \cdot \sin\theta$
其中 $\theta$ 为起跳角度(通常固定为45度以获得最远距离)。
3. 贪心最优落点策略
对于每个目标平台,我们需要:
1. 计算当前位置到目标平台中心的水平距离
2. 反推所需的按压时长
3. 考虑平台尺寸,选择平台中心作为最优落点以获得最高分数
Java实现
package jumpgame;
import java.util.ArrayList;
import java.util.List;
/**
* 跳一跳物理模拟与最优落点计算系统
* 核心算法:抛物线物理模型 + 贪心最优落点策略
*/
public class JumpOneJump {
// 物理常量
private static final double GRAVITY = 9.8; // 重力加速度 (m/s^2)
private static final double PRESS_FACTOR = 2.5; // 按压时长到速度映射系数
private static final double JUMP_ANGLE = Math.PI / 4; // 起跳角度 (45度)
/**
* 平台类,表示游戏中的落脚平台
*/
static class Platform {
double centerX, centerY; // 平台中心坐标
double width, depth; // 平台宽度和深度
int scoreBonus; // 平台中心奖励分数
public Platform(double centerX, double centerY, double width, double depth, int scoreBonus) {
this.centerX = centerX;
this.centerY = centerY;
this.width = width;
this.depth = depth;
this.scoreBonus = scoreBonus;
}
/**
* 判断落点是否在平台范围内
*/
boolean contains(double x, double y) {
return Math.abs(x - centerX) <= width / 2 && Math.abs(y - centerY) <= depth / 2;
}
}
/**
* 跳跃轨迹类,存储抛物线上的离散点
*/
static class Trajectory {
List<double[]> points = new ArrayList<>(); // [x, y, t] 三维点
double flightTime; // 总飞行时间
double landingX, landingY; // 落点坐标
public Trajectory(double landingX, double landingY, double flightTime) {
this.landingX = landingX;
this.landingY = landingY;
this.flightTime = flightTime;
}
}
/**
* 计算给定按压时长的跳跃轨迹
* @param pressTime 按压时长 (毫秒)
* @param startX 起跳点X坐标
* @param startY 起跳点Y坐标
* @param angle 跳跃方向角度 (弧度)
* @return 跳跃轨迹对象
*/
public Trajectory calculateTrajectory(double pressTime, double startX, double startY, double angle) {
// 根据按压时长计算初速度
double velocity = PRESS_FACTOR * pressTime / 1000.0; // 转换为秒
double vx = velocity * Math.cos(JUMP_ANGLE) * Math.cos(angle);
double vy = velocity * Math.cos(JUMP_ANGLE) * Math.sin(angle);
double vz = velocity * Math.sin(JUMP_ANGLE); // 垂直方向初速度
// 计算飞行时间:落回同一高度时 vz * t - 0.5 * g * t^2 = 0
double flightTime = 2 * vz / GRAVITY;
// 计算落点
double landingX = startX + vx * flightTime;
double landingY = startY + vy * flightTime;
Trajectory traj = new Trajectory(landingX, landingY, flightTime);
// 采样轨迹点用于可视化
int samples = 50;
for (int i = 0; i <= samples; i++) {
double t = flightTime * i / samples;
double x = startX + vx * t;
double y = startY + vy * t;
double z = vz * t - 0.5 * GRAVITY * t * t;
traj.points.add(new double[]{x, y, z, t});
}
return traj;
}
/**
* 贪心策略:计算跳到目标平台中心所需的最优按压时长
* @param startX 当前X坐标
* @param startY 当前Y坐标
* @param target 目标平台
* @return 最优按压时长 (毫秒),若无法到达返回 -1
*/
public double calculateOptimalPressTime(double startX, double startY, Platform target) {
// 计算到平台中心的水平距离
double dx = target.centerX - startX;
double dy = target.centerY - startY;
double horizontalDistance = Math.sqrt(dx * dx + dy * dy);
// 计算跳跃方向角
double angle = Math.atan2(dy, dx);
// 反推按压时长:R = 2 * v^2 * cos(θ) * sin(θ) / g = v^2 * sin(2θ) / g
// v = PRESS_FACTOR * pressTime / 1000
// 解得 pressTime = 1000 * sqrt(R * g / sin(2θ)) / PRESS_FACTOR
double sin2Theta = Math.sin(2 * JUMP_ANGLE);
if (sin2Theta < 0.001) return -1; // 避免除零
double velocityNeeded = Math.sqrt(horizontalDistance * GRAVITY / sin2Theta);
double pressTime = velocityNeeded * 1000.0 / PRESS_FACTOR;
// 验证落点是否在平台范围内(考虑平台边缘容错)
Trajectory traj = calculateTrajectory(pressTime, startX, startY, angle);
if (target.contains(traj.landingX, traj.landingY)) {
return pressTime;
}
// 如果中心落点失败,尝试在平台范围内找到最佳落点
return findBestLandingPressTime(startX, startY, target, angle, horizontalDistance);
}
/**
* 在平台范围内搜索最佳落点对应的按压时长
* 使用二分搜索在平台边缘附近寻找可行解
*/
private double findBestLandingPressTime(double startX, double startY,
Platform target, double angle, double distance) {
double low = 0, high = 5000; // 按压时长范围 0~5000ms
double bestPressTime = -1;
double minError = Double.MAX_VALUE;
// 二分搜索100次精化
for (int i = 0; i < 100; i++) {
double mid = (low + high) / 2;
Trajectory traj = calculateTrajectory(mid, startX, startY, angle);
double error = Math.sqrt(
Math.pow(traj.landingX - target.centerX, 2) +
Math.pow(traj.landingY - target.centerY, 2)
);
if (target.contains(traj.landingX, traj.landingY)) {
// 在平台内,记录更优解(更接近中心)
if (error < minError) {
minError = error;
bestPressTime = mid;
}
high = mid; // 尝试更短的按压
} else {
// 不在平台内,判断是跳近了还是跳远了
double landingDist = Math.sqrt(
Math.pow(traj.landingX - startX, 2) +
Math.pow(traj.landingY - startY, 2)
);
if (landingDist < distance) {
low = mid; // 跳近了,增加按压
} else {
high = mid; // 跳远了,减少按压
}
}
}
return bestPressTime;
}
/**
* 连续跳跃序列规划:贪心选择下一个最优平台
* @param platforms 所有可用平台列表
* @param startIdx 起始平台索引
* @return 跳跃序列(按压时长列表)
*/
public List<Double> planJumpSequence(List<Platform> platforms, int startIdx) {
List<Double> sequence = new ArrayList<>();
int currentIdx = startIdx;
double currentX = platforms.get(currentIdx).centerX;
double currentY = platforms.get(currentIdx).centerY;
// 贪心策略:每次选择距离适中且奖励最高的可达平台
boolean[] visited = new boolean[platforms.size()];
visited[currentIdx] = true;
while (true) {
int bestNext = -1;
double bestScore = -1;
double bestPressTime = -1;
for (int i = 0; i < platforms.size(); i++) {
if (visited[i]) continue;
Platform next = platforms.get(i);
double pressTime = calculateOptimalPressTime(currentX, currentY, next);
if (pressTime > 0) {
// 评分函数:奖励分数 / (按压时长 + 距离惩罚)
double dist = Math.sqrt(
Math.pow(next.centerX - currentX, 2) +
Math.pow(next.centerY - currentY, 2)
);
double score = next.scoreBonus / (1 + pressTime / 1000 + dist / 10);
if (score > bestScore) {
bestScore = score;
bestNext = i;
bestPressTime = pressTime;
}
}
}
if (bestNext == -1) break; // 无处可跳
sequence.add(bestPressTime);
visited[bestNext] = true;
currentIdx = bestNext;
currentX = platforms.get(currentIdx).centerX;
currentY = platforms.get(currentIdx).centerY;
}
return sequence;
}
public static void main(String[] args) {
JumpOneJump game = new JumpOneJump();
// 创建测试平台序列
List<Platform> platforms = new ArrayList<>();
platforms.add(new Platform(0, 0, 2, 2, 1)); // 起点
platforms.add(new Platform(3, 1, 2, 2, 2)); // 近距离
platforms.add(new Platform(7, 2, 1.5, 1.5, 3)); // 中距离
platforms.add(new Platform(12, 0, 2, 2, 2)); // 远距离
platforms.add(new Platform(15, 3, 1, 1, 5)); // 高难度高分
// 计算跳到第二个平台的最优按压时长
double pressTime = game.calculateOptimalPressTime(0, 0, platforms.get(1));
System.out.printf("跳到第2个平台的最优按压时长: %.2f ms%n", pressTime);
// 显示轨迹
Trajectory traj = game.calculateTrajectory(pressTime, 0, 0,
Math.atan2(1, 3));
System.out.printf("飞行时间: %.2f s, 落点: (%.2f, %.2f)%n",
traj.flightTime, traj.landingX, traj.landingY);
// 规划完整跳跃序列
List<Double> sequence = game.planJumpSequence(platforms, 0);
System.out.println("\n最优跳跃序列(按压时长 ms):");
for (int i = 0; i < sequence.size(); i++) {
System.out.printf(" 第%d跳: %.2f ms%n", i + 1, sequence.get(i));
}
}
}
算法复杂度分析
| 操作 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 单轨迹计算 | $O(n)$,$n$ 为采样点数 | $O(n)$ |
| 最优按压时长(二分搜索) | $O(k \cdot n)$,$k$ 为二分迭代次数 | $O(n)$ |
| 序列规划(贪心) | $O(m^2 \cdot k \cdot n)$,$m$ 为平台数 | $O(m)$ |
扩展思考
- 视觉距离估计:实际游戏中需要通过屏幕图像识别来估算平台距离,可使用边缘检测或深度学习方法。
- 动态障碍物:加入移动平台后,需要预测平台位置,引入时间维度进行预判跳跃。
- 强化学习优化:使用DQN等算法让AI自主学习最优按压策略,适应不同平台布局。
总结
本文通过物理抛物线模型将跳一跳游戏抽象为数学优化问题,使用Java实现了轨迹计算、最优落点搜索和贪心序列规划。核心算法简单高效,时间复杂度可控,适合作为物理模拟与贪心策略的经典教学案例。