HUD 是玩家在战斗中唯一能看到的 UI 层。godot-game-one 的 HUD 要同时展示血量、分数、技能冷却、Boss 状态、排行榜,还要处理多人模式下的玩家标识。东西不少,但拆开来看每个都不复杂。
HUD 布局 HUD 的场景文件在 scenes/ui/hud.tscn,核心节点结构:
1 2 3 4 5 6 7 8 9 10 11 12 HUD (CanvasLayer) ├── ScoreLabel ← 左上 (16,12) "总分: 0" ├── LevelLabel ← 左上 (16,42) "等级 1" ├── PlayerScoresLabel ← 左上 (16,60) "P1: 0 | P2: 0" ├── HealthBar ← 左上 (16,84→220,110) ProgressBar │ └── HPNum ← 居中 "200/200" ├── PowerupDisplay ← 左上 (16,156) VBoxContainer(属性条) ├── ControlsLabel ← 右上锚定 操作提示 ├── FpsLabel ← 右上锚定 "FPS: 60" ├── SkillBar ← 右下锚定 HBoxContainer(技能槽) ├── LeaderboardPanel ← 右侧 (宽213) 半透明排行榜 └── GameOverPanel ← 居中 (隐藏) 结算面板
所有 HUD 元素都在 CanvasLayer 上,确保不随游戏世界摄像机移动。布局上采用固定像素定位 + 锚点定位混合:左侧信息用固定像素,右侧技能栏和排行榜用锚点适配屏幕宽度。
Boss 血条:双层设计 Boss 血条有两层:Boss 实体上的内联血条 + HUD 层的全局状态条。
内联血条 Boss 场景(scenes/entities/boss.tscn)自带一个 ProgressBar,跟随 Boss 移动:
1 2 3 [node name="HealthBar" type="ProgressBar" parent="."] offset_left = -90.0 offset_top = -76.0 offset_right = 90.0 offset_bottom = -64.0 theme_override_styles/fill = Color(0.6, 0.1, 0.6, 1) # 亮紫
初始化时设置最大值,每帧同步当前血量:
1 2 3 4 5 6 if _health_bar: _health_bar.max_value = _max_health _health_bar.value = _health if _label: _label.text = "裂隙母舰 Lv%d" % _level
内联血条的优点是直观——玩家看到血条在 Boss 头上,空间关系明确。缺点是 Boss 移动时血条也跟着动,不容易精确判断血量。
HUD 层状态条 HUD 层在屏幕顶部居中显示一个固定位置的状态条,包含百分比和阶段名:
1 2 3 4 5 6 7 8 func show_boss_status(current: float , maximum: float , phase_name: String) -> void: _ensure_boss_status_ui() _boss_status_root.visible = true var ratio: float = clampf(current / maxf(maximum, 1.0 ), 0.0 , 1.0 ) _boss_status_fill.size.x = 520.0 * ratio _boss_status_label.text = "裂隙母舰 %d%%" % int (round (ratio * 100.0 )) _boss_status_phase.text = phase_name
状态条是动态创建的:
1 2 3 4 5 6 7 func _ensure_boss_status_ui() -> void: _boss_status_root = Control.new() _boss_status_root.position = Vector2( maxf((viewport_size.x - 560.0 ) * 0.5 , 16.0 ), 16.0 ) _boss_status_root.size = Vector2(560.0 , 48.0 ) _boss_status_root.z_index = 45
Boss 有三个阶段,每个阶段有独立名称:
1 2 3 4 5 func get_phase_name() -> String: match _phase: BossPhase.SUPPRESSION: return "压制校准" BossPhase.RIFT: return "裂隙展开" BossPhase.OVERLOAD: return "核心过载"
双层血条的设计意图是:内联血条提供空间感(Boss 在哪里),HUD 层状态条提供精确信息(还剩多少血、当前什么阶段)。
分数系统 基础分数 分数通过信号驱动更新:
1 2 3 4 5 6 7 func _on_score_changed(new_score: int ) -> void: _update_score(new_score) func _update_score(score: int ) -> void: _score_label.text = "总分: %d" % score _cached_score = score
HUD 每帧检查分数变化,避免不必要的重绘:
1 2 3 4 func _refresh_score_if_needed() -> void: var current_score: int = GameState.score if current_score != _cached_score: _update_score(current_score)
多人分数 多人模式下,分数按存活人数均分。HUD 用竖线分隔显示每个玩家的分数:
1 2 3 4 5 6 func _update_player_scores(scores: Dictionary, total_score: int ) -> void: var rows: Array[String] = [] for key in keys: var pid: int = int (String(key)) rows.append("%s: %d" % [_peer_label(pid), int (scores[key])]) _player_scores_label.text = " | " .join(rows)
默认显示格式:P1: 1200 | P2: 800
属性条动画 升级道具拾取后,属性条有 Tween 动画反馈:
1 2 3 4 5 6 7 8 9 func _animate_powerup_row(row, bg, fill, val_lbl, prev_fill_w, fill_w, bar_h, bar_color): row_tween.tween_property(row, "scale" , Vector2(1 , 1 ), 0.18 ) if fill_w > prev_fill_w: trail_tween.tween_property(trail, "modulate:a" , 0.0 , 0.22 ) else : flash_tween.tween_property(edge_flash, "modulate:a" , 0.0 , 0.16 )
升级和降级有不同的视觉反馈——升级是白色拖尾向上,降级是边缘闪烁。这种细微差异帮助玩家不看文字也能感知变化方向。
多人玩家标识 标签映射 ENet 的 peer_id 不固定,项目限制 1 个 Client,所以 UI 层按角色槽位显示:
1 2 3 4 func _peer_label(peer_id: int ) -> String: if peer_id == 1 : return "P1" return "P2"
玩家节点的 authority 设置 每个玩家节点的 multiplayer_authority 设为该玩家的 peer_id:
1 2 3 4 5 6 7 var peer_id: int = 1 func _init_multiplayer() -> void: if name.is_valid_int(): peer_id = name.to_int() set_multiplayer_authority(peer_id)
本地玩家解析 HUD 需要区分当前本地玩家和远程玩家:
1 2 3 4 5 6 7 8 9 func _resolve_local_player() -> Node: for p in get_tree().get_nodes_in_group("player" ): if not NetworkManager.is_online(): return p if int (maybe_peer_id) == multiplayer.get_unique_id(): return p if p.is_multiplayer_authority(): return p return nil
单机模式下直接返回唯一的玩家节点,在线模式下通过 multiplayer_authority 匹配。
动态 UI 元素 中央横幅 等级提升和 Boss 出现时显示全屏横幅:
1 2 3 4 5 func show_center_banner(text: String, hold: float = 2.0 , color: Color = ...): tween.tween_property(banner, "modulate:a" , 1.0 , 0.18 ) tween.tween_interval(maxf(hold, 0.2 )) tween.tween_property(banner, "modulate:a" , 0.0 , 0.28 )
受击红屏 玩家受伤时全屏红色闪烁:
1 2 3 4 func _setup_damage_flash() -> void: _damage_flash = ColorRect.new() _damage_flash.z_index = 200
拾取 Toast 道具拾取时在世界坐标位置显示上浮淡出的文字提示,跟随道具位置消失。
技能冷却遮罩 技能图标上有半透明暗色遮罩,冷却完成时变为全透明:
1 2 3 4 func set_ready_progress(value: float ) -> void: _progress = clamp(value, 0.0 , 1.0 ) color = Color(0.0 , 0.0 , 0.0 , 0.5 * (1.0 - _progress))
技能栏的响应式布局 技能栏在右下角定位,移动端和桌面端有不同的边距:
1 2 3 4 5 6 7 8 9 10 11 12 const SKILL_SLOT_DESKTOP: Vector2 = Vector2(144 , 144 ) const SKILL_SLOT_MOBILE: Vector2 = Vector2(116 , 116 ) const SKILL_BAR_MOBILE_MARGIN: Vector2 = Vector2(30 , 38 ) const SKILL_BAR_DESKTOP_MARGIN: Vector2 = Vector2(20 , 18 ) func _layout_skill_bar() -> void: _skill_bar.anchor_left = 1.0 _skill_bar.anchor_top = 1.0 _skill_bar.anchor_right = 1.0 _skill_bar.anchor_bottom = 1.0 if OS.has_feature("android" ) or OS.has_feature("ios" ): _skill_bar.offset_left = -bar_size.x - SKILL_BAR_MOBILE_MARGIN.x else : _skill_bar.offset_left = -bar_size.x - SKILL_BAR_DESKTOP_MARGIN.x
移动端技能格更小(116px vs 144px),边距更大(30px vs 20px),避免被系统手势区域遮挡。
排行榜 右侧排行榜实时显示分数排名和历史记录:
1 2 3 4 func _refresh_leaderboard() -> void: label.text = "#%d %s : %d" % [i + 1 , _peer_label(int (row.peer_id)), int (row.score)] history.text = "历史 %s 总分:%d %s Lv.%d" % [ str (entry.time), int (entry.score), players_text, int (entry.level)]
排行榜是半透明面板,不遮挡游戏画面,但足够清晰。
设计取舍 HUD 的取舍:固定像素定位为主,没用 Godot 的锚点布局。弹幕游戏目标分辨率明确,固定定位更可控。Boss 状态条和受击红闪用代码动态创建,方便控制生命周期。基础元素用场景预设,编辑器里能直接调。所有 HUD 元素在同一个 CanvasLayer,没做分层。