godot icon indicating copy to clipboard operation
godot copied to clipboard

(C#) .Bind missing on the Callable object

Open TandersT opened this issue 2 years ago • 6 comments

Godot version

Godot Engine v4.0.beta1.mono.official.4ba934bf3

System information

Windows 11, Vulkan API 1.2.0 - Using Vulkan Device #0: AMD - AMD Radeon RX 5700

Issue description

The method .Bind does not exists in the current beta, along with some of the docuemented code such as the .Connect on a delegate. I'm unsure if this is due to the fact that the documentation is regarding latest, and not beta, but as of now I can not see a way to connect a signal with additional paramteres.

Steps to reproduce

Create a new callable, and try to call .Bind, or any other form of passing a variable on build in signal

Minimal reproduction project

No response

TandersT avatar Sep 19 '22 13:09 TandersT

This part of the documentation is still outdated, I have an open PR (see https://github.com/godotengine/godot/pull/64930) to fix this but it's still a draft pending some changes to Callable.

For now, you can use lambdas to connect to signals passing additional parameters:

public class MyNode : Node
{
	[Signal]
	public delegate void MySignalEventHandler(int a, int b, int c);
	
	public override void _Ready()
	{
		MySignal += MySignalCallback;
		MySignal += (a, b, c) => MySignalCallbackWithAdditionalParams(a, b, c, 1, 2, 3);
	}

	private void MySignalCallback(int a, int b, int c) {}
	private void MySignalCallbackWithAdditionalParams(int a, int b, int c, int d, int e, int f) {}
}

About ".Connect on a delegate", this is not possible in C#, I apologize for having invalid syntax in the documentation. My recommendation is to use the generated events instead as shown in the example above. Otherwise, you'll have to use the .Connect method on the node and pass the name of the signal and a Callable instance:

public class MyNode : Node
{
	[Signal]
	public delegate void MySignalEventHandler(int a, int b, int c);
	
	public override void _Ready()
	{
		// This way of constructing Callables will change in the future
		var callable = new Callable(MySignalCallback);
		Connect(SignalName.MySignal, callable);
	}

	private void MySignalCallback(int a, int b, int c) {}
}

raulsntos avatar Sep 19 '22 16:09 raulsntos

That makes sense, the documentation was a bit odd. Your code works fine for signals that I create, but if I were to try the same with a built-in signal, such as Timeout, it does not accept additional parameters. Is there any way around this for now?

E.g.

public partial class Node2ds : Node2D
{
    [Signal]
    public delegate void MySignalEventHandler(int a, int b, int c);
    public override void _Ready()
    {
        var _t = GetTree().CreateTimer(1);
        MySignal += (a, b, c) => MySignalCallback(a, b, c); // all good
        _t.Timeout += (a, b, c) => MySignalCallback(a, b, c); // no good
    }
    private void MySignalCallback(int a, int b, int c) { }
}

TandersT avatar Sep 19 '22 17:09 TandersT

@TandersT: You can't add additional parameters to default signals, it's the same in GDScript

Zireael07 avatar Sep 19 '22 17:09 Zireael07

Okay, so as of now is there any way to achieve something similar to this, in which the variable _fire, is passed with the signal?

timer.Connect("timeout", this, nameof(FreeFire), new Godot.Collections.Array { _fire });

TandersT avatar Sep 19 '22 17:09 TandersT

@TandersT I think you misunderstood my example, you can add additional parameters by using closures, see this example:

var _t = GetTree().CreateTimer(1);
_t.Timeout += () => MySignalCallback(1, 2, 3);
// Or capturing variables:
int a = 1;
int b = 2;
int c = 3;
_t.Timeout += () => MySignalCallback(a, b, c); // a, b, c are captured by the closure

To answer your last comment:

timer.Timeout += () => FreeFire(_fire); // The _fire variable is captured by the closure

@Zireael07 I'm pretty sure you can use bind/unbind with GDScript for default signals, unless I misunderstood what you meant:

func _ready() -> void:
    var t := get_tree().create_timer(1)
    t.timeout.connect(_on_timeout.bind(1, 2, 3))


func _on_timeout(a: int, b: int, c: int) -> void:
    pass

raulsntos avatar Sep 19 '22 18:09 raulsntos

Thank you, that does the trick! That's on my end, I see I misunderstood :)

TandersT avatar Sep 19 '22 18:09 TandersT

@raulsntos Wow, TIL - this trick is worth documenting in the docs!

Zireael07 avatar Sep 20 '22 08:09 Zireael07

this trick is worth documenting in the docs!

It is used in the documentation, e.g. in Tween.

paulloz avatar Sep 20 '22 09:09 paulloz

Should also be documented in Callable documentation itself, and possibly signals

Zireael07 avatar Sep 20 '22 09:09 Zireael07

@Zireael07 Sure. I just added C# examples where there were already GDScript ones. The Callable doc page doesn't provide examples for bind in any language at the moment. And pages outside the class ref are outdated on so many levels...

paulloz avatar Sep 20 '22 09:09 paulloz

It's currently documented in Object's connect method (a bit outdated, see https://github.com/godotengine/godot/pull/64930):

https://github.com/godotengine/godot/blob/aa553f403099a31520ab0c75a43f352642170d5f/doc/classes/Object.xml#L172-L306

But it should probably be documented in Callable too.

raulsntos avatar Sep 20 '22 09:09 raulsntos

Missed that one while reading through the docs. I can update those if you need me to 🙂

paulloz avatar Sep 20 '22 09:09 paulloz

I already update it in https://github.com/godotengine/godot/pull/64930, marked as a draft pending some changes to Callable on C#'s side.

raulsntos avatar Sep 20 '22 09:09 raulsntos

@TandersT I think you misunderstood my example, you can add additional parameters by using closures, see this example:

var _t = GetTree().CreateTimer(1);
_t.Timeout += () => MySignalCallback(1, 2, 3);
// Or capturing variables:
int a = 1;
int b = 2;
int c = 3;
_t.Timeout += () => MySignalCallback(a, b, c); // a, b, c are captured by the closure

To answer your last comment:

timer.Timeout += () => FreeFire(_fire); // The _fire variable is captured by the closure

@Zireael07 I'm pretty sure you can use bind/unbind with GDScript for default signals, unless I misunderstood what you meant:

func _ready() -> void:
    var t := get_tree().create_timer(1)
    t.timeout.connect(_on_timeout.bind(1, 2, 3))


func _on_timeout(a: int, b: int, c: int) -> void:
    pass

How to remove such a lambda bind from the signal after it was added?

wp2000x avatar Nov 23 '22 19:11 wp2000x

How to remove such a lambda bind from the signal after it was added?

Do you mean disconnect the lambda from the event? It works like it does in C#, you need to store the lambda:

// Store the lambda in a variable
var myCallback = () => MySignalCallback(1, 2, 3);

// Connect the callback to the signal
MySignal += myCallback;

// Disconnect the callback from the signal
MySignal -= myCallback;

Keep in mind, the events we generate with source generators don't generally need to be disconnected because on disposing the object we will automatically disconnect all signals, if that's what you are worried about:

https://github.com/godotengine/godot/blob/f6f8a48459f9bbe97ee76a7186b9ae37e71e724b/modules/mono/csharp_script.cpp#L1743-L1745

raulsntos avatar Nov 24 '22 15:11 raulsntos

The method described here fixed my issues, and since this Callable seems to have its docs updated, hence closing this issues.

TandersT avatar Feb 17 '23 11:02 TandersT