低帧率策略游戏类帧同步保障公平性软件详细设计文档
版本 :v1.0
文档状态 :正式版
适用项目 :多人在线即时策略游戏(类星际争霸)
核心设计原则:服务器权威(Server-Authoritative)、确定性帧同步(Deterministic Lockstep)、公平性优先(Fairness First)
1. 引言
1.1 目的
本文档定义了一款低帧率(<20 FPS)多人在线即时策略游戏 的帧同步网络架构设计方案。该方案旨在通过服务器权威 + 类锁步等待机制,在保证所有玩家绝对公平竞技的前提下,兼顾网络抖动容忍度和断线恢复能力。
1.2 适用范围
- 游戏逻辑帧率:< 20 帧/秒(每帧 50ms)
- 玩家规模:2 ~ 10 人
- 网络模型:纯服务器权威,客户端仅负责渲染与控制指令发送
- 赛事等级:支持从娱乐局到职业局域网比赛的多级配置
1.3 设计约束
- 客户端不参与任何游戏逻辑计算,仅作为状态渲染终端
- 游戏引擎(服务器进程)单一实例运行,不存在分布式共识
- 逻辑计算必须具备确定性(Deterministic),以支持断线恢复后的快照加载
- 不采用 AI 托管,掉线玩家直接阻塞引擎,由外部裁判系统裁决
2. 系统架构概览
2.1 整体拓扑
┌─────────────────────────────────────────────────────────────────┐
│ 赛事大厅 / 裁判系统 │
│ (匹配、裁决、超时监测、判负指令) │
└─────────────┬───────────────────────────────────┬───────────────┘
│ 管理指令通道 │ 管理指令通道
▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ 游戏引擎(权威服务器) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 主线程(计算线程): 帧循环 + 条件等待 + 状态广播 │ │
│ │ 管理线程(指令监听): 接收判负/继续等待指令 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ 内存状态: 当前帧完整快照 + 环形缓冲区(近30帧历史快照) │
└─────────────┬───────────────────────────────────┬───────────────┘
│ 控制指令通道 │ 状态推送通道
▼ ▼
┌─────────────────────┐ ┌─────────────────────────┐
│ 客户端 A │ │ 客户端 B ... N │
│ • 指令采集与上报 │ │ • 指令采集与上报 │
│ • 状态接收与渲染 │ │ • 状态接收与渲染 │
│ • 重连与快照加载 │ │ • 重连与快照加载 │
└─────────────────────┘ └─────────────────────────┘
2.2 模块职责
| 模块 | 职责 |
|---|---|
| 游戏引擎(Engine) | 帧推进、逻辑计算、状态快照维护、状态广播 |
| 等待调度器(Wait Scheduler) | 阻塞等待所有玩家指令,支持条件变量中断 |
| 快照管理器(Snapshot Manager) | 环形缓冲区维护最近N帧完整状态,响应重连拉取 |
| 管理指令处理器(Admin Command Processor) | 接收并执行"判负/继续等待"等管理指令 |
| 裁判系统(Referee System) | 超时监测、裁决逻辑、裁判人工操作界面 |
| 客户端(Client) | 指令采集上报、状态接收渲染、重连协议 |
3. 核心机制设计
3.1 帧同步基础模型
3.1.1 帧与帧号
- 游戏以固定时间步长(50ms)推进,每步称为一帧(Frame)
- 帧号从 0 开始单调递增,永不回退
- 所有逻辑计算(单位移动、攻击、生产等)均在帧边界执行
3.1.2 确定性要求
由于客户端重连时加载的是"计算好的状态快照"而非"重放指令序列",服务器单实例计算不存在跨平台浮点不一致问题。但以下场景仍需保证确定性:
- 录像回放(Replay):如果未来支持,需确保相同输入序列产生相同输出
- 单元测试:相同种子下的逻辑回归测试
- 实现方式 :禁用
std::rand(),使用确定性线性同余生成器(LCG);禁用double,统一使用int64_t定点数表示坐标与血量
3.2 帧推进等待机制(核心公平性保障)
3.2.1 基本流程
服务器推进至第 N 帧后,进入第 N+1 帧准备阶段:
1. 检查所有存活玩家是否已提交"第 N+1 帧"的控制指令
2. 若未收齐 → 进入阻塞等待(Wait)状态
3. 若收齐 → 执行第 N+1 帧逻辑,广播状态变更,推进至 N+1
3.2.2 客户端指令上报规范
- 客户端必须**每帧(每 50ms)**主动上报指令,即使无操作也必须发送"空指令(Null Input)"
- 上报数据包必须包含:
req_frame:客户端期望服务器计算的下一帧号ack_frame:客户端已确认接收并渲染的最新帧号input_mask:本帧操作位掩码(按键/鼠标状态)
- 上报通道:控制指令通道,采用可靠有序的流式传输(如 TCP 长连接),客户端异步非阻塞发送
3.2.3 等待条件变量唤醒
主线程等待采用条件变量(Condition Variable) + 双重唤醒条件:
wait_until(
all_inputs_received() OR admin_interrupt_signal()
)
- 输入唤醒:所有玩家针对当前待计算帧的指令均已到达
- 管理唤醒:外部裁判系统发来"判负/继续等待"指令
3.3 最大超前帧数(Max Ahead Frames)
3.3.1 机制定义
引擎允许服务器当前计算帧与最慢玩家确认帧(ack_frame)之间的最大差值。
- 设服务器当前帧为
current_frame,玩家 P 的确认帧为ack_frame[P] - 若
current_frame - ack_frame[P] >= max_ahead_frames,则引擎停止推进,等待该玩家 - 该参数为静态配置,在比赛开始时确定,中途不可变更
3.3.2 分级配置标准
| 比赛等级 | max_ahead_frames | 等效缓冲时间 | 适用场景 |
|---|---|---|---|
| 娱乐/路人局 | 30 ~ 60 | 1.5 ~ 3.0 秒 | 容忍WiFi波动,降低重连频率 |
| 线上排位(天梯) | 10 | 0.5 秒 | 兼顾公平与流畅 |
| 线上职业赛 | 2 | 0.1 秒 | 吸收网络丢包重传,近乎绝对同步 |
| 线下局域网赛 | 0 | 0 秒 | 绝对锁步,一人卡全员停 |
3.3.3 工作原理图示
时间线(服务器视角):
帧: 1000 1001 1002 ... 1020 1021
↑ ↑ ↑ ↑ ↑
快玩家确认帧: 1000 1001 1002 1020 (正常推进)
慢玩家确认帧: 1000 1000 1000 1000 (卡在1000帧)
若 max_ahead_frames = 20:
当服务器计算到 1020 帧时,检查发现 1020 - 1000 = 20 ≥ 20
→ 引擎暂停,等待慢玩家追上
3.4 快照管理(Snapshots & Delta)
3.4.1 双通道推送策略
| 通道 | 协议特征 | 内容 | 触发条件 |
|---|---|---|---|
| 状态变更通道 | 不可靠但低延迟广播(如 UDP 组播或 PUB 模式) | 本帧发生变化的实体(增量) | 每帧正常广播 |
| 快照通道 | 可靠单播传输(如 TCP 请求-响应) | 完整的游戏世界状态(全量) | 客户端重连请求 |
3.4.2 环形缓冲区(Ring Buffer)
- 服务器内存中维护一个 容量为 30 帧的环形缓冲区
- 每帧计算完成后,将当前帧完整状态序列化存入缓冲区
- 缓冲区索引:
buffer[frame_id % 30] = serialize(world) - 用途:响应重连客户端的"拉取最新帧"请求,且可提供最近30帧的任意帧快照(用于调试或未来扩展)
3.4.3 重连快照拉取协议
客户端重连请求:
→ 携带 client_last_ack_frame(客户端本地确认的最新帧号)
→ 服务器忽略该数值,直接返回 buffer[latest_frame % 30](最新完整快照)
→ 客户端加载快照,覆盖所有本地状态
→ 客户端设置 ack_frame = latest_frame,继续上报后续帧指令
设计决策:不追溯中间帧,不做增量回放。断线玩家直接"时空跳跃"至当前最新状态。
4. 指令与状态同步详细流程
4.1 正常帧循环(无人掉线)
┌──────┐ ┌──────────────┐ ┌──────────┐ ┌──────────────┐
│客户端│ │控制指令通道 │ │ 引擎 │ │状态推送通道 │
└──┬───┘ └──────┬───────┘ └────┬─────┘ └──────┬───────┘
│ │ │ │
│ 每50ms上报 │ │ │
│───────────────>│ │ │
│ req_frame=N+1 │ │ │
│ ack_frame=N │ │ │
│ input=... │ │ │
│ │ 存入待处理队列 │ │
│ │───────────────────>│ │
│ │ │ 检查是否收齐 │
│ │ │ 所有玩家指令 │
│ │ │ (是) │
│ │ │ │
│ │ │ 执行 tick(N+1) │
│ │ │ 计算 Delta │
│ │ │──────────────────>│
│ │ │ │ 广播Delta
│ │ │ │─────────>客户端
│ │ │ 存入快照 │
│ │ │ buffer[N+1] │
│ │ │ │
│ │ │ N = N+1 │
│ │ │ │
│ │ 继续下一轮 │ │
│<───────────────│───────────────────│ │
│ │ │ │
4.2 阻塞等待场景(某玩家指令未到)
时间: T0 T0+100ms T0+200ms T0+500ms
帧: 1000 等待中 等待中 收到指令 → 1001
│ │ │ │
引擎主线程 检查发现玩家A 条件变量阻塞 玩家A重连后提交
执行第1000帧 → 的指令未到 → 等待唤醒 → 指令, 条件变量
逻辑完成 进入等待状态 触发唤醒
│ │ │
其他玩家: 画面定格 画面定格 画面恢复流畅
画面正常 (卡顿开始) (持续卡顿) (卡顿结束)
4.3 裁判判负干预流程
┌──────────┐ ┌──────────────┐ ┌──────────┐
│ 裁判系统 │ │ 引擎管理线程 │ │ 引擎主线程│
└────┬─────┘ └──────┬───────┘ └────┬─────┘
│ │ │
│ 检测到玩家A超时 │ │ 主线程阻塞在
│ (例如120秒无响应) │ │ wait_condition
│ │ │
│ 裁判点击"判负" │ │
│──────────────────>│ │
│ 判负指令(A) │ │
│ │ 收到管理指令 │
│ │ 设置中断标志 │
│ │ 唤醒条件变量 │
│ │───────────────────>│
│ │ │ 主线程醒来
│ │ │ 检查中断标志
│ │ │ 执行处理管理指令
│ │ │ → 移除玩家A
│ │ │ → 清除A所有单位
│ │ │ 回到循环首
│ │ │ 检查存活玩家
│ │ │ 指令已收齐
│ │ │ 继续推进
│ │ │
5. 通信通道设计
5.1 通道分类与职责
| 通道名称 | 方向 | 传输特征 | 内容描述 |
|---|---|---|---|
| 控制指令通道 | 客户端 → 服务器 | 可靠、有序、流式 | 每帧上报玩家操作指令(含空指令) |
| 状态推送通道 | 服务器 → 客户端 | 不可靠、低延迟、广播 | 每帧推送游戏状态变更(Delta) |
| 快照通道 | 客户端 ↔ 服务器 | 可靠、请求-响应 | 重连时拉取完整世界状态快照 |
| 管理指令通道 | 裁判系统 → 服务器 | 可靠、优先级高 | 判负、继续等待等管理指令 |
5.2 控制指令通道协议规范
5.2.1 数据包结构
┌──────────────┬──────────────┬──────────────┬──────────────┐
│ 帧号 │ 确认帧号 │ 操作掩码 │ 校验和 │
│ (int32) │ (int32) │ (uint32) │ (uint16) │
└──────────────┴──────────────┴──────────────┴──────────────┘
5.2.2 传输要求
- 保证按序到达(同一连接内的消息顺序与发送顺序一致)
- 保证可靠到达(丢失重传机制)
- 客户端每帧定时发送,不受用户操作影响
5.3 状态推送通道协议规范
5.3.1 Delta 消息格式
json
{
"frame": 1001,
"timestamp": 1734567890123,
"deltas": [
{"entity_id": 101, "field": "hp", "value": 85},
{"entity_id": 102, "field": "position", "x": 12.5, "y": 34.2},
{"entity_id": 103, "field": "state", "value": "attacking"}
],
"removed_entities": [104, 105]
}
5.3.2 传输要求
- 允许丢包(客户端重连时通过快照通道补偿)
- 低延迟优先于可靠性
- 广播方式发送,无需客户端确认
5.4 快照通道协议规范
5.4.1 请求格式
json
{
"player_id": 1,
"client_last_frame": 980
}
5.4.2 响应格式
json
{
"current_frame": 1020,
"serialized_world": "<base64编码的完整游戏状态>",
"compressed": true
}
5.4.3 传输要求
- 可靠传输(TCP 或等效协议)
- 大块数据传输支持压缩
- 单次请求-响应模式
5.5 管理指令通道协议规范
5.5.1 指令类型
| 指令类型 | 参数 | 说明 |
|---|---|---|
DECLARE_DEFEAT |
target_player_id |
宣告某玩家失败,清除其所有单位 |
CONTINUE_WAIT |
target_player_id, duration_ms |
裁判决定继续等待指定时长 |
FORCE_ADVANCE |
target_player_id |
强制推进(仅调试模式) |
QUERY_STATUS |
无 | 查询引擎当前状态 |
5.5.2 传输要求
- 可靠性极高(命令丢失不可接受)
- 具有权限验证机制(仅裁判系统可发送)
- 实时性要求高于普通控制指令
6. 引擎模块详细设计
6.1 主线程(计算线程)伪代码
cpp
class GameEngine {
public:
void Run() {
while (is_running_) {
// ========== 阶段1: 等待所有玩家输入 ==========
InputCollection inputs;
bool is_admin_interrupted = false;
// 条件变量等待,双重唤醒条件
{
std::unique_lock<std::mutex> lock(wait_mutex_);
wait_cv_.wait(lock, [this, &inputs, &is_admin_interrupted]() {
// 条件1: 所有存活玩家已提交待计算帧的指令
if (IsAllInputsReady(current_frame_ + 1)) {
inputs = CollectInputs(current_frame_ + 1);
return true;
}
// 条件2: 管理线程发来中断信号
if (admin_interrupt_flag_) {
is_admin_interrupted = true;
return true;
}
return false;
});
}
// ========== 阶段2: 处理外部管理指令 ==========
if (is_admin_interrupted) {
ProcessAdminCommands(); // 可能移除玩家、判负等
admin_interrupt_flag_ = false;
continue; // 回到循环开始,重新检查输入条件
}
// ========== 阶段3: 执行逻辑帧 ==========
// 执行第 current_frame_ + 1 帧
world_.Tick(inputs);
current_frame_++;
// ========== 阶段4: 更新广播与快照 ==========
// 计算 Delta
Delta delta = world_.CollectDelta();
state_push_channel_.Send(delta.Serialize());
// 存储完整快照到环形缓冲区
snapshot_buffer_[current_frame_ % BUFFER_SIZE] = world_.Serialize();
// 等待下一帧(50ms - 实际耗时)
SleepUntilNextFrameTick();
}
}
private:
std::condition_variable wait_cv_;
std::mutex wait_mutex_;
std::atomic<bool> admin_interrupt_flag_{false};
std::queue<AdminCommand> admin_command_queue_;
int32_t current_frame_ = 0;
int32_t max_ahead_frames_ = 20; // 可配置
SnapshotBuffer snapshot_buffer_;
World world_;
};
6.2 管理线程(指令监听)伪代码
cpp
class AdminCommandHandler {
public:
void OnAdminCommand(const AdminCommand& cmd) {
std::lock_guard<std::mutex> lock(engine_->wait_mutex_);
switch (cmd.type) {
case AdminCommand::DECLARE_DEFEAT:
// 将判负指令入队
engine_->admin_command_queue_.push({
.type = CommandType::kDeclareDefeat,
.target_player = cmd.target_player_id
});
break;
case AdminCommand::CONTINUE_WAIT:
// 延长等待,不做任何状态变更,仅记录日志
LOG(INFO) << "裁判指令: 继续等待玩家 " << cmd.target_player_id;
break;
case AdminCommand::FORCE_ADVANCE:
// 调试模式专用,谨慎使用
engine_->admin_command_queue_.push({
.type = CommandType::kForceAdvance,
.target_player = cmd.target_player_id
});
break;
}
// 唤醒主线程
engine_->admin_interrupt_flag_ = true;
engine_->wait_cv_.notify_one();
}
};
6.3 判负处理逻辑
cpp
void GameEngine::ProcessAdminCommands() {
while (!admin_command_queue_.empty()) {
AdminCommand cmd = admin_command_queue_.front();
admin_command_queue_.pop();
switch (cmd.type) {
case CommandType::kDeclareDefeat: {
Player* player = FindPlayer(cmd.target_player);
if (player && player->IsAlive()) {
// 1. 标记玩家为失败状态
player->SetState(PlayerState::kDefeated);
// 2. 执行游戏内清除逻辑(所有单位爆炸/消失)
world_.KillAllUnitsBelongingTo(player->id());
// 3. 从等待列表移除(不再等待该玩家的指令)
active_players_.erase(player->id());
LOG(INFO) << "玩家 " << player->id() << " 被裁判判负,已清除所有单位";
}
break;
}
case CommandType::kForceAdvance:
// 强制推进:注入空指令给掉线玩家
// 仅在调试/开发模式允许
if (config_.allow_force_advance) {
InjectDefaultInputsForLaggingPlayers();
}
break;
}
}
}
6.4 最大超前帧数检查逻辑
cpp
bool GameEngine::IsAllInputsReady(int32_t frame_id) {
for (const auto& player : active_players_) {
// 检查该玩家是否已提交 frame_id 的指令
if (!player.HasSubmittedInput(frame_id)) {
// 检查是否已触发 max_ahead_frames 上限
int32_t lag_frames = current_frame_ - player.ack_frame();
if (lag_frames >= config_.max_ahead_frames) {
// 超过上限,触发等待状态
// 这里不直接返回false,而是让主线程进入阻塞等待
// 同时由外部裁判系统监测超时并干预
LOG(WARNING) << "玩家 " << player.id()
<< " 落后 " << lag_frames
<< " 帧,已达到上限 " << config_.max_ahead_frames
<< ",引擎即将阻塞等待";
return false;
}
}
}
// 所有玩家都已提交指令,或落后的玩家尚未达到上限
return true;
}
7. 异常处理与边界情况
7.1 玩家主动退出
| 场景 | 处理方式 |
|---|---|
| 玩家点击"退出/认输" | 客户端通过控制指令通道发送退出请求 → 引擎标记该玩家为失败 → 清除其单位 → 继续推进(无需等待其指令) |
| 玩家强制关闭进程 | 无退出请求 → 引擎检测到指令超时 → 进入阻塞等待 → 外部裁判系统超时判负 |
7.2 客户端重连
重连流程:
1. 客户端检测到状态推送通道超时(收不到 Delta 超过 2 秒)
2. 客户端通过快照通道发起重连请求
3. 服务器返回 snapshot_buffer_[current_frame_ % BUFFER_SIZE]
4. 客户端清空本地所有游戏对象,反序列化快照
5. 客户端设置 ack_frame = server.current_frame
6. 客户端继续正常上报 req_frame = ack_frame + 1 的指令
7. 服务器检测到该玩家重新加入活跃列表,继续等待其指令
8. 若快照拉取失败(网络原因),客户端可重试,引擎继续等待
7.3 裁判系统超时阈值配置
| 比赛等级 | 引擎等待超时阈值(裁判判负触发) | 说明 |
|---|---|---|
| 娱乐局 | 120 秒 | 给予充足时间重启路由器 |
| 排位赛 | 60 秒 | 容忍短暂网络波动 |
| 职业线上 | 30 秒 | 选手需在30秒内重连 |
| 职业线下 | 无限(由裁判人工控制) | 裁判现场判断,不受自动超时约束 |
7.4 环形缓冲区满覆盖处理
- 缓冲区容量固定为 30 帧
- 当
frame_id % 30的位置已有数据时,直接覆盖(旧帧不再需要) - 若重连客户端请求的帧号比
current_frame_ - 30更早,返回错误码ERR_FRAME_TOO_OLD,客户端只能拉取最新帧
8. 配置参数汇总
8.1 引擎配置文件 (server_config.yaml)
yaml
game:
tick_rate_hz: 20
max_ahead_frames: 20 # 默认1秒缓冲
snapshot_buffer_size: 30 # 环形缓冲区容量
channels:
control_channel_port: 50051
state_push_channel_port: 5556
snapshot_channel_port: 50052
admin_channel_port: 50053
referee:
mode: hybrid # auto / manual / hybrid
auto_timeout_seconds: 120 # 自动化超时判负阈值
allow_force_advance: false # 是否允许调试强制推进
logging:
level: info
enable_frame_trace: false # 仅调试时开启
8.2 赛事等级预设配置
yaml
# 预设1: 娱乐局
preset_casual:
max_ahead_frames: 60
auto_timeout_seconds: 120
# 预设2: 排位赛
preset_ranked:
max_ahead_frames: 10
auto_timeout_seconds: 60
# 预设3: 线上职业赛
preset_tournament_online:
max_ahead_frames: 2
auto_timeout_seconds: 30
# 预设4: 线下局域网赛
preset_tournament_lan:
max_ahead_frames: 0
auto_timeout_seconds: 0 # 由裁判人工操作,自动超时禁用
9. 接口定义
9.1 客户端 → 引擎(控制指令通道)
| 接口 | 方向 | 说明 |
|---|---|---|
ReportInput(stream InputRequest) |
客户端流式 → 引擎 | 每帧上报玩家操作指令 |
RequestSnapshot(SnapshotRequest) |
客户端 → 引擎 | 重连时拉取完整快照 |
RequestQuit(QuitRequest) |
客户端 → 引擎 | 主动退出/认输 |
数据契约:
json
InputRequest:
{
"player_id": 1,
"req_frame": 1001,
"ack_frame": 1000,
"input_mask": 0x0000000A
}
SnapshotRequest:
{
"player_id": 1,
"client_last_frame": 980
}
SnapshotResponse:
{
"current_frame": 1020,
"serialized_world": "<binary data>"
}
9.2 裁判系统 → 引擎(管理指令通道)
| 接口 | 方向 | 说明 |
|---|---|---|
SendAdminCommand(AdminRequest) |
裁判系统 → 引擎 | 发送判负/继续等待等指令 |
GetEngineStatus(Empty) |
裁判系统 → 引擎 | 查询引擎运行状态 |
GetFrameSnapshot(FrameRequest) |
裁判系统 → 引擎 | 调试用,拉取指定帧快照 |
数据契约:
json
AdminRequest:
{
"type": "DECLARE_DEFEAT | CONTINUE_WAIT | FORCE_ADVANCE",
"target_player_id": 1,
"wait_duration_ms": 30000
}
EngineStatus:
{
"current_frame": 1020,
"player_count": 8,
"waiting_for_player": 3,
"state": "RUNNING | WAITING | FINISHED"
}
9.3 引擎 → 客户端(状态推送通道)
主题/分类: delta.{frame_id}
内容:
{
"frame": 1001,
"timestamp": 1734567890123,
"deltas": [...],
"removed_entities": [...]
}
10. 未来扩展考虑
| 扩展方向 | 设计兼容性说明 |
|---|---|
| 录像回放(Replay) | 当前架构已保存每帧完整状态快照,录像只需存储所有帧的输入序列 + 随机种子即可重放 |
| 观战模式(Spectator) | 观战客户端可订阅状态推送通道,拉取最新帧快照初始化,无需提交指令 |
| P2P 模式备选 | 若后续需要支持客户端建房(无服务器),可参考替代方案(如 GameNetworkSockets + ICE),作为本方案的并行模式 |
| 断线重连历史回放 | 当前为"最新帧快照"策略,未来若希望断线玩家看到中间过程,可将环形缓冲区容量扩展并支持增量回放链 |
11. 术语表
| 术语 | 定义 |
|---|---|
| 帧(Frame) | 游戏逻辑的最小时间单位,固定 50ms |
| Delta | 本帧相对于上一帧发生变化的实体状态集合(增量) |
| Snapshot | 某一帧的完整游戏世界状态(全量) |
| 最大超前帧数(Max Ahead Frames) | 服务器允许领先最慢客户端确认帧的最大帧数差值 |
| 确定性(Deterministic) | 相同输入序列 + 相同随机种子在任何环境产生相同输出的性质 |
| 条件变量(Condition Variable) | 线程同步原语,用于阻塞等待特定条件满足 |
| 服务器权威(Server-Authoritative) | 所有关键逻辑计算仅在服务器执行,客户端仅作为展示端 |
| 环形缓冲区(Ring Buffer) | 固定大小的循环队列,新数据覆盖旧数据 |
| 裁判系统(Referee System) | 独立于引擎的外部服务,负责超时监测、判负裁决、人工操作界面 |
| 指令(Input) | 玩家在单帧内的操作集合,包含键盘/鼠标状态位掩码 |
| 控制指令通道(Control Channel) | 客户端到服务器的指令上报通道,可靠有序传输 |
| 状态推送通道(State Push Channel) | 服务器到客户端的状态广播通道,允许丢包,低延迟优先 |