Unity Netcode 预测切换(Prediction Switching)实战:EntityComponentSystemSamples 中按需启停客户端预测的完整解析
【免费下载链接】EntityComponentSystemSamples项目地址: https://gitcode.com/GitHub_Trending/en/EntityComponentSystemSamples
本篇基于 EntityComponentSystemSamples 仓库中 PredictionSwitching 示例文档 展开,讲解 Unity Netcode for Entities 的"预测切换"机制:如何在游戏运行时(playmode)动态切换 Ghost 的预测/插值模式,让客户端只对近处实体做客户端预测、远处实体退回插值,从而节省 CPU 开销。读完本文,你将理解该示例的沙盒结构、核心 System 的双 Job 半径判定逻辑、配置组件的烘焙链路,以及可直接调用的场景默认参数。
一、什么是 Prediction Switching
Netcode for Entities 中每个同步的实体(Ghost)都有一种"GhostMode":客户端可以选择预测(Predicted)——用客户端预测本地模拟以追求响应性,也可以选择插值(Interpolated)——只回放服务器历史状态以换取更低的客户端 CPU 开销。Prediction Switching 允许你在运行时切换某个 Ghost 的 GhostMode,即让客户端在两种模式之间按需动态"加入(opt-in)"或"退出"预测,从而在响应性与性能之间取得平衡,同时保留服务器权威。
原文档给出的核心结论(Takeaways)值得完整保留:
- 一般而言,与 Netcode 中所有机制相同:移动越快、越不可预测的实体,补偿(compensation)起来就越困难;
- Prediction Switching 让你选择性地对某些 Ghost 开启预测,在**客户端性能(通过插值)与游戏响应性(通过客户端预测)**之间取得合理的折中,同时维持服务器权威。
二、示例结构:一个简化的"足球"沙盒
该示例用一个简化的"football"沙盒演示上述思想。示例位于 NetcodeSamples/Assets/Samples/PredictionSwitching 目录,包含两个关键 Prefab:
| 资产 | 角色 |
|---|---|
| Sphere.prefab | 物理球。它(在数量多时)是一个预测成本相对较高的对象,正是本次优化的目标——即"足球"。 |
| Player.prefab | 玩家(Character Controller),它会与这些球发生碰撞交互,并定义"Prediction Switching Radius"的圆心(见 PredictionSwitchingSystem.cs)。 |
颜色图例(Color Key)
- 青色(Cyan)——处于插值模式的 Ghost;
- 绿色(Green)——处于预测模式的 Ghost;
- 玩家自身的颜色变化不在图例范围内(被排除)。
注意:原文档特别指出,这套颜色图例同时被"Bounding Box Drawer"工具使用,可通过
Multiplayer PlayMode Tools Window > Bounding Box Drawer > Disabled按钮切换该工具。
运行后的可观察现象
进入 playmode 后你可以观察到:
- 位于玩家半径内的球会切换到 Predicted(绿色),半径外的则回到 Interpolated(青色),反之亦然;
- 在模式切换的瞬间(Transition)可以观察到插值平滑过渡正在生效——尤其是当球与球之间互相弹开时。
三、配置项:PredictionSwitchingSettings 及其场景默认值
示例文档要求读者通过PredictionSwitchingSettingsAuthoring这个MonoBehaviour修改设置并观察其对玩法的影响。该 Authoring 组件(PredictionSwitchingSettingsAuthoring.cs)通过RegisterBinding把序列化字段映射到 ECS 组件PredictionSwitchingSettings(PredictionSwitchingSettings.cs),并由内部Baker烘焙出该组件:
public struct PredictionSwitchingSettings : IComponentData { public Entity Player; public float PlayerSpeed; public float TransitionDurationSeconds; public float PredictionSwitchingRadius; /// <summary>The margin must be large enough that moving from predicted time to interpolated time does not move the ghost back into the prediction sphere.</summary> public float PredictionSwitchingMargin; public byte BallColorChangingEnabled; }各字段含义与仓库中 PredictionSwitchingEntityScene.unity 场景里的实际默认值:
| 字段 | 含义 | 场景默认值 |
|---|---|---|
Player | 玩家实体引用(Player Prefab 实例) | 指向 Player Prefab |
PlayerSpeed | 玩家移动速度(供输入应用 Job 使用) | 12 |
TransitionDurationSeconds | 预测/插值切换时的过渡时长(秒),过渡期间施加平滑插值 | 1.2 |
PredictionSwitchingRadius | 预测切换半径(进入半径),以玩家为圆心 | 18 |
PredictionSwitchingMargin | 退出半径的额外余量;源码注释明确要求:该余量必须足够大,保证 Ghost 从预测时间切到插值时间时,不会因时间回退而重新落回预测球内部(否则会出现来回抖动的"乒乓效应") | 4 |
BallColorChangingEnabled | 是否启用球体变色指示(1 启用 / 0 禁用) | 1 |
四、核心实现:PredictionSwitchingSystem 的双 Job 半径判定
从源码结构看,整个机制由一个仅在客户端模拟世界运行的 Burst 编译系统 PredictionSwitchingSystem.cs 驱动:
[BurstCompile] [WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)] public partial struct PredictionSwitchingSystem : ISystemOnCreate中声明了三个前置依赖:PredictionSwitchingSettings(配置组件)、CommandTarget(本连接对应的玩家实体)、GhostPredictionSwitchingQueues(Netcode 提供的预测切换队列),并建立GhostOwner的 ComponentLookup 以区分"本方拥有的 Ghost"(玩家自己)与他人的 Ghost。
OnUpdate的流程(第 25~62 行):
- 从
CommandTarget取出玩家实体并读取其LocalTransform位置作为圆心; - 通过
EndSimulationEntityCommandBufferSystem创建ParallelWriter 命令缓冲,用于安全地在 Job 内修改组件; - 并行调度两个
IJobEntity:SwitchToPredictedGhostViaRange:进入半径判定(第 41~50 行);SwitchToInterpolatedGhostViaRange:退出半径判定(第 53~61 行)。
进入半径 Job:把远处的球切为预测
[BurstCompile] [WithNone(typeof(PredictedGhost), typeof(SwitchPredictionSmoothing))] partial struct SwitchToPredictedGhostViaRange : IJobEntity { // ... void Execute(Entity ent, [EntityIndexInQuery] int entityIndexInQuery, in LocalTransform transform, in GhostInstance ghostInstance) { if (ghostInstance.ghostType < 0) return; // 跳过无效 Ghost if (math.distancesq(playerPos, transform.Position) < enterRadiusSq) { predictedQueue.Enqueue(new ConvertPredictionEntry { TargetEntity = ent, TransitionDurationSeconds = transitionDurationSeconds, }); if (ballColorChangingEnabled == 1 && !ghostOwnerFromEntity.HasComponent(ent)) parallelEcb.AddComponent(entityIndexInQuery, ent, new URPMaterialPropertyBaseColor { Value = new float4(0, 1, 0, 1) }); // 绿色 } } }要点:
- 查询用
WithNone(typeof(PredictedGhost), typeof(SwitchPredictionSmoothing))过滤,只对尚未预测且不在过渡中的 Ghost 生效; - 使用平方距离(
distancesq < enterRadiusSq)避免开方开销,enterRadiusSq = Radius × Radius(第 46 行); - 判定命中后向
GhostPredictionSwitchingQueues.ConvertToPredictedQueue入队一条ConvertPredictionEntry,携带TargetEntity与TransitionDurationSeconds——由 Netcode 内部的转换系统在过渡时长内完成模式切换与平滑; - 若启用了变色且该 Ghost 不是本方所有(
ghostOwnerFromEntity.HasComponent(ent)为假),就通过并行 ECB 添加 URP 基础色组件,把球染成绿色(0,1,0,1)。
退出半径 Job:把近处的球切回插值
[BurstCompile] [WithNone(typeof(SwitchPredictionSmoothing))] [WithAll(typeof(PredictedGhost))] partial struct SwitchToInterpolatedGhostViaRange : IJobEntity { // ... void Execute(Entity ent, [EntityIndexInQuery] int entityIndexInQuery, in LocalTransform transform, in GhostInstance ghostInstance) { if (ghostInstance.ghostType < 0) return; if (math.distancesq(playerPos, transform.Position) > exitRadiusSq) { interpolatedQueue.Enqueue(new ConvertPredictionEntry { TargetEntity = ent, TransitionDurationSeconds = transitionDurationSeconds, }); if (!ghostOwnerFromEntity.HasComponent(ent)) parallelEcb.RemoveComponent<URPMaterialPropertyBaseColor>(entityIndexInQuery, ent); // 恢复默认色(青色插值态) } } }关键设计在于退出半径比进入半径大(第 52~58 行):
var radiusPlusMargin = (predictionSwitchingSettings.PredictionSwitchingRadius + predictionSwitchingSettings.PredictionSwitchingMargin); // exitRadiusSq = radiusPlusMargin * radiusPlusMargin这正是配置项PredictionSwitchingMargin的作用:进入用半径 R(默认 18),退出用 R+M(默认 22)。这个"滞回(hysteresis)"双阈值结构避免实体在半径边界附近来回穿越时反复切换模式;而WithAll(typeof(PredictedGhost))保证只对当前处于预测状态的 Ghost 做退出判定。
五、配套系统:连接、输入与相机跟随
示例还包含若干支撑系统,完整呈现了一个 Netcode 客户端/服务器分治的骨架(均带文件路径可查证):
- 连接与玩家实例化(PredictionSwitchingConnect.cs):
- 客户端侧
PredictionSwitchingConnectClientSystem在InitializationSystemGroup中为所有带NetworkId且尚未标记的本地实体加上NetworkStreamInGame组件,表示"已进入游戏"; - 服务器侧
PredictionSwitchingConnectServerSystem为每个连接实体Instantiate配置中的Player原型,打上对应NetworkId的GhostOwner(第 44 行),按NetworkId奇偶在场地边缘排成一行生成(第 46~56 行),并把玩家实体加入该连接的LinkedEntityGroup,使其在断线时被自动销毁(第 59~60 行)。
- 客户端侧
- 输入命令(PredictionSwitchingInput.cs):
PredictionSwitchingInput是一个ICommandData,通过[GhostComponent(OwnerSendType = SendToOwnerType.SendToNonOwner)]只把输入命令发给非本方的客户端(服务器与旁观者),而本方玩家直接用本地输入。系统PredictionSwitchingSampleInputSystem在GhostInputSystemGroup中读取 WASD/方向键/触摸键写入DynamicBuffer<PredictionSwitchingInput>(第 30~53 行)。 - 薄客户端(Thin Client)演示:
PredictionSwitchingThinInputSystem(第 58~93 行)在ThinClientSimulation世界中按服务器 tick 周期自动生成左右往复的假输入,用于在没有完整模拟的瘦客户端上验证输入流。 - 服务端输入应用:
PredictionSwitchingApplyInputSystem(第 97~142 行)运行在PhysicsSystemGroup、PhysicsInitializeGroup之前,从指定 tick 取出输入命令并归一化方向、乘以PlayerSpeed写入PhysicsVelocity——这正是服务器权威模拟中"输入命令驱动角色"的标准写法。 - 相机跟随:
PredictionSwitchingCameraFollowSystem(第 146~176 行)在客户端跟随带GhostOwnerIsLocal的本地玩家,保持固定偏移。 - 输入缓冲的烘焙由 PredictionSwitchingInputAuthoring.cs 完成,仅为 Player Prefab 实例添加
DynamicBuffer<PredictionSwitchingInput>。
六、环境与运行前提
- 本示例属于仓库的NetcodeSamplesUnity 项目,
Packages/manifest.json锁定版本为:com.unity.netcode 1.12.0、com.unity.entities 1.4.4、com.unity.physics 1.4.4、com.unity.render-pipelines.universal 17.3.0; - 编辑器版本见 ProjectVersion.txt:
6000.3.9f1(Unity 6000 系列); - 代码位于独立程序集 PredictionSwitching.asmdef(开启
allowUnsafeCode),依赖 Netcode、Entities、Physics、Graphics 等程序集; - 运行方式:在 Unity 中打开该 Netcode 项目,进入
PredictionSwitching场景后直接 Play。编辑器会按 Netcode 的客户端/服务器引导流程在单进程内同时拉起 Server 与 Client 世界,此时即可观察到球随玩家距离在青色(插值)与绿色(预测)之间切换。
七、可复用的工程经验
结合原文档 Takeaways 与源码实现,这套示例沉淀了三条可迁移到实际项目的经验:
- 预测是有预算的:移动快、交互多(如物理球群)的实体,客户端预测的 CPU 成本显著更高;对远离视线的实体退回插值,是标准的性能优化手段。
- 切换需要过渡与滞回:
TransitionDurationSeconds(本例 1.2s)保证切换期间插值平滑,避免位置跳变;PredictionSwitchingRadius + PredictionSwitchingMargin的双阈值滞回结构避免边界抖动,两者共同保证了"切换无感知"。 - 一切在客户端判定、服务器保持权威:从源码结构看,半径判定与切换队列全部发生在
ClientSimulation世界([WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)]),服务器模拟不受影响;切换本身通过 Netcode 的GhostPredictionSwitchingQueues官方队列完成,而非手动改组件,这正是该机制可被官方支持的扩展点。
如果你想继续深入,仓库中 NetcodeSamples/README.md 列出了 NetCube、HelloNetcode、Asteroids、PredictionSwitching、PlayerList 等全部 Netcode 示例,其中 HelloNetcode 系列按 Basic/Intermediate/Advanced 分层,适合配合本示例补全对服务器权威、RPC、输入命令等前置机制的理解。
【免费下载链接】EntityComponentSystemSamples项目地址: https://gitcode.com/GitHub_Trending/en/EntityComponentSystemSamples
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考