Let’s Learn Godot 4 by Making an RPG — Part 21: Simple Shopkeeper🤠
Tags: gamedev, beginners, programming, tutorial
Published: 2023-07-10 03:15:00


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.
```python  
    ### Player.gd
    
    # older code
    
    # Pickups
    var ammo_pickup = 13
    var health_pickup = 2
    var stamina_pickup = 2 
    var coins = 200
```
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](https://cdn-images-1.medium.com/max/2000/1*9Qxa5e1NiiVWewVZpUDsWw.png)

Change the CoinAmount icon to “coin_04d.png”.

![Godot RPG](https://cdn-images-1.medium.com/max/2000/1*0BB7mOQY5asGhpxWMLX05g.png)

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](https://cdn-images-1.medium.com/max/2000/1*HsaofFAGeWq7XZL7HPROjQ.png)

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

```python   
    ### 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
```
![Godot RPG](https://cdn-images-1.medium.com/max/2000/1*Poh05KBYsJBVr4Zrcuj0jQ.png)

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.
```python  
    ### 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)
```
```python  
    ### 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)
```
Next, we’ll create a new function in our Player script that will emit the signal whenever our coin amount changes.
```python  
    ### Player
    
    # ---------------------- Consumables ------------------------------------------
    # older code
    
    # Add coins to inventory
    func add_coins(coins_amount):
        coins += coins_amount
        coins_updated.emit(coins)
```
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.
```python  
    ### 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
```

```python  
    ### 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)
```
Don’t forget to also save and load your coin data in your Player script.
```python  
    ### 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)
```
If you run your scene now and you kill an enemy or complete a quest, your coin amount should update!

![Godot RPG](https://cdn-images-1.medium.com/max/3840/1*dRAvQNGXVmQi7aB-yMBdJQ.png)

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](https://cdn-images-1.medium.com/max/2000/1*VKKbkTGOkngciTHoBQ1T2g.png)

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](https://cdn-images-1.medium.com/max/2000/1*7DfBA3pgNmi1pQRMmPgFNw.png)

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

![Godot RPG](https://cdn-images-1.medium.com/max/2000/1*TFgOhQP6oMxKpUj5xm7DHQ.png)

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](https://docs.godotengine.org/en/stable/classes/class_sprite2d.html#class-sprite2d-property-hframes) 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](https://cdn-images-1.medium.com/max/2000/1*COwuc3RuvI_MLJihFYeGeA.png)

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

![Godot RPG](https://cdn-images-1.medium.com/max/2000/1*zcA3IF-rzK_p_jrkwhko0A.png)

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](https://docs.godotengine.org/en/stable/classes/class_itemlist.html), 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](https://cdn-images-1.medium.com/max/2000/1*9WMEvn2aXIGpWyWdx_XvLQ.png)

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

![Godot RPG](https://cdn-images-1.medium.com/max/3818/1*t6Bj6q2Srablpl_3v6R4mA.png)

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](https://cdn-images-1.medium.com/max/3784/1*XFxm6dsph1JhHDnw2zYMdQ.png)

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](https://cdn-images-1.medium.com/max/3830/1*negZt63RGGf3OBacW64afw.png)

![Godot RPG](https://cdn-images-1.medium.com/max/3818/1*DXhLCrtkyK9GqXJIEzDKpg.png)

![Godot RPG](https://cdn-images-1.medium.com/max/3818/1*DXhLCrtkyK9GqXJIEzDKpg.png)

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](https://cdn-images-1.medium.com/max/3834/1*7pe9qRthKk6by3uFr-adSQ.png)

![Godot RPG](https://cdn-images-1.medium.com/max/3834/1*7pe9qRthKk6by3uFr-adSQ.png)

![Godot RPG](https://cdn-images-1.medium.com/max/3840/1*XxEWyzHubsEg0_48c_yINw.png)

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](https://cdn-images-1.medium.com/max/3840/1*D4JgVmAZ9yqsahNFpuqg9A.png)

![Godot RPG](https://cdn-images-1.medium.com/max/3830/1*uCZNbpbF-Fr6OeBVGa372w.png)

![Godot RPG](https://cdn-images-1.medium.com/max/3840/1*vlD1x_aP7Yer4ZmV10wRTA.png)

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](https://cdn-images-1.medium.com/max/3832/1*xP1xoRkzswO2Ymz4gw0Q8Q.png)

![Godot RPG](https://cdn-images-1.medium.com/max/3838/1*-q840a7XH8fbDBPyUJBx5w.png)

![Godot RPG](https://cdn-images-1.medium.com/max/3828/1*sWoVUnNSmCovnzqgJpkTLQ.png)

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](https://cdn-images-1.medium.com/max/2000/1*gSlWAqMTRbXom0hgo2xfog.png)

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](https://cdn-images-1.medium.com/max/3834/1*8DNxIjuPas_gaUelLXjZZw.png)

![Godot RPG](https://cdn-images-1.medium.com/max/3838/1*J4eEJN9jfHq34ibrXexFqw.png)

![Godot RPG](https://cdn-images-1.medium.com/max/3838/1*L5iC5M8bEKl-cXiM7gYZkg.png)

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

![Godot RPG](https://cdn-images-1.medium.com/max/2000/1*EisrqgXopKPh8w-c4C-Xdg.png)

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](https://cdn-images-1.medium.com/max/2000/1*S6I36Eh5fYbmi9vLQxwiKA.png)

![Godot RPG](https://cdn-images-1.medium.com/max/2000/1*u_pF68qOFjGCgvPekpx9cQ.png)

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](https://cdn-images-1.medium.com/max/2000/1*Y6AEN7cxtUFBu9PcT59EMQ.png)

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.
```python  
    ###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)
```
Then, we’ll open and close our “popup”. Remember to set the nodes visibility to hidden by default.
```python  
    ###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)
```
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](https://cdn-images-1.medium.com/max/2000/1*F7rhbmOikUo1nxL_MPuGoQ.png)
```python  
    ###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)
```
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.
```python  
    ###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)
```
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](https://cdn-images-1.medium.com/max/2000/1*J4SJZ5blhwvWUAutseR-5A.png)

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](https://cdn-images-1.medium.com/max/2000/1*tQB8fpDtN1v7Jfo0PyIZNA.png)

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](https://cdn-images-1.medium.com/max/2000/1*Nz8OeMsRPdU5rmGGhza6xw.png)

![Godot RPG](https://cdn-images-1.medium.com/max/3840/1*8aHb4kdBf6VqEVYgcq09yg.png)

![Godot RPG](https://cdn-images-1.medium.com/max/3840/1*9sdJ_Wt2pCQjmUs3UUUGyw.png)

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.](https://github.com/christinec-dev/DustyTrails_RPG_Parts/tree/main/Part_21)

<a href='https://ko-fi.com/J3J2LP2U5' target='_blank'><img height='36' style='border:0px;height:36px;' src='https://storage.ko-fi.com/cdn/kofi2.png?v=3' border='0' alt='Buy Me a Coffee at ko-fi.com' /></a>

---
### FULL TUTORIAL

![Godot RPG](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pd9n6khb9jtaj0bynp7e.png)

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!](https://ko-fi.com/s/19c32424e5)😊

You can find the updated list of the tutorial links for all 23 parts in this series [here.](https://oops-i-devd.gitbook.io/oops-i-devd/godot-tutorials/dusty-trails/tutorial)