在完成我们的RPG系列之前,我们不能不添加一个商店老板到我们的游戏中。我们希望玩家能够从商店老板那里购买弹药、生命值和耐力值。这意味着玩家不必总是冒着生命危险去寻找弹药和消耗品!不再多说,让我们在游戏中添加一个简单的商店老板吧!

你将在这部分学到的内容:

  • 如何在Sprite2D节点中裁剪动画帧。

在创建商店老板场景之前,我们需要先给玩家一些金币——并更新我们的NPC和敌人脚本,以便在玩家完成任务或杀死敌人时给予他们金币。在你的玩家脚本中,定义一个名为“coins”的新变量,并给它一个初始值。我将给玩家200金币作为初始值。

Player.gd

# 旧代码

# 拾取物
var ammo_pickup = 13
var health_pickup = 2
var stamina_pickup = 2 
var coins = 200

我们希望这个金币数量显示在UI中,所以让我们为金币数量添加一个新的UI元素。你可以复制并粘贴你的StaminaAmount元素,并将其重命名为“CoinAmount”。
1.jpg
将CoinAmount图标更改为“coin_04d.png”。
2.jpg
然后,将CoinAmount的ColorRect的变换和锚点预设属性更改为下图所示。我通过图片展示这些属性以加快速度,因为你现在应该知道如何更改这些属性了。
3.jpg
就像我们的其他UI组件一样,让我们定义一个新的信号,并将脚本附加到我们的CoinAmount节点上。

Player.gd

# 旧代码

# 自定义信号
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
signal coins_updated

4.jpg
在我们的CoinAmount脚本中,让我们创建一个函数来根据金币数量更新UI值。然后,在玩家脚本中,我们将把这个函数连接到我们的信号。

CoinAmount.gd

extends ColorRect

# 节点引用
@onready var value = $Value
@onready var player = $"../.."

# 加载时显示正确的值
func _ready():
    value.text = str(player.coins)
    
# 更新UI
func update_coin_amount_ui(coin_amount):
    value.text = str(coin_amount)

Player.gd

extends CharacterBody2D

# 节点引用
@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 coin_amount = $UI/CoinAmount

# 旧代码

func _ready():
    # 将信号连接到UI组件的函数
    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)
    coins_updated.connect(coin_amount.update_coin_amount_ui)
    # 重置颜色
    animation_sprite.modulate = Color(1,1,1,1)
    Input.set_mouse_mode(Input.MOUSE_MODE_HIDDEN)

接下来,我们将在玩家脚本中创建一个新函数,每当金币数量发生变化时发出信号。

Player.gd

# ---------------------- 消耗品 ------------------------------------------
# 旧代码

# 添加金币到库存
func add_coins(coins_amount):
    coins += coins_amount
    coins_updated.emit(coins)

然后,在我们的NPC和敌人脚本中,我们将在完成任务或杀死敌人时调用此函数。我们将传入我们想要奖励玩家的金币数量作为参数。

Enemy.gd

# 当敌人受到攻击时会受到伤害
func hit(damage):
    health -= damage
    if health > 0:
        # 伤害
        animation_player.play("damage")
    else:
        # 死亡
        # 停止移动
        timer_node.stop()
        direction = Vector2.ZERO
        # 停止生命值恢复
        set_process(false)
        # 触发动画完成信号
        is_attacking = true     
        # 最后,播放死亡动画
        animation_sprite.play("death")
        # 添加经验值
        player.update_xp(70)
        player.add_coins(10)
        death.emit()
        # 以90%的几率随机掉落战利品
        if rng.randf() < 0.9:
            var pickup = Global.pickups_scene.instantiate()
            pickup.item = rng.randi() % 3 # 我们的枚举中有三个拾取物
            get_tree().root.get_node("%s/PickupSpawner/SpawnedPickups" % Global.current_scene_name).call_deferred("add_child", pickup)
            pickup.position = position

NPC.gd

# 对话树    
func dialog(response = ""):
    # 将NPC的动画设置为“talk”
    animation_sprite.play("talk_down")
    
    # 设置dialog_popup的npc引用为此npc
    dialog_popup.npc = self
    dialog_popup.npc_name = str(npc_name)
    
    # 对话树
    match quest_status:
        QuestStatus.NOT_STARTED:
            match dialog_state:
        # 旧代码
        QuestStatus.STARTED:
                    match dialog_state:
                        0:
                            # 更新对话树状态
                            dialog_state = 1
                            # 显示对话弹出窗口
                            dialog_popup.message = "找到那本书了吗?"
                            if quest_complete:
                                dialog_popup.response = "[A] 是  [B] 否"
                            else:
                                dialog_popup.response = "[A] 否"
                            dialog_popup.open()
                        1:
                            if quest_complete and response == "A":
                                # 更新对话树状态
                                dialog_state = 2
                                # 显示对话弹出窗口
                                dialog_popup.message = "耶!现在我可以做猫眼汤了。给你,拿着这个。"
                                dialog_popup.response = "[A] 再见"
                                dialog_popup.open()
                            else:
                                # 更新对话树状态
                                dialog_state = 3
                                # 显示对话弹出窗口
                                dialog_popup.message = "我太饿了,请快点..."
                                dialog_popup.response = "[A] 再见"
                                dialog_popup.open()
                        2:
                            # 更新对话树状态
                            dialog_state = 0
                            quest_status = QuestStatus.COMPLETED
                            # 关闭对话弹出窗口
                            dialog_popup.close()
                            # 将NPC的动画设置回“idle”
                            animation_sprite.play("idle_down")
                            # 给玩家添加拾取物和经验值
                            player.add_pickup(Global.Pickups.AMMO)
                            player.update_xp(50)
                            player.add_coins(20)

别忘了在你的玩家脚本中保存和加载金币数据。

Player.gd

#-------------------------------- 保存与加载 -----------------------
# 要保存的数据
func data_to_save():
    return {
        "position" : [position.x, position.y],
        "health" : health,
        "max_health" : max_health,
        "stamina" : stamina,
        "max_stamina" : max_stamina,
        "xp" : xp,
        "xp_requirements" : xp_requirements,
        "level" : level,
        "ammo_pickup" : ammo_pickup,
        "health_pickup" : health_pickup,
        "stamina_pickup" : stamina_pickup,
        "coins" : coins 
        }

# 从保存的数据中加载数据
func data_to_load(data):
    position = Vector2(data.position[0], data.position[1])
    health = data.health
    max_health = data.max_health
    stamina = data.stamina
    max_stamina = data.max_stamina
    xp = data.xp
    xp_requirements = data.xp_requirements
    level = data.level
    ammo_pickup = data.ammo_pickup
    health_pickup = data.health_pickup
    stamina_pickup = data.stamina_pickup
    coins = data.coins
    
# 从保存的数据中加载数据
func values_to_load(data):
    health = data.health
    max_health = data.max_health
    stamina = data.stamina
    max_stamina = data.max_stamina
    xp = data.xp
    xp_requirements = data.xp_requirements
    level = data.level
    ammo_pickup = data.ammo_pickup
    health_pickup = data.health_pickup
    stamina_pickup = data.stamina_pickup    
    coins = data.coins
    # 更新UI组件以显示正确的加载数据   
    $UI/AmmoAmount/Value.text = str(data.ammo_pickup)
    $UI/StaminaAmount/Value.text =  str(data.stamina_pickup)
    $UI/HealthAmount/Value.text =  str(data.health_pickup)
    $UI/XP/Value.text =  str(data.xp)
    $UI/XP/Value2.text =  "/ " + str(data.xp_requirements)
    $UI/Level/Value.text = str(data.level)
    $UI/CoinAmount/Value.text = str(data.coins)

如果你现在运行场景并杀死敌人或完成任务,你的金币数量应该会更新!
5.jpg
有了玩家的金币设置,我们可以继续创建我们的商店老板。让我们创建一个以Node2D节点为根的新场景。我们使用这个节点是因为我们不会移动这个角色,所以CharacterBody2D节点将是多余的。将此根重命名为“ShopKeeper”并将场景保存在你的Scenes文件夹中。同时,附加一个脚本并将其保存在你的Scripts文件夹中。
6.jpg
对于这个节点,我们希望有一个简单的Sprite2D来显示商店老板的身体。在这个身体前面,我们希望有一个Area2D节点,如果玩家进入它的身体,将显示ShopMenu CanvasLayer。ShopMenu弹出窗口将包含玩家可以以特定价格购买的拾取物列表。让我们添加以下节点:
7.jpg
在你的Assets目录中,有一个名为“NPC”的文件夹。将“NPC’s.png”图像分配给你的Sprite2D节点。
8.jpg
我们希望裁剪出第二行中的第一个人(拿着啤酒的男人)。为此,我们需要在Inspector面板中的Animations属性中更改HFrames、VFrames和Frames值。HFrames指的是水平帧。我们可以数出3帧,因为每行有3个人,所以它的值应该是3。VFrames也是如此。然后我们只需更改Frames值,直到我们找到拿着啤酒的男人!
9.jpg
然后,让我们为Area2D节点添加一个矩形碰撞形状,并将其移动到商店老板的前面。
10.jpg
现在,工作来了!UI创建始终是游戏开发中最繁琐的部分——至少对我来说是这样。对于我们的ShopMenu,我们希望有三个ColorRects来显示弹药、生命值和耐力值的图标、标签和购买按钮。如果我们有一个动态库存(一个会改变物品类型的库存),我们会通过列表和盒子来实现这一点,但由于我们有一个静态库存(一个不会改变的库存),它只由3个物品组成,我们将直接为每个物品添加一个ColorRect、Label、Sprite2D和Button节点。

添加以下节点(ColorRect > Label 和 3 x ColorRect > Sprite2D > Label > Button)并按照下图所示重命名它们。
11.jpg
然后,将第一个ColorRect的颜色更改为#581929,并将其锚点预设更改为“Full-Rect”。
12.jpg
然后,将Label节点的文本更改为“SHOP”。将其字体大小更改为20,字体为“Schrödinger”,字体颜色为#2a0810。将其变换和预设值更改为与下图匹配。
13.jpg
将Ammo ColorRect的颜色更改为#3f0f1b。将其变换和预设值更改为与下图匹配。你可以对Health和Stamina ColorRects执行相同的操作。
14.jpg
15.jpg
16.jpg
然后,将你的图标更改为你在玩家UI中选择的弹药图标。将其变换和预设值更改为与下图匹配。你可以对Health和Stamina图标执行相同的操作。
17.jpg
18.jpg
19.jpg
将Ammo的Label字体更改为“Schrödinger”,大小为10,字体颜色为#f2a6b2。将其变换、文本和预设值更改为与下图匹配。你可以对Health和Stamina标签执行相同的操作。
20.jpg
21.jpg
22.jpg
接下来,将PurchaseAmmo按钮的字体更改为“Schrödinger”,大小为10,字体颜色为#77253a。将其变换、文本和预设值更改为与下图匹配。你可以对HealthPurchase和StaminaPurchase按钮执行相同的操作。
23.jpg
24.jpg
25.jpg
现在,对于我们的主ColorRect,我们需要添加三个新节点:Sprite2D、Label和Button。Sprite2D和Label将显示我们剩余的金币值,Button将允许我们关闭弹出窗口。
26.jpg
将它们的值更改为与下图匹配。Close节点是Button,CoinAmount节点是Label(将其字体颜色更改为#2a0810,字体为“Schrödinger”),Icon是我们的金币Sprite2D(选择“coin_03d.png”作为其纹理)。
27.jpg
28.jpg
29.jpg
你的ShopMenu弹出窗口的最终UI应该如下所示:
30.jpg
现在,将每个Button的pressed()信号连接到你的脚本。如果我们按下这些按钮,我们将为每个拾取物购买,并且我们的金币数量应该更新。我们的关闭按钮应该隐藏弹出窗口并取消暂停游戏。
31.jpg
32.jpg
同时,将你的Area2D节点的body_entered()信号连接到你的脚本。我们将使用它来显示弹出窗口并暂停游戏。
33.jpg
我们首先需要获取玩家节点的引用,因为我们想要更新和检查他们的金币数量,并调用他们的add_pickup()函数。我们还将在process()函数中更新弹出窗口中返回的金币值。在ready()函数中,我们将初始化玩家引用并隐藏屏幕,以确保在游戏加载时商店老板进入主场景时它是隐藏的。

ShopKeeper.gd

extends Node2D

@onready var player = get_tree().root.get_node("Main/Player")
@onready var shop_menu = $ShopMenu

# 玩家引用
func _ready():
    shop_menu.visible = false

# 更新金币数量
func _process(delta):
    $ShopMenu/ColorRect/CoinAmount.text = "Coins: " + str(player.coins)

然后,我们将打开和关闭我们的“弹出窗口”。记得默认将节点的可见性设置为隐藏。

ShopKeeper.gd

extends Node2D

@onready var player = get_tree().root.get_node("Main/Player")
@onready var shop_menu = $ShopMenu

# 玩家引用
func _ready():
    shop_menu.visible = false

# 更新金币数量
func _process(delta):
    $ShopMenu/ColorRect/CoinAmount.text = "Coins: " + str(player.coins)

func _on_close_pressed():
    shop_menu.visible = false
    get_tree().paused = false
    set_process_input(false)
    player.set_physics_process(true)

func _on_area_2d_body_entered(body):
    if body.is_in_group("player"):
        shop_menu.visible = true
        get_tree().paused = true
        set_process_input(true)
        player.set_physics_process(false)

我们还可以在Area2D节点的body_exited信号中隐藏我们的菜单,这将确保如果我们不在Area2D体内,菜单将被禁用。同时显示/隐藏你的光标。
34.jpg

ShopKeeper.gd

extends Node2D

@onready var player = get_tree().root.get_node("Main/Player")
@onready var shop_menu = $ShopMenu

func _ready():
    shop_menu.visible = false

# 更新金币数量
func _process(delta):
    $ShopMenu/ColorRect/CoinAmount.text = "Coins: " + str(player.coins)

# 显示菜单
func _on_area_2d_body_entered(body):
    if body.is_in_group("player"):
        shop_menu.visible = true
        get_tree().paused = true
        set_process_input(true)
        player.set_physics_process(false)
        Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
        
# 关闭菜单
func _on_close_pressed():
    shop_menu.visible = false
    get_tree().paused = false
    set_process_input(false)
    player.set_physics_process(true)
    Input.set_mouse_mode(Input.MOUSE_MODE_HIDDEN)

func _on_area_2d_body_exited(body):
    if body.is_in_group("player"):
        shop_menu.visible = false
        get_tree().paused = false
        set_process_input(false)
        player.set_physics_process(true)
        Input.set_mouse_mode(Input.MOUSE_MODE_HIDDEN)

最后,我们需要仅在玩家有足够金币时购买拾取物。你可以将此值设置为任何值,或者你可以为每个拾取物定义一个变量,而不是像我一样将其设置为常量值。

ShopKeeper.gd

extends Node2D

@onready var player = get_tree().root.get_node("Main/Player")
@onready var shop_menu = $ShopMenu

func _ready():
    shop_menu.visible = false

# 更新金币数量
func _process(delta):
    $ShopMenu/ColorRect/CoinAmount.text = "金币: " + str(player.coins)

# 花费10金币购买弹药
func _on_purchase_ammo_pressed():
    if player.coins >= 10:
        player.add_pickup(Global.Pickups.AMMO)
        player.coins -= 10
        player.add_coins(player.coins)

# 花费5金币购买生命值
func _on_purchase_health_pressed():
    if player.coins >= 5:
        player.add_pickup(Global.Pickups.HEALTH)
        player.coins -= 5
        player.add_coins(player.coins)

# 花费2金币购买耐力
func _on_purchase_stamina_pressed():
    if player.coins >= 2:
        player.add_pickup(Global.Pickups.STAMINA)
        player.coins -= 2
        player.add_coins(player.coins)

我们需要做的最后一件事是将 ShopKeeper 的处理模式更改为 Always,因为当游戏暂停时,它的弹出菜单必须显示出来,但 Area2D 节点必须在游戏未暂停时触发信号。
35.jpg
我们还需要将 ShopMenu 的图层属性更改为 2 或更高。这将使菜单显示在玩家的 UI 之上,因为它位于更高的 z-index 上。z-index 决定了当多个元素占据同一空间时,哪个元素显示在“顶部”。z-index 值较高的元素会渲染在 z-index 值较低的元素之上。
36.jpg
Main 场景中实例化你的 ShopKeeper。现在如果你运行场景,并且碰到 ShopKeeper,你的菜单应该会显示出来,并且你应该能够购买一些道具。如果你关闭弹出窗口,这些值应该会传递到玩家的 HUD 中。杀死敌人和完成任务也应该会增加你的金币数量!
37.jpg
38.jpg
39.jpg
实现商店系统的方法有很多,其中许多方法比这个更好,但这是对我们游戏来说最简单的方式。在下一部分中,我们将为游戏添加音乐和音效。记得保存你的项目,我们下一部分见!

标签: 2D游戏, 游戏开发, 游戏自学

添加新评论