I am aware of @export_node_path which allows me to constrain the export to only instances of a certain class. But is there any way to constrain to only Objects which have a particular variable or function?

For example, I can do

@export var some_object: Object

func _ready():
    if (is_instance_valid(some_object) && some_object.has_method("required_method"):
        some_object.required_method();

But is there any way to do this so that when displayed in the inspector it will only accept objects which have "required_method"? Or is there any good way of using @onready or something in _ready() to find a child which meets that requirement?

You could use classes and inheritance. Though Godot does not support interfaces (this is what you would use in something like Java or C#) or abstract base classes (in C++). However, you could fake it with your own base class that is only used to declare the functions needed for the interface.

Yeah I thought about that, but unfortunately it creates more problems than it solves.

If I were working in the Unity game engine, I would use a CustomPropertyDrawer and reflection to check the serialized object for an interface, rejecting it if the interface is not present. Unfortunately it wouldn't be serialized as that interface, and so a runtime cast to the interface would be required.

GDScript avoids that need for a runtime cast thanks to being dynamically-typed, and it seems like some similar serialization magic should be achievable, but I'm not sure how. _get_property_list() seemed promising, but on further inspection it doesn't seem to enable me to affect the variables as they are being serialized at all.

Is there any way I can look into to create something custom for this? If it would require diving into C++ then that's something I can do, but of course I want to know first if there's already a way to do it.

Edit: It seems like an EditorInspectorPlugin might be able to do what I'm looking for. Will report back 👍

soundgnome

That seems fully capable of doing what I want, but it comes with a lot of overhead.

  1. I need to add checks for Engine.is_editor_hint all throughout my class to ensure the rest of my code doesn't run when it shouldn't be running.
  2. The @tool and the setget need to be added in every script and for every variable that I want to do this validation, with no real way of abstracting it. That's a lot of duplicated code.

That unfortunately makes this inconvenient and harder to maintain when I'm trying for the opposite.

The EditorInspectorPlugin seems like it can achieve what I'm aiming for, but then the problem becomes, "How do I tell my EditorInspectorPlugin what it is that I'm trying to validate for?"

It would be neat if I could supply custom data via some kind of custom @export hint, but I'm not seeing any way to do that. The best solution I can see would be to put the data into the variable name itself, something like

@export var name_of_var_rqrfnctn_name_of_function: Object

And then parse that all from the variable name in the EditorInspectorPlugin. Still far from ideal...

What is the problem you are trying to solve?

I'm looking for a good method of decoupling. I want to be able to assign (or find) an Object, usually a child, which has X function which can be called. X may have a return value, and the Object needn't know anything about the script it is being called from, which is why signals won't solve my use-case.

I see a lot of example scripts with code like:

@onready var some_variable = "$Variable/Path"

func _process(_delta)
    some_variable.some_method()

and it drives me crazy because it's extremely fragile! Using @export with a class type solves the fragility problem, but now my code is tightly coupled. @export NodePath at least is a little less heinous than using a hardcoded path, but still provides me no guarantee that the function I'm going to call exists, nor that get_node will even return me a valid instance of the class I'm looking for.

One way to solve the coupling issue is to have a Singleton (Autoload) that acts as a message bus. It is accessible from anywhere, but the objects communicating need not know about each other. Usually this is done using Signals, but you can also use functions. Like one object can request a particular thing (let's say it sends a string of a function name) and the singleton can reply with whatever you want. You can implement duck typing there, using has method or getting the property list. But you can also use any other identifier, such as an Enum, or maybe some sort of hash table (using Dictionaries). I think that is going to be the most flexible, even if it isn't ideal.

    cybereality

    That's a good idea in general, and I've been considering using something like that for this project. Still I don't think an event or command queue quite solves my need here. I'll just go ahead and post my script I'm working on right now as an example (just a prototype while I'm exploring things)

    class_name Player
    
    extends CharacterBody2D
    
    @export var _movement_buffer__path: NodePath
    @onready var _movement_buffer:Object = get_node(_movement_buffer__path)
    
    @export var speed := 300.0
    @export var brake := 0.2
    
    func _physics_process(delta: float) -> void:
    	var input_movement := Vector3.ZERO
    	var input_received := false
    
    	var received_input:int
    	if is_instance_valid(_movement_buffer) && _movement_buffer.has_method("front"):
    		received_input = _movement_buffer.front()
    	else:
    		push_error("_movement_buffer missing or missing function front")
    
    	if Config.INPUT_DIRECTIONS_MAP.has(received_input):
    		input_movement = Config.DIRECTION_VECTORS[Config.INPUT_DIRECTIONS_MAP[received_input]]
    		input_received = true
    	elif received_input != Config.Inputs.NONE:
    		push_error("Expected directional input. Received %s" % received_input)
    
    	if input_received:
    		velocity = Hex.cube_to_offset(input_movement * speed)
    	else:
    		velocity = velocity.move_toward(Vector2.ZERO, (speed / brake) * delta)
    
    	var _collided = move_and_slide()

    So you see, all I'm really doing is calling a getter-type function, front, on my child object. Storing that as a NodePath means I don't have to worry about renaming or moving the child, even out of the local hierarchy, and I can replace the script with whatever as long as it has front which returns a Vector3. The unfortunate part is that I have to ensure that myself, both with the error-checking and with being careful to assign a valid Node.

    I'm calling this in _physics_process, so I need a value right away. I unfortunately don't have time to wait for a response from a global event which might not have a listener, and throwing that event would require excessive data. I would need to do something like pass Self and a callback and have the listener check if it's a child before calling the callback and... it'd be messy.

    If gdscript had interfaces like C# does, that would be ideal, but unfortunately it does not and will not. And I know I can work in C# with Godot, but even with 4.x C# just isn't quite there for me, plus I'd lose out on things that make Godot special with gdscript.

    Edit: Also I don't think C# interface variables can be exported, can they?

    Well there is always going to be some coupling. It is almost impossible for there not to be, and still have communication. There are different ways to do it, some worse than others, but like to send someone an email, at some point in the chain, you need their email address.