Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add static signals in GDScript #6851

Open
4d49 opened this issue May 10, 2023 · 11 comments
Open

Add static signals in GDScript #6851

4d49 opened this issue May 10, 2023 · 11 comments

Comments

@4d49
Copy link

4d49 commented May 10, 2023

Describe the project you are working on

Strategy game.

Describe the problem or limitation you are having in your project

I have a singleton with many signals for different things. Sometimes a singleton can contain signals that should be moved elsewhere. After adding static variables, it would be nice to add static signals as well. This will create helper objects that do not need to be instantiated. These "objects" can be used as singletons.

Describe the feature / enhancement and how it helps to overcome the problem or limitation

We just connect/disconnect anywhere. We don't need to have an instance of the class.

func _enter_tree() -> void:
	ClassName.signal_name.connect(_on_method)
	
	
func _exit_tree() -> void:
	ClassName.signal_name.disconnect(_on_method)

Describe how your proposal will work, with code, pseudo-code, mock-ups, and/or diagrams

An example of a class with a static signal:

# selected_cell.gd
class_name SelectedCell


static signal selected_cell_changed(cell: Cell)


static var selected_cell: Cell = null:
   set = set_selected_cell


static func set_selected_cell(n_selected_cell: Cell) -> void:
	if is_same(selected_cell, n_selected_cell):
		return

	selected_cell = n_selected_cell
	selected_cell_changed.emit(n_selected_cell)

Sample code that uses a static signal:

# other_script.gd
func _ready() -> void:
	# We don't need an object instance to use it.
	SelectedCell.selected_cell_changed.connect(_on_selected_cell_changed)


func _on_selected_cell_changed(cell: Cell) -> void:
	print(cell)

If this enhancement will not be used often, can it be worked around with a few lines of script?

Yes, it is potentially possible to use a static variable with a signal:

# selected_cell.gd
class_name SelectedCell


static var signal : Signal

But then it still requires an "entry point" to assign that signal:

# other_script.gd
signal real_signal(cell: Cell)


func _init() -> void:
	SelectedCell.signal = real_signal

Is there a reason why this should be core and not an add-on in the asset library?

It's part of GDScript.

@dalexeev
Copy link
Member

Only instances can have signals, not classes. But a GDScript class is an instance of the GDScript class, so probably it's possible to implement this without even adding static signals to the core (but need support in DocData for user documentation).

Also note that if the class is unloaded (if the script has an @static_unload annotation and no references to the script left), then connections will probably be lost as well.

@timothyqiu
Copy link
Member

Another workaround is to use the event bus pattern: define these signals in a dedicated autoload script for all static signals.

# EventBus.gd

signal selected_cell_changed(cell: Cell)


# selected_cell.gd
class_name SelectedCell

static var selected_cell: Cell = null:
	set = set_selected_cell

static func set_selected_cell(n_selected_cell: Cell) -> void:
	if is_same(selected_cell, n_selected_cell):
		return

	selected_cell = n_selected_cell
	EventBus.selected_cell_changed.emit(n_selected_cell)


# other_script.gd
func _ready() -> void:
	# We don't need an object instance to use it.
	EventBus.selected_cell_changed.connect(_on_selected_cell_changed)


func _on_selected_cell_changed(cell: Cell) -> void:
	print(cell)

The downside is you have to name the signals carefully.

@L4Vo5
Copy link

L4Vo5 commented May 11, 2023

It would be interesting if all signals could double as static signals. Then you could choose to either connect to a particular instance's signals as usual, or instead connect to the whole class' signal to receive it when any instance emits it. For example my_enemy.poisoned.connect(...) for the first and Enemy.poisoned.connect(...) for the second.
When an instance emits a signal it'd call all the connected functions for its own signal as well as the functions connected to the class' signal. Static functions would only emit the class' signal.

@theraot
Copy link

theraot commented May 20, 2023

Allowing to connect signals to autoloads (sigletons) (See #1694 and #4993) for the good it would do※ has a hurdle: When a scene is instantiated it is not in the scene tree, so it cannot reach autoload (singletons) at that moment. Which means such connection would have to be delayed. Perhaps this is desirable?

An alternative is to have static signals, which is what is being proposed here. Since static signals would not depend on the scene tree, then connecting automatically to them when a scene is instantiated would be viable. That would, of course, require different proposals to the linked ones to allow connected them visually from the editor, which is what I'm interested in.

I am interested in this in particular as a way to ease addons communicate with each other, without adding dependencies between them, and while the designer stays in control.


※: These are some of the situations where we would rather connect visually to a signal bus (event bus) instead of doing it from a script:

  • The script is reused in many places, and not all should be connected, instead it is on designer discretion.
  • Editing the script is responsibility of another member of the team, or the connection is being made by a designer who is not familiar enough with the code to make the connection.
  • It is third-party code (e.g. from an addon) that we rather not modify.

@Shadowblitz16
Copy link

How would this even work?

The only why you can access static members is with a class name or a ugly preload
And since class name doesn't bind to the scene your basically just accessing the script with nothing to operate on

@DasGandlaf
Copy link

DasGandlaf commented Jul 12, 2023

Instead of signals, you can now (since 4.1 I believe) have a static array which saves the listening objects. e.g.

class_name Settings

static var listeners: Array

static func on_change_sub(object: Object): # To subscribe, usage: Settings.on_change(self)
    listeners.append(object)

static func on_change_unsub(object: Object): # To unsubscribe, usage: Settings.on_change_unsub(self)
    listeners.remove_at(save_listeners.find(object))

static func invoke():
    for listener in listeners:
       listener.onChange()

Dont forget to unsubscribe nodes that get freed, or else it will try to call the method on a null instance.

@gokiburikin
Copy link

You can functionally utilize static signals (at least how I want to use them) now using a static var singleton pattern (for the instance), though it does feel hacky. Might be side effects to doing it this way?

# settings.gd
class_name Settings extends RefCounted

static var singleton := Settings.new()
signal _changed
static var changed:Signal:
	get: return singleton._changed

static func change( key:StringName, value ) -> void:
	singleton.set( key, value )
	Settings.changed.emit( key, value )

static var example_property := "foo"
# test.gd
func _ready() -> void:
	print( Settings.example_property )
	Settings.changed.connect( func( key:String, value ):
		print( "%s changed to %s" % [key, value] ))
	Settings.change( "example_property", "bar" )
	print( Settings.example_property )
# output
foo
example_property changed to bar
bar

Static variables aren't currently auto-completed though so Settings.singleton.example_property for that.

@theraot
Copy link

theraot commented Sep 25, 2023

I want to point out somebody figured out how to bind signals to a class and consume them using the expected syntax: https://stackoverflow.com/a/77026952/402022

What they are doing is adding - during initialization - a signal to the script using add_user_signal, creating a Signal object from it, and storing it in an static var.

The static signal is then consumed by accessing the static var from the script using the class name, and calling emit or connect which are available since the static var is a Signal.

That works today, and I have used it successfully. I hope this clears any doubt of how static signals might work.

@TheJehoiada
Copy link

TheJehoiada commented Mar 5, 2024

I found this to be an easy workaround...

extends Node3D
class_name Drone

signal _real_signal
static var singleton := Drone.new()
static var static_signal:= Signal(singleton._real_signal)

Example use below

func _ready() -> void:
	connect_signal()
	emit()

func connect_signal():
	static_signal.connect(Callable(self, "getting_signal"))

func getting_signal():
	print("we got the signal")

func emit():
	print("emiting signal")
	 Drone.static_signal.emit()

@Calinou
Copy link
Member

Calinou commented Mar 5, 2024

@TheJehoiada PS: Code blocks should use triple backticks like this (with an optional language name for syntax highlighting):

```gdscript
code here
```

I edited your post accordingly, but remember to do this in the future 🙂

@neatsketched
Copy link

Just wanted to post that I have found a more concise workaround for static signals, based on some of the other solutions that have been posted before.

I created a new class that exists as a quick creator for static signals, and then you can refer to this function from anywhere:

extends Object
class_name StaticSignal

static var static_signal_id: int = 0

static func make() -> Signal:
        var signal_name: String = "StaticSignal-%s" % static_signal_id
	var owner_class := (StaticSignal as Object)
	owner_class.add_user_signal(signal_name)
	static_signal_id += 1
	return Signal(owner_class, signal_name)

Here is an example of creating a static signal from this method:

static var example_signal: Signal = StaticSignal.make()

From here, you can use example_signal as you would expect with no other drawbacks that I know of.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests