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
- Make a scene; call it “Merchant System”. And add an inventory to it (do not use inheritance, use composition here).
- 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_hint
s for every slot. - 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
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).
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:
Thank you for reading <3 If you have any questions, feel free to ask in the comments.