拓冰建站拓冰建站
首页 / 资讯中心 / 正文

HelloNetcode PredictedSpawning 示例:Unity Netcode for Entities 中的客户端预测生成与自定义分类系统实现

HelloNetcode PredictedSpawning 示例Unity Netcode for Entities 中的客户端预测生成与自定义分类系统实现【免费下载链接】EntityComponentSystemSamples项目地址: https://gitcode.com/GitHub_Trending/en/EntityComponentSystemSamples本篇以 Unity Netcode for Entities 官方示例库中的 PredictedSpawning 样本为核心讲解“预测生成Predicted Spawning”的完整机制客户端在服务器确认前先行实例化网络对象并在服务器幽灵快照ghost snapshot到达时通过自定义分类系统把“本地预测实体”与“快照中的新生成实体”按 SpawnId 精确匹配并替换从而消除玩家自己生成物如手榴弹的视觉延迟。读完后你将掌握预测生成的组件设计、输入事件计数匹配方案、自定义GhostSpawnClassificationSystemGroup系统的写法以及配置参数的调优方式。一、背景为什么需要预测生成在 Netcode for Entities 的服务器权威同步模型下见 NetcodeSamples/README.md所有网络对象由服务器拥有正常流程是服务器生成后通过幽灵快照下发给客户端客户端再实例化。这套流程对“服务器主动生成”的实体没有问题但当生成动作由客户端输入触发时例如玩家开枪、扔手榴弹客户端必须等服务器收到输入、模拟、生成并下发快照玩家自己看到的物体会有明显的“慢一拍”。PredictedSpawning 样本给出的解法是客户端在收到本地生成输入的瞬间就在预测prediction系统中先实例化该对象等服务器快照中的新生成到达时客户端不做第二次实例化而是把快照中的生成项“替换”为已经存在的预测实体。样本中的具体场景是在 CharacterController 角色控制样本的基础上给玩家加一门榴弹发射器玩家按住右键SecondaryFire时客户端和服务器同时预测生成一颗手榴弹。二、样本前置依赖与场景结构根据 PredictedSpawning.md 的说明该样本建立在以下三个前置样本之上复用了它们的输入配置与角色控制预制体前置样本文档路径GoInGame连接后进入游戏GoInGame.mdSpawnPlayer自动生成玩家SpawnPlayer.mdCharacterController角色控制器CharacterController.md场景与源码文件位于 02_PredictedSpawning 目录PredictedSpawning.unity样本主场景其中放置了可调节各种数值的配置预制体手榴弹初速、引信时间等Grenade.prefab手榴弹幽灵预制体其幽灵组件数据中配置了SupportedGhostModes: 2、RollbackPredictionOnStructuralChanges: 1Explosion/客户端爆炸粒子特效混合渲染的 hybrid ParticleSystem。三、核心概念预测幽灵与 SpawnId 匹配样本的文档描述了整套机制的关键要素这里逐条展开手榴弹是预测幽灵predicted ghost。文档说明“Its configured as a predicted ghost so it will collide with objects immediately on the client, but get corrected if the servers view differs.” 即客户端上它会立即与其他物理对象碰撞若服务器视角不同则会被回滚修正。每次生成都带一个 SpawnId由“输入事件计数器 连接的 NetworkId”组合而成。所有玩家的计数器都从同一值开始因此必须把属主 NetworkId 编入才能全局唯一。自定义分类系统替代 Netcode 的默认预测生成分类用 SpawnId 把本地预测生成的手榴弹与幽灵快照中的新生成匹配匹配成功则“复用”已实例化的对象而不是再实例化一个新对象。3.1 功能开关组件整个样本的所有系统都依赖一个标记组件来启用// EnablePredictedSpawningAuthoring.cs public struct EnablePredictedSpawning : IComponentData { } public class EnablePredictedSpawningAuthoring : MonoBehaviour { class Baker : BakerEnablePredictedSpawningAuthoring { public override void Bake(EnablePredictedSpawningAuthoring authoring) { var entity GetEntity(TransformUsageFlags.Dynamic); AddComponentEnablePredictedSpawning(entity); } } }在 EnablePredictedSpawningAuthoring.cs 中场景里挂上这个 MonoBehaviour 并烘焙后后续每个相关系统都用state.RequireForUpdateEnablePredictedSpawning()声明依赖从而实现“没有该组件时整个样本逻辑不运行”。3.2 幽灵组件设计哪些数据被同步、哪些被刻意隔离GrenadeDataAuthoring.cs 定义了手榴弹的幽灵组件这是整个机制中非常关键的设计[GhostComponent(PrefabType GhostPrefabType.AllPredicted)] public struct GrenadeData : IComponentData { [GhostField] public uint SpawnId; // 唯一被同步的字段预测匹配的“指纹” public float DestroyTimer; // 未标记 [GhostField]不参与快照同步 } public class GrenadeDataAuthoring : MonoBehaviour { class Baker : BakerGrenadeDataAuthoring { public override void Bake(GrenadeDataAuthoring authoring) { var entity GetEntity(TransformUsageFlags.Dynamic); // Prevent predicted spawned grenades from predicting that they should be destroyed, // by setting DestroyTimer to inf. AddComponent(entity, new GrenadeData { DestroyTimer float.PositiveInfinity }); } } }两点值得注意PrefabType GhostPrefabType.AllPredicted把手榴弹标记为预测幽灵这正是文档所说“clients can predict spawn the object and then swap it in when the ghost is received in a server snapshot”的配置基础DestroyTimer没有[GhostField]且烘焙时初始化为PositiveInfinity。文档明确说明“the clients do not predict this event引信到点销毁这一事件”——破坏手榴弹、对周围物理对象施加爆炸冲量这两个权威行为只在服务器上发生客户端检测到幽灵被销毁后才补上纯视觉的粒子爆炸粒子特效是 hybrid particle system播完一轮后需要手动销毁对应下文ExplosionSystem的处理。3.3 配置组件可调参数一览文档提到“A configuration prefab is in the scene where various values can be tuned, like the initial velocity (power of the throw) for the grenade and its fuse timer”对应 GrenadeConfigAuthoring.cs 中的GrenadeConfig组件与默认值参数类型默认值作用InitialVelocityint15手榴弹抛出初速发射器前向 × 此值BlastTimerfloat3引信时间秒到点即引爆BlastRadiusint40爆炸半径源码中与距离平方比较BlastPowerint10爆炸推力强度BlastPowerClampYfloat1.5爆炸方向上做垂直偏置先对位移向量的 y 分量按此值钳制使爆炸更有“向上顶”的效果单位 m/sChainReactionForceExplodeDurationSecondsfloat0.4连锁反应被击中的手榴弹最多在此时间后强制起爆若原本就快要炸则保持原时间这些参数由场景中的配置预制体烘焙进GrenadeConfig单例供生成与引爆逻辑读取。四、预测生成路径ProcessFireCommandsSystem客户端按下右键后ProcessFireCommandsSystem.cs 在预测系统组HelloNetcodePredictedSystemGroup中消费输入事件并实例化手榴弹。关键实现细节1. 每个 tick 只预测一次。预测系统对同一个 tick 可能运行多轮回滚后重跑因此必须先做守卫var networkTime SystemAPI.GetSingletonNetworkTime(); if (!networkTime.IsFirstTimeFullyPredictingTick) return;2. 从输入增量确定生成数量。InputBufferDataCharacterControllerPlayerInput存储的是“相对上一 tick 的增量”系统读取character.Input.SecondaryFire.Count作为要生成的手榴弹数。源码注释解释了为什么必须用增量而非绝对值用户可能丢包点击计数器一次跳增多格、可能在部分 tick 内连续快速点击例如模拟频率 10Hz 时 100ms 内点两次、服务器也可能因性能问题批量处理多个 tick。同时系统会从输入缓冲中按networkTime.ServerTick取出绝对计数器值用于后面重构 SpawnId。此外还有防刷限流const int maxGrenadesPerPlayerPerServerTick 5; if (grenadesToSpawn maxGrenadesPerPlayerPerServerTick) { netDebug.LogWarning($Clamping player input, as theyre attempting to spawn {grenadesToSpawn} grenades in one tick (max: {maxGrenadesPerPlayerPerServerTick})!); grenadesToSpawn maxGrenadesPerPlayerPerServerTick; }3. 批量实例化并逐颗设置参数。用commandBuffer.Instantiate(grenadePrefab, grenadeEntities)一次性实例化然后对每颗手榴弹生成点取自玩家身上的锚点链slot → launcher → spawnPoint嵌套三层取其世界坐标与旋转初始物理速度 出生点前向 ×config.InitialVelocity同时按spawnId / maxGrenadesPerPlayerPerServerTick × DeltaTime × 速度对同帧多颗手榴弹做位置偏移避免叠在一起写入GrenadeDatavar grenadeData new GrenadeData() { DestroyTimer (float) time.ElapsedTime config.BlastTimer }; // Set the spawn ID for this particular local spawn so it can be used later in the classification system // Needs to include the network ID of the owner since everyones counters/spawnId starts at 1 grenadeData.SpawnId (uint) character.OwnerNetworkId 16 | secondaryFireCount;SpawnId高 16 位是属主 NetworkId、低 16 位是 SecondaryFire 的绝对计数同一 tick 内连发则逐颗递减重构这就是文档所说“the spawn ID (the input event counter with the network ID value of the connection)”。最后写入GhostOwner { NetworkId character.OwnerNetworkId }。源码注释强调这一点“important until its replaced by its interpolated version”——预测期间该实体由本地模拟驱动属主信息保证预测正确归属。由于该系统挂在预测系统组中服务器与客户端运行的是同一段代码服务器侧的“预测”即权威生成客户端侧的“预测”即为提前实例化二者天然共享 SpawnId 计算规则这是匹配能够成功的前提。五、分类系统把快照新生成“换”成本地预测实体默认情况下Netcode 的预测生成分类系统会尝试自动匹配预测实体与快照新生成但匹配依据有限例如只能按数量顺序对号。本样本展示了完全自定义分类系统的写法这也是文档第二段重点“This sample also demonstrates how you can implement a custom classification system instead of using the default one.”实现位于 GrenadeClassificationSystem.cs其系统声明本身就是一份“接入规范”[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)] // 只在客户端运行分类是客户端行为 [UpdateInGroup(typeof(GhostSpawnClassificationSystemGroup))] // 插入到 Netcode 的生成分类系统组 [UpdateAfter(typeof(GhostSpawnClassificationSystem))] // 在默认分类系统之后运行 [CreateAfter(typeof(GhostCollectionSystem))] [CreateAfter(typeof(GhostReceiveSystem))] [BurstCompile] public partial struct GrenadeClassificationSystem : ISystem流程分三步1. 解析本样本负责处理的幽灵类型。首次更新时从GrenadeSpawner.Grenade的预制体实体出发在GhostCollection实体的GhostCollectionPrefab缓冲中线性查找该预制体下标得到m_GhostType——后续用它区分“这条新生成是不是手榴弹”。2. 遍历快照带来的新生成列表并做替换。核心的GrenadeClassificationJob用[WithAll(typeof(GhostSpawnQueue))]锁定“有新生成待处理”的实体逐条处理DynamicBufferGhostSpawnBuffer newSpawns// 不是手榴弹的生成项直接跳过 if (newGhostSpawn.GhostType ! ghostType) continue; // 已被分类、或本身不是预测生成的跳过 if (newGhostSpawn.SpawnType ! GhostSpawnBuffer.Type.Predicted || newGhostSpawn.PredictedSpawnEntity ! Entity.Null) continue; // Mark all the grenade spawns as classified even if not our own predicted spawns ... newGhostSpawn.HasClassifiedPredictedSpawn true;这里有一个容易忽略的健壮性细节即使这条手榴弹不是自己预测生成的比如别的玩家扔的也要标记HasClassifiedPredictedSpawn true。源码注释解释了原因——否则当默认分类系统随后运行时可能会误把它与本方预测列表中尚未分类的条目错误配对。3. 用 SpawnId 完成精确匹配。从快照历史中取出该新生成的GrenadeDatasnapshotDataLookup.TryGetComponentDataFromSnapshotHistory再与本客户端PredictedGhostSpawnList预测生成清单每条含entity、ghostType中的条目比对var spawnIdFromList grenadeDataLookup[predictedSpawnList[j].entity].SpawnId; if (grenadeData.SpawnId spawnIdFromList) { newGhostSpawn.PredictedSpawnEntity predictedSpawnList[j].entity; // 指向本地已实例化实体 predictedSpawnList.RemoveAtSwapBack(j); // 从预测清单移除避免重复匹配 break; } newSpawns[i] newGhostSpawn;一旦写回PredictedSpawnEntityNetcode 后续生成阶段就不会为新生成再做实例化而是直接“换入”这个本地实体——文档中“uses the already spawned object instead of spawning a new one”落到源码就是这两行。若匹配失败例如预测已被回滚丢弃、或该生成来自其他玩家则条目保持未替换走常规生成路径实例化新对象。六、引爆与客户端表现GrenadeSystem 的双端分支GrenadeSystem.cs 在HelloNetcodePredictedSystemGroup中运行对手榴弹的引信与爆炸做双端差异化处理foreach (var (data, grenadeTransform, grenade) in SystemAPI.QueryRefROGrenadeData, RefROLocalTransform() .WithAllSimulate().WithNoneDisableRendering().WithEntityAccess()) { if (time.ElapsedTime data.ValueRO.DestroyTimer) { // 对爆炸半径内的物理对象施加冲量距离越远影响越小 foreach (var (velocity, grenadeData, transform) in SystemAPI.QueryRefRWPhysicsVelocity, RefRWGrenadeData, RefROLocalTransform().WithAllSimulate()) { var diff transform.ValueRO.Position - grenadeTransform.ValueRO.Position; var distanceSqrt math.lengthsq(diff); if (distanceSqrt config.BlastRadius distanceSqrt ! 0) { var scaledPower 1.0f - distanceSqrt / config.BlastRadius; // Add some verticality to the explosion, biasing towards world.up. diff.y diff.y -0.05f ? math.max(config.BlastPowerClampY, diff.y) : math.min(-config.BlastPowerClampY, diff.y); velocity.ValueRW.Linear config.BlastPower * scaledPower * (diff / math.sqrt(distanceSqrt)); // 连锁反应让被波及的手榴弹最多在 ChainReactionForceExplodeDurationSeconds 后起爆 grenadeData.ValueRW.DestroyTimer math.min(grenadeData.ValueRW.DestroyTimer, (float)time.ElapsedTime config.ChainReactionForceExplodeDurationSeconds); } } if (isServer) { commandBuffer.DestroyEntity(grenade); // 服务器权威销毁 } else { // 客户端仅生成爆炸 VFX if (networkTime.IsFirstTimeFullyPredictingTick) { var explosion commandBuffer.Instantiate(explosionPrefab); commandBuffer.SetComponent(explosion, LocalTransform.FromPosition(grenadeTransform.ValueRO.Position)); commandBuffer.AddComponentExplosionParticleSystem(explosion); // Hide it, and in doing so, prevent re-triggering (see above query filter). commandBuffer.AddComponentDisableRendering(grenade); } } } }与文档描述对应的几个要点服务器分支销毁手榴弹并对范围内物理对象按距离施加冲量scaledPower 1 - 距离²/爆炸半径²的衰减这正是文档所说“the grenade is destroyed by the server and will push other physics object away depending on their distance”。客户端分支只在第一次完整预测该 tick 时实例化爆炸粒子预制体打上ExplosionParticleSystem标记并给手榴弹本体加DisableRendering防止特效重复触发。文档强调该特效“is only visual and so happens only on the client”由于粒子系统挂在 Entities 实体上不会自动自我销毁同文件中的ExplosionSystemPresentationSystemGroup仅客户端负责在ps.time超过main.duration时手动销毁实体。发射器朝向同文件的GrenadeLauncherSystem在客户端与服务器上同时运行把武器槽AnchorPoint.WeaponSlot即榴弹发射器的挂点按角色俯仰角旋转——注释说明这是客户端/服务器确定手榴弹出生点所必需的一致行为。七、颜色校验肉眼验证“换入”没有出错文档说明“The spawned grenades will alternate between red and green coloring, and black for ones created via snapshot system and not swapped with the predict spawn. Just to help see that the swapping is not incorrect.”对应实现是 SetGrenadeColorSystem.cs// The Change filter ensures we only set this color if the GrenadeData component changes, // which will only happen once (when it spawns). foreach (var (urpColorRw, grenadeDataRo) in SystemAPI.QueryRefRWURPMaterialPropertyBaseColor, RefROGrenadeData() .WithChangeFilterGrenadeData()) { urpColorRw.ValueRW.Value grenadeDataRo.ValueRO.SpawnId % 2 1 ? new float4(1, 0, 0, 1) // 红 : new float4(0, 1, 0, 1); // 绿 }该系统放在PresentationSystemGroup而非预测系统中源码注释给出了理由可能不是自己生成的实体、在预测代码中设置可能赶不上同帧呈现导致黑一帧、以及预测生成失败时该实体对本地而言就是“新幽灵”——因此着色统一放在表现层按SpawnId奇偶交替红/绿。若某颗手榴弹始终呈黑色说明它来自快照路径且未与本地预测实体换入这是排查预测匹配问题最直观的视觉信号。八、机制小结与适用边界从源码结构看样本的完整闭环是输入增量 →ProcessFireCommandsSystem预测组双端生成手榴弹写入唯一SpawnId属主 NetworkId 高 16 位 输入计数器低 16 位客户端预测实体进入PredictedGhostSpawnList服务器快照携带新生成到达GrenadeClassificationSystem在默认分类之后运行用TryGetComponentDataFromSnapshotHistory取快照侧SpawnId与预测清单比对命中则写回PredictedSpawnEntity完成“换入”并对非己方手榴弹强制标记HasClassifiedPredictedSpawn防止默认系统误配引信到点后服务器权威销毁并施放爆炸冲量客户端仅补纯视觉粒子特效颜色系统提供红/绿/黑的直观校验便于发现匹配错误。需要说明的适用前提整套代码依赖当前仓库所用 Netcode for Entities 版本的预测与幽灵快照 API如GhostSpawnBuffer、PredictedGhostSpawn、SnapshotDataLookupHelper、NetworkTime.IsFirstTimeFullyPredictingTick并假设输入通过InputBufferData增量事件传输若更换网络包或改用绝对计数输入SpawnId 的构造方式需要相应调整。同时样本对“每 tick 最多 5 颗”这类限流是示例性质实际项目中应结合游戏设计做更完整的速率与资源限制。参考文档PredictedSpawning.md原文还建议查阅 Netcode 官方手册中 Prediction 与 Ghost snapshot 章节的相应内容相关示例入口见 NetcodeSamples/README.md。【免费下载链接】EntityComponentSystemSamples项目地址: https://gitcode.com/GitHub_Trending/en/EntityComponentSystemSamples创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门