
1. 为什么用 AI 来写一个开放世界项目先介绍一下背景。上个月我起了一个新的实验项目名字叫 aethercraft目标很直接验证在当前 AI 辅助编程的能力下一个人能不能把一个开放世界游戏从零搭起来。这里的“从零”不是指不用引擎、不用现成库而是指从项目结构、网络同步、世界服务、渲染表现到玩法循环都由我一个人通过 AI 辅助的方式推进。目前项目完成度大约 10%这个数字听起来不高但对开放世界类项目来说已经是一个比较关键的节点。基础架构已经搭建成功客户端和服务端能跑通多人联机的效果比较理想而且整个项目运行在浏览器里不需要安装任何客户端打开网址就能进入。为什么会有这个想法其实长期写业务代码的人都有一种感觉日常 CRUD 做得再多也很难积累出“从零构建一个系统”的完整经验。而开放世界游戏恰好是一个复杂度足够高的场景涉及实时通信、状态同步、场景管理、资源加载、服务器架构等多个维度。用 AI 辅助来写不是为了偷懒而是想探一探这条路的边界在哪里哪些环节 AI 能明显提效哪些环节仍然需要人来兜底。本文会围绕 aethercraft 当前 10% 进度做一次完整复盘内容包括项目目标、技术选型、基础架构设计、网络联机实现思路、AI 辅助开发的实际流程、浏览器端运行的限制与优化、常见问题排查以及后续从 10% 走向更高完成度的规划。适合对游戏开发、Web 实时应用和 AI 辅助编程感兴趣的后端、前端和全栈开发者阅读。需要说明的是这个项目还处在非常早期的阶段文中所有代码都是“示例思路”不是可以无脑复制到生产环境的完整代码。每个片段我会尽量写明它放在哪个文件、解决什么问题、有哪些需要根据实际版本调整的地方。2. 项目完成度 10% 到底做了哪些事情很多人看到“完成度 10%”会觉得这只是一个刚起步的半成品。但从工程角度看10% 恰好是“从零到一”和“从一到十”的分界线。2.1 当前已完成的能力清单aethercraft 目前的 10% 进度主要集中在三个层面第一层是基础工程架构。项目已经拆分为客户端、服务端和共享协议三部分客户端负责三维场景渲染和玩家操作服务端负责房间管理、世界状态维护和消息转发共享协议则定义了客户端与服务端之间的消息格式。代码仓库结构清晰能够持续迭代这是后续所有功能的地基。第二层是核心渲染链路。浏览器端已经能够加载一个低多边形风格的地形场景包含天空、光照、地面和简单的玩家角色。玩家可以控制角色在场景中移动摄像机跟随角色运行。渲染层面使用 WebGL 技术实现没有引入重量级商业引擎。第三层是网络联机能力。当前已经支持多个玩家同时进入同一个房间每个玩家都能看到其他玩家的实时位置和朝向。服务端会广播玩家的移动消息客户端对远端玩家做插值平滑处理因此在浏览器上体验联机时不会出现明显的“瞬移”问题。这一块是标题里说的“网络连机效果佳”的具体含义。2.2 还没做的事情有哪些与此同时90% 没做的事也非常明确。当前还没有完整的玩法循环没有物品系统、战斗系统、任务系统没有持久化存档地形也是静态生成的没有动态加载的世界区块。服务端只维护了房间内的实时状态没有做玩家数据库、世界服务器、AOI 兴趣区域管理这些大型多人游戏必备的模块。这些没有做的部分并不是“忘了做”而是刻意控制在 10% 这个范围内。原因是如果在基础架构和网络同步还没验证清楚的情况下就铺开玩法功能后续一旦架构调整返工成本会非常高。先把最核心的链路跑通再基于稳定的地基往上加玩法这个节奏对独立项目和 AI 辅助开发都更加友好。2.3 为什么先做联机而不是先做玩法从玩家视角看一个没有玩法的开放世界很无聊。但从技术验证角度看联机能力比玩法更需要提前验证。原因有两点。第一联机能力影响所有后续玩法设计。如果你做了一个单人战斗系统后面再想加入多人战斗可能需要推翻重来。但如果你先把多人同时在线、位置同步、房间管理这些能力做好了不管以后做的是建造玩法、探索玩法还是合作战斗都能直接复用这套网络地基。第二联机是最容易暴露架构问题的模块。单机游戏出 bug 只影响一个人顶多自己崩溃。联机游戏涉及客户端状态、服务端权威、网络延迟、断线重连等复杂问题任何一个环节设计不合理都会在两个人同时在线时立刻暴露。提前把网络同步链路做扎实等于给后续所有多人玩法上了一道保险。因此aethercraft 当前 10% 的核心产出并不是“能看的地形”或“能动的角色”而是“经得起后续扩展的联机架构”。这也是本文想把架构思路讲清楚的原因。3. 技术选型为什么选择浏览器 WebSocket技术选型往往比写代码本身更能决定项目走向。aethercraft 在技术选型上做了几个关键决定每个决定背后都有对应的考量。3.1 客户端为什么是浏览器而不是 Unity 或 Unreal开放世界游戏最常见的客户端是 Unity 和 Unreal它们成熟、强大、生态完善。但 aethercraft 选择浏览器作为客户端有三个原因。第一个原因是分发成本几乎为零。浏览器游戏不需要安装、不需要配置环境、不需要处理不同操作系统下的兼容问题只要服务端部署好玩家输入网址就能进入。这种分发方式在开发早期特别有价值因为你可以把最新的构建结果直接分享给协作者或测试者对方不需要做任何准备工作。第二个原因是对 AI 辅助开发更友好。Web 技术栈的代码形态非常规整HTML、CSS、JavaScript/TypeScript 的语法边界清晰AI 生成这些代码时的准确率相对较高。而且 Web 生态里有大量现成的三维渲染库、网络库和工具链AI 可以基于这些库生成更可靠的代码片段。第三个原因是技术挑战本身有价值。浏览器不是为大型实时三维游戏设计的它存在内存限制、CPU/GPU 资源限制、网络协议限制。在浏览器里做开放世界需要你思考资源管理、加载策略和性能优化这些经验在现代前端开发和云游戏领域同样适用。这里需要说明的是项目当前没有使用重量级游戏引擎而是选择了 Three.js 这类更偏底层的 WebGL 渲染库。这样做的原因是底层 API 暴露得越多AI 生成代码时你的控制力越强也越容易理解每一帧渲染发生了什么。等场景复杂度上来之后再引入更上层的框架也不迟。3.2 服务端轻量但可扩展的实现思路服务端选型时考虑过两条路线。一条是使用 Node.js好处是前后端语言一致AI 生成代码时不需要切换上下文另一条是使用 Go好处是并发能力强、部署简单。最终选择 Node.js 作为当前阶段的服务端语言主要看重的是开发效率。aethercraft 现在处于快速验证阶段服务端的核心职责是房间管理和消息转发并发压力并不高。Node.js 的事件循环模型天然适合这种海量短连接消息转发的场景而且前后端统一用 JavaScript/TypeScript协议定义可以直接共享减少了大量重复代码。但这里要表态如果项目进入大规模在线阶段比如单房间支持上百人、单服务器支撑上千人服务端很可能会迁移到 Go 或 Rust 这类更强调并发控制和内存效率的语言。现阶段选择 Node.js不是因为它是最好的而是因为它是当前阶段最快能验证架构的方案。3.3 网络协议为什么首选 WebSocket浏览器天然支持 HTTP但 HTTP 是请求-响应模型不适合实时联机。想象一个场景玩家 A 在场景中移动如果每次移动都发送一次 HTTP 请求服务器每次都要建立连接、处理请求、返回响应延迟和开销都非常高。WebSocket 解决了这个问题。它通过一次 HTTP 握手建立长连接之后客户端和服务端可以随时互相推送消息不需要反复握手。对实时联机游戏来说WebSocket 是目前浏览器环境下最成熟的方案。aethercraft 当前阶段选择 WebSocket 而不是 WebRTC是因为 WebRTC 虽然延迟更低、支持 P2P 直连但它需要处理 STUN/TURN 服务器、ICE 协商、NAT 穿透等复杂逻辑开发成本远高于 WebSocket。而且 WebRTC 更适合一对一的实时音视频或小规模 P2P 场景对服务端权威的多人在线游戏来说WebSocket 的中心化拓扑反而更容易做状态管理和安全校验。下面的网络拓扑简图展示了当前的设计浏览器客户端 A ---- WebSocket 服务端 ---- 浏览器客户端 B 浏览器客户端 C ---- WebSocket 服务端 ---- 浏览器客户端 D所有客户端都连接到同一个服务端服务端负责消息转发和状态维护。这种星型拓扑相比 P2P 直连实现起来更简单也更容易在后期添加服务端权威校验。4. 基础架构搭建的核心思路基础架构搭建是这个项目 10% 进度中最核心的部分。下面从总体架构、客户端骨架、服务端骨架三个层面拆解。4.1 总体架构分层设计aethercraft 当前的代码分为三个部分client、server、shared。aethercraft/ ├── client/ # 浏览器端渲染与交互 │ ├── src/ │ │ ├── main.js # 入口初始化渲染器与场景 │ │ ├── world/ # 场景、地形、光照 │ │ ├── player/ # 本地玩家控制 │ │ ├── network/ # WebSocket 客户端封装 │ │ └── ui/ # 界面与交互 │ └── index.html ├── server/ # 服务端 │ ├── src/ │ │ ├── index.js # 服务端入口 │ │ ├── room.js # 房间管理 │ │ ├── worldState.js # 世界状态维护 │ │ └── network/ # WebSocket 服务端封装 │ └── package.json └── shared/ # 前后端共享的协议定义 ├── messages.js # 消息类型 └── constants.js # 通用常量这个分层的核心意义在于shared 目录的存在让前后端不再各自维护一份消息定义。如果协议字段变了只需要改 shared 中的定义前后端同步更新即可。用 AI 辅助开发时这一点尤其重要因为 AI 容易在长对话中遗忘之前的约定共享协议文件提供了唯一的“事实来源”。4.2 客户端最小骨架渲染一个三维场景客户端的核心职责是渲染场景、接收玩家输入、把输入通过网络发给服务端同时接收服务端下发的其他玩家状态并渲染。下面是一个最小化的 Three.js 场景初始化代码示意骨架思路// 文件路径client/src/main.js核心片段需根据实际版本调整 import * as THREE from three; import { OrbitControls } from three/addons/controls/OrbitControls.js; // 创建渲染器并挂载到页面 const renderer new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); document.body.appendChild(renderer.domElement); // 创建场景与相机 const scene new THREE.Scene(); const camera new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 0.1, 1000 ); camera.position.set(0, 10, 20); camera.lookAt(0, 0, 0); // 添加基础光照 const ambientLight new THREE.AmbientLight(0xffffff, 0.5); scene.add(ambientLight); const directionalLight new THREE.DirectionalLight(0xffffff, 0.8); directionalLight.position.set(10, 20, 10); scene.add(directionalLight); // 添加一个简单的地面 const groundGeometry new THREE.PlaneGeometry(100, 100); const groundMaterial new THREE.MeshStandardMaterial({ color: 0x4a7c59, side: THREE.DoubleSide }); const ground new THREE.Mesh(groundGeometry, groundMaterial); ground.rotation.x -Math.PI / 2; scene.add(ground); // 动画循环 function animate() { requestAnimationFrame(animate); renderer.render(scene, camera); } animate(); // 窗口大小变化时自适应 window.addEventListener(resize, () { camera.aspect window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); });这里有几个关键点值得展开。渲染器的 antialias 参数控制抗锯齿打开后边缘更平滑但对性能有一定消耗。对于地形场景可以保留如果遇到性能瓶颈可以关闭。相机使用的是 PerspectiveCamera 透视相机这符合开放世界游戏的视觉习惯。far 参数设置为 1000意味着超出 1000 单位的物体会被裁剪不会渲染。当前场景比较小1000 足够后续如果生成大世界需要改为区块加载机制而不是直接加大 far 值因为 far 值越大深度冲突和渲染负担越明显。动画循环使用 requestAnimationFrame这是浏览器端渲染的基本方式。它的好处是浏览器会在每次屏幕刷新时回调从而保证画面流畅。与 setInterval 相比requestAnimationFrame 还能在页面切到后台时自动暂停不会浪费资源。4.3 服务端最小骨架消息转发与房间管理服务端的核心职责是管理多个房间每个房间内维护一份玩家列表并负责把玩家消息广播给房间内其他人。以下是一个服务端 WebSocket 转发的简化示意代码// 文件路径server/src/index.js核心片段需根据实际版本调整 import { WebSocketServer } from ws; const wss new WebSocketServer({ port: 8080 }); // 用 Map 管理多个房间。key 是房间 IDvalue 是该房间内的客户端连接列表 const rooms new Map(); function getOrCreateRoom(roomId) { if (!rooms.has(roomId)) { rooms.set(roomId, new Map()); } return rooms.get(roomId); } function broadcastToRoom(roomId, message, excludeClient) { const room rooms.get(roomId); if (!room) return; const data JSON.stringify(message); room.forEach((client, key) { if (client ! excludeClient client.readyState WebSocket.OPEN) { client.send(data); } }); } wss.on(connection, (ws, req) { // 握手阶段通过 URL 参数或子协议传入房间 ID const url new URL(req.url, http://${req.headers.host}); const roomId url.searchParams.get(room); // 分配一个本地 id实际项目应由服务端生成全局唯一 id const playerId Date.now().toString(36) Math.random().toString(36).slice(2, 6); // 把玩家加入房间 const room getOrCreateRoom(roomId); room.set(playerId, ws); // 通知房间内其他玩家有人加入 broadcastToRoom(roomId, { type: playerJoined, playerId, x: 0, y: 0, z: 0 }); // 发给当前玩家你的 ID 和当前房间已有玩家列表 const existingPlayers []; room.forEach((client, id) { if (id ! playerId) { existingPlayers.push({ playerId: id }); } }); ws.send(JSON.stringify({ type: init, playerId, players: existingPlayers })); // 接收客户端消息并转发到房间内其他人 ws.on(message, (raw) { let msg; try { msg JSON.parse(raw); } catch (err) { console.warn([server] invalid message, raw.toString()); return; } if (msg.type playerMove) { // 在真实项目中这里应该做服务端校验判断移动是否合法 msg.playerId playerId; broadcastToRoom(roomId, msg, ws); } }); // 处理断线 ws.on(close, () { room.delete(playerId); broadcastToRoom(roomId, { type: playerLeft, playerId }); if (room.size 0) { rooms.delete(roomId); } }); }); console.log(Aethercraft server running on ws://localhost:8080);这个片段虽然短但包含了几个非常关键的设计决策。第一个决策是房间与玩家分离。rooms 是一个 Map外层 key 是房间 ID内层是以 playerId 为 key 的客户端连接集合。这种两级索引结构让后续增加“查询房间内玩家数量”“根据 roomId 踢人”之类的能力变得非常方便。第二个决策是所有消息都要带 playerId而且 playerId 由服务端设置。客户端发送消息时不能自己声明“我是谁”否则任何人都可以伪造别人的身份。在真实项目中playerId 应该在鉴权通过后分配并且客户端消息一律不信任。第三个决策是断线处理。WebSocket 断开时需要从房间中移除玩家并且广播给其他人。如果不做这一步其他玩家会看到已经断线的人永远站在原地。这个逻辑看起来基础但很多早期联机项目都容易漏掉。4.4 共享协议的重要性前后端能够联动依赖的是 shared 中定义的消息格式。当前阶段消息类型很简单主要包括 playerJoined、playerLeft、playerMove、init 这几类。但即使这么简单也值得单独放在一个文件里管理。举例来说playerMove 消息包含以下字段// 文件路径shared/messages.js核心片段 export const MessageType { PlayerJoin: playerJoined, PlayerLeave: playerLeft, PlayerMove: playerMove, Init: init }; // playerMove 的结构示例 export const PlayerMoveMessage { type: MessageType.PlayerMove, playerId: , // 服务端填充 x: 0, // 世界坐标 x y: 0, // 世界坐标 y高度 z: 0, // 世界坐标 z rotationY: 0, // 朝向 timestamp: 0 // 客户端时间戳 };把协议单独定义的好处是后续任何改动都先改 shared再改前后端引用代码。用 AI 辅助开发时重复定义消息字段是很容易出现的问题比如服务端用playerId客户端却写成了id这种隐蔽问题通过共享协议可以从根上避免。5. 网络联机的关键实现与优化这一节是 aethercraft 当前完成度中技术含量最高的部分。网络联机并不只是把消息从 A 发给 B还涉及状态同步策略、插值平滑、网络抖动处理等多个问题。5.1 位置同步直接同步还是插值平滑最简单的联机同步方式是客户端 A 移动时把最新坐标发给服务端服务端广播给客户端 B客户端 B 直接把 A 的模型移动到新坐标。这种方式实现最简单但用户体验很差。原因在于网络消息不是瞬间到达的。如果你的游戏在 60FPS 下运行玩家每帧都会产生新的位置但网络发送频率通常只有每秒 15-30 条消息。客户端 B 收到 A 的位置消息时中间会有若干帧间隔。如果直接把模型“瞬移”到新位置玩家看起来就是一顿一顿地跳着走。aethercraft 使用插值方案处理这个问题。具体思路是客户端收到远端玩家的新位置后记录当前时间戳和目标位置然后在渲染循环中逐步把模型移动到目标位置。插值既能平滑视觉表现又能掩盖网络抖动。下面是一段远端玩家位置插值的示意代码// 文件路径client/src/network/remotePlayer.js核心片段需根据实际版本调整 // 远端玩家对象持有当前位置和插值参数 export class RemotePlayer { constructor(scene) { // 创建代表玩家的网格 this.mesh createPlayerMesh(); scene.add(this.mesh); // 当前位置 this.x 0; this.y 0; this.z 0; // 插值起点与终点 this.startX 0; this.startY 0; this.startZ 0; this.targetX 0; this.targetY 0; this.targetZ 0; // 插值进度0 到 1 this.lerpProgress 1; } // 收到网络消息时调用 updateTarget(x, y, z) { this.startX this.mesh.position.x; this.startY this.mesh.position.y; this.startZ this.mesh.position.z; this.targetX x; this.targetY y; this.targetZ z; this.lerpProgress 0; } // 每帧调用执行插值 update(deltaTime, lerpSpeed 0.15) { if (this.lerpProgress 1) { this.lerpProgress deltaTime * lerpSpeed; if (this.lerpProgress 1) this.lerpProgress 1; const t this.lerpProgress; const currentX this.startX (this.targetX - this.startX) * t; const currentY this.startY (this.targetY - this.startY) * t; const currentZ this.startZ (this.targetZ - this.startZ) * t; this.mesh.position.set(currentX, currentY, currentZ); } } }这段代码的核心思想是不直接把模型“设到”新坐标而是让它在一段时间内平滑地移动过去。lerpSpeed 控制插值速度如果网络消息到达稀疏适当调大速度如果消息到达密集可以调小速度减少延迟感。插值不是银弹。它的代价是远端玩家的位置总会比真实位置“滞后”一点。这只是联机游戏的一个基本取舍要么牺牲平滑度接受瞬移要么接受一点延迟换取视觉流畅。对当前 10% 完成度来说平滑优先是对的。5.2 客户端输入本地即时响应 网络异步发送一个容易犯的错误是玩家移动要等消息从服务端返回后才更新本地位置。这样做的后果是本地玩家操作有明显的延迟感游戏体验非常笨重。正确的做法是本地响应和网络发送异步进行。玩家按下方向键本地角色立刻移动这是客户端表现层同时把移动意图发给服务端让其他玩家看到你的新位置。服务端和客户端各自维护一个世界状态客户端只对自己的角色做权威处理对远端角色做插值展示。下面演示键盘控制移动的示意逻辑// 文件路径client/src/player/control.js核心片段需根据实际版本调整 const keys {}; window.addEventListener(keydown, (e) { keys[e.code] true; }); window.addEventListener(keyup, (e) { keys[e.code] false; }); // 本地移动速度 const moveSpeed 8; // 单位米/秒 export function handleLocalInput(player, deltaTime) { let dx 0; let dz 0; if (keys[KeyW] || keys[ArrowUp]) dz - 1; if (keys[KeyS] || keys[ArrowDown]) dz 1; if (keys[KeyA] || keys[ArrowLeft]) dx - 1; if (keys[KeyD] || keys[ArrowRight]) dx 1; // 防止斜向移动时速度叠加 const len Math.sqrt(dx * dx dz * dz); if (len 0) { dx / len; dz / len; } player.x dx * moveSpeed * deltaTime; player.z dz * moveSpeed * deltaTime; }这里的关键点是移动速度乘以 deltaTime也就是上一帧到这一帧的时间差。如果不乘帧率高的机器上玩家会跑得更快帧率低的机器上会跑得更慢。这个看似不起眼的细节是联机游戏统一移动步调的基础。5.3 心跳、超时与断线重连WebSocket 连接可能在不经意间断开比如网络切换、服务器重启、运营商的 NAT 超时。如果不做任何处理玩家会看到画面卡住但没有任何提示。行业常用方案是心跳机制。客户端每隔一段时间发送一个 ping 消息服务端收到后回复 pong 消息。如果在规定时间内没有收到任何消息就认为连接已经断开走断线处理流程。下面是客户端心跳的示意代码// 文件路径client/src/network/heartbeat.js核心片段需根据实际版本调整 export function startHeartbeat(socket, intervalMs 10000) { // 定时发送 ping const timer setInterval(() { if (socket.readyState WebSocket.OPEN) { socket.send(JSON.stringify({ type: ping, timestamp: Date.now() })); } }, intervalMs); return () clearInterval(timer); }对应服务端收到 ping 时立即回复 pong。同时服务端需要记录每个客户端最后一次活跃时间如果在某个阈值内没有收到心跳就主动关闭连接走断线清理流程。断线重连是更复杂的话题需要处理“连接断开后玩家角色还在世界中”“重连后状态如何恢复”等问题。当前 aethercraft 还没有做自动重连断线后玩家需要刷新页面重新进入房间。这是 10% 阶段可以接受的简化但在后续迭代中是优先级较高的功能。5.4 服务端权威还是客户端权威联机游戏有一个经典的架构问题关于玩家位置到底谁说了算如果客户端说了算客户端把自己的坐标发给服务端服务端直接广播给其他玩家这个方案实现简单、表现流畅但存在严重的作弊问题。恶意玩家可以伪造坐标瞬间传送到地图任何角落其他玩家和服务端都无法分辨。如果服务端说了算服务端根据玩家上报的输入状态来计算合法坐标这种方案更安全、更权威但实现复杂度明显上升。服务端需要模拟移动逻辑、验证移动是否合法同时还要考虑不同客户端网络延迟的补偿。aethercraft 当前阶段采用的是客户端权威简化版服务端会转发客户端上报的坐标但不会校验坐标的真实性。这个方案在“只有熟人一起开发测试”的场景下够用但如果未来开放公网联机必须在服务端加入移动合法性校验或者干脆改成服务端权威。这里建议所有联机项目在确定同步方案之前先想清楚一个重要问题你的游戏是纯合作型、竞技型、还是混合型纯合作型的作弊危害较小可以优先选客户端权威保证体验竞技型则必须服务端权威否则公平性无法保证。6. 用 AI 辅助开发的实际流程aethercraft 与一般游戏项目的最大区别在于大量代码是通过 AI 辅助生成的。下面分享一下实际协作流程这可能是对想“用 AI 做点东西”的开发者最有参考价值的部分。6.1 AI 辅助开发的整体思路AI 在写代码这件事上有一个鲜明特点它擅长从需求到代码的高效转换但不擅长在没有明确上下文时做出高质量架构决策。因此使用 AI 辅助开发不能是“把需求丢给 AI 让它全包”而应该形成一个人机协作的闭环。我的整体思路总结为四个阶段任务拆解把大功能拆成小任务每个任务的目标足够清晰。提示词编写把拆好的任务写成 AI 能理解的请求包含输入、输出、边界条件。代码审查AI 生成代码后逐行或逐块审查不盲信。集成测试把 AI 生成的代码合入项目实际运行并验证功能。这个流程看起来简单但每一步都有自己的坑。下面展开说明。6.2 任务拆解大功能拆成小任务很多人使用 AI 写代码失败是因为给 AI 的任务太大。比如“帮我写一个开放世界游戏的联机模块”这个请求太过庞大AI 无法给出高质量输出。正确的做法是把功能拆分到“一次对话能完成”的粒度。举个例子搭建 WebSocket 服务端这个任务可以继续拆成以下子任务创建 Node.js 项目并引入 ws 依赖实现 WebSocket 服务监听指定端口实现房间管理加入、离开、广播消息实现消息解析与转发处理断线清理每个子任务的代码量大约在 20 到 100 行之间。这个粒度对 AI 来说最合适生成结果的准确性也明显提高。6.3 提示词的质量决定了代码质量写提示词不是简单地描述“我要什么”。高质量提示词通常包含以下要素角色设定告诉 AI 你是资深前后端开发者。功能描述明确要实现什么语义。技术约束使用什么技术栈、什么库、目标运行环境。接口设计如果已经有接口约定必须写清楚。边界条件比如参数为空怎么办、连接断开怎么办。下面是一个实际使用过的提示词示例请使用 Node.js 和 ws 库实现一个 WebSocket 联机房间服务端。 要求 1. 支持多个房间每个房间互不影响。 2. 玩家通过 URL 参数携带房间 ID 进入房间。 3. 服务端收到 playerMove 消息后在消息体上附加 sourcePlayerId并广播给房间内其他玩家。 4. 玩家连接断开后从房间移除并广播 playerLeft 消息。 5. 消息格式遵守 shared/messages.js 中定义的 MessageType 常量。 请输出完整代码并给出文件路径说明。这段提示词之所以有效是因为它包含了足够多的上下文。AI 知道用什么库、要实现什么行为、消息格式在哪、边界条件是什么。相比“帮我写一个 WebSocket 房间服务”这段提示词的信息密度高了一个量级。6.4 AI 代码审查清单AI 生成的代码不能直接拿来用。我的审查流程通常包括以下检查项API 是否存在AI 容易编造不存在的库函数或参数。检查每一个引入的 API 是否在当前版本真实存在。错误处理是否完整AI 生成的代码往往只处理“正常路径”忽略异常路径。比如数组越界、连接断开、非法消息格式。变量名是否模糊如果 AI 生成的变量名是data1、temp、res这类无意义写法即使能跑也会增加后续维护成本。是否泄漏资源定时器是否清理事件监听器是否移除WebSocket 连接是否在不需要时关闭安全边界AI 生成的服务端代码经常忽略输入校验。比如玩家发送的坐标是否可以超出地图边界消息体是否可以伪造 playerId下面这种情况就经常出现。AI 可能生成如下代码ws.on(message, (raw) { const msg JSON.parse(raw); // 没做任何校验直接把客户端的 playerId 当作可信数据 broadcastToRoom(room.id, msg); });问题很明显JSON.parse 可能因为格式错误而抛异常而且直接信任客户端提供的 playerId 是严重的安全漏洞。正确做法应该是在服务器端重写 playerId并对消息结构做校验。AI 不会主动考虑这些问题这就是人类与 AI 协作中人类必须做的一层兜底。6.5 判断 AI 代码能不能用的标准经过多次实践我总结出三个判断标准。第一AI 生成的代码是否在逻辑上自洽。如果你读完代码无法清楚地说出它的输入、输出和处理流程那说明代码不够好需要让 AI 重写或优化。第二AI 生成的代码是否与现有项目风格一致。多人协作中长期维护的代码风格一致性非常重要。如果一个文件是 ES Module另一个文件是 CommonJS后续维护者会非常痛苦。第三AI 生成的代码是否包含完整的错误处理。一个只处理正常路径的函数在真实项目中早晚会出事故。如果 AI 偷懒没有写错误处理可以把“补充异常处理”作为新的子任务返回给它。7. 浏览器直接访问的意义与性能边界aethercraft 的一个重要特点是可以“直接用浏览器访问打开”。这在开发早期带来很大的便利但也带来了明确的性能边界。7.1 零分发成本给开发带来的优势浏览器访问意味着不需要为测试者和协作者准备任何安装包。每次改完代码重新构建前端资源并部署到服务器对方刷新页面就能体验最新版本。这种即时反馈对早期项目的迭代速度极其重要。在实际开发中还有一个更具体的场景联机调试。传统游戏联机调试需要多人各自安装客户端、配好 IP、确认版本一致流程非常长。浏览器访问模式下只要大家打开同一个网址就能直接进入同一个房间。当前 aethercraft 的联机联调效率很高跟这种方式有很大关系。7.2 浏览器端的三座大山内存、显存、协议但浏览器不是专门的游戏运行时它有三条明显的限制。第一条是内存限制。浏览器标签页的可用内存通常受设备物理内存和浏览器策略影响大型场景贴图、模型数据很容易把内存吃满。aethercraft 当前场景很小还不存在这个问题。未来如果要加载大量区块必须考虑按需加载和资源释放策略否则页面迟早崩溃。第二条是显存限制。WebGL 纹理、几何体、着色器都占用 GPU 显存。一个合理的优化方向是使用纹理图集把多个小纹理合并到一张大纹理上减少绘制状态切换。另一个方向是控制 draw call 数量把多个网格合并成单个几何体。第三条是协议限制。浏览器环境下 WebSocket 是可靠的选择但它基于 TCP存在队头阻塞问题。对实时性要求极高的动作游戏可能需要考虑 WebTransport 等更新的方案。不过这些方案仍处于演进阶段现阶段使用 WebSocket 是稳妥的。7.3 目前实测到的体验与后续优化方向根据当前局域网环境下多开浏览器窗口的测试aethercraft 在 3 到 4 个客户端同时在线时表现稳定画面流畅没有出现明显的卡顿或同步异常。这主要得益于当前场景面数低、消息频率低、同房间人数少这三个条件。如果后续继续增加人数和场景复杂度优先要做的优化包括降低消息发送频率从每帧发送改为每 50ms 发送一次位置快照。添加 AOI 兴趣区域管理只同步玩家周围一定范围内的其他玩家状态减少网络带宽消耗。静态场景合批把地形、建筑等不动的物体合并渲染降低 draw call。资源按需加载离玩家较远的物体不创建或不渲染当玩家接近时再加载。8. 常见问题与排查思路在 aethercraft 开发过程中我遇到的坑不少。下面整理几个有代表性的问题供遇到类似情况的开发者参考。问题现象常见原因解决思路页面白屏控制台提示 WebGL 相关报错浏览器不支持 WebGL或 GPU 硬件加速被禁用检查浏览器关于webgl的支持情况尝试关闭硬件加速后重新打开多个窗口打开同一页面互相看不到对方WebSocket 连接失败或房间 ID 参数不一致打开浏览器控制台查看 WebSocket 连接状态确认 URL 中的 room 参数一致远端玩家角色一顿一顿地移动消息频率太低或插值速度设置不合理检查消息发送频率适当增加插值 lerp 速度玩家关闭页面后其他玩家画面中残留角色断线清理逻辑没有覆盖 close 事件在服务端 close 事件中移除玩家并广播 playerLeftAI 生成的代码报错提示某个模块不存在AI 引用了错误的依赖路径或包名核对实际安装的依赖版本修正 import 路径游戏画面卡顿帧率明显偏低场景物体太多且未做合批或像素比设置过高调低 pixelRatio关闭抗锯齿检查 draw call 数量下面挑几个问题展开说明排查路径。8.1 多人不同步位置漂移严重这个问题的根源通常是“消息里没有时间戳”。当客户端 A 在不同网络延迟下发送坐标时客户端 B 收到消息的时间点与消息产生的时间点不同。如果不带时间戳接收方只能假设消息是立刻到达的这会引入误差。解决思路是在每条位置消息中带一个客户端时间戳。接收方收到消息后用当前时间减去时间戳估算网络延迟再结合延迟进行位置补偿。这是比较进阶的同步优化方法当前 10% 阶段可以先不做但设计协议时要预留 timestamp 字段。8.2 WebSocket 连接被浏览器拒绝如果打开页面时控制台显示WebSocket connection failed优先检查以下几项。第一连接地址是否正确。浏览器环境下的 WebSocket 地址以ws://或wss://开头不是http://。如果服务端端口是 8080客户端连接地址应类似ws://localhost:8080。第二是否存在跨域问题。服务端需要配置允许的来源否则浏览器会阻止连接。在 ws 库中可以在处理握手时添加允许来源判断。第三防火墙或内网策略。如果你在虚拟机、云服务器或公司内网中运行服务端需要确保端口对外开放。这个问题在局域网联机时经常出现烦恼程度非常高。8.3 AI 生成的代码导致内存持续增长AI 在生成定时器、事件监听相关代码时经常忘记清理资源。比如在useEffect中设置定时器但不清除或在页面刷新时没有取消之前的 rAF 循环。排查方法是使用浏览器开发者工具的 Performance 面板和 Memory 面板。脚本运行时观察堆内存是否为持续上升的锯齿状曲线。如果每次创建连接、销毁连接后内存没有回到原水平说明存在泄漏。逐行检查 AI 生成的代码中是否有 addEventListener、setInterval、requestAnimationFrame 而没有对应的 removeEventListener、clearInterval、cancelAnimationFrame。9. 最佳实践与工程建议节选了 aethercraft 在开发过程中积累的经验整理成一套可以复用的工程建议。9.1 AI 辅助开发的协作规范如果团队使用 AI 辅助开发建议形成一套明确的规范。第一所有 AI 生成的代码必须经过人工审查。不要因为“AI 说代码能跑”就直接合入主干。要让 AI 输出代码的同时输出文件路径、依赖说明和运行步骤减少人工验证成本。第二维护一份项目上下文文档。AI 的上下文窗口有限长对话后它会忘记之前的约定。把技术栈、目录结构、消息格式、命名规范写在一个文档中每次让 AI 写代码前把相关片段贴进提示词。这个文档非常重要相当于给 AI 的一份浓缩版需求规格书。第三把 AI 当成交互式结对编程伙伴。不要一次让它生成一大坨代码而是用“对话 迭代”的方式逐步实现。发现代码有问题直接把报错信息贴回去让 AI 修改效率远高于自己翻文档。9.2 联机系统的安全边界网络联机系统必须时刻保持“永不信任客户端”的边界意识。当前 aethercraft 是测试项目暂未做严格校验但真实项目在第一天就要考虑以下安全基线。首先是身份认证。连接 WebSocket 时必须校验玩家身份不能只凭 URL 参数或客户端自报 ID 就确认身份。Token 校验、会话过期、单点登录这些机制都需要前置设计。其次是消息合法性校验。玩家移动速度是否合理所在坐标是否在地图边界内这些校验最好放在服务端。如果追求性能可以做采样校验而不是每帧校验全部信息。最后是资源访问控制。不要让客户端随意上传或下载任意文件特别是纹理、模型、地图数据等资源。所有资源获取都应该经过服务端校验。另外WebSocket 本身不提供加密公网环境必须使用 wss://即 WebSocket over TLS否则消息内容可以被中间人窃取。9.3 配置管理与环境隔离aethercraft 当前的配置很简单服务端监听端口和客户端连接地址都硬编码在代码中。但项目只要进入多人协作阶段就必须引入配置管理。建议至少做到三点。第一环境配置与代码分离。把监听端口、数据库地址、资源服务器地址等放到环境变量或配置文件中避免改配置需要改代码。第二不同环境使用不同配置。本地开发连 localhost测试环境连内网服务器生产环境连公网域名用环境变量区分。第三敏感配置不提交到代码仓库。数据库密码、Token 密钥等一律通过部署系统注入。9.4 日志与可观测性联机系统一旦部署到服务器开发者无法再通过本地控制台观察运行状态。因此服务端日志非常重要。当前 aethercraft 服务端只在几处关键位置打印了日志包括玩家加入、玩家离开、非法消息。这些都还不够。后续建议为每个 WebSocket 连接生成唯一的连接 ID所有日志带上连接 ID、房间 ID、玩家 ID。这样排查问题时可以串联出完整时间线。客户端侧的日志同样重要。浏览器控制台记录 WebSocket 连接状态、每次连接的重连次数、消息发送接收频率。这些日志可以拓展成埋点数据用来分析玩家行为和网络质量。9.5 版本迭代节奏10% 完成度意味着后面还有大量工作。但独立项目的节奏不能是“一口气写完”。更合理的方式是保持小步快跑每次提交只增加一个可验证的能力。仍以联机系统为例。升级服务端从“客户端权威”到“服务端权威”就可以拆成四步先加消息校验再加移动合法性判断再加状态快照同步最后再把客户端渲染改为完全由服务端状态驱动。每一步完成后都可以跑通并测试不会出现长期分支导致合并冲突。10. 从 10% 走向 20%下一步计划最后聊聊 aethercraft 从 10% 到 20% 会做什么这部分既是项目规划也是读者可以关注的技术方向。10.1 短期优先级排序下一步迭代会优先做以下三项。第一加入持久化存档与玩家登录。当前进入房间后没有身份概念刷新页面后就丢失所有状态。加入最简单的用户名登录和后端存档会让项目从“每天重新开始”变成“可以养成的世界”。第二实现区块动态加载与生成。当前地形是一次性生成的小场景离“开放世界”还差得远。下一步会引入区块概念以玩家为中心加载周边区块卸载离玩家较远的区块用程序化生成算法让每次进入世界都获得不同地形。这个方向也是后续“开放世界”完成度质变的关键。第三引入服务端权威的位置校验。这一步会把当前“客户端说了算”的架构演进为“服务端权威”为未来公网联机做准备。服务端需要在内存中维护玩家状态周期性做状态校验并把合法状态下发给客户端。10.2 中长期技术方向完成 20% 之后项目会进入更深的水域。以下几个方向是我已经在持续关注的技术主题。第一是同步方案升级。从 WebSocket 逐条消息转发演进为基于快照的同步协议。服务端定期生成世界状态快照客户端在两次快照之间插值这样即使某个消息丢失客户端也不会出现严重跳变。快照同步是多人游戏引擎中最常用的同步方式之一实现后联机稳定性会有明显提升。第二是渲染性能优化。场景面数增加后必须引入 LOD 技术。远处的物体使用低精度模型靠近后再替换为高精度模型。同时引入合批和遮挡剔除把每帧渲染负担控制在稳定范围内。第三是玩法循环设计。开放世界需要给玩家目标。简单版本可以先做资源采集和建筑建造后续再加入制作系统和探索奖励。玩法设计不是纯技术问题需要持续迭代和玩家反馈这部分反而是 10% 之后最大的挑战。10.3 给同样在尝试 AI 辅助开发的人一个建议如果你也想用 AI 辅助方式搭建自己的项目我的建议是不要一开始就追求“全自动生成”。先把项目的地基打好把目录结构、协议定义、核心架构用传统方式确定下来再让 AI 在这个框架内写具体代码。AI 在框架明确时表现最好在框架模糊时反而容易带偏方向。aethercraft 目前 10% 的进度里大部分时间花在架构和网络同步这些问题上真正让 AI 写代码的时间反而没有想象中多。但正因如此这个项目才能在早期就保证地基足够稳。后面从 10% 走向更高完成度时AI 的提效价值才会更加明显。如果你也在做类似的开放世界或联机项目或者也在探索 AI 辅助开发的实际工作流欢迎在评论区交流。后续做到了新的进度节点我会继续整理架构演进、性能优化和踩坑记录。