High-level help with an interactable objects system

Godot Version

Godot 4.2.2 (using gdscript)

Question

I’m thinking about making a system where the player can interact with different kinds of objects. Some will just be items that the player can pick up (it’ll add an item to the inventory and remove the object from the world), some will be things that can be harvested (add inventory, but aren’t deleted and start growing again), some will be things like doors that just change their state (open/closed), or do other things.

All these object will have some kind of CollisionObject3D (most StaticBody or RigidBody, some just an Area for stuff like plants you can just walk through), and some might have multiple in case they have a weird shape of their physics shape is too difficult to exactly look at I’ll add an extra Area around it or something.

I was wondering, is there something like Interfaces in Godot so I can properly type my scripts? Or can I send events or something similar to objects that can then handle it? Or should I create some kind of custom node type for this?

I’m new to Godot scripting, and I’m struggling with figuring out what the Godot way of doing this kind of thing is. I don’t need real code, just a high-level idea of how you would design something like this in Godot would be super useful!

Thank you!

Sounds like a cool system, good luck. For proper interfaces I think you’ll be wanting the C# version of Godot. But gdscript can come pretty close. Here’s what I was doing for interfaces before I switched over to basically entirely c#. Basically create helper class, and everything you would have given a shared interface to, assign each of those some sub class of that helper class. Where each subclass shares common methods to the base, and those common base methods are like your interface methods. Then the helper sub-classes would each negotiate stuff like how to find the collider on the current object, if it has one and that’s relevant, and details like that.

When I would do this, I didn’t use nodes for those helper classes because I didn’t want to junk up the scene tree with all the extra baggage, so I’d use basic objects for them. But then they can’t access the scene tree, so what I’d do is have each of the helper classes have a SEAT(_host:Node)->my_helper_class function, so they accept the hosting node as a parameter, which they would cache, then return themselves as return value of that function. Then if the helper class needs the scene tree of the hosting nodes tree structure, it has access to it. Then whenever the host node needs to interact with the helper, it does that through myHelperClass.SEAT(self), like myHelper.SEAT(self).INTERACT()

I’m not sure this is actually the most effecient, godot-like way to approach this, but it worked ok for me, maybe it’ll get you where you’re trying to go. But again, C# is ultimately a far better option for enforcing types and interfaces etc.

1 Like

Thank you!

What I am doing at the moment though, is not worry too much about types and for this specific case it’s not perfectly types but I do checks where needed.

So the system I build now is this: There are two “magic” functions. The first one is:

func get_view_label_text() -> String:

When a script has this, whatever string it returns is shown under the crosshair.

For example, on my door script I have:

func get_view_label_text() -> String:
	if state == DoorState.OPEN:
		return "Close door"
	else:
		return "Open door"

And then you get something like this in game:

image

Similarly, I have a function for interactions:

func on_interaction(player: Player) -> void:

This is called whenever a user clicks while looking at an object with a script that has this.

In my player script I check if these functions exist, and then call them if they do exist. And I check parents of objects too so I can have multiple colliders/areas on a single object with a script on the parent.

For that, the implementation is:

func _process(_delta: float) -> void:
	# Allow us to quit
	if Input.is_action_just_pressed("quit_game"):
		get_tree().quit()

	update_view_label()
	handle_interacting()


func update_view_label() -> void:
	# Update the crosshair/view label depending on what we're looking at
	var view_label_text: String = ""
	if interaction_ray.is_colliding():
		var collider: Node = interaction_ray.get_collider()
		view_label_text = call_on_node_or_parent(collider, GET_VIEW_LABEL_TEXT_FUNC_NAME, [], "")

	if view_label_text:
		view_label.visible = true
		view_label.text = view_label_text
	else:
		view_label.visible = false


func handle_interacting() -> void:
	var collider: Node = interaction_ray.get_collider()
	if collider && Input.is_action_just_pressed("primary_action"):
		call_on_node_or_parent(collider, ON_INTERACTION_FUNC_NAME, [self])


func call_on_node_or_parent(
	node: Node, function_name: String, arg_array: Array, default: Variant = null
) -> Variant:
	while node:
		if node.has_method(function_name):
			return node.callv(function_name, arg_array)
		node = node.get_parent()

	return default

So this part isn’t perfectly typed, I add types where needed but there is some untyped stuff (well, “typed” with Variant and Array).

This seems to work well so far. I have an apple I can pick up (doesn’t go into an inventory yet as I don’t have that, but the interaction works) and a door I can open/close. Adding these methods to the scripts has been very simple and very nice to use.

I hope this might help anybody trying to do something similar! Or if you have comments on how I can improve this or problems you see, I’d love to hear about that too!