Best practices for static typed GDScript Signals and interfaces/abstract classes

Godot Version

4.2.2

Question

Hi everyone,

I’m very new to Godot but have a background in software development. I’m trying to design the architecture for my first Godot game using GDScript. I really like the rapid iteration capabilities of GDScript and how well it ties into the editor, so I’d prefer to use it over C#, but having read the best practices documentation and enabled type hints I’m still having some challenges around static typing.

Specifically, when I connect up signals:

# event_bus_autoload.gd
...
signal thing_happened(triggered_by: Node, message: String);

# broadcaster.gd
...
EventBusAutoload.thing_happened.emit(self, "yo!");

# listener.gd
...
EventBusAutoload.thing_happened.connect(_on_thing_happened);
...
func _on_thing_happened(triggered_by: Node, message: String) -> void:
    #do something

I get no type hints/intellisense from the editor when passing arguments to .emit() or .connect(). Worse than that, I get no compilation error if I pass incorrectly typed arguments to .emit() or attempt to pass a method which has the wrong parameters to .connect(). Having looked around the forums, I think this might be expected behaviour, but if anyone can give me a workaround I would really appreciate it. So far the best I’ve come up with is this:

# event_bus_autoload.gs
signal _thing_happened(triggered_by: Node, message: String);
func thing_happened(triggered_by: Node, message: String) -> void:
    _thing_happened.emit(triggered_by, message);

and then call the public func instead of emitting the “private” signal. This does give me type safety when emitting, but unfortunately not when connecting. I can’t pull the same trick when connecting because (as far as I have seen) you can’t specify types for a Callable’s parameters (is this true?).

Related to this - I’m also very interested to know if there’s a way to make _private_members actually private, and in any workarounds/editor plugins that tackle the lack of interfaces and abstract classes. I’d love to find a way to cause compile time errors when, for example, accidentally assigning an “abstract” base class to something instead of a derived class. I appreciate the composition over inheritance ideology reduces the need for interfaces and abstracts, but I would still like to be able to use eg. the strategy pattern and unfortunately, I’ve not found a type safe way to this so far in Godot.

These may just be limitations I will have to get used to, but I thought it was worth asking first.

Thanks for reading!

At least reading the source there is abstracted machinery to setup the signals on a class. In some sense I think it could auto complete for you, but it probably isn’t implemented to do so.

I know for a fact that when you use the editor to connect a signal it knows the argument names of the function signature. So at least some API is available to help the auto complete.

When a signal is added it is done via the ClassDB class. The types of the parameters are stored. From there it’s not clear how the machine works, (I would need to read the src more). I suppose there is a table of callables for each signal and it’s connections in the end.

1 Like

Hmm that’s interesting. I haven’t delved into Godot’s source yet (and to be honest, I think it will be some time until I do) but it sounds like when I’ve got a bit further along it might be worth doing. Maybe then, if no one beats me to it, I can look into implementing it myself and make a pull request. Again though, that’ll be some time, I’ve only been here a day. Haha.

Given that you started digging around in the engine code for a solution (thanks by the way!) I’m guessing there aren’t any commonly known work arounds?

Are you defining a class_name?

e.g. class_name Player extends Node2D

then in another script you can reference the player and auto-complete is available for the signals

@onready var player: Player = get_node("../Player")

...

player.died.connect(show_death_screen)
1 Like

In this example I was using an autoload rather than class_name, but the same issue applies to both (and to using signals from the same class in which they were defined).

The signal itself can be used statically, but unfortunately any parameters it has can’t because:

  • The signature for Signal.emit is: emit(…) → void
  • The signature for Signal.connect is: connect(Callable, int) → int

If I had a signal defined to carry an int and a string, like so:

signal died(player_id: int, cause_of_death: String);

Then to use it in a type safe manner I would need:

  • The emit() method for this signal to want an int and a String, rather than zero or more Variants.
  • The connect() method for this signal to want a Callable which specifically has the signature (int, String) → void

I don’t beleive this is possible (yet!) in GDScript, but would be very pleased if I’m wrong! There are some related proposals in the Godot GitHub repository (the most critical to this issue I think being this one: Statically typed Callable · Issue #10807 · godotengine/godot-proposals · GitHub), so I’m hoping that this will be possible some day!