mcpField
H
hud-system

hud-system

github.com/jame581デザイン・UI889181 回インストール4 日前に更新
リポジトリ

hud-system について

Godot 4.3以降でゲームHUDを実装する開発者向けのスキルガイドです。GDScriptとC#の例を用い、CanvasLayerの構成や体力、スコア、通知などを説明します。

主な機能

  1. 01CanvasLayerによるHUD構成
  2. 02コンポーネント連携の体力バー
  3. 03Tweenによるスコア表示
  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 システム

すべての例は、非推奨 API を使用せずに Godot 4.3+ を対象としています。まず GDScript を示し、続いて C# を示します。

関連スキル: Control ノードのレイアウトとテーマについては godot-ui、HealthComponent の統合については component-system、スコア/通知シグナルについては event-bus、インベントリ UI のパターンについては inventory-system、CanvasLayer の設定と描画順については 2d-essentials、クールダウンバーとリソースバーのバインドパターンについては ability-system を参照してください。


1. HUD のアーキテクチャ

CanvasLayer を使う理由

CanvasLayer は、独自の変換を使って子ノードを描画します。この変換は Camera2D から独立しています(follow_viewport_enabled が有効な場合を除きます)。Camera3D が 2D ノードを動かすことはありません。CanvasLayer を使わない場合、シーンルートのデフォルトキャンバス(レイヤー 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. 通知システム

トースト/通知のスタックです。右上に固定した VBoxContainer を使い、max_visible で表示数を制限して、キューに従って通知を消去します。新しいトーストは古いトーストの表示時間が終わるまで待機します。

自動消去タイマー付きの完全な 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) を使用してください。

プロンプトと Interactable Area2D の組み合わせを実装した完全な GDScript と C# については、references/interaction-prompts.md を参照してください。


8. チェックリスト

  • すべての HUD ノードが、ワールドの影響を受けない CanvasLayer の子であり、ワールドより手前に描画されるよう layer >= 1 に設定されています
  • スムーズなトゥイーンアニメーションを実現するため、step が 0.0 に設定されています(TextureProgressBar はデフォルトで整数値にスナップします)
  • 体力バーは HealthComponent.health_changed シグナルに接続されており、_process でポーリングしていません
  • 新しいトゥイーンを開始する前に、トゥイーンを停止(_tween.kill())しているため、短時間に連続してダメージを受けてもアニメーションが重なりません
  • スコアカウンターは tween_method を使って表示する整数を補間しており、値が急に切り替わりません
  • ダメージ数値の位置は、get_viewport().get_canvas_transform() を使ってワールド座標からスクリーン座標に変換されています
  • ダメージ数値プールのサイズは、トゥイーンが完了する前にラベルが再利用されないだけの十分な大きさです
  • 通知スタックは max_visible を適用し、通知が消えるたびにキューを再確認します
  • トーストの自動消去には 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 に設定されています

コメント