基于 Bounce 确定性3D物理引擎的多人联机示例项目集合。Bounce 是一个纯 TypeScript(无 WebAssembly)编写的快速确定性物理库,专为多人锁步/回滚游戏设计。
lockstop-client/
├── bounce/ # 引擎源码 (MIT, v1.9.0, ~14K 行 TS)
│ ├── docs/documentation.md # 完整 API 参考文档
│ ├── src/ # 引擎源码
│ └── tests/ # Vitest 测试套件
│
├── bounce-character-controller/ # npm: @perplexdotgg/bounce-character-controller (v0.3.0)
│ └── src/index.ts # 角色控制器实现
│
├── bounce-simple-example/ # 简单演示: 球 + 桌面 + Suzanne 模型
├── bounce-character-controller-example/ # 可玩演示: WASD 角色 + 楼梯
├── bounce-lockstep-example/ # 多人锁步演示: 匹配 + 帧同步
└── mecs-tower-defense-example/ # 高级: 使用 MECS ECS 的塔防游戏
每个子项目独立运行:
# 安装依赖
npm install
# 启动 Vite 开发服务器
npm run dev
# 生产构建
npm run build
# 运行测试 (仅 bounce 和 bounce-character-controller)
npm run test# 终端 1 — 中继服务器(克隆并启动)
git clone https://github.com/beijian128/lockstep-server
cd lockstep-server && go run main.go
# 终端 2 — 客户端开发服务器
cd bounce-lockstep-example && npm run dev
# 打开两个浏览器标签页访问 http://localhost:5173
# 在每个标签页中点击 "Start Matchmaking"- 语言: TypeScript (strict),ESM 模块
- 构建: Vite (v7–v8)
- 物理: @perplexdotgg/bounce (纯 TS, v1.6–1.9)
- 渲染: Three.js v0.182
- 测试: Vitest
- ECS: @perplexdotgg/mecs (仅塔防示例)
Bounce 是确定性的 — 在任何 IEEE 754 兼容平台上,相同输入产生相同结果。
- World — 物理模拟容器。
takeOneStep(dt)用于确定性步进,advanceTime(dt)用于可变帧率 - Shape — 碰撞几何体。工厂方法:
createSphere(),createBox(),createCapsule(),createCylinder(),createConvexHull(),createTriangleMesh(),createHeightMap(),createCompoundShape() - Body — 物理对象。三种类型:
Dynamic(受力+碰撞),Kinematic(用户控制),Static(不可移动) - 约束 —
PointConstraint,DistanceConstraint,FixedConstraint,HingeConstraint - 场景查询 —
intersectShape(),castRay(),castShape() - 序列化 —
world.toArray()/world.fromArray()用于状态快照/回滚
- 所有客户端使用相同的
dt调用takeOneStep() - 避免在物理相关逻辑中使用
Math.sin/cos/tan - 保持操作顺序一致(浮点加法不可结合)
- 使用序列化进行快照/回滚
[客户端 A] ──MatchmakingRequest──▶ ┌─────────────────────┐
[客户端 B] ──MatchmakingRequest──▶ │ 中继服务器 (Go) │
│ (:8080) │
[客户端 A] ◀──MatchmakingStarted── │ 1Hz 匹配循环 │
[客户端 B] ◀──MatchmakingStarted── │ 30Hz tick 循环 │
│ 哈希校验 │
[双方] ◀──JoinResponse─────── └─────────────────────┘
[双方] ◀──GameStart──────────
[双方] ◀══FrameInput (30Hz)══
[双方] ──PlayerInput (按键变更时)──▶
[双方] ──StateHash (每300 tick)──▶
- 服务端驱动步进: 客户端仅在收到
FrameInput时步进 - 匹配队列: 点击按钮 →
MatchmakingRequest→ 服务器匹配 ≥2 名玩家 →GameStart - 相机相对输入: WASD 按键会按相机角度旋转后发送
- 状态哈希: 每 300 tick 对所有玩家位置+速度做 FNV-1a 哈希,服务端校验
[4字节 小端长度][1字节 类型标签][JSON UTF-8]
| 标签 | 方向 | 消息 | 用途 |
|---|---|---|---|
| 2 | C→S | PlayerInput | WASD + Jump |
| 3 | C→S | StateHash | 确定性校验 |
| 5 | C→S | MatchmakingRequest | 进入匹配队列 |
| 1 | S→C | JoinResponse | 分配 playerId |
| 2 | S→C | GameStart | 种子、玩家列表、倒计时 |
| 3 | S→C | FrameInput | 30Hz 输入广播 |
| 4 | S→C | GameOver | 胜利者信息 |
| 5 | S→C | PlayerEliminated | 断线通知 |
| 6 | S→C | MatchmakingStarted | 排队确认 |
- Bounce 官网 — 演示、API 文档
- Bounce 代码仓库
- 中继服务器 — 锁步示例配套 Go 中继服务器
- npm: @perplexdotgg/bounce
- npm: @perplexdotgg/bounce-character-controller