跳到主要内容

权威模拟

最后更新:2026 年 9 月 15 日

玩家进入房间后,由服务器决定实际发生了什么——谁在哪里、谁打中了谁、比分是多少。把这些放在服务器而非客户端上完成,正是让游戏具有权威性、难以作弊的原因。房间 SDK 提供固定步长的 tick 循环、把逻辑组合成 sim system 的方式,以及将结果状态高效流式发送给客户端的快照。

信息

权威模拟是可选的。回合制或弱同步的游戏可以完全忽略 tick 循环,只用房间花名册和消息即可。当服务器需要推进一个连续世界时——移动、物理、实时战斗——再采用它。


tick 循环​

房间以固定步长推进,从而无论网络时序如何抖动都保持行为的确定性。

  • OnSimStep(float dt) 运行一个固定的模拟步。SimStepSeconds 设置步长(默认 1/30 秒)。
  • OnTick(float dt) 每个 host tick 运行一次并驱动步进,最多补齐 MaxCatchUpSteps 个错过的步(默认 6),使短暂停顿不会让世界永久落后。
protected override float SimStepSeconds => 1f / 60f;   // 60 Hz 模拟

protected override void OnSimStep(float dt)
{
// 推进移动、结算战斗、走计时器——全部在服务器上权威执行
}

补齐被有意设了上限:如果服务器跟不上,它每个 tick 最多多跑几步,然后接受一个更慢的世界,而不是陷入不断增长的积压。


sim system​

与其写一个庞大的 OnSimStep,你可以把行为拆成 sim system——每个只做一件事的小单元(先移动、再碰撞、再重生),按既定顺序运行。有两种形态:对整个房间运行一次的 system(ISimSystem)和按玩家运行的 system(IPlayerSimSystem),各自绑定到某个 SimPhase,使顺序显式可见。用 AddPlayerSystem(...) 注册按玩家的 system。

这让大型游戏保持可读:每条规则相互隔离、可测试,而它们的运行顺序是数据,而非一堆缠绕的调用。默认的 OnSimStep 就是运行你注册的这些 system。


快照​

服务器持有真相;客户端需要一份稳定而廉价的视图。快照是你声明一次、由 SDK 按计划流式发送给客户端的房间状态投影。

protected override void Setup()
{
DeclareSnapshot<MySnapshot>(plan =>
{
plan.Group = "room";
plan.Cadence = SnapshotCadence.EveryN(2); // 每 2 个 tick
plan.Assemble = ctx => BuildSnapshot(ctx); // 从当前状态构建
});
}

节奏控制快照发出的频率——SnapshotCadence.EveryTick 适合快节奏动作,EveryN(n) 则用于较平缓的游戏或节省带宽而降低频率。快照与模拟速率相互独立,因此你可以以 60 Hz 模拟、以 20 Hz 广播。快照通过一个群组送达客户端,并且默认在相关事件发生时也会立即发布(ForcePublishOnEvent)。

自适应节奏​

对于强度会变化的房间,AdaptiveCadencePolicy 可让广播速率随活跃度起落——交火时高频推送,房间安静时回落——受 WsOpsBudget 约束,并夹在 MinHz(默认 1)与 MaxHz(默认 15)之间。可通过 PLAYSERV_BROADCAST_HZ 按环境固定一个速率。SnapshotContext 为方案提供组装每帧所需的 tick 和已排空的事件。

备注

快照描述的是发送什么、多久发送一次,而非客户端如何绘制。插值、预测与校正位于客户端;服务器的职责是发出一条权威且节奏良好的流。


gameplay 构件​

对于常见机制,你把玩家或实体类型由一组小的角色接口组合而成(位置、带速度的物体、生命值、输入序号等),好让内置 system 能作用于任何采用它们的游戏。并非必须使用它们——机制不同的游戏可定义自己的——它们只是内置 system 所讲的“词汇”。


持久化结果​

一局结束时,你通常希望有些东西能留存下来——比分、统计、库存变化。两条声明把房间连到你的游戏数据:DeclareDataMirror(...) 在房间状态变化时把它镜像到一张表,DeclarePersistedCounter(...) 累加一个运行值(如比分)并写回。由于服务器 SDK 以服务器主体身份行事,这些写入不受客户端表权限约束——见访问控制。


下一步​