🎮 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的解决方案经常带来新的灵感
相关推荐
飞哥数智坊13 分钟前
Trae vs Cursor:深度体验 Trae 一个月后,我的真实感受
人工智能·cursor·trae
ZXT28 分钟前
代码规范与提交
前端
柑橘乌云_33 分钟前
vue中如何在父组件监听子组件的生命周期
前端·javascript·vue.js
北海天空1 小时前
react-scripts的webpack.config.js配置解析
前端
LilyCoder1 小时前
HTML5中华美食网站源码
前端·html·html5
拾光拾趣录2 小时前
模块联邦(Module Federation)微前端方案
前端·webpack
江湖人称小鱼哥2 小时前
react接口防抖处理
前端·javascript·react.js
GISer_Jing2 小时前
腾讯前端面试模拟详解
前端·javascript·面试
saadiya~2 小时前
前端实现 MD5 + AES 加密的安全登录请求
前端·安全
zeqinjie2 小时前
回顾 24年 Flutter 骨架屏没有释放 CurvedAnimation 导致内存泄漏的血案
前端·flutter·ios