Skip to content

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 system

Setup.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 bars

The 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 systemReason
EarlyUpdate.AnalyticsCoreStatsUpdateUnity analytics not used
EarlyUpdate.ARCoreUpdateAR not used
EarlyUpdate.UpdateKinectKinect not used
EarlyUpdate.XRUpdateVR/XR not used
FixedUpdate.Physics2DFixedUpdate2D physics not used
FixedUpdate.NewInputFixedUpdateNew Input System not used
PreLateUpdate.AIUpdatePostScriptUnity AI not used
PreLateUpdate.Physics2DLateUpdate2D physics not used
PreLateUpdate.UpdateMasterServerInterfaceUnity master server not used
PreLateUpdate.UpdateNetworkManagerUnity UNet not used
PreUpdate.AIUpdateUnity AI not used
PreUpdate.NewInputUpdateNew Input System not used
PreUpdate.Physics2DUpdate2D physics not used
PreUpdate.SendMouseEventsCustom input handling replaces this
PostLateUpdate.EnlightenRuntimeUpdateEnlighten realtime GI not used
PostLateUpdate.UpdateSubstanceSubstance not used
PostLateUpdate.XRPostLateUpdateXR 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 barVisibility conditionReports
DownloaddownloadProgressBar.IsVisible = trueWorkshop file download progress (0.0 to 1.0)
Asset BundleloadingStats.isLoadingAssetBundlesMaster bundle file load progress
SearchSearchLocationsFinishedSearching < RegisteredSearchLocationsFilesystem scan for .asset/.dat files
ReadFilesRead < FilesFoundParsing found asset definition files
Asset LoadingAlways visiblePopulateAsset 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:

  1. Player screenshots in Screenshots/ directory (if OptionsSettings.enableScreenshotsOnLoadingScreen is true and the screenshots have "NoUI" in their filename)
  2. Level-specific screenshots in Levels/<LevelName>/Screenshots/ or the level's Level.png
  3. 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 <= lastLoading

When blocked:

  • UnturnedMasterVolume.mutedByLoadingScreen is set to mute all audio
  • The placeholderCamera is 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

  1. Read command-line flags (AppId, server, port, players, name, map, password, etc.)
  2. Initialize the Steamworks API via SteamAPI.Init()
  3. Set Provider.isServer based on command-line flags
  4. Initialize networking transport layer
  5. Register debug callbacks

Provider.start

  1. Set player identity from Steamworks
  2. Initialize master server connection
  3. Begin the asset loading coroutine
  4. 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 corresponding Item*Asset C# classes
  • Assets.useableTypes — maps string useable names to their corresponding Useable* 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 progress

Between 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:

  1. 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.

  2. 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.

  3. 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 channel
  • Interlocked increments track loading progress counters
  • The WorkerThreadState has a searcher thread (recursive directory walk) and a reader thread (file I/O and dat parsing)
  • Both threads signal completion via Interlocked.Exchange on isFinishedSearching
  • The main thread calls TryDequeueResult each 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 visible

Loading Screen Architecture Summary

ComponentSource fileRole
SetupUnturned/Loading/Setup.csBoot sequence orchestrator
UnturnedPlayerLoopUnturned/Loading/UnturnedPlayerLoop.csPlayer loop subsystem pruning
LoadingUIUnturned/Loading/LoadingUI.csLoading screen rendering and progress
ELoadingTipUnturned/Loading/LoadingUI.csTip enum (42 tips)
SleekLoadingScreenProgressBarUI namespaceProgress bar widget
AssetLoadingStatsUnturned/Bundles/Assets.csLoading progress tracking
AssetsWorkerUnturned/Bundles/AssetsWorker.csMulti-threaded asset definition loading
ManagersUnturned/Managers/Managers.csSingleton 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:

  • AssetsWorker file 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:

FlagValue typeDefaultPurpose
-AppIduint304930Steam App ID override
-serverflagnot setRun in dedicated server mode
-portuint1627015Server listen port
-playersuint824Max player count
-namestring"Unturned"Server name
-mapstring"PEI"Default map
-passwordstring""Server password
-nographicsflagnot setRun without graphics (server)
-logAssemblyResolveflagnot setLog assembly resolution attempts
-NoVanillaAssemblySearchflagnot setDisable auto DLL discovery
-ModulesPathstringnullOverride Modules directory
-SkipAssetsflagnot setSkip all asset loading
-NoDeferAssetsflagnot setDisable deferred asset loading
-ValidateAssetsflagnot setExtra asset validation
-ParseAssetMetadataflagnot setParse .dat metadata
-ResaveAssetsflagnot setRe-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:

  1. Hides all progress bars
  2. Resets the level loading progress to 0%
  3. Loads level-specific background screenshots
  4. Sets the level name, version, and server info
  5. 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:

  1. Process master bundle results from worker queue
  2. Process asset definition results from worker queue
  3. Yield to let the loading UI render
  4. 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:

ComponentClient behaviorServer behavior
LoadingUIRenders full UI with background images, tips, progress barsText-only progress via CommandWindow.Log; Destroy(gameObject) after init
GlazierFactory.Create()Creates UI systemSkipped — no UI system on headless build
GraphicsSettings.applyResolution()Sets display resolutionSkipped
GraphicsSettings.ApplyVSyncAndTargetFrameRate()Caps FPS during loadingSkipped — server uses application target frame rate
ModuleHook.shouldLoadModulesBlocked when third-party anti-cheat is enabledAlways 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 ProgressPercentage

Each 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 modeBehavior
Steam not runningFalls back to LAN-only mode
Workshop download failureLogs error, continues without workshop content
Module file parse errorLogs warning, skips that module
Asset file parse errorSets 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 phaseDisabled subsystemUnity type
EarlyUpdateAnalyticsCoreStatsUpdateUnityEngine.PlayerLoop.EarlyUpdate.AnalyticsCoreStatsUpdate
EarlyUpdateARCoreUpdateUnityEngine.PlayerLoop.EarlyUpdate.ARCoreUpdate
EarlyUpdateDeliverIosPlatformEventsUnityEngine.PlayerLoop.EarlyUpdate.DeliverIosPlatformEvents
EarlyUpdateUpdateKinectUnityEngine.PlayerLoop.EarlyUpdate.UpdateKinect
EarlyUpdateXRUpdateUnityEngine.PlayerLoop.EarlyUpdate.XRUpdate
FixedUpdateNewInputFixedUpdateUnityEngine.PlayerLoop.FixedUpdate.NewInputFixedUpdate
FixedUpdatePhysics2DFixedUpdateUnityEngine.PlayerLoop.FixedUpdate.Physics2DFixedUpdate
FixedUpdateXRFixedUpdateUnityEngine.PlayerLoop.FixedUpdate.XRFixedUpdate
InitializationXREarlyUpdateUnityEngine.PlayerLoop.Initialization.XREarlyUpdate
PostLateUpdateEnlightenRuntimeUpdateUnityEngine.PlayerLoop.PostLateUpdate.EnlightenRuntimeUpdate
PostLateUpdateExecuteGameCenterCallbacksUnityEngine.PlayerLoop.PostLateUpdate.ExecuteGameCenterCallbacks
PostLateUpdateUpdateLightProbeProxyVolumesUnityEngine.PlayerLoop.PostLateUpdate.UpdateLightProbeProxyVolumes
PostLateUpdateUpdateSubstanceUnityEngine.PlayerLoop.PostLateUpdate.UpdateSubstance
PostLateUpdateXRPostLateUpdateUnityEngine.PlayerLoop.PostLateUpdate.XRPostLateUpdate
PostLateUpdateXRPostPresentUnityEngine.PlayerLoop.PostLateUpdate.XRPostPresent
PostLateUpdateXRPreEndFrameUnityEngine.PlayerLoop.PostLateUpdate.XRPreEndFrame
PreLateUpdateAIUpdatePostScriptUnityEngine.PlayerLoop.PreLateUpdate.AIUpdatePostScript
PreLateUpdatePhysics2DLateUpdateUnityEngine.PlayerLoop.PreLateUpdate.Physics2DLateUpdate
PreLateUpdateUpdateMasterServerInterfaceUnityEngine.PlayerLoop.PreLateUpdate.UpdateMasterServerInterface
PreLateUpdateUpdateNetworkManagerUnityEngine.PlayerLoop.PreLateUpdate.UpdateNetworkManager
PreUpdateAIUpdateUnityEngine.PlayerLoop.PreUpdate.AIUpdate
PreUpdateNewInputUpdateUnityEngine.PlayerLoop.PreUpdate.NewInputUpdate
PreUpdatePhysics2DUpdateUnityEngine.PlayerLoop.PreUpdate.Physics2DUpdate
PreUpdateSendMouseEventsUnityEngine.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

VersionDateAuthorNotes
1.02026-07-2757 StudiosInitial publication. Boot sequence, player loop, loading UI, provider lifecycle.