Let’s Learn Godot 4 by Making an RPG — Part 21: Simple Shopkeeper🤠
christine
Posted on July 10, 2023
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
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”.
Change the CoinAmount icon to “coin_04d.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.
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
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)
### 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.
### 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.
### 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
### 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.
### 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!
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.
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:
In your Assets directory, there is a folder called “NPC”. Assign the “NPC’s.png” image to your Sprite2D node.
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!
Then, let’s add a rectangular collision shape to our Area2D node and move it in front of our shopkeeper.
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.
Then change your first ColorRect’s color to #581929, and change its anchor preset to “Full-Rect”.
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.
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.
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.
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.
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.
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.
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).
Your final UI for your ShopMenu popup should look like this:
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.
Also, connect your Area2D node’s body_entered() signal to your script. We will use this to show our popup and pause the game.
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)
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)
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.
###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.
###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.
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.
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!
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.
FULL TUTORIAL
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.
Posted on July 10, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
August 9, 2023