Advice on how to control loading and setup of managers properly

Hi all,

I have been building bits of games and UI for a few years, and I always get tangled up when it comes to turning things into a single game. I am totally self-taught (using Unity Learn, some Udemy courses, and lots of YouTube), so I apologise for not having any actually programming eduction.

When I am building a module (e.g. MyInputSystemInterface, MyTitleMenu, MySaveLoadSystem, etc) I am making it modular, testable, decoupled, etc. But when it comes to building a full game, I want more control over the order in which everything is Awakened, Started, etc. This is so I can assemble my GameManager first, then the UIManager, then get them to subscribe to relevant events from each other AFTER they are both assembled.

I fine this problem starts to overcomplicate my code even for very simple games.

The route I am trying is to use a Bootstrappable : Monobehaviour class, which implements IBootstrappable (requiring void Assemble() and void Initialise()) from which to derive all monobehaviours that need to be loaded at startup. There is also a Boostrapper monobehaviour which takes an ordered Bootstrappable array in the Inspector of components.

I would love to know if:

  1. I am going about this the right way
  2. There is a neat trick that I am missing (that probably anyone with a GCSE in Computer Science would know)
  3. There are any resources online you can recommend to deal with this. FYI I have read lots about design patterns but find they are a bit too abstract for me to work out how to apply them in this case.

I am probably just missing the really obvious, so happy to have the pointed out to me!

Thanks in advance
Ben

I like this pattern:

Some super-simple Singleton examples to take and modify:

Simple Unity3D Singleton (no predefined data):

Unity3D Singleton with a Prefab (or a ScriptableObject) used for predefined data:

These are pure-code solutions, DO NOT put anything into any scene, just access it via .Instance

Alternately you could start one up with a [RuntimeInitializeOnLoad] attribute.

There is never a reason to drag a GameObject into a scene if it will be DontDestroyOnLoad.

If you do:

  • you may drag it into something else that isn’t DDOL. FAIL
  • you may drag something else into it that isn’t DDOL. FAIL

Just DO NOT drag anything into a scene that will be marked DontDestroyOnLoad. Just Don’t Do It!

1 Like

There’s no way to “properly do something” so it comes down to two major things for me on a personal project:

  1. Is your code clean enough to work with without falling into the category of giving up because you feel lost / overwhelmed.
  2. Is it modular enough to add whatever is needed quickly or remove / refactor quickly.

Managers:

There’s no issues if you want to add your own custom bootstrapper but realistically you’re going to need most the classes anyways so I would suggest using Dependency Injection with Zenject (easy, already done and documented): GitHub - modesttree/Zenject: Dependency Injection Framework for Unity3D

//How to easily inject your class 
[Inject]
IPlayerController playerController;

Also within saying all of this, not everything needs to be a MonoBehavior, you can use a singular mono for entry if you want and everything else other that views or things that need to be rendered in scene can be a native class / struct.

UI:

Lets start with your MyTitleMenu this is more of a personal decision but I suggest MVC for any UI:

using System;

namespace Model
{
    public class CounterModel
    {
        public event Action<int> OnValueChanged;
        private int counterValue;

        public int CounterValue
        {
            get => counterValue;
            private set
            {
                counterValue = value;
                OnValueChanged?.Invoke(counterValue);
            }
        }

        public void Increment() => CounterValue++;
        public void Decrement() => CounterValue--;
        public void Reset() => CounterValue = 0;
    }
}

using UnityEngine;
using UnityEngine.UI;

namespace View
{
    public class CounterView : MonoBehaviour
    {
        public Text counterText;
        public Button incrementButton;
        public Button decrementButton;
        public Button resetButton;

        public void SetCounterText(int value) => counterText.text = value.ToString();

        public void BindIncrement(System.Action action) => incrementButton.onClick.AddListener(() => action());
        public void BindDecrement(System.Action action) => decrementButton.onClick.AddListener(() => action());
        public void BindReset(System.Action action) => resetButton.onClick.AddListener(() => action());
    }
}

using UnityEngine;
using Model;
using View;

namespace Controller
{
    public class CounterController 
    {
        private CounterModel model;
        public CounterView view;

        private void Awake()
        {
            model = new CounterModel();
            model.OnValueChanged += view.SetCounterText;

            view.BindIncrement(model.Increment);
            view.BindDecrement(model.Decrement);
            view.BindReset(model.Reset);
        }

        private void Start() => view.SetCounterText(model.CounterValue);
    }
}

You can add your initalizers in the view as well:

    [Inject]
    public void Construct(ITitleController controller)
    {
        Construct();
        this.controller = controller;
        controller.Initialize(this);
    }

    public void OnIsEnabledChanged(bool v)
    {
        canvasGroup.gameObject.SetActive(v);
        canvasGroup.alpha = v ? 1f : 0f;
        canvasGroup.interactable = v;
        canvasGroup.blocksRaycasts = v;
    }

This moves all your logic into a easily managed system allowing you to move, add, edit anything easily. You can even inject your controller behind a zenject installer to access it even more.

Your UI can / should be behind a state machine as you should only ever have one state active in a UI system: GitHub - rontian/MonsterLove.StateMachine

1 Like

I don’t recommend singletons, they provide easy global access, their misuse can lead to tightly coupled, hard-to-maintain code for new users.

While you’re free to choose to use any pattern, the reasons I don’t recommend singletons are two major reasons:

Tight Coupling

  • Problem: Classes depending on singletons become tightly coupled to specific implementations, making refactoring or replacing functionality difficult.
  • Consequence: Testing, reusing, or modifying code without affecting multiple parts of the project becomes harder.

Encourages Anti-Patterns

  • Problem: Singletons promote a “god object” pattern where the singleton manages too many responsibilities.
  • Consequence: Violates SOLID principles, especially Single Responsibility Principle and Dependency Inversion Principle.

Interesting as I like to have the same sorts of control. In particular I’ve using Unity to create VRChat worlds and I have to write in a subset of C# called UdonSharp. It is C# under the hood but has limited access to C# libraries so one has to be creative to overcome the restrictions.

No interfaces for instance.

Depending upon the Start methods as you know is not a good idea if there are class/object interdependencies.

So I have a base class that most classes subclass from named InitableBase. It would be an Interface but I can’t do it that way. This gives my classes an Init() and an InitMaster() method. These class register themselves in Start with an “initer” via InitRegister.

In any case, it works so very, very well I’m amazed.

1 Like

In general it’s good practice to try and make it so that all methods of a class can be used immediately after the moment that it has been initialized. You want to, as much as possible, avoid situations where you need to manually remember to avoid calling some methods / subscribe to some events on some services, before things are ready. The more of your services that you can built in such away that they can basically never break, the less fragile your project will become overall.

This can be a bit challenging in Unity, however, given that all components in a loaded scene will initialize in seemingly random order by default, unless you tinker with the [DefaultExecutionOrder] attribute.

Using serialized fields can help, since the deserialization process takes place for all components before Awake/OnEnable is executed for any of them. In contrast, if you only fetch references during the Awake event using something like GetComponent, then you can run into MissingReferenceExceptions much more easily.

Lazily-initialized singletons can indeed also be one approach to tackling the initialization order problem. If a service is lazily-initialized at the moment that its first needed by some client, and all its dependencies are also lazily-initialized at that time, etc., then in theory everything can automatically get initialized in the right order, and no dependencies should never be null for anything in the chain.

But the problem with the singleton pattern is that anything can access any member of any singleton, anywhere, and at any time. This means that if any members on any of your singletons are not always ready to be used immediately from the moment that the game is loaded, things can break quite easily.

Another approach is to let a dependency injection framework handle injecting all dependencies automatically. Then you will be able to always safely subscribe to any events in any Awake/OnEnable methods, knowing that all dependencies will already be available at that point.

You can also build your components in such a way that if any of their public methods are called, and they have not been initialized yet, then they will initialize themselves on-the-fly at that moment, before continuing with the execution of the method.

Yet another strategy that can help with initialization order issues is using a so-called Preload / Boostrap scene. The idea is to place all services that need to be initialized early into the first scene that is loaded, and then only load the second scene containing many of their clients, once you know that all the services are fully ready to be used.

1 Like

One more approach which I personally use is just fixing the initialization order using:

    [DefaultExecutionOrder(-999)]
    public class SomeSingleton: MonoBehaviour
    {
        static public SomeSingleton S;
        private void Start() => S = this;
    }

The default in custom scripts is 0, so everything that has a smaller number will be executed first. The order of events is still preserved of course. First all Awake() calls, then all Start() calls etc.
Am using this consistently with NON-lazy singletons (like above) that initialize themselves in the Start() method. Then I follow the rule (which I think Unity officially recommends) that no Awake() event accesses any other objects, incl. singletons.

However, together this ensures that everything is available when its needed and it’s also compatible with disabled domain-reload-on-start.

To keep an overview, I paste the orders also into a note file which I keep pinned in the editor to never be lazy to change there when necessary.
E.g. currently my game has:

------- OVERRIDDEN DEFAULT ORDERS:
    -3000 -> LogHelpers, ForwarderForOnRenderImage
    -2000 -> GlobalRegister, StaticQS, GlobalDebug, FadeOutSingleton, PrimeTweenInit
    -1000 -> CNF, SceneManager, StopTweensFromLastScene, GlobalBuoyancyManager
    -999 -> PermanentRefs, SelectedCharacter, ChallengesList, RippleEffect
    -100 -> MiniTestManager

It works for me, but am not someone who has properly shipped a game yet, so take it as you will :slight_smile:

1 Like

It is nice to see people developing “systems”. I like discussions that solve challenges we will likely all encounter.

I mention VRChat for a few reasons including it is a restricted implementation. No access to Awake methods for instance. It is also multiplayer and has a built-in world sync system. This further complicates what sounds like simple start-up scenarios. The first player in the world is the master and all subsequent players receive the values the master has set.

Properties set in Start have an excellent chance of being overwritten when the sync occurs. Add to that values retrieved via external API calls and coordinating what happens and when becomes a necessity.

I mean the first question you should ask yourself is what do these managers do, why are they both dependent on one another, and do you even need them in the first place?

I think that’s the first sign of architectural trouble, when your top level managerial systems are getting intertwined. The dubious ‘manager’ should really just be an API entry point in isolation as much as possible, one that gets used by other parts of the project. All usages should be one-directional.

Also consider that they don’t need to be monobehaviours. They could be scriptable objects, regular C# classes, even static classes. Or consider whether the manager is needed at all, and whether you can just handle things on a regular per-scene instance basis with components.

Kurt’s self-loading singleton examples are useful too, as very often a manager doesn’t need to be initialised until its actually needed.

3 Likes

Well, this kind of discussion has happened really often - reason being there is no absolute solution that works for everyone.
Or at least none that can be explained and taught easily..
Hence for example this gigantic thread on “dependency injection” which is the industrialized solution (from other industries, not games) to this kind of problems: Dependency Injection in Unity [Inversion of Control Container] It went about 10 times back and forth whether games should use that and to what extent and whether Unity’s built in mechanisms already are (sufficing for) dependency injection…

When you can achieve that, it’s indeed ideal!
How to do that exactly is what’s hard to explain and teach, isn’t it?

1 Like

Actually is not that hard at all.
But you need to design systems and data with a separation in mind from day one on the project.

When start intertwining, then later it is very hard to unwind all dependencies.

I currently structurise code, that classes / structs are only allowed to be accessed one directional.

Namespaces and assembly definitions can help greatly, to maintain separations.

2 Likes

It’s also possible to use a class with public constants as a single source of truth for all execution orders:

public sealed class ExecutionOrders
{
	public const int LogHelpers = -3000;
	public const int GlobalRegisters = -3000;
	public const int SceneManagers = -1000;
	...
}

This way it’d be easier to spot mistakes than when using magic numbers, and to e.g. tweak the initialization orders of all scene managers in unison.

Or, since the DefaultExecutionOrder attribute isn’t sealed, it’s also possible to create a custom attribute that derives from it and accepts human-readable enums instead of ints as its constructor arguments:

[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public sealed class InitOrderAttribute : UnityEngine.DefaultExecutionOrder
{
	public InitOrderAttribute(Category category, Order order = Order.Default) : base((int)category + (int)order) { }

	public InitOrderAttribute(Category category, int order) : base((int)category + order) { }
}

public enum Category
{
	ServiceInitializer = -30000,
	Initializer = -29000,
	Service = -20000,
	Default = 0
}

public enum Order
{
	VeryEarly = -500,
	Early = -100,
	Default = 0,
	Late = 100,
	VeryLate = 500
}

I see that many Unity developers follow this structure, but I’ve never personally really liked it, because:

  1. I don’t like having to just remember to do things in a certain way to avoid bugs. I’d rather enforce things in code, making bugs impossible.
  2. It leaves the otherwise very useful OnEnable event in a very awkward position, when all components only acquire their dependencies and become ready to be used after the Start event.

Thanks for the suggestion. The problem I found with this at a very basic level was that it didn’t really solve my problems with over-connected code. Or rather: it made me lazy AF, and I ran into issues of interdependency later on down the road.

I am using Singletons, but trying to avoid a single “god object” implementation (as @jmgek describes it).

At the the ‘nuts and bolts’ level I am pretty happy: modular, clean code (clean enough for me, anyway). I maintain a “MyCore” namespace and package across all my projects that uses some common elements e.g. a plug-n-play UI State Machine fo managing UI states.
A FortuneWheel module that randomly selects a player (using a RandomResultRoller module). This can be connected to the UI State Machine easily with a Scriptable Object (provided the MyCore package is installed).
But there are a few things that I think all games need to have connected with each other: something to manage the game state ‘thing’, a UI ‘thing’, and maybe something to make the two things load and talk to each other, if the game state thing is not going to do that as well.
If I do the game manager (which would be highly specific to a project) as a controller of the loading of the UI, then it is a bit harder to re-use this in another project maybe?

Create a instance with Zenject

using UnityEngine;
using Zenject;

public class UIInstaller : MonoInstaller
{
    public override void InstallBindings()
    {
        Container.Bind<ITitleController>().To<TitleController>().AsSingle();
    }
}

and control your MVC

using System;
using Zenject;
using UnityEngine;

public interface ITitleController
{
    void Initialize(TitleView view);
    void SetVisible(bool v);
    void SetEnabled(bool v);
    void Tick();
    void Dispose();
}

public class TitleController : ITitleController, IDisposable
{
    private TitleModel model;
    private TitleView view;

    public TitleController()
    {
        this.model = new TitleModel();
    }

    public void Initialize(TitleView view)
    {
        this.view = view;
        if (model != null)
        {
            model.OnIsVisibleChanged += view.OnIsVisibleChanged;
            model.OnIsEnabledChanged += view.OnIsEnabledChanged;
            model.OnIsDirtyChanged += view.OnIsDirtyChanged;
            model.Initialize();
        }
    }

    public void SetVisible(bool v) => model.IsVisible = v;

    public void SetEnabled(bool v) => model.IsEnabled = v;

    public void Tick()
    {
    }

    public void Dispose()
    {
        if (model != null)
        {
            model.OnIsVisibleChanged -= view.OnIsVisibleChanged;
            model.OnIsEnabledChanged -= view.OnIsEnabledChanged;
            model.OnIsDirtyChanged -= view.OnIsDirtyChanged;
        }
        if (view != null)
        {
            UnityEngine.Object.Destroy(view.gameObject);
        }
    }
}

and the view:

using System;
using UnityEngine;
using UnityEngine.UI;
using Zenject;

public class TitleView : MonoBehaviour
{
    private Canvas canvas;
    private CanvasGroup canvasGroup;

    private ITitleController controller;

    [SerializeField]
    Button newGameButton;
    [SerializeField]
    Button continueGameButton;

    [Inject]
    IStateGameSystem stateGameSystem;

    [Inject]
    public void Construct(ITitleController controller)
    {
        Construct();
        this.controller = controller;
        controller.Initialize(this);
    }

    private void Construct()
    {
        var canvases = GetComponentsInParent<Canvas>();
        if (canvases.Length == 0)
        {
            Debug.LogError($"No canvas found on {gameObject.name}");
        }

        canvas = canvases[canvases.Length - 1];
        canvasGroup = GetComponentInChildren<CanvasGroup>();
        if (canvasGroup == null)
        {
            Debug.LogError($"No canvas group found on {gameObject.name}");
        }

        if (newGameButton != null)
        {
            newGameButton.onClick.AddListener(OnNewGameClicked);
        }

        if (continueGameButton != null)
        {
            continueGameButton.onClick.AddListener(OnContinueGameClicked);
        }
    }

    public void OnIsVisibleChanged(bool v)
    {
        canvasGroup.alpha = v ? 1f : 0f;
        canvasGroup.interactable = v;
        canvasGroup.blocksRaycasts = v;
    }

    public void OnIsEnabledChanged(bool v)
    {
        canvasGroup.gameObject.SetActive(v);
        canvasGroup.alpha = v ? 1f : 0f;
        canvasGroup.interactable = v;
        canvasGroup.blocksRaycasts = v;
    }

    public void OnIsDirtyChanged(bool newIsDirty)
    {
        // Handle dirty state changes here.
    }

    private void OnNewGameClicked()
    {
        stateGameSystem.ChangeToNewGame();
    }

    // Continue Game Button Click Event
    private void OnContinueGameClicked()
    {
    }

    private void OnDestroy()
    {
        if (newGameButton != null)
        {
            newGameButton.onClick.RemoveListener(OnNewGameClicked);
        }

        if (continueGameButton != null)
        {
            continueGameButton.onClick.RemoveListener(OnContinueGameClicked);
        }
    }
}

use it in your state machine:

    [Inject]
    ITitleController titleController;

...

    void NewGame_Enter()
    {
         titleController.SetVisible(true);
    }

    void NewGame_Exit()
    {
        titleController.SetVisible(false);
    }

In project the major thing you’re going to be battling is the architecture and keeping it in a state you want to continue working with. By creating these simple systems it gives you more time to finalize your designs.

Dependency Injection removes all the things you need to do manually to control all those “connections” . It also puts your code in a state that just allows you to swap what you need out easily. No need to worry about instancing when it’s all handled for you.

1 Like

This was partly what I was trying to get away from. But seemed to be just creating my version of it with SelfAssemble(),WireUp(), or WhateverNext(). The Unity events are great for heaps of stuff, like instantiating new pieces on a board, new players, projectiles etc. But there feels like a little control gap at the top level.

Maybe I am just trying to ignore the need for a more technically sophisticated approach

1 Like

I am trying to do the same, although probably with a lot less experience. And the interdependencies sneak up and bury themselves in your code like ticks. I have no idea what an assembly definition is, but namespaces are keeping me honest. Perhaps I need a namespace for ExecutionControl?

Assembly Definition (ASMDF) are Unity equivalent to DLL. However, you don’t need to build the library of files prior, to make changes.

Basically you organise your files into libraries.
Like you have all files of the same namespace in one directory tree.
Then you put ASMDF in that directory.
ASMDFs require attaching of other ASDMFs references, if you want to use them.
Some are by default, like UnityEngine namespace.

ASMDFs won’t allow you to have circular references.
And if anything, will throw you error otherwise.

It is worth to get familiar with AMSDFs.

1 Like

Unfortunately with code there is no right way, it’s just a scalar value, or in a vector into a certain dimension.

In order to answer the ‘right’ way to do it, you need to ask, how many people will work on the code? How large will the project be? How likely is it the order of these managers will change? How regularly will they change? etc.

For a solo development, in most cases [DefaultExecutionOrder(-10)] is more than enough to ensure things are executed in the correct sequence. It’s actually how meta codes the OVR Framework. If it’s good enough for them, it’s probably good enough for you.

There are many other cleaner and more complex ways to achieve it, but do you really need to?

2 Likes

Can always just look things up and you might find the docs explain all: Unity - Manual: Organizing scripts into assemblies

I don’t think I quite agree with this. Don’t think in terms of ‘managers’, think in terms of systems. You’re not making a one and done manager, you’re (usually, or preferably) making a system made up of multiple pieces.

Then other parts of the project use and hook into this systems. A system might also use another system (but it shouldn’t go two ways), and something might use multiple systems.

Very often you will probably need some singleton entry point for a system. Ideally it lives in isolation and doesn’t require anything else to be primed before hand.

Though in a current, fairly large project, I have ended up encapsulating all the top-level systems into an overarching GameplayRuntime object. One of these gets spun up when a play loads a save game, and can be spun down to neatly clean up everything when exiting a save. Though the order the systems initialise in isn’t important, as they don’t rely on one another.