DEV Community

Cover image for Let’s Learn Godot 4 by Making an RPG — Part 21: Simple Shopkeeper🤠
christine
christine

Posted on • Updated on

Let’s Learn Godot 4 by Making an RPG — Part 21: Simple Shopkeeper🤠

We can’t finish off our RPG-Series without adding a shopkeeper to our game. We want our player to be able to buy ammo, health, and stamina pickups from our shopkeeper. This means our player does not constantly have to risk their lives to find ammo and consumables! Without any further diddle-daddling, let’s add a simple shopkeeper to our game!


WHAT YOU WILL LEARN IN THIS PART:

· How to crop animation frames in Sprite2D nodes.


Before we create our Shop-keeper scene, we need to first give our player some coins — plus update our NPC and Enemy scripts to give our player coins when they complete a quest or kill an enemy. In your Player script, define a new variable named “coins” and give it a value. I’m going to give my player 200 coins to start with.

    ### Player.gd

    # older code

    # Pickups
    var ammo_pickup = 13
    var health_pickup = 2
    var stamina_pickup = 2 
    var coins = 200
Enter fullscreen mode Exit fullscreen mode

We want this coin amount to be displayed in our UI, so let’s add a new UI element just for our coin amount. You can copy and paste your StaminaAmount element and rename it to “CoinAmount”.

Godot RPG

Change the CoinAmount icon to “coin_04d.png”.

Godot RPG

Then, change your CoinAmount ColorRect’s transform and anchor-preset properties to be as indicated in the image below. I’m showing you the properties via images to speed things up since you should know how to change these properties by now.

Godot RPG

Just like with our other UI components, let’s define a new signal, and attach a script to our CoinAmount node.

    ### Player.gd

    # older code

    # 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
    signal coins_updated
Enter fullscreen mode Exit fullscreen mode

Godot RPG

In our CoinAmount script, let’s create a function to update our UI value based on our coin amount. Then, in our Player script, we will connect this function to our signal.

    ### CoinAmount.gd

    extends ColorRect

    # Node ref
    @onready var value = $Value
    @onready var player = $"../.."

    # Show correct value on load
    func _ready():
        value.text = str(player.coins)

    # Update ui
    func update_coin_amount_ui(coin_amount):
        value.text = str(coin_amount)
Enter fullscreen mode Exit fullscreen mode
    ### 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 coin_amount = $UI/CoinAmount

    # 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)
        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)
        # Reset color
        animation_sprite.modulate = Color(1,1,1,1)
        Input.set_mouse_mode(Input.MOUSE_MODE_HIDDEN)
Enter fullscreen mode Exit fullscreen mode

Next, we’ll create a new function in our Player script that will emit the signal whenever our coin amount changes.

    ### Player

    # ---------------------- Consumables ------------------------------------------
    # older code

    # Add coins to inventory
    func add_coins(coins_amount):
        coins += coins_amount
        coins_updated.emit(coins)
Enter fullscreen mode Exit fullscreen mode

Then in our NPC and Enemy scripts, we will call this function whenever we complete a quest or kill the enemy. We’ll pass in the amount of coins that we want to reward the player with as a parameter.

    ### Enemy.gd

    #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 
            animation_sprite.play("death")
            #add xp values
            player.update_xp(70)
            player.add_coins(10)
            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("%s/PickupSpawner/SpawnedPickups" % Global.current_scene_name).call_deferred("add_child", pickup)
                pickup.position = position
Enter fullscreen mode Exit fullscreen mode
    ### NPC.gd

    #dialog tree    
    func dialog(response = ""):
        # Set our NPC's animation to "talk"
        animation_sprite.play("talk_down")

        # Set dialog_popup npc to be referencing this npc
        dialog_popup.npc = self
        dialog_popup.npc_name = str(npc_name)

        # dialog tree
        match quest_status:
            QuestStatus.NOT_STARTED:
                match dialog_state:
            # older code
            QuestStatus.STARTED:
                        match dialog_state:
                            0:
                                # Update dialog tree state
                                dialog_state = 1
                                # Show dialog popup
                                dialog_popup.message = "Found that book yet?"
                                if quest_complete:
                                    dialog_popup.response = "[A] Yes  [B] No"
                                else:
                                    dialog_popup.response = "[A] No"
                                dialog_popup.open()
                            1:
                                if quest_complete and response == "A":
                                    # Update dialog tree state
                                    dialog_state = 2
                                    # Show dialog popup
                                    dialog_popup.message = "Yeehaw! Now I can make cat-eye soup. Here, take this."
                                    dialog_popup.response = "[A] Bye"
                                    dialog_popup.open()
                                else:
                                    # Update dialog tree state
                                    dialog_state = 3
                                    # Show dialog popup
                                    dialog_popup.message = "I'm so hungry, please hurry..."
                                    dialog_popup.response = "[A] Bye"
                                    dialog_popup.open()
                            2:
                                # Update dialog tree state
                                dialog_state = 0
                                quest_status = QuestStatus.COMPLETED
                                # Close dialog popup
                                dialog_popup.close()
                                # Set NPC's animation back to "idle"
                                animation_sprite.play("idle_down")
                                # Add pickups and XP to the player. 
                                player.add_pickup(Global.Pickups.AMMO)
                                player.update_xp(50)
                                player.add_coins(20)
Enter fullscreen mode Exit fullscreen mode

Don’t forget to also save and load your coin data in your Player script.

    ### Player.gd

    #-------------------------------- Saving & Loading -----------------------
    #data to save
    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 
            }

    #loads data from saved data
    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

    #loads data from saved data
    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
        #update ui components to show correct loaded data   
        $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)
Enter fullscreen mode Exit fullscreen mode

If you run your scene now and you kill an enemy or complete a quest, your coin amount should update!

Godot RPG

With our player’s coins set up, we can go ahead and create our shopkeeper. Let’s create a new scene with a Node2D node as its root. We’re using this node because we won’t move this character around, so a CharacterBody2D node would be redundant. Rename this root as “ShopKeeper” and save the scene in your Scenes folder. Also, attach a script to it and save it in your Scripts folder.

Godot RPG

For this node, we want to have a simple Sprite2D that will show our shopkeeper’s body. In front of this body we want to have an Area2D node that if the player enters its body, the ShopMenu CanvasLayer will be displayed. The ShopMenu popup will contain a list of our pickups items that the player can buy for certain prices. Let’s add the following nodes:

Godot RPG

In your Assets directory, there is a folder called “NPC”. Assign the “NPC’s.png” image to your Sprite2D node.

Godot RPG

We want to crop out the first person in the second row (the man holding the beer). To do this, we need to change the HFrames, VFrames, and Frames values in our Animations property in the Inspector panel. The HFrames refer to horizontal frames. We can count 3 frames because there are 3 people per row, so its value should be three. The same should go for our VFrames. Then we just change our Frames value until we get to our beer-guy!

Godot RPG

Then, let’s add a rectangular collision shape to our Area2D node and move it in front of our shopkeeper.

Godot RPG

Now, here comes the work! UI creation is always the most tedious part of game development — well, for me at least. For our ShopMenu, we want three ColorRects to show the icon, label, and purchasing button for our Ammo, Health, and Stamina pickups. If we had a dynamic inventory (an inventory that changes item types), we’d be doing this via Lists and Boxes, but because we have a static inventory (an inventory that doesn’t change) that is composed of just 3 items, we’ll just go ahead and add a ColorRect, Label, Sprite2D, and Button node for each.

Add the following nodes (ColorRect > Label and 3 x ColorRect > Sprite2D > Label > Button) and rename them as indicated in the picture below.

Godot RPG

Then change your first ColorRect’s color to #581929, and change its anchor preset to “Full-Rect”.

Godot RPG

Then, change your Label nodes text to “SHOP”. Change its font size to 20, font to “Schrödinger”, and its font color to #2a0810. Change its transform and preset values to match that of the image below.

Godot RPG

Change your Ammo ColorRect’s color to #3f0f1b. Change its transform and preset values to match that of the image below. You can do the same for your Health and Stamina ColorRects.

Godot RPG

Godot RPG

Godot RPG

Then, change your Icon to the icon you chose for your Ammo on your Player’s UI. Change its transform and preset values to match that of the image below. You can do the same for your Health and Stamina Icons.

Godot RPG

Godot RPG

Godot RPG

Change your Ammo’s Label to be with the font “Schrödinger”, size 10, and font color #f2a6b2. Change its transform, text, and preset values to match that of the image below. You can do the same for your Health and Stamina Labels.

Godot RPG

Godot RPG

Godot RPG

Next, change your PurchaseAmmo button’s font to “Schrödinger”, size 10, and font color #77253a. Change its transform, text, and preset values to match that of the image below. You can do the same for your HealthPurchase and StaminaPurchase buttons.

Godot RPG

Godot RPG

Godot RPG

Now, to our main ColorRect, we need to add three new nodes: Sprite2D, Label, and Button. The Sprite2D and Label will show us our remaining coin value and the Button will allow us to close the popup.

Godot RPG

Change their values to match that of the images below. The Close node is the Button, the CoinAmount node is the Label (change its font color to #2a0810, with the font “Schrödinger”), and the Icon is our coin Sprite2D (choose “coin_03d.png” to be its texture).

Godot RPG

Godot RPG

Godot RPG

Your final UI for your ShopMenu popup should look like this:

Godot RPG

Now, connect each of your Button’s pressed() signal to your script. If we press these buttons, we will purchase our Pickup for each, and our coin amount should be updated. Our close button should hide the popup and unpause our game.

Godot RPG

Godot RPG

Also, connect your Area2D node’s body_entered() signal to your script. We will use this to show our popup and pause the game.

Godot RPG

We first need to get a reference to our player node since we want to update and check their coin amount, as well as call their add_pickup() function. We’ll also update the coin value returned in our popup in our process() function. In our ready() function, we will initialize our player reference and hide our screen to ensure that it is hidden when the shopkeeper enters the Main scene on game load.

    ###ShopKeeper.gd

    extends Node2D

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

    #player reference
    func _ready():
        shop_menu.visible = false

    #updates coin amount
    func _process(delta):
        $ShopMenu/ColorRect/CoinAmount.text = "Coins: " + str(player.coins)
Enter fullscreen mode Exit fullscreen mode

Then, we’ll open and close our “popup”. Remember to set the nodes visibility to hidden by default.

    ###ShopKeeper.gd

    extends Node2D

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

    #player reference
    func _ready():
        shop_menu.visible = false

    #updates coin amount
    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)
Enter fullscreen mode Exit fullscreen mode

We can also hide our menu in our Area2D node’s body_exited signal, which will ensure that the menu is disabled if we aren’t in the Area2D body. Also show/hide your cursor.

Godot RPG

    ###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

    #updates coin amount
    func _process(delta):
        $ShopMenu/ColorRect/CoinAmount.text = "Coins: " + str(player.coins)

    # Show Menu
    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)

    # Close Menu
    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)
Enter fullscreen mode Exit fullscreen mode

And then finally, we need to purchase our pickups only if our player has enough coins. You can set this value to be anything, or you could define a variable for each instead of making it a constant value as I did.

    ###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

    #updates coin amount
    func _process(delta):
        $ShopMenu/ColorRect/CoinAmount.text = "Coins: " + str(player.coins)

    #purhcases ammo at the cost of $10
    func _on_purchase_ammo_pressed():
        if player.coins >= 10:
            player.add_pickup(Global.Pickups.AMMO)
            player.coins -= 10
            player.add_coins(player.coins)

    #purhcases health at the cost of $5 
    func _on_purchase_health_pressed():
        if player.coins >= 5:
            player.add_pickup(Global.Pickups.HEALTH)
            player.coins -= 5
            player.add_coins(player.coins)

    #purhcases stamina at the cost of $2
    func _on_purchase_stamina_pressed():
        if player.coins >= 2:
            player.add_pickup(Global.Pickups.STAMINA)
            player.coins -= 2
            player.add_coins(player.coins)
Enter fullscreen mode Exit fullscreen mode

The last thing that we need to do is to change our ShopKeeper’s processing mode to Always because their popup must show when the game is paused, but the Area2D node must trigger the signal if the player runs into it when the game is not paused.

Godot RPG

We’ll also need to change our ShopMenu’s layer property to be 2 or higher. This will show the menu over our Player’s UI, as it is on a higher z-index. The z-index determines which element appears “on top” when multiple elements occupy the same space. Elements with a higher Z-index value are rendered on top of elements with a lower Z-index value.

Godot RPG

Instance your ShopKeeper in the Main scene. Now if you run your scene, and you run into your ShopKeeper, your menu should show, and you should be able to purchase some pickups. If you close your popup, the values should carry over into your Player’s HUD. Killing Enemies and completing quests should also increase your coin amount!

Godot RPG

Godot RPG

Godot RPG

There are many ways to implement a shopkeeper system, and many of them are a lot better than this, but this was the simplest way that worked for our game. In the next part, we will add music and SFX to our game. Remember to save your project, and I’ll see you in the next part!

The final source code for this part should look like this.

Buy Me a Coffee at ko-fi.com


FULL TUTORIAL

Godot RPG

The tutorial series has 23 chapters. I’ll be releasing all of the chapters in sectional daily parts over the next couple of weeks.

If you like this series or want to skip the wait and access the offline, full version of the tutorial series, you can support me by buying the offline booklet for just $4 on Ko-fi!😊

You can find the updated list of the tutorial links for all 23 parts in this series here.

Top comments (0)