让我们通过制作 RPG 来学习 Godot 4 — 第 5 部分:设置游戏 GUI #1
在开始设置敌人和任务等有趣的部分之前,我们要完成游戏GUI的创建。GUI可以让我们直观地显示玩家统计数据的变化,例如他们的弹药和拾取量、健康和耐力值以及当前经验和等级。
图 10:GUI 布局计划。
我将GUI创建分为三个部分:健康和耐力;拾取物;以及经验和升级。
您将在本部分中学习到的内容:
· 如何向场景添加 UI 元素。
· 如何复制节点。
· 如何通过自定义信号更新 UI 元素。
· 如何改变节点的锚定。
· 如何创建、初始化和连接自定义信号。
在您的 Player Scene 中,添加一个新的CanvasLayer节点。此节点是屏幕上绘制图形的容器,例如进度条或标签。将UI元素添加到主场景,然后通过信号将这些元素与实例化的Player脚本连接起来以更新它们的值。
将其重命名为UI。
我们希望 UI 元素包含在屏幕上的蓝色边框内。我隐藏了现有节点,以便我们现在可以只关注 UI,所以如果您发现我不再有地图或玩家,请不要惊慌!
健康与耐力棒
让我们将ColorRect节点作为子节点添加到 UI 节点。这将绘制一个我们可以用颜色填充的矩形。此节点将作为我们健康值的进度条。
向新添加的节点添加另一个 ColorRect 节点。将第一个 ColorRect 重命名为 HealthBar,将第二个 ColorRect 重命名为 Value。外部矩形(HealthBar)将用作进度条的框或边框,内部矩形(Value)将是显示进度值的实际颜色。
现在,在 Inspector 面板的 Layout > Transform 下,将 HealthBar 节点大小更改为 x:100 和 y:10。对 Value 节点执行相同操作,但将其 x 更改为:98 和 y 更改为:8。
我们还希望将 Value 矩形置于 HealthBar 矩形的中心。为此,请将其锚点预设更改为中心。
将 HealthBar 的颜色更改为 #3a39356a,将 Value 的颜色更改为 #a4de79。在我的游戏中,HealthBar 为绿色,但您可以随意更改颜色。
让我们将生命值条锚定在屏幕的左下方。在 Inspector 面板的 Layout 下,将锚点预设从中心更改为左下方,并将其位置值更改为 x: 5, y: 155。
复制整个 HealthBar 节点(及其子节点值)并将其重命名为 StaminaBar。
将 StaminaBar 中的 Value 节点的颜色更改为 #377c9e。这会使其变成蓝色,但再次强调,这是您的游戏,因此您可以将其设置为任何您喜欢的颜色。
我们还将耐力条锚定在屏幕的左下角。在 Inspector 面板的 Layout 下,将锚点预设从中心更改为左下角,并将其位置值更改为 x: 5, y: 165。
现在我们已经设置了健康和耐力进度条!我们现在可以继续在代码中实现这些元素的功能。
在我们的 Player 脚本中,我们需要添加变量来捕获我们的健康值、最大健康值和健康恢复值,以及我们的耐力值。在 Player.gd 脚本的顶部,在is_attacking代码下方,让我们添加这些变量。
### Player.gd
# 旧代码
# UI 变量
var health = 100
var max_health = 100
var regen_health = 1
var stamina = 100
var max_stamina = 100
var regen_stamina = 5
创建变量后,我们需要为健康和体力创建自定义信号,以便游戏中的其他对象可以监听变化事件并做出反应,而无需直接相互引用。换句话说,这些信号将通知我们的游戏我们的健康或体力值发生了变化,然后触发其他事件。
如果我们的健康和耐力变量发生变化,我们希望向其发出信号,以便这些变化在我们的进度条中直观显示,并且我们的健康值得到更新。我们之前使用过信号,当时我们将内置的on_animation_finished()信号连接到我们的播放器脚本,但这次,我们将创建自己的自定义信号。
为什么要使用自定义信号?
虽然内置信号涵盖了许多常见用例(例如按钮点击或鼠标事件),但它们可能无法处理游戏或应用程序特有的所有特定交互或事件。自定义信号提供了一种定义特定于游戏逻辑的事件集的方法,使您的代码库更有条理、可重复使用且更易于维护。
现在,在创建自定义信号之前,让我们尝试了解何时需要发出这些信号并通知游戏我们的健康和体力值已发生变化。我们希望在按下冲刺输入时、以及在受到子弹伤害或稍后喝下健康药水时发出信号。
图 12:自定义信号概览
我们将把自定义信号添加到代码顶部,因为我们稍后会添加更多信号。这将使我们的所有信号保持整洁有序。要定义自定义信号,我们使用关键字signal后跟信号名称。我们的信号将更新我们的健康和耐力值,因此我们将它们称为 health_updated 和 stamina_updated。
### Player.gd
# 旧代码
# 自定义信号
signal health_updated
signal stamina_updated
现在我们已经创建了信号和变量,我们还需要不断检查它们,看看我们的健康或耐力值是否需要更新或重新生成。我们可以在_process()函数中进行此检查,该函数将在每次绘制帧时调用(每秒 60 次)。该函数有一个名为delta的参数,它表示自上一帧以来经过的时间。
对于每个健康和耐力,我们需要计算每个的更新值。我们将使用min()方法执行此操作,这将确保它永远不会超过我们的 max_health 和 max_stamina 变量的最大值。在这个 min() 函数中,我们将计算健康和耐力变量的更新值,并将其添加到当前帧中捕获的这些变量的值中。
### Player.gd
# older code
# ------------------------- UI --------------------------------------------------
func _process(delta):
#calculate health
var updated_health = min(health + regen_health * delta, max_health)
#calculate stamina
var updated_stamina = min(stamina + regen_stamina * delta, max_stamina)
如果这些值与我们的原始值(var 健康和耐力)不同,我们将把我们的健康更新为新值并发出信号通知游戏这一变化。
### Player.gd
# older code
# --------------- UI ---------------------------------
func _process(delta):
#regenerates health
var updated_health = min(health + regen_health * delta, max_health)
if updated_health != health:
health = updated_health
health_updated.emit(health, max_health)
#regenerates stamina
var updated_stamina = min(stamina + regen_stamina * delta, max_stamina)
if updated_stamina != stamina:
stamina = updated_stamina
stamina_updated.emit(stamina, max_stamina)
既然我们已经完成了,让我们更新一下冲刺输入动作,以便在按下时消耗一些体力。您会注意到,我们通过 . emit()方法发出信号。这会发出此信号,并且与此信号相关的所有可调用函数都将被触发。我们将在 UI 脚本中创建函数作为可调用函数,因此如果发出信号,此可调用函数将收到通知以更新我们的 UI 组件。
### Player.gd
# older code
# -------------------- Movement & Animations -------------
func _physics_process(delta):
# Get player input (left, right, up/down)
var direction: Vector2
direction.x = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left")
direction.y = Input.get_action_strength("ui_down") - Input.get_action_strength("ui_up")
# Normalize movement
if abs(direction.x) == 1 and abs(direction.y) == 1:
direction = direction.normalized()
# Sprinting
if Input.is_action_pressed("ui_sprint"):
if stamina >= 25:
speed = 100
stamina = stamina - 5
stamina_updated.emit(stamina, max_stamina)
elif Input.is_action_just_released("ui_sprint"):
speed = 50
# older code
您会注意到,我们将耐力变量作为参数传递到了信号中。由于我们将使用这些信号来更新 UI 中的进度条,因此在信号中传递 health/stamina 和 max_health/max_stamina 可以为 UI 组件提供必要的信息,以便准确灵活地显示玩家的耐力状态。它还可以确保游戏代码的一致性和效率。
现在我们需要将信号连接到 UI 组件(可调用函数),因此在您的 Player 场景中为 HealthBar 和 StaminaBar 添加一个新脚本,并将这些脚本保存在您的 GUI 文件夹下。将其中一个命名为 Health,将另一个命名为 Stamina。
在您新创建的 Health.gd 脚本中,让我们为 HealthBar/Value 节点的值创建一个@onready变量。
### Health.gd
extends ColorRect
# Node refs
@onready var value = $Value
然后,我们需要创建一个函数来更新 Value 节点的颜色值。我们可以将其宽度 (98) 乘以玩家健康值除以 max_health 来实现这一点。这将返回一个百分比值,该值将反映我们的 Value 节点的新宽度。
### Health.gd
extends ColorRect
# Node refs
@onready var value = $Value
# Updates UI
func update_health_ui(health, max_health):
value.size.x = 98 * health / max_health
对 Stamina.gd 中的耐力值执行相同操作。
### Stamina.gd
extends ColorRect
# Node refs
@onready var value = $Value
# Updates UI
func update_stamina_ui(stamina, max_stamina):
value.size.x = 98 * stamina / max_stamina
现在,我们可以将 UI 组件中的函数连接到内置_ready()函数中的信号。当 Player 节点进入主场景时,此函数会将我们的可调用函数连接到我们的信号 — 这样 UI 将能够在游戏加载时更新进度条。
何时使用 _ready()?
每当我们需要设置或初始化在节点及其子节点完全添加到场景后立即运行的代码时,我们都会使用 _ready() 函数。此函数在任何 _process() 或 _physics_process() 函数之前只会执行一次。
我们将通过connect关键字连接 Player 的 health_updated 信号,它将连接到的可调用函数是 HealthBar 的update_health_ui函数。这意味着每次我们的健康值发生变化时,玩家脚本都会发出信号,我们的健康栏将更新其值。我们将创建一个指向 HealthBar 和 StaminaBar 节点的节点引用,以便我们可以从其附加脚本访问这些函数。
### Player.gd
extends CharacterBody2D
# Node references
@onready var animation_sprite = $AnimatedSprite2D
@onready var health_bar = $UI/HealthBar
@onready var stamina_bar = $UI/StaminaBar
# older code
func _ready():
# Connect the signals to the UI components' functions
health_updated.connect(health_bar.update_health_ui)
stamina_updated.connect(stamina_bar.update_stamina_ui)
如果您现在运行您的场景并且冲刺,您会看到耐力条减少,同时又会再生!
让我们继续讨论 Pickups UI 的下一个 GUI 部分。记得保存并备份您的项目,下一部分见。