So i’m trying to make an inventory system, and after following a tutorial on youtube i’ve got a good basis (I hope), but i’m finding it a bit difficult to customize the general Inventory layout. Right now the inventory consists of:
Panel container, with a margin container child, and a grid container that is a child of the margin container. The code then populated the item grid with a “slot” scene in up to 6 columns until it hit’s the limit i’ve set.
I’m trying to just add a simple gap between the first Row and the 2nd row to differentiate between the hotbar items and the rest of the inventory and having trouble with such. I’m wondering if this was even the best way to make an inventory system or if there is just a simple bit of code I can utilize to add this gap and i’m oblivious to it. Thanks for any feedback
The code of the inventory in question:
extends PanelContainer
const Slot = preload("res://Scenes/Inventory/slot.tscn")
@onready var item_grid = $MarginContainer/ItemGrid
func set_inventory_data(inventory_data: InventoryData):
inventory_data.inventory_updated.connect(populate_item_grid)
populate_item_grid(inventory_data)
func clear_inventory_data(inventory_data: InventoryData):
inventory_data.inventory_updated.disconnect(populate_item_grid)
#populates the slot data for the amount of items in the "test_inv"array
func populate_item_grid(inventory_data: InventoryData) -> void:
for child in item_grid.get_children():
child.queue_free() #clears item grid of any children it has so function can be used
for slot_data in inventory_data.slot_datas:
var slot = Slot.instantiate()
item_grid.add_child(slot)
slot.slot_clicked.connect(inventory_data.on_slot_clicked)
if slot_data != null:
slot.set_slot_data(slot_data)
You could insert 6 empty Control nodes at position 7-12 and set their custom_minimum_size to enforce a certain height, but the cleaner solution would be to restructure your scene like this:
MarginContainer
VBoxContainer
HBoxContainer
GridContainer
The bold nodes are new additions. The HBoxContainer represents your hotbar, while setting the separation property of the VBoxContainer allows you to set the size of the gap.
Now that 2nd options seems to be going in the right direction, i added the vbox and hbox and then change the code to this:
extends PanelContainer
const Slot = preload("res://Scenes/Inventory/slot.tscn")
#@onready var item_grid = $MarginContainer/ItemGrid
@onready var hotbar_grid = $MarginContainer/VBoxContainer/HotbarGrid
@onready var item_grid = $MarginContainer/VBoxContainer/ItemGrid
func set_inventory_data(inventory_data: InventoryData):
inventory_data.inventory_updated.connect(populate_item_grid)
populate_item_grid(inventory_data)
func clear_inventory_data(inventory_data: InventoryData):
inventory_data.inventory_updated.disconnect(populate_item_grid)
#populates the slot data for the amount of items in the "test_inv"array
func populate_item_grid(inventory_data: InventoryData) -> void:
for child in item_grid.get_children():
child.queue_free() #clears item grid of any children it has so function can be used
for child in hotbar_grid.get_children():
child.queue_free()
var index = 0
for slot_data in inventory_data.slot_datas:
var slot = Slot.instantiate()
if index < 6:
hotbar_grid.add_child(slot)
else:
item_grid.add_child(slot)
slot.slot_clicked.connect(inventory_data.on_slot_clicked)
if slot_data != null:
slot.set_slot_data(slot_data)
index += 1
However now for some reason, the populated slots in the hotbar work fine but in the item_grid when i click on anything in there, it clicks on 1 tile above to grab items. I’m wondering now if utilizing a panel container as the inventory slot wasn’t a great idea and if i should’ve just used a button
Huh, that’s strange. I don’t see anything wrong with your code that would explain that behavior. Can you share the code for inventory_data.on_slot_clicked?
Yeah that’s what I was thinking as well because it worked before with no issues, but certainly here is that code:
extends Resource
class_name InventoryData
signal inventory_updated(inventory_data: InventoryData)
signal inventory_interact(inventory_data: InventoryData, index: int, button: int)
@export var slot_datas: Array[SlotData]
func grab_slot_data(index: int) -> SlotData:
var slot_data = slot_datas[index]
if slot_data:
slot_datas[index] = null
inventory_updated.emit(self)
return slot_data
else:
return null
func drop_slot_data(grabbed_slot_data: SlotData, index: int) -> SlotData:
var slot_data = slot_datas[index]
var return_slot_data: SlotData
if slot_data and slot_data.can_fully_merge_with(grabbed_slot_data):
slot_data.fully_merge_with(grabbed_slot_data)
else:
slot_datas[index] = grabbed_slot_data
return_slot_data = slot_data
inventory_updated.emit(self)
return return_slot_data
func drop_single_slot_data(grabbed_slot_data: SlotData, index: int) -> SlotData:
var slot_data = slot_datas[index]
if not slot_data:
slot_datas[index] = grabbed_slot_data.create_single_slot_data()
elif slot_data.can_merge_with(grabbed_slot_data):
slot_data.fully_merge_with(grabbed_slot_data.create_single_slot_data())
inventory_updated.emit(self)
if grabbed_slot_data.quantity > 0:
return grabbed_slot_data
else:
return null
#decriments item quantity if it is Usable and right clicked on
func use_slot_data(index: int):
var slot_data = slot_datas[index]
if not slot_data:
return
if slot_data.item_data is ItemDataConsumable:
slot_data.quantity -= 1
if slot_data.quantity < 1:
slot_datas[index]= null
print(slot_data.item_data.name)
PlayerManager.use_slot_data(slot_data)
inventory_updated.emit(self)
#if items are stackable and is being picked up, it will add to existing stack and if there isn't take empty slot
func pick_up_slot_data(slot_data: SlotData) -> bool:
for index in slot_datas.size():
if slot_datas[index] and slot_datas[index].can_fully_merge_with(slot_data):
slot_datas[index].fully_merge_with(slot_data)
inventory_updated.emit(self)
return true
#sends true signal to pickup, as well as puts item in slot if there is free room or leaves it
for index in slot_datas.size():
if not slot_datas[index]:
slot_datas[index] = slot_data
inventory_updated.emit(self)
return true
return false
func on_slot_clicked(index: int, button: int):
inventory_interact.emit(self, index, button)
I believe that may be coming from the slot script in here
extends PanelContainer
signal slot_clicked(index: int, button: int)
@onready var texture_rect = $MarginContainer/TextureRect
@onready var quantity_label = $QuantityLabel
func set_slot_data(slot_data: SlotData):
var item_data = slot_data.item_data
texture_rect.texture = item_data.texture
tooltip_text = "%s\n%s" % [item_data.name, item_data.description]
if slot_data.quantity > 1:
quantity_label.text = "x%s" % slot_data.quantity
quantity_label.show()
else:
quantity_label.hide()
func _on_gui_input(event: InputEvent):
if event is InputEventMouseButton\
and (event.button_index == MOUSE_BUTTON_LEFT\
or event.button_index == MOUSE_BUTTON_RIGHT)\
and event.is_pressed():
slot_clicked.emit(get_index(), event.button_index)
However, as I said I followed a tutorial on youtube that I don’t think was the greatest at explaining things, but figured i’d go back and try to figure my way through it afterwards and try to make it more understandable to myself which is what i was doing now, some methods just felt very obtuse with how he went about it but that is most likely just how I learn things
I see. So the slot is using get_index() to get its own position in the parent container. So for the items in your hotbar it will be 0, 1, 2, 3, 4 or 5 and for the items in your grid it will be also 0, 1, 2, 3, 4, 5, … Now I guess whatever connects to inventory_interact will use this number without the context of which container it originated from. Can you show me the function(s) inventory_interact is connected to?
Yes exactly, and this would be this script which is connected to the GUI that stores all the inventory containers so something like:
GUI
InventoryInterface
PlayerInventory
GrabbedSlot
extends Control
signal drop_slot_data(slot_data:SlotData)
signal force_close
var grabbed_slot_data: SlotData
var external_inventory_owner
@onready var player_inventory = $PlayerInventory
@onready var grabbed_slot = $GrabbedSlot
@onready var external_inventory = $ExternalInventory
@onready var equip_inventory = $EquipInventory
func _physics_process(delta):
if grabbed_slot.visible:
grabbed_slot.global_position = get_global_mouse_position() + Vector2(5,5)
func set_player_inventory_data(inventory_data: InventoryData):
inventory_data.inventory_interact.connect(on_inventory_interact)
player_inventory.set_inventory_data(inventory_data)
func set_equip_inventory_data(inventory_data: InventoryData):
inventory_data.inventory_interact.connect(on_inventory_interact)
equip_inventory.set_inventory_data(inventory_data)
func set_external_inventory(_external_inventory_owner):
external_inventory_owner = _external_inventory_owner
var inventory_data = external_inventory_owner.inventory_data
inventory_data.inventory_interact.connect(on_inventory_interact)
external_inventory.set_inventory_data(inventory_data)
external_inventory.show()
func clear_external_inventory():
if external_inventory_owner:
var inventory_data = external_inventory_owner.inventory_data
inventory_data.inventory_interact.disconnect(on_inventory_interact)
external_inventory.clear_inventory_data(inventory_data)
external_inventory.hide()
external_inventory_owner = null
func on_inventory_interact(inventory_data: InventoryData, index: int, button: int):
#if left cliccking on a null "button"
match[grabbed_slot_data, button]:
[null, MOUSE_BUTTON_LEFT]:
grabbed_slot_data = inventory_data.grab_slot_data(index)
[_, MOUSE_BUTTON_LEFT]:
grabbed_slot_data = inventory_data.drop_slot_data(grabbed_slot_data, index)
#use case for an item, ie. use a potion
[null, MOUSE_BUTTON_RIGHT]:
inventory_data.use_slot_data(index)
[_, MOUSE_BUTTON_RIGHT]:
grabbed_slot_data = inventory_data.drop_single_slot_data(grabbed_slot_data, index)
print(grabbed_slot_data)
update_grabbed_slot()
func update_grabbed_slot():
if grabbed_slot_data:
grabbed_slot.show()
grabbed_slot.set_slot_data(grabbed_slot_data)
else:
grabbed_slot.hide()
func _on_gui_input(event):
if event is InputEventMouseButton and event.is_pressed() and grabbed_slot_data:
match event.button_index:
MOUSE_BUTTON_LEFT:
drop_slot_data.emit(grabbed_slot_data)
grabbed_slot_data = null
MOUSE_BUTTON_RIGHT:
drop_slot_data.emit(grabbed_slot_data.create_single_slot_data())
if grabbed_slot_data.quantity < 1:
grabbed_slot_data = null
update_grabbed_slot()
func _on_visibility_changed():
#if you close inventory while holding an item, it will drop item
if not visible and grabbed_slot_data:
drop_slot_data.emit(grabbed_slot_data)
grabbed_slot_data = null
update_grabbed_slot()
Okay, so the easiest fix would probably to adjust your slot script:
func _on_gui_input(event: InputEvent):
if event is InputEventMouseButton\
and (event.button_index == MOUSE_BUTTON_LEFT\
or event.button_index == MOUSE_BUTTON_RIGHT)\
and event.is_pressed():
var index = get_index()
if get_parent().name == "ItemGrid":
index += 6 # number of slots in your hotbar
slot_clicked.emit(index, event.button_index)
It’s not the most beautiful solution (since it will stop working if you rename your GridContainer or change the amount of items in a row), but should fix your issue.
Well son of a gun that worked! Thank you so much man you were a huge help! Now do you mind if i ask what led you to this solution/how in the world this fixed it? If not i’ll do some tests myself to see why this is but i can’t thank you enough! I’m thinking if i remake an inventory system again i may choose a different approach than the video i watched.
The _on_gui_input function is what’s triggered first when you click on a slot. Everything else follows from here! It emits a signal slot_clicked, which is connected to the InventoryData function on_slot_clicked, where another signal inventory_interact is emitted, which ultimately get’s connected to the on_inventory_interact function in your GUI script, which then finally triggers the actual action (based on the used mouse button) by calling the corresponding function.
Note how all of this is just boilerplate code and any actual information used goes back to that original _on_gui_input function! So if a wrong element is selected, then the information about what element to select (the index) was already wrong there, right in the beginning!
All those functions for interacting with the inventory (´grab_slot_data, drop_slot_data, use_slot_data and drop-single_slot_data) just get an index and then use slot_datas to look up on which slot they operate. However, the slot data contains both the slots from the hotbar and the regular grid, while the index provided by the containers only depends on where a slot is located inside that (and only that!) container. So both the first slot in the hotbar and the first slot in the grid will return an index of 0 and therefore refer to the 0-th element in slot_datas, which happens to be correct for the hotbar, but not for the grid. My quick and dirty fix acocunts for this by artifically increasing the index for items in the grid by the amount of slots in the hotbar, to align the index with the data in slot_datas again.