Netcode for Entities 实战指南:如何让 ECS 多人同步驯服延迟、告别橡皮筋

Netcode for Entities 实战指南:如何让 ECS 多人同步驯服延迟、告别橡皮筋 Netcode for Entities 实战指南如何让 ECS 多人同步驯服延迟、告别橡皮筋【免费下载链接】EntityComponentSystemSamples项目地址: https://gitcode.com/GitHub_Trending/en/EntityComponentSystemSamples你按下射击键角色 0.3 秒后才开火松开摇杆角色又往前滑了两米。如果你曾在多人游戏里被这种手感卡住那 ECS 网络同步就是绕不开的一课。Netcode for Entities 是 Unity 官方给出的 DOTS 生态答案服务器持有唯一真实状态客户端负责即时性两者之间隔着一层薄而精确的网络协议。这里的示例全部来自官方示例仓库的 NetcodeSamples 与 Dots101/Netcode101 目录可以打开编辑器跟着跑。想象这一群鱼每条鱼都是一个玩家角色——你要的是所有客户端看到的鱼群一致但每个人自己的操作按下键就生效。按了射击键 0.3 秒才开火多人游戏卡在哪道坎上⚠️ 多人项目的第一个坑几乎从来不是发不出去而是发出去但不一样。三类抱怨会在每个项目里重复出现开枪的人说我明明先开的枪挨打的人说我明明躲开了围观的人说为什么你们俩看到的画面不一样。这三句抱怨背后是同一个矛盾服务器要花时间验证和计算你想要服务器说了算就得付出等待的代价。Netcode 的选择不是二选一而是让两边同时跑——客户端先演一遍服务器后复述一遍两边对不上时由框架把差异平滑掉。整个架构因此围绕三个角色运转服务器 World 跑全部权威逻辑客户端 World 做预测、插值和渲染中间的 NetStream 通道把命令往上送、把快照往下送。你不需要手工搭这三样东西。仓库里 Dots101/Netcode101 的 Kickball 示例就是一个完整的最小闭环同一台编辑器开两个客户端一个玩家踢出球另一个立刻看到。上手时建议先跑一遍 Kickball 再读代码体感会比文字清楚得多。接受了服务器说了算紧接着的第一个具体问题就是哪些数据值得过网络这是同步的地基。同步地基三步定好 Ghost 同步类型新手最常问的一句话是生命值要不要标 [GhostComponent]速度要不要枪口火花要不要全标上带宽账单爆炸标少了客户端连画面都画不出来。先打个比方——Ghost 同步类型就像群聊里的 规则AllPredicted 是所有人而且你能预览我发的内容Server 是群主单方面宣布其他人只管收Client 是你自己的草稿谁也别想看。游戏里的数据其实只有两种状态你站在哪、还剩多少血和命令这一帧我做了什么。Netcode 把两者统一成 Ghost 组件实现 ICommandData 的是命令其余是状态命令身上还挂着一个 NetworkTick告诉服务器这条命令属于哪一帧。这份数据在干什么谁能写谁能读该选的类型玩家输入摇杆、开火本地客户端所有人服务器拿它回放AllPredicted ICommandData角色位置与速度服务器算、客户端预测所有人渲染AllPredicted血量、分数、击杀数服务器所有人Server本地 UI、粒子、屏幕震动本机客户端仅本机Client第一步是定义消息。Asteroids 示例里飞船的操纵键就是下面这样——关键看两处[GhostComponent] 属性决定同步策略ICommandData 接口告诉 Netcode这是命令不是状态。[GhostComponent(PrefabType GhostPrefabType.AllPredicted)] public struct ShipCommandData : ICommandData { public NetworkTick Tick { get; set; } // 关键标记这条命令属于哪一帧 public byte left; public byte right; public byte thrust; public byte shoot; }四个动作各用一个 byte 装下是省带宽的小例子快照里实际只会发送变化过的字段这一点在带宽账单部分再拆开说。源码在 ShipCommandData.cs。第二步是把按键写进缓冲。Kickball 的输入系统是个标准模板注意 GhostInputSystemGroup 和 GhostOwnerIsLocal 两处前者保证写入发生在 Netcode 规定的输入阶段后者保证只改本地玩家的缓冲不会动到别人输入副本。[UpdateInGroup(typeof(GhostInputSystemGroup))] public partial struct PlayerInputSystem : ISystem { public void OnUpdate(ref SystemState state) { var moveValue InputSystem.actions.FindAction(Move).ReadValueVector2(); // 关键只写本地玩家的输入缓冲 foreach (var input in SystemAPI.QueryRefRWPlayerInput() .WithAllGhostOwnerIsLocal()) { input.ValueRW.Horizontal moveValue.x; input.ValueRW.Vertical moveValue.y; // ... 此处省略约 10 行按键检测如踢球标志位 } } }写完输入后就不用再想怎么发Netcode 会自动打上 tick 号发往服务器。第三步最容易被忘掉把角色预制体注册进 Subscene 的 EntityPrefabs——不注册的话服务器不知道这个实体长什么样客户端收到的只会是一串没法定型的 ID。源码见 PlayerInputSystem.cs。数据流向定好之后下一个问题就是手感玩家不想等服务器那一个来回。手感优先预测半径调多大才不橡皮筋角色比你的按键慢 100 毫秒你就会觉得这不是我的角色。预测的做法像点外卖先自己炒一盘吃上外卖到了再对账玩家先吃先看到对账后置。完整链路是这样按下键 → 客户端当场本地预测出移动本帧就生效 → 命令同时带 tick 上行 → 服务器用权威逻辑重放同一帧 → 快照下行 → 客户端把我预测的和服务器说的对账。对得上玩家毫无感知对不上Netcode 把误差摊到几帧里修正。这套先演后对账就是客户端预测回滚机制。预测不是免费的每个预测实体都要让客户端多跑一份模拟所以不是场上每个实体都值得预测。Asteroids 示例用两个并行 Job 做近处预测、远处插值注意它用了两个半径var queues SystemAPI.GetSingletonRWGhostPredictionSwitchingQueues().ValueRW; // 半径内把 ghost 切到预测 new SwitchToPredictedGhostViaRange { playerPos playerPos, enterRadiusSq settings.predictionRadius * settings.predictionRadius, predictedQueue queues.ConvertToPredictedQueue, // ... 此处省略约 5 行并行命令缓冲初始化 }.ScheduleParallel(); // 退出半径 预测半径 余量防跨界状态抖动 new SwitchToInterpolatedGhostViaRange { playerPos playerPos, exitRadiusSq (settings.predictionRadius settings.predictionRadiusMargin) * (settings.predictionRadius settings.predictionRadiusMargin), interpolatedQueue queues.ConvertToInterpolatedQueue, // ... 此处省略约 5 行并行命令缓冲初始化 }.ScheduleParallel(); 半径怎么定给个实操公式覆盖最坏往返时间里角色能跑出的距离100ms ping × 10m/s ≈ 一两米再加余量margin 取半径的 1.2~2 倍。两个半径错开就是不橡皮筋的关键同一阈值切换时玩家贴着边界走会逐帧在预测/插值之间反复横跳错开之后进入和离开各只触发一次切换。自己的角色建议永远预测其他角色交给半径。源码在 AsteroidSwitchPredictionSystem.cs。服务器和预测对不上时Netcode 手里握着每个实体的预测历史快照一到就回滚到对应 tick、重放几步差异小就按 TransitionDurationSeconds 摊开差异大通常是服务器端物理或延迟造成就先瞬移再混合。玩家只会觉得轻轻一推而不是被传走了。有了预测也少不了跟服务器打个电话——玩家要求进对局、服务器要给某个特定客户端单独推消息时靠的就是 RPC。跨端对话RPC 从定义到广播的完整链路RPC 像在群里寄一封实体信把内容命令结构体写进信封Netcode 负责投递和回执。完整链路五步定义实现 IRpcCommand 的消息结构体 → 客户端造一个临时实体挂上消息 SendRpcCommandRequest 发出 → 服务器端它变成一个带 ReceiveRpcCommandRequest 的实体 → 你的系统处理单播设置了 TargetConnection或广播不设置→ 处理完销毁实体。flowchart LR A[定义 IRpcCommand 结构体] -- B[客户端: 临时实体br/ SendRpcCommandRequest] B -- C[Netcode 网络传输] C -- D[服务器: 实体携带br/ReceiveRpcCommandRequest 出现] D -- E{发送方式} E --|TargetConnection 已设置| F[单播: 仅该连接收到]【免费下载链接】EntityComponentSystemSamples项目地址: https://gitcode.com/GitHub_Trending/en/EntityComponentSystemSamples创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考