让我们通过制作 RPG 来学习 Godot 4 — 第 16 部分:等级和 XP
现在我们有了基本的敌人和玩家,我们需要给自己一个杀死敌人的动机。我们可以通过升级系统来实现这一点,该系统在获得一定数量的 XP 后提高玩家的等级。如果玩家升级,他们会得到奖励,包括拾取物品和属性(健康、耐力)补充。我们还会在升级时增加他们的最大健康和最大耐力值。
您将在本部分中学习到的内容:
· 如何使用 Popup 节点。
· 如何暂停场景树。
· 如何允许/禁止输入处理。
· 如何改变节点的处理模式。
· 如何(可选)更改鼠标光标的图像和可见性。
升级概述
升级弹出窗口
准备就绪后,打开你的游戏项目,在你的播放器脚本中通过你的信号定义三个新信号,它们将更新我们的 xp、xp 要求和级别值。
### Player.gd
# Custom signals
signal health_updated
signal stamina_updated
signal ammo_pickups_updated
signal health_pickups_updated
signal stamina_pickups_updated
signal xp_updated
signal level_updated
signal xp_requirements_updated
接下来,我们需要创建这些信号在发出时将更新的变量。您可以将玩家的初始经验值、等级和所需经验值更改为您想要的任何值。
### Player.gd
# XP and levelling
var xp = 0
var level = 1
var xp_requirements = 100
如果您还记得我们在前面的部分如何更新健康和耐力 GUI 元素,您就会知道我们必须在玩家场景中的 XP 和 Level 元素中创建函数,然后将它们连接到玩家场景中的信号。
在您的 Player Scene 中,在您的 UI CanvasLayer 下方,将新脚本附加到您的 XP 和 Level ColorRect。确保将其保存在您的 GUI 文件夹下方。
我们的 XP ColorRect 也应该有两个值,一个用于我们的 XP,一个用于我们的 XP 要求,因此请继续复制您的 Value 节点。其变换值如下图所示。
我们想要从这些 ColorRects(而不是 Label)更新 Value 子节点,因此让我们继续为每个新脚本创建一个函数来更新我们的 XP 和 Level 值。
### XPAmount.gd
extends ColorRect
# Node refs
@onready var value = $Value
@onready var value2 = $Value2
#return xp
func update_xp_ui(xp):
#return something like 0
value.text = str(xp)
#return xp_requirements
func update_xp_requirements_ui(xp_requirements):
#return something like / 100
### LevelAmount.gd
extends ColorRect
# Node refs
@onready var value = $Value
# Return level
func update_level_ui(level):
#return something like 0
value.text = str(level)
现在我们只需将这些 UI 元素函数连接到我们的 Player 脚本中每个新创建的信号。
### Player.gd
extends CharacterBody2D
# Node references
@onready var animation_sprite = $AnimatedSprite2D
@onready var health_bar = $UI/HealthBar
@onready var stamina_bar = $UI/StaminaBar
@onready var ammo_amount = $UI/AmmoAmount
@onready var stamina_amount = $UI/StaminaAmount
@onready var health_amount = $UI/HealthAmount
@onready var xp_amount = $UI/XP
@onready var level_amount = $UI/Level
@onready var animation_player = $AnimationPlayer
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)
ammo_pickups_updated.connect(ammo_amount.update_ammo_pickup_ui)
health_pickups_updated.connect(health_amount.update_health_pickup_ui)
stamina_pickups_updated.connect(stamina_amount.update_stamina_pickup_ui)
xp_updated.connect(xp_amount.update_xp_ui)
xp_requirements_updated.connect(xp_amount.update_xp_requirements_ui)
level_updated.connect(level_amount.update_level_ui)
我们希望在杀死敌人后更新我们的 xp 数量,因此要做到这一点,我们需要在我们的 Player 脚本中创建一个新函数,它将更新我们的 xp 值并发出xp_updated信号。
### Player.gd
# older code
# ----------------- Level & XP ------------------------------
#updates player xp
func update_xp(value):
xp += value
#emit signals
xp_requirements_updated.emit(xp_requirements)
xp_updated.emit(xp)
level_updated.emit(level)
我们可以在任意需要更新 xp 的地方调用此函数,比如在我们的add_pickups()函数中,或者在敌人的damage()函数中判断其死亡。
### Player.gd
# older code
# ---------------------- Consumables ------------------------------------------
# Add the pickup to our GUI-based inventory
func add_pickup(item):
if item == Global.Pickups.AMMO:
ammo_pickup = ammo_pickup + 3 # + 3 bullets
ammo_pickups_updated.emit(ammo_pickup)
print("ammo val:" + str(ammo_pickup))
if item == Global.Pickups.HEALTH:
health_pickup = health_pickup + 1 # + 1 health drink
health_pickups_updated.emit(health_pickup)
print("health val:" + str(health_pickup))
if item == Global.Pickups.STAMINA:
stamina_pickup = stamina_pickup + 1 # + 1 stamina drink
stamina_pickups_updated.emit(stamina_pickup)
print("stamina val:" + str(stamina_pickup))
update_xp(5)
### Enemy.gd
# older code
#will damage the enemy when they get hit
func hit(damage):
health -= damage
if health > 0:
#damage
animation_player.play("damage")
else:
#death
#stop movement
timer_node.stop()
direction = Vector2.ZERO
#stop health regeneration
set_process(false)
#trigger animation finished signal
is_attacking = true
#Finally, we play the death animation and emit the signal for the spawner.
animation_sprite.play("death")
#add xp values
player.update_xp(70)
death.emit()
#drop loot randomly at a 90% chance
if rng.randf() < 0.9:
var pickup = Global.pickups_scene.instantiate()
pickup.item = rng.randi() % 3 #we have three pickups in our enum
get_tree().root.get_node("Main/PickupSpawner/SpawnedPickups").call_deferred("add_child", pickup)
pickup.position = position
在本部分中,我们将更多地构建此功能,因为我们仍然需要更新xp_requirements并运行检查以查看我们的玩家是否已获得足够的 xp 来升级。如果他们获得了足够的 xp,我们需要将当前 xp 数量重置回零,并增加玩家的级别和所需的 xp 值。然后我们需要发出信号来通知我们的游戏这些值的变化。
### Player.gd
# older code
# ----------------- Level & XP ------------------------------
#updates player xp
func update_xp(value):
xp += value
#check if player leveled up after reaching xp requirements
if xp >= xp_requirements:
#reset xp to 0
xp = 0
#increase the level and xp requirements
level += 1
xp_requirements *= 2
#emit signals
xp_requirements_updated.emit(xp_requirements)
xp_updated.emit(xp)
level_updated.emit(level)
如果您现在运行您的场景并杀死一些敌人(确保您将他们的伤害值改为零,以便他们无法在这次测试中杀死您),您将看到您的 xp 和等级值更新。
如果我们的玩家升级了,我们希望屏幕显示玩家升级的通知,以及他们升级后获得的奖励摘要。我们将通过向玩家的 UI 节点添加 CanvasLayer 节点来实现这一点。在您的玩家场景中,向您的 UI 层添加一个新的 CanvasLayer 节点。将其重命名为 LevelUpPopup。
在此 CanvasLayer 节点中,添加两个 ColorRects 和一个Button节点。第一个 ColorRect 将包含我们的升级标签,第二个 ColorRect 将包含我们的奖励摘要。该按钮将允许玩家确认通知并继续游戏。您可以按如下方式重命名它们:
将消息节点的颜色更改为 #d6c376,并将其大小更改为 (x:142,y:142);位置更改为 (x:4,y:4);anchor_preset 更改为 (中心)。
将 Rewards 节点拖到 Message 节点中,使其成为该节点的子节点。将其颜色更改为 #365655,并将其大小更改为 (x: 100, y: 75);位置更改为 (x: 20, y: 30);anchor_preset 更改为 (center)。
另外,将 Confirm 节点拖到 Message 节点中,使其成为该节点的子节点。将其颜色更改为 #365655,并将其大小更改为 (x: 100, y: 75);位置更改为 (x: 20, y: 30);anchor_preset 更改为 (center)。将其字体更改为“Schrodinger”,并将其文本更改为“CONTINUE”。
现在,在 Message 节点的顶部,让我们添加一个新的 Label 节点来显示我们的欢迎文本“Level Up!”。将其字体更改为“Arcade Classic”,字体大小更改为 15。然后更改其 anchor_preset (center-top)、水平对齐 (center)、垂直对齐 (center) 和位置 (y: 5)。
在您的奖励节点中,添加六个新的标签节点。
将其重命名如下:
更改其属性如下:
全部:
Text = “1”
Font = “Schrödinger”
Font-size = 10
Anchor Preset = center-top
Horizontal Alignment = center
Vertical Alignment = center
等级提升:
Position= y: 0
健康增加:
Position= y: 10
体力增加:
Position= y: 20
健康获得:
Position= y: 30
耐力提升获得:
Position= y:40
拾取弹药获得:
Position= y: 50
创建 Popup 后,我们现在可以继续隐藏 Popup。您可以在 Inspector 面板的 Canvas Item > Visibility 下执行此操作,或者只需单击节点旁边的眼睛图标即可将其隐藏。
我们需要返回Player 场景中的update_xp()函数来更新检查玩家是否升级的条件。如果他们正在升级,我们需要暂停游戏,显示包含所有奖励值的弹出窗口,并且仅在玩家单击确认按钮时隐藏弹出窗口。在这个函数中,我们必须提高玩家的最大生命值和耐力,并为他们提供一些弹药以及健康和耐力饮料。完成此操作后,我们需要在 UI 元素中反映这些变化。
如果我们想暂停游戏,只需使用SceneTree.paused方法。如果游戏暂停,则不会接受玩家或我们的任何输入,因为一切都暂停了。除非我们更改节点的处理模式。Godot 中的每个节点都有一个“处理模式”,用于定义其处理时间。可以在检查器中节点的Node属性下找到并更改它。
以下是每个模式告诉节点要做的事情:
- 继承:节点将根据父节点的状态工作或被处理。如果父节点的处理模式是可暂停的,则该节点也将是可暂停的,等等。
- 可暂停:仅当游戏未暂停时,节点才会工作或被处理。
- 暂停时:仅当游戏暂停时,节点才会工作或被处理。
- 始终:无论游戏是否暂停,节点都会工作或被处理。
- 已禁用:该节点将不起作用,也不会被处理。
我们需要 LevelUpPopup 节点仅在游戏暂停时工作。这样我们就可以单击“确认”按钮来取消游戏暂停,从而允许其他节点继续处理,因为它们只有在游戏处于未暂停状态时才工作或处理输入。让我们将 LevelUpPopup 的“处理模式”更改为“ WhenPaused”。您可以在“节点”>“处理”>“模式”下找到此选项。
因为我们更改了 LevelUpPopup 节点的处理模式,所以它的所有子节点也将继承该处理模式,因此当游戏暂停时,它们都将正常工作。在代码中暂停游戏之前,我们还需要首先允许通过set_process_input方法处理输入。此方法启用或禁用输入处理。然后我们将增加健康、耐力、经验值、等级和拾取值,并将这些更改反映在我们的 UI 上!让我们在代码中进行这些更改。
### Player.gd
extends CharacterBody2D
# Node references
@onready var animation_sprite = $AnimatedSprite2D
@onready var health_bar = $UI/HealthBar
@onready var stamina_bar = $UI/StaminaBar
@onready var ammo_amount = $UI/AmmoAmount
@onready var stamina_amount = $UI/StaminaAmount
@onready var health_amount = $UI/HealthAmount
@onready var xp_amount = $UI/XP
@onready var level_amount = $UI/Level
@onready var animation_player = $AnimationPlayer
@onready var level_popup = $UI/LevelPopup
# ----------------- Level & XP ------------------------------
#updates player xp
func update_xp(value):
xp += value
#check if player leveled up after reaching xp requirements
if xp >= xp_requirements:
#allows input
set_process_input(true)
#pause the game
get_tree().paused = true
#make popup visible
level_popup.visible = true
#reset xp to 0
xp = 0
#increase the level and xp requirements
level += 1
xp_requirements *= 2
#update their max health and stamina
max_health += 10
max_stamina += 10
#give the player some ammo and pickups
ammo_pickup += 10
health_pickup += 5
stamina_pickup += 3
#update signals for Label values
health_updated.emit(health, max_health)
stamina_updated.emit(stamina, max_stamina)
ammo_pickups_updated.emit(ammo_pickup)
health_pickups_updated.emit(health_pickup)
stamina_pickups_updated.emit(stamina_pickup)
xp_updated.emit(xp)
level_updated.emit(level)
#reflect changes in Label
$UI/LevelPopup/Message/Rewards/LevelGained.text = "LVL: " + str(level)
$UI/LevelPopup/Message/Rewards/HealthIncreaseGained.text = "+ MAX HP: " + str(max_health)
$UI/LevelPopup/Message/Rewards/StaminaIncreaseGained.text = "+ MAX SP: " + str(max_stamina)
$UI/LevelPopup/Message/Rewards/HealthPickupsGained.text = "+ HEALTH: 5"
$UI/LevelPopup/Message/Rewards/StaminaPickupsGained.text = "+ STAMINA: 3"
$UI/LevelPopup/Message/Rewards/AmmoPickupsGained.text = "+ AMMO: 10"
#emit signals
xp_requirements_updated.emit(xp_requirements)
xp_updated.emit(xp)
level_updated.emit(level)
最后,我们需要让确认按钮能够关闭弹出窗口并取消游戏暂停。我们可以将其pressed()信号连接到 Player 脚本来实现这一点。
在这个新创建的_on_confirm_pressed():函数中,我们将简单地再次隐藏弹出窗口并取消游戏暂停。
### Player.gd
# older code
# close popup
func _on_confirm_pressed():
level_popup.visible = false
get_tree().paused = false
现在,如果我们运行场景并射击足够多的敌人,我们将看到弹出窗口显示我们的奖励值,如果我们单击确认按钮,弹出窗口将关闭,然后我们可以用新的值继续游戏!
显示和隐藏光标
我不喜欢光标总是显示的方式。无论游戏是否暂停,我们的光标总是停留在屏幕上。由于这不是点击游戏,因此当游戏未暂停时,光标没有理由显示,因为我们会花时间四处奔跑和射击敌人。因此,我们的光标应该只在我们处于菜单屏幕(例如暂停或主菜单)时显示,甚至在我们的对话屏幕中显示 - 即当游戏暂停并且我们需要光标进行输入时。
幸运的是,这是一个快速解决方案。我们可以通过 Input 单例MouseMode方法更改鼠标光标的可见性。在我们的 Player 脚本中,我们将在游戏暂停时显示光标,如果没有暂停,我们将隐藏光标。
### Player.gd
# ----------------- Level & XP ------------------------------
#updates player xp
func update_xp(value):
xp += value
#check if player leveled up after reaching xp requirements
if xp >= xp_requirements:
#allows input
set_process_input(true)
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
#pause the game
#emit signals
xp_requirements_updated.emit(xp_requirements)
xp_updated.emit(xp)
level_updated.emit(level)
# close popup
func _on_confirm_pressed():
level_popup.visible = false
get_tree().paused = false
Input.set_mouse_mode(Input.MOUSE_MODE_HIDDEN)
我们还需要在加载时隐藏光标。
### Player.gd
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)
ammo_pickups_updated.connect(ammo_amount.update_ammo_pickup_ui)
health_pickups_updated.connect(health_amount.update_health_pickup_ui)
stamina_pickups_updated.connect(stamina_amount.update_stamina_pickup_ui)
xp_updated.connect(xp_amount.update_xp_ui)
xp_requirements_updated.connect(xp_amount.update_xp_requirements_ui)
level_updated.connect(level_amount.update_level_ui)
# Reset color
animation_sprite.modulate = Color(1,1,1,1)
Input.set_mouse_mode(Input.MOUSE_MODE_HIDDEN)
我们还可以更改鼠标光标的图像。如果您进入“项目设置”>“显示”>“鼠标光标”,则可以更改鼠标光标的图像。
您可以在此处找到免费的鼠标光标包。我使用了VOiD1 Gaming 的免费基本光标包。
现在,如果您运行游戏,当暂停状态改变时,光标应该被隐藏/显示。
加载显示经验值
如果我们更改变量的值并运行游戏,您可能会注意到 Level、XP 和 Pickup 值没有更新。我们需要通过进入 UI 脚本并在加载时从 Player 脚本调用我们的值来解决这个问题。
HealthAmount.gd
扩展了ColorRect
### HealthAmount.gd
extends ColorRect
# Node ref
@onready var value = $Value
@onready var player = $"../.."
# Show correct value on load
func _ready():
value.text = str(player.health_pickup)
# Update ui
func update_health_pickup_ui(health_pickup):
value.text = str(health_pickup)
### StaminaAmount.gd
extends ColorRect
# Node ref
@onready var value = $Value
@onready var player = $"../.."
# Show correct value on load
func _ready():
value.text = str(player.stamina_pickup)
# Update ui
func update_stamina_pickup_ui(stamina_pickup):
value.text = str(stamina_pickup)
### LevelAmount.gd
extends ColorRect
# Node refs
@onready var value = $Value
@onready var player = $"../.."
# On load
func _ready():
value.text = str(player.level)
# Return level
func update_level_ui(level):
#return something like 0
value.text = str(level)
### XPAmount.gd
extends ColorRect
# Node refs
@onready var value = $Value
@onready var value2 = $Value2
@onready var player = $"../.."
# On load
func _ready():
value.text = str(player.xp)
value2.text = "/" + str(player.xp_requirements)
#return xp
func update_xp_ui(xp):
#return something like 0
value.text = str(xp)
#return xp_requirements
func update_xp_requirements_ui(xp_requirements):
#return something like / 100
value2.text = "/" + str(xp_requirements)
### AmmoAmount.gd
extends ColorRect
# Node ref
@onready var value = $Value
@onready var player = $"../.."
# Show correct value on load
func _ready():
value.text = str(player.ammo_pickup)
# Update ui
func update_ammo_pickup_ui(ammo_pickup):
value.text = str(ammo_pickup)
现在,如果您运行场景,您的值应该正确显示,这在我们稍后加载游戏时很有用!
恭喜,您的玩家现在可以因杀死坏人而获得奖励。我们还没有 100% 完成,因为在下一部分中,我们将添加一个基本的 NPC 和任务,完成此任务后,玩家还会获得 XP 奖励。记得保存您的项目,下一部分见。