Unity and C# attaching multiple events to one listener. Is there a more efficient way?

Viewed 728

I am creating a game in Unity using C#. The has a grid-like board (think Chess). Each square in the board is responsible for itself (selection status, animations, events) and will emit events (e.g. when selected it will emit an OnSelect event) to indicate what's going on.

There is also an overall BoardManager class, which needs to monitor those events and respond to them.

As I understand it, in the BoardManager class, I would need:

  1. To create a separate reference for each of those Square classes, so that I can monitor the events they output
  2. Subscribe to each event from each square to the required callback function in BoardManager
  3. Remember to unsubscribe each event on Destroy

I can do this using a foreach loop, it just "feels wrong".

I was hoping that there is a way to, rather making 50 object references and 50 event subscriptions, to be able to just tell it to subscribe to any instantiated version of that particular object (e.g. Any instance of Square), so that I can reference it that ONE time, and subscribe to it ONE time. Does that make sense?

1 Answers

As @Jesse and @Nigel Bess in the comment mentioned static events and foreach are both good, and valid solutions that both have its upsides and downsides regarding code readability, usability, and performance.

SINGLE EVENT

// Manager.cs
// You can use Singleton pattern or make OnSelect static

public Action<Cell> OnSelect;

void Start()
{
    OnSelect += OnSelectAction;
}

void OnDestroy()
{
    OnSelect -= OnSelectAction;
}

// Cell.cs
void Update()
{
    if(isSelected)
    {
       _manager.OnSelect(this);
    }

}

This approach has a significant advantage in that you clearly separate the responsibility of each object. Cell manages its own state. And you just receive a callback to when stuff happened.

This however you need to make sure that _manager exists in this context or prevent testability and extendability by using Singleton.

This approach would be pretty clean to implement and execute but as all delegates have they are hard to debug and manage for potential issues.

FOREACH CELL EVENT

// Manager.cs

void InitializeCell(Cell cell)
{
    cell.OnSelect += OnSelectAction;
}

void DestroyCell(Cell cell)
{
    // If an object would be destroyed via Unity's Destroy, this is not needed
    cell.OnSelect -= OnSelectAction;
}

// Cell.cs
public event Action<Cell> OnSelect;

void Start(){
    _manager.InitializeCell(this);
}

void OnDestroy(){
    _manager.DestroyCell(this);
}

void Update()
{ 
    if(isSelected && OnSelect != null)
    {
       OnSelect(this);
    }
}

This approach is a bit messier, as you not only need to assume that _manager exists in this context but also that OnSelect is subscribed in the manager for a given instance.

This however gives you an option to stop receiving OnSelect for any selected instance directly from the Manager class. That is not possible with the first approach. In the first approach, you would need to process the input via if cell == foo or via cell class itself.

This method is potentially slower if given Action pointer is not stored in CPU cache. So its speed would be on par or up to 100ns slower per cell than the singleton approach, but I would argue that it's not noticeable to be of concern.

A different method of implementing foreach approach would be with foreach. There is no difference with speed of initialization and reduces dependency in Cell for Manager

// Manager.cs

void Start()
{
    foreach(var cell in Cells)
    {
        cell.OnSelect += OnSelectAction;
    }
}

// Cell.cs
public event Action<Cell> OnSelect;

void Update()
{
    if(isSelected && OnSelect != null)
    {
       OnSelect(this);
    }
}

In this approach, management of the lifetime of the cell might get complicated because we don't know when it will be destroyed.

DIRECT APPROACH

There is also an option to do it directly, if we assume that we have access to Manager either via Singleton or OnSelectAction is static. This option is very easily debuggable (where actions are not), but has the same problems as first approach.

// Manager.cs
// You can use Singleton pattern or make OnSelect static

void OnSelectAction(Cell cell)
{
   ...
}

// Cell.cs
void Update()
{
    if(isSelected)
    {
       _manager.OnSelectAction(this);
    }

}

This approach would be the fastest to execute and would have a lower memory footprint on the application as you would not have to store much data. It's also cleanest to read and implement.

Related