8. 游戏界面
2026/4/14大约 4 分钟
8. 雷霆战机——游戏界面
8.1 界面布局
雷霆战机的UI布局比较紧凑,不能遮挡游戏区域:
┌──────────────────────┐
│ BOSS: ████░░ 60% │ ← Boss血条(仅Boss战时显示)
│ │
│ SCORE: 12500 │ ← 左上角分数
│ │
│ │
│ │
│ 战斗区域 │
│ │
│ │
│ ❤❤❤ 💣💣💣 ★★★ │ ← 左下:生命 炸弹 武器等级
│ [炸弹按钮] │ ← 移动端
└──────────────────────┘8.2 HUD
C
using Godot;
/// <summary>
/// 雷霆战机HUD
/// </summary>
public partial class RaidenHUD : CanvasLayer
{
// 分数
private Label _scoreLabel;
// 生命(用图标表示)
private HBoxContainer _livesContainer;
private PackedScene _lifeIconScene;
// 炸弹
private HBoxContainer _bombsContainer;
private PackedScene _bombIconScene;
// 武器等级
private Label _weaponLabel;
// 波次提示
private Label _waveLabel;
private float _waveLabelTimer = 0f;
// Boss血条
private BossHealthBar _bossHealthBar;
private RaidenGameManager _gameManager;
public override void _Ready()
{
_scoreLabel = GetNode<Label>("ScorePanel/ScoreValue");
_livesContainer = GetNode<HBoxContainer>("StatusPanel/Lives");
_bombsContainer = GetNode<HBoxContainer>("StatusPanel/Bombs");
_weaponLabel = GetNode<Label>("StatusPanel/WeaponLevel");
_waveLabel = GetNode<Label>("WaveLabel");
_bossHealthBar = GetNode<BossHealthBar>("BossHealthBar");
_gameManager = GetNode<RaidenGameManager>(
"/root/RaidenGameManager");
// 连接信号
_gameManager.ScoreChanged += OnScoreChanged;
_gameManager.LivesChanged += OnLivesChanged;
_gameManager.BombsChanged += OnBombsChanged;
_gameManager.WeaponChanged += OnWeaponChanged;
// 连接波次管理器
var waveMgr = GetNodeOrNull("/root/Main/WaveManager");
if (waveMgr != null)
{
waveMgr.Connect("wave_started",
Callable.From((int n) => ShowWaveText($"WAVE {n}")));
waveMgr.Connect("boss_wave_started",
Callable.From(() => ShowWaveText("WARNING!\nBOSS INCOMING")));
}
UpdateLivesDisplay();
UpdateBombsDisplay();
}
public override void _Process(double delta)
{
// 波次提示自动消失
if (_waveLabelTimer > 0)
{
_waveLabelTimer -= (float)delta;
if (_waveLabelTimer <= 0)
{
var tween = CreateTween();
tween.TweenProperty(_waveLabel, "modulate:a",
0f, 0.5f);
}
}
}
private void OnScoreChanged(int score)
{
_scoreLabel.Text = score.ToString("N0");
}
private void OnLivesChanged(int lives)
{
UpdateLivesDisplay();
}
private void OnBombsChanged(int bombs)
{
UpdateBombsDisplay();
}
private void OnWeaponChanged(int level)
{
_weaponLabel.Text = $"WPN Lv.{level}";
// 升级闪光
var tween = CreateTween();
tween.TweenProperty(_weaponLabel, "modulate",
Color(0, 1, 1, 1), 0.1f);
tween.TweenProperty(_weaponLabel, "modulate",
Colors.White, 0.3f);
}
/// <summary>
/// 更新生命显示(用心形图标)
/// </summary>
private void UpdateLivesDisplay()
{
foreach (var child in _livesContainer.GetChildren())
{
child.QueueFree();
}
for (int i = 0; i < _gameManager.Lives; i++)
{
var icon = new TextureRect();
icon.Texture = GD.Load<Texture2D>(
"res://assets/sprites/ui/heart.png");
icon.ExpandMode = TextureRect.ExpandModeEnum.FitWidthProportional;
icon.CustomMinimumSize = new Vector2(16, 16);
_livesContainer.AddChild(icon);
}
}
/// <summary>
/// 更新炸弹显示
/// </summary>
private void UpdateBombsDisplay()
{
foreach (var child in _bombsContainer.GetChildren())
{
child.QueueFree();
}
for (int i = 0; i < _gameManager.Bombs; i++)
{
var icon = new TextureRect();
icon.Texture = GD.Load<Texture2D>(
"res://assets/sprites/ui/bomb_icon.png");
icon.ExpandMode = TextureRect.ExpandModeEnum.FitWidthProportional;
icon.CustomMinimumSize = new Vector2(16, 16);
_bombsContainer.AddChild(icon);
}
}
/// <summary>
/// 显示波次提示文字
/// </summary>
public void ShowWaveText(string text)
{
_waveLabel.Text = text;
_waveLabel.Modulate = new Color(1, 1, 1, 1);
_waveLabelTimer = 2f;
// 缩放动画
var tween = CreateTween();
_waveLabel.Scale = new Vector2(1.5f, 1.5f);
tween.TweenProperty(_waveLabel, "scale",
Vector2.One, 0.3f).SetTrans(Tween.TransitionType.Back);
}
}GDScript
extends CanvasLayer
## 雷霆战机HUD
@onready var _score_label = $ScorePanel/ScoreValue
@onready var _lives_container = $StatusPanel/Lives
@onready var _bombs_container = $StatusPanel/Bombs
@onready var _weapon_label = $StatusPanel/WeaponLevel
@onready var _wave_label = $WaveLabel
@onready var _boss_health_bar = $BossHealthBar
var _game_manager: RaidenGameManager
var _wave_label_timer: float = 0.0
func _ready() -> void:
_game_manager = get_node("/root/RaidenGameManager")
_game_manager.score_changed.connect(_on_score_changed)
_game_manager.lives_changed.connect(_on_lives_changed)
_game_manager.bombs_changed.connect(_on_bombs_changed)
_game_manager.weapon_changed.connect(_on_weapon_changed)
var wave_mgr = get_node_or_null("/root/Main/WaveManager")
if wave_mgr:
wave_mgr.connect("wave_started",
func(n): show_wave_text("WAVE %d" % n))
wave_mgr.connect("boss_wave_started",
func(): show_wave_text("WARNING!\nBOSS INCOMING"))
_update_lives_display()
_update_bombs_display()
func _process(delta: float) -> void:
if _wave_label_timer > 0:
_wave_label_timer -= delta
if _wave_label_timer <= 0:
var tween = create_tween()
tween.tween_property(_wave_label, "modulate:a", 0.0, 0.5)
func _on_score_changed(score: int) -> void:
_score_label.text = "%d" % score
func _on_lives_changed(lives: int) -> void:
_update_lives_display()
func _on_bombs_changed(bombs: int) -> void:
_update_bombs_display()
func _on_weapon_changed(level: int) -> void:
_weapon_label.text = "WPN Lv.%d" % level
var tween = create_tween()
tween.tween_property(_weapon_label, "modulate", Color(0, 1, 1, 1), 0.1)
tween.tween_property(_weapon_label, "modulate", Color.WHITE, 0.3)
func _update_lives_display() -> void:
for child in _lives_container.get_children():
child.queue_free()
for i in range(_game_manager.lives):
var icon = TextureRect.new()
icon.texture = load("res://assets/sprites/ui/heart.png")
icon.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL
icon.custom_minimum_size = Vector2(16, 16)
_lives_container.add_child(icon)
func _update_bombs_display() -> void:
for child in _bombs_container.get_children():
child.queue_free()
for i in range(_game_manager.bombs):
var icon = TextureRect.new()
icon.texture = load("res://assets/sprites/ui/bomb_icon.png")
icon.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL
icon.custom_minimum_size = Vector2(16, 16)
_bombs_container.add_child(icon)
## 显示波次提示文字
func show_wave_text(text: String) -> void:
_wave_label.text = text
_wave_label.modulate = Color(1, 1, 1, 1)
_wave_label_timer = 2.0
var tween = create_tween()
_wave_label.scale = Vector2(1.5, 1.5)
tween.tween_property(_wave_label, "scale", Vector2.ONE, 0.3)\
.set_trans(Tween.TransitionType.BACK)8.3 暂停菜单
暂停菜单提供"继续"、"重新开始"和"返回主菜单"三个选项,和俄罗斯方块的暂停菜单类似。
8.4 游戏结束画面
游戏结束时显示最终得分和最高分,以及操作选项。
8.5 排行榜
本地排行榜保存玩家的历史最高分。
C
/// <summary>
/// 简单的本地排行榜
/// </summary>
public static class Leaderboard
{
private const int MaxEntries = 10;
private const string SavePath = "user://raiden_leaderboard.cfg";
/// <summary>
/// 获取排行榜数据
/// </summary>
public static List<(string Name, int Score)> GetEntries()
{
var config = new ConfigFile();
config.Load(SavePath);
var entries = new List<(string, int)>();
for (int i = 0; i < MaxEntries; i++)
{
string name = (string)config.GetValue(
"scores", $"name_{i}", "");
int score = (int)config.GetValue(
"scores", $"score_{i}", 0);
if (score > 0)
entries.Add((name, score));
}
return entries;
}
/// <summary>
/// 添加新分数
/// </summary>
public static int AddScore(string name, int score)
{
var entries = GetEntries();
entries.Add((name, score));
// 按分数降序排列
entries.Sort((a, b) => b.Score.CompareTo(a.Score));
// 只保留前10名
if (entries.Count > MaxEntries)
entries = entries.GetRange(0, MaxEntries);
// 保存
var config = new ConfigFile();
for (int i = 0; i < entries.Count; i++)
{
config.SetValue("scores", $"name_{i}", entries[i].Name);
config.SetValue("scores", $"score_{i}", entries[i].Score);
}
config.Save(SavePath);
// 返回排名(1-based)
return entries.FindIndex(e => e.Score == score
&& e.Name == name) + 1;
}
}GDScript
## 简单的本地排行榜
class_name Leaderboard
const MAX_ENTRIES: int = 10
const SAVE_PATH: String = "user://raiden_leaderboard.cfg"
## 获取排行榜数据
static func get_entries() -> Array:
var config = ConfigFile.new()
config.load(SAVE_PATH)
var entries = []
for i in range(MAX_ENTRIES):
var name = config.get_value("scores", "name_%d" % i, "")
var score = config.get_value("scores", "score_%d" % i, 0)
if score > 0:
entries.append({"name": name, "score": score})
return entries
## 添加新分数
static func add_score(name: String, score: int) -> int:
var entries = get_entries()
entries.append({"name": name, "score": score})
entries.sort_custom(func(a, b): return a["score"] > b["score"])
if entries.size() > MAX_ENTRIES:
entries = entries.slice(0, MAX_ENTRIES)
var config = ConfigFile.new()
for i in range(entries.size()):
config.set_value("scores", "name_%d" % i, entries[i]["name"])
config.set_value("scores", "score_%d" % i, entries[i]["score"])
config.save(SAVE_PATH)
for i in range(entries.size()):
if entries[i]["score"] == score and entries[i]["name"] == name:
return i + 1
return entries.size()8.6 本章小结
| 组件 | 说明 |
|---|---|
| HUD | 分数、生命图标、炸弹图标、武器等级 |
| 波次提示 | "WAVE 1"、"WARNING BOSS"等提示 |
| Boss血条 | 顶部显示,仅Boss战时出现 |
| 排行榜 | 本地保存前10名最高分 |
关键设计点:
- UI不能遮挡太多游戏区域
- 用图标代替文字显示生命和炸弹,更直观
- 波次切换时有醒目的文字提示
- 排行榜数据持久化保存
下一章添加音效和特效。
