Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Use when building in-game HUDs — health bars, score displays, minimap, notifications, and damage numbers
.claude/skills/jame581-hud-system/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 48% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 62% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 59% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 83% | 0% |
| case-05 | ✓→✓ | = Same ✓ | 208% | 0% |
All examples target Godot 4.3+ with no deprecated APIs. GDScript is shown first, then C#.
> Related skills: godot-ui for Control node layout and themes, component-system for HealthComponent integration, event-bus for score/notification signals, inventory-system for inventory UI patterns, 2d-essentials for CanvasLayer setup and draw order, ability-system for cooldown bar and resource bar binding patterns.
A CanvasLayer renders its children in a fixed screen-space layer that is completely independent of any Camera2D or Camera3D transform. Without it, HUD nodes attached to the scene root still move with the camera when you pan or zoom. Wrapping all HUD nodes in a CanvasLayer (layer ≥ 1) ensures the HUD always stays in place regardless of camera movement.
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)Key rules:
CanvasLayer. Do not mix HUD nodes into the game world tree.layer = 1 for the main HUD. Use higher values (e.g. 10) for overlays or pause menus that must appear above the HUD.Node2D child of the CanvasLayer and use get_viewport().get_screen_transform() to convert world positions to screen positions.| Node | When to use | |---|---| | ProgressBar | Prototyping, plain-colour bars | | TextureProgressBar | Pixel-art or stylised bars using sprite sheets |
Both expose min_value, max_value, and value. Set step = 0 so tweening produces a smooth animation rather than snapping to integer steps.
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)
csharp// 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); } }
Tip: If you use TextureProgressBar, set fill_mode to FILL_LEFT_TO_RIGHT and assign your bar texture to texture_progress. The value / max_value ratio drives how much of the texture is revealed.
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)
csharp// 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() { EventBus.Instance.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 signals needed:
gdscript# autoloads/event_bus.gd signal score_changed(new_score: int)
csharp// EventBus.cs (partial — score signal) [Signal] public delegate void ScoreChangedEventHandler(int newScore);
Emit from wherever points are awarded:
gdscript# Inside a collectible or enemy death handler EventBus.score_changed.emit(GameState.score)
csharp// Inside a collectible or enemy death handler EventBus.Instance.EmitSignal(EventBus.SignalName.ScoreChanged, GameState.Score);
Floating "−25" labels that rise and fade above the hit point. Pooled in a HUD-side spawner; world position converted to screen via get_viewport().get_canvas_transform(). Optional crit colorization before spawn.
> See references/damage-numbers.md for the full GDScript and C# DamageNumber scene + pooled spawner.
Toast / notification stack — a VBoxContainer anchored top-right with max_visible clamping and queue-driven dismissal. New toasts wait for an old one to expire before showing.
> See references/notifications.md for the full GDScript and C# stack with auto-dismiss timers.
Render a top-down view via a dedicated SubViewport + Camera2D that follows the player. Display the SubViewport texture in a TextureRect inside the HUD. Optional circular mask via ColorRect shader. Set render_target_update_mode = UPDATE_ALWAYS.
> See references/minimap.md for the SubViewport setup, MinimapCamera GDScript + C#, and circular-mask shader.
Screen-space "Press E] to interact" prompt — a Label inside the HUD that follows an interactable's screen position each frame. Driven by body_entered / body_exited on the interactable's Area2D. Use InputMap.action_get_events(name) to display the correct key for the player's current binding.
> See references/interaction-prompts.md for the full GDScript and C# prompt + Interactable Area2D pair.
CanvasLayer with layer >= 1 so they are unaffected by camera transformsProgressBar.step is set to 0.0 for smooth tween animation rather than integer snappingHealthComponent.health_changed signal — does not poll in _process_tween.kill()) before starting a new one so rapid damage does not stack animationstween_method to interpolate the displayed integer — not a jump cutget_viewport().get_canvas_transform()max_visible and re-checks the queue after each dismissalTimer node — not await get_tree().create_timer()SubViewport for minimap has render_target_update_mode = UPDATE_ALWAYSCamera2D zoom and cull mask are configured so only the intended layers are visibleInputMap.action_get_events() is used to display the correct key for the player's current bindingmouse_filter = MOUSE_FILTER_IGNORE to avoid blocking game clicks| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 16,502 | 8,552 | -48% | 1 | 1 | 0% | 3,099 | 4,576 | +48% | 0 | 0 | — |
case-02 | pass→pass | 15,391 | 8,194 | -47% | 1 | 1 | 0% | 2,833 | 4,583 | +62% | 0 | 0 | — |
case-03 | pass→pass | 14,542 | 8,243 | -43% | 1 | 1 | 0% | 2,873 | 4,556 | +59% | 0 | 0 | — |
case-04 | pass→pass | 16,619 | 15,824 | -5% | 1 | 1 | 0% | 3,335 | 6,109 | +83% | 0 | 0 | — |
case-05 | pass→pass | 13,123 | 19,267 | +47% | 1 | 1 | 0% | 2,207 | 6,793 | +208% | 0 | 0 | — |
case-06 | pass→pass | 9,246 | 7,670 | -17% | 1 | 1 | 0% | 1,460 | 4,277 | +193% | 0 | 0 | — |
case-07 | pass→pass | 10,880 | 11,235 | +3% | 1 | 1 | 0% | 1,787 | 5,031 | +182% | 0 | 0 | — |
case-08 | pass→pass | 15,287 | 11,510 | -25% | 1 | 1 | 0% | 2,375 | 4,883 | +106% | 0 | 0 | — |
case-09 | pass→pass | 7,823 | 4,992 | -36% | 1 | 1 | 0% | 1,273 | 3,804 | +199% | 0 | 0 | — |
case-10 | pass→pass | 13,321 | 9,093 | -32% | 1 | 1 | 0% | 2,299 | 4,704 | +105% | 0 | 0 | — |
case-11 | pass→pass | 14,038 | 8,726 | -38% | 1 | 1 | 0% | 2,214 | 4,294 | +94% | 0 | 0 | — |
case-12 | pass→pass | 11,588 | 10,529 | -9% | 1 | 1 | 0% | 1,844 | 4,669 | +153% | 0 | 0 | — |
case-13 | pass→pass | 10,908 | 12,026 | +10% | 1 | 1 | 0% | 1,767 | 5,033 | +185% | 0 | 0 | — |
case-14 | pass→pass | 16,919 | 14,399 | -15% | 1 | 1 | 0% | 2,899 | 5,616 | +94% | 0 | 0 | — |
case-15 | pass→pass | 6,838 | 3,664 | -46% | 1 | 1 | 0% | 1,146 | 3,688 | +222% | 0 | 0 | — |
case-16 | pass→pass | 12,486 | 6,460 | -48% | 1 | 1 | 0% | 2,324 | 4,078 | +75% | 0 | 0 | — |
case-17 | pass→pass | 19,205 | 14,851 | -23% | 1 | 1 | 0% | 3,165 | 5,674 | +79% | 0 | 0 | — |
case-18 | pass→pass | 11,271 | 8,947 | -21% | 1 | 1 | 0% | 1,896 | 4,534 | +139% | 0 | 0 | — |
case-19 | pass→pass | 12,151 | 11,120 | -8% | 1 | 1 | 0% | 1,924 | 4,784 | +149% | 0 | 0 | — |
case-20 | pass→pass | 17,655 | 15,069 | -15% | 1 | 1 | 0% | 2,824 | 5,421 | +92% | 0 | 0 | — |
case-21 | pass→pass | 8,897 | 3,734 | -58% | 1 | 1 | 0% | 1,352 | 3,621 | +168% | 0 | 0 | — |
case-22 | pass→pass | 16,367 | 13,481 | -18% | 1 | 1 | 0% | 2,864 | 5,363 | +87% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +5 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.