引言
滚珠迷宫(Marble Maze)是一款经典的物理益智游戏:玩家通过倾斜游戏板控制重力方向,引导金属小球在复杂迷宫中滚动,避开陷阱和孔洞,最终到达目标出口。本文用Java实现一个完整的滚珠迷宫游戏,核心算法包括:基于欧拉积分的刚体滚动物理、碰撞检测与响应、以及A星寻路算法在动态重力场中的应用。
核心算法概述
1. 滚动物理模型
小球在倾斜平面上的运动由重力和摩擦力共同决定。重力沿斜面方向的分量提供加速度,滚动摩擦力则与运动方向相反:
a_gravity = g · sin(θ) // 沿倾斜方向
a_friction = -μ · g · cos(θ) · v̂ // 与速度方向相反
其中 θ 为倾斜角,μ 为滚动摩擦系数。本文采用欧拉法进行数值积分。
2. 碰撞检测与响应
迷宫由墙壁(线段)构成,小球与墙壁的碰撞检测使用线段-圆碰撞算法:
计算圆心到线段的最近点
d = |圆心 - 最近点|
若 d < 球半径:碰撞发生
碰撞响应:沿法线方向弹回,速度反射并乘以弹性系数
3. A星最短路径
在已知迷宫地图的条件下,使用A星算法预先计算从起点到终点的最优路径。由于重力可改变球的滚动方向,传统的四连通/八连通网格扩展为基于可行滚动方向的邻居搜索。
Java实现
滚珠物理引擎
import java.util.ArrayList;
import java.util.List;
/**
* 滚珠物理引擎:模拟小球在倾斜迷宫中的滚动
*/
public class MarblePhysics {
// 物理常量
private static final double GRAVITY = 9.81; // m/s²
private static final double MARBLE_RADIUS = 0.008; // m (8mm)
private static final double MARBLE_MASS = 0.005; // kg (5g)
private static final double FRICTION_COEFF = 0.15; // 滚动摩擦系数
private static final double ELASTICITY = 0.7; // 碰撞弹性系数
private static final double TIME_STEP = 0.001; // s (1ms)
private static final double STOP_SPEED = 0.01; // 停止阈值
// 当前重力方向(由板的倾斜角度决定)
private double gravityAngleX = 0; // 绕X轴倾斜角 (rad)
private double gravityAngleY = 0; // 绕Y轴倾斜角 (rad)
/**
* 小球状态
*/
public static class Marble {
public double x, y; // 位置 (m)
public double vx, vy; // 速度 (m/s)
public boolean active;
public Marble(double x, double y) {
this.x = x; this.y = y;
this.vx = 0; this.vy = 0;
this.active = true;
}
public double speed() {
return Math.hypot(vx, vy);
}
}
/**
* 墙壁线段
*/
public static class Wall {
public double x1, y1, x2, y2;
public Wall(double x1, double y1, double x2, double y2) {
this.x1=x1; this.y1=y1; this.x2=x2; this.y2=y2;
}
}
private Marble marble;
private List<Wall> walls = new ArrayList<>();
private List<Hole> holes = new ArrayList<>();
/**
* 陷阱孔洞
*/
public static class Hole {
public double x, y, radius;
public Hole(double x, double y, double r) { this.x=x; this.y=y; this.radius=r; }
}
public void setMarble(Marble m) { this.marble = m; }
public void addWall(Wall w) { walls.add(w); }
public void addHole(Hole h) { holes.add(h); }
/**
* 设置板的倾斜角度
*/
public void setTilt(double angleX, double angleY) {
// 限制最大倾斜角 ±30度
double maxTilt = Math.toRadians(30);
this.gravityAngleX = Math.max(-maxTilt, Math.min(maxTilt, angleX));
this.gravityAngleY = Math.max(-maxTilt, Math.min(maxTilt, angleY));
}
/**
* 单步物理模拟
*/
public void step() {
if (!marble.active || marble.speed() < STOP_SPEED) {
// 即使速度很低,重力仍会导致滑动
if (gravityAngleX == 0 && gravityAngleY == 0) return;
}
// 计算重力加速度分量
double ax = GRAVITY * Math.sin(gravityAngleY);
double ay = GRAVITY * Math.sin(gravityAngleX);
// 摩擦力(与运动方向相反)
double speed = marble.speed();
if (speed > STOP_SPEED) {
double frictionDecel = FRICTION_COEFF * GRAVITY * TIME_STEP;
double nx = marble.vx / speed;
double ny = marble.vy / speed;
ax -= frictionDecel * nx;
ay -= frictionDecel * ny;
}
// 更新速度
marble.vx += ax * TIME_STEP;
marble.vy += ay * TIME_STEP;
// 更新位置
double newX = marble.x + marble.vx * TIME_STEP;
double newY = marble.y + marble.vy * TIME_STEP;
// 碰撞检测与响应
for (Wall w : walls) {
double[] resolved = resolveWallCollision(newX, newY, w);
newX = resolved[0];
newY = resolved[1];
}
marble.x = newX;
marble.y = newY;
// 检测是否落入陷阱
for (Hole h : holes) {
double dist = Math.hypot(marble.x - h.x, marble.y - h.y);
if (dist < h.radius - MARBLE_RADIUS * 0.5) {
marble.active = false;
return;
}
}
}
/**
* 线段-圆碰撞检测与响应
*/
private double[] resolveWallCollision(double cx, double cy, Wall w) {
// 计算圆心到线段的最近点
double dx = w.x2 - w.x1;
double dy = w.y2 - w.y1;
double lenSq = dx*dx + dy*dy;
double t = Math.max(0, Math.min(1,
((cx-w.x1)*dx + (cy-w.y1)*dy) / lenSq));
double closestX = w.x1 + t * dx;
double closestY = w.y1 + t * dy;
double distX = cx - closestX;
double distY = cy - closestY;
double dist = Math.hypot(distX, distY);
if (dist < MARBLE_RADIUS && dist > 1e-9) {
// 碰撞法向量
double nx = distX / dist;
double ny = distY / dist;
// 位置校正
double overlap = MARBLE_RADIUS - dist;
cx += nx * overlap;
cy += ny * overlap;
// 速度反射
double dot = marble.vx * nx + marble.vy * ny;
if (dot < 0) { // 只有朝向墙壁的速度分量才反射
marble.vx -= (1 + ELASTICITY) * dot * nx;
marble.vy -= (1 + ELASTICITY) * dot * ny;
}
}
return new double[]{cx, cy};
}
public Marble getMarble() { return marble; }
public List<Wall> getWalls() { return walls; }
public List<Hole> getHoles() { return holes; }
}
A星寻路算法
import java.util.*;
/**
* A星寻路:在迷宫网格中寻找从起点到终点的最短路径
*/
public class AStarPathfinder {
/**
* 网格节点
*/
public static class Node {
public int x, y;
public double g; // 从起点到当前节点的实际代价
public double h; // 启发式估计(到终点的欧氏距离)
public Node parent;
public Node(int x, int y) {
this.x = x; this.y = y;
this.g = Double.MAX_VALUE;
this.h = 0;
}
public double f() { return g + h; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Node)) return false;
Node n = (Node) o;
return x == n.x && y == n.y;
}
@Override
public int hashCode() { return Objects.hash(x, y); }
}
private boolean[][] walkable; // true=可通行
private int width, height;
public AStarPathfinder(boolean[][] walkable) {
this.walkable = walkable;
this.height = walkable.length;
this.width = walkable[0].length;
}
/**
* A星寻路
* @param start 起点 (grid坐标)
* @param goal 终点 (grid坐标)
* @return 路径节点列表,若无路径则返回空列表
*/
public List<Node> findPath(Node start, Node goal) {
PriorityQueue<Node> openSet = new PriorityQueue<>(Comparator.comparingDouble(Node::f));
Set<Node> closedSet = new HashSet<>();
start.g = 0;
start.h = heuristic(start, goal);
openSet.add(start);
Map<String, Node> nodeMap = new HashMap<>();
nodeMap.put(key(start), start);
while (!openSet.isEmpty()) {
Node current = openSet.poll();
if (current.x == goal.x && current.y == goal.y) {
return reconstructPath(current);
}
closedSet.add(current);
for (Node neighbor : getNeighbors(current)) {
if (closedSet.contains(neighbor)) continue;
double tentativeG = current.g + distance(current, neighbor);
String nKey = key(neighbor);
Node existing = nodeMap.get(nKey);
if (existing == null || tentativeG < existing.g) {
Node n = existing != null ? existing : new Node(neighbor.x, neighbor.y);
n.g = tentativeG;
n.h = heuristic(n, goal);
n.parent = current;
nodeMap.put(nKey, n);
openSet.remove(n);
openSet.add(n);
}
}
}
return new ArrayList<>(); // 无路径
}
/**
* 获取邻居节点(8方向)
*/
private List<Node> getNeighbors(Node n) {
List<Node> neighbors = new ArrayList<>();
int[][] dirs = {{-1,-1},{-1,0},{-1,1},{0,-1},{0,1},{1,-1},{1,0},{1,1}};
for (int[] d : dirs) {
int nx = n.x + d[0];
int ny = n.y + d[1];
if (nx >= 0 && nx < width && ny >= 0 && ny < height && walkable[ny][nx]) {
neighbors.add(new Node(nx, ny));
}
}
return neighbors;
}
private double heuristic(Node a, Node b) {
// 欧氏距离
return Math.hypot(a.x - b.x, a.y - b.y);
}
private double distance(Node a, Node b) {
// 对角线移动代价为√2,直线为1
return Math.hypot(a.x - b.x, a.y - b.y);
}
private String key(Node n) {
return n.x + "," + n.y;
}
private List<Node> reconstructPath(Node goal) {
List<Node> path = new ArrayList<>();
Node current = goal;
while (current != null) {
path.add(0, current);
current = current.parent;
}
return path;
}
}
迷宫生成器与游戏主控
import java.util.*;
/**
* 迷宫生成器:使用递归分割算法生成随机迷宫
*/
public class MazeGenerator {
/**
* 使用递归分割生成迷宫
* @param width 网格宽度
* @param height 网格高度
* @return true表示可通行,false表示墙壁
*/
public boolean[][] generateMaze(int width, int height) {
boolean[][] maze = new boolean[height][width];
// 初始全部可通行
for (int y = 0; y < height; y++) {
Arrays.fill(maze[y], true);
}
// 添加外边界墙
for (int x = 0; x < width; x++) { maze[0][x] = false; maze[height-1][x] = false; }
for (int y = 0; y < height; y++) { maze[y][0] = false; maze[y][width-1] = false; }
// 递归分割内部
divide(maze, 1, 1, width - 2, height - 2);
// 确保起点和终点可通行
maze[1][1] = true;
maze[height-2][width-2] = true;
return maze;
}
private void divide(boolean[][] maze, int x, int y, int w, int h) {
if (w < 3 || h < 3) return;
Random rand = new Random();
// 选择水平或垂直分割
boolean horizontal = h > w || (h == w && rand.nextBoolean());
if (horizontal) {
int wy = y + rand.nextInt(h - 2) + 1;
// 确保wy为奇数,保持通道宽度
if (wy % 2 == 0) wy = wy + 1 >= y + h ? wy - 1 : wy + 1;
int hole = x + rand.nextInt(w);
for (int i = x; i < x + w; i++) {
if (i != hole) maze[wy][i] = false;
}
divide(maze, x, y, w, wy - y);
divide(maze, x, wy + 1, w, y + h - wy - 1);
} else {
int wx = x + rand.nextInt(w - 2) + 1;
if (wx % 2 == 0) wx = wx + 1 >= x + w ? wx - 1 : wx + 1;
int hole = y + rand.nextInt(h);
for (int i = y; i < y + h; i++) {
if (i != hole) maze[i][wx] = false;
}
divide(maze, x, y, wx - x, h);
divide(maze, wx + 1, y, x + w - wx - 1, h);
}
}
/**
* 将网格迷宫转换为物理墙壁列表
*/
public List<MarblePhysics.Wall> mazeToWalls(boolean[][] maze, double cellSize) {
List<MarblePhysics.Wall> walls = new ArrayList<>();
int h = maze.length;
int w = maze[0].length;
for (int y = 0; y < h; y++) {
for (int x = 0; x < w; x++) {
if (!maze[y][x]) {
double cx = x * cellSize;
double cy = y * cellSize;
// 每个墙壁单元格生成4条边
walls.add(new MarblePhysics.Wall(cx, cy, cx + cellSize, cy));
walls.add(new MarblePhysics.Wall(cx + cellSize, cy, cx + cellSize, cy + cellSize));
walls.add(new MarblePhysics.Wall(cx + cellSize, cy + cellSize, cx, cy + cellSize));
walls.add(new MarblePhysics.Wall(cx, cy + cellSize, cx, cy));
}
}
}
return walls;
}
}
主程序与可视化框架
import javax.swing.*;
import java.awt.*;
import java.util.List;
/**
* 滚珠迷宫游戏主程序
*/
public class MarbleMazeGame extends JPanel {
private static final double CELL_SIZE = 0.02; // m (2cm per grid cell)
private static final int PIXEL_SCALE = 40; // pixels per meter
private MarblePhysics physics;
private boolean[][] mazeGrid;
private List<AStarPathfinder.Node> path;
private double goalX, goalY;
private java.util.List<double[]> trajectory = new java.util.ArrayList<>();
public MarbleMazeGame() {
setPreferredSize(new Dimension(600, 600));
setBackground(Color.DARK_GRAY);
initGame();
}
private void initGame() {
// 生成迷宫
MazeGenerator gen = new MazeGenerator();
int gridW = 25, gridH = 25;
mazeGrid = gen.generateMaze(gridW, gridH);
// 创建物理引擎
physics = new MarblePhysics();
physics.setMarble(new MarblePhysics.Marble(CELL_SIZE * 1.5, CELL_SIZE * 1.5));
// 添加墙壁
List<MarblePhysics.Wall> walls = gen.mazeToWalls(mazeGrid, CELL_SIZE);
for (MarblePhysics.Wall w : walls) physics.addWall(w);
// 添加陷阱孔洞
physics.addHole(new MarblePhysics.Hole(CELL_SIZE * 10, CELL_SIZE * 10, CELL_SIZE));
physics.addHole(new MarblePhysics.Hole(CELL_SIZE * 18, CELL_SIZE * 15, CELL_SIZE));
// 目标
goalX = (gridW - 2) * CELL_SIZE;
goalY = (gridH - 2) * CELL_SIZE;
// A星寻路
AStarPathfinder pf = new AStarPathfinder(mazeGrid);
path = pf.findPath(
new AStarPathfinder.Node(1, 1),
new AStarPathfinder.Node(gridW - 2, gridH - 2)
);
System.out.println("迷宫生成完成: " + gridW + "x" + gridH);
System.out.println("A星路径长度: " + path.size() + " 步");
}
/**
* 更新物理并记录轨迹
*/
public void updatePhysics() {
if (!physics.getMarble().active) return;
physics.step();
MarblePhysics.Marble m = physics.getMarble();
trajectory.add(new double[]{m.x, m.y});
// 检测到达终点
if (Math.hypot(m.x - goalX, m.y - goalY) < CELL_SIZE) {
System.out.println("恭喜到达终点!");
m.active = false;
}
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
int offsetX = 20, offsetY = 20;
// 绘制迷宫
g2d.setColor(Color.GRAY);
for (int y = 0; y < mazeGrid.length; y++) {
for (int x = 0; x < mazeGrid[0].length; x++) {
if (!mazeGrid[y][x]) {
int px = offsetX + (int)(x * CELL_SIZE * PIXEL_SCALE);
int py = offsetY + (int)(y * CELL_SIZE * PIXEL_SCALE);
int size = (int)(CELL_SIZE * PIXEL_SCALE) + 1;
g2d.fillRect(px, py, size, size);
}
}
}
// 绘制A星路径
if (path != null && path.size() > 1) {
g2d.setColor(new Color(0, 150, 0, 100));
for (int i = 0; i < path.size() - 1; i++) {
int x1 = offsetX + (int)(path.get(i).x * CELL_SIZE * PIXEL_SCALE + CELL_SIZE * PIXEL_SCALE / 2);
int y1 = offsetY + (int)(path.get(i).y * CELL_SIZE * PIXEL_SCALE + CELL_SIZE * PIXEL_SCALE / 2);
int x2 = offsetX + (int)(path.get(i+1).x * CELL_SIZE * PIXEL_SCALE + CELL_SIZE * PIXEL_SCALE / 2);
int y2 = offsetY + (int)(path.get(i+1).y * CELL_SIZE * PIXEL_SCALE + CELL_SIZE * PIXEL_SCALE / 2);
g2d.drawLine(x1, y1, x2, y2);
}
}
// 绘制陷阱
g2d.setColor(Color.BLACK);
for (MarblePhysics.Hole h : physics.getHoles()) {
int px = offsetX + (int)(h.x * PIXEL_SCALE);
int py = offsetY + (int)(h.y * PIXEL_SCALE);
int r = (int)(h.radius * PIXEL_SCALE);
g2d.fillOval(px - r, py - r, r * 2, r * 2);
}
// 绘制目标
g2d.setColor(Color.GREEN);
int gx = offsetX + (int)(goalX * PIXEL_SCALE);
int gy = offsetY + (int)(goalY * PIXEL_SCALE);
g2d.fillOval(gx - 8, gy - 8, 16, 16);
// 绘制轨迹
if (trajectory.size() > 1) {
g2d.setColor(new Color(255, 100, 100, 120));
for (int i = 1; i < trajectory.size(); i++) {
int x1 = offsetX + (int)(trajectory.get(i-1)[0] * PIXEL_SCALE);
int y1 = offsetY + (int)(trajectory.get(i-1)[1] * PIXEL_SCALE);
int x2 = offsetX + (int)(trajectory.get(i)[0] * PIXEL_SCALE);
int y2 = offsetY + (int)(trajectory.get(i)[1] * PIXEL_SCALE);
g2d.drawLine(x1, y1, x2, y2);
}
}
// 绘制小球
MarblePhysics.Marble m = physics.getMarble();
int mx = offsetX + (int)(m.x * PIXEL_SCALE);
int my = offsetY + (int)(m.y * PIXEL_SCALE);
int mr = Math.max(3, (int)(MarblePhysics.MARBLE_RADIUS * PIXEL_SCALE));
g2d.setColor(m.active ? Color.CYAN : Color.RED);
g2d.fillOval(mx - mr, my - mr, mr * 2, mr * 2);
}
/**
* 设置板的倾斜(模拟重力方向变化)
*/
public void setBoardTilt(double angleX, double angleY) {
physics.setTilt(angleX, angleY);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
JFrame frame = new JFrame("滚珠迷宫 — 重力控制与A星寻路");
MarbleMazeGame game = new MarbleMazeGame();
frame.add(game);
frame.pack();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
// 游戏循环
javax.swing.Timer timer = new javax.swing.Timer(16, e -> {
// 自动演示:沿A星路径方向倾斜
if (game.path != null && game.path.size() > 1 &&
game.physics.getMarble().active) {
autoTilt(game);
}
game.updatePhysics();
game.repaint();
});
timer.start();
});
}
private static void autoTilt(MarbleMazeGame game) {
MarblePhysics.Marble m = game.physics.getMarble();
// 找到路径上最近的点
double minDist = Double.MAX_VALUE;
int nearestIdx = 0;
for (int i = 0; i < game.path.size(); i++) {
AStarPathfinder.Node n = game.path.get(i);
double dist = Math.hypot(m.x - n.x * CELL_SIZE, m.y - n.y * CELL_SIZE);
if (dist < minDist) {
minDist = dist;
nearestIdx = i;
}
}
// 向下一个路径点倾斜
if (nearestIdx < game.path.size() - 1) {
AStarPathfinder.Node next = game.path.get(nearestIdx + 1);
double dx = next.x * CELL_SIZE - m.x;
double dy = next.y * CELL_SIZE - m.y;
double maxTilt = Math.toRadians(15);
game.setBoardTilt(
Math.max(-maxTilt, Math.min(maxTilt, dy * 5)),
Math.max(-maxTilt, Math.min(maxTilt, dx * 5))
);
}
}
}
算法复杂度分析
| 模块 | 时间复杂度 | 说明 |
|---|---|---|
| 物理单步 | O(w) | w为墙壁数量,每步需检测所有墙壁 |
| A星寻路 | O(b^d) | b为分支因子,d为解深度;实际约O(n log n) |
| 迷宫生成 | O(n) | n为网格单元数,递归分割线性时间 |
| 碰撞优化 | O(1) 平均 | 使用空间网格划分后仅检测邻近墙壁 |
扩展思考
- 空间哈希优化:将墙壁按空间网格划分,碰撞检测时仅查询小球所在网格及邻接网格,可将复杂度从O(w)降至O(1)。
- 强化学习控制:使用DQN或PPO训练AI自动学习板的倾斜策略,在复杂迷宫中实现零陷阱通关。
- 多人协作模式:扩展为双球同时控制,需要协调重力方向使两球都到达各自目标,可建模为多智能体路径规划问题。
总结
本文实现了一个完整的滚珠迷宫游戏系统,包含物理滚动模拟、线段-圆碰撞检测、递归分割迷宫生成、以及A星最短路径算法。自动演示模块展示了如何结合路径规划与物理控制,通过动态调整重力方向引导小球沿最优路径到达终点。该系统可作为物理益智游戏开发、机器人路径规划教学的参考实现。