有人用 Three.js 做了个在魔方上开车的游戏,六个面全能跑
Cube World 是开发者 Paul Robello 开源的一个 Three.js 项目:一辆车在魔方的表面行驶,路网覆盖全部六个面,面与面之间无缝衔接。更有意思的是,魔方会周期性地发生切片旋转,把正在行驶的路面和其他面重新拼在一起——车还在跑,世界已经变了。
在线 demo:https://paulrobello.github.io/cube-world/ (opens in a new tab)

难在哪里
在平面上生成路网不复杂,但在立方体六个面上做这件事,有两个核心问题需要解决:
第一,路怎么从一个面延伸到另一个面?
每个面的坐标系是独立的,车辆从 Top 面走到边缘,下一步踏入 Front 面,行和列的对应关系并不直接。
第二,魔方转动时,地块、建筑、车辆的状态怎么同步?
切片旋转会把一整行或一整列的地块搬到另一个面,每个地块的朝向要跟着调整,车辆的位置和行进方向也要同步更新,否则就乱了。
跨面衔接:邻接表 + step 函数
项目用一张静态的邻接表(ADJACENCY)描述六个面之间的拓扑关系,每条记录包含目标面、坐标变换函数和穿越后的行进方向:
interface AdjEntry {
face: FaceId;
transform: (row: number, col: number) => [number, number];
continueDir: Dir;
}
const ADJACENCY: Record<FaceId, Record<Dir, AdjEntry>> = {
top: {
north: { face: "back", transform: (_, c) => [2, c], continueDir: "north" },
south: { face: "front", transform: (_, c) => [0, c], continueDir: "south" },
east: { face: "right", transform: (r, _) => [r, 0], continueDir: "east" },
west: { face: "left", transform: (r, _) => [r, 2], continueDir: "west" },
},
// ...其余五个面
};transform 是关键——它把当前面边缘的坐标映射到相邻面对应的入口坐标。例如从 Top 面向北走出去,进入 Back 面时行坐标固定为 2,列坐标不变。
实际移动通过 step() 函数完成,每走一格先计算面内新坐标,越界时调用邻接表完成跨面转移:
function step(face: FaceId, row: number, col: number, dir: Dir) {
const [nr, nc] = moveInDir(row, col, dir);
// 面内移动
if (nr >= 0 && nr < 3 && nc >= 0 && nc < 3) {
return { face, row: nr, col: nc, dir };
}
// 跨面移动:查邻接表,做坐标变换
const adj = ADJACENCY[face]?.[dir];
if (!adj) return null;
const [newRow, newCol] = adj.transform(row, col);
return { face: adj.face, row: newRow, col: newCol, dir: adj.continueDir };
}程序化路网生成
路网由 generateRoadPath() 在游戏启动时随机生成,每次结果不同。生成逻辑基于 step() 迭代延伸,同时控制单面内的路块密度,避免路段集中在某一面:
export function generateRoadPath(): PathNode[] {
const path: PathNode[] = [];
const visited = new Set<string>();
// 随机选起始面和边缘入口点
let face: FaceId = allFaces[Math.floor(Math.random() * allFaces.length)];
const startEdge = edgeOptions[Math.floor(Math.random() * edgeOptions.length)];
// 最多延伸 22 步,倾向于在同一面不超过 3 个地块后跨面
while (path.length < 22) {
const key = `${face},${row},${col}`;
if (visited.has(key)) break;
visited.add(key);
path.push({ face, row, col });
const next = step(face, row, col, dir);
if (!next) break;
({ face, row, col, dir } = next);
}
return path;
}路径生成完成后,每个路块根据前后节点的进出方向被分类为直道、弯道、T 形路口或十字路口,非路块地块则随机放置建筑、公园等环境元素,并自动朝向相邻的道路。
切片旋转时的状态同步
切片旋转是难度最高的部分。rotateSlice() 接收轴向、层级和方向三个参数,把相关联的几个面的地块循环交换。以 Y 轴旋转为例,前→右→后→左四个面的对应行依次交换,每次跨面都调整地块旋转角度:
// Y 轴旋转:前 → 右 → 后 → 左
function rotateSliceY(index: number, dir: number) {
const faces = ["front", "right", "back", "left"] as FaceId[];
const saved = getRow(state[faces[0]], index);
for (let i = 0; i < 3; i++) {
const from = faces[i];
const to = faces[(i + 1) % 4];
const row = getRow(state[from], index);
// 每次跨面旋转 90°
adjustTiles(row, dir * 90);
setRow(state[to], index, row);
}
setRow(state[faces[0]], index, adjustTiles(saved, dir * 90 * 3));
}当旋转发生在边缘层(index 为 0 或 2)时,对应的整个面也要跟着旋转 90°,通过 rotateFace() 处理,坐标变换遵循标准的矩阵旋转公式:
function rotateFace(faceId: FaceId, dir: number) {
const original = deepCopy(state[faceId]);
for (let r = 0; r < 3; r++) {
for (let c = 0; c < 3; c++) {
const tile = original[r][c];
tile.rotation = (tile.rotation + dir * 90 + 360) % 360;
// 顺时针:新位置 (c, 2-r);逆时针:新位置 (2-c, r)
if (dir === 1) state[faceId][c][2 - r] = tile;
else state[faceId][2 - c][r] = tile;
}
}
}旋转完成后,系统检测车辆是否还在道路上,如果脱轨则触发粒子效果并重新生成路网。
本地运行
需要提前安装 Node.js。
git clone https://github.com/paulrobello/cube-world.git
cd cube-world
npm install
npm run dev启动后访问本地地址即可进入游戏。控制面板支持切换昼夜、相机视角(跟随 / 俯视 / 电影感 / 车内)、音效开关和手动重新生成世界。
写在最后
Cube World 在技术上有两个值得关注的地方:一是用邻接表统一描述六面拓扑,把"跨面移动"这件事收敛到一个可复用的 step() 函数里,路网生成和车辆移动都建立在同一套抽象上;二是切片旋转时多个对象的状态同步,地块坐标、朝向、路面角度、车辆位置一次性全部对齐,没有遗漏。
它是一个完整可运行的游戏 demo,不是库,用法是克隆后直接跑,或者拆取其中的邻接表 + step 寻路逻辑用在自己的项目里。对想研究 Three.js 在非平面空间下做路网生成或状态同步的开发者,源码值得翻一翻。
GitHub 地址:https://github.com/paulrobello/cube-world (opens in a new tab)