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

Re-opening: Proper Godot Signal Support #288

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

lfod1997
Copy link

Re-opening of #199 in the belief it'll help people :)

@lfod1997
Copy link
Author

lfod1997 commented Jan 12, 2025

Reason for re-opening

Well, first, seems like few people saw my previous PR (I myself forgot it) :p

After using R3 for about 2 months, I came to the belief that this is still worth it. Signals allow cross-language programming with GDScript, and relying solely on Rx (even C#) is like throwing away lots of the flexibility that Godot provides. Besides, many of Godot's engine features are exposed as signals.

And yes I'm aware that my original implementation was premature and actually quite dirty. So I rewrote it almost entirely, utilizing existing R3 facilities as much as I could.

So please take a look.


Below is edited from the original PR:

Summary

Extension methods and utility classes for easy conversion from Godot signal to observable or CancellationToken.

Motivation

Although we can use FromEvent to create observable from any event:

var observable = Observable.FromEvent<WhoType>(
    h => myCheese.Touched += h,
    h => myCheese.Touched -= h
);

It:

  1. usually involves two capturing lambdas as there's no "FromEventWithState<T, TState>"; otherwise 2 methods must be introduced just for this
  2. requires a Func<Action<T>, TDelegate> conversion (= more boilerplate!) if the signal has more than one event arg
  3. is verbose, is less declarative
  4. does multiple connecting (GodotObject.Connect) if multiple subscribers
  5. will not call OnComplete unless canceled by a CancellationToken, and making one that cancels on TreeExited requires even more code

Prefer this:

var observable = myCheese.SignalAsObservable<WhoType>(Cheese.SignalName.Touched);
  1. no capturing
  2. no converters when multi-arg
  3. DRY syntax, very much declarative
  4. connects once, to a backing Subject's OnNext
  5. finishes automatically when Node "myCheese" dies and emits TreeExited
  6. refers to Godot's SignalName explicitly

Additionally, support CancellationToken, to allow the stream to be finished even for general GodotObjects that do not have TreeExited:

var observable = myCheese.SignalAsObservable<WhoType>(Cheese.SignalName.Touched, funnyToken);

If no token is provided (or "funnyToken" is actually None token), the stream automatically gets disposed when all subscriptions are disposed.

For convenience, I also provide a CancelOnSignal extension to allow generating CancellationTokens that cancel on any signal:

var funnyToken = this.CancelOnSignal(Node.SignalName.Ready); // cancels when this `Node` emits `Ready`

which can also be used with RegisterTo:

public override void _Ready()
{
    base._Ready();
    // one-statement signal wiring:
    Disposable.Combine(
        _game.ScoreObservable
            .Subscribe(this, (score, self) => self.ScoreLabel.Text = score.ToString()),
        _game.SignalAsObservable(Game.SignalName.GameOver)
            .Subscribe(this, (_, self) => self.DisplayGameOverBanner()),
        // ... lots of subscriptions
    ).RegisterTo(HudRoot.CancelOnSignal(Node.SignalName.TreeExited)); // easy cancellation on any signal
}

@lfod1997 lfod1997 changed the title Re-opening: Godot Signal as Observable Re-opening: Proper Godot Signal Support Jan 13, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant