一、前言
本人喜欢玩魔方,之前摸鱼的时候发现有可以在线玩的小游戏,于是突发奇想是否可以自己实现一个,刚好这段时间 Trae 很火,那就借助Trae和Threejs来自己实现一个魔方小游戏吧。
👉️ 在线试玩地址
由于日常使用较多,本人已充值🥰

二、开发流程
2.1 项目初始化
当我向Trae描述"我需要基于Threejs和webpack生成一个前端3d魔方,请完成基础架构目录搭建"时,它立即理解了需求并生成了清晰的模块结构:

虽让我的描述很少,但是生成的内容很详细,其中包括:



核心代码较多不一一展示了

同时也生成readme和安装运行文档👍️👍️👍️


效果预览:渲染逻辑已经基本完成

2.2 核心功能开发
- 魔方状态检测(完成/未完成)
- 支持2~6阶魔方
- 支持复原和打乱逻辑

依旧是将需求直接丢给Trae ,直接一路点击应用即可😉😉😉
效果预览:

2.3 Bug解决
先贴一张图,看看大家能否能一眼看出bug在哪😆

解答:玩过魔方的小伙伴应该知道,这种魔方的顶点位置如果出现相同的颜色就代表无法复原,bug出现在打乱时只考虑了颜色的数量,而忽略了颜色的分布,从而导致出现无法复原的场景

依旧是将bug丢给Trae,完美解决,并且自行添加旋转动画

三、核心模块解析
3.1 基础场景搭建
javascript
export class Rubiks {
constructor(container) {
// 透视相机设置 - Trae自动选择了合适的视角参数
this.camera = new THREE.PerspectiveCamera(
45, // 视角
1, // 宽高比
0.1, // 近裁剪面
100 // 远裁剪面
);
this.camera.position.set(0, 0, 15); // 相机位置
// 场景初始化
this.scene = new THREE.Scene();
this.scene.background = new THREE.Color('#000');
// 渲染器配置 - Trae建议开启抗锯齿
this.renderer = new THREE.WebGLRenderer({
antialias: true, // 抗锯齿
alpha: true // 透明背景支持
});
// 响应式设计 - Trae自动添加的窗口缩放处理
window.addEventListener('resize', () => {
this.setSize(container);
this.render();
});
}
// 动态相机距离调整 - Trae的创新解决方案
setOrder(order) {
// 根据魔方阶数自动调整相机距离
const cube = new Cube(order);
const coarseSize = cube.getCoarseCubeSize(this.camera, {
w: this.renderer.domElement.clientWidth,
h: this.renderer.domElement.clientHeight
});
// 智能计算最佳视距
const ratio = Math.max(
2.2 / (winW / coarseSize),
2.2 / (winH / coarseSize)
);
this.camera.position.z *= ratio;
}
}
当我描述需要"自适应不同阶数魔方的显示"时,Trae不仅生成了基础代码,还提出了动态调整相机距离的方案,确保不同阶数的魔方都能完美显示在视口中。
3.2 魔方渲染逻辑
通过 THREE.Shape和贝塞尔曲线 先构造2d平面椭圆矩形(包括有颜色的面和黑色背景面,模拟正常魔方的外部和内部)
js
export function createSquare(color, element) {
const squareShape = new THREE.Shape();
const x = 0, y = 0;
// 创建圆角矩形
squareShape.moveTo(x - 0.4, y + 0.5);
squareShape.lineTo(x + 0.4, y + 0.5);
squareShape.bezierCurveTo(x + 0.5, y + 0.5, x + 0.5, y + 0.5, x + 0.5, y + 0.4);
squareShape.lineTo(x + 0.5, y - 0.4);
squareShape.bezierCurveTo(x + 0.5, y - 0.5, x + 0.5, y - 0.5, x + 0.4, y - 0.5);
squareShape.lineTo(x - 0.4, y - 0.5);
squareShape.bezierCurveTo(x - 0.5, y - 0.5, x - 0.5, y - 0.5, x - 0.5, y - 0.4);
squareShape.lineTo(x - 0.5, y + 0.4);
squareShape.bezierCurveTo(x - 0.5, y + 0.5, x - 0.5, y + 0.5, x - 0.4, y + 0.5);
const geometry = new THREE.ShapeGeometry(squareShape);
const material = new THREE.MeshBasicMaterial({ color });
const mesh = new THREE.Mesh(geometry, material);
mesh.scale.set(0.9, 0.9, 0.9);
const square = new SquareMesh(element);
square.add(mesh);
const mat2 = new THREE.MeshBasicMaterial({
color: 'black',
side: THREE.DoubleSide,
});
const plane = new THREE.Mesh(geometry, mat2);
plane.position.set(0, 0, -0.01);
square.add(plane);
const posX = element.pos.x;
const posY = element.pos.y;
const posZ = element.pos.z;
square.position.set(posX, posY, posZ);
square.lookAt(element.pos.clone().add(element.normal));
return square;
}
然后通过遍历魔方数据来生成整个魔方
js
for (let i = 0; i < this.data.elements.length; i++) {
const square = createSquare(
new THREE.Color(this.data.elements[i].color),
this.data.elements[i],
);
this.add(square);
}
this.state = new CubeState(this.squares);
3.3 魔方转动逻辑
射线检测,查看鼠标落在那个方块上并记录数据
js
operateStart(offsetX, offsetY) {
if (this.start) {
return; // 防止重复开始
}
this.start = true;
// 使用射线检测获取鼠标点击的方块
const intersect = this.getIntersects(offsetX, offsetY);
this._square = null;
if (intersect) {
// 记录选中的方块和起始位置
this._square = intersect.square;
this.startPos = new THREE.Vector2(offsetX, offsetY);
}
}
监听鼠标落下,抬起,移动,移出事件并绑定方法
js
mousedownHandle(event) {
event.preventDefault();
this.operateStart(event.offsetX, event.offsetY);
}
mouseupHandle(event) {
event.preventDefault();
this.operateEnd();
}
mousemoveHandle(event) {
event.preventDefault();
this.operateDrag(event.offsetX, event.offsetY, event.movementX, event.movementY);
}
mouseoutHandle(event) {
event.preventDefault();
this.operateEnd();
}
init() {
this.domElement.addEventListener("mousedown", this.mousedownHandle.bind(this));
this.domElement.addEventListener("mouseup", this.mouseupHandle.bind(this));
this.domElement.addEventListener("mousemove", this.mousemoveHandle.bind(this));
this.domElement.addEventListener("mouseout", this.mouseoutHandle.bind(this));
}
mousemoveHandle鼠标移动方法中实现具体转动逻辑
js
mousemoveHandle(event) {
event.preventDefault();
this.operateDrag(event.offsetX, event.offsetY, event.movementX, event.movementY);
}
operateDrag(offsetX, offsetY, movementX, movementY) {
if (this.start && this.lastOperateUnfinish === false) {
if (this._square) {
// 情况1:拖动某个方块 - 旋转对应层
const curMousePos = new THREE.Vector2(offsetX, offsetY);
this.cube.rotateOnePlane(
this.startPos, // 起始位置
curMousePos, // 当前位置
this._square, // 选中的方块
this.camera, // 相机
{w: this.domElement.clientWidth, h: this.domElement.clientHeight}
);
} else {
// 情况2:拖动空白处 - 旋转整个魔方
const dx = movementX;
const dy = -movementY;
// 根据移动距离计算旋转角度
const movementLen = Math.sqrt(dx * dx + dy * dy);
const cubeSize = this.cube.getCoarseCubeSize(this.camera, {
w: this.domElement.clientWidth,
h: this.domElement.clientHeight
});
const rotateAngle = Math.PI * movementLen / cubeSize;
// 计算旋转轴(垂直于移动方向)
const moveVect = new THREE.Vector2(dx, dy);
const rotateDir = moveVect.rotateAround(new THREE.Vector2(0, 0), Math.PI * 0.5);
// 执行旋转
rotateAroundWorldAxis(this.cube, new THREE.Vector3(rotateDir.x, rotateDir.y, 0), rotateAngle);
}
this.renderer.render(this.scene, this.camera);
}
}
鼠标抬起时会有自动对齐的逻辑,防止旋转到一半卡住不动
js
// 位置:src/js/Control.js 第129-132行
mouseupHandle(event) {
event.preventDefault();
this.operateEnd();
}
operateEnd() {
if (this.lastOperateUnfinish === false) {
if (this._square) {
// 创建自动对齐动画
const rotateAnimation = this.cube.getAfterRotateAnimation();
this.lastOperateUnfinish = true;
const animation = (time) => {
const next = rotateAnimation(time);
this.renderer.render(this.scene, this.camera);
if (next) {
requestAnimationFrame(animation);
} else {
// 动画结束,更新完成状态
if (window.setFinish) {
window.setFinish(this.cube.finish);
}
this.lastOperateUnfinish = false;
}
}
requestAnimationFrame(animation);
}
this.start = false;
this._square = null;
}
}
3.3 魔方打乱逻辑
模拟用户从初始完成状态 随机旋转n次后的状态作为打乱后的状态,当然旋转次数不能太小
js
// 执行单次随机转动
performRandomRotation() {
// 随机选择一个方块作为控制点
const randomSquare = this.squares[Math.floor(Math.random() * this.squares.length)];
// 获取该方块所在面的其他方块
const squareNormal = randomSquare.element.normal;
const squarePos = randomSquare.element.pos;
// 找到同一面的其他方块
const commonDirSquares = this.squares.filter(
(square) =>
square.element.normal.equals(squareNormal) &&
!square.element.pos.equals(squarePos),
);
if (commonDirSquares.length < 2) return;
// 选择转动轴方向
let rotateAxisSquares = [];
const axisTypes = [];
if (squareNormal.x !== 0) {
// X面:可以按Y轴或Z轴转动
const yAxisSquares = commonDirSquares.filter(s => s.element.pos.y === squarePos.y);
const zAxisSquares = commonDirSquares.filter(s => s.element.pos.z === squarePos.z);
if (yAxisSquares.length > 0) axisTypes.push({ type: 'y', squares: yAxisSquares });
if (zAxisSquares.length > 0) axisTypes.push({ type: 'z', squares: zAxisSquares });
} else if (squareNormal.y !== 0) {
// Y面:可以按X轴或Z轴转动
const xAxisSquares = commonDirSquares.filter(s => s.element.pos.x === squarePos.x);
const zAxisSquares = commonDirSquares.filter(s => s.element.pos.z === squarePos.z);
if (xAxisSquares.length > 0) axisTypes.push({ type: 'x', squares: xAxisSquares });
if (zAxisSquares.length > 0) axisTypes.push({ type: 'z', squares: zAxisSquares });
} else if (squareNormal.z !== 0) {
// Z面:可以按X轴或Y轴转动
const xAxisSquares = commonDirSquares.filter(s => s.element.pos.x === squarePos.x);
const yAxisSquares = commonDirSquares.filter(s => s.element.pos.y === squarePos.y);
if (xAxisSquares.length > 0) axisTypes.push({ type: 'x', squares: xAxisSquares });
if (yAxisSquares.length > 0) axisTypes.push({ type: 'y', squares: yAxisSquares });
}
if (axisTypes.length === 0) return;
// 随机选择转动轴
const selectedAxis = axisTypes[Math.floor(Math.random() * axisTypes.length)];
const targetSquare = selectedAxis.squares[Math.floor(Math.random() * selectedAxis.squares.length)];
// 计算转动轴
const rotateDirLocal = targetSquare.element.pos
.clone()
.sub(randomSquare.element.pos)
.normalize();
const rotateAxisLocal = squareNormal
.clone()
.cross(rotateDirLocal)
.normalize();
// 找到需要转动的所有方块
const rotateSquares = [];
const controlTemPos = getTemPos(randomSquare, this.data.elementSize);
for (let i = 0; i < this.squares.length; i++) {
const squareTemPos = getTemPos(this.squares[i], this.data.elementSize);
const squareVec = controlTemPos.clone().sub(squareTemPos);
if (Math.abs(squareVec.dot(rotateAxisLocal)) < 0.01) { // 使用小的容差值
rotateSquares.push(this.squares[i]);
}
}
if (rotateSquares.length === 0) return;
// 随机选择转动角度:90度、180度或270度
const rotationAngles = [Math.PI * 0.5, Math.PI, Math.PI * 1.5];
const randomAngle = rotationAngles[Math.floor(Math.random() * rotationAngles.length)];
// 执行转动
const rotateMat = new THREE.Matrix4();
rotateMat.makeRotationAxis(rotateAxisLocal, randomAngle);
for (let i = 0; i < rotateSquares.length; i++) {
rotateSquares[i].applyMatrix4(rotateMat);
rotateSquares[i].updateMatrix();
}
// 更新方块的element数据
this.updateElementsAfterRotation(rotateSquares, rotateAxisLocal, randomAngle);
}
四、性能优化
4.1 性能问题定位
当我发现6阶魔方有些卡顿时,向Trae求助:
text
// 提示词
"6阶魔方运行时有点卡,帮我分析性能瓶颈"
Trae的分析和优化:
- 识别出频繁的矩阵计算是瓶颈
- 建议缓存计算结果
- 优化了渲染循环
- 推荐使用
requestAnimationFrame
4.2 Bug修复
遇到旋转后方块位置偏移的问题:
text
// 提示词
"魔方旋转后,有些方块的位置出现了微小偏移,怎么解决?"
Trae快速定位问题:
- 浮点数累积误差导致
- 提供了四舍五入修正方案
- 添加了状态验证机制
4.3 Trae辅助测试
Trae还帮我生成了完整的测试用例:
javascript
// Trae生成的测试代码
// 1. 阶数切换测试
for (let order = 2; order <= 6; order++) {
rubiks.setOrder(order);
console.log(`${order}阶魔方渲染正常`);
}
// 2. 交互测试
// 模拟鼠标拖动
const simulateDrag = (startX, startY, endX, endY) => {
// Trae生成的模拟代码
};
// 3. 状态测试
// 检测魔方是否完成
const checkCompletion = () => {
return rubiks.cube.finish;
};
4.4 性能表现
在Trae的优化建议下,项目达到了优秀的性能表现:
设备类型 | 魔方阶数 | 帧率(FPS) | 内存占用 |
---|---|---|---|
高端PC | 6阶 | 60 | 45MB |
中端PC | 6阶 | 45-60 | 42MB |
手机端 | 4阶 | 30-45 | 35MB |
五、Trae 编辑器使用心得
5.1 Trae的独特优势
- 理解力强:能准确理解复杂的3D交互需求
- 代码质量高:生成的代码结构清晰、可维护性好
- 创新能力:经常提供超出预期的解决方案
- 持续优化:能识别性能问题并提供优化建议
5.2 高效使用Trae的技巧
-
描述要具体:
- ❌ "实现旋转功能"
- ✅ "实现魔方层旋转,支持90度对齐,有300ms的缓动动画"
-
分步骤开发:
- 先实现核心功能
- 再添加动画效果
- 最后优化性能
-
充分利用对话:
- 遇到问题时详细描述现象
- 让Trae帮助分析原因
- 一起探讨解决方案
-
代码审查:
- 理解Trae生成的代码逻辑
- 根据实际需求调整细节
- 保持代码风格一致
5.3 Trae带来的开发体验提升
- 开发效率:原本预计2周的项目,3天就完成了核心功能
- 代码质量:模块化设计让代码易于维护和扩展,同时注释也较为全面
- 学习成长:通过Trae的代码学到了很多Three.js最佳实践
- 创新思维:Trae的解决方案经常带来新的灵感