关于 hud-system
面向 Godot 4.3+ 游戏开发者的 HUD 技能指南,提供 GDScript 和 C# 示例。介绍基于 CanvasLayer 的布局,以及生命值、分数、通知、小地图和交互提示等实现模式。
主要功能
- 01基于 CanvasLayer 的 HUD 架构
- 02绑定组件的生命值条
- 03使用 Tween 动画更新分数
- 04池化的浮动伤害数字
- 05队列式通知堆栈
- 06小地图与交互提示
适用场景
- 01摄像机移动时保持 HUD 固定
- 02显示生命值和分数变化
- 03呈现排队的 Toast 通知
- 04添加小地图和情境交互提示
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ jame581/ godotprompter --skill hud-system安装后在 Claude Code 中输入/hud-system确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/damage-numbers.mdinteraction-prompts.mdminimap.mdnotifications.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,以避免阻挡游戏中的点击