Optimal way of handling custom signals in Godot C#/Mono

Godot Version

v4.4.1.stable.mono.official [49a5bc7b6]

Question

I’m trying to figure out what is the best way of handling custom signals in Godot C#/Mono. The entire idea of signals is to decouple stuff, which I like. But in order to receive signal, you need reference to where it is located.

Example: player can pick up stuff (like coins).

[Signal]
public delegate void PickupAttemptedEventHandler(PickupType pickupType, Pickup pickup);
private void OnBodyEntered(Node body)
{
   //no need to check what is it because layers will do that
   EmitSignal(SignalName.PickupAttempted, (int)PickupType, this);
}

And now from Player’s perspective, I need a reference.

public override void _Ready()
{
    foreach (var pickup in GetTree().GetNodesInGroup("Pickup"))
    {
        if (pickup is Pickup)
        {
            var pickupRef = pickup as Pickup;
            pickupRef.PickupAttempted += HandlePickup;
        }
    }
}

I don’t have an example to show, but I have found out you can just create an autoload singleton class that contains all signals. This solves the problem above, meaning I don’t have to search for the signal, it’s in one dedicated place, that can be accessed from everywhere. But as the game grows, so will the singleton, and I don’t know how good it will be for the memory.

I wanted to ask which option is better, or maybe there is something else I don’t know about. In theory it works similarly to business programming (C# is C# everywhere, with exceptions of some specific frameworks), but games are much more dynamic, and I don’t know what is the best approach to handle things in this dynamic environment.

A better way would be to use an EventBus. I can show you an example of one I’ve been using in my game recently.

The event bus implementation itself is very simple.

public static class EventBus
{
    private static readonly Dictionary<Type, List<Delegate>> Subscribers = new();

    public static void Subscribe<T>(Action<T> callback)
    {
        Type type = typeof(T);
        if (!Subscribers.TryGetValue(type, out List<Delegate> value))
        {
            value = [];
            Subscribers[type] = value;
        }

        value.Add(callback);
    }

    public static void Unsubscribe<T>(Action<T> callback)
    {
        Type type = typeof(T);
        if (Subscribers.TryGetValue(type, out List<Delegate> list))
        {
            list.Remove(callback);
        }
    }

    public static void Publish<T>(T evt)
    {
        Type type = typeof(T);
        if (!Subscribers.TryGetValue(type, out List<Delegate> list))
        {
            return;
        }
        foreach (Delegate cb in list.ToArray())
        {
            ((Action<T>)cb)?.Invoke(evt);
        }
    }
}

Then, you need to write some basic events. In this example, we’ll write an event that gets fired when the player jumped, but it can be anything.

public class PlayerJumpedEvent
{
    public Vector2 JumpStartingPosition { get; set; }
}

Then, in another part of your code, you can publish this event like so:

EventBus.Publish(new PlayerJumpedEvent()
{
    JumpStartingPosition = new Vector2(Position.X, Position.Y)
});

And in another part of your code without having ANY reference to what published the event, you can simply do this:

EventBus.Subscribe<PlayerJumpedEvent>(OnPlayerJumped);

// And somewhere else the OnPlayerJumped function you create

private void OnPlayerJumped(PlayerJumpedEvent obj)
{
    GD.Print(obj.JumpStartingPosition);
}
2 Likes

This is not far from what I meant initially, except your version is tidy, where in my version you just create one class where you define the events, instead of “sending” them to the bus.

I think the best thing to do is to use best of both worlds version:

  • Use the bus in situations where getting the reference would be troublesome
  • Use normal events in situations where getting reference is easy (this will prevent the bus from growing too much)

And I think I will just use default C# delegates instead of Godot signals. One advantage is that they work with enums without screaming that it needs to be converted to int.

1 Like

My solution doesn’t use godot signals because I find them incredibly annoying and bad to work with when you use C#, I much more prefer pure events as well.
I do hope that one day signals will be made much better for C# as well.

3 Likes

You can’t see C# events in Godot GUI, but I don’t use it to connect signals because for me seeing those connections in code makes everything easier to manage and debug.

I will mark your post as solution, but as I said I will use mixed approach. In very small and simple scenes it’s easier to get reference (sometimes you won’t even really need a signal), in large scenes (like entire level) using bus seems to be more reasonable than trying to do stuff like searching through entire tree.

1 Like

Yes Event bus is the way to go.

However, and this is pretty important, if you are passing any parameters to Godot signals they will create an array which will allocate on the heap. So if you are doing this a lot I would recommend just using C# events instead.

I moved all of the signals in my event bus on my current project to pure C# events tbh. They are a little more fiddly to set up, but will be more performant from a garbage collection point of view.

Signals are fine if you are not passing any parameters on, but as stated, anything else just use events instead.

3 Likes

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.