Vibe Coding 多人游戏(七)—— Phase 2:切换到状态同步 + TSRPC
从 basic1 到 new_basic1:AI 改造
改动看起来不大,但是质变:
basic1(Meta3D + 帧同步)↓ AI 改造:把服务端从"命令中继"改成"状态权威"
↓ 替换引擎:Meta3D → Three.js
↓ 保留通信层:TSRPC
new_basic1(Three.js + 状态同步 + TSRPC)
内核变化就一行指令:服务器角色从"命令中继"变成了"状态权威"。
我没让 AI 从零写——它太贵也太慢。我直接把 basic1 的代码丢给 AI,告诉它:
- "删除帧同步的回滚逻辑"
- "服务端加一个 tick 循环,每帧执行命令、广播状态"
- "客户端不执行逻辑,只展示服务端发来的位置"
结果令人惊讶:半天不到,状态同步就跑通了。AI 对 existing codebase 的理解比预期好得多。
带来的连锁反应:
- 去掉 Meta3D 引擎 → 包体积从 20.6MB 降到 7.95MB
- 引入 Three.js → 标准的 WebGL 渲染管线,文档多、社区大
- 服务端代码量翻倍(要跑游戏循环 + 碰撞检测),但客户端代码量骤降
10:30 - 13:10:new_basic2 MVP
一上午,在 new_basic1 的基础上继续迭代,我把产物目录改叫 new_basic2:
服务端(demos/room-service/)
- TSRPC WebSocket 服务
- 15 FPS authoritative tick 循环
executeCommand处理移动指令broadcastMsgGameState广播全量玩家状态
// 服务端 tick 循环 — Game.ts
setInterval(() => {
state = executeCommand(state) // 执行所有玩家命令
state = computeCollisionDamage(state) // 碰撞检测
broadcastMsgGameState(state) // 广播全量状态
}, 1000 / 15) // 15 FPS
客户端(demos/new_basic2/)
- Three.js 渲染场景(天空 + 网格地面 + 两个方块表示玩家)
- TSRPC
WsClient连接服务端 onGameState接收状态 → 更新方块位置
两个玩家在屏幕上移动成功,碰撞变红 ✅
15:10 - 16:20:小人模型 + 动画
Step 1:FBX 模型替换方块
- 经典小人模型
Infantry.fbx - 每个玩家独立加载 FBX 实例(不 clone,避免 SkinnedMesh 骨骼共享问题)
Step 2:Idle / Running 动画
Idle.fbx(54 track,2 秒循环),Running.fbx(54 track,0.83 秒循环)- Three.js
AnimationMixer管理每个玩家的动画状态机
坑:FBX 动画 Clip 名冲突
所有从 Mixamo 导出的 FBX,clip 名都叫 'mixamo.com'。mixer.clipAction('mixamo.com') 永远返回同一个 action——改 idle 时 running 也跟着变。
解法:clip.clone() 重命名为唯一名。
坑:按钮只触发一次指令
方向按钮按一次,sendMoveState 只发一条命令。表现为点一下走一步。
解法:长按时 setInterval 持续发,松开时 clearInterval。
16:26 - 16:50:WASD + 摄像头 + 客户端预测
CameraController.ts
- 第三人称跟随,鼠标左键拖拽旋转,滚轮缩放
- 自动跟随本地玩家位置(
camera.position.lerp(target, 0.05))
客户端预测——试了三个版本
状态同步的痛点:你的输入→服务端→回来,至少 50-100ms。不立刻响应的话手感像在沼泽里走路。
- 尝试 1(时间锚点):
pos = anchor + speed * (now - anchorTime)→ 各窗口漂移 - 尝试 2(deltaTime + 3% 修正):60FPS 移动 + 每帧拉向服务端 → 拉扯感
- 最终方案(deltaTime 预测 + 服务端覆盖):本地即时响应,服务端 GameState 直接覆盖全部 ✅
// 本地:60FPS 即时移动 mesh.position.x += dirX * speed * deltaTime
// 收到服务端 GameState → 直接覆盖
onGameState(msg):
msg.players.forEach(p => {
playerMeshes.get(p.username).position.set(p.x, p.y, p.z)
})
核心思路:自己按 60FPS 跑着爽,服务端按 15FPS 纠正。 每秒 60 帧本地响应 + 15 次服务端纠正,体感完全平滑。
17:00 - 17:45:MMD 巨大娘 + 碰撞系统
第一版角色区分
- 房主(Creator)→ 巨大娘模型(PMX)
- 成员(Member)→ 小人模型(FBX)
PMX 用 Three.js 的 MMDLoader + CCDIKSolver(脚部 IK),idle/walk 各一套 VMD 动画。
碰撞系统(终于放服务端了)
// 服务端:检测两玩家 XZ 距离
function _isCollision(p1, p2) {
let dx = p1.x - p2.x, dz = p1.z - p2.z
return Math.sqrt(dx*dx + dz*dz) < 1.0
}
- 碰撞盒尺寸:巨大娘 4×8×3,小人 0.8×1.8×0.5
- 服务端算结果 →
MsgGameState广播 → 客户端渲染绿/红
17:50 - 18:00:AI 巨大娘
让一个 MMD 巨大娘在地图上追玩家:
function _updateAI(enemy, players) {
let target = players[0]
let dx = target.x - enemy.x, dz = target.z - enemy.z
enemy.x += normalize(dx, dz) * 0.8
enemy.z += normalize(dz, dx) * 0.8
}
10 分钟写完。证明了服务端可以同时处理玩家 + AI 的状态同步。
当天总结:第一次 AI 辅助编码
| 时段 | 内容 | 方式 |
|---|---|---|
| 上午(~3h) | new_basic2 MVP | AI 改造 new_basic1 |
| 下午前半(~1h) | FBX 模型 + 动画 | AI 辅助 + 我修坑 |
| 下午中段(~1h) | Camera + WASD + 预测 | 我定方向 + AI 实现 |
| 下午后段(~1h) | MMD 巨大娘 + 碰撞 | AI 辅助 |
| 傍晚(~10min) | AI 巨大娘 | AI 生成 |
basic1 我一个人手写了两周。从 new_basic1(AI 状态同步改造)到 new_basic2(完整可玩的 Demo),核心功能不到两天完成。
这就是 Vibe Coding 的威力——不是让 AI 从零写,而是让 AI 站在已有代码的肩膀上改。
代价也很明显:AI 留下了大量隐性技术债——服务端更新后必须手动重启(debug 半小时才发现)、TSRPC 的 serviceProto.ts 不会自动同步、FBX 同名 clip 需要人工查文档才发现……basic1 里这些问题不会出现(我自己写的,每行都清楚),但在 AI 辅助模式下面,它们会静悄悄地累积。
两天后,我们就面临了下一个决断——代码结构已经撑不住了。
下一篇:Vibe Coding 多人游戏(八)—— 大重构:Monorepo + 双服务 + Logic 共享层 + 开闭原则
浙公网安备 33010602011771号