Skip to content

Level Config, HUD Visibility, and Game Mode System

The level configuration system in Unturned is anchored by LevelInfoConfigData, a class that maps directly to each map's Config.json file. It stores every per-map toggle, crediting, physics override, difficulty override, arena loadout, workshop dependency, and HUD visibility flag. The LevelInfo object wraps this config with filesystem metadata (path, name, size, type, hash, workshop status). The LevelManager class runs the per-frame game mode logic — arena state machine, airdrop scheduling, and level type detection.

This article covers the full Config.json deserialization path, HUD element visibility registration, the arena compactor state machine, the horde mode blocker, and the ELevelType / EGameMode enums that govern which subsystems activate.

Source code location: Unturned/Level/LevelInfo.cs, Unturned/Managers/LevelManager.cs, Unturned/Game/EGameMode.cs, Unturned/Game/GameMode.cs, Unturned/Level/Level.cs

Config.json Deserialization

LevelInfoConfigData is the direct object model of a map's Config.json. Every field is public and Newtonsoft.Json-serialized. The constructor sets sensible defaults:

csharp
public LevelInfoConfigData()
{
    Creators = new string[0];
    Collaborators = new string[0];
    Thanks = new string[0];
    CustomCredits = new Dictionary<string, string[]>();
    Item = 0;
    Associated_Stockpile_Items = new int[0];
    Feedback = null;
    Asset = AssetReference<LevelAsset>.invalid;
    Trains = new List<LevelTrainAssociation>();
    Mode_Config_Overrides = new Dictionary<string, object>();

    Allow_Underwater_Features = false;
    Terrain_Snow_Sparkle = false;
    Use_Legacy_Clip_Borders = true;
    Use_Legacy_Ground = true;
    Use_Legacy_Water = true;
    Use_Vanilla_Bubbles = true;
    Use_Legacy_Snow_Height = true;
    Use_Legacy_Oxygen_Height = true;
    Use_Rain_Volumes = false;
    Use_Snow_Volumes = false;
    Is_Aurora_Borealis_Visible = false;
    Snow_Affects_Temperature = true;
    Has_Atmosphere = true;
    Allow_Crafting = true;
    Allow_Skills = true;
    Allow_Information = true;
    Gravity = -9.81f;
    Blimp_Altitude = 150.0f;
    Max_Walkable_Slope = -1;
    Prevent_Building_Near_Spawnpoint_Radius = 16;

    Category = ESingleplayerMapCategory.MISC;

    Use_Arena_Compactor = true;
    Arena_Loadouts = new List<ArenaLoadout>();
    Spawn_Loadouts = new List<ArenaLoadout>();
    RequiredWorkshopFileIds = new ulong[0];

    Version = "3.0.0.0";
}

Credit and Metadata Fields

The config stores map creators as a flat string array (Creators), a separate Collaborators array, and a Thanks array. A CustomCredits dictionary maps section names to string arrays, supporting per-language or per-role credit sections. The Item field is the map's inventory icon item ID. Associated_Stockpile_Items links the map to specific stockpile item IDs. The Feedback string is a web URL or null; if set, LevelInfo.feedbackUrl returns it instead of the default Workshop discussions URL.

The Asset field is an AssetReference<LevelAsset> that lets maps override their level asset. If unset, the fallback is LevelAsset.defaultLevel.

Physics and Environment Overrides

csharp
public float Gravity;                    // default -9.81
public float Blimp_Altitude;             // default 150.0
public float Max_Walkable_Slope;         // default -1 (unlimited)
public float Prevent_Building_Near_Spawnpoint_Radius; // default 16

Gravity replaces the default physics gravity when non-zero. Blimp_Altitude sets the height of the arena blimp / directional indicator. Max_Walkable_Slope clamps the angle at which the player can walk; -1 disables the clamp. Prevent_Building_Near_Spawnpoint_Radius creates a no-build zone around player spawns.

Feature Toggles

A large block of boolean flags controls legacy compatibility and feature opt-in:

  • Use_Legacy_Clip_Borders, Use_Legacy_Ground, Use_Legacy_Water — revert to pre-overhaul rendering.
  • Use_Legacy_Snow_Height, Use_Legacy_Fog_Height, Use_Legacy_Oxygen_Height — use old height-based thresholds instead of volume-based zones.
  • Use_Underground_Whitelist — when true, positions underground require a whitelist volume to be valid.
  • Use_Rain_Volumes, Use_Snow_Volumes — enable the volume-based weather system.
  • Has_Atmosphere — toggles skybox atmosphere rendering.
  • Has_Global_Electricity — when true, powered objects always have electricity (generators are decorative).
  • Allow_Holiday_Redirects — certain objects redirect to holiday variants in-game.
  • Allow_Crafting, Allow_Skills, Allow_Information — disable crafting, skills, and info popups.
  • Enable_Clutter_Option — map creator has verified clutter works as expected.
  • Enable_Static_Volumes — map creator has verified volumes are placed only in the level editor (not Unity prefabs).
  • Is_Aurora_Borealis_Visible — controls aurora rendering on snow maps.
  • Snow_Affects_Temperature — snow regions lower player temperature.
  • Allow_Underwater_Features — enables underwater bubble particles and underwater post-processing.
  • Terrain_Snow_Sparkle — ground sparkle effect in snow biomes.
  • Use_Legacy_Fog_Height — use old fog height calculation.
  • Use_Vanilla_Bubbles — use default bubble particle system.
  • Weather_OverrideELevelWeatherOverride enum that forces rain or snow regardless of weather cycle.

Per-Difficulty Config Overrides

csharp
public Dictionary<string, object> GetPerDifficultyConfigOverrides(EGameMode mode)
{
    switch (mode)
    {
        case EGameMode.EASY:
            return EasyDifficulty_Config_Overrides;
        case EGameMode.NORMAL:
            return NormalDifficulty_Config_Overrides;
        case EGameMode.HARD:
            return HardDifficulty_Config_Overrides;
        default:
            return null;
    }
}

GetPerDifficultyConfigOverrides(EGameMode mode) returns a Dictionary<string, object> from either EasyDifficulty_Config_Overrides, NormalDifficulty_Config_Overrides, or HardDifficulty_Config_Overrides. These dictionaries override Config.json values at runtime based on the difficulty selection. The Mode_Config_Overrides dictionary provides mode-specific overrides that apply regardless of difficulty.

The override dictionaries use string-keyed object values, meaning they are schema-less at the config level — any Config.json key can be overridden per-difficulty. The consuming code checks modeConfigData at runtime, which merges these overrides into the base config.

Batching and Loading

csharp
public int Batching_Version;
public int Batching_Max_Texture_Size = 128;

Batching_Version is non-zero only when the map creator has verified level batching works. Batching_Max_Texture_Size caps the atlas texture size to prevent exceeding GPU limits (4K recommended max). The Hash byte array stores the SHA1 hash of Level.dat for integrity verification. RequiredWorkshopFileIds stores the UGC dependency list.

The [Newtonsoft.Json.JsonIgnore] attribute on Hash and PackedVersion means these fields are computed at runtime, not serialized in Config.json. PackedVersion is the Version string ("a.b.c.d") packed into a uint for efficient network comparison.

Train Associations

csharp
public class LevelTrainAssociation
{
    public ushort VehicleID;
    public ushort RoadIndex;
    public float Min_Spawn_Placement = 0.1f;
    public float Max_Spawn_Placement = 0.9f;
}

LevelTrainAssociation pairs a VehicleID (the train vehicle asset) with a RoadIndex (the road path index) and spawn placement range (Min_Spawn_Placement / Max_Spawn_Placement from 0.1 to 0.9). The level only spawns each train vehicle once — if the vehicle already exists on the map, it is not respawned. This prevents duplicate train spawns on reload.

HUD Element Visibility

The PlayerUI_*Visible booleans in LevelInfoConfigData control which HUD elements are shown:

csharp
public bool PlayerUI_HealthVisible = true;
public bool PlayerUI_FoodVisible = true;
public bool PlayerUI_WaterVisible = true;
public bool PlayerUI_VirusVisible = true;
public bool PlayerUI_StaminaVisible = true;
public bool PlayerUI_OxygenVisible = true;
public bool PlayerUI_GunVisible = true;

All default to true. Map creators set any to false to suppress that element, useful for custom game modes or hub maps where survival stats are irrelevant. These are consumed by PlayerUI to conditionally enable/disable the corresponding UI panels.

The visibility flags are rigid — there is no system for adding custom HUD elements via config. Each flag maps to a hardcoded UI element in the PlayerUI class. If a flag is false, the corresponding panel is either hidden or set to display: none at the Glazier level.

LevelInfo — Map Metadata Object

LevelInfo wraps the filesystem state of a loaded map. Its constructor:

csharp
public LevelInfo(string newPath, string newName, ELevelSize newSize, ELevelType newType,
    bool newEditable, LevelInfoConfigData newConfigData, ulong publishedFileId, byte[] hash)
{
    path = newPath;
    _name = newName;
    _size = newSize;
    _type = newType;
    _isEditable = newEditable;
// In Unity Editor, always editable:
#if UNITY_EDITOR
    _isEditable = true;
#endif
    configData = newConfigData;
    isFromWorkshop = publishedFileId > 0;
    this.publishedFileId = publishedFileId;
    this.hash = hash;
}

Parameters:

  • newPath — absolute path to the map folder.
  • newName — short folder name.
  • newSizeELevelSize enum (TINY, SMALL, MEDIUM, LARGE, INSANE).
  • newTypeELevelType enum (SURVIVAL, ARENA, HORDE, MENU, EDITOR).
  • newEditable — whether the map can be modified in the editor.
  • newConfigData — the LevelInfoConfigData instance.
  • publishedFileId — Workshop file ID (zero for non-Workshop maps).
  • hash — SHA1 of the Level.dat.

isCurated

csharp
public bool isCurated
{
    get
    {
        if (isFromWorkshop)
        {
            foreach (CuratedMapLink link in Provider.statusData.Maps.Curated_Map_Links)
            {
                if (link.Workshop_File_Id == publishedFileId)
                    return true;
            }
            return false;
        }
        else
        {
            return name == "France" || name == "Canyon Arena";
        }
    }
}

Checks whether the Workshop file ID matches an entry in Provider.statusData.Maps.Curated_Map_Links or if the map name is the hardcoded "France" or "Canyon Arena".

feedbackUrl

csharp
public string feedbackUrl
{
    get
    {
        if (configData != null && string.IsNullOrEmpty(configData.Feedback) == false)
            return configData.Feedback;
        else if (isFromWorkshop)
            return "https://steamcommunity.com/sharedfiles/filedetails/discussions/" + publishedFileId;
        else
            return null;
    }
}

Returns the explicit feedback URL from config, falls back to the Workshop discussions page, or null for non-Workshop maps.

Localization

getLocalization() probes in order:

  1. {path}/{language}.dat — map-specific localization for current language.
  2. {localizationRoot}/Maps/{name}.dat — global localization for this map name.
  3. {localizationRoot}/Maps/{name_with_underscores}.dat — same but with underscores.
  4. {path}/English.dat — fallback English localization.
  5. Empty Local — no localization (returns the folder name as the display name).

getLocalizedName() returns the Name key from localization files, or the folder name as fallback.

Image Paths

GetPreviewImageFilePath() returns Preview.png (320x180) with a fallback to the loading screen image. GetLoadingScreenImagePath() picks a random screenshot from /Screenshots or falls back to Level.png. GetRandomScreenshotPath() lists files in the /Screenshots directory and picks one via LoadingUI.GetRandomImagePathInDirectory().

Dependency Checking

csharp
public bool IsMissingAnyDependencies()
{
    if (configData == null || configData.RequiredWorkshopFileIds == null || configData.RequiredWorkshopFileIds.Length < 1)
        return false;
    foreach (ulong workshopFileId in configData.RequiredWorkshopFileIds)
    {
        AssetOrigin origin = Assets.FindWorkshopFileOrigin(workshopFileId);
        if (origin == null || origin.assets == null || origin.assets.IsEmpty())
            return true;
    }
    return false;
}

Checks whether all required Workshop dependencies are present and loaded. Returns true if any required file has no loaded assets.

ELevelType and Game Mode Activation

csharp
public enum ELevelType
{
    SURVIVAL,
    ARENA,
    HORDE,
    MENU,
    EDITOR
}

LevelManager.levelType is set from Level.info.type on level load. This governs:

  • Arena mode (ELevelType.ARENA): Activates the arena state machine (arenaTick()), arena compactor, arena player tracking, and arena-specific airdrop logic. The isArenaMode property is a shorthand for levelType == ELevelType.ARENA.
  • Horde mode (ELevelType.HORDE): Disables airdrops entirely — airdropInit() is skipped, AirdropUpdate() returns immediately. The horde mode game loop is driven by waves (managed by separate systems).
  • Survival mode (ELevelType.SURVIVAL): Full airdrop scheduling, normal gameplay loop, saved spawn positions via Player.dat.

EGameMode is a [NetEnum] with values EASY, NORMAL, HARD, ANY, TUTORIAL. It governs difficulty-based config override selection and is sent over the network via the NetEnum codegen.

The GameMode base class:

csharp
public class GameMode
{
    public virtual GameObject getPlayerGameObject(SteamPlayerID playerID)
    {
        if (Dedicator.IsDedicatedServer)
            return Object.Instantiate(Resources.Load<GameObject>("Characters/Player_Dedicated"));
        else
        {
            if (playerID.steamID == Provider.client)
            {
                string path = /* WITH_NOREDIST check for Player_Server */;
                return Object.Instantiate(Resources.Load<GameObject>(path));
            }
            else
                return Object.Instantiate(Resources.Load<GameObject>("Characters/Player_Client"));
        }
    }
}

getPlayerGameObject() selects the player prefab:

  • Dedicated server: Player_Dedicated (minimal, no visuals or audio).
  • Local client on listen server: Player_Server (with audio listener).
  • Remote clients: Player_Client (input-receiving, no audio listener).

ArenaGameMode and SurvivalGameMode are trivial subclasses that override ToString() to return "Arena" and "Survival" respectively. They don't override getPlayerGameObject(), so the base class behavior applies.

Arena Mode State Machine

LevelManager implements the arena game mode as a deterministic state machine with eight states (EArenaState):

csharp
public enum EArenaState
{
    LOBBY,
    CLEAR,
    WARMUP,
    SPAWN,
    PLAY,
    FINALE,
    RESTART,
    INTERMISSION
}

The state machine runs from arenaTick(), called every frame in arena mode on the server:

csharp
private void arenaTick()
{
    // Update compactor radius lerp
    if (Time.realtimeSinceStartup > nextAreaModify)
    {
        _arenaCurrentRadius = arenaCurrentRadius - (Time.deltaTime * arenaCompactorSpeed);
        if (arenaCurrentRadius < arenaTargetRadius)
        {
            _arenaCurrentRadius = arenaTargetRadius;
            // If compactor pause is enabled, compute new target zone
            if (Provider.isServer && Level.info.configData.Use_Arena_Compactor &&
                Provider.modeConfigData.Events.Arena_Use_Compactor_Pause)
            {
                float newArenaCompactorSpeed = compactorSpeed;
                Vector3 newArenaTargetCenter;
                float newArenaTargetRadius;
                getArenaTarget(arenaTargetCenter, arenaTargetRadius,
                    out newArenaTargetCenter, out newArenaTargetRadius);
                SendArenaOrigin.InvokeAndLoopback(ENetReliability.Reliable,
                    Provider.GatherRemoteClientConnections(),
                    arenaTargetCenter, arenaTargetRadius,
                    arenaTargetCenter, arenaTargetRadius,
                    newArenaTargetCenter, newArenaTargetRadius,
                    newArenaCompactorSpeed,
                    (byte)Provider.modeConfigData.Events.Arena_Compactor_Pause_Timer);
            }
        }
        arenaSqrRadius = arenaCurrentRadius * arenaCurrentRadius;
    }
    // Update visual walls on client
    if (!Dedicator.IsDedicatedServer) { /* position and scale arena wall meshes */ }
    // Countdown timer
    if (countTimerMessages >= 0) { /* countdown display and audio */ }
    // State machine dispatch
    if (Provider.isServer)
    {
        switch (arenaState) { /* dispatch to state methods */ }
    }
}

Lobby

Waiting for enough players. Calls findGroups() to count non-grouped players and unique groups. Compares nonGroups + arenaGroups.Count against minPlayers (which returns Provider.modeConfigData.Events.Arena_Min_Players on dedicated servers, or 1 in singleplayer). If insufficient, broadcasts EArenaMessage.LOBBY. If enough players are ready, transitions to CLEAR.

Clear

Destroys all existing world state via manager clear methods. Initializes the arena compactor:

csharp
private void arenaClear()
{
    AnimalManager.askClearAllAnimals();
    VehicleManager.askVehicleDestroyAll();
    BarricadeManager.askClearAllBarricades();
    StructureManager.askClearAllStructures();
    ItemManager.askClearAllItems();
    EffectManager.askEffectClearAll();
    ObjectManager.askClearAllObjects();
    ResourceManager.askClearAllResources();
    arenaPlayers.Clear();

    Vector3 newArenaCurrentCenter = Vector3.zero;
    float newArenaCurrentRadius = Level.size / 2.0f;
    if (Level.info.configData.Use_Arena_Compactor)
    {
        ArenaCompactorVolume node = ArenaCompactorVolumeManager.Get().GetRandomVolumeOrNull();
        if (node != null)
        {
            newArenaCurrentCenter = node.transform.position;
            newArenaCurrentCenter.y = 0;
            newArenaCurrentRadius = node.GetSphereRadius();
        }
    }
    // ... compute target, send to clients, transition to WARMUP
}

If Use_Arena_Compactor is true, picks a random ArenaCompactorVolume for initial center and radius. If false, sets radius to 16384 (effectively disabling the compactor). If Arena_Use_Compactor_Pause is on, computes a target shrink zone; otherwise targets radius 0.5. Sends SendArenaOrigin with all parameters.

Warmup

Counts down via countTimerMessages. When the timer expires, re-checks player count. If enough, transitions to SPAWN. If players left, returns to LOBBY.

Spawn

Spawns items at all item spawnpoints, vehicles from LevelVehicles.spawns with WasNaturallySpawned = true, animals from LevelAnimals.spawns. Filters player alt-spawns to those within the arena's current safe zone, then distributes players randomly across spawnpoints:

csharp
private void arenaSpawn()
{
    // Spawn items at all item spawnpoints
    for (byte x = 0; x < Regions.WORLD_SIZE; x++)
    {
        for (byte y = 0; y < Regions.WORLD_SIZE; y++)
        {
            if (LevelItems.spawns[x, y].Count > 0)
            {
                for (int index = 0; index < LevelItems.spawns[x, y].Count; index++)
                {
                    ItemSpawnpoint itemSpawn = LevelItems.spawns[x, y][index];
                    ushort id = LevelItems.getItem(itemSpawn);
                    if (id != 0)
                    {
                        Item item = new Item(id, EItemOrigin.ADMIN);
                        ItemManager.dropItem(item, itemSpawn.point, false, false, false);
                    }
                }
            }
        }
    }
    // Spawn vehicles
    foreach (VehicleSpawnpoint vehicleSpawn in LevelVehicles.spawns) { /* ... */ }
    // Spawn animals
    foreach (AnimalSpawnpoint animalSpawn in LevelAnimals.spawns) { /* ... */ }
    // Filter spawns within arena radius, distribute players
    List<PlayerSpawnpoint> playerSpawns = LevelPlayers.getAltSpawns();
    float removeSqrRadius = arenaCurrentRadius - SafezoneNode.MIN_SIZE;
    removeSqrRadius *= removeSqrRadius;
    // Remove spawns outside arena
    for (int spawnIndex = playerSpawns.Count - 1; spawnIndex >= 0; spawnIndex--)
    {
        if (MathfEx.HorizontalDistanceSquared(playerSpawns[spawnIndex].point, arenaCurrentCenter) > removeSqrRadius)
            playerSpawns.RemoveAt(spawnIndex);
    }
    // Distribute alive players to random spawns
    // Award arena loadouts
    foreach (ArenaLoadout loadout in Level.info.configData.Arena_Loadouts)
    {
        for (ushort amount = 0; amount < loadout.Amount; amount++)
        {
            ushort itemID = SpawnTableTool.ResolveLegacyId(loadout.Table_ID, EAssetType.ITEM, ...);
            if (itemID != 0)
                arenaPlayer.steamPlayer.player.inventory.forceAddItemAuto(new Item(itemID, true), ...);
        }
    }
}

Awards Arena_Loadouts from level config — resolves each loadout's spawn table into item IDs via SpawnTableTool.ResolveLegacyId() and force-adds them. Calls arenaAirdrop() if airdrops are enabled and a valid AirdropDevkitNode exists within the target zone.

Play

Per-frame logic in arenaPlay():

csharp
private void arenaPlay()
{
    if (nonGroups + arenaGroups.Count < minPlayers)
    {
        // Transition to FINALE — declare winner(s) or everyone loses
        arenaState = EArenaState.FINALE;
        // ...
    }
    else
    {
        for (int index = arenaPlayers.Count - 1; index >= 0; index--)
        {
            ArenaPlayer arenaPlayer = arenaPlayers[index];
            // Check distance to arena center
            float distance = MathfEx.HorizontalDistanceSquared(
                arenaPlayer.steamPlayer.player.transform.position, arenaCurrentCenter);
            bool outsideArea = distance > arenaSqrRadius || arenaCurrentRadius < 1.0f;
            if (outsideArea)
            {
                // Apply compactor damage (scales over time)
                float extraDamage = Provider.modeConfigData.Events.Arena_Compactor_Extra_Damage_Per_Second
                    * arenaPlayer.timeOutsideArea;
                float totalDamage = Provider.modeConfigData.Events.Arena_Compactor_Damage + extraDamage;
                byte roundedDamage = MathfEx.RoundAndClampToByte(totalDamage);
                arenaPlayer.steamPlayer.player.life.askDamage(roundedDamage, Vector3.up * 10,
                    EDeathCause.ARENA, ELimb.SPINE, CSteamID.Nil, out kill, bypassSafezone: true);
            }
            if (arenaPlayer.hasDied)
            {
                // Remove dead player, notify others
                SendArenaPlayer.InvokeAndLoopback(ENetReliability.Reliable, ...);
                arenaPlayers.RemoveAt(index);
            }
        }
    }
}

Players outside the safe zone take escalating damage. bypassSafezone: true prevents medical item spam exploits.

Finale / Restart / Intermission

Finale waits Arena_Finale_Timer seconds before transitioning to Restart. Restart sends timer, awards EPlayerStat.ARENA_WINS to survivors, kills remaining players. Intermission broadcasts and waits for timer, then returns to Lobby.

Compactor Target Calculation

csharp
private void getArenaTarget(Vector3 currentCenter, float currentRadius,
    out Vector3 targetCenter, out float targetRadius)
{
    targetCenter = currentCenter;
    targetRadius = currentRadius * Provider.modeConfigData.Events.Arena_Compactor_Shrink_Factor;
    // Random offset angle
    float offsetAngleInRadians = Random.Range(0, Mathf.PI * 2);
    float offsetDirection_X = Mathf.Cos(offsetAngleInRadians);
    float offsetDirection_Z = Mathf.Sin(offsetAngleInRadians);
    float offsetDistance = Random.Range(0, currentRadius - targetRadius);
    targetCenter += new Vector3(
        offsetDirection_X * offsetDistance, 0, offsetDirection_Z * offsetDistance);
    // Clamp to level bounds
    if (targetCenter.x - targetRadius < (-Level.size / 2) + Level.border)
        targetRadius = targetCenter.x - ((-Level.size / 2) + Level.border);
    // ... same for +x, -z, +z
}

The next safe zone is computed by shrinking the radius by Arena_Compactor_Shrink_Factor, offsetting the center by a random angle and distance, then clamping to the level border.

compactorSpeed

csharp
public static float compactorSpeed
{
    get
    {
        switch (Level.info.size)
        {
            case ELevelSize.TINY:  return Provider.modeConfigData.Events.Arena_Compactor_Speed_Tiny;
            case ELevelSize.SMALL:  return Provider.modeConfigData.Events.Arena_Compactor_Speed_Small;
            case ELevelSize.MEDIUM: return Provider.modeConfigData.Events.Arena_Compactor_Speed_Medium;
            case ELevelSize.LARGE:  return Provider.modeConfigData.Events.Arena_Compactor_Speed_Large;
            case ELevelSize.INSANE: return Provider.modeConfigData.Events.Arena_Compactor_Speed_Insane;
            default: return 0;
        }
    }
}

The compactor speed scales with level size, configurable per-size in the mode config.

Airdrop System

LevelManager manages airdrops separately from the arena state machine. On load, airdropInit() collects all AirdropDevkitNode instances with assigned cargo spawn tables. In survival mode, AirdropUpdate() schedules regular airdrops based on airdropFrequency (a timer measured in lighting cycles).

InternalSpawnAirdrop

csharp
private static void InternalSpawnAirdrop(Vector3 dropPosition, SpawnAsset cargoSpawnTable, float speed)
{
    // Pick approach direction
    Vector3 startingPosition = Vector3.zero;
    if (Random.value < 0.5f) // horizontal approach
    {
        startingPosition.x = Level.size / 2 * -Mathf.Sign(dropPosition.x);
        startingPosition.z = Random.Range(0, Level.size / 2) * -Mathf.Sign(dropPosition.z);
    }
    else // vertical approach
    {
        startingPosition.x = Random.Range(0, Level.size / 2) * -Mathf.Sign(dropPosition.x);
        startingPosition.z = Level.size / 2 * -Mathf.Sign(dropPosition.z);
    }
    float flightHeight = dropPosition.y + Random.Range(450.0f, 475.0f);
    dropPosition.y = 0;
    Vector3 direction = (dropPosition - startingPosition).normalized;
    startingPosition += direction * -2048.0f;
    float timeUntilDrop = (dropPosition - startingPosition).magnitude / speed;
    startingPosition.y = flightHeight;
    dropPosition.y = flightHeight;
    Vector3 velocity = direction * speed;
    // Send to clients
    SendAirdropState.InvokeAndLoopback(ENetReliability.Reliable,
        Provider.GatherRemoteClientConnections(), startingPosition, velocity);
}

The dropship trajectory is fully client-predicted from starting position and velocity. The server tracks the drop timer to spawn the carepackage at the correct time.

On insane-size maps, the airdrop position uses intBitCount: 14 for Vector3 serialization (range [-8192, 8192)) instead of the default 13 to accommodate aircraft starting positions up to 2 km outside the level.

Carepackage Spawning

SpawnCarepackage() instantiates the carepackage prefab (from LevelAsset.airdropRef or AirdropAsset.defaultAirdrop), applies the cargo spawn table, and sets up the Carepackage behavior component. The ConstantForce component is configured with the server's configured airdrop force.

Airdrop Update Loop

csharp
private void AirdropUpdate()
{
    float deltaTime = Time.deltaTime;
    for (int index = airdrops.Count - 1; index >= 0; index--)
    {
        AirdropInfo info = airdrops[index];
        info.state += info.Velocity * deltaTime;
        // Update visual model position on client
        if (Provider.isServer && !info.ServerHasDeployedCarepackage)
        {
            info.ServerTimeUntilDrop -= deltaTime;
            if (info.ServerTimeUntilDrop <= 0)
            {
                // Deploy carepackage
                info.ServerHasDeployedCarepackage = true;
                SpawnCarepackage(dropPosition, spawnTable, constantForce);
                SendSpawnCarepackage.Invoke(...);
            }
        }
        // Remove airplane model when it exits level bounds
    }
    // Schedule next airdrop in survival mode
    if (Provider.isServer && levelType == ELevelType.SURVIVAL && Provider.modeConfigData.Events.Use_Airdrops)
    {
        // Count down airdropFrequency, fire when zero
    }
}

Level Manager Networking

LevelManager uses the ClientStaticMethod RPC pattern for arena and airdrop state synchronization:

csharp
private static readonly ClientStaticMethod<Vector3, float, Vector3, float, Vector3, float, float, byte>
    SendArenaOrigin = ClientStaticMethod<Vector3, float, Vector3, float, Vector3, float, float, byte>
    .Get(ReceiveArenaOrigin);

[SteamCall(ESteamCallValidation.ONLY_FROM_SERVER)]
public static void ReceiveArenaOrigin(Vector3 newArenaCurrentCenter, float newArenaCurrentRadius,
    Vector3 newArenaOriginCenter, float newArenaOriginRadius,
    Vector3 newArenaTargetCenter, float newArenaTargetRadius,
    float newArenaCompactorSpeed, byte delay)
{
    _arenaCurrentCenter = newArenaCurrentCenter;
    _arenaCurrentRadius = newArenaCurrentRadius;
    arenaSqrRadius = arenaCurrentRadius * arenaCurrentRadius;
    // ...
    if (delay == 0) nextAreaModify = 0;
    else nextAreaModify = Time.realtimeSinceStartup + delay;
}

Arena message, player update, level number, and timer all follow the same pattern. SendInitialGlobalState() sends the current arena state to newly connecting clients so they have the correct compactor position and arena message.