⚙️ Core Concept

Layer Design

Modules Lifespan.png

When using this architecture, you are expected to intentionally design the layers of your game systems.

One of the core principles is separating modules based on their lifespan — how long they live during the game execution. This separation helps keep responsibilities clear, reduces unexpected dependencies, and makes the project easier to maintain and scale.

There are two main types of modules you should design:

Persistent Module

Persistent modules live for the entire duration of the game.

They are usually initialized once and remain alive across scene changes.

These modules represent core systems that should always be available, regardless of which scene is currently active.

Common examples of persistent modules:

Temporary/Scene Module

Temporary modules only live within a specific scene or gameplay context.

They are created when a scene is loaded and destroyed when the scene is unloaded.

These modules focus on local gameplay behavior and scene-specific logic.

Common examples of temporary modules:

Module Layer.drawio.png

When designing your game architecture, it is also important to clearly define the communication rules between layers.

This architecture follows a one-directional communication rule based on lifespan.

Temporary layers are allowed to access persistent layers, but persistent layers must never directly access temporary layers.

The reason is simple: persistent systems live longer than temporary ones.

Temporary modules exist only within a specific scene, while persistent modules stay alive throughout the entire game session.

Because of this difference in lifespan, allowing persistent systems to depend on temporary systems can easily lead to serious problems.

For example:

Imagine a persistent system tries to access a temporary module that exists in Scene A.

While the game is running, the player moves from Scene A to Scene B.

Scene A is unloaded, and all temporary modules inside it are destroyed.

However, the persistent system is still alive and still holds a reference to that temporary module from Scene A.

At this point, the persistent system is now referencing an object that no longer exists.

This can result in:

Because of this, persistent systems should remain scene-agnostic and never assume that a specific temporary module exists.

Instead:

By enforcing this communication rule, your architecture stays safe across scene changes and remains stable as the project grows.

Service Locator

ServiceLocator.png

Service Locator is a pattern used to provide controlled access to shared services across the project. Instead of modules creating or holding direct references to these services, they request them from the Service Locator when needed. This helps reduce tight coupling and keeps dependencies clear and manageable. By using a Service Locator, the project avoids long dependency chains, simplifies communication between systems, and makes it easier to maintain and refactor shared functionality.

In this architecture, the Service Locator acts as a central place where core services are registered and accessed. Any module that needs to be accessed by other modules must be registered through an installer. Installers are responsible for registering modules into the Service Locator so they can be safely discovered and used by other parts of the system.

There are two types of installers, based on module lifespan:

Persistent Installer

The persistent installer is used to register persistent modules that live for the entire duration of the game. These modules remain available across scene changes.

Temporary Installer

The temporary installer is used to register temporary modules that only exist within a specific scene. These registrations are valid only while the scene is active. Once a module is registered by an installer, other modules can access it through the Service Locator.

It is important to note that not every module needs to be registered. Only modules that are intended to be accessed by other modules should be registered in an installer. Local or self-contained modules that are not shared can remain unregistered to keep the system simple and clean.

In this architecture, installers provide two ways to register modules, depending on the type of module being used.

Registering MonoBehaviour Modules

MonoBehaviour-based modules can be registered directly through the Inspector.

This approach is useful for modules that:

By assigning the module in the installer via the Inspector, it will be automatically registered into the Service Locator at runtime.

Registering Native Class Modules

Native class modules (non-MonoBehaviour) are registered through code.

These modules are registered inside the Persistent Installer by overriding the RegisterServices() function and calling the Services.Register() method.

This approach is suitable for:

Using this method keeps core systems lightweight, testable, and independent from Unity-specific lifecycles.

By supporting both MonoBehaviour and native class registration, the architecture stays flexible while maintaining clear and controlled access to shared modules.

IInitalizable & IDisposable

If you need to perform certain actions when a module is registered, you can implement the IInitializable interface and override the Initialize() function.

The Initialize() function will be called automatically after the module has been registered by the installer.

This is useful for setup logic such as:

If you need to perform cleanup logic when a module is unregistered, you can implement the IDisposable interface and use the Dispose() function.

The Dispose() function is called when the module is removed from the Service Locator, allowing you to:

For MonoBehaviour-based modules, you can still use Unity’s built-in lifecycle functions such as Awake(), Start(), and OnDestroy().

However, the key advantage of using Initialize() and Dispose() is that you can control the execution order.

Because these functions are managed by the installer, you can define and guarantee the initialization and disposal order between modules, which is not always possible with Unity’s default lifecycle callbacks.

This makes system startup and shutdown behavior more predictable and easier to manage in complex projects.

Event Bus

An Event Bus is a communication mechanism that allows modules to send and receive events without directly referencing each other.

Instead of calling other modules directly, a module can publish an event to the Event Bus.

Any module that is interested in that event can subscribe and react to it. Event subscription should be performed in Initialize() or OnEnable() to ensure the module is ready to receive events, while event unsubscription should be handled in Dispose() or OnDisable() to prevent invalid references and memory leaks.

This approach helps keep systems decoupled and makes communication between modules cleaner and easier to maintain.

The main functions of the Event Bus are: