Unity 6.3
0 онлайн 89 гостей 3 в системе
Вход
Модульная игровая архитектура на ScriptableObjects Глава 7 из 12 Оригинал, стр. 45

Паттерн «Наблюдатель»

Русский

Паттерн «Наблюдатель»

При разработке игры нескольким объектам GameObject часто требуется обмениваться данными или состояниями. В небольшой игре между ними можно создать прямые ссылки, но при масштабировании этот подход быстро становится неудобным. Управление такими зависимостями требует значительных усилий и нередко приводит к ошибкам. По мере роста приложения потребуется более эффективное . решение.

Отказ от синглтонов Многие разработчики выбирают синглтон, то есть единственный глобальный экземпляр класса, который сохраняется при загрузке сцен. Однако синглтоны создают глобальное состояние и затрудняют модульное тестирование. Если префаб ссылается на синглтон, для проверки одной изолированной функции придется подключить все зависимости этого синглтона. Это снижает модульность, а отладка кода усложняется. В качестве альтернативы для обмена данными между объектами можно использовать события на основе ScriptableObject.

Подробнее о синглтонах Синглтоны в разработке игр на Unity часто вызывают споры. Они могут быть подходящим решением для небольших проектов или прототипов, но в крупных приложениях недостатки обычно перевешивают преимущества. Поэтому многие разработчики считают синглтон антипаттерном.

Синглтоны легко изучить и понять, однако при неправильном применении они создают проблемы. Большинство описанных здесь паттернов помогает обходиться без синглтонов. Если нужен простой доступ к общим данным, рассмотрите Runtime Set на основе ScriptableObject, описанный далее. Для обмена сообщениями между объектами подойдет канал событий на основе ScriptableObject. Переход от синглтонов к такой архитектуре может повысить масштабируемость и тестируемость проекта.

Подробнее о преимуществах и недостатках синглтонов см. в электронной книге «Повышаем уровень кода с паттернами проектирования и SOLID».

События на основе ScriptableObject Как вы уже видели, ScriptableObject предназначен не только для работы с данными. Как и любой другой скрипт, он может содержать методы, через которые объекты будут обмениваться сообщениями. В паттерне «Наблюдатель» субъект передает сообщение одному или нескольким слабо связанным наблюдателям. Каждый наблюдатель реагирует независимо от субъекта и ничего не знает об остальных наблюдателях. Субъект также называют издателем или отправителем, а наблюдателей - подписчиками или слушателями.

Архитектура на основе событий выполняет код только при необходимости, а не в каждом кадре. Поэтому она часто эффективнее логики, добавленной в методы обновления MonoBehaviour. Подписчики Наблюдатель

Событие

Издатель

Субъект

Наблюдатель

Наблюдатель

Базовый паттерн «Наблюдатель»

Паттерн «Наблюдатель» можно реализовать с помощью MonoBehaviour или объектов C#. Такой подход широко применяется в Unity, но при использовании одних скриптов дизайнеры будут обращаться к программистам для создания каждого события игрового процесса. Альтернативой служат события на основе ScriptableObject. Это удобный для дизайнеров способ настроить паттерн «Наблюдатель»: ScriptableObject выступает посредником между субъектом и наблюдателем и предоставляет графический интерфейс в редакторе Unity.

Подписчики Наблюдатель

Канал событий

Издатель

Субъект

Событие

Событие

Наблюдатель

ScriptableObject Наблюдатель

Канал событий на основе ScriptableObject

На первый взгляд кажется, что в паттерне «Наблюдатель» появился лишь дополнительный служебный слой, однако такая структура дает ряд преимуществ. ScriptableObject является ассетом, поэтому доступен всем объектам в иерархии сцены и не исчезает при загрузке другой сцены. Именно ради простого постоянного доступа к определенным ресурсам разработчики часто используют синглтоны. ScriptableObject нередко дает те же преимущества, не создавая столько лишних зависимостей. В событиях на основе ScriptableObject издателем, который отправляет событие, и подписчиком, который его принимает, может быть любой объект. ScriptableObject находится между ними, передает сигнал и выступает единым посредником.

Такую структуру удобно представить как канал событий. ScriptableObject подобен радиовышке, сигналы которой принимает любое количество объектов. Заинтересованный MonoBehaviour подписывается на канал событий и реагирует, когда что-либо происходит.

Пример: каналы событий Любой ScriptableObject со следующими элементами может работать как канал событий: - Делегат UnityAction или System.Action: он уведомляет подписчиков и передает нужные данные в параметрах. UnityAction удобнее для художников и дизайнеров; в остальных случаях хорошо подходит System.Action. Ключевое слово event ограничивает делегат: вызвать его можно только из класса ScriptableObject, в котором он объявлен, или из производного класса. - Метод отправки события: этот открытый метод вызывает делегат. Вот и все. Можно создать любое количество каналов событий для управления разными аспектами игрового процесса. Объекты ScriptableObject существуют на уровне проекта, поэтому могут отправлять глобально доступные события. Так можно связывать даже не связанные напрямую объекты сцены без ущерба для масштабируемости.

System.Action или UnityAction System.Action - универсальный тип делегата, определенный в пространстве имен System платформы .NET Framework. Его можно использовать в проектах Unity без объявления собственного делегата. Ключевое слово event делает делегат доступным только для чтения: другие объекты могут подписываться на него, регистрируя свои методы, но не могут вызывать делегат напрямую.

UnityAction - тип делегата, определенный специально в пространстве имен UnityEngine.Events. Обычно он применяется вместе с классом UnityEvent, который предоставляет еще один способ создавать события в Unity. UnityEvent и UnityAction отображаются в окне Inspector, поэтому с их помощью часто удобнее реализовывать паттерн «Наблюдатель» для пользователей, художников и дизайнеров.

В зависимости от задачи можно использовать System.Action, UnityAction или оба типа в одном проекте. Если требуется универсальный делегат, не привязанный к игровому движку Unity, используйте System.Action. Если делегат предназначен специально для UnityEvent, выберите UnityAction.

В этом примере VoidEventChannelSO отправляет событие без параметров. Он содержит UnityAction с именем OnEventRaised.

C#
[CreateAssetMenu(menuName = "Events/Void Event Channel")] public class VoidEventChannelSO : ScriptableObject {
    public event UnityAction OnEventRaised;
    public void RaiseEvent() {

if (OnEventRaised != null)

C#
OnEventRaised.Invoke();
}
}

После создания ScriptableObject типа VoidEventChannelSO любой MonoBehaviour сможет подписаться на OnEventRaised. Например, можно создать ScriptableObject StartNewGame типа VoidEventChannelSO. Другой объект может вызвать открытый метод RaiseEvent и тем самым отправить событие.

Событие на основе ScriptableObject: пример канала событий

Другой MonoBehaviour может сослаться на ScriptableObject канала событий в окне Inspector, а затем подписаться на OnEventRaised или отменить подписку.

Когда канал событий вызывает OnEventRaised, в ответ выполняется StartNewGame: public class StartGame : MonoBehaviour { [SerializeField] private VoidEventChannelSO m_onNewGameButton = default;

C#
private void Start() {

m_onNewGameButton.OnEventRaised += StartNewGame;

C#
private void OnDestroy() {

m_onNewGameButton.OnEventRaised -= StartNewGame;

C#
private void StartNewGame() {
    // здесь размещается логика загрузки уровня...

}
}
Чтобы сделать компоненты-слушатели удобнее для художников и дизайнеров, можно создать MonoBehaviour, не требующий дополнительной настройки скриптом. Класс VoidEventListener не добавляет новых функций, но предоставляет поля, доступные в окне Inspector:
public class VoidEventListener : MonoBehaviour {
    [SerializeField] private VoidEventChannelSO m_channel = default;
C#
public UnityEvent OnEventRaised;
C#
private void OnEnable() {

if (m_channel != null)

m_channel.OnEventRaised += Respond; }

C#
private void OnDisable() {

if (m_channel != null)

m_channel.OnEventRaised -= Respond; }

C#
private void Respond() {

if (OnEventRaised != null)

C#
OnEventRaised.Invoke();
}
}

Добавьте VoidEventListener на GameObject, затем перетащите ScriptableObject канала событий в поле _channel окна Inspector. Чтобы реагировать на событие, добавьте в OnRaisedEvent нужные обработчики UnityAction.

Слушатель событий позволяет специалисту без навыков программирования настраивать действия, запускаемые событиями.

Независимо от того, какой компонент выбран в качестве слушателя, каналы событий обеспечивают взаимодействие между объектами во время выполнения. Игрок выполнил задание или заработал очко? Игра завершилась? Событие может уведомить любой GameObject в сцене, которому нужна эта информация.

Поскольку такие события на основе ScriptableObject являются ассетами уровня проекта, они могут управлять значительной частью инфраструктуры приложения. Это особенно удобно для обмена сообщениями между системами, лежащими в основе архитектуры игры.

Распространенные системы управления: - Управление звуком: самые разные события в игре могут запускать звуки. Эта система может воспроизводить объекты AudioClip или изменять параметры AudioMixer в ответ на события приложения.

- Управление сценами: эта система отвечает за загрузку и выгрузку сцен Unity и игровых уровней. - Управление UI: эта система отвечает за экраны меню до начала игры, во время нее и после завершения. - Управление сохранениями: эта система отвечает за сохранение игровых данных и настроек в файловой системе, а также за их загрузку. Все эти системы решают разные задачи, но им необходимо взаимодействовать. События служат связующим звеном между ними. Дополнительные примеры реализации собственных событий на основе ScriptableObject приведены в сопутствующем примере проекта.

GameManager из примера проекта использует каналы событий.

События с разной полезной нагрузкой позволяют передавать данные разных типов. Например, среди событий на основе ScriptableObject есть IntEventChannelSO, Vector2EventChannelSO, VoidEventChannelsSO и другие. Выбор события зависит от контекста.

Создавайте дополнительные типы событий с учетом игровой логики. Например, событию нанесения урона могут понадобиться сведения о том, кто нанес урон и какова его величина.

Варианты применения каналов событий ограничены только вашей фантазией. Помимо перечисленных основных систем, события нередко помогают связать совершенно разные игровые подсистемы и наладить их взаимодействие:

- Камеры: позволяют создавать выразительные и кинематографичные эффекты, например тряску камеры или переход к другому ракурсу. - Задания: задачи или цели, которые игрок должен выполнить, чтобы продвинуться по игре или получить награду. Задания часто объединяют разные элементы игрового процесса, например сбор предметов, победу над противниками или решение головоломок. - Здоровье: этот важный элемент многих игр связывает игрока, противников, а также любые объекты и действия, способные нанести игроку урон. - Достижения: как и задания, это особые награды, которые игроки открывают за выполнение определенных задач или целей. Достижения могут охватывать разные элементы игрового процесса, например достижение заданного уровня или накопление определенного количества очков. В свою очередь, эти элементы игрового процесса взаимодействуют через события с другими системами управления, включая звук, UI и сохранения. Такой подход обеспечивает модульность и независимость компонентов архитектуры, сохраняя при этом возможность обмениваться данными с другими системами.

Отладка каналов событий Пользовательский Editor или PropertyDrawer может добавить в окно Inspector кнопку «Вызвать событие». С ее помощью событие можно запускать вручную при отладке. Ниже приведен простой скрипт Editor, который добавляет в окно Inspector специальную кнопку для VoidEventChannelSO: [CustomEditor(typeof(VoidEventChannelSO))]

C#
public class VoidEventChannelSOEditor : Editor {
    public override void OnInspectorGUI() {
        DrawDefaultInspector();
        VoidEventChannelSO eventChannel = (VoidEventChannelSO)target;
        if (GUILayout.Button("Raise Event")) { eventChannel.RaiseEvent(); }
    }
}

В результате появляется кнопка, с помощью которой событие можно вызвать в любой момент, что упрощает поиск проблем во время выполнения.

Кнопка, добавленная пользовательским Editor, помогает тестировать каналы событий.

Добавив немного кода, можно создать кнопки и для каналов событий, передающих данные. Предусмотрите в самом канале поле для отладочного значения, а затем передайте его в скрипт Editor. Примеры такой реализации есть в проектах SOAP и Unity Atoms.

По мере использования каналов событий для разделения зависимостей между объектами стоит развивать средства отладки, например вести список всех слушателей каждого события. В класс канала событий можно добавить методы регистрации и отмены регистрации объектов, чтобы во время выполнения было проще определить, какие события вызывают конкретное поведение.

Пример: InputReader Объектам, которые обрабатывают пользовательский ввод, нужен специализированный тип канала событий. Input System в Unity использует InputAction, чтобы представлять необработанные входные данные в виде логических действий, например прыжка или ходьбы.

Каждый InputAction, в свою очередь, содержит собственные события started, performed и canceled.

Настройка действий (Actions) и карт действий (Action Maps) в редакторе Input System

Для обработки привязок InputAction можно создать специальный ScriptableObject InputReader. Он снова выступает посредником между субъектом и наблюдателями. Однако в этом случае компоненты MonoBehaviour не вызывают события напрямую. Вместо этого роль субъекта, то есть отправителя событий, выполняет Input System:

Наблюдатель

Ресурс Game Input

Действие Move

Input Reader

Событие OnMove

Наблюдатель

ScriptableObject

Unity Input System

Наблюдатель

MonoBehaviour

ScriptableObject InputReader ретранслирует события от объектов InputAction.

Здесь в Input System настраиваются действия (Actions) и карты действий (Action Maps). Каждый InputAction описывает отдельную ось ввода и связывается с клавиатурой, геймпадом или любым другим устройством ввода.

Вместо прямой подписки на InputAction контроллеры ракеток подписываются соответственно на события OnMoveP1.performed и OnMoveP2.performed.

Геймпад

Изменения Input System затрагивают только InputReader

Ресурс Game Input

Input Actions

Input Reader

Событие OnMoveP1

Событие OnMoveP2

Сенсорный экран ScriptableObject

Событие OnMove

Unity Input System

Клавиатура

Ввод

MonoBehaviour

InputReader изолирует объекты от прямой зависимости от системы ввода.

В результате InputReader задает единый способ обработки действий с геймпада и клавиатуры. Каждый GameObject, которому нужен ввод: - хранит ссылку на ScriptableObject InputReader; - подписывается на нужные события и привязывает к ним свои методы-обработчики. Для небольшой игры такой паттерн может показаться избыточным, но этот пример помогает лучше понять принцип его работы. Преимущества станут очевидны, когда проект разрастется и в нем появится гораздо больше компонентов. Отделение ввода от использующих его GameObject делает систему гибче и упрощает повторное использование компонентов. Если во время разработки потребуется изменить InputAction, достаточно сопровождать только InputReader. Если сами события менять не требуется, объекты-слушатели не затрагиваются. Поэтому поддерживать связь между вводом и наблюдателями становится проще, особенно если наблюдателей много.

C#
Статические и нестатические события Можно использовать статические события, чтобы слушающим объектам не приходилось искать ScriptableObject. Например, в методе OnEnable компонент MonoBehaviour может подписаться на статические события MoveP1Event и MoveP2Event класса InputReader: InputReader.MoveP1Event += OnMoveP1;
InputReader.MoveP2Event += OnMoveP2;
При работе со статическими событиями особенно тщательно управляйте подписками. Не забудьте отписаться в OnDisable: InputReader.MoveP1Event -= OnMoveP1;
InputReader.MoveP2Event -= OnMoveP2;
Статические события всегда остаются доступными и при наличии активных подписчиков не освобождаются сборщиком мусора. Забытые подписчики будут препятствовать очистке до завершения работы приложения. Однако статические события не сериализуются. Если нужна интерактивная работа в редакторе Unity, используйте нестатические события и укажите в окне Inspector ссылку на соответствующий ScriptableObject.
English

The Observer pattern

When developing a game, it’s common to have multiple GameObjects that need to share data or states with each other. In a small game, you can make direct references between these objects, but this doesn’t scale very well. Managing these dependencies can require significant effort and is often a source of bugs. You’ll need a better solution as your application grows in size.

Avoiding singletons Many developers opt to use singletons – one global instance of a class that survives scene loading. Singletons, however, introduce global states and make unit testing difficult. If you’re working with a prefab that references a singleton, you’ll end up importing all of its dependencies just to test an isolated function. This reduces modularity and makes your code harder to debug. As an alternate solution you can use ScriptableObject-based events to help your objects communicate.

More on singletons The subject of singletons in Unity game development is often cause for debate. Singletons can be a suitable solution for smaller projects or prototyping. In large applications, the cons of using singletons often outweigh their advantages. Many developers consider the singleton to be an anti-pattern for this reason. Singletons are easy to learn and understand but can introduce issues when they’re used incorrectly. Most of the patterns described here will help you avoid relying on singletons. If you want easy access to shared data, consider a Runtime Set based on ScriptableObjects (see below). If you need a way to send messages between objects, try a ScriptableObject-based event channel. Restructuring your architecture away from singletons may improve scalability and testability. Read the e-book Level up your code with design patterns and SOLID to learn more about the pros and cons of singletons.

ScriptableObject-based events As you’ve already seen, ScriptableObjects aren’t just for handling data. They can contain methods, just like any other script. These methods can serve as a means for objects to communicate. In the observer design pattern, a subject broadcasts a message to one or more loosely coupled observers. Each observing object can react independently of the subject but is unaware of the other observers. The subject can also be referred to as the “publisher” or “broadcaster.” The observers are also known as “subscribers” or “listeners.” An event-based architecture only executes when needed, rather than running each frame. For this reason, it’s often more optimized than adding logic to a MonoBehaviour’s update methods.

The basic observer pattern

You can implement the observer pattern with MonoBehaviours or C# objects. While this is already common practice in Unity development, a script-only approach means your designers will rely on the programming team for every event needed during gameplay. An alternative is to create ScriptableObject-based events. This is a designer-friendly way to set up the observer pattern. Here, the ScriptableObject works as an intermediary between subject and observer, providing a graphical interface in the Editor.

The ScriptableObject-based event channel

While at first glance it appears that you’ve only added a layer of overhead to the observer pattern, this structure offers some advantages. Because ScriptableObjects are assets, they are accessible to all objects in your Scene Hierarchy and don’t disappear on scene loading. This is why many developers use singletons in the first place: easy, persistent access to certain resources. ScriptableObjects can often provide the same benefits without introducing as many unnecessary dependencies. In ScriptableObject-based events, any object can serve as publisher (which broadcasts the event), and any object can serve as a subscriber (which listens for the event). The ScriptableObject sits in the middle and helps relay the signal, acting like a centralized intermediary between the two. One way to think about this is as an “event channel.” Imagine the ScriptableObject as a radio tower that has any number of objects listening for its signals. An interested Monobehaviour can simply subscribe to the event channel and respond when something happens.

Example: Event channels Any ScriptableObject that includes the following can function as an event channel: A delegate (UnityAction or System.Action): This notifies subscribers and passes the appropriate data as parameters. Use a UnityAction for a more artist-friendly experience; otherwise, the System.Action delegate works well here.

The event keyword limits the delegate so that it can only be invoked from within the ScriptableObject class (or derived class where it’s declared). An event-raising method: This public method invokes the delegate.

And that’s it. You can set up any number of event channels to determine various aspects of gameplay. Because they exist at the project level, ScriptableObjects can raise events that are globally accessible. This can connect otherwise unrelated objects in the scene in a scalable way.

System.Action or UnityAction System.Action is a general purpose delegate type defined in the .NET Framework’s System namespace. It can be used in your Unity projects without needing to declare a custom delegate. Adding the event keyword makes the delegate type read only; other objects can listen for the delegate’s registered methods, but they can’t invoke those methods directly. UnityAction is a delegate type that’s specifically defined within the UnityEngine.Events namespace. You will typically use it with the UnityEvent class, which is an alternative means of creating events in Unity. UnityEvents and UnityActions appear in the Inspector, so they often serve as a more user- or artist-friendly way to implement the observer pattern. In general, you can use either System.Action or UnityAction, depending on your specific needs. You have the option of deploying either or both in the same project. If you want a more general purpose delegate that is not tied to the Unity game engine, use System.Action. If you want a delegate specifically designed for UnityEvents, use UnityAction.

Here, you can make a VoidEventChannelSO that raises an event without passing any parameters. This one contains a UnityAction named OnEventRaised.

C#
[CreateAssetMenu(menuName = "Events/Void Event Channel")] public class VoidEventChannelSO : ScriptableObject {
    public event UnityAction OnEventRaised;
    public void RaiseEvent() {

if (OnEventRaised != null)

OnEventRaised.Invoke(); } } Once you create a ScriptableObject of type VoidEventChannelSO, any MonoBehaviour can listen for OnEventRaised. For example, we can make a StartNewGame ScriptableObject that is of type VoidEventChannelSO. Another object can invoke the

C#
public RaiseEvent method to trigger the event.

A ScriptableObject-based event, an example of an event channel

Another MonoBehaviour can reference the event channel ScriptableObject in the Inspector, then subscribe/unsubscribe to OnEventRaised.

This invokes StartNewGame as a response whenever the event channel calls OnEventRaised: public class StartGame : MonoBehaviour { [SerializeField] private VoidEventChannelSO m_onNewGameButton = default;

C#
private void Start() {

m_onNewGameButton.OnEventRaised += StartNewGame;

C#
private void OnDestroy() {

m_onNewGameButton.OnEventRaised -= StartNewGame;

C#
private void StartNewGame() { // load level logic here… }

}
For more artist- or designer-friendly listening components, you could instead create a MonoBehaviour that doesn’t require any script setup. This VoidEventListener class doesn’t add extra functionality but has fields that are accessible in the Inspector:
public class VoidEventListener : MonoBehaviour {
    [SerializeField] private VoidEventChannelSO m_channel = default;
C#
public UnityEvent OnEventRaised;
C#
private void OnEnable() {

if (m_channel != null)

m_channel.OnEventRaised += Respond; }

C#
private void OnDisable() {

if (m_channel != null)

m_channel.OnEventRaised -= Respond; }

C#
private void Respond() {

if (OnEventRaised != null)

C#
OnEventRaised.Invoke();
}
}
Simply add the VoidEventListener to a GameObject, then drag the event channel ScriptableObject into the _channel field in the Inspector. Create UnityActions on the OnRaisedEvent in order to respond to the event.

An Event Listener allows a nonprogrammer to set up event-driven actions.

Regardless of which component you choose to listen for events, the event channels provide a means of communicating between your objects at runtime. Did the player complete a task or score a point? Is the game over? An event can notify any GameObject in the scene that needs that information. Because they are assets at the project level, ScriptableObject-based events can then drive much of the infrastructure of your application. This is especially useful for sending messages between the different systems that underpin the game architecture.

Some common management systems include: —

Audio management: Many things in your game can trigger sounds. This system can play AudioClips or adjust the AudioMixer in response to application events.

Scene management: This system handles loading and unloading of Unity scenes and game levels.

UI management: This is responsible for menu screens before, during, and/or after gameplay.

Save Data management: This handles saving and loading game data, as well as settings to your file system.

These systems all specialize in different tasks, but they need to talk to one another. Events can form the glue that keeps them connected. Explore the accompanying sample project for more examples of how to implement your own ScriptableObject-based events.

The sample project’s GameManager uses event channels.

Note that you can send different types of data with each event, using different event payloads. For example, the ScriptableObject-based events include IntEventChannelSO, a Vector2EventChannelSO, a VoidEventChannelsSO, and so on. The event used will depend on the context. Customize additional event types according to gameplay. For instance, a damage event may need to pass along who inflicted the damage and how much was done.

How you deploy these event channels is limited only by your creativity. In addition to the core systems above, events can often help join very different in-game systems so that they can interact: —

Cameras: These are used to add dramatic or cinematic effects, such as shaking or cutting to a different perspective.

Quests: These are tasks or objectives that the player must complete in order to progress through the game or receive a reward. Quests often involve a variety of gameplay elements, such as fetching items, defeating enemies, or solving puzzles.

Health: This important aspect of many games connects the player, enemies, and any objects or actions that can cause damage to the player.

Achievements: Like quests, these are special rewards that players can unlock by completing certain tasks or objectives within the game. Achievements can span different gameplay elements, such as reaching a certain level or accumulating a certain number of points.

These gameplay elements, in turn, will interact with other management systems, such as audio, UI, and save data, through the use of events. This approach promotes modularity and independence within each component of the architecture while still allowing for communication with other systems.

Debugging event channels A custom Editor or property drawer can create a “Raise Event” button in the Inspector. This can help you manually invoke the event for debugging. For example, here’s a basic Editor script that creates a custom Inspector button for the VoidEventChannelSO: [CustomEditor(typeof(VoidEventChannelSO))]

C#
public class VoidEventChannelSOEditor : Editor {
    public override void OnInspectorGUI() {
        DrawDefaultInspector();
        VoidEventChannelSO eventChannel = (VoidEventChannelSO)target;
        if (GUILayout.Button("Raise Event")) { eventChannel.RaiseEvent(); }
    }
}

This creates a button that allows you to raise the event at will, making it easier to diagnose issues at runtime.

A custom Editor button can help test event channels.

With a little more work, you can make buttons for event channels that carry data as well. Reserve a field for a debug value in the event channel itself, then pass this to the Editor script. You can find examples of how to implement this in the SOAP or Unity Atoms projects. As you continue using event channels for object decoupling, consider developing debugging tools, such as keeping a record of all listeners for each event. The event channel class can have methods to subscribe and unsubscribe objects, making it easier to identify which events are causing specific behaviors during runtime.

Example: InputReader Objects that listen for user input need a specialized type of event channel. Unity’s Input System uses InputActions to represent raw input data as logical concepts (e.g., jump, walk, etc.). Each InputAction, in turn, includes its own started, performed, and canceled events.

Setting up Actions and Action Maps in the Input System Editor

In order to decipher the InputAction bindings, you can create a special InputReader ScriptableObject. Again, this acts as an intermediary between the subject and observers. In this case, however, MonoBehaviours won’t raise the events explicitly. Instead, the Input System takes the place of the subject or broadcaster:

A ScriptableObject InputReader relays events from InputActions.

Here, we set up Actions and ActionMaps in the Input System. Each InputAction describes a separate axis of input and binds to the keyboard, gamepad, or whatever input device you like to use.

Rather than directly subscribing to the InputActions themselves, the paddle controllers listen for the OnMoveP1.performed event and OnMoveP2.performed events, respectively.

The InputReader insulates objects from direct dependency on the inputs.

The resulting InputReader standardizes how your GameObjects will process gamepad or keyboard actions. Any GameObject that needs input: —

Maintains a reference to the InputReader ScriptableObject

Subscribes to the relevant events and connects its event-handling methods

While this pattern may be overkill for a smaller game, we demonstrate this to make the concept easier to digest. The benefits won’t be apparent until your project grows and you add many more components. Decoupling the inputs from the GameObjects consuming them gives added flexibility and reusability. If you have to modify the InputActions during development, you only need to maintain the InputReader itself. The listening objects are unaffected if the events don’t need to change. Thus, maintaining the connection from input to observers becomes less work – especially when you have a lot of observers.

C#
static events You can choose to use static events to ease the burden of locating the ScriptableObject on the listening objects. For example, a MonoBehaviour could subscribe to the InputReader’s static MoveP1Event and MoveP2Event events in its OnEnable method: InputReader.MoveP1Event += OnMoveP1;
InputReader.MoveP2Event += OnMoveP2;
When using static events, be extra diligent when managing subscriptions. Don’t forget to unsubscribe in OnDisable: InputReader.MoveP1Event -= OnMoveP1;
InputReader.MoveP2Event -= OnMoveP2;
Static events will always be reachable and won’t be collected by the garbage collector if they have active subscribers. Any dangling subscribers will prevent their cleanup for the duration of your application. Static events, however, aren’t serializable. If you want to work interactively in the Editor, choose non-static events and make sure you reference the appropriate ScriptableObject in the Inspector.