🎮 AI编程新时代:Trae×Three.js打造沉浸式3D魔方游戏

一、前言

本人喜欢玩魔方,之前摸鱼的时候发现有可以在线玩的小游戏,于是突发奇想是否可以自己实现一个,刚好这段时间 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的分析和优化:

  1. 识别出频繁的矩阵计算是瓶颈
  2. 建议缓存计算结果
  3. 优化了渲染循环
  4. 推荐使用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的独特优势

  1. 理解力强:能准确理解复杂的3D交互需求
  2. 代码质量高:生成的代码结构清晰、可维护性好
  3. 创新能力:经常提供超出预期的解决方案
  4. 持续优化:能识别性能问题并提供优化建议

5.2 高效使用Trae的技巧

  1. 描述要具体

    • ❌ "实现旋转功能"
    • ✅ "实现魔方层旋转,支持90度对齐,有300ms的缓动动画"
  2. 分步骤开发

    • 先实现核心功能
    • 再添加动画效果
    • 最后优化性能
  3. 充分利用对话

    • 遇到问题时详细描述现象
    • 让Trae帮助分析原因
    • 一起探讨解决方案
  4. 代码审查

    • 理解Trae生成的代码逻辑
    • 根据实际需求调整细节
    • 保持代码风格一致

5.3 Trae带来的开发体验提升

  1. 开发效率:原本预计2周的项目,3天就完成了核心功能
  2. 代码质量:模块化设计让代码易于维护和扩展,同时注释也较为全面
  3. 学习成长:通过Trae的代码学到了很多Three.js最佳实践
  4. 创新思维:Trae的解决方案经常带来新的灵感
相关推荐
代码搬运媛3 小时前
Jest 测试框架详解与实现指南
前端
counterxing3 小时前
Agent 跑起来之后,难的是复用、观测和评测
node.js·agent·ai编程
uccs4 小时前
大模型底层机制与Agent开发
agent·ai编程·claude
counterxing4 小时前
我把 Codex 里的 Skills 做成了一个 MCP,还支持分享
前端·agent·ai编程
wangqiaowq4 小时前
windows下nginx的安装
linux·服务器·前端
夜雪闻竹4 小时前
vectra 向量索引文件损坏怎么办
ai编程·向量·vectra
ZzT5 小时前
Harness 到底指什么
openai·ai编程·claude
之歆5 小时前
DAY_12JavaScript DOM 完全指南(二):实战与性能篇
开发语言·前端·javascript·ecmascript
宅小年5 小时前
AI 创业最危险的地方:太容易做出来
openai·ai编程·claude
发现一只大呆瓜5 小时前
Vite凭什么这么快?3分钟带你彻底搞懂 Vite 热更新的幕后黑手
前端·面试·vite