Interactions and inventory

Godot Version

4.3

Question

I’m new to observable patters and I’m looking for an optimal way of implementing this inventory system. What I have works, but I am wondering if there are improvements that could be made.

There are 5 components that make up my existing system. Interactor.gd, Interactable, Pickup, Item, and Inventory.

Instead of having the Interactable driving the interaction based on input, my system collects all nearby interactables in the Interactor and returns the one closest to the center of the screen so you dont need to be looking directly at it to pick it up. The Interator exists on the player and controls gathering all of the interactables within its vicinity and sorts them and assigns the interaction that has focus.

When the player presses the interact key, if there is a focused interactable it will call its interact function and call in self as the caller.

Because interactables are meant to be generic and the Interactor shouldn’t care about what the interaction will do, I don’t pass down any additional information into the interaction. Instead, the interactable node emits a signal that it’s child components like my Pickup script are monitoring to handle a pickup interaction.

This all felt good until I needed to get the item from the Pickup and put it into the Inventory that exists on the player. I don’t want to pass down the inventory through the signal chain because then it would know about things it shouldn’t, so instead I get the inventory of the player by first getting the parent from the interactor node and then getting the inventory node. This works because I know the structure of the player, but it doesn’t feel flexible.

So what I’m wondering is if there is a better way to get a pickup interaction to communicate with an inventory from the thing that interacted with it.

I removed code that isn’t relevant to the issue

Interator.gd

extends Area3D

#region Locals

var _focused_interactable: Interactable
var _interactables: Array[Interactable] = []

#endregion

#region System funcs

func _input(event):
	if Engine.is_editor_hint(): return
	if not event is InputEventKey or not event.is_action_pressed("interact"): return
	if _focused_interactable:
		_focused_interactable.interact(self)

func _process(_delta):
	if Engine.is_editor_hint(): return
	_get_nearest_interactable()

#endregion

#region Signal funcs

func _on_area_entered(area):
	if area is Interactable:
		print("{0} entered interaction area".format([area.name]))
		_interactables.append(area as Interactable)

func _on_area_exited(area):
	if area is Interactable:
		print("{0} exited interaction area".format([area.name]))
		var index = _interactables.find(area)
		if index > -1:
			_interactables.remove_at(index)
		print("You have {0} items.".format([%Inventory.get_items().size()]))

#endregion

#region Local funcs

func _get_nearest_interactable():	
	if _interactables.size() == 0:
		_focused_interactable = null
		return
	
	var nearest_interactable = _get_nearest_node(_interactables)

	# Stuff happens to determine the focused interactable 

	_focused_interactable = nearest_interactable

func _get_nearest_node(objects):
	# Implementation

#endregion

Interatable.gd

@tool
class_name Interactable
extends Area3D

#region Signal Defs

signal interacted(root: Node, interactor: Node)

#endregion

#region Public funcs

func interact(interactor: Node):
	print_rich("[color=Cornflowerblue]Interacted with:[/color] {0}".format([name]))
	interacted.emit(self, interactor)

#endregion

Pickup.gd

@tool
class_name  Pickup
extends Node3D

@export var item: Item :
	set(resource):
		item = resource
		update_configuration_warnings()

func _get_configuration_warnings():
	if not Engine.is_editor_hint(): return
	var warnings = []
	if not item:
		warnings.append("Must attach an item to the pickup")
	return warnings

func _on_item_pickup_interacted(root: Node, interactor: Node):
	print("You picked up a: ", item.name)
	var inventory = interactor.get_parent().find_child("Inventory") as Inventory
	inventory.add_item(item)
	root.queue_free()

item.gd

class_name Item
extends Resource

@export var name: String
@export var icon: Texture2D

Inventory

class_name Inventory
extends Node

var _content: Array[Item] = []

func add_item(item: Item):
	_content.append(item)

func remove_item(item: Item):
	_content.erase(item)

func get_items() -> Array[Item]:
	return _content

I think I would have done it the same way. Interactable could also be part of a door, switch etc. And those don’t need to know about inventory when interacting with them.

Maybe I would add some additional checks.
For example:

	var inventory = interactor.get_parent().find_child("Inventory") as Inventory
    if inventory:
	     inventory.add_item(item)

And maybe make inventory.add_item return a boolean, in case inventory is full and item doesn’t get picked up.

 if inventory.add_item(item)
      root.queue_free()

or

if inventory and inventory.add_item(item):
      root.queue_free()

But you didn’t ask for this and probably would have done that anyway at some point. :smile: