AI辅助重构:用React+Three.js+Rapier重写3D娃娃机项目
2026/8/15 9:11:37 网站建设 项目流程

1. 项目缘起:一个被遗忘的“半成品”与AI的邂逅

五年前,我沉迷于用代码创造一些“无用但有趣”的东西。当时Three.js正火,物理引擎也让人着迷,于是心血来潮,想做一个在线的、可交互的娃娃机。想法很美好:用React搭界面,Three.js渲染3D场景,再找个物理引擎模拟抓取的真实感。我吭哧吭哧搞了几天,实现了基础的场景搭建、模型加载和一个极其简陋的抓取动画。然后……就没有然后了。项目文件夹静静地躺在硬盘角落,成了典型的“从入门到放弃”的标本。

直到最近,AI编程工具的风潮让我重新审视这些陈年旧码。看着那个粗糙的、爪子穿模、逻辑混乱的娃娃机项目,一个念头冒了出来:如果让现在的AI来“重写”它,会怎么样?不是让它从零生成,而是基于我五年前的思路和残破的代码,用今天更成熟的技术栈和AI的理解能力,重新构建一个完整、可玩、代码优雅的项目。这不仅仅是一次代码翻新,更像是一场跨越时间的对话,看看五年间,我的技术认知与AI的代码能力能碰撞出什么火花。于是,我花了半天时间,以“AI辅助重写”为核心,将这个老项目彻底复活。

2. 技术栈选型与AI工具的角色定位

五年前,我的技术选择带着明显的探索和拼凑痕迹。今天重做,首先要明确一个清晰、现代且高效的技术栈。AI在这里不仅是写代码的“手”,更是提供选型建议、规避历史陷阱的“顾问”。

2.1 前端框架:坚守React,但拥抱全新开发范式

项目主体是浏览器内的3D交互应用,React依然是构建复杂UI状态的首选,其组件化模型与3D场景中的对象管理有天然的契合度。但五年前我用的是React Class Component和一堆散乱的setState,状态流像一团乱麻。

AI的辅助建议与实施:我向AI工具(如Cursor或Claude Code)描述了项目需求:一个需要精细控制3D物体状态、抓取流程、游戏分数和UI联动的应用。AI几乎一致推荐使用React Hooks + TypeScript的组合。Hooks(尤其是useState,useEffect,useRef,useReducer)能让状态和副作用逻辑更清晰、更内聚。例如,娃娃机的抓取臂状态、计时器、奖品池管理,都可以用自定义Hook封装,与3D渲染逻辑解耦。

TypeScript的引入是本次重写的关键提升。我为整个项目定义了清晰的接口(Interface):IClawMachineStateIPrizeItemIPhysicsWorld等。AI在根据我的自然语言描述生成代码片段时,TypeScript能立刻提供类型约束和智能提示,极大减少了因类型错误导致的运行时Bug。当我说“创建一个控制抓钩上下移动的函数”时,AI生成的代码已经包含了参数类型(speed: number)和返回类型(Promise<void>),使得代码意图一目了然。

2.2 3D渲染核心:Three.js的版本升级与最佳实践

五年前我用的是Three.js r89,很多API如今已变。AI帮我快速梳理了版本差异,并直接采用了最新的稳定版本(如r162)。重写不是简单替换版本号,而是在AI的帮助下,应用当前社区公认的最佳实践。

  1. 场景图(Scene Graph)管理:过去我把所有模型(机器、爪子、奖品)都直接addscene里,管理混乱。AI建议并帮我重构为更清晰的层级结构:

    // 使用Group进行逻辑分组 const machineGroup = new THREE.Group(); const clawGroup = new THREE.Group(); const prizesGroup = new THREE.Group(); machineGroup.add(cabinetModel, glassModel); clawGroup.add(armModel, clawModel); prizesGroup.add(prize1, prize2, prize3); scene.add(machineGroup, clawGroup, prizesGroup);

    这样,移动爪子只需操作clawGroup,旋转整个机器只需操作machineGroup,逻辑清晰,便于AI理解和后续维护。

  2. 资源管理与加载:当年用THREE.TextureLoaderTHREE.GLTFLoader都是直接内联,没有加载状态和错误处理。AI帮我引入了LoadingManager和异步加载模式,并生成了加载进度条UI的配套代码,提升了用户体验。

  3. 性能优化:AI提示我,对于静态的娃娃机柜体模型,应使用THREE.StaticCopy属性或在加载时合并几何体以减少draw call。对于大量重复的奖品模型,建议使用实例化网格(THREE.InstancedMesh),这在五年前我完全没概念。

2.3 物理引擎:从“纸片”动画到真实模拟

旧项目最大的败笔是物理。抓取动画是用Tween.js做的简单线性运动,爪子与奖品是“检测矩形框重叠”这种2D游戏逻辑,毫无真实感。

AI的决策支持过程:我向AI提出了需求:“需要为3D娃娃机添加真实的抓取物理,包括碰撞检测、抓取判定、奖品被抓起后的摆动。” AI没有直接推荐某个引擎,而是列出了几个选项并分析了在Web环境下的优劣:

  • Cannon.js:纯JavaScript,轻量,API直接,但功能相对基础,社区活跃度一般。
  • Ammo.js(Bullet物理引擎的Emscripten移植):功能强大,工业级,但包体积大,初始化慢。
  • Rapier.rs(编译为WebAssembly):新兴引擎,性能极好,API现代,但中文资料相对少。

考虑到本项目并非需要极度复杂的物理特性(如流体、软体),而是注重实时交互性能和开发体验,AI最终建议我尝试Rapier。它WASM的特性保证了性能,且其TypeScript类型定义完善,与我的技术栈契合。

AI不仅给出了建议,还生成了初始化的样板代码:

import * as RAPIER from '@dimforge/rapier3d'; // 初始化物理世界 const world = new RAPIER.World({ x: 0.0, y: -9.81, z: 0.0 }); // 创建地面和碰撞体 const groundColliderDesc = RAPIER.ColliderDesc.cuboid(10, 0.1, 10); world.createCollider(groundColliderDesc); // 在Three.js对象和Rapier刚体之间建立关联 const rigidBodyDesc = RAPIER.RigidBodyDesc.dynamic().setTranslation(x, y, z); const rigidBody = world.createRigidBody(rigidBodyDesc); const colliderDesc = RAPIER.ColliderDesc.ball(radius); world.createCollider(colliderDesc, rigidBody); // 在渲染循环中更新物理世界 function animate() { world.step(); // 同步Three.js对象位置到物理刚体位置 const position = rigidBody.translation(); threeJsObj.position.set(position.x, position.y, position.z); requestAnimationFrame(animate); }

这段由AI生成的代码,清晰地勾勒出了整合物理引擎的核心链路,我只需在此基础上,将我的3D模型与对应的刚体、碰撞体绑定即可。

2.4 AI编程工具:Cursor作为核心“协作者”

整个重写过程,我主要使用Cursor作为AI编程助手。它的优势在于深度集成在IDE中,能直接分析整个项目上下文(包括我五年前的烂代码),提供基于上下文的建议。

我的工作流是

  1. 需求拆解与描述:我用自然语言在Cursor里描述一个功能点,比如“我想让爪子下落时,如果碰到奖品就停止并闭合,然后上升”。
  2. 代码生成与审查:Cursor会生成一段包含物理检测、状态变更的TypeScript函数。我并非直接采纳,而是仔细阅读,理解其逻辑(例如,它使用了Rapier的world.intersectionsWith进行碰撞检测)。
  3. 迭代与调试:如果生成的代码运行不如预期(比如抓取判定太灵敏),我会继续对话:“抓取判定需要更精确,只有当爪子的两个钳口都接触到奖品时才触发抓取。” Cursor会修改代码,可能引入更详细的射线检测或形状查询。
  4. 代码重构:对于AI生成的一些冗长函数,我会命令它:“将这个函数重构,将物理检测和状态更新逻辑分离。” 它便能给出更模块化的代码。

AI在此过程中扮演的不是替代者,而是一个知识渊博、不知疲倦的结对编程伙伴。它快速将我的意图转化为语法正确的代码草案,而我负责把握整体架构、业务逻辑的正确性和代码最终质量。

3. 核心功能模块的重构与AI实现

有了清晰的技术栈和工具,重写工作就可以系统性地展开。我将娃娃机拆解为几个核心模块,逐一用AI辅助重构。

3.1 游戏状态管理:从混乱到清晰

旧项目状态散落在各个角落,难以维护。我决定使用useReducerHook来集中管理游戏状态。我向AI描述了状态机:

游戏状态:IDLE(待机) -> COIN_INSERTED(投币) -> CLAW_MOVING(移动爪子) -> CLAW_DESCENDING(下爪) -> CLAW_GRASPING(抓取中) -> CLAW_ASCENDING(上升) -> RESULT(判定结果) -> IDLE

AI为我生成了状态和动作(Action)的类型定义,以及reducer函数的基本骨架。我在此基础上,填充了每个状态转换的具体逻辑。例如,当派发‘GRASP_COMPLETE’动作时,reducer会计算是否抓取成功,并更新奖品列表和玩家分数。这种集中式状态管理,让UI(如按钮禁用状态、提示文字)和3D场景动画的同步变得异常简单。

3.2 抓取物理与交互:真实感的核心

这是本次重写的重中之重,也是AI辅助价值最高的部分。

  1. 物理实体绑定:首先,需要为Three.js场景中的每个可交互物体(爪子、奖品)创建对应的Rapier刚体(RigidBody)和碰撞体(Collider)。我让AI编写了一个工具函数createPhysicsEntity,它接收一个Three.js的Mesh对象和物理参数(质量、形状、类型),返回一个包含rigidBodycollider的对象,并自动将两者的位置同步。这避免了手动维护两套坐标的麻烦。

  2. 抓取逻辑实现:下爪过程不再是简单的动画。AI帮我实现的逻辑是:

    • 爪子(一个Dynamic刚体)在CLAW_DESCENDING状态下,被施加一个向下的力(rigidBody.applyForce)。
    • 在每一帧物理更新后,检测爪子碰撞体(clawCollider)与所有奖品碰撞体的交集(world.contactPair(clawCollider, prizeCollider))。
    • 如果检测到接触,且接触点数量或深度满足“抓取”阈值,则切换状态到CLAW_GRASPING。此时,AI生成的代码会创建一个物理约束:将奖品刚体与爪子刚体通过一个FixedImpulseJointBallJoint连接起来。这就模拟了爪子“抓住”奖品的物理效果,奖品会随着爪子上升而自然摆动,而不是僵直地粘在一起。
    • 上升到顶后,解除物理约束,并根据奖品最终位置(是否落入出货口)判定游戏结果。

    这个过程中,AI快速帮我处理了复杂的Rapier API调用,并解释了不同关节(Joint)类型的区别,让我能做出合适的选择。

  3. 用户输入与控制:我保留了旧项目的“方向键移动爪子”设计。AI将键盘事件监听与物理引擎结合,生成的控制代码不再是直接设置position.x,而是对爪子的刚体施加一个冲量(applyImpulse)或设置速度(setLinvel),使得移动带有惯性,手感更真实。

3.3 3D模型与动画优化

五年前的模型是网上随便找的.obj文件,材质粗糙。这次,我使用AI图像生成工具(如Midjourney或Stable Diffusion的辅助设计功能),重新生成了更卡通、更精致的娃娃机和奖品概念图,然后借助Blender快速建模导出为.glb格式。

在加载环节,我遇到了一个经典问题:“glb模型为什么到three.js里打开全是黑的?”旧项目也遇到过,当时没解决。这次我直接问AI。AI给出了系统性的排查清单:

  1. 检查光源:场景中是否添加了AmbientLight(环境光)和DirectionalLight(平行光)?没有光源,模型就是黑的。
  2. 检查相机位置:相机是否在模型内部或被模型遮挡?使用camera.lookAt()指向场景中心。
  3. 检查模型材质:某些.glb模型的材质可能需要THREE.sRGBEncoding或特定的渲染器配置。AI建议我尝试在加载后遍历模型材质,并设置material.needsUpdate = true
  4. 检查模型缩放:模型可能过大或过小,超出视锥体。添加一个BoxHelper来可视化模型边界。

通过AI的指引,我迅速定位到问题是光源不足和材质编码问题,并得以解决。AI还建议我为抓取臂的张开/闭合添加骨骼动画(THREE.AnimationMixer),让动作更流畅,并生成了混合动画剪辑的基础代码。

3.4 UI与游戏逻辑集成

使用React,UI层变得非常简单。我创建了GameControlPanel组件显示状态、币数、分数,以及控制按钮。状态通过Context或Props从主reducer传递下来。AI帮我快速搭建了这些UI组件,并确保了样式与3D画风的统一。

对于“开始游戏”、“移动”等按钮事件,它们只是派发对应的Action(如‘MOVE_LEFT’),由reducer和物理系统去响应,实现了完美的关注点分离。

4. 开发、构建与部署的现代化改造

五年前,我用的是create-react-app的早期版本,构建配置一无所知。现在,我用Vite+TypeScript作为构建工具链,速度极快。

  1. Vite配置:AI帮我初始化了项目,并解决了TypeScript装饰器解析的问题。当我在tsconfig.json中遇到“选项‘baseUrl’已弃用”的警告时,AI解释了这是TypeScript 7.0的变更,并给出了迁移到‘rootDir’和路径映射(paths)的正确配置方法。

  2. 调试与性能分析:AI建议我使用Three.js的调试库drei和物理引擎的调试渲染器,将碰撞体以线框形式可视化,这对于调试抓取判定至关重要。在性能方面,AI提示我使用stats.js监控帧率,并注意在组件卸载时正确销毁Three.js的渲染器、场景和物理世界,防止内存泄漏。

  3. 部署:项目是纯前端的,部署极其简单。AI建议我可以使用Vercel、Netlify或GitHub Pages。我选择了Vercel,只需关联Git仓库,它就能自动识别Vite项目并进行部署。AI还生成了必要的vercel.json配置(虽然本项目基本不需要),并提醒我注意构建输出目录是dist

5. 半日重构:经验、教训与AI的边界

从打开旧项目到新版本上线,总共耗时约半天。这个过程中,AI的贡献是颠覆性的,但也暴露出其局限性。

AI带来的效率提升是惊人的:

  • 知识检索与代码生成:我不再需要反复搜索“Three.js如何创建阴影”、“Rapier怎么创建碰撞传感器”,用自然语言描述,AI就能给出可直接使用或稍作修改的代码块。
  • 代码重构与规范化:AI能迅速将过程式代码重构为声明式、模块化的函数组件和自定义Hook,并强制加入TypeScript类型,代码质量远超我五年前的水平。
  • 问题排查与调试:遇到“模型全黑”、“物理对象穿透”等问题时,向AI描述现象,它能给出结构化的排查思路,节省了大量盲目试错的时间。

但AI并非万能,我的核心作用无可替代:

  1. 架构设计与决策:AI不会主动说“我们应该用useReducer来管理状态”。它需要我提出清晰、正确的指令。整体应用架构、模块划分、数据流设计,仍然依赖我的经验和判断。
  2. 业务逻辑理解:娃娃机抓取的“玄学”手感(如爪子的抓力、奖品的滑动)需要微调参数。AI可以帮我写参数调节的代码框架,但“怎样算好玩”这个感觉,需要我亲自测试和调整。AI无法理解“趣味性”这种主观体验。
  3. 错误处理与边界情况:AI生成的代码往往是“理想路径”。当网络加载失败、物理引擎初始化出错、用户快速连续点击时,需要我手动添加健壮的错误处理和状态保护。AI可能会遗漏这些边缘情况。
  4. 代码审查与优化:对AI生成的代码,我必须逐行审查。有时它会使用已弃用的API,或写出性能不佳的写法(如在渲染循环中创建新对象)。我需要识别并纠正这些。

一个具体的踩坑案例:在实现爪子抓取奖品时,AI最初生成的代码是在检测到碰撞的瞬间,就将奖品刚体的类型从Dynamic改为KinematicPositionBased,并直接设置其位置跟随爪子。这导致了诡异的“抽搐”现象。我意识到这是物理模拟和直接设置位置的冲突。经过几轮与AI的对话调试,我们最终采用了前面提到的物理关节(Joint)方案,这才是符合物理规律的实现。这个过程让我深刻体会到,AI提供的是“素材”和“可能性”,而工程师的價值在于“选择”和“判断”。

这次“AI重写”实验,结果远超预期。一个死气沉沉的老项目,在半天内脱胎换骨,拥有了现代化的代码结构、真实的物理交互和不错的视觉效果。它证明了在当前阶段,AI编程工具已经成为前端开发者,尤其是从事3D、游戏等复杂交互领域开发的强大“倍增器”。它极大地压缩了“查找资料-理解API-编写样板代码”的循环,让开发者能更专注于核心创意、架构设计和用户体验打磨。

然而,它没有改变编程的本质——解决问题的逻辑思维。AI更像是一本超级智能的、能对话的API文档和代码片段生成器。驾驭它,需要你清楚地知道自己要去哪里,以及如何下达正确的指令。对于新手,它可能让你更快地做出东西,但也可能让你在不理解原理的情况下堆砌代码;对于有经验的开发者,它则能解放生产力,让你从繁琐的重复劳动中解脱出来,去挑战更有价值的问题。我的这个“新”娃娃机,就是这种新工作流下一个有趣的产物。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询