Is it possible to add a gap between 1 row in a grid container?

Godot Version

4.1.2

Question

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.

2 Likes

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?

1 Like

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 might be missing something, but… on_slot_clicked expects two arguments index and button, but when you connect it, you don’t provide any of that? :thinking:

slot.slot_clicked.connect(inventory_data.on_slot_clicked)

I’d have expected you get some runtime errors from that?

1 Like

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?

1 Like

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.

1 Like

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.

I spent a lot of time staring at code in my life. :grin:

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.

1 Like