Game Initialization and the Single-Threaded Tick Loop
Unturned's boot sequence is linear and strictly ordered. From the moment Unity calls Awake on the root Setup MonoBehaviour through the entire lifecycle of the game, there is exactly one execution path with no branching concurrency. The engine initializes subsystems in a fixed dependency chain, loads modules, registers asset types, and hands control to a custom tick loop that processes the entire game simulation on a single thread. Understanding this boot sequence is the prerequisite for understanding every other subsystem in the SDK.
This article documents the boot sequence from Setup.Awake through module resolution and asset loading to the tick loop. It covers the custom UnturnedPlayerLoop system that prunes unused Unity subsystems, the loading screen state machine, loading bar progression, and the Provider lifecycle that bridges Steamworks and the game simulation.
Source code location: SDG.Unturned.Loading.Setup, SDG.Unturned.Loading.UnturnedPlayerLoop, SDG.Unturned.Loading.LoadingUI, SDG.Unturned.UnturnedNexus, SDG.Framework.Modules.ModuleHook
The Setup MonoBehaviour
The root game object in the initial Unity scene carries a Setup MonoBehaviour. This is the single entry point into Unturned's managed code. Setup.Awake runs before any other Awake in the scene because the Setup component is the first executed in Unity's script execution order.
Setup.Awake sequence
─────────────────────
1. UnturnedPlayerLoop.initialize()
— Prunes the Unity PlayerLoop of unused subsystems
— Disables 2D physics, XR, Kinect, AI, Network, Substance, light probes
2. ThreadUtil.setupGameThread()
— Records the managed thread ID as "the game thread"
— Used elsewhere to assert single-thread access
3. Dedicator.awake()
— Reads command-line flags to determine dedicated server mode
— Sets Dedicator.IsDedicatedServer
4. Logs.awake()
— Initializes the console and file logging system
— Creates UnturnedLog.info/error/warn paths
5. ModuleHook.awake()
— Hooks AppDomain.AssemblyResolve and AppDomain.TypeResolve
— Loads module .module config files from the Modules directory
— Discovers and sorts modules by dependency order
— Creates Module objects and registers their assembly paths
6. Provider.awake()
— Initializes the Steamworks API (SteamClient, SteamUser, SteamFriends)
— Reads command-line configuration for server/client mode
— Sets up networking transport layer
7. ModuleHook.start()
— Scans core assembly for IModuleNexus implementations
— Calls initialize() on each found nexus (UnturnedNexus, FrameworkNexus)
— Calls initialize() on each loaded module's IModuleNexus implementations
8. Provider.start()
— Connects to Steam backend, initializes player identity
— Downloads workshop content list (client) or starts server bind (dedicated)
9. GlazierFactory.Create()
— Creates the UI system (IMGUI, uGUI, or UI Toolkit depending on build)
10. UnturnedPathfinding.Initialize()
— Sets up the navigation pathfinding systemSetup.Start
After Awake completes, Unity calls Setup.Start. The Start method initializes the custom post-processing stack, loads menu settings, applies resolution and VSync, and renders the first loading screen:
Setup.Start sequence
─────────────────────
1. postProcess.initialize()
— Setup the UnturnedPostProcess for custom rendering
2. MenuSettings.load()
— Read saved graphical and UI preferences
3. GraphicsSettings.applyResolution()
— Set the display resolution from saved settings
4. GraphicsSettings.ApplyVSyncAndTargetFrameRate()
— Limit FPS during async asset loading (public issue #3825)
5. LoadingUI.updateScene()
— Render the initial loading screen with background image,
tips, and progress barsThe VSync and frame-rate application in Setup.Start is a deliberate optimization: without it, Unity would render at unlimited FPS during the async asset loading phase, consuming CPU cycles that should go to asset decompression and parsing. The comment in source calls this out explicitly (public issue #3825).
The Custom Unity PlayerLoop
Unturned disables Unity native subsystems that it does not use. The UnturnedPlayerLoop.initialize() method in Setup.Awake step 1 calls PlayerLoop.GetDefaultPlayerLoop(), removes specific system types, and sets the modified loop back with PlayerLoop.SetPlayerLoop().
The disabled subsystems include:
| Disabled system | Reason |
|---|---|
EarlyUpdate.AnalyticsCoreStatsUpdate | Unity analytics not used |
EarlyUpdate.ARCoreUpdate | AR not used |
EarlyUpdate.UpdateKinect | Kinect not used |
EarlyUpdate.XRUpdate | VR/XR not used |
FixedUpdate.Physics2DFixedUpdate | 2D physics not used |
FixedUpdate.NewInputFixedUpdate | New Input System not used |
PreLateUpdate.AIUpdatePostScript | Unity AI not used |
PreLateUpdate.Physics2DLateUpdate | 2D physics not used |
PreLateUpdate.UpdateMasterServerInterface | Unity master server not used |
PreLateUpdate.UpdateNetworkManager | Unity UNet not used |
PreUpdate.AIUpdate | Unity AI not used |
PreUpdate.NewInputUpdate | New Input System not used |
PreUpdate.Physics2DUpdate | 2D physics not used |
PreUpdate.SendMouseEvents | Custom input handling replaces this |
PostLateUpdate.EnlightenRuntimeUpdate | Enlighten realtime GI not used |
PostLateUpdate.UpdateSubstance | Substance not used |
PostLateUpdate.XRPostLateUpdate | XR not used |
The recursive pruning function recursiveTidyPlayerLoop walks the subsystem tree and removes nodes whose System.Type matches the disabled set. It preserves all subsystems not in the exclusion set. This means Unturned still runs Unity's native rendering loop (Camera.Render), the 3D physics simulation (FixedUpdate.PhysicsFixedUpdate), and the animation system — but skips everything the game does not need.
The Director subsystem was removed from the disabled list in a 2022-12-09 update because mods and plugins use the Animator component, which depends on it.
The Loading UI State Machine
The LoadingUI MonoBehaviour is created during scene initialization and persists for the entire game session via DontDestroyOnLoad. It manages the visual loading screen with four progress bars and rotates gameplay tips.
Progress bars
The LoadingUI renders up to four concurrent progress bars, each reporting a different phase of asset loading:
| Progress bar | Visibility condition | Reports |
|---|---|---|
| Download | downloadProgressBar.IsVisible = true | Workshop file download progress (0.0 to 1.0) |
| Asset Bundle | loadingStats.isLoadingAssetBundles | Master bundle file load progress |
| Search | SearchLocationsFinishedSearching < RegisteredSearchLocations | Filesystem scan for .asset/.dat files |
| Read | FilesRead < FilesFound | Parsing found asset definition files |
| Asset Loading | Always visible | PopulateAsset calls on parsed definitions |
The bars stack vertically with 10px padding. Each bar is a SleekLoadingScreenProgressBar with a description label and a progress fill. The positions are recalculated in UpdateLoadingBarPositions() whenever a bar's visibility changes.
Background image system
The loading screen selects a random background image from one of three sources:
- Player screenshots in
Screenshots/directory (ifOptionsSettings.enableScreenshotsOnLoadingScreenis true and the screenshots have "NoUI" in their filename) - Level-specific screenshots in
Levels/<LevelName>/Screenshots/or the level'sLevel.png - Vanilla
LoadingScreens/directory
The image is loaded from disk via ReadWrite.readTextureFromFile. Files exceeding 10MB are excluded to prevent freezes. The background supports a parallax animation (disabled since 2022-05-17 due to async loading concerns).
Tip rotation
At each loading screen transition, updateScene() picks a random ELoadingTip value (ensuring it differs from the previous tip) and formats the tip text from Menu/MenuTips.dat. The tips cover hotkeys, gameplay mechanics, and crafting station information.
Ping warning system
When connecting to a server, LoadingUI.Update checks whether the connection ping (Provider.ClientPingMs) significantly exceeds the server browser ping (Provider.CurrentServerAdvertisement.PingMs). If the mismatch exceeds a threshold from LiveConfig, a warning box is shown suggesting an anycast proxy in front of Steam A2S server cache.
The isBlocked mechanism
LoadingUI uses a frame-count-based blocking mechanism to prevent the game world from rendering while the loading screen is active:
lastLoading = Time.frameCount + 1
isBlocked = Time.frameCount <= lastLoadingWhen blocked:
UnturnedMasterVolume.mutedByLoadingScreenis set to mute all audio- The
placeholderCamerais enabled to prevent the "no cameras rendering" warning - The UI root is set to the loading screen's SleekWindow
When unblocked, control passes to the appropriate UI system (PlayerUI, MenuUI, or EditorUI).
The Manager Singleton Pattern
The Managers MonoBehaviour is the singleton manager root. Its Awake method checks _isInitialized to prevent duplicate initialization (defensive against Unity scene reload quirks) and calls DontDestroyOnLoad. The only initialization it performs directly is calling GetComponent<SteamChannel>().setup() — all other manager initialization happens in Provider's startup sequence.
Provider Lifecycle
Provider is the central singleton that owns the Steamworks integration, networking state, server configuration, and the main tick loop. Its lifecycle spans the entire game session:
Provider.awake
- Read command-line flags (
AppId,server,port,players,name,map,password, etc.) - Initialize the Steamworks API via
SteamAPI.Init() - Set
Provider.isServerbased on command-line flags - Initialize networking transport layer
- Register debug callbacks
Provider.start
- Set player identity from Steamworks
- Initialize master server connection
- Begin the asset loading coroutine
- Launch into the appropriate game mode (menu, server, editor)
The Tick Loop
Unturned does not use Unity's Update() pattern for its core simulation. Instead, it drives game state through a combination of Unity's native FixedUpdate for physics and a manually-managed tick counter that governs game logic updates at a fixed rate.
The tick rate is controlled by Provider.UPDATE_RATE and related configuration. The game loop processes the world simulation in fixed-size time steps, with each tick handling:
- Player and zombie position updates
- Network message sending/receiving
- Collision detection
- Server-side AI updates
- Environmental state changes (weather, time-of-day)
Single-threaded design
Every subsystem that reads Provider.isInitialized or accesses the asset registry does so from the same thread that called Setup.Awake. The AssetsWorker uses background threads for file I/O and dat parsing, but the results are dequeued on the main thread via AssetsWorker.Update, which runs in Unity's main-thread Update loop. There is no lock contention because there is no concurrent access.
Asset Type Registration in UnturnedNexus
The UnturnedNexus.initialize() method is the single point where all built-in asset types and useable types are registered. It is called by ModuleHook.start() during initialization step 7. The registration populates two static registries:
Assets.assetTypes— maps string type names (e.g., "Gun", "Hat", "Shirt") to their correspondingItem*AssetC# classesAssets.useableTypes— maps string useable names to their correspondingUseable*C# classes
The shutdown() method performs the inverse: it removes every registered type from both dictionaries. This is called when the game shuts down or when modules are reloaded.
The type-to-class mapping
The string name in addType("Gun", typeof(ItemGunAsset)) directly corresponds to the Type field in mod .dat files. When the parser reads Type Gun, it calls assetTypes.getType("Gun") which returns typeof(ItemGunAsset), and the engine instantiates that class via Activator.CreateInstance.
Assets Loading Coroutine
The Assets.LoadAllAssets coroutine runs after Provider initialization. It is the longest phase of the boot sequence and is managed as a Unity coroutine so that the loading UI can render between frames.
Asset loading phases
─────────────────────
Phase 1: Discover master bundles
— Search registered content paths
— Look for MasterBundle.dat files
— Queue master bundle file reads on worker threads
Phase 2: Load master bundles
— Dequeue completed master bundle reads
— Call AssetBundle.LoadFromMemoryAsync on the main thread
Phase 3: Search for asset definitions
— Recursively scan directories for .asset and .dat files
— Parse files on worker threads
— Queue parsed AssetDefinition objects
Phase 4: Populate assets
— For each AssetDefinition on the main thread:
- Instantiate the asset class by Type
- Call PopulateAsset with bundle, data, localization
- Register the asset in defaultAssetMapping
- Update LoadingUI progressBetween each phase, the coroutine yields to allow the LoadingUI to render and the progress bars to update. The AssetsWorker handles file I/O and dat parsing on ThreadPool threads. Worker results are dequeued through a ConcurrentQueue<ResultItem>.
Loading Screen Transitions
The loading screen transitions through distinct visual states:
Startup loading — First screen after Unity loads. Shows a random tip, the "Loading" description text, and a progress bar for asset loading. Background image from LoadingScreens/ or Screenshots/ directory.
Level loading — When joining a server or loading a map. Shows level-specific background screenshots, level name, version, server name, and VAC/anti-cheat security status. Creator/contributor credits are rendered as a rich-text list positioned at 75% screen width.
Workshop download — When downloading workshop content. Shows a download progress bar with the filename of the currently-downloading item.
Tick Rate Management
The game simulation tick rate is decoupled from Unity's frame rate. FixedUpdate handles physics at Unity's configured timestep (typically 0.02 seconds, or 50 Hz). The game logic tick rate is governed by Provider.UPDATE_RATE and can vary based on server configuration.
Unturned applies VSync and target frame rate early in Setup.Start to prevent the rendering loop from consuming CPU during the asset loading phase. After loading completes, the frame rate is governed by the normal Unity rendering pipeline.
The Firerate field on gun assets operates in terms of ticks, not seconds. The rate of fire formula documented elsewhere (50 ÷ (Firerate + 1) rounds per second) is directly tied to the tick count, meaning the tick rate is a fundamental dimension of all gameplay timing in Unturned.
Worker Thread Safety
The AssetsWorker manages background loading through a thread-safe architecture:
ConcurrentQueue<ResultItem>is the single consumer-producer channelInterlockedincrements track loading progress counters- The
WorkerThreadStatehas a searcher thread (recursive directory walk) and a reader thread (file I/O and dat parsing) - Both threads signal completion via
Interlocked.ExchangeonisFinishedSearching - The main thread calls
TryDequeueResulteach frame to drain completed work
Worker threads use ThreadPool.QueueUserWorkItem rather than creating dedicated threads. The searcher thread does not yield during directory scanning — it loops at maximum speed until all subdirectories are enumerated. The reader thread yields on await during file reads but does not sleep while work is available.
Boot Sequence Flow Diagram
Unity launches
│
▼
Setup.Awake()
│
├─ UnturnedPlayerLoop.initialize()
│ └─ Prune Unity subsystems
│
├─ ThreadUtil.setupGameThread()
│ └─ Record main thread ID
│
├─ Dedicator.awake()
│ └─ Read server flags
│
├─ Logs.awake()
│ └─ Initialize logging
│
├─ ModuleHook.awake()
│ ├─ Hook AssemblyResolve
│ ├─ Discover .module files
│ ├─ Sort by dependency
│ └─ Register assembly paths
│
├─ Provider.awake()
│ └─ Steamworks init
│
├─ ModuleHook.start()
│ ├─ Find IModuleNexus in core
│ ├─ UnturnedNexus.initialize()
│ │ └─ Register 60+ asset types
│ ├─ FrameworkNexus.initialize()
│ └─ initializeModules()
│ └─ Call initialize on each module
│
├─ Provider.start()
│ └─ Begin asset loading coroutine
│
└─ Setup.Start()
├─ postProcess.initialize()
├─ MenuSettings.load()
├─ GraphicsSettings.applyResolution()
├─ VSync and target frame rate
└─ LoadingUI.updateScene()
└─ Loading screen visibleLoading Screen Architecture Summary
| Component | Source file | Role |
|---|---|---|
Setup | Unturned/Loading/Setup.cs | Boot sequence orchestrator |
UnturnedPlayerLoop | Unturned/Loading/UnturnedPlayerLoop.cs | Player loop subsystem pruning |
LoadingUI | Unturned/Loading/LoadingUI.cs | Loading screen rendering and progress |
ELoadingTip | Unturned/Loading/LoadingUI.cs | Tip enum (42 tips) |
SleekLoadingScreenProgressBar | UI namespace | Progress bar widget |
AssetLoadingStats | Unturned/Bundles/Assets.cs | Loading progress tracking |
AssetsWorker | Unturned/Bundles/AssetsWorker.cs | Multi-threaded asset definition loading |
Managers | Unturned/Managers/Managers.cs | Singleton manager root |
ThreadUtil and Game Thread Assertions
The call to ThreadUtil.setupGameThread() in Setup.Awake records the managed thread ID of the calling Unity main thread. This recorded ID is used elsewhere in the codebase for runtime assertions that game systems are accessed only from the correct thread. The assertion system provides a safety net:
csharp
public static void AssertGameThread()
{
// If current thread doesn't match recorded game thread, log warning
}In practice, the assertion rarely triggers because the entire game simulation runs on one thread. The only multi-threaded operations in Unturned are:
AssetsWorkerfile I/O and dat parsing (ThreadPool threads)- Network transport callbacks (Steam SDK callbacks on worker threads)
- Async file operations (Stream I/O with async/await)
All other code — game logic, rendering, UI, physics, animation — runs on the Unity main thread. This eliminates the need for locks, mutexes, or concurrent collections in gameplay code.
Loading Screen Animation System
The LoadingUI supports a background image animation that creates a slow parallax effect:
csharp
private static void EnableBackgroundAnim()
{
const float BACKGROUND_PADDING = 0.01f;
backgroundImage.SizeScale_X = 1.0f + BACKGROUND_PADDING;
backgroundImage.SizeScale_Y = 1.0f + BACKGROUND_PADDING;
if (Random.value < 0.5f)
{
// Left to right
animStart_X = -BACKGROUND_PADDING;
animEnd_X = 0.0f;
}
else
{
// Right to left
animStart_X = 0.0f;
animEnd_X = -BACKGROUND_PADDING;
}
float verticalDistance = Random.Range(0.0f, BACKGROUND_PADDING);
float verticalOffset = Random.Range(0.0f, BACKGROUND_PADDING - verticalDistance);
// Randomize vertical direction
}The animation pans the background image by 1% of its size over the course of the loading period. The animation direction (left-to-right or right-to-left, top-to-bottom or bottom-to-top) is randomized each time updateScene() is called.
However, as noted in the source code comments, this animation was disabled on 2022-05-17 because it "sadly does not look good enough yet without more async loading". The shouldAnimate variable is hard-coded to false, and the DisableBackgroundAnim() path is always taken.
Provider Configuration Flags
The Provider class reads a set of configuration flags from command-line arguments during awake(). These flags control every aspect of the game's runtime behavior:
| Flag | Value type | Default | Purpose |
|---|---|---|---|
-AppId | uint | 304930 | Steam App ID override |
-server | flag | not set | Run in dedicated server mode |
-port | uint16 | 27015 | Server listen port |
-players | uint8 | 24 | Max player count |
-name | string | "Unturned" | Server name |
-map | string | "PEI" | Default map |
-password | string | "" | Server password |
-nographics | flag | not set | Run without graphics (server) |
-logAssemblyResolve | flag | not set | Log assembly resolution attempts |
-NoVanillaAssemblySearch | flag | not set | Disable auto DLL discovery |
-ModulesPath | string | null | Override Modules directory |
-SkipAssets | flag | not set | Skip all asset loading |
-NoDeferAssets | flag | not set | Disable deferred asset loading |
-ValidateAssets | flag | not set | Extra asset validation |
-ParseAssetMetadata | flag | not set | Parse .dat metadata |
-ResaveAssets | flag | not set | Re-save .dat files after load |
Each flag is declared as a static field on Provider, Dedicator, or the relevant subsystem class. The flags are parsed from the command line by CommandLine.Get() and are available for the entire game session.
Unity Scene Loading
Unturned uses Unity's scene-based level system. The initial boot scene contains the Setup GameObject and the Managers GameObject. After the loading sequence completes, the game transitions to either:
- The Menu scene (
Menu/) for the main menu - The Level scene for a specific map (when connecting to a server or loading a save)
Scene transitions go through LoadingUI.updateScene() which:
- Hides all progress bars
- Resets the level loading progress to 0%
- Loads level-specific background screenshots
- Sets the level name, version, and server info
- Configures per-level tips and credits
The placeholder camera is enabled during scene transitions to prevent Unity's "no cameras rendering" warning. The LoadingUI persists across scene loads via DontDestroyOnLoad.
Async Loading and Coroutine Management
Asset loading uses Unity coroutines to spread work across multiple frames:
csharp
public static void RequestReloadAllAssets()
{
if (hasFinishedInitialStartupLoading && !isLoading)
{
instance.StartCoroutine(instance.LoadAllAssets());
}
}The LoadAllAssets coroutine cycles through the loading phases:
- Process master bundle results from worker queue
- Process asset definition results from worker queue
- Yield to let the loading UI render
- Repeat until all work is done
Between each yield, the AssetLoadingStats counters are updated and LoadingUI.NotifyAssetDefinitionLoadingProgress() redraws the progress bars. This interleaving ensures the loading screen remains responsive even during heavy asset processing.
The ShouldWaitForNewAssetsToFinishLoading property is checked by systems that need to wait for assets before proceeding:
csharp
internal static bool ShouldWaitForNewAssetsToFinishLoading
=> isLoading || instance.worker.IsWorking;Workshop Content Loading
Workshop content is handled after the initial asset loading is complete. The Provider downloads workshop files in the background, and when new content is installed, it calls Assets.RequestAddSearchLocation() to add the workshop directory as a new search location:
csharp
public static void RequestAddSearchLocation(string absoluteDirectoryPath, AssetOrigin origin)
{
instance.AddSearchLocation(absoluteDirectoryPath, origin);
}The AssetsWorker then scans the new directory, finds any .asset or .dat files, parses them, and queues them for population. The LoadingUI shows a download progress bar with the workshop file name during this process.
The hasLoadedUgc and hasLoadedMaps booleans on Assets track whether initial UGC and map loading has completed, preventing workshop installs that occur during startup from being processed before the core asset pipeline is ready.
The Dedicated Server Boot Path
When Dedicator.IsDedicatedServer is true, the boot sequence diverges from the client path in several significant ways:
| Component | Client behavior | Server behavior |
|---|---|---|
LoadingUI | Renders full UI with background images, tips, progress bars | Text-only progress via CommandWindow.Log; Destroy(gameObject) after init |
GlazierFactory.Create() | Creates UI system | Skipped — no UI system on headless build |
GraphicsSettings.applyResolution() | Sets display resolution | Skipped |
GraphicsSettings.ApplyVSyncAndTargetFrameRate() | Caps FPS during loading | Skipped — server uses application target frame rate |
ModuleHook.shouldLoadModules | Blocked when third-party anti-cheat is enabled | Always true (server can always load custom code) |
The dedicated server uses CommandWindow.Log for all loading progress output. The LoadingUI placeholder camera is never created — the server has no rendering pipeline, no post-processing stack, and no GL overlay system. The ModuleHook.awake() logs module discovery output via UnturnedLog.info regardless of platform.
ThreadUtil and Single-Thread Assertion
The ThreadUtil.setupGameThread() call in Setup.Awake records the managed thread ID of the calling thread:
csharp
// Simplified from actual implementation
public static void setupGameThread()
{
gameThreadId = Environment.CurrentManagedThreadId;
}This recorded ID is used elsewhere for debug assertions that game systems are only accessed from the expected thread. The assertion provides a safety net against accidental cross-thread access. In practice, the entire game simulation runs on one thread, so no lock contention exists. The only multi-threading is in AssetsWorker (file I/O), ThreadPool (worker threads), and network transport callbacks.
LoadingUI Update Loop Detail
The LoadingUI.Update() method runs every frame and implements the loading screen state machine:
csharp
private void Update()
{
if (!Dedicator.IsDedicatedServer && (
Assets.isLoading || Provider.isLoading ||
Level.isLoading || Player.isLoading || Level.isExiting))
{
lastLoading = Time.frameCount + 1;
}
bool blockedThisFrame = isBlocked;
UnturnedMasterVolume.mutedByLoadingScreen = blockedThisFrame;
placeholderCamera.enabled = blockedThisFrame;
if (blockedThisFrame)
{
// Render loading UI
Glazier.Get().Root = window;
// Show ping warning (client connecting to server)
}
else if (PlayerUI.instance != null)
{
PlayerUI.instance.Player_OnGUI();
}
else if (MenuUI.instance != null)
{
MenuUI.instance.Menu_OnGUI();
}
else if (EditorUI.instance != null)
{
EditorUI.instance.Editor_OnGUI();
}
}The frame-count approach (Time.frameCount + 1) was chosen over a realtime approach because realtime became unreliable when individual frames took too long to process. The loading screen holds the UI for one frame beyond the last loading operation, guaranteeing that the 100% progress state is visible before transitioning to the game UI.
Progress Bar Data Flow
The progress bars are updated through a multi-layered pipeline:
AssetsWorker (worker threads)
└─ Increments Interlocked counters
└─ Main thread Assets.Update() reads counters
└─ AssetLoadingStats calculates percentages
└─ LoadingUI.NotifyAssetDefinitionLoadingProgress()
├─ UpdateAssetBundleProgress()
├─ UpdateSearchProgress()
├─ UpdateReadProgress()
└─ UpdateAssetLoadingProgress()
└─ Set progress bar DescriptionText and ProgressPercentageEach progress bar has a visibility gating mechanism that shows the bar for one frame after its phase ends, so the "100%" text is visible. The wasLoadingAssetBundles, wasSearching, and wasReading booleans track the previous frame's state to implement this one-frame extension.
Provider Error Handling During Boot
If Steamworks initialization fails during Provider.awake(), the boot sequence does not abort. The Provider sets fallback mode flags that allow the game to run without Steam integration for local development. Error paths during initialization:
| Failure mode | Behavior |
|---|---|
| Steam not running | Falls back to LAN-only mode |
| Workshop download failure | Logs error, continues without workshop content |
| Module file parse error | Logs warning, skips that module |
| Asset file parse error | Sets asset.HasErrors = true, continues loading |
All critical subsystems have initialization guards (_isInitialized bool) that prevent double-initialization or use-before-init. The Managers singleton is the most visible example.
Appendix A: Complete Disabled Subsystem Reference
The following table lists every Unity PlayerLoop subsystem disabled in UnturnedPlayerLoop.initialize(), grouped by loop phase:
| Loop phase | Disabled subsystem | Unity type |
|---|---|---|
EarlyUpdate | AnalyticsCoreStatsUpdate | UnityEngine.PlayerLoop.EarlyUpdate.AnalyticsCoreStatsUpdate |
EarlyUpdate | ARCoreUpdate | UnityEngine.PlayerLoop.EarlyUpdate.ARCoreUpdate |
EarlyUpdate | DeliverIosPlatformEvents | UnityEngine.PlayerLoop.EarlyUpdate.DeliverIosPlatformEvents |
EarlyUpdate | UpdateKinect | UnityEngine.PlayerLoop.EarlyUpdate.UpdateKinect |
EarlyUpdate | XRUpdate | UnityEngine.PlayerLoop.EarlyUpdate.XRUpdate |
FixedUpdate | NewInputFixedUpdate | UnityEngine.PlayerLoop.FixedUpdate.NewInputFixedUpdate |
FixedUpdate | Physics2DFixedUpdate | UnityEngine.PlayerLoop.FixedUpdate.Physics2DFixedUpdate |
FixedUpdate | XRFixedUpdate | UnityEngine.PlayerLoop.FixedUpdate.XRFixedUpdate |
Initialization | XREarlyUpdate | UnityEngine.PlayerLoop.Initialization.XREarlyUpdate |
PostLateUpdate | EnlightenRuntimeUpdate | UnityEngine.PlayerLoop.PostLateUpdate.EnlightenRuntimeUpdate |
PostLateUpdate | ExecuteGameCenterCallbacks | UnityEngine.PlayerLoop.PostLateUpdate.ExecuteGameCenterCallbacks |
PostLateUpdate | UpdateLightProbeProxyVolumes | UnityEngine.PlayerLoop.PostLateUpdate.UpdateLightProbeProxyVolumes |
PostLateUpdate | UpdateSubstance | UnityEngine.PlayerLoop.PostLateUpdate.UpdateSubstance |
PostLateUpdate | XRPostLateUpdate | UnityEngine.PlayerLoop.PostLateUpdate.XRPostLateUpdate |
PostLateUpdate | XRPostPresent | UnityEngine.PlayerLoop.PostLateUpdate.XRPostPresent |
PostLateUpdate | XRPreEndFrame | UnityEngine.PlayerLoop.PostLateUpdate.XRPreEndFrame |
PreLateUpdate | AIUpdatePostScript | UnityEngine.PlayerLoop.PreLateUpdate.AIUpdatePostScript |
PreLateUpdate | Physics2DLateUpdate | UnityEngine.PlayerLoop.PreLateUpdate.Physics2DLateUpdate |
PreLateUpdate | UpdateMasterServerInterface | UnityEngine.PlayerLoop.PreLateUpdate.UpdateMasterServerInterface |
PreLateUpdate | UpdateNetworkManager | UnityEngine.PlayerLoop.PreLateUpdate.UpdateNetworkManager |
PreUpdate | AIUpdate | UnityEngine.PlayerLoop.PreUpdate.AIUpdate |
PreUpdate | NewInputUpdate | UnityEngine.PlayerLoop.PreUpdate.NewInputUpdate |
PreUpdate | Physics2DUpdate | UnityEngine.PlayerLoop.PreUpdate.Physics2DUpdate |
PreUpdate | SendMouseEvents | UnityEngine.PlayerLoop.PreUpdate.SendMouseEvents |
Total: 24 subsystems disabled. The recursive pruning handles nested subsystem lists, so if any of these subsystems had child subsystems, they are also removed.
Appendix B: Asset Type Registration Complete Listing
The full list of asset types registered in UnturnedNexus.initialize():
Item types (43): Hat, Pants, Shirt, Backpack, Vest, Mask, Glasses, Gun, Sight, Tactical, Grip, Barrel, Magazine, Food, Water, Medical, Melee, Fuel, Tool, Vehicle_Repair_Tool, Barricade, Storage, Tank, Generator, Beacon, Farm, Trap, Structure, Supply, Throwable, Grower, Optic, Refill, Fisher, Cloud, Map, Compass, Key, Box, Arrest_Start, Arrest_End, Detonator, Charge, Library, Filter, Sentry, Tire, Oil_Pump, Vehicle_Paint_Tool, Vehicle_Lockpick_Tool
Non-item types (17): Effect, Large, Medium, Small, NPC, Decal, Resource, Vehicle, Animal, Mythic, Skin, Spawn, Dialogue, Quest, Vendor, RewardsList, Redirector, ServerCuration, Tag, Road
Useable types (21): Barricade, Battery_Vehicle, Carjack, Clothing, Consumeable, Fisher, Fuel, Grower, Gun, Melee, Optic, Refill, Structure, Throwable, Tire, Cloud, Arrest_Start, Arrest_End, Detonator, Filter, Carlockpick, Walkie_Talkie, Housing_Planner, Vehicle_Paint
Document history
| Version | Date | Author | Notes |
|---|---|---|---|
| 1.0 | 2026-07-27 | 57 Studios | Initial publication. Boot sequence, player loop, loading UI, provider lifecycle. |
