8. 游戏界面
2026/4/14大约 9 分钟
8. 保卫萝卜——游戏界面
游戏界面的"地图"
游戏界面(UI)就像是商店的"招牌和货架"——玩家通过界面来了解游戏状态、做出操作决策。一个塔防游戏的界面通常包含以下部分:
| UI 组件 | 比喻 | 位置 | 功能 |
|---|---|---|---|
| HUD(抬头显示) | 仪表盘 | 顶部 | 金币、生命值、波次、分数 |
| 塔选择面板 | 商品货架 | 底部 | 选择要建造的塔类型 |
| 升级弹窗 | 操作菜单 | 塔旁边 | 升级/出售按钮 |
| 波次预告 | 预告片 | 屏幕中央 | "第N波即将来临" |
| 关卡结算 | 成绩单 | 全屏 | 胜利/失败、分数统计 |
HUD(抬头显示)
HUD 是固定在屏幕顶部的信息栏,始终可见。它显示玩家最需要实时关注的数据。
HUD 场景结构
HUD (CanvasLayer)
└── TopBar (HBoxContainer)
├── GoldLabel (HBoxContainer)
│ ├── GoldIcon (TextureRect) ← 金币图标
│ └── GoldValue (Label) ← 金币数量
├── LivesLabel (HBoxContainer)
│ ├── LivesIcon (TextureRect) ← 萝卜图标
│ └── LivesValue (Label) ← 生命值
├── WaveLabel (HBoxContainer)
│ ├── WaveIcon (TextureRect) ← 波次图标
│ └── WaveValue (Label) ← 当前波次
└── ScoreLabel (HBoxContainer)
├── ScoreIcon (TextureRect) ← 分数图标
└── ScoreValue (Label) ← 分数HUD 脚本
C
using Godot;
/// <summary>
/// HUD —— 游戏抬头显示
/// 固定在屏幕顶部,显示金币、生命值、波次、分数
/// 就像汽车的仪表盘,让玩家一眼看到关键信息
/// </summary>
public partial class HUD : CanvasLayer
{
// ===== 节点引用 =====
[ExportGroup("UI References")]
/// <summary>金币数值标签</summary>
[Export] public Label GoldValue { get; set; }
/// <summary>生命值标签</summary>
[Export] public Label LivesValue { get; set; }
/// <summary>波次标签</summary>
[Export] public Label WaveValue { get; set; }
/// <summary>分数标签</summary>
[Export] public Label ScoreValue { get; set; }
/// <summary>金币图标(用于闪烁效果)</summary>
[Export] public TextureRect GoldIcon { get; set; }
/// <summary>生命值图标(用于受伤动画)</summary>
[Export] public TextureRect LivesIcon { get; set; }
public override void _Ready()
{
// 连接 GameManager 的信号
var gm = GameManager.Instance;
gm.GoldChanged += OnGoldChanged;
gm.LivesChanged += OnLivesChanged;
gm.WaveChanged += OnWaveChanged;
gm.StateChanged += OnStateChanged;
// 初始显示
GoldValue.Text = gm.Gold.ToString();
LivesValue.Text = gm.Lives.ToString();
WaveValue.Text = "0";
ScoreValue.Text = gm.Score.ToString();
}
/// <summary>
/// 金币变化回调
/// </summary>
private void OnGoldChanged(int newGold)
{
GoldValue.Text = newGold.ToString();
GoldPulseAnimation();
}
/// <summary>
/// 生命值变化回调
/// </summary>
private void OnLivesChanged(int newLives)
{
LivesValue.Text = newLives.ToString();
LivesShakeAnimation();
// 生命值低时变红
if (newLives <= 3)
{
LivesValue.AddThemeColorOverride(
"font_color", Colors.Red);
}
}
/// <summary>
/// 波次变化回调
/// </summary>
private void OnWaveChanged(int newWave)
{
WaveValue.Text = newWave.ToString();
}
/// <summary>
/// 游戏状态变化回调
/// </summary>
private void OnStateChanged(int newState)
{
var state = (GameState)newState;
switch (state)
{
case GameState.Victory:
ShowVictoryEffect();
break;
case GameState.Defeat:
ShowDefeatEffect();
break;
}
}
/// <summary>
/// 金币获取脉冲动画
/// </summary>
private void GoldPulseAnimation()
{
if (GoldIcon == null) return;
var tween = GoldIcon.CreateTween();
tween.TweenProperty(GoldIcon, "scale",
new Vector2(1.3f, 1.3f), 0.1f);
tween.TweenProperty(GoldIcon, "scale",
Vector2.One, 0.1f);
}
/// <summary>
/// 生命值减少抖动动画
/// </summary>
private void LivesShakeAnimation()
{
if (LivesIcon == null) return;
var tween = LivesIcon.CreateTween();
tween.TweenProperty(LivesIcon, "position",
new Vector2(-3, 0), 0.05f);
tween.TweenProperty(LivesIcon, "position",
new Vector2(3, 0), 0.05f);
tween.TweenProperty(LivesIcon, "position",
new Vector2(-2, 0), 0.05f);
tween.TweenProperty(LivesIcon, "position",
new Vector2(2, 0), 0.05f);
tween.TweenProperty(LivesIcon, "position",
Vector2.Zero, 0.05f);
}
private void ShowVictoryEffect()
{
// 胜利效果(后续实现)
}
private void ShowDefeatEffect()
{
// 失败效果(后续实现)
}
}GDScript
extends CanvasLayer
class_name HUD
## HUD —— 游戏抬头显示
## 固定在屏幕顶部,显示金币、生命值、波次、分数
## 金币数值标签
@export var gold_value: Label
## 生命值标签
@export var lives_value: Label
## 波次标签
@export var wave_value: Label
## 分数标签
@export var score_value: Label
## 金币图标
@export var gold_icon: TextureRect
## 生命值图标
@export var lives_icon: TextureRect
func _ready() -> void:
var gm: GameManager = GameManager.instance
gm.gold_changed.connect(_on_gold_changed)
gm.lives_changed.connect(_on_lives_changed)
gm.wave_changed.connect(_on_wave_changed)
gm.state_changed.connect(_on_state_changed)
# 初始显示
gold_value.text = str(gm.gold)
lives_value.text = str(gm.lives)
wave_value.text = "0"
score_value.text = str(gm.score)
## 金币变化
func _on_gold_changed(new_gold: int) -> void:
gold_value.text = str(new_gold)
_gold_pulse_animation()
## 生命值变化
func _on_lives_changed(new_lives: int) -> void:
lives_value.text = str(new_lives)
_lives_shake_animation()
if new_lives <= 3:
lives_value.add_theme_color_override("font_color", Color.RED)
## 波次变化
func _on_wave_changed(new_wave: int) -> void:
wave_value.text = str(new_wave)
## 状态变化
func _on_state_changed(new_state: int) -> void:
match new_state:
GameState.VICTORY:
_show_victory_effect()
GameState.DEFEAT:
_show_defeat_effect()
## 金币脉冲动画
func _gold_pulse_animation() -> void:
if not gold_icon:
return
var tween = gold_icon.create_tween()
tween.tween_property(gold_icon, "scale", Vector2(1.3, 1.3), 0.1)
tween.tween_property(gold_icon, "scale", Vector2.ONE, 0.1)
## 生命值抖动动画
func _lives_shake_animation() -> void:
if not lives_icon:
return
var tween = lives_icon.create_tween()
tween.tween_property(lives_icon, "position", Vector2(-3, 0), 0.05)
tween.tween_property(lives_icon, "position", Vector2(3, 0), 0.05)
tween.tween_property(lives_icon, "position", Vector2(-2, 0), 0.05)
tween.tween_property(lives_icon, "position", Vector2(2, 0), 0.05)
tween.tween_property(lives_icon, "position", Vector2.ZERO, 0.05)
func _show_victory_effect() -> void:
pass
func _show_defeat_effect() -> void:
pass塔选择面板
塔选择面板固定在屏幕底部,展示所有可购买的塔类型。玩家点击某个塔图标后进入建造模式。
面板场景结构
TowerPanel (CanvasLayer)
└── PanelContainer
└── HBoxContainer
├── TowerButton (Button) ← 瓶子塔
│ ├── Icon (TextureRect)
│ ├── Name (Label)
│ └── Cost (Label)
├── TowerButton (Button) ← 风扇塔
├── TowerButton (Button) ← 火箭塔
└── TowerButton (Button) ← 雷达塔塔选择面板脚本
C
using Godot;
/// <summary>
/// 塔选择面板 —— 底部的塔"商品货架"
/// 玩家从这里选择要建造的塔类型
/// </summary>
public partial class TowerPanel : CanvasLayer
{
/// <summary>塔按钮容器</summary>
[Export] public HBoxContainer ButtonContainer { get; set; }
/// <summary>所有可用的塔数据</summary>
[Export] public TowerData[] AvailableTowers { get; set; }
/// <summary>塔按钮预制体</summary>
[Export] public PackedScene TowerButtonScene { get; set; }
/// <summary>建造控制器引用</summary>
private BuildController _buildController;
public override void _Ready()
{
_buildController = GetTree().Root
.FindChild("BuildController", true, false)
as BuildController;
// 连接信号
if (_buildController != null)
{
_buildController.BuildModeChanged += OnBuildModeChanged;
}
// 连接金币变化信号(更新按钮可用状态)
GameManager.Instance.GoldChanged += OnGoldChanged;
// 创建塔按钮
CreateTowerButtons();
}
/// <summary>
/// 动态创建塔按钮
/// </summary>
private void CreateTowerButtons()
{
foreach (var data in AvailableTowers)
{
var button = CreateTowerButton(data);
ButtonContainer.AddChild(button);
}
}
/// <summary>
/// 创建单个塔按钮
/// </summary>
private Button CreateTowerButton(TowerData data)
{
var button = new Button
{
CustomMinimumSize = new Vector2(80, 100),
TooltipText = $"{data.TowerName}\n{data.Description}"
};
// 设置按钮文字
button.Text = $"{data.TowerName}\n{data.Cost}G";
// 绑定点击事件
button.Pressed += () => OnTowerButtonPressed(data);
// 添加按钮到组中(方便后续更新)
button.AddToGroup("tower_buttons");
// 存储关联的塔数据(通过 Metadata)
button.SetMeta("tower_data", data);
return button;
}
/// <summary>
/// 点击塔按钮
/// </summary>
private void OnTowerButtonPressed(TowerData data)
{
if (_buildController == null) return;
if (_buildController.IsBuildingMode)
{
// 已经在建造模式,切换塔类型
_buildController.ExitBuildMode();
}
_buildController.EnterBuildMode(data);
}
/// <summary>
/// 建造模式变化回调
/// </summary>
private void OnBuildModeChanged(
bool isBuilding, TowerData towerData)
{
// 建造模式下禁用面板
// (玩家可以先取消建造再选别的塔)
// 也可以不禁用,让玩家自由切换
}
/// <summary>
/// 金币变化时更新按钮状态
/// </summary>
private void OnGoldChanged(int newGold)
{
var buttons = GetTree()
.GetNodesInGroup("tower_buttons");
foreach (var node in buttons)
{
if (node is not Button btn) continue;
var data = btn.GetMeta("tower_data") as TowerData;
if (data == null) continue;
// 金币不够时按钮变灰
btn.Disabled = newGold < data.Cost;
}
}
}GDScript
extends CanvasLayer
class_name TowerPanel
## 塔选择面板 —— 底部的塔"商品货架"
## 塔按钮容器
@export var button_container: HBoxContainer
## 可用的塔数据
@export var available_towers: Array[TowerData] = []
var _build_controller: BuildController = null
func _ready() -> void:
_build_controller = get_tree().root.find_child(
"BuildController", true, false
) as BuildController
if _build_controller:
_build_controller.build_mode_changed.connect(_on_build_mode_changed)
GameManager.instance.gold_changed.connect(_on_gold_changed)
_create_tower_buttons()
## 动态创建塔按钮
func _create_tower_buttons() -> void:
for data in available_towers:
var button: Button = _create_tower_button(data)
button_container.add_child(button)
## 创建单个塔按钮
func _create_tower_button(data: TowerData) -> Button:
var button = Button.new()
button.custom_minimum_size = Vector2(80, 100)
button.tooltip_text = "%s\n%s" % [data.tower_name, data.description]
button.text = "%s\n%dG" % [data.tower_name, data.cost]
var tower_data = data # 闭包捕获
button.pressed.connect(func(): _on_tower_button_pressed(tower_data))
button.add_to_group("tower_buttons")
button.set_meta("tower_data", data)
return button
## 点击塔按钮
func _on_tower_button_pressed(data: TowerData) -> void:
if not _build_controller:
return
if _build_controller.is_building_mode:
_build_controller.exit_build_mode()
_build_controller.enter_build_mode(data)
## 建造模式变化
func _on_build_mode_changed(_is_building: bool, _data: TowerData) -> void:
pass
## 金币变化更新按钮
func _on_gold_changed(new_gold: int) -> void:
var buttons = get_tree().get_nodes_in_group("tower_buttons")
for node in buttons:
if not node is Button btn:
continue
var data = btn.get_meta("tower_data") as TowerData
if not data:
continue
btn.disabled = new_gold < data.cost升级弹窗
当玩家点击已放置的塔时,在塔的旁边弹出一个小面板,显示塔的信息和升级/出售按钮。
升级弹窗脚本
C
using Godot;
/// <summary>
/// 塔信息弹窗 —— 显示在塔旁边的操作面板
/// 包含塔的属性信息和升级/出售按钮
/// </summary>
public partial class TowerInfoPanel : CanvasLayer
{
[Export] public PanelContainer Panel { get; set; }
[Export] public Label TowerNameLabel { get; set; }
[Export] public Label LevelLabel { get; set; }
[Export] public Label DamageLabel { get; set; }
[Export] public Label RangeLabel { get; set; }
[Export] public Button UpgradeButton { get; set; }
[Export] public Button SellButton { get; set; }
[Export] public Label UpgradeCostLabel { get; set; }
[Export] public Label SellRefundLabel { get; set; }
private TowerInteractionController _interactionController;
public override void _Ready()
{
_interactionController = GetTree().Root
.FindChild("TowerInteractionController", true, false)
as TowerInteractionController;
// 初始隐藏
Panel.Visible = false;
// 连接信号
_interactionController.TowerSelected += OnTowerSelected;
_interactionController.TowerDeselected += OnTowerDeselected;
UpgradeButton.Pressed += () =>
_interactionController.UpgradeSelectedTower();
SellButton.Pressed += () =>
_interactionController.SellSelectedTower();
// 金币变化时更新按钮状态
GameManager.Instance.GoldChanged += OnGoldChanged;
}
private void OnTowerSelected(Tower tower)
{
// 更新信息
TowerNameLabel.Text = tower.Name;
LevelLabel.Text = $"等级: {tower.CurrentLevel}/{tower.MaxLevel}";
DamageLabel.Text = $"攻击: {tower.CurrentDamage}";
RangeLabel.Text = $"射程: {tower.CurrentRange:F0}";
// 升级信息
if (tower.CurrentLevel < tower.MaxLevel)
{
int cost = _interactionController.GetUpgradeCost(tower);
UpgradeButton.Visible = true;
UpgradeButton.Disabled = GameManager.Instance.Gold < cost;
UpgradeCostLabel.Text = $"升级 ({cost}G)";
}
else
{
UpgradeButton.Visible = false;
}
// 出售信息
int refund = _interactionController.GetSellRefund(tower);
SellRefundLabel.Text = $"出售 (+{refund}G)";
// 定位到塔的位置
PositionPanel(tower.GlobalPosition);
Panel.Visible = true;
}
private void OnTowerDeselected()
{
Panel.Visible = false;
}
/// <summary>
/// 将面板定位到塔的旁边
/// </summary>
private void PositionPanel(Vector2 towerWorldPos)
{
// 将世界坐标转换为屏幕坐标
var camera = GetViewport().GetCamera2D();
if (camera == null) return;
Vector2 screenPos = camera
.GetScreenCenterPosition() + towerWorldPos;
// 放在塔的右上方
Panel.Position = new Vector2(
screenPos.X + 40,
screenPos.Y - 60);
}
private void OnGoldChanged(int newGold)
{
if (_interactionController.SelectedTower == null) return;
var tower = _interactionController.SelectedTower;
if (tower.CurrentLevel < tower.MaxLevel)
{
int cost = _interactionController.GetUpgradeCost(tower);
UpgradeButton.Disabled = newGold < cost;
}
}
}GDScript
extends CanvasLayer
class_name TowerInfoPanel
## 塔信息弹窗 —— 显示在塔旁边的操作面板
@export var panel: PanelContainer
@export var tower_name_label: Label
@export var level_label: Label
@export var damage_label: Label
@export var range_label: Label
@export var upgrade_button: Button
@export var sell_button: Button
@export var upgrade_cost_label: Label
@export var sell_refund_label: Label
var _interaction_controller: TowerInteractionController = null
func _ready() -> void:
_interaction_controller = get_tree().root.find_child(
"TowerInteractionController", true, false
) as TowerInteractionController
panel.visible = false
_interaction_controller.tower_selected.connect(_on_tower_selected)
_interaction_controller.tower_deselected.connect(func(): panel.visible = false)
upgrade_button.pressed.connect(func():
_interaction_controller.upgrade_selected_tower()
)
sell_button.pressed.connect(func():
_interaction_controller.sell_selected_tower()
)
GameManager.instance.gold_changed.connect(_on_gold_changed)
func _on_tower_selected(tower: Tower) -> void:
tower_name_label.text = tower.name
level_label.text = "等级: %d/%d" % [tower.current_level, tower.max_level]
damage_label.text = "攻击: %d" % tower.current_damage
range_label.text = "射程: %d" % tower.current_range
if tower.current_level < tower.max_level:
var cost: int = _interaction_controller._get_upgrade_cost(tower)
upgrade_button.visible = true
upgrade_button.disabled = GameManager.gold < cost
upgrade_cost_label.text = "升级 (%dG)" % cost
else:
upgrade_button.visible = false
var refund: int = _interaction_controller._get_sell_refund(tower)
sell_refund_label.text = "出售 (+%dG)" % refund
_position_panel(tower.global_position)
panel.visible = true
func _position_panel(tower_world_pos: Vector2) -> void:
var camera = get_viewport().get_camera_2d()
if not camera:
return
var screen_pos = camera.get_screen_center_position() + tower_world_pos
panel.position = Vector2(screen_pos.x + 40, screen_pos.y - 60)
func _on_gold_changed(new_gold: int) -> void:
if not _interaction_controller.selected_tower:
return
var tower = _interaction_controller.selected_tower
if tower.current_level < tower.max_level:
var cost: int = _interaction_controller._get_upgrade_cost(tower)
upgrade_button.disabled = new_gold < cost关卡结算面板
游戏胜利或失败时弹出结算面板,显示本局成绩。
C
using Godot;
/// <summary>
/// 关卡结算面板 —— 游戏结束时的"成绩单"
/// </summary>
public partial class GameResultPanel : CanvasLayer
{
[Export] public PanelContainer Panel { get; set; }
[Export] public Label TitleLabel { get; set; }
[Export] public Label ScoreLabel { get; set; }
[Export] public Label WaveLabel { get; set; }
[Export] public Label TowerLabel { get; set; }
[Export] public Button RestartButton { get; set; }
[Export] public Button NextLevelButton { get; set; }
public override void _Ready()
{
Panel.Visible = false;
GameManager.Instance.GameOver += OnGameOver;
RestartButton.Pressed += OnRestartPressed;
NextLevelButton.Pressed += OnNextLevelPressed;
}
private void OnGameOver(bool isVictory)
{
TitleLabel.Text = isVictory ? "胜利!" : "失败...";
TitleLabel.AddThemeColorOverride(
"font_color",
isVictory ? Colors.Gold : Colors.Red);
ScoreLabel.Text = $"分数: {GameManager.Instance.Score}";
WaveLabel.Text = $"到达波次: {GameManager.Instance.CurrentWave}";
// 统计放置的塔数量
var towerCount = GetTree()
.GetNodesInGroup("towers").Count;
TowerLabel.Text = $"放置塔数: {towerCount}";
NextLevelButton.Visible = isVictory;
Panel.Visible = true;
}
private void OnRestartPressed()
{
GetTree().ReloadCurrentScene();
}
private void OnNextLevelPressed()
{
// 加载下一关
GD.Print("加载下一关...");
}
}GDScript
extends CanvasLayer
class_name GameResultPanel
## 关卡结算面板 —— 游戏结束时的"成绩单"
@export var panel: PanelContainer
@export var title_label: Label
@export var score_label: Label
@export var wave_label: Label
@export var tower_label: Label
@export var restart_button: Button
@export var next_level_button: Button
func _ready() -> void:
panel.visible = false
GameManager.instance.game_over.connect(_on_game_over)
restart_button.pressed.connect(_on_restart_pressed)
next_level_button.pressed.connect(_on_next_level_pressed)
func _on_game_over(is_victory: bool) -> void:
title_label.text = "胜利!" if is_victory else "失败..."
title_label.add_theme_color_override(
"font_color",
Color.GOLD if is_victory else Color.RED
)
score_label.text = "分数: %d" % GameManager.score
wave_label.text = "到达波次: %d" % GameManager.current_wave
var tower_count: int = get_tree().get_nodes_in_group("towers").size()
tower_label.text = "放置塔数: %d" % tower_count
next_level_button.visible = is_victory
panel.visible = true
func _on_restart_pressed() -> void:
get_tree().reload_current_scene()
func _on_next_level_pressed() -> void:
print("加载下一关...")本节小结
| UI 组件 | 功能 |
|---|---|
| HUD | 顶部信息栏,显示金币/生命/波次/分数 |
| TowerPanel | 底部塔选择面板,选择要建造的塔 |
| TowerInfoPanel | 塔旁边弹出,显示属性和升级/出售按钮 |
| GameResultPanel | 结算面板,显示成绩和重玩/下一关按钮 |
所有 UI 组件都通过信号与 GameManager 和其他管理器通信,实现了数据和显示的分离。下一节我们将添加音效和特效,让游戏更有"感觉"。
