Event Based Architecture in Unity

Creating Event-Based Architecture in Unity with Event Args

Event-based architecture is a widely used design pattern in software development, and it has been adopted by many game developers as well. This pattern involves the use of events to trigger actions and reactions between different components of an application. Unity, one of the most popular game engines, provides an event system that developers can use to implement event-based architecture in their projects.

In this blog post, we will explore the basics of event-based architecture in Unity and show how event args can be used to enhance the functionality of the event-based architecture. We’ll also provide a practical example to help you understand how to implement event args in Unity.

What is Event-Based Architecture?

Event-based architecture is a programming pattern that simplifies code maintenance and scalability by decoupling components of the system using events. This pattern makes it easy to add new features or remove existing ones without causing any breaking changes. In event-based architecture, when an event occurs, all subscribed components (i.e., listeners) receive a notification to perform an action. This way, components can respond to changes without knowing about other components or the system’s internal workings.

In Unity, this pattern is commonly used in UI systems, animation systems, and game mechanics that rely on player input. An excellent example of using an event-based architecture in Unity is creating a health system for a game character.

Health System Implementation with Event-Based Architecture

A health system is a vital component of any game, and creating one that’s flexible and easy to maintain is essential. Let’s implement a basic health system using event-based architecture in Unity. To achieve this, we’ll create a ‘HealthSystem‘ a class that manages the character’s health, defines a ‘HealthChangedEventArgs‘ a class that holds the current health value, create a ‘HealthUI’ a class that updates the UI whenever the health changes, and then subscribes to the ‘HealthUI‘ class to the ‘HealthSystem‘ class using the ‘EventBus‘ class.

The HealthSystem Class

First, we’ll create a ‘HealthSystem‘ a class that manages the character’s health. We’ll define an event named ‘HealthChanged’ that will notify all listeners whenever their health changes. We’ll also define the ‘TakeDamage‘ and Heal methods to update the character’s health and raise the ‘HealthChanged‘ event whenever the health changes.

public class HealthSystem : MonoBehaviour
{
    public event EventHandler<HealthChangedEventArgs> HealthChanged;
    public int MaxHealth { get; private set; }
    private int _currentHealth;

    public int CurrentHealth
    {
        get => _currentHealth;
        private set
        {
            if (_currentHealth != value)
            {
                _currentHealth = Mathf.Clamp(value, 0, MaxHealth);
                HealthChanged?.Invoke(this, new HealthChangedEventArgs(_currentHealth));
            }
        }
    }

    private void Start()
    {
        MaxHealth = 100;
        CurrentHealth = MaxHealth;
    }

    public void TakeDamage(int damageAmount)
    {
        CurrentHealth -= damageAmount;
    }

    public void Heal(int healAmount)
    {
        CurrentHealth += healAmount;
    }
}

As you can see, we have an event called ‘HealthChanged‘ that is raised whenever the character’s health changes. We also have a 'MaxHealth‘ property and a ‘CurrentHealth‘ property that is clamped between 0 and ‘MaxHealth‘. Finally, we have ‘TakeDamage‘ and Heal methods that decrease and increase the ‘CurrentHealth‘, respectively.

The HealthChangedEventArgs Class

We also need to create a 'HealthChangedEventArgs‘ a class that we will use to pass the current health value to any listeners of the HealthChanged event.

public class HealthChangedEventArgs : EventArgs
{
    public int CurrentHealth { get; }

    public HealthChangedEventArgs(int currentHealth)
    {
        CurrentHealth = currentHealth;
    }
}

The HealthUI Component

Now that we have created the health system, we need to create a ‘HealthUI‘ a component that listens to the ‘HealthChanged‘ event and updates the health UI on the screen.

public class HealthUI : MonoBehaviour
{
    [SerializeField] private TextMeshProUGUI _healthText;
    private HealthSystem _healthSystem;
    private void Start()
    {
        _healthSystem = FindObjectOfType<HealthSystem>();
        EventBus.Subscribe<HealthChangedEventArgs>(OnHealthChanged);
    }
    private void OnDestroy()
    {
        EventBus.Unsubscribe<HealthChangedEventArgs>(OnHealthChanged);
    }
    private void OnHealthChanged(object sender, HealthChangedEventArgs e)
    {
        _healthText.SetText($"Health: {e.CurrentHealth}");
    }
}

We use the ‘FindObjectOfType‘ method to find the ‘HealthSystem‘ object in the scene and then subscribe to the ‘HealthChanged‘ event using the ‘EventBus.Subscribe‘ method. In the ‘OnHealthChanged‘ method, we update the health text on the screen using the SetText method.

The ‘OnDestroy‘ method is used to unsubscribe from the ‘HealthChanged‘ event when the 'HealthUI‘ component is destroyed. This is important to prevent memory leaks.

The EventBus Class

Finally, we’ll create the ‘EventBus‘ a class that handles event subscriptions, unsubscriptions, and raising. This class is a static class that uses a dictionary to store a list of handlers for each event type. Here’s the code:

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

    public static void Raise<T>(object sender, T args) where T : EventArgs
    {
        Type eventType = typeof(T);
        List<Delegate> handlers = null;

        lock (_lock)
        {
            if (!_handlers.TryGetValue(eventType, out handlers))
                return;

            for (int i = 0; i < handlers.Count; i++)
            {
                var handler = handlers[i];
                if (handler.Target != null)
                    handler.DynamicInvoke(sender, args);
            }
        }
    }

    public static void Subscribe<T>(Action<object, T> handler) where T : EventArgs
    {
        Type eventType = typeof(T);
        lock (_lock)
        {
            if (!_handlers.TryGetValue(eventType, out var handlers))
            {
                handlers = new List<Delegate>();
                _handlers.Add(eventType, handlers);
            }

            handlers.Add(handler);
        }
    }

    public static void Unsubscribe<T>(Action<object, T> handler) where T : EventArgs
    {
        Type eventType = typeof(T);
        lock (_lock)
        {
            if (_handlers.TryGetValue(eventType, out var handlers))
            {
                handlers.Remove(handler);

                if (handlers.Count == 0)
                    _handlers.Remove(eventType);
            }
        }
    }
}

The ‘Raise‘ method raises an event by invoking all the handlers that are subscribed to that event. The ‘Subscribe’ method subscribes a handler to an event and the ‘Unsubscribe‘ method unsubscribes a handler from an event.

Conclusion

In this post, we showed you how to implement an event-based architecture in Unity. We created a health system for a game character that uses events to communicate changes in health to other components in the game. We also created a ‘HealthUI‘ a component that listens to the ‘HealthChanged‘ event and updates the health text on the screen.

Finally, we created an ‘EventBus‘ the class that handles event subscriptions, unsubscriptions, and raising. Using an event-based architecture like this can make your game code more maintainable and scalable, as well as easier to debug.

I hope this post has been helpful to you. Happy coding!

Leave a Comment