PubSubManager - Communication between Scripts in Unity

Let's talk about communication for once. This time, however, not in the interpersonal sense, but with regard to software. When it comes to developing software, communication between different scripts and pieces of code is essential. These scripts need to chat with each other, exchange data, and work together to solve complex problems.

It's like a team of coders working together to build great software. The way they communicate with each other can be done in a variety of ways, such as data transfer, function calls, or resource sharing. Good communication between scripts is key to developing powerful and flexible software that runs smoothly.

In the introduction, I had already written about the use of delegates and events within Awesome Accessories. But let's take a step back and look at the advantages and disadvantages of different types of communication.

Direct Access

First of all, there is the simplest variant of communication. Most of us will probably have already learned about it in one or the other tutorial. I'm talking about the use of public methods and variables. Let's imagine that we have a script A that needs to communicate with script B. We can then implement this for example as follows:

public class ScriptA : MonoBehaviour 
{ 
  public ScriptB otherScript; 

  private void Start() 
  { 
    // Access a method in ScriptB 
    otherScript.DoSomething(); 
  } 
} 

public class ScriptB : MonoBehaviour
{
  public void DoSomething() 
  { 
    // Code here 
  } 
}

This has several advantages:

  • Simple and straightforward to set up.

  • Allows direct access to methods and variables of other scripts.

But also some disadvantages:

  • Creates a direct dependency between scripts, resulting in tight coupling.

  • Potential problems if references are not set correctly or scripts are missing. For example, if objects are instantiated later.

Dynamic Access

We of course very often have the case that we can't reference scripts directly. For example, because they do not yet exist at the time of instantiation of the first script. Here we have several ways to deal with this.

Within the same GameObject, we can use GetComponent() to search for a specific MonoBehaviour. This is relatively quick. With FindObjectOfType() or FindWithTag() and other methods, we can also search the entire scene if we are dealing with different GameObjects. This is understandably incredibly slow. Here's an example on how this might look like:

public class ScriptA : MonoBehaviour 
{ 
  private ScriptB otherScript; 

  private void Start() 
  { 
    // Find ScriptB in the scene 
    otherScript = FindObjectOfType<ScriptB>(); 
    if (otherScript != null) { 
      // Access a method in ScriptB 
      otherScript.DoSomething(); 
    } 
  } 
}

This also offers some advantages:

  • Provides flexibility in dynamically searching and referencing scripts at runtime.

  • Allows reuse and flexible linking of scripts.

And, of course, has some disadvantages:

  • Can be slower compared to direct references, especially if used frequently.

  • Requires caution with multiple instances of a script, as the intended target object may not be found.

UnityEvents & Delegates

Then there is also the possibility to subscribe to UnityEvents or Delegates. This is especially useful when it comes to asynchronous processes. For example, if data has to be queried somewhere or something in ScriptB can change without ScriptA's interaction.

UnityEvents and delegates basically work the same way. A delegate is a language construct in C# that is a reference to one or more methods. A UnityEvent is a specialized implementation of an event system used for communication between Unity components, specifically for user interface (UI) interaction. The main difference is that you can edit UnityEvents in the Unity Editor, but you cannot edit Delegates.

public class ScriptA : MonoBehaviour 
{ 
  // Define an event delegate 
  public delegate void MyEventHandler(); 

  // Declare an event of the delegate type 
  public static event MyEventHandler OnMyEvent;

  private void Start() 
  { 
    // Raise the event 
    OnMyEvent?.Invoke(); 
  } 
} 

public class ScriptB : MonoBehaviour 
{ 
  private void OnEnable() 
  { 
    // Subscribe to the event 
    ScriptA.OnMyEvent += HandleMyEvent; 
  } 

  private void OnDisable() 
  { 
    // Unsubscribe from the event 
    ScriptA.OnMyEvent -= HandleMyEvent; 
  } 

  private void HandleMyEvent() 
  { 
    // Code to handle the event 
  }
}

Advantages:

  • Promotes loose coupling between scripts, which facilitates maintenance and reusability.

  • Provides a clear separation of responsibilities by decoupling the sender and receiver of events.

  • Allows multiple scripts to respond independently to the same event.

Disadvantages:

  • Requires more initial setup and management of events and delegates.

  • May introduce additional complexity for complex event hierarchies or sequences of event execution.

  • Debugging event-related problems can be more difficult than with direct references.

Publish-Subscribe-Service

Finally, there is the possibility to define one (or more) publish-subscribe services. A publish-subscribe service is a mechanism that allows different scripts to communicate with each other without being directly dependent on each other.

It works similarly to event-driven communication, but through a central mediator called the event bus. Scripts can subscribe to certain events (subscribe) and other scripts can trigger those events (publish). The event bus forwards the events to all registered scripts. Here we will have a look at the advantages and disadvantages.

Advantages:

  1. Loose coupling: EventBus enables loose coupling between scripts. Scripts do not need to know or depend on each other directly, which improves code flexibility and reusability.

  2. Simplified communication: the EventBus provides a central platform for communication between scripts. This makes communication easier, as scripts can publish events without explicitly referencing other scripts.

  3. Extensibility: by using the Event Bus, new scripts or functions can be easily added by registering for relevant events. This facilitates scalability and maintainability of the project.

  4. Decoupling: scripts can send events without having knowledge about the receivers. This promotes decoupling of components and makes the script structure more flexible and modular.

Disadvantages:

  1. Complexity: Using an event bus can add complexity, especially when many scripts are involved. It requires careful planning and organization to ensure that events are caught and processed properly.

  2. Performance: using an event bus can have an impact on performance, especially if many events need to be processed simultaneously. It is important to keep performance in mind and design the event bus efficiently to avoid unnecessary bottlenecks.

  3. Debugging: Debugging errors or unexpected behavior associated with the event bus can be more complex than with direct connections between scripts. It may require tracing event flows and verifying the registration of scripts.

  4. Potential for abuse: while an event bus provides flexibility, there is also a risk that it can be abused by creating too many events or defining events with too large a scope. Proper planning and structuring are required to make good use of the event bus.

When do I use which form of communication?

Now you are probably wondering when you should use which type of communication. I think the following guidelines can help you choose.

  1. Direct access

    • Use Public References when tight coupling between scripts is acceptable and direct access to methods and variables is needed.

    • Suitable if the relationship between scripts is stable and predictable and rarely changes.

  2. Dynamic access

    • Use FindObjectOfType or GetComponent when a more flexible and dynamic connection between scripts is required.

    • Useful when referencing other scripts needs to be done at runtime and/or scripts need to be reused.

  3. UnityEvents & Delegates

    • Use Event-driven Communication when loose coupling between scripts is desired and independent communication between them is required.

    • Useful when scripts should publish events and other scripts should respond to them independently.

  4. Publish-Subscribe Service (Event Bus):

    • Use the Publish-Subscribe Service (Event Bus) when a central communication instance is needed to mediate events between scripts.

    • Suitable for complex projects where a large number of scripts need to communicate with each other without having direct dependencies.

Using the PubSubManager

The PubSubManager in Awesome Accessories offers an easy way to build a global event bus. Of course you can also instantiate the PubSubService itself and thus run multiple event managers in parallel. Details can be found in the README of the module.

The PubSubManager offers the opportunity to publish events in different channels. The channels are defined in a global class. This is not mandatory, but makes development and refactoring easier.

If you want to define your own event, it must implement the IGameEvent interface:

using Aureola.PubSub;

public class CustomEvent : IGameEvent
{
    public string myVariable1;
    public string myVariable2;

    public CustomEvent(string myVariable1, string myVariable2)
    {
        this.myVariable1 = myVariable1;
        this.myVariable2 = myVariable2;
    }
}

Events can define any variable you want to pass around. The interface is only used to identify the events as such.

So you can publish event in a channel:

PubSubManager.service?.Publish("myChannel", new CustomEvent("foo", "bar"));

Other scripts can listen for events in that channel:

PubSubManager.service?.Subscribe("myChannel", OnCustomEvent);

The OnCustomEvent method is called whenever an event is published on the myChannel channel. Please note that different events can be published on the same channel. Therefore, you must always check the type of the event to decide whether you want to respond to it.

private void OnCustomEvent(IGameEvent gameEvent)
{
    if (gameEvent.GetType() == typeof(CustomEvent)) {
        var customEvent = (CustomEvent) gameEvent;
        Debug.Log(customEvent.myVariable1);
        Debug.Log(customEvent.myVariable2);
    }
}

You can unsubscribe from a channel:

PubSubManager.service?.Unsubscribe("myChannel", OnCustomEvent);

There is an auxiliary method to react only to certain events:

PubSubManager.service?.Subscribe("myChannel", OnCustomEvent, typeof(CustomEvent));

Please note that you will then have to unsubscribe from the channel in the same way:

PubSubManager.service?.Unsubscribe("myChannel", OnCustomEvent, typeof(CustomEvent));

You can clear a channel completely (this removes all registered listeners):

PubSubManager.service?.Clear("myChannel");

You can also reset the entire service (this will delete all channels):

PubSubManager.service?.Reset();

In general, it is a good idea to subscribe to the channel in the OnEnable method and to unsubscribe in the OnDisable method. This way you can be sure that only active GameObjects react to events.

public class MyAwesomeBehaviour : MonoBehaviour
{

    private void OnEnable()
    {
        PubSubManager.service?.Subscribe("myChannel", OnCustomEvent);
    }

    private void OnDisable()
    {
        PubSubManager.service?.Unsubscribe("myChannel", OnCustomEvent);
    }

    private void OnCustomEvent(IGameEvent gameEvent)
    {
        if (gameEvent.GetType() == typeof(CustomEvent)) {
            var customEvent = (CustomEvent) gameEvent;
            Debug.Log(customEvent.myVariable1);
            Debug.Log(customEvent.myVariable2);
        }
    }

}

Your Feedback is important!

What do you think of the services presented here? Is there anything missing or does something not work as expected? As always, I'm happy to listen to your feedback. Let me know what you think about this module. You can use the comment section below the article for this. You can also find other ways to contact me here. If you found a bug or want an enhancement, please create an issue in the GitHub repository. Further documentation can be found in the README of the corresponding module.

There are no comments yet.