Skip to content

血条与飘字统合组件系统 ​

血条和飘字现在使用统一的轻量绘制组件来绘制,名为 Widget 。它由数据模板创建,运行时可以修改文字、图片、颜色、显隐和进度条百分比。

它不是 GameUI 控件,也不是 Canvas 即时绘制 API。可以把它理解为一套数据驱动的小型绘制模板:适合单位头顶血条、战斗数字、场景标记、简单图标和条形进度;但没有复杂布局能力,不适合复杂布局、按钮交互、滚动列表或富文本页面。

什么时候用 ​

需求推荐
按布局组合按钮、面板、列表、弹窗、输入框GameUI 控件
自己逐帧绘制图形、小地图、棋盘、弹幕、复杂 2D 游戏画面Canvas / CanvasAnimated
在单位、世界坐标或屏幕坐标上显示轻量文本、图片、条形进度Widget + StatusBar / FloatingText

Widget 目前主要有三种宿主:

宿主说明
StatusBar单位当前绑定的血条,由 GameDataUnit.StatusBarSetting 选择模板
FloatingText一次性的飘字表现,常用于伤害、治疗、经验、金币提示
StatusBarCustom由代码主动创建和销毁的无主血条,适合自定义场景标记、轻量屏幕提示或临时进度条

数据结构 ​

血条模板使用 GameDataStatusBar:

csharp
public List<IGameLink<GameDataWidget>?> Widgets { get; set; }
public Vector2? Anchor { get; set; }
public List<WidgetAnimationSequence> Animations { get; set; }
public int Priority { get; set; }

飘字模板使用 GameDataFloatingText:

csharp
public List<IGameLink<GameDataWidget>?> Widgets { get; set; }
public Vector2? Anchor { get; set; }
public Vector2? RandomRange { get; set; }
public List<WidgetAnimationSequence> Animations { get; set; }
public TimeSpan? OverlayWindow { get; set; }
public bool AllowOverlay { get; set; }

Widget 类型 ​

类型用途
GameDataWidgetPanel容器节点,用于组织子 Widget
GameDataWidgetText普通文字或图片字
GameDataWidgetImage图片
GameDataWidgetBar条形进度
GameDataWidgetRuler刻度

所有 Widget 都有这些通用字段:

csharp
public WidgetRect Rect { get; set; }
public Color? Color { get; set; }
public bool Visible { get; set; }
public WidgetPropertyBinding? Value { get; set; }
public WidgetPropertyBinding? Blink { get; set; }
public TimeSpan? BlinkInterval { get; set; }
public List<IGameLink<GameDataWidget>?> Children { get; set; }

Value 表示这个 Widget 绑定显示的单位数据;Blink 表示闪烁条件,非零值会让 Widget 闪烁。

文本 Widget ​

GameDataWidgetText 常用字段:

csharp
public string Text { get; set; }
public Font? Font { get; set; }
public string? ImageFontPrefix { get; set; }
public float? FontSize { get; set; }
public WidgetTextHorizontalAlignment HorizontalAlignment { get; set; }
public WidgetTextVerticalAlignment VerticalAlignment { get; set; }
public bool DisplayValueAsInteger { get; set; }
public bool ReadValueAsInteger { get; set; }
public string? TextPrefix { get; set; }
public string? TextSuffix { get; set; }
public int Stroke { get; set; }
public Color? StrokeColor { get; set; }

Font 和 ImageFontPrefix 是两个不同概念:

字段含义示例
Font普通文字渲染使用的字体文件路径UI/font/Regular/RegularBold.otf
ImageFontPrefix图片字/数字图集前缀回血、物理、法爆

如果要显示一串普通文字,使用 Font。如果要用项目已有的图片字表现战斗数字,使用 ImageFontPrefix。

TextPrefix 和 TextSuffix 是显示文本的前后缀。例如治疗飘字可以设置 TextPrefix = "+"。它们只影响显示,不改变绑定属性本身的数值。

DisplayValueAsInteger 和 ReadValueAsInteger 也不要混用:

字段作用
DisplayValueAsInteger只影响显示格式,把小数显示成整数
ReadValueAsInteger按整数载荷读取单位属性值

大多数“等级”“整数状态”这类本来就是整数语义的数据,应使用 ReadValueAsInteger。

属性绑定 ​

WidgetPropertyBinding 只绑定一个数据来源:

csharp
public IGameLink<GameDataUnitProperty>? Property { get; set; }
public PropertyVital? Vital { get; set; }
public PropertyUnit? UnitField { get; set; }

如果多个字段都被填写,序列化时按下面顺序取第一个非空值:

  1. Property
  2. Vital
  3. UnitField

建议一个绑定只填一个字段,避免以后读配置时产生歧义。

常见绑定:

csharp
new WidgetPropertyBinding { Vital = PropertyVital.Health }
new WidgetPropertyBinding { Vital = PropertyVital.MaxHealth }
new WidgetPropertyBinding { UnitField = PropertyUnit.Level }
new WidgetPropertyBinding { UnitField = PropertyUnit.StatusBarDisplayName }
new WidgetPropertyBinding { UnitField = PropertyUnit.StatusBarDisplayImage }
new WidgetPropertyBinding { Property = GameEntry.ScopeData.GameDataUnitProperty.SomeProperty }

UnitField 使用的是 PropertyUnit,可以选择框架定义的字段,也可以选择项目通过 EnumExtension 扩展出来的字段。数值字段按数值写入即可;如果绑定字段承载的是文字或图片路径,写入单位属性时必须使用 UTF8String,不要直接用 string,否则 native 血条按 UTF8 读取时会拿到错误字节。

StatusBarDisplayName 和 StatusBarDisplayImage 是专门给血条展示用的单位字段,适合把“血条上显示的名字/图片”和单位真实名称、模型表现解耦。服务器侧长期修改这两个字段时,优先使用 Unit 上的便利属性:

csharp
#if SERVER
unit.StatusBarDisplayName = "Boss";
unit.StatusBarDisplayImage = "game_hud/icon/boss_title.png";
#endif

如果项目自己扩展 PropertyUnit 并把它绑定到文字或图片 Widget,请显式写入 UTF8String:

csharp
#if SERVER
unit.SetProperty(CustomPropertyUnit.StatusTitle, new UTF8String("蓄力中"));
unit.SetProperty(CustomPropertyUnit.StatusIcon, new UTF8String("game_hud/icon/casting.png"));
#endif

访问单位血条 ​

单位当前血条可以通过 unit.StatusBar 访问:

下面示例里的 GameEntry.ScopeData.GameDataWidget.* 链接名需要替换成你项目中实际生成的 Widget 数据链接。

csharp
#if CLIENT
using GameCore.Display;
using GameCore.EntitySystem;

void UpdateBossName(Unit boss, string name)
{
    var statusBar = boss.StatusBar;
    if (!statusBar.IsValid)
    {
        return;
    }

    var nameWidget = statusBar.GetWidgetText(GameEntry.ScopeData.GameDataWidget.BossNameText);
    nameWidget?.SetText(name);
}
#endif

GetWidgetText、GetWidgetImage、GetWidgetBar 都接收 Widget 数据链接。推荐用链接查找,因为同一个模板里可能出现多个同名 Widget,名字不是稳定 API。

如果确实是在做调试工具或通用遍历,可以使用:

csharp
int count = statusBar.WidgetCount;
Widget? widget = statusBar.GetWidgetByIndex(0);

业务逻辑不要长期依赖 index。模板结构改动、子节点顺序变化或数编调整都可能改变 index 对应的 Widget。

单位血条可能在模板切换、关系变化或 native 实例重建时销毁并重新创建。需要长期维护血条文字、图片或进度条时,可以订阅生命周期事件,并在初始化回调里重新获取 Widget handle:

csharp
#if CLIENT
using GameCore.EntitySystem;

void SetupBossStatusBar(Unit boss)
{
    var statusBar = boss.StatusBar;

    statusBar.OnInitialized += () =>
    {
        statusBar.GetWidgetText(GameEntry.ScopeData.GameDataWidget.BossNameText)
            ?.SetText("Boss");

        statusBar.GetWidgetBar(GameEntry.ScopeData.GameDataWidget.BossHpBar)
            ?.SetPercentage(1.0f);
    };

    statusBar.OnDestroyed += () =>
    {
        // 之前缓存的 Widget handle 已经不可继续使用。
    };
}
#endif

如果订阅 OnInitialized 时血条已经存在,会立刻调用一次回调;事件没有订阅时不会启用 native 生命周期转发。

修改 Widget ​

运行时 handle 支持有限 set API:

csharp
Widget widget;
widget.SetVisible(true);
widget.SetColor(Color.White);

WidgetText text;
text.SetText("Lv. 10");

WidgetImage image;
image.SetImage("game_hud/icon/boss.png");

WidgetBar bar;
bar.SetPercentage(0.65f);
bar.SetSkinIndex(0);

SetPercentage 会把值限制到 0..1 范围。SetSkinIndex 使用从 0 开始的 skin 下标。

Widget handle 指向当前 native 实例。单位离开屏幕、模板切换、血条重建、飘字结束或自定义血条销毁后,旧 handle 可能失效。长生命周期逻辑里应重新获取 Widget,并检查 IsValid。

csharp
var hpBar = unit.StatusBar.GetWidgetBar(GameEntry.ScopeData.GameDataWidget.BossHpBar);
if (hpBar is not null && hpBar.IsValid)
{
    hpBar.SetPercentage(currentHp / maxHp);
}

创建飘字 ​

GameDataFloatingText 可以在世界坐标、屏幕坐标或单位身上创建飘字:

csharp
#if CLIENT
using System.Numerics;

var textData = GameEntry.ScopeData.GameDataFloatingText.Heal;

var worldText = textData.Data.CreateFloatingText(new Vector3(0, 2, 0), "+120");
var screenText = textData.Data.CreateFloatingText(new Vector2(640, 360), "Ready");
var unitText = textData.Data.CreateFloatingText(unit, "-35");

unitText?.GetWidgetText(GameEntry.ScopeData.GameDataWidget.FloatingTextContent)
    ?.SetColor(Color.Yellow);
#endif

飘字通常会按模板动画和生命周期自动结束。需要提前销毁时,可以调用:

csharp
unitText?.Destroy();

创建无主血条 ​

StatusBarCustom 是从血条模板主动创建的独立实例,可以放在世界坐标或屏幕坐标:

csharp
#if CLIENT
using System.Numerics;

var marker = GameEntry.ScopeData.GameDataStatusBar.MainBaseHealthBar.Data
    .CreateStatusBarCustom(new Vector3(10, 0, 20));

marker?.GetWidgetText(GameEntry.ScopeData.GameDataWidget.MarkerTitle)
    ?.SetText("采集中");

marker?.GetWidgetBar(GameEntry.ScopeData.GameDataWidget.MarkerProgress)
    ?.SetPercentage(0.35f);

marker?.SetWorldPosition(new Vector3(12, 0, 20));
marker?.Destroy();
#endif

屏幕坐标版本适合做少量轻量提示:

csharp
var tip = GameEntry.ScopeData.GameDataStatusBar.MainTitle.Data
    .CreateStatusBarCustom(new Vector2(960, 180));

tip?.SetScreenPosition(new Vector2(960, 220));

创建出来的 StatusBarCustom 不会自动跟随某个单位,也不会由单位生命周期管理。不再使用时应调用 Destroy()。

生命周期建议 ​

  • 单位血条由单位和可见性系统管理;不要自己销毁单位血条。
  • 单位血条可订阅 OnInitialized / OnDestroyed;初始化回调中重新获取需要操作的 Widget handle。
  • FloatingText 是临时表现,通常让模板动画自然结束;如果提前结束,调用 Destroy()。
  • StatusBarCustom 由创建者负责销毁,适合放进自己的字段或集合里管理。
  • 不要把 Widget handle 当成永久对象缓存。缓存 Widget 数据链接,使用前重新获取 handle 更稳。
  • 不要用 Widget 承担复杂交互。如果需要点击、焦点、布局、输入框或滚动容器,使用 GameUI 控件。

和资源路径的关系 ​

Widget 数据里的资源字段应通过数据编辑器选择或填写与现有数据一致的路径。

常见形态:

text
Texture: game_hud/bloodStrip/血条_蓝.png
Font: UI/font/Regular/RegularBold.otf
ImageFontPrefix: 回血

Texture 通常使用项目 ui/ 下的相对资源路径。Font 当前使用字体文件路径。ImageFontPrefix 不是文件路径,而是图片字资源前缀。

常见问题 ​

为什么拿不到 Widget?

先确认传入的是 Widget 数据链接,而不是血条或飘字模板链接。然后确认当前血条模板确实包含这个 Widget。可以用 HasWidget(link) 做检查。

两个 Widget 同名时会取到谁?

运行时公开查找按 Widget 数据链接,不按名字。两个 Widget 名字一样也不会互相影响;但如果用 index 遍历,就只代表当前实例顺序。

为什么 set 后过一会儿又变回去了?

Widget 可能被宿主重建,或者模板绑定值在下一次刷新时覆盖了运行时设置。需要长期生效的名字或图片,优先写入 unit.StatusBarDisplayName / unit.StatusBarDisplayImage;自定义 UnitField 文本或图片属性要写 UTF8String。临时效果再用 handle set。

能不能用 Widget 做大量 HUD?

可以用于轻量文本、图片和条形进度,比完整 GameUI 控件更轻。但它不是通用 UI 框架,没有复杂布局和输入系统。大量动态对象仍要注意创建销毁频率,长期存在的自定义实例应自己复用或集中管理。

相关文档 ​