10. UI打磨
2026/4/15大约 4 分钟
10. UI打磨:让游戏界面专业起来
10.1 HUD 布局
MOBA 游戏的 HUD(抬头显示)信息量很大,需要合理布局:
┌──────────────────────────────────────────────────────┐
│ [队友头像+血条] [敌人击杀数] │
│ [队友头像+血条] [己方击杀数] │
│ [队友头像+血条] │
│ [队友头像+血条] │
│ [队友头像+血条] │
│ │
│ ┌─────┐ ┌────────────┐ │
│ │小地图│ │ 击杀信息 │ │
│ │ │ │ 滚动显示 │ │
│ └─────┘ └────────────┘ │
│ │
│ ┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐ [金币] │
│ │装备││装备││装备││装备││装备││装备│ [等级] │
│ └──┘└──┘└──┘└──┘└──┘└──┘ [时间] │
│ │
│ ┌──────────────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │ 英雄头像 │ │ Q │ │ E │ │ R │ │ B/F │ │
│ │ +经验条 │ │ 技能1│ │ 技能2│ │ 大招 │ │ 回城│ │
│ │ +血条/蓝条 │ └─────┘ └─────┘ └─────┘ └─────┘ │
│ └──────────────┘ │
└──────────────────────────────────────────────────────┘10.2 技能栏 UI
C#
// SkillBarUI.cs
using Godot;
public partial class SkillBarUI : HBoxContainer
{
private SkillSlotUI[] _slots = new SkillSlotUI[4]; // Q, E, R, B
public override void _Ready()
{
// 创建技能槽
for (int i = 0; i < 4; i++)
{
var slot = new SkillSlotUI();
slot.SlotIndex = i;
AddChild(slot);
_slots[i] = slot;
}
}
// 更新冷却显示
public void UpdateCooldowns(SkillManager skillManager)
{
for (int i = 0; i < 4; i++)
{
string skillId = skillManager.GetSkillIdBySlot(i);
if (string.IsNullOrEmpty(skillId)) continue;
float remaining = skillManager.GetCooldownRemaining(skillId);
int level = skillManager.GetSkillLevel(skillId);
_slots[i].UpdateCooldown(remaining, level);
}
}
}
// SkillSlotUI.cs - 单个技能槽
public partial class SkillSlotUI : PanelContainer
{
public int SlotIndex { get; set; }
private TextureRect _icon;
private ColorRect _cooldownOverlay;
private Label _cooldownLabel;
private Label _keyLabel;
public override void _Ready()
{
// 技能图标
_icon = new TextureRect();
_icon.CustomMinimumSize = new Vector2(50, 50);
AddChild(_icon);
// 冷却遮罩
_cooldownOverlay = new ColorRect();
_cooldownOverlay.Color = new Color(0, 0, 0, 0.6f);
_cooldownOverlay.CustomMinimumSize = new Vector2(50, 50);
AddChild(_cooldownOverlay);
// 冷却倒计时文字
_cooldownLabel = new Label();
_cooldownLabel.HorizontalAlignment = HorizontalAlignment.Center;
_cooldownLabel.VerticalAlignment = VerticalAlignment.Center;
AddChild(_cooldownLabel);
// 按键提示
_keyLabel = new Label();
string[] keys = { "Q", "E", "R", "B" };
_keyLabel.Text = keys[SlotIndex];
AddChild(_keyLabel);
}
public void UpdateCooldown(float remaining, int level)
{
if (remaining > 0)
{
_cooldownOverlay.Visible = true;
_cooldownLabel.Text = remaining.ToString("F1");
_cooldownLabel.Visible = true;
}
else
{
_cooldownOverlay.Visible = false;
_cooldownLabel.Visible = false;
}
}
}GDScript
# skill_bar_ui.gd
extends HBoxContainer
var slots: Array[Node] = []
func _ready():
for i in range(4):
var slot = SkillSlotUI.new()
slot.slot_index = i
add_child(slot)
slots.append(slot)
func update_cooldowns(skill_manager: Node):
for i in range(4):
var skill_id = skill_manager.get_skill_id_by_slot(i)
if skill_id == "":
continue
var remaining = skill_manager.get_cooldown_remaining(skill_id)
var level = skill_manager.get_skill_level(skill_id)
slots[i].update_cooldown(remaining, level)
# skill_slot_ui.gd
extends PanelContainer
var slot_index: int
var icon: TextureRect
var cooldown_overlay: ColorRect
var cooldown_label: Label
var key_label: Label
func _ready():
custom_minimum_size = Vector2(50, 50)
icon = TextureRect.new()
icon.custom_minimum_size = Vector2(50, 50)
add_child(icon)
cooldown_overlay = ColorRect.new()
cooldown_overlay.color = Color(0, 0, 0, 0.6)
cooldown_overlay.custom_minimum_size = Vector2(50, 50)
add_child(cooldown_overlay)
cooldown_label = Label.new()
cooldown_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
cooldown_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
add_child(cooldown_label)
key_label = Label.new()
var keys = ["Q", "E", "R", "B"]
key_label.text = keys[slot_index]
add_child(key_label)
func update_cooldown(remaining: float, level: int):
if remaining > 0:
cooldown_overlay.visible = true
cooldown_label.text = "%.1f" % remaining
cooldown_label.visible = true
else:
cooldown_overlay.visible = false
cooldown_label.visible = false10.3 小地图
小地图是 MOBA 游戏最重要的 UI 元素之一,让玩家随时了解全局战况:
C#
// MinimapUI.cs
using Godot;
public partial class MinimapUI : Control
{
private SubViewport _minimapViewport;
private Camera3D _minimapCamera;
private Node3D _worldRoot;
// 小地图尺寸和世界尺寸的比例
private Vector2 _mapSize = new Vector2(100, 80);
private Vector2 _minimapSize = new Vector2(150, 120);
public override void _Ready()
{
CustomMinimumSize = _minimapSize;
// 创建小地图视口
_minimapViewport = new SubViewport();
_minimapViewport.Size = Vector2I((int)_minimapSize.X, (int)_minimapSize.Y);
AddChild(_minimapViewport);
// 小地图摄像机(正上方俯视)
_minimapCamera = new Camera3D();
_minimapCamera.Projection = Camera3D.ProjectionType.Orthographic;
_minimapCamera.Size = _mapSize.Y; // 正交相机覆盖整个地图
_minimapCamera.Position = new Vector3(0, 100, 0); // 正上方
_minimapCamera.LookAt(Vector3.Zero, Vector3.Forward);
_minimapViewport.AddChild(_minimapCamera);
}
// 在小地图上绘制标记点(英雄位置)
public override void _Draw()
{
foreach (var node in GetTree().GetNodesInGroup("heroes"))
{
var hero = node as Node3D;
if (hero == null) continue;
// 世界坐标转小地图坐标
var mapPos = WorldToMinimap(hero.GlobalPosition);
// 根据队伍颜色画点
int teamId = (int)hero.Get("team_id");
var color = teamId == 0 ? new Color(0.2f, 0.4f, 1.0f) : new Color(1.0f, 0.2f, 0.2f);
DrawCircle(mapPos, 4, color);
}
// 绘制防御塔标记
foreach (var node in GetTree().GetNodesInGroup("towers"))
{
var tower = node as Node3D;
if (tower == null) continue;
var mapPos = WorldToMinimap(tower.GlobalPosition);
int teamId = (int)tower.Get("team_id");
var color = teamId == 0 ? new Color(0.2f, 0.4f, 1.0f) : new Color(1.0f, 0.2f, 0.2f);
DrawRect(new Rect2(mapPos.X - 3, mapPos.Y - 3, 6, 6), color);
}
}
private Vector2 WorldToMinimap(Vector3 worldPos)
{
float x = (worldPos.X + _mapSize.X / 2) / _mapSize.X * _minimapSize.X;
float y = (worldPos.Z + _mapSize.Y / 2) / _mapSize.Y * _minimapSize.Y;
return new Vector2(x, y);
}
}GDScript
# minimap_ui.gd
extends Control
var minimap_viewport: SubViewport
var minimap_camera: Camera3D
var map_size := Vector2(100, 80)
var minimap_size := Vector2(150, 120)
func _ready():
custom_minimum_size = minimap_size
minimap_viewport = SubViewport.new()
minimap_viewport.size = Vector2i(int(minimap_size.x), int(minimap_size.y))
add_child(minimap_viewport)
minimap_camera = Camera3D.new()
minimap_camera.projection = Camera3D.PROJECTION_ORTHOGONAL
minimap_camera.size = map_size.y
minimap_camera.position = Vector3(0, 100, 0)
minimap_camera.look_at(Vector3.ZERO, Vector3.FORWARD)
minimap_viewport.add_child(minimap_camera)
func _draw():
# 绘制英雄标记
for node in get_tree().get_nodes_in_group("heroes"):
var hero = node as Node3D
if hero == null:
continue
var map_pos = world_to_minimap(hero.global_position)
var team_id: int = hero.get("team_id")
var color = Color(0.2, 0.4, 1.0) if team_id == 0 else Color(1.0, 0.2, 0.2)
draw_circle(map_pos, 4, color)
# 绘制防御塔标记
for node in get_tree().get_nodes_in_group("towers"):
var tower = node as Node3D
if tower == null:
continue
var map_pos = world_to_minimap(tower.global_position)
var team_id: int = tower.get("team_id")
var color = Color(0.2, 0.4, 1.0) if team_id == 0 else Color(1.0, 0.2, 0.2)
draw_rect(Rect2(map_pos.x - 3, map_pos.y - 3, 6, 6), color)
func world_to_minimap(world_pos: Vector3) -> Vector2:
var x = (world_pos.x + map_size.x / 2) / map_size.x * minimap_size.x
var y = (world_pos.z + map_size.y / 2) / map_size.y * minimap_size.y
return Vector2(x, y)10.4 击杀播报
当有英雄被击杀时,屏幕上方显示击杀信息:
[蓝方战士] 击杀了 [红方法师] ← 3秒后消失
[红方射手] 击杀了 [蓝方战士] ← 3秒后消失10.5 计分板
按 Tab 键显示计分板,查看双方数据:
┌──────────────────────────────────────────┐
│ 比赛进行 15:32 │
├──────────────┬──────────────────────────┤
│ 蓝方 12杀 │ 红方 8杀 │
├──────────────┼──────────────────────────┤
│ 战士 3/1/5 │ 战士 2/3/4 │
│ 法师 5/2/3 │ 法师 1/4/2 │
│ 射手 4/2/4 │ 射手 5/1/2 │
└──────────────┴──────────────────────────┘章节导航
| 上一章 | 下一章 |
|---|---|
| ← 9. 网络5v5对战 | 11. 打磨与发布 → |
