DEV Community

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

Posted on • Edited 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)