mcpField
H
hud-system

hud-system

github.com/jame581设计与 UI889181 次安装4 天前更新
代码仓库

关于 hud-system

面向 Godot 4.3+ 游戏开发者的 HUD 技能指南,提供 GDScript 和 C# 示例。介绍基于 CanvasLayer 的布局,以及生命值、分数、通知、小地图和交互提示等实现模式。

主要功能

  1. 01基于 CanvasLayer 的 HUD 架构
  2. 02绑定组件的生命值条
  3. 03使用 Tween 动画更新分数
  4. 04池化的浮动伤害数字
  5. 05队列式通知堆栈
  6. 06小地图与交互提示

适用场景

  1. 01摄像机移动时保持 HUD 固定
  2. 02显示生命值和分数变化
  3. 03呈现排队的 Toast 通知
  4. 04添加小地图和情境交互提示

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/jame581/godotprompter --skill hud-system

安装后在 Claude Code 中输入/hud-system确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
damage-numbers.md
interaction-prompts.md
minimap.md
notifications.md

详细介绍

Godot 4.3+ 中的 HUD 系统

所有示例均面向 Godot 4.3+,不使用已弃用的 API。先展示 GDScript,再展示 C#。

相关技能: godot-ui 用于 Control 节点布局和主题,component-system 用于 HealthComponent 集成,event-bus 用于分数/通知信号,inventory-system 用于背包 UI 模式,2d-essentials 用于 CanvasLayer 设置和绘制顺序,ability-system 用于冷却条和资源条绑定模式。


1. HUD 架构

为什么使用 CanvasLayer

CanvasLayer 使用自身的变换渲染子节点,不受 Camera2D 影响(除非启用了 follow_viewport_enabled);Camera3D 不会移动 2D 节点。如果不使用它,场景根节点默认画布(第 0 层)上的 HUD 节点会在平移或缩放 Camera2D 时随之移动,因为摄像机会设置该画布的变换。将所有 HUD 节点放在 CanvasLayer 中即可让 HUD 保持原位;第 ≥ 1 层(默认值为 1)会绘制在世界上方。

场景树
World (Node2D / Node3D)
├── TileMapLayer          ← game world
├── Player (CharacterBody2D)
│   ├── Camera2D
│   ├── HealthComponent
│   └── HurtboxComponent
├── Enemies
└── HUD (CanvasLayer — layer: 1)
    ├── MarginContainer (anchor: Full Rect — provides edge padding)
    │   ├── TopBar (HBoxContainer)
    │   │   ├── HealthBarPanel (PanelContainer)
    │   │   │   └── HealthBar (TextureProgressBar or ProgressBar)
    │   │   └── ScoreLabel (Label)
    │   └── BottomBar (HBoxContainer)
    │       └── InteractionPrompt (Label — hidden by default)
    ├── DamageNumbersLayer (Node2D — world-space spawning point)
    ├── MinimapContainer (SubViewportContainer)
    │   └── MinimapViewport (SubViewport)
    │       ├── MinimapCamera (Camera2D)
    │       └── MinimapWorld (mirrors or references world nodes)
    └── NotificationStack (VBoxContainer — anchored top-right)

关键规则:

  • 将所有 HUD 场景放在同一个 CanvasLayer 下。不要将 HUD 节点混入游戏世界的场景树。
  • 主 HUD 使用 layer = 1。需要显示在 HUD 上方的覆盖层或暂停菜单则使用更高的值(例如 10)。
  • 伤害数字是例外:它们可以放在 CanvasLayer 的 Node2D 子节点中,并使用 get_viewport().get_canvas_transform() 将世界坐标转换为屏幕坐标。

2. 生命值条

ProgressBar 与 TextureProgressBar
节点适用场景
ProgressBar原型开发、纯色条
TextureProgressBar使用精灵表的像素风或风格化进度条

两者都提供 min_value、max_value 和 value。将 step = 0,让补间产生平滑动画而不是逐档跳变(TextureProgressBar 的默认 step 为 1,ProgressBar 为 0.01)。

GDScript
## health_bar.gd — attach to a ProgressBar or TextureProgressBar
class_name HealthBar
extends ProgressBar

## Reference to the HealthComponent this bar tracks.
## Assign in the Inspector or connect programmatically from the HUD root.
@export var health_component: HealthComponent

## Duration (seconds) for the smooth tween on health change.
@export var tween_duration: float = 0.25

var _tween: Tween


func _ready() -> void:
    step = 0.0  # allow fractional values for smooth animation
    if health_component:
        _connect_component(health_component)


## Call this if the HealthComponent is not available at _ready time
## (e.g. the player spawns after the HUD).
func bind(component: HealthComponent) -> void:
    if health_component:
        health_component.health_changed.disconnect(_on_health_changed)
    health_component = component
    _connect_component(component)


func _connect_component(component: HealthComponent) -> void:
    max_value = component.max_health
    value     = component.current_health
    component.health_changed.connect(_on_health_changed)


func _on_health_changed(current: int, maximum: int) -> void:
    max_value = maximum
    _animate_to(current)


func _animate_to(target_value: float) -> void:
    if _tween:
        _tween.kill()
    _tween = create_tween()
    _tween.set_ease(Tween.EASE_OUT)
    _tween.set_trans(Tween.TRANS_QUAD)
    _tween.tween_property(self, "value", target_value, tween_duration)
C#
// HealthBar.cs — attach to a ProgressBar or TextureProgressBar
using Godot;

public partial class HealthBar : ProgressBar
{
    [Export] public HealthComponent HealthComponent { get; set; }
    [Export] public float TweenDuration { get; set; } = 0.25f;

    private Tween _tween;

    public override void _Ready()
    {
        Step = 0.0;
        if (HealthComponent != null)
            ConnectComponent(HealthComponent);
    }

    /// <summary>Call this when the HealthComponent is not available at _Ready time.</summary>
    public void Bind(HealthComponent component)
    {
        if (HealthComponent != null)
            HealthComponent.HealthChanged -= OnHealthChanged;
        HealthComponent = component;
        ConnectComponent(component);
    }

    private void ConnectComponent(HealthComponent component)
    {
        MaxValue = component.MaxHealth;
        Value    = component.CurrentHealth;
        component.HealthChanged += OnHealthChanged;
    }

    private void OnHealthChanged(int current, int maximum)
    {
        MaxValue = maximum;
        AnimateTo(current);
    }

    private void AnimateTo(float targetValue)
    {
        _tween?.Kill();
        _tween = CreateTween();
        _tween.SetEase(Tween.EaseType.Out);
        _tween.SetTrans(Tween.TransitionType.Quad);
        _tween.TweenProperty(this, "value", targetValue, TweenDuration);
    }
}

提示: 如果使用 TextureProgressBar,请将 fill_mode 设为 FILL_LEFT_TO_RIGHT,并将进度条纹理赋给 texture_progress。value / max_value 的比值决定纹理显示的范围。


3. 分数/标签显示

GDScript
## score_display.gd — attach to a Label
class_name ScoreDisplay
extends Label

## Duration (seconds) to count from old to new score value.
@export var count_duration: float = 0.4

var _displayed_score: int = 0
var _tween: Tween


func _ready() -> void:
    EventBus.score_changed.connect(_on_score_changed)
    text = "0"


func _on_score_changed(new_score: int) -> void:
    _animate_counter(_displayed_score, new_score)


func _animate_counter(from: int, to: int) -> void:
    if _tween:
        _tween.kill()

    _tween = create_tween()
    _tween.set_ease(Tween.EASE_OUT)
    _tween.set_trans(Tween.TRANS_QUAD)
    # Tween an intermediate float; update the label text each step.
    _tween.tween_method(_set_counter_value, float(from), float(to), count_duration)


func _set_counter_value(value: float) -> void:
    _displayed_score = int(value)
    text = str(_displayed_score)
C#
// ScoreDisplay.cs — attach to a Label
using Godot;

public partial class ScoreDisplay : Label
{
    [Export] public float CountDuration { get; set; } = 0.4f;

    private int _displayedScore = 0;
    private Tween _tween;

    public override void _Ready()
    {
        GetNode<EventBus>("/root/EventBus").ScoreChanged += OnScoreChanged;
        Text = "0";
    }

    private void OnScoreChanged(int newScore)
    {
        AnimateCounter(_displayedScore, newScore);
    }

    private void AnimateCounter(int from, int to)
    {
        _tween?.Kill();
        _tween = CreateTween();
        _tween.SetEase(Tween.EaseType.Out);
        _tween.SetTrans(Tween.TransitionType.Quad);
        _tween.TweenMethod(
            Callable.From<double>(SetCounterValue),
            (double)from,
            (double)to,
            CountDuration
        );
    }

    private void SetCounterValue(double value)
    {
        _displayedScore = (int)value;
        Text = _displayedScore.ToString();
    }
}

所需的 EventBus 信号:

# autoloads/event_bus.gd
signal score_changed(new_score: int)
// EventBus.cs (partial — score signal)
[Signal] public delegate void ScoreChangedEventHandler(int newScore);

在发放积分的位置发出信号:

# Inside a collectible or enemy death handler
EventBus.score_changed.emit(GameState.score)
// Inside a collectible or enemy death handler
GetNode<EventBus>("/root/EventBus").EmitSignal(EventBus.SignalName.ScoreChanged, GameState.Score);

4. 伤害数字

显示在受击点上方、向上浮动并逐渐淡出的“−25”标签。由 HUD 侧的生成器进行对象池管理;通过 get_viewport().get_canvas_transform() 将世界坐标转换为屏幕坐标。生成前可选用暴击颜色。

完整的 GDScript 和 C# DamageNumber 场景及对象池生成器,请参阅 references/damage-numbers.md。


5. 通知系统

Toast/通知堆栈——一个锚定在右上方的 VBoxContainer,通过 max_visible 限制可见数量,并由队列驱动关闭。新 Toast 会等待旧 Toast 到期后再显示。

带自动关闭计时器的完整 GDScript 和 C# 堆栈,请参阅 references/notifications.md。


6. 小地图概念

通过专用的 SubViewport 和跟随玩家的 Camera2D 渲染俯视图。在 HUD 内的 TextureRect 中显示 SubViewport 纹理。可通过 ColorRect 着色器添加圆形遮罩。将 render_target_update_mode = UPDATE_ALWAYS。

SubViewport 设置、MinimapCamera 的 GDScript 和 C# 代码,以及圆形遮罩着色器,请参阅 references/minimap.md。


7. 交互提示

屏幕空间中的“按 [E] 交互”提示——HUD 内的一个 Label,每帧跟随可交互对象的屏幕位置。由可交互对象 Area2D 上的 body_entered / body_exited 驱动。使用 InputMap.action_get_events(name) 显示玩家当前绑定的正确按键。

完整的 GDScript 和 C# 提示及 Interactable Area2D 组合,请参阅 references/interaction-prompts.md。


8. 检查清单

  • 所有 HUD 节点都是 CanvasLayer 的子节点(不受 Camera2D 影响),且 layer >= 1,以确保它们绘制在世界上方
  • 将 step 设为 0.0,以实现平滑的补间动画(TextureProgressBar 默认会跳变为整数值)
  • 血条绑定到 HealthComponent.health_changed 信号,而不是在 _process 中轮询
  • 开始新动画前先终止 Tween(_tween.kill()),避免快速受到伤害时动画叠加
  • 分数计数器使用 tween_method 对显示的整数进行插值,而不是直接跳变
  • 使用 get_viewport().get_canvas_transform() 将伤害数字的位置从世界坐标转换为屏幕坐标
  • 伤害数字池的大小足够,确保标签不会在补间动画完成前被回收
  • 通知堆栈限制 max_visible,并在每条通知消失后重新检查队列
  • Toast 自动消失使用 Timer 节点,而不是使用 await get_tree().create_timer()
  • 小地图的 SubViewport 设置了 render_target_update_mode = UPDATE_ALWAYS
  • 配置小地图 Camera2D 的缩放和 SubViewport 的 canvas_cull_mask,确保只显示预期的可见层
  • 交互提示每帧都将可交互对象的世界坐标转换为屏幕坐标,而不是在生成时缓存坐标
  • 使用 InputMap.action_get_events() 显示与玩家当前按键绑定对应的按键
  • 不需要接收输入的 HUD 节点将 mouse_filter = MOUSE_FILTER_IGNORE,以避免阻挡游戏中的点击

评论

支持 Markdown