news 2026/9/16 16:20:37

Unity Netcode for Entities 实战:在 HelloNetcode 示例中实现世界空间血条(World-Space Health Bars)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Unity Netcode for Entities 实战:在 HelloNetcode 示例中实现世界空间血条(World-Space Health Bars)

Unity Netcode for Entities 实战:在 HelloNetcode 示例中实现世界空间血条(World-Space Health Bars)

【免费下载链接】EntityComponentSystemSamples项目地址: https://gitcode.com/GitHub_Trending/en/EntityComponentSystemSamples

本文基于 Unity Entities(DOTS)仓库中 HelloNetcode 系列高级示例01_HealthBars,完整讲解如何在多人游戏中为每个玩家生成并维护一个跟随角色、始终朝向摄像机的世界空间血条。读完后你将掌握:如何把托管侧 GameObject 预制体引入 ECS 世界并在客户端动态实例化、如何通过IDisposable/ICloneable托管组件解决 Ghost 实体的 UI 生命周期问题,以及如何正确区分本地玩家与远端对手的血条放置和死亡(服务器权威)状态处理。

示例定位与前置要求

01_HealthBars属于 HelloNetcode 示例体系 的3_Advanced(高级)层级,它构建在两个中级示例之上:

  • HitScanWeapon(命中扫描武器):负责"射击 → 命中 → 扣血",源码见 ShootingSystem.cs 与 HitScanWeapon 示例文档;
  • Respawning(重生):负责为角色添加Health组件、死亡判定与重生逻辑,见 Respawning 示例文档。

HealthBars.md 中列出的前置示例链为:

  • GoInGame
  • SpawnPlayer
  • Physics
  • CharacterController
  • HitScanWeapon
  • Respawning

运行方式:进入 Play Mode 时,在 Multiplayer Play Mode 工具窗口中至少启用一个Thin Client,即可看到所有玩家头顶出现血条(黑色背景 + 绿色血条)。射击命中后血条递减,耗尽即触发 Respawning 示例描述的重生流程,角色在新位置以满血状态重生。

核心数据:Health 组件从哪来

血条显示的数值并非本示例自定义,而是复用 Respawning 示例中的Health组件(HealthAuthoring.cs):

public struct Health : IComponentData { [GhostField(Smoothing = SmoothingAction.Clamp)] public short MaximumHitPoints; [GhostField(Smoothing = SmoothingAction.Clamp)] public short CurrentHitPoints; }

几个值得注意的设计点:

  • 使用short而非浮点数,官方注释(HealthAuthoring.cs)解释:避免浮点精度问题,且允许取负值以兼容治疗等"加血"操作;
  • 两个字段都是 Ghost 字段并指定Smoothing = SmoothingAction.Clamp,即插值平滑时不允许数值在插值过程中越过实际端点,保证血条数值变化平滑但不"超前";
  • Authoring 默认MaximumHitPoints = 100,Baker 同时用它初始化CurrentHitPoints

扣血逻辑在 DamageSystem.cs:每次命中使受害者的CurrentHitPoints -= 20,因此满血 100 点、5 次命中致死,与示例文档"Five successful hits will be enough to knock them down"完全对应。

第一步:Authoring 烘焙生成器配置

HealthBarSpawnerAuthoring.cs 定义了一个 ECS 组件与对应的 MonoBehaviour 作者化组件:

public class HealthBarSpawner : IComponentData { public GameObject HealthBarPrefab; // 血条 UI 预制体引用 public float OpponentHeightOffset; // 对手头顶偏移(向上) public float PlayerTowardCameraOffset; // 本地玩家向摄像机方向的推拉距离 public float PlayerHeightOffset; // 本地玩家高度偏移 } public class HealthBarSpawnerAuthoring : MonoBehaviour { public GameObject HealthBarPrefab; public float OpponentHeightOffset = 0.5f; public float PlayerTowardCameraOffset = 1.8f; public float PlayerHeightOffset = -1.5f; class Baker : Baker<HealthBarSpawnerAuthoring> { public override void Bake(HealthBarSpawnerAuthoring authoring) { var entity = GetEntity(TransformUsageFlags.Dynamic); AddComponentObject(entity, new HealthBarSpawner { HealthBarPrefab = authoring.HealthBarPrefab, OpponentHeightOffset = authoring.OpponentHeightOffset, PlayerTowardCameraOffset = authoring.PlayerTowardCameraOffset, PlayerHeightOffset = authoring.PlayerHeightOffset, }); } } }

要点解析:

  • 三个偏移参数是血条放置策略的全部配置,语义上区分了两类实体:对手(Opponent,血条悬于头顶+0.5单位)与本地玩家(第一/三人称下头顶血条会被模型或视角挡住,故用PlayerHeightOffset = -1.5PlayerTowardCameraOffset = 1.8把血条压到角色下方并沿"血条→摄像机"方向拉近/推远,保证可见且不被遮挡);
  • 之所以用AddComponentObject(而非AddComponent)烘焙,是因为HealthBarSpawner是一个(class)组件,其内部持有对GameObject预制体的引用。这正是 HealthBars.md "Note" 一节强调的设计说明:HealthBarSpawnerAuthoring刻意把 IComponent 实现为 class 而非 struct,就是为了保持对血条预制体 GameObject 的引用。整个组件及两套#if !UNITY_DISABLE_MANAGED_COMPONENTS条件编译,意味着该功能仅在允许托管组件的构建下可用;
  • 该组件所在的实体在场景中是HealthBar实体场景内的一个 Authoring 对象,最终由 SpawnHealthBarSystem 以GetSingleton方式读取。

第二步:SpawnHealthBarSystem —— 客户端实例化 UI 并挂托管组件

SpawnHealthBarSystem.cs 是整个示例最关键的文件,它同时定义了 UI 承载组件HealthUI与生成系统。

HealthUI:可销毁、可克隆的托管组件

public class HealthUI : IComponentData, IDisposable, ICloneable { public Transform HealthBar; public Image HealthSlider; public float OpponentHeightOffset; public float PlayerHeightOffset; public float PlayerTowardCameraOffset; public void Dispose() { // 由于实现了 IDisposable,可以在 Ghost 实体被销毁时 // 触发对 HealthBar GameObject 的销毁 if (HealthBar != null) Object.Destroy(HealthBar.gameObject); } public object Clone() { if (HealthBar == null || HealthBar.gameObject == null) return new HealthUI(); var newHealthBar = Object.Instantiate(HealthBar.gameObject); var images = HealthBar.gameObject.GetComponentsInChildren<Image>(); return new HealthUI { HealthBar = newHealthBar.GetComponent<Transform>(), HealthSlider = images[1] }; } }

这套设计解决了托管 UI 与 DOTS 实体生命周期错位的经典问题:

  • IDisposable:当 Netcode 在服务器权威下销毁 Ghost 实体(例如玩家断线、重生重建实体)时,ECS 会自动调用Dispose(),从而Object.Destroy掉对应的血条 GameObject,避免场景里残留"幽灵 UI";
  • ICloneable:Ghost 实体在客户端本地预测重建(如本地玩家重新生成)时会克隆组件,Clone()通过Object.Instantiate为新实体创建一份全新的血条 GameObject 并返回新组件,保证每个实体各有一份独立 UI。

生成系统的查询与实例化

系统声明为(SpawnHealthBarSystem.cs):

[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)] [UpdateInGroup(typeof(PresentationSystemGroup))] public partial struct SpawnHealthBarSystem : ISystem

OnUpdate的核心逻辑(SpawnHealthBarSystem.cs):

var ecb = new EntityCommandBuffer(Allocator.Temp); var query = state.EntityManager.CreateEntityQuery(ComponentType.ReadOnly<HealthBarSpawner>()); var spawner = query.GetSingleton<HealthBarSpawner>(); foreach (var (_, entity) in SystemAPI.Query<RefRO<Health>>() .WithEntityAccess().WithNone<HealthUI>()) { var go = Object.Instantiate(spawner.HealthBarPrefab); var image = go.GetComponentsInChildren<Image>(); ecb.AddComponent(entity, new HealthUI { HealthBar = go.transform, HealthSlider = image[1], OpponentHeightOffset = spawner.OpponentHeightOffset, PlayerTowardCameraOffset = spawner.PlayerTowardCameraOffset, PlayerHeightOffset = spawner.PlayerHeightOffset, }); } ecb.Playback(state.EntityManager);

实现细节与文档呼应之处:

  • 只用常规Object.Instantiate实例化——这是 HealthBars.md Note 一节明确指出的做法:血条 UI 不是 DOTS 实体,不走烘焙/子场景流程,而是在客户端运行时直接实例化托管侧 UI 预制体(HealthBar.prefab);
  • 主查询Query<RefRO<Health>>().WithNone<HealthUI>()是幂等设计:只处理"拥有Health但尚未挂HealthUI"的实体,因此无论是新连接的远端玩家、还是重生后重建的本地玩家,都恰好会被实例化一次血条;
  • OnCreate中通过RequireForUpdate<HealthBarSpawner>()RequireForUpdate<Health>()做惰性激活——场景里没有血条生成器实体或没有玩家时,系统自动停用,零开销;
  • 组件添加通过EntityCommandBuffer缓冲后统一Playback,符合 ECS 中批量变更的标准写法。

HealthBarPrefab内部的层级约定是:GetComponentsInChildren<Image>()取第[0]个为黑色背景底板、第[1]个为作为fillAmount滑动条的血条填充层(白色像素贴图着色)。配套的 CharacterWithHealthbar.prefab 展示了带血条的角色装配形态,纹理资源为同目录下的 32x32WhitePixel.jpg。

第三步:UpdateHealthBarSystem —— 跟随、朝向与死亡状态

UpdateHealthBarSystem.cs 负责每帧把血条"钉"在角色上并刷新血量,同样运行于ClientSimulationPresentationSystemGroup,并标注[RequireMatchingQueriesForUpdate](查询无匹配实体时不执行)。

防御性启用检查

if (Camera.main == null) { state.Enabled = false; return; }

场景中不存在 Main 摄像机(例如纯服务器/无 UI 世界)时直接停用系统,避免空引用。

本地玩家与对手的差异化放置

主查询(UpdateHealthBarSystem.cs)同时取HealthUIRefRO<Health>RefRO<AutoCommandTarget>RefRO<GhostOwner>RefRO<LocalToWorld>五类数据,并按实体区分两类放置策略:

if (state.EntityManager.IsComponentEnabled<GhostOwnerIsLocal>(entity)) { // 本地玩家:抬高后沿"血条→摄像机"方向推拉,并整体 LookRotation 朝向摄像机 var targetHealthBarPos = ltw.ValueRO.Position; targetHealthBarPos.y += ui.PlayerHeightOffset; var n = mainCamera.transform.position - ui.HealthBar.position; targetHealthBarPos += (float3)(n.normalized * ui.PlayerTowardCameraOffset); ui.HealthBar.SetPositionAndRotation(targetHealthBarPos, Quaternion.LookRotation(n)); } else { // 远端对手:直接置于头顶 OpponentHeightOffset 处,并朝向摄像机 var targetHealthBarPos = ltw.ValueRO.Position; targetHealthBarPos.y += ui.OpponentHeightOffset; var n = mainCamera.transform.position - ui.HealthBar.position; ui.HealthBar.SetPositionAndRotation(targetHealthBarPos, Quaternion.LookRotation(n)); }
  • 通过GhostOwnerIsLocal组件判断该 Ghost 是否属于本地玩家,从而套用两套 Authoring 中配置好的偏移参数;
  • 两者都用Quaternion.LookRotation让血条平面始终正对主摄像机,实现"永远可读"的世界空间 UI;
  • 位置来源是实体的LocalToWorld.Position,因此血条天然跟随角色的插值/预测位置,无需额外同步。

血量渲染与"等待服务器确认"的死亡处理

var hpNormalized = math.saturate((float)health.ValueRO.CurrentHitPoints / health.ValueRO.MaximumHitPoints); var playerColor = NetworkIdDebugColorUtility.GetColor(owner.ValueRO.NetworkId); // Killed by server: if (act.ValueRO.Enabled) { // 存活:血条填充为玩家调试色 ui.HealthSlider.color = playerColor; } else { // 无视预测,直接置 0,并把背景调成半透明(alpha=0.3)表示"权威死亡" hpNormalized = 0; playerColor.a = 0.3f; ui.HealthSlider.transform.parent.GetComponent<Image>().color = playerColor; } ui.HealthSlider.fillAmount = hpNormalized;

这里的AutoCommandTarget是 Netcode 的权威状态信号:

  • act.ValueRO.Enabled == true表示服务器已确认该实体存活,血条按CurrentHitPoints / MaximumHitPoints归一化后驱动Image.fillAmount,填充颜色用NetworkIdDebugColorUtility.GetColor(owner.NetworkId)按玩家 NetworkId 着色,便于在多人场景中区分个体;
  • act.ValueRO.Enabled == false表示服务器已判定死亡。由于本地预测可能尚未追上(客户端可能仍认为目标存活),此处选择"无论预测如何一律置 0",并将底板透明度降到 0.3 呈现灰色"权威死亡"外观。源码注释解释了只在此条件成立时处理的意图:"We're waiting for server confirmation"——即死亡表现以服务器权威为准,不跟随预测回滚;
  • 死亡后由 RespawnSystem 重建玩家实体,旧实体销毁触发HealthUI.Dispose()销毁旧血条,新实体因再次满足WithNone<HealthUI>()查询而获得新血条,血量随之满血恢复——形成完整闭环。

关键设计小结

设计点做法依据
UI 预制体引用用 class 型IComponentDataHealthBarSpawner)承载GameObject引用,AddComponentObject烘焙HealthBars.md Note 一节、HealthBarSpawnerAuthoring.cs
UI 实例化客户端系统内常规Object.Instantiate,非 DOTS 实体SpawnHealthBarSystem.cs
生成幂等性Query<RefRO<Health>>().WithNone<HealthUI>()只处理未挂 UI 的实体SpawnHealthBarSystem.cs
UI 生命周期HealthUI实现IDisposable(随实体销毁)与ICloneable(预测重建时克隆)SpawnHealthBarSystem.cs
数值权威死亡表现等待AutoCommandTarget服务器确认,期间血条强制置 0UpdateHealthBarSystem.cs
运行前提托管组件可用(!UNITY_DISABLE_MANAGED_COMPONENTS);至少一个 Thin Client 进 Play Mode 可观察HealthBars.md、三处源码文件顶部的条件编译

如何运行与验证

  1. 打开仓库中的 NetcodeSamples 工程,确认当前场景包含3_Advanced/01_HealthBars的 HealthBar 场景/实体场景;
  2. 依赖链确认:GoInGame、SpawnPlayer、Physics、CharacterController、HitScanWeapon、Respawning 各示例的 Authoring 组件均在场景装配(对应文档 Requirements 一节);
  3. 进入 Play Mode 并在 Multiplayer Play Mode 工具中启用至少一个 Thin Client;
  4. 观察点:所有玩家头顶出现黑底绿条血条 → 射击命中后血条递减(每次 -20)→ 血量归零时服务器确认后血条灰化 → 角色随机位置重生且血条满血恢复。

如需继续深入,可参考同一体系中的 HitScanWeapon 文档(命中判定与Hit组件流转)和 Respawning 文档(实体销毁重建时CommandTargetComponentLinkedEntityGroup的重新装配要求),三者共同构成了本血条示例的完整数据链路。

【免费下载链接】EntityComponentSystemSamples项目地址: https://gitcode.com/GitHub_Trending/en/EntityComponentSystemSamples

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 16:19:57

开源商业化转型:机遇、挑战与实战策略

1. 开源商业化的时代机遇开源软件正在经历从"社区共建"到"商业共赢"的关键转型期。根据Linux基金会最新报告&#xff0c;2024年企业级开源软件采用率已达82%&#xff0c;但其中实现商业转化的项目不足35%。这种供需落差恰恰构成了开源商业化的黄金窗口——…

作者头像 李华
网站建设 2026/9/16 16:19:06

肇庆30米DEM与shp边界数据:从裁剪到地形因子提取全流程

简介&#xff1a;《广东省肇庆市DEM数字高程30m》是一份面向地理信息学习与研究者的实用数据集&#xff0c;包含肇庆市行政边界范围文件&#xff0c;适合用于地形分析、地表水资源模拟、城市规划辅助、环境研究与灾害风险评估等场景。压缩包内共12个文件&#xff0c;核心是30米…

作者头像 李华
网站建设 2026/9/16 16:19:05

Trippy 权限指南:raw socket 特权要求与 macOS 无特权模式全解析

Trippy 权限指南&#xff1a;raw socket 特权要求与 macOS 无特权模式全解析 【免费下载链接】trippy A network diagnostic tool 项目地址: https://gitcode.com/GitHub_Trending/tr/trippy Trippy 是一款基于 raw socket 的网络诊断工具&#xff0c;其核心探测机制决…

作者头像 李华
网站建设 2026/9/16 16:16:02

C++智能充电桩调度系统:从优先级队列到并发架构实战

简介&#xff1a;C智能充电桩调度系统源码包&#xff0c;面向需要掌握系统级C开发的初中级程序员&#xff0c;围绕充电桩监控、资源调度、并发请求等典型业务场景&#xff0c;展示面向对象设计与工程化组织方式。包内共12个文件&#xff0c;含5个cpp实现文件、4个h头文件及3个m…

作者头像 李华