3D 实时作战游戏服务端架构
这是一款 3D 实时作战游戏的服务端架构:玩家在 3D 场景里实时移动、释放技能、走位躲弹、组队开团。这类系统的工程难度和普通后端完全不在一个维度——普通后端是「吞吐量」的艺术,实时战斗服务端是「毫秒级确定性」的艺术:同样的一局战斗,在任何机器上回放必须得到完全一致的结果,否则「我明明躲开了」就会变成客诉。
一、实时作战和普通后端的本质区别
| 维度 | 普通业务后端 | 3D 实时作战服务端 |
|---|---|---|
| 请求模式 | 低频请求-响应 | 每玩家 10~20Hz 持续输入流 |
| 状态 | 可丢可重算 | 战斗状态一帧都不能错 |
| 延迟敏感度 | 秒级可接受 | 100ms 手感即崩 |
| 一致性要求 | 最终一致即可 | 全端强一致(确定性) |
| 出错代价 | 一笔订单异常 | 整局战斗结果作废 |
核心矛盾:网络天然不可靠 + 战斗要求全端确定性。所有架构设计都在解这一对矛盾。
二、同步方案选型:为什么是帧同步
3D 实时作战的两个主流方案:
| 方案 | 原理 | 适合 | 我们的选择 |
|---|---|---|---|
| 帧同步(Lockstep) | 只同步玩家输入,各端在相同初始状态下跑相同逻辑 | RTS、MOBA、动作格斗、操作密集型战斗 | ✅ |
| 状态同步 | 服务器算权威状态,广播给客户端 | MMO、大世界开放场景 |
选帧同步的三个决定性理由:
- 带宽:一帧同步 N 个玩家的输入(每人数十字节),比同步全量状态便宜一个数量级——团战 20 人同屏,状态同步的广播流量会爆炸
- 手感:客户端本地立即执行输入(零延迟反馈),网络只影响「别人的画面」,不影响自己的操作
- 回放/观战免费:存下输入流就存下了整局战斗——录像系统、战斗回放、观战、反作弊复核全部白送
代价也明确:逻辑必须完全确定性——浮点数不能用、随机数必须同种子同序列、容器遍历顺序必须一致、逻辑层不能依赖任何本地时钟。这是整个项目最深的纪律。
三、确定性:帧同步的生死线
帧同步项目里 90% 的疑难 bug 都是「不同步」。工程上的防御体系:
1. 定点数替代浮点
// 逻辑层全程 Q16.16 定点数,跨平台位级一致
using Fixed = int64_t; // 数值 << 16
Fixed fmul(Fixed a, Fixed b) { return (Fixed)(((__int128)a * b) >> 16); }- 移动、弹道、技能距离、伤害计算全部定点化
- 表现层(渲染、特效)才允许浮点——表现层的浮点误差不影响判定
2. 统一随机数
// 全局唯一 Lehmer RNG,随战斗初始化种子,逻辑层禁用其他随机源
uint32_t rng_state;
uint32_t rng_next() {
rng_state = (uint64_t)rng_state * 279470273u % 0xfffffffbu;
return rng_state;
}暴击、掉落、技能随机效果全部走这个序列,种子在开局时同步给所有端。
3. 逻辑/表现分离(本项目最重要的架构决策)
┌────────────────────────────────────┐
│ 表现层(浮点·本地时钟·可以不精确) │
│ 插值、平滑、特效、镜头、动画、音效 │
├─────────── 状态快照接口 ────────────┤
│ 逻辑层(定点·确定性·帧驱动) │
│ 移动、技能、命中、伤害、Buff、AI │
└────────────────────────────────────┘逻辑层输出的状态快照(位置/朝向/血量/技能状态)交给表现层做插值平滑。表现层「看起来流畅」,逻辑层「算得精确」,两边互不污染。技能特效多炫、掉帧多卡,都不影响战斗判定——判定只认逻辑帧。
四、战斗核心系统
1. 技能系统:数据驱动 + 流水线
技能不写死在代码里,全部配置化(策划每天改 50 个技能不能都找程序):
{
"skill_id": 40021,
"cast": { "type": "direction", "range": 800, "cd": 12000 },
"phases": [
{ "type": "windup", "duration": 300, "cancelable": false },
{ "type": "dash", "distance": 400, "duration": 200 },
{ "type": "hit", "shape": "capsule", "radius": 120, "length": 300,
"damage": [120, 160], "on_hit": ["stun:400", "apply_buff:2001"] }
]
}每个技能 = 前摇/位移/命中判定/后摇的阶段流水线。命中形状(胶囊、扇形、球形、矩形)做成通用几何库,所有技能复用——技能的「花活」在配置,判定的「严肃」在引擎。
2. 命中判定:帧内分步 + 查询加速
3D 空间里「这一剑砍没砍到」的判定,性能和正确性都要命:
- 逻辑帧内分步移动:快速位移(冲刺/弹道)按小步长切片检测,防止「一帧穿人」(tunneling)——步长 = 最大移动速度 × 帧时长,3D 下尤其关键
- 空间划分加速:3D 场景用均匀体素网格做粗筛(战场范围有限,均匀网格比动态八叉树简单且快),先查候选再精判几何
- 判定必须确定性:同一帧内多个判定按实体 ID 排序执行,消除遍历顺序带来的差异
3. Buff 与结算顺序
Buff 系统(加速、减速、沉默、护盾、DoT)是同步地狱的重灾区——同一帧 10 个 Buff 到期、伤害结算顺序不同,结果就不同。规则:
- 所有结算事件进优先级队列,按(帧号 → 优先级 → 实体 ID)全序执行
- Buff 数值修改走「快照制」:每帧开始时数值快照,帧内修改基于快照,避免本帧内互相叠加的顺序依赖
五、网络层:卡顿对抗
客户端预测 + 服务器校验
客户端:输入 → 本地立即执行(手感零延迟) → 发给服务器
服务器:验证输入合法性(速度上限/CD/距离) → 帧缓冲 → 定帧广播
客户端:收到输入流 → 与本地预测对比 → 有偏差则回滚重演关键参数的平衡:
- 帧缓冲 2 帧:吸收抖动,代价是本地输入延迟 ~66ms(20Hz 逻辑帧)——动作游戏的命门参数,压到这个数以下靠的是「本地预测兜住了手感」
- 超时断线重连:断线 60 秒内可重连,服务端保留输入流,客户端拉全量快照 + 追帧(加速倍率快进输入流)
流量控制
- 输入编码:每玩家每帧压到 4~6 字节(按键位图 + 增量),20 人团战单帧广播 < 1KB
- 空闲玩家(无输入)只发心跳位,连发压缩再砍一半
六、反作弊:服务器权威
帧同步的经典质疑是「客户端算逻辑,怎么防作弊」。方案是服务器同跑逻辑:
- 服务器不转发原始输入,而是权威验证后再广播:速度超限、瞬移、CD 作弊的输入直接拒绝
- 服务器同跑一份战斗逻辑做结果校验:关键结算(击杀、伤害统计)以服务器为准
- 输入流全量落盘:被举报的战斗直接确定性回放复核——开挂的操作在回放里无处遁形(帧同步回放的免费红利)
七、性能指标
| 指标 | 目标 | 说明 |
|---|---|---|
| 逻辑帧率 | 15~20 FPS(逻辑帧) | 表现层 60 FPS 渲染,两者解耦 |
| 单服战斗并发 | 【待补充】 | 单进程多战斗,线程池隔离 |
| 输入广播延迟 | ≤3 帧 | 帧缓冲的代价 |
| 断线重连 | ≤60 秒 | 保留战斗 + 追帧 |
| 战斗回放体积 | 【待补充】 | 输入流存储,远小于录像 |
八、收获
做 3D 实时战斗后端最大的成长是对「确定性」的敬畏:普通系统里「大概对」就行,这里必须「位级一致」。逻辑/表现分离、定点数、全序结算这些手段,本质都是把「不确定性」从逻辑层彻底驱逐。这套纪律反哺到其他项目也受益——写任何分布式系统时,我都会先问一句:这段代码的结果,在另一台机器上重跑会一样吗?