#begin
In the last blog I‘ve laid out some of the problems with Singletons in a Unity3D context. As I pointed out; I think the root cause of “Singletons, Singletons everywhere“ is the fact that Unity does not provide a clear entry point to an application. In today‘s blog we‘ll explore some of the ways to create such an entry point to your Unity application yourself.
There are numerous ways to solve the entry point problem, and some of them might not work for you depending on the architecture of your app. Some projects are set-up to exploit the full power of the component-based architecture Unity promotes, while other architectures are in desperate need for some clear architectural design.
But before we start the explore the solutions lets clarify what the problem is exactly. I call it “the mythological hydra“…
The Mythological Hydra
Programs have historically exposed only one clear entry point into an application. The entry point is often indicated by a class that implements a (static) function Main
like in C, C++, C# or Java. Unity3D doesn‘t expose such an entry point and that often leads to confusion for even the most experienced developers among us.
It is for this reason that developers often resort to using Singletons in Unity. And yet, even with a Singleton the entry point problem remains unsolved. While singletons might be used everywhere there might still be countless other MonoBehaviour derived classes attached to GameObjects in the scene. All such objects probably hook into Unity‘s event loop with Awake
, OnEnable
or Start
. During any of these functions they might reach out to Singletons or maybe ScriptableObjects (SO) in the scene or project. To add even more fuel to the fire; there are most likely all kinds of crisscross dependencies ‘drag‘n dropped‘ and serialized in the inspector. All of this creates one unmaintainable mess also known as the ‘Big Ball Of Mud‘ in the software industry.
A Big Ball Of Mud ‘architecture‘ is identified by its unregulated growth. All objects in the scene stating their own entry point contributes to this mess. I‘ll ask you: Have you ever needed to change the script execution order to fix a null-reference exception in your Unity project? I‘ve done that and I bet you have too. Or maybe you found that cross-referenced singletons that required the Unity awake or start function are initialized in the wrong order leading to issues.
We want to get rid of al this mess and have a clear hierarchy of the dependencies being passed down. If you don‘t provide a clear mechanism you might cut of one head of the hydra in your pull-request, yet your colleague will spawn a fresh new head in his. We need to cut the Hydra‘s heads and cauterize them one by one in order to slay that sucker.
So, more or less the problem we are trying to solve is to avoid having multiple functions that would qualify as main
. In most cases having one, clear, entry point to your Unity app will fit the architecture of the application. Yet, I can hear you screaming that having distributed
entry points is the only way your architecture works or even makes sense. I think that in any case a single one is better so lets explore the options.
In the examples below I‘ve tried to be as explicit with naming etc. as possible just for demonstration purposes. Your production code should be more closely aligned with your domain. Additionally, these are mere examples and should be extended with test-cases and proper namespaces or Assembly Definitions
.
Personal favorite – RuntimeInitializeOnLoad
My personal favorite way to create a controlled entry point into a Unity3D application is to have a method marked with the [RuntimeInitializeOnLoad()]
method attribute. You can choose a couple of different stages to hook into the Unity3D event loop and I usually pick BeforeSceneLoad
.
So how does it work? Well its very easy; you just mark some static
method in a class with the [RuntimeInitializeOnLoad(BeforeSceneLoad)]
attribute and Unity3D will call that method at the stage of your choosing. A simplified example is shown below:
public class MyunityApp
{
public interface IUnityEvents
{
public event Action Quit;
public event Action Update;
}
private class UnityEvents : MonoBehaviour, IUnityEvents
{
public event Action Quit;
public event Action Update;
private void Awake()
{
// Extend the object's lifetime across scenes
DontDestroyOnLoad(gameObject);
// Hide the object in hierarchy
gameObject.hideFlags = HideFlags.HideInHierarchy;
}
private void Update()
{
Update?.Invoke();
}
private void OnApplicationQuit()
{
Quit?.Invoke();
}
}
internal class MyMainObject
{
private readonly IUnityEvents _unityEvents;
internal MyMainObject(IUnityEvents unityEvents, MyDataBaseConnection connection, Root root)
{
_unityEvents = unityEvents;
_dataBaseConnection = connection;
_root = root;
// Assign lifecycle events
_unityEvents.Update += OnUpdateHandler;
// OnApplicationQuit is important to release and dispose resources properly.
_unityEvents.Quit += OnApplicationQuitHandler;
}
private void OnUpdateHandler()
{
// Invoke Update on objects that require updating
}
// Remark: OnApplicationQuit should be replaced by OnApplicationPause on mobile platforms
// as there is no guarantee it is invoked.
private void OnApplicationQuitHandler()
{
// Dispose objects and release resources to prevent memory leaks of state corruption
_unityEvents.Update -= OnApplicationUpdateHandler;
_unityEvents.Quit -= OnApplicationQuitHandler;
}
}
[RuntimeInitializeOnLoad(BeforeSceneLoad)]
public static void Main()
{
// Instantiate object that hooks into Unity event lifecycle to pass along methods like update and application quit
var unityEvents = new GameObject("UnityEvents").AddComponent<UnityEvents>();
// Setup some database connection and -
var myDataBaseConnection = new DatabaseConnection("some/path/my.sqlite");
// load a resource to spawn
var myRootPrefab = Resources.Load<Root>();
var rootInstance = Object.Instantiate(myRootPrefab);
// Instantiate main by injecting dependencies -> this improves testability of the app
_ = new MyMainObject(unityEvents, myDataBaseConnection, rootInstance);
}
}
So how does it work exactly? We bind this Main
function to the RuntimeInitializeOnLoad attribute. When this function is invoked by Unity we initialize and setup all the dependencies we need. Important here is to inject some sort of object that monitors Unity3D lifecycle events. We want to abstract the lifecycle events for many reasons; one being that we need to know when to shutdown and dispose resources properly. Second we want to abstract direct dependencies on let say the Update
loop to be able to control it for testing purposes for example. Of course, you can expose any other Unity3D lifecycle function like losing/gaining focus, fixed update, OnGUI and more.
Another important aspect of this main object is that most, if not all, core dependencies are injected for the purpose dependency inversion. If we inject the dependencies at runtime we can do a similar thing during testing. So, for example: during testing we will inject our custom IUnityEvents that exposes public function to simulate Update
or Quit
. We can also inject a custom InMemoryDataBaseConnection
that replaces any query done on the database to some in-memory dictionary. I‘ve done this trick a million times and it works great.
But how then do we get access to Scene
based objects? Well, for one; we don‘t want objects reaching out to our main object we want to invert the direction somehow. We could do that by using some SO that we loaded from Unity Resources
which exposes some event to fetch a dependency. This way you can still keep your highly distributed entry point yet avoid Singletons all over the place. Another way might be to have a very strictly managed Event-Bus
where certain objects might listen to. Yet, you should be very cautious about that since such an event-bus can become a viral dependency plague.
My preferred means of spreading dependencies is to instantiate everything needed at run-time. This way you have full control which dependency ends up where. But I know very well that this might not work for Unity Apps that heavily depend on Scene
files where the entire world is designed rigorously. Most, if not all the applications I ever wrote for Unity are heavily controlled by code or are procedurally generated. Meaning we have full control on the code and scene files are practically empty. One thing I would like to advocate is that dependency inversion and the interface segregation principle are important to keep in the back of your mind here. Its best to inject very specific and dedicated interfaces instead of passing the Main
object around.
The above example is one that I personally like very much. When-ever I‘m writing a personal project that executes in run-time I‘ll use the RuntimeInitializeOnLoad
approach. I just like this option because it‘s a code first solution and that matches a lot with the kind of project I write. But there are also other options. So let‘s explore the next one…
Main.cs as Monobehaviour
Another option which I‘ve used quite a number of times is to have a Monobehaviour
that acts as main. This is a fairly common solution however it can be implemented in multiple ways. One superior than the other. With this Main.cs
monobehaviour approach we want to adhere to the same rules and constraints as with the previous approach. Main must create the dependencies and inject these dependencies in the core domain object. This essentially means; no direct dependencies on Main.cs
. So, no serialized fields that refer to main! We really want to break this monobehaviour tyranny
.
Monobehaviours spread across the scene will need some other way to communicate and receive events. One very important detail is that since Main.cs
is a monobehaviour it is inherently bound to the Unity3D event loop. This means any object that needs some sort of dependency can only access dependencies, after main is fully initialized. So, we should do initialisation of main in the Awake
and then during Start
objects can expect to be able to access their dependencies.
We can use the same approach and inject some SO as means of event handler for startup procedures. The key concept to understand is to try and decouple your business code as much from the direct Unity3D event cycle. We need to have full control over these events to write deterministic and highly testable programs.
So let‘s quickly have a look at what this could look like.
public interface IUnityEvents
{
public event Action Quit;
public event Action Update;
}
internal class Main : Monobehaviour, IUnityEvents
{
public event Action Quit;
public event Action Update;
private MyMainObject _main;
private void Awake()
{
// Extend the object's lifetime across scenes
DontDestroyOnLoad(gameObject);
// Hide the object in hierarchy
gameObject.hideFlags = HideFlags.HideInHierarchy;
// Setup some database connection and -
var myDataBaseConnection = new DatabaseConnection("some/path/my.sqlite");
// load a resource to spawn
var myRootPrefab = Resources.Load<Root>();
var rootInstance = Object.Instantiate(myRootPrefab);
// Lets also fetch some dependencies defined as monobehaviours
// which are added as children in Main's hierarchy.
var _myOtherDependencies = GetComponentsInChildren<IDependency>();
_main = new MyMainObject(this, myDatabaseConnection, rootInstance, _myOtherDependencies);
DontDestroyOnLoad(this.gameObject);
}
private void Update()
{
Update?.Invoke();
}
private void OnApplicationQuit()
{
Quit?.Invoke();
}
}
As we can see, this solution is also very straightforward. We derive our Main.cs
from monobehaviour, implement the IUnityEvents
and pass that into the constructor of MyMainObject along with the database connection and root instance. In this example we also inject a collection of objects implementing the IDependency
interface as we can leverage the fact we have access to main‘s hierarchy in the scene. And again, please take note that we pass IDependency
and not monobehaviour or any other class bound to Unity namespaces.
This works pretty nicely until someone decides to invoke Object.Destroy(main.gameobject)
. This should not happen of course as we explicitly hide it in hierarchy. When you are developing a game of your own this isn‘t much of a problem. However, when you‘re writing mostly libraries you want to prevent external users to mess up your internal state. This is why I prefer the [RuntimeInitializeOnLoad]
approach over the monobehaviour driven one. The problem remains that you simply cannot guard any random code invoking destroy on your main object.
That being said; this solution works very well in most situations and I‘ve used this a lot as well. Also note that, you can combine both approaches where the [RuntimeInitializeOnLoad]
approach is used for application scoped dependencies and the main.cs
is used on a scene based scope. This way, you could have the best of both worlds. Having Main.cs
and child objects in the scene allows for a more distributed start-up procedure which might fit better in your problem domain. Just make sure you are deliberate about avoiding dependencies on Unity concepts. Push these out to the boundaries of your architecture.
The last approach I would like to cover is what I call the Init.Scene
approach which I‘ve used for creating a fully distributed entry point. So, let‘s have a look…
Main.scene
As the name implies the Main.scene
approach uses a Scene object to start-up your Unity Application. With this approach we add a specific initialisation scene as the first scene in our build settings. An method to ‘hide‘ this scene from the user is to show some sort of splashscreen while the init sequence is running.
Before we dive into the details of this Main.scene
approach we need to briefly cover some important concept in computer science which is: Choreography vs. Orchestration. Both previous examples are based on an orchestration approach.
There is a single object (main), the orchestrator, responsible for creating, instantiating and configuring the dependencies used for your Unity app. This allows for a centralized and controlled, flow of information and the cognitive load is often low as you only need to understand at a single class file.
Choreography however is a different approach where objects ideally are unaware of each other and communicate over some event-bus or pub-sub mechanism. I wont go into the pitfalls of eventbusses and pub-sub in this blog right now. I‘ll save that for an upcoming edition of this series. But, choreography allows us to create a high decoupled start-up sequence where objects don‘t necessarily need references to each other since communication is done through events. I hope this brief description of the concepts is good enough, if not, go check some YouTube videos or ask your ever so friendly AI assistant.
Now, let‘s take a look at this Main.Scene
approach.
public interface IService : IDisposable
{
public abstract void Initialize();
}
public abstract class ServiceAdapter<T> : Monobehaviour where T : IService
{
protected T Service { get; private set; }
private CancellationTokenSource _cancellationTokenSource;
private bool _awaking;
private void Awake()
{
DontDestroyOnLoad(this.gameObject);
_awaking = true;
Service = CreateService();
Service.Initialize();
_cancellationTokenSource = new CancellationTokenSource();
DoAwake();
EventBus.Publish(new InitializeEvent(
new EventIdentifier(Service.GetType().Name),
_cancellationTokenSource.Token,
EventPool.GetTraceIdentifier());
_awaking = false;
}
// Optional override
protected virtual void DoAwake()
{
}
protected void InitialisationDone()
{
if(_awaking)
throw new ServiceInitialisationException(Service, "Cannot send InitialisationDoneEvent in 'Awake'");
EventBus.Publish(
new InitialisationDoneEvent(
new EventIdentifier(Service.GetType().Name),
_cancellationTokenSource.Token,
EventPool.GetTraceIdentifier());
}
// Required to override
protected abstract T CreateService();
protected void OnDestroy()
{
DoDestroy();
_cancellationTokenSource.Cancel();
Service.Dispose();
}
// Optional override to clean up resources
protected virtual void DoDestroy()
{
}
}
public class DataBaseServiceAdapter : ServiceAdapter<MyDataBaseService>
{
// Inject into TextAsset during CI/CD for example
[SerializeField] private TextAsset _dataBaseConnectionString;
protected override MyDataBaseService CreateService()
{
return new MyDataBaseService(_dataBaseConnectionString.text);
}
// do some syncrhonous initialisation
private void Start()
{
// do some initialisation
InitialisationDone();
}
// or
// you can also replace Start with an IEnumerator and
// initialize async
private IEnumerator Start()
{
yield return SomeLongCoroutine();
// or
yield return SomeCSharpTask().ToCoroutine();
InitialisationDone();
}
}
public readonly struct EventIdentifier
{
private readonly string _identifier;
public EventIdentifier(string identifier)
{
// Do some validation on the identifier maybe
_identifier = identifier;
}
// Override the equals and GetHashCode
// and optionally the == and != operators for conveinience
}
public class Main
{
private class StartUpSequenceService : IService
{
private int _initiliseCounter;
private int _initialisationDoneCounter;
private CancellationToen _token;
public StartUpSequenceService(CancellationToken token)
{
_token = token;
}
public void Initialize()
{
EventBus.SubScribe += OnInitialize;
}
private void OnInitializ(IEvent @evemt)
{
if(@event is InitialiseEvemt)
initialiseCounter++;
else if (@event is InitialisationDoneEvent)
initialisationDoneCounter++:
if(initialiseCounter == initialisationDoneCounter)
FinitStartUpSequence();
}
private void FinishStartUpSequence()
{
EventBus.Publish(new FinishedStartUpEvent(
new EventIdentifier("startup complate"),
_token, EventPool.GetTraceIdentifier()));
Dispose();
SceneManager.LoadScene("Game");
}
public void Dispose()
{
EventBus.SubScribe -= OnInitialize;
}
}
[RuntimeInitializeOnLoad(BeforeSceneLoad)]
public static void Init()
{
var cancellationTokenSource = new CancellationTokenSource();
unityEvents = new GameObject("UnityEvents").AddComponent<UnityEvents>();
unitEvents.Quit += Quit;
var startUpService = new StartUpSequenceService();
_ = new EventPool(cancellationTokenSource.Token);
return;
void Quit()
{
startUpService.Dispose();
unityEvents.Quit -= Quit;
EventBus.Publish(new QuitEvent(
new EventIdentifier("quit"),
cancellationTokenSource.Token,
EventBus.GetTraceIdentifier()));
cancellationTokenSource.Cancel();
}
}
}
public interface IEvent
{
// Some identifier for your event
public EventIdentifier Identifier { get; }
// Trace identifier for debugging (thank me later)
public int TraceIdentifier { get; }
public bool IsCancelled { get; }
}
public abstract class PooledEvent : IEvent
{
// Some identifier for your event
public EventIdentifier Identifier { get; private set;}
// Trace identifier for debugging (thank me later)
public int TraceIdentifier { get; private set;}
public bool IsCancelled => _cancellationToken.IsCancellationRequested;
public bool IsPublished { get; private set;}
private CancellationToken _cancellationToken;
internal void Aquire(EventIdentifier identifier, CancellationToken token, int traceIdentifier)
{
Identifier = identifier;
_cancellationToken = token;
TradeIdentifier = traceIdentifier;
}
internal void Publish()
{
IsPubliced = true;
}
internal void Release()
{
Identifier = default;
IsPublished = false;
TraceIdentifier = -1;
}
}
// Very naive event bus...
public static class EventBus
{
public event Action<IEvent> Subscribe;
public static void Publish(PooledEvent pooledEvent)
{
pooledEvent.IsPublished = true;
Subscribe?.Invoke(@event);
}
public static void Publish(IEvent @event)
{
Subscribe?.Invoke(@event);
}
}
public class EventPool
{
private static long _currentTraceId;
private static YourFavoriteObjectPool _objectPool;
private static CancellationToken _token;
internal EventPool(IUnityEvents unityEvents, CancellationToken token)
{
_currentTraceId = 0;
_objectPool = new YourFavoriteObjectPool(); // This could be injected too
_tokenSource = new CancellationTokenSource();
}
// Add a thread-safe way to get a new trace id
public static long GetTaceIdentifer()
{
return long Increment(ref long _currentTraceId);
}
public static T AquireEvent<T>(EventIdentifier identifier, IEvent parent = null) where T : Event, new()
{
// Get an event from the pool.
// Inject a cancellation token to trigger cancel when needing to clean up resources.
// Add the trace identifier for debugging.
return _objectPool.GetPooled<T>(identifier, _tokenSource.Token, parent?.TraceIdentifier ?? GetTraceIdentifier());
}
public static void Release(IEvent eventToRelease)
{
// Reset event to proper state and return to pool
eventToRelease.Release();
}
}
// This event is raised when services start their initialisation
public struct InitialiseEvent : Event
{
public EventIdentifier Identifier { get; private set;}
public int TraceIdentifier { get; private set;}
public bool IsCancelled => _cancellationToken.IsCancellationRequested;
public InitialiseEvent(EventIdentifier identifier, CancellationToken token, int traceIdentifier)
{
Identifier = identifier;
_cancellationToken = token;
TradeIdentifier = traceIdentifier;
}
}
// This event is raised when services are done initialising
public struct InitialisationDoneEvent : Event
{
public EventIdentifier Identifier { get; private set;}
public int TraceIdentifier { get; private set;}
public bool IsCancelled => _cancellationToken.IsCancellationRequested;
public InitializeEvent(EventIdentifier identifier, CancellationToken token, int traceIdentifier)
{
Identifier = identifier;
_cancellationToken = token;
TradeIdentifier = traceIdentifier;
}
}
// This event is raised when the number of initialise and initialisationdone events are equal
// which signifies startup is done
public struct StartupCompletedeEvent : Event
{
public EventIdentifier Identifier { get; private set;}
public int TraceIdentifier { get; private set;}
public bool IsCancelled => _cancellationToken.IsCancellationRequested;
public InitializeEvent(EventIdentifier identifier, CancellationToken token, int traceIdentifier)
{
Identifier = identifier;
_cancellationToken = token;
TradeIdentifier = traceIdentifier;
}
}
As you can see from the sheer size of the code snippet, the Main.scene
approach has many more cogs to turn the machine. The code I‘ve written here is just for demonstration purposes to clarify my explanation but some things are implemented naively. The EventBus
is very, very basic. In production code, you really want to differentiate between ‘messages‘ and ‘events‘. I‘ve tried to encode that in this simple example by making the startup events struct
value types and added a simple example for an EventPool
to acquire reference type Events
. That all being said… Let‘s digest the code and discuss the example.
The Main.scene
approach tries to fully leverage the distributed, component based nature of Unity3D objects in the scene. An important component is called the EventBus
which is a central hub to publish messages to. As I mentioned before, an EventBus has many problems which I‘ll lay out in a future blog. All I‘ll say right now is that you should be very strategic about which objects are allowed to access it. As the great philosopher Marcus Aurelius once said: “Be tolerant with others and strict with yourself.“
The EventBus
and EventPool
initialisation is bound to a method annotated with a [RuntimeInitialiseOnLoad]
method to make sure it‘s executed before any Unity3D lifecycle event. During awake you can register what I call an InitialiseEvent
which simply says that some service is initialising. When the service is done initialising it will publish an InitialisationDoneEvent
to notify it is ready. One assumption of this approach is that services will send their InitialisationEvent
in Awake
, followed with their InitialisationDoneEvent
in Start
or later to make sure the count lines up properly. Services cannot do both in the Awake
else an exception will be thrown.
I added a naive StartUpSequenceService
which simply matches the count of initialiseEvent
and InitialisationDoneEvent
and if they match up, the Game
scene is loaded. You most likely need some more intricate start-up mechanism but I think this gets the message across. I‘ve also used staged start-up sequences before where some services and resources are initialised in stage 1 followed by other services is stage 2 or any later stages you design. Its quite s flexible approach and you can go as crazy as you want. Just make sure you maintain service independence and be agnostic of Unity.
For services I made sure to include an example to show how to decouple your services from MonoBehaviour
. The ServiceAdapter
class can be used to create monobehaviours that act as services without exposing service classes themselves to Unity. This way you can test the service classes easily in edit-mode tests.
Conclusion
In this blog I‘ve tried to explain the entry point problem that‘s been haunting Unity3D for a long time. I think the lack of a solid entry point leads to many Unity3D projects becoming unmanageable and show symptoms of ‘Big Ball of Mud‘ sickness. This blog provides 3 answers to the entry point problem which solve the problem is different ways. I‘m very well aware that solving the entry point issue is not limited to the 3 approached I‘ve listed here. There are many other solutions and I‘ve just listed the ones I personally like and used in the past. I‘ve also explicitly shown you approaches where you, as the developer, have full control. There are no external dependencies used in any of the presented solutions. That is on purpose! I‘m really against having any external dependencies governing the core of any software solution. Because at that point you have surrendered to the will of that dependency. Even to the point where we abstract Unity3D out of our business logic as much as we can. Because, whenever some marketing genius invents another iteration of the ‘runtime fee‘ you aught to be able to switch haha. But I think they‘ve learned from that situation and will most likely not pull of such a disaster in the future.
In this blog I‘ve also expressed my worries about the concept of the EventBus. We will dive deeper into event busses in the next installment of the Unity Anti-patterns blog series.
For now, I hope you enjoyed this blog. I‘m certain that with these examples you can design a start-up sequence to slay that entry point hydra. Cut of all its heads and cauterise them to enact full control. Very important here is to adjust the examples to fit your problem domain. Just as the Code of the Pirate Brethren in the Pirates of the Caribbean, any design pattern; is not a rule, just a guideline.
Now, go write some code!
#end
01010010 01110101 01100010 01100101 01101110
Recent Comments