Unity runs gameplay through discrete update cycles. Within a frame, unless you explicitly constrain it, the relative execution order of different scripts is not something you should build core architecture around. Most of the time that is fine. At 60 FPS, one frame lasts about 16.67 ms, and many systems do not care which script updates first.
Startup is different. Initialization order can decide whether a game works at all. A global service must exist before other systems use it. A save gateway may need configuration before a gameplay scene reads player state. An audio manager may need mixer routing before anything plays sound. When those requirements are handled casually, the bugs tend to be subtle because they depend on timing.
Unity provides Script Execution Order settings, and attributes such as DefaultExecutionOrder can be useful for local constraints. But a large project should not rely on a long manual ordering list as its startup architecture. The list becomes difficult to audit, easy to forget, and fragile when new systems are added.
Bootstrapping is a cleaner approach. Instead of letting the game wake up everywhere at once, startup begins from one controlled entry point.
Bootstrap Scene
A bootstrap scene is intentionally small. It contains one object: the bootstrapper. Keep UI, gameplay objects, ambient managers, and scene-specific behavior out of this scene.
Place the bootstrap scene first in the project’s build scene list or build profile. That gives the application a deterministic beginning: the bootstrapper runs before content scenes and owns early startup.
Its responsibilities are narrow:
- Install global systems that must exist before gameplay begins.
- Transition into the first content scene once installation is complete.
Here is a minimal version:
using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;
[DisallowMultipleComponent]
[DefaultExecutionOrder(-10000)]
public sealed class GameBootstrapper : PersistentSingleton<GameBootstrapper>
{
[SerializeField] private string firstSceneName;
[SerializeField] private InstallerAssetBase[] installers;
private bool _hasBootstrapped;
protected override void OnSingletonInitialized()
{
if (_hasBootstrapped)
{
return;
}
_hasBootstrapped = true;
StartCoroutine(BootstrapRoutine());
}
private IEnumerator BootstrapRoutine()
{
RunInstallers();
if (!string.IsNullOrWhiteSpace(firstSceneName))
{
yield return SceneManager.LoadSceneAsync(firstSceneName);
}
}
private void RunInstallers()
{
foreach (var installer in installers)
{
if (installer == null)
{
continue;
}
installer.Install();
}
}
}
DefaultExecutionOrder(-10000) is still present, but it is not being used as a project-wide dependency graph. It simply makes the bootstrapper early within its own scene. The larger guarantee comes from the bootstrap scene itself: nothing else important exists yet.
Because the bootstrapper extends PersistentSingleton<GameBootstrapper>, it also follows the singleton rules from Persistent Singletons in Unity: one runtime owner, duplicate cleanup, and persistence across scene transitions.
Installers
The bootstrapper should orchestrate startup, not construct every system directly. Installers keep that work focused. An installer is a small asset whose job is to create or configure one part of the game’s foundation.
Using ScriptableObject installers has practical benefits:
- They are visible in the editor.
- They can hold serialized configuration.
- They can be reordered in the bootstrapper’s array.
- They keep startup logic out of gameplay scenes.
A minimal installer base can be as small as this:
using UnityEngine;
public abstract class InstallerAssetBase : ScriptableObject
{
public abstract void Install();
}
A simple random service installer might only ensure that the singleton exists:
[CreateAssetMenu(menuName = "Installers/Random Installer")]
public sealed class RandomInstallerAsset : InstallerAssetBase
{
public override void Install()
{
_ = RandomService.Instance;
}
}
There is intentionally very little code here. The installer makes randomness an explicit part of startup. Any system that runs after bootstrapping can rely on RandomService existing without scattered defensive checks or script execution order rules.
More complex installers can apply configuration:
[CreateAssetMenu(menuName = "Installers/Audio Installer")]
public sealed class AudioInstallerAsset : InstallerAssetBase
{
[SerializeField] private AudioClip startupClip;
[SerializeField] private bool loopStartupClip = true;
public override void Install()
{
var audio = AudioManager.Instance;
audio.ApplyConfiguration(startupClip, loopStartupClip);
}
}
In this model, the bootstrapper defines when installation happens. Each installer defines what one system needs. The gameplay scene then starts from a known base.
Bootstrapping and Readiness
A singleton can enforce existence and uniqueness, but it does not automatically guarantee readiness. AudioManager.Instance can return an object, but that does not prove the object has received its configuration. SaveSystem.Instance can exist before save data has been loaded. RandomService.Instance can exist before a seed has been assigned.
Bootstrapping addresses that gap by making readiness part of startup. If a system needs configuration, the relevant installer performs that configuration before the first content scene is loaded. After that point, consumers can treat the installed systems as part of the game’s foundation.
This is the main reason a bootstrap flow scales better than scattered initialization. Startup becomes visible. The order is encoded in one scene and one installer list, not hidden across arbitrary Awake, Start, and first-access calls.
With a bootstrap scene, a single bootstrapper, and focused installers, startup is no longer an accident of execution order. It becomes a deliberate phase of the application: create the foundation, configure it, then enter gameplay.