Making Merchant (Trading) System in Godot

trading mechanics in godot

Earlier in the inventory system tutorial, I explained how to build a modular inventory system. Inventory consisted of a bunch of slots carrying an item each. And it was visualized to player so player can interact with it using GUI inputs.

Merchant system extends the inventory. The purpose is to allow 2 items to be exchanged (or traded). For example, 500 wood can be brought is we pay 100 gold. It means there is 5:1 ratio of exchange. If player inserts 1 gold, he must get 5 wood in return.

Such a trading system can easily be made by arranging inventory slots in 2 columns, with the first column showing items and the other column showing the amount of gold required for that item. When player puts gold in the gold-slot, it can pick the item generated on the other slot. This system can be generalized to an “exchange” system so any 2 things can be exchanged this way.

Breakdown

  1. Make a scene; call it “Merchant System”. And add an inventory to it (do not use inheritance, use composition here).
  2. Write a MerchantSystem.gd script, and enforce underlying inventory to be arranged in 2 columns (so a pair forms for every item exchange). And other necessary things such as enforcing inventory to have item_hints for every slot.
  3. In the same script, attach the inventory signals so inventory input events are also captured by the merchant system.
    • Merchant system creates new items in slots if the other slot in pair gets an item in it.
    • It recalculates (or removes) the amount in other slot in the pair if the slot item is picked.

Merchant system scene setup

godot trading game tutorial

Merchant is a Control node with a script attached. The only other important nodes are those that are highlighted (see above image). Rest of the nodes are just for layout to make it look good, so you can ignore them.

The 1st CenterContainer is the node where player’s inventory will be attached. When player visits a merchant, player’s inventory must be shown so we can pick-and-drop items. So this nodes serves as the anchor point. We just have to reparent inventory root to be the child of this node.

The 2nd CenterContainer has already an inventory instance attached to it. This is the merchant inventory; responsible for actual exchange of items. This inventory is restricted in code to contain 2 columns; and rows as many as the number of items that are to be traded.

Initializing variables

Attach a script to MerchantSystem root. Our first goal is to store our important nodes in the variables.

extends Building
class_name Merchant

# Merchant columns must NEVER exceed 2 !!!!!!!!!!!!!!!!!!
# merchant_inventory.slots hold a list of slots, where index 0 and 1 are the first row,
# index 2 and 3 are the second row, and so on.
# each row has two slots, one for the item and one for the exchange item.
# the item slot is the item that the merchant is selling, and the exchange
# item is the item that is the price for the item.

# for example, if merchant sells apples and wants gold for them, the inventory
# would look like this:
# slot 0: apples, amount 50
# slot 1: gold, amount 10
# it would mean that the merchant sells 50 apples for 10 gold.

@export var inventory_item_scene: PackedScene

# List of all items that the merchant sells
@export var merchant_items: Array[PackedScene] = [] # Array[Item]
@export var merchant_inventory: Inventory

# Array of items such as gold, emrald (currency items).
@export var exchange_items: Array[PackedScene] = []


var exhange_pairs: Array = [] # Array[Array[InventorySlot]]


# When ship docks, its inventory will be made child of this node.
# Attach a test inventory here to visualize how it will appear
# It has no children by default
@export var player_inventory_parent: CenterContainer

See how our @export variables are shown in inspector (right-side panel).

trade system in godot tutorial

The variable exchange_pairs is used to store pairs of slots; for every row. We will later use it to calculate the amount generated in one of these slots for the amount placed by the user in other slot from the same pair (for example, you put gold in one slot and wood is generated in its pair slot).

Initializing the merchant inventory


# Called when the node enters the scene tree for the first time.
func _ready():
	super()
	
	assert(
		merchant_items.size() == merchant_inventory.rows,
		"Merchant inventory rows must be equal to merchant items size."
	)
	
	assert(
		exchange_items.size() > 0,
		"There must be at least one exchange item."
	)
	
	for p in range(merchant_inventory.rows):
		exhange_pairs.append(get_exchange_pair(p))
	

	fill_merchant_inventory()
	
	for pair in exhange_pairs:
		pair[0].slot_input.connect(_on_merchant_slot_input)
		pair[1].slot_input.connect(_on_merchant_slot_input)


# Fill the slots with items. Curently set the exhange_item to gold for all items.
func fill_merchant_inventory():
	for i in range(exhange_pairs.size()):
		var item = merchant_items[i].instantiate() # Must already provide the item amount
		var exchange_item = exchange_items.pick_random().instantiate()

		set_exchange_pair(i, item, exchange_item)


func get_exchange_pair(index: int) -> Array[InventorySlot]:
	var pair: Array[InventorySlot] = []
	if index < merchant_inventory.rows:
		# add row's first slot and second slot to the pair
		pair.append(merchant_inventory.slots[index * 2])
		pair.append(merchant_inventory.slots[index * 2 + 1])
	return pair


func set_exchange_pair(index: int, item: Item, exchange_item: Item):
	if index < merchant_inventory.rows:
		merchant_inventory.slots[index * 2].set_item_hint(item, 2)
		merchant_inventory.slots[index * 2 + 1].set_item_hint(exchange_item, 3)

The above code forces the inventory to have its rows equal to the items that are to be traded. it also fills the exchange_pairs array by getting both slots from each row and adding it to the exchange_pairs array. It calls get_exchange_pairs(row_index) to get the 2 slots from the row whose index is passed. 0 means first row, 1 means second row and so on.

After that, it fills the merchant inventory with the item hints. It loops over every pair in the exchange_pairs and for every pair, it calls set_exchange_item(row_index, item, exchange_item). This function assigns item to one slot and exchange_item (which is gold in most cases) to the other slot in the same pair.

One thing to note is that we have not assigned the amounts of both these items; it is because the InventoryItem class we created in inventory-tutorial already has amount attribute.

Actual exchange mechanics

Earlier you have noticed that we connected signals _on_merchant_slot_input to the signal of inventory system’s slots when they are pressed.

The reason why we have to connect this signal to the merchant system (even though inventory system already handles them) is because apart from what inventory does (selecting and de-selecting), our merchant system must also receive them to calculate the amount of other item generated or removed when item from an inventory slot is picked or thrown.

So we call a function whenever user does something with the underlying inventory:

func _on_merchant_slot_input(which: InventorySlot, action: InventorySlot.InventorySlotAction):
	update_merchant_slots.call_deferred(which) # On slot 'which' pressed


func update_merchant_slots(which: InventorySlot):
	var slot = which
	var other = get_pair_slot(slot)

	var slot_hint = slot.hint_item
	var other_hint = other.hint_item
	
	# 3 conditions:
	if not slot.item: # If user has taken it
		if other.item: other.item.queue_free() # then make other null as well
		other.item = null
	
	else: # means sither user has put item or taken chunk of item
		# update other slot amounts
		if not other.item: # If other slot is empty
			other.item = inventory_item_scene.instantiate() # Duplicate
			other.item.set_data(
				other_hint.item_name, other_hint.icon,
				other_hint.is_stackable, (
					round(((slot_hint.amount as float / other_hint.amount as float) * slot.item.amount as float)) as int
				)
			)
			other.update_slot()
		else:
			# Update the amount of other item based on the amount of slot_item
			other.item.amount = round(((slot_hint.amount as float / other_hint.amount as float) * slot.item.amount as float)) as int
			other.update_slot()


In above code, get_pair_slot(other) is used to get the other slot from the pair if we pass its sister slot (if we pass 0, we get 1, if we pass 1 we get 0; from the same pair). Its code is:

# Given a slot, return the other slot in the same row/pair
func get_pair_slot(other: InventorySlot) -> InventorySlot:
	var index = merchant_inventory.slots.find(other)
	if index % 2 == 0:
		return merchant_inventory.slots[index + 1]
	else:
		return merchant_inventory.slots[index - 1]

Interacting with the player inventory

When player visits the trader/merchant, its inventory must be attached on GUI panel so we can pick items from it or throw items on it. For this, earlier we created a CenterContainer and a variable to reference it.

Now when player visits the trader, this function will be called:

# Call it when player visits:
attach_player_inventory_here(true) # call when _on_visit or something..


# implementation looks like this:

var player: Node # Assuming player has inventory
var player_inventory_original_parent: Node # tmp variable


func attach_player_inventory_here(should: bool):
	if should:
		player_inventory_original_parent = player.inventory.get_parent()
		player.inventory.reparent(player_inventory_parent)
	else:
		player.inventory.reparent(player_inventory_original_parent)



func _on_leave_button_pressed():
	# TODO: add price fluctuaion here (add random number to hint_items-amount
	attach_player_inventory_here(false) # Detach inventory before leaving
	self.leave() # Make visible=false, set player reference to null... and so on

More info

In my case, Merchant extends the base Building, so some of the methods such as visit() and leave() are defined by the building. Building also takes reference o9f the player who is visiting them.

Since merchant extends the building, it inherits all of this & accessing player gets easier (we don’t have to define player in merchant system, since we already extend it). I did it for the sake of tutorial to make it understandable.

Inspiration

Originally, I wanted to make a Tradewinds-like trading system, but I found it more interesting to make Minecraft-like system for trading so my implementation is based more on how Minecraft trading works. Here are some images:

Minecraft trading mechanics
Tradewinds 2 marketplace trade system

Thank you for reading <3 If you have any questions, feel free to ask in the comments.

Leave a Reply

Your email address will not be published. Required fields are marked *