的完整解析)
Unity Netcode for EntitiesHelloNetcode 中实现玩家死亡与重生Respawning 样例的完整解析【免费下载链接】EntityComponentSystemSamples项目地址: https://gitcode.com/GitHub_Trending/en/EntityComponentSystemSamples本文围绕仓库中 NetcodeSamples 项目的 HelloNetcode 系列之04_Respawning样例展开讲清如何在基于 Entities 的网络游戏中实现“玩家被击倒后旋转倒地、再在随机点重建实体”的重生Respawning流程。读完后你将理解该样例的完整前置依赖、伤害结算链路、RespawnSystem的销毁—重建逻辑以及最关键的 Netcode 组件GhostOwner、CommandTarget、ConnectionOwner、LinkedEntityGroup在重生时如何被正确“补装”从而保证玩家断线时实体能被正确清理。1. 样例定位与前置要求Respawning是 HelloNetcode 中级系列2_Intermediate的第四个样例。README 对 HelloNetcode 系列的总体定位是一组小而简单、逐项展示功能的样例后续样例会复用前面的样例因此“连接、进入游戏”等公共流程不会重复实现样例可以逐层叠加复杂度。Respawning正是这一思路的产物——它建立在 HitScanWeapon 样例之上只额外增加重生逻辑。样例说明文档 列出了运行该样例所需的前置功能模块GoInGame进入游戏流程SpawnPlayer连接建立后自动为玩家生成角色Physics动态物理对象本样例中的物理对象即 Physics 样例的产物CharacterController玩家角色控制器ThinClients瘦客户端支持HitScanWeapon即时命中hitscan武器是本样例的直接基础。运行方式用**至少一个瘦客户端thin client**启动该样例客户端数量可在 Unity 的 multiplayer playmode 工具中设置。玩家用鼠标瞄准、左键射击命中对手命中效果由 HitScanWeapon 样例描述的命中标记hit marks体现。累计 5 次成功命中即可击倒对手死亡后角色先做一次旋转倒地的表现然后在地图上的一个随机点重生。“5 次命中”这个数字与源码完全对应玩家生命为 100单次命中扣 20 点下文会给出代码证据。相关资产文件均位于 04_Respawning 目录文件作用Respawning.unity样例外挂场景RespawningCharacter.prefab玩家角色预制体带幽灵同步配置RespawnSystem.cs重生核心系统DamageSystem.cs把命中事件结算成伤害HealthAuthoring.cs生命值组件与烘焙器2. 伤害链路从命中到击倒2.1 Health 组件整数 HP 与幽灵字段生命值定义在 HealthAuthoring.cspublic struct Health : IComponentData { [GhostField(Smoothing SmoothingAction.Clamp)] public short MaximumHitPoints; [GhostField(Smoothing SmoothingAction.Clamp)] public short CurrentHitPoints; }两个设计细节值得注意HP 用short而非浮点数。源码注释解释得很直白避免浮点精度问题并且允许出现负值——这在存在治疗等正向 HP 操作、或伤害结算超调时可能实际发生。两个字段都标记为[GhostField(Smoothing SmoothingAction.Clamp)]。HP 是离散整数状态插值Interpolate会在网络平滑阶段产生“19.x 血”这类无意义的中间态因此选用Clamp直接钳制到最新值。配套的HealthAuthoring烘焙器把场景/预制体上挂的 MonoBehaviour 转成 ECS 组件初始CurrentHitPoints等于MaximumHitPoints默认 100。在 RespawningCharacter.prefab 中该值被配置为MaximumHitPoints: 100。2.2 DamageSystem每次命中扣 20 血DamageSystem 运行在HelloNetcodePredictedSystemGroup中、且[UpdateAfter(typeof(ShootingSystem))][RequireMatchingQueriesForUpdate] [UpdateInGroup(typeof(HelloNetcodePredictedSystemGroup))] [UpdateAfter(typeof(ShootingSystem))] public partial struct DamageSystem : ISystem { public void OnUpdate(ref SystemState state) { var healthFromEntity SystemAPI.GetComponentLookupHealth(); foreach (var hit in SystemAPI.QueryRefRWHit()) { if (!healthFromEntity.TryGetComponent(hit.ValueRO.Victim, out var health)) { continue; } health.CurrentHitPoints - 20; healthFromEntity[hit.ValueRO.Victim] health; hit.ValueRW.Victim Entity.Null; } } }它遍历所有携带 Hit 组件的实体该组件由 HitScanWeapon 样例的 ShootingSystem 在射线命中后写入含Victim、Tick、HitPoint三个字段对受害者扣 20 点 HP然后把Victim置回Entity.Null表示“本 tick 的命中已被结算”。100 HP ÷ 20/次 5 次命中与文档描述完全一致。ShootingSystem的上游逻辑值得一提它只在首次预测某个 server tick 时做射线检测IsFirstTimeFullyPredictingTick并通过PhysicsWorldHistorySingleton.GetCollisionWorldFromTick按 tick 回查历史物理世界来支持 lag compensation这正是 hitscan 玩法在网络环境下保持一致体验的关键而Respawning直接复用了这一整套。3. RespawnSystem 深度解析RespawnSystem 是整个样例的核心也是文档中强调的“展示如何销毁旧玩家实体并重建它”的地方。3.1 死亡判定用的 Aspect系统并没有直接查询一堆组件而是定义了一个 Aspectreadonly partial struct CharacterWithHealth : IAspect { readonly RefRWAutoCommandTarget m_AutoCommandTarget; readonly RefROHealth m_Health; readonly RefROGhostOwner m_GhostOwner; readonly RefROConnectionOwner m_ConnectionOwner; public bool IsAlive() m_Health.ValueRO.CurrentHitPoints 0; }CharacterWithHealth把重生所需的四样东西打包在一起可写的AutoCommandTarget死亡表现阶段要禁用输入、只读的Health、GhostOwner网络身份和ConnectionOwner指向连接实体的反向指针。IsAlive()即“HP 0”。3.2 系统声明与运行环境[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)] [RequireMatchingQueriesForUpdate] [UpdateInGroup(typeof(HelloNetcodePredictedSystemGroup))] [UpdateAfter(typeof(DamageSystem))] public partial struct RespawnSystem : ISystem { Random m_Random; public void OnCreate(ref SystemState state) { m_Random new Random((uint)SystemAPI.Time.ElapsedTime 1); state.RequireForUpdateSpawner(); state.RequireForUpdateHealth(); } ...几个要点WorldSystemFilter(ServerSimulation)重生是服务端权威逻辑只在 server world 里运行客户端只通过幽灵同步看到结果UpdateAfter(DamageSystem) 同属 HelloNetcodePredictedSystemGroup保证先结算伤害、后处理死亡系统组本身挂在PredictedSimulationSystemGroup之后与预测流程协同RequireForUpdateSpawner()/RequireForUpdateHealth()场景里还没有出生点Spawner或任何带Health的角色时系统不运行避免空转m_Random用启动时间做种子保证不同玩家进程/重启间随机点不同。3.3 OnUpdate 主循环public void OnUpdate(ref SystemState state) { var playerPrefab SystemAPI.GetSingletonSpawner().Player; var ecb new EntityCommandBuffer(Allocator.Temp); var linkedEntityGroupFromEntity SystemAPI.GetBufferLookupLinkedEntityGroup(); foreach (var (character, localTransform, entity) in SystemAPI.QueryCharacterWithHealth, RefRWLocalTransform().WithEntityAccess()) { if (character.IsAlive()) { continue; // 活着跳过 } if (FallingDown(ref character.AutoCommandTarget, ref localTransform.ValueRW.Rotation, SystemAPI.Time.DeltaTime)) { continue; // 正在“倒地”表演中跳过 } DestroyAndRespawnPlayer(ecb, entity, playerPrefab, character.GhostOwner, character.ConnectionOwner, linkedEntityGroupFromEntity); } ecb.Playback(state.EntityManager); }流程是典型的“检测—表演—重建”三段式存活的实体直接跳过已死亡的实体先走FallingDown倒地动画阶段动画期间跳过动画完成后调用DestroyAndRespawnPlayer所有结构变更统一写进临时EntityCommandBuffer在循环结束后一次性Playback。这里用 ECB 批量延迟应用结构变更销毁、实例化、改组件、改 buffer是 DOTS 中处理“同帧多实体结构变化”的规范做法。3.4 FallingDown用旋转实现“倒地”static bool FallingDown(ref AutoCommandTarget autoCommandTarget, ref quaternion rotation, float deltaTime) { autoCommandTarget.Enabled false; rotation math.mul(rotation, quaternion.RotateZ(deltaTime)); var rotatedLessThan90Degrees math.mul(rotation, math.up()).y 0; return rotatedLessThan90Degrees; }对应文档中“当客户端死亡时会做一次旋转When dying the clients will do a rotation and then respawn”。实现方式相当朴素死亡瞬间起每帧把实体的LocalTransform.Rotation绕本地 Z 轴乘一个deltaTime弧度即恒定角速度的倾倒同时关掉AutoCommandTarget.Enabled——玩家死了就不再接受输入命令。函数返回值表示“是否还在倒地过程中”把本地 up 向量变换到世界系后若y 0说明还没转到 90 度还没趴下返回true继续表演一旦转过去y 0返回false下一帧进入重建。从源码结构看这是一段为了演示目的硬编码的动画角速度、轴都固定且直接写LocalTransform角色本体带幽灵的旋转字段客户端能看到倾倒过程。生产环境通常会换成动画状态机或独立的死亡表现系统。4. 核心难点销毁后如何“补装”网络组件这是 Respawning.md 文档强调的重点重生后必须把 Netcode 相关组件重新设置正确尤其是CommandTarget与LinkedEntityGroup否则玩家在重生后断开连接时实体不会被正确销毁。4.1 背景SpawnPlayer 初次生成时做了什么要理解重生为什么要“补装”先对照 SpawnPlayerSystem 首次为玩家生成实体时的五步操作var player state.EntityManager.Instantiate(prefab); // ... 偏移出生位置 ... // 1. 网络身份拥有该 NetworkId 的客户端才有权限向这个幽灵提交输入 state.EntityManager.SetComponentData(player, new GhostOwner { NetworkId networkId.Value }); // 2. 连接实体侧命令目标指向玩家支持瘦客户端 state.EntityManager.SetComponentData(connectionEntity, new CommandTarget { targetEntity player }); // 3. 连接实体的 LinkedEntityGroup buffer 里加入玩家 // 断线时连接实体被销毁组内实体会被连带销毁 state.EntityManager.GetBufferLinkedEntityGroup(connectionEntity).Add(new LinkedEntityGroup { Value player }); // 4. 玩家侧反向指针方便从玩家实体反查其连接实体 state.EntityManager.AddComponentData(player, new ConnectionOwner { Entity connectionEntity }); // 5. 标记该连接已生成过玩家避免重复处理 state.EntityManager.AddComponentPlayerSpawned(connectionEntity);这四组网络关系构成了“连接实体 ↔ 玩家实体”的完整绑定GhostOwner玩家身上声明“我是谁的幽灵”赋予对应客户端输入权限CommandTarget/AutoCommandTarget连接实体身上玩家可写该连接的输入命令投递到哪个实体LinkedEntityGroup连接实体的 buffer断线清理机制——连接实体被销毁时buffer 里记录的实体一并被销毁ConnectionOwner玩家身上反向索引方便任意系统从玩家找回连接。4.2 重生时的等价重建RespawnSystem的销毁—重建对 InitializeNewPlayer 的注释概括得很准确“把网络组件补回来patch up the network components”。完整逻辑void DestroyAndRespawnPlayer(EntityCommandBuffer ecb, Entity entity, Entity playerPrefab, GhostOwner networkId, ConnectionOwner connectionOwner, BufferLookupLinkedEntityGroup linkedEntityGroupFromEntity) { ecb.DestroyEntity(entity); InitializeNewPlayer(ecb, entity, playerPrefab, networkId, connectionOwner, linkedEntityGroupFromEntity); } void InitializeNewPlayer(EntityCommandBuffer ecb, Entity destroyedPlayer, Entity newPlayer, GhostOwner networkId, ConnectionOwner connectionOwner, BufferLookupLinkedEntityGroup linkedEntityGroupFromEntity) { var spawnedPlayer ecb.Instantiate(newPlayer); ecb.SetComponent(spawnedPlayer, networkId); // 1. 沿用旧 GhostOwner var newX m_Random.NextInt(-40, 40); var newZ m_Random.NextInt(-40, 40); ecb.SetComponent(spawnedPlayer, LocalTransform.FromPosition(new float3(newX, 1, newZ))); // 2. 随机位置 ecb.SetComponent(connectionOwner.Entity, new CommandTarget { targetEntity spawnedPlayer }); // 3. 重指命令目标 ecb.AddComponent(spawnedPlayer, new ConnectionOwner { Entity connectionOwner.Entity }); // 4. 补反向指针 var linkedEntityGroups linkedEntityGroupFromEntity[connectionOwner.Entity]; for (var index 0; index linkedEntityGroups.Length; index) { var linkedEntityGroup linkedEntityGroups[index]; if (linkedEntityGroup.Value destroyedPlayer) { linkedEntityGroup.Value newPlayer; // 5. 替换绑定 } } }逐步对照首次生成的五步可以看到重生把每一步都做了等价补偿步骤首次生成SpawnPlayerSystem重生RespawnSystem网络身份写入新GhostOwnerecb.SetComponent(spawnedPlayer, networkId)——沿用旧 GhostOwner玩家仍是同一 NetworkId 的“主人”客户端输入权限不变出生位置按networkId.Value * 2偏移避免重叠NextInt(-40, 40)生成 XZ 平面上的随机整数坐标y 1注意方法注释写的是[-50;50]代码实际取的是[-40, 40]以代码为准命令目标CommandTarget { targetEntity player }用ecb.SetComponent覆盖连接实体上的CommandTarget指向新玩家。SetComponent而非AddComponent很关键——旧值必须被替换否则命令会继续投递到已被销毁的旧实体反向指针给旧玩家加ConnectionOwner给新玩家重新AddComponentConnectionOwner断线清理LinkedEntityGroupbufferAdd玩家遍历连接实体的 buffer把Value destroyedPlayer的项原地替换为newPlayer而不是 Remove Append注释中也保留了被否掉的 remove/append 写法第 5 步直接回应了文档的警告LinkedEntityGroup如果还指向旧实体已随DestroyEntity消失或 buffer 里残留了失效引用那么该玩家断线时新玩家实体不在清理范围内就会变成一个“幽灵残留”——没有连接、无法操作、还占着网络带宽。同理若CommandTarget不重指玩家重生后提交的所有输入都找不到目标角色会“死机”。值得一提的是Health组件不需要手工“补装”ecb.Instantiate(playerPrefab)实例化的是 RespawningCharacter.prefab 烘焙后的 prefab 实体其Health组件天然带着初始满值 100 HP重生命中的“满血复活”是 prefab 数据自带的。4.3 预制体侧的幽灵配置RespawningCharacter.prefab 根节点上的幽灵授权组件GhostAuthoring序列化配置为DefaultGhostMode: 2Predicted、SupportedGhostModes: 3支持 Predicted 与 Interpolated 两种模式、HasOwner: 1幽灵有所有者即GhostOwner机制生效、SupportAutoCommandTarget: 1支持自动命令目标配合CommandTarget、TrackInterpolationDelay: 1记录插值延迟供 ShootingSystem 的 lag compensation 读取。这些配置保证了角色既能走预测模拟、又能接收输入是重生后“身份不变、行为不变”的前提。角色本体为 Kinematic 刚体 半径 0.5、高 2 的胶囊碰撞体头部子节点带一个 BoxCollider 作为命中部位。5. 运行与验证建议打开 Respawning.unity在 Unity 的 multiplayer playmode 配置中启动 server 至少 1 个 thin client客户端移动角色CharacterController 输入并用鼠标左键射击另一个玩家命中时会出现命中标记HitScanWeapon 机制对同一目标累计命中 5 次每次 20 伤害共 100 HP目标角色停止响应输入并沿 Z 轴旋转倒地约 90 度倒地完成后该角色在 XZ ∈[-40, 40]、Y 1 的随机点以满血状态出现输入立即恢复验证网络正确性的关键操作让一个玩家死亡重生后再断开连接观察其新玩家实体是否被正确销毁——这正是CommandTarget/LinkedEntityGroup补装是否到位的判定标准。若该玩家断线后场景里留下无人操控的角色说明断线清理链路断了。6. 实现要点小结死亡与表演解耦HP ≤ 0 只是状态倒地是独立的、基于旋转角度的时间过程FallingDown用“本地 up 的世界 y 分量是否仍大于 0”作为动画结束条件无需计时器结构变更走 ECB销毁、实例化、组件覆写、buffer 修改全部延迟到Playback避免同帧结构变化引起查询失效重生 身份沿用 关系重建GhostOwner原样搬移保持网络身份连续性CommandTarget用SetComponent覆盖保证输入不断流LinkedEntityGroup原地替换保证断线清理不断链——三者缺一都会出现文档所警告的断线残留问题服务端权威 客户端可见RespawnSystem只跑在 ServerSimulation客户端通过幽灵同步观察倒地与重生HP 字段用SmoothingAction.Clamp保证整数血量的干净同步。7. 延伸阅读仓库内相关路径Respawning 样例说明文档RespawnSystem.cs / DamageSystem.cs / HealthAuthoring.cs前置样例SpawnPlayerSystem.cs首次生成时的网络组件绑定、SpawnerAuthoring.cs出生点 prefab 引用、ShootingSystem.cs带 lag compensation 的 hitscan 射线、HitAuthoring.cs、ApplyHitMarkSystem.cs系统组定义HelloNetcodeSystemGroup.cs【免费下载链接】EntityComponentSystemSamples项目地址: https://gitcode.com/GitHub_Trending/en/EntityComponentSystemSamples创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考