Skip to content

Level Spawn Loading

The spawn system manages spawn point tables for items, zombies, vehicles, animals, and players. Five Level subclasses load weighted-tier tables and spawnpoint lists from separate data files. LevelNavigation (572 lines at Unturned/Level/LevelNavigation.cs) provides navmesh bounds and flag data for AI spawning and pathfinding with regional spatial indexing.

Source code locations: Unturned/Level/LevelItems.cs, Unturned/Level/LevelZombies.cs, Unturned/Level/LevelVehicles.cs, Unturned/Level/LevelAnimals.cs, Unturned/Level/LevelPlayers.cs, Unturned/Level/LevelNavigation.cs

Spawn Table Architecture

Weighted Tier Resolution

Every spawn table (items, zombies, vehicles, animals) uses the same resolution algorithm:

csharp
public T ResolveFromTable<T>(List<Tier<T>> tiers) where T : SpawnEntry
{
    // Step 1: Sum chances
    float totalChance = 0f;
    foreach (var tier in tiers) totalChance += tier.chance;

    // Step 2: Random roll
    float roll = Random.Range(0f, totalChance);
    float accumulated = 0f;

    // Step 3: Walk tiers
    foreach (var tier in tiers)
    {
        accumulated += tier.chance;
        if (roll <= accumulated)
        {
            // Step 4: Uniform random within tier
            return tier.entries[Random.Range(0, tier.entries.Count)];
        }
    }

    // Fallback: last tier's last entry
    return tiers.Last().entries.Last();
}

The buildTable() method pre-computes the cumulative distribution for O(n) lookup with O(1) per-resolution cost.

tableID Resolution

csharp
public ushort ResolveTableId(ushort tableID, EAssetType type)
{
    if (tableID == 0) return 0;
    return SpawnTableTool.ResolveLegacyId(tableID, type, (error) => {
        UnturnedLog.warning($"Spawn table {tableID} resolution failed: {error}");
    });
}

When tableID is 0, no resolution is attempted (the spawnpoint has no loot/spawn). When non-zero, SpawnTableTool.ResolveLegacyId converts the legacy table ID to a modern GUID-based reference.

LevelItems

File Format

Spawns/Items.dat:

byte version
// version 4-5: CSteamID (legacy creator)
byte tableCount
for each table:
    Color color (4 bytes: RGBA)
    string name (length-prefixed)
    byte tierCount
    for each tier:
        string tierName
        float chance
        byte spawnCount
        for each spawn:
            ushort itemID
ushort spawnpointCount
for each spawnpoint:
    byte type (table index)
    Vector3 point (12 bytes)

ItemTable Class

csharp
public class ItemTable
{
    public Color color;               // Editor UI identification
    public string name;               // Human-readable label
    public ushort tableID;            // SpawnTableAsset reference
    public List<ItemTier> tiers;      // Weighted tier list
    public float[] cumulativeChances; // Pre-computed for O(log n) resolution
}

public class ItemTier
{
    public string name;
    public float chance;
    public List<ItemSpawn> table;
}

public class ItemSpawn
{
    public ushort item;               // Asset ID
}

LevelZombies

ZombieTable Configuration

csharp
public class ZombieTable
{
    public Color color;
    public string name;
    public ushort tableID;
    public ushort lootID;             // Legacy loot table ID
    public List<ZombieTier> tiers;
    public ZombieSlot[] slots;        // 4 clothing slots
    public string difficultyGUID;     // ZombieDifficultyAsset reference
    public float hp;
    public float damage;
    public uint XP;
    public uint regen;
    public bool isMega;
    public uint tableUniqueId;        // Persistent unique ID for quest refs
}

ZombieSpwnpoint

Zombie spawnpoints are stored in a 2D region grid:

csharp
private static List<ZombieSpawnpoint>[,] _spawns;
public static List<ZombieSpawnpoint>[,] spawns => _spawns;

Each spawnpoint has a type field indexing into tables, a point (Vector3 position), and region coordinates for spatial lookup.

ZombieSlot — Clothing on Spawn

csharp
public class ZombieSlot
{
    public float chance;              // Probability of this slot being populated
    public List<ZombieCloth> cloths;  // Available clothing items
}

public class ZombieCloth
{
    public ushort item;               // Clothing item ID
}

Four slots represent hat, shirt, pants, and gear. Each slot has an independent chance of being populated. Slots are resolved at spawn time per zombie instance.

tableUniqueId and Boss Quests

csharp
public static int FindTableIndexByUniqueId(uint uniqueId)
{
    for (int i = 0; i < tables.Count; i++)
    {
        if (tables[i].tableUniqueId == uniqueId)
            return i;
    }
    return -1;
}

The ZombieBossQuest system uses tableUniqueId to track which zombie table the boss came from, enabling quest-specific boss tracking across sessions.

LevelVehicles

csharp
private static List<VehicleTable> tables;
private static List<VehicleSpawnpoint> spawns;

public VehicleAsset GetRandomAssetForSpawnpoint(VehicleSpawnpoint spawn)
{
    VehicleTable table = tables[spawn.type];
    VehicleSpawn entry = table.Resolve(); // Weighted random
    return Assets.find(EAssetType.VEHICLE, entry.vehicle) as VehicleAsset;
}

Vehicle tables support VehicleRedirectorAsset references for paint color handling — when a redirector is the resolved asset, the actual vehicle ID is looked up from the redirector's mapping.

LevelAnimals

Animal spawnpoints are stored in a flat list (not a 2D grid):

csharp
private static List<AnimalTable> tables;
private static List<AnimalSpawnpoint> spawns;

Each spawnpoint stores type (table index) and point (Vector3). Animal tables have tiers with weighted animal IDs, identical to the other spawn table formats.

LevelPlayers

csharp
public class PlayerSpawnpoint
{
    public Vector3 point;
    public float yaw;  // Degrees
}

Loaded from Spawns/Players.dat. Spawnpoints are validated at load time to ensure they're within level bounds and on/near terrain surface. Used for initial spawn and respawn positioning.

LevelNavigation

Bounds Data

csharp
private static List<Bounds> _bounds;                   // Expanded (+64) for player zone checks
private static List<Bounds> nonExpandedNavmeshBounds;  // Exact navmesh bounds for AI
private static RegionList<int> regionalBounds;          // O(1) spatial index in play mode

Flag Data

csharp
public class FlagData
{
    public string difficultyGUID;     // ZombieDifficultyAsset
    public byte maxZombies;           // Default 64
    public bool spawnZombies;         // Default true
    public bool hyperAgro;            // Default false
    public int maxBossZombies;        // -1 = no cap
}

Flag Class

csharp
public class Flag
{
    public Transform model;                           // Editor visual
    public IUnturnedNavmeshInterface navmeshInterface; // Baked navmesh
    public FlagData data;

    public Bounds CalculateBakingBounds()
    {
        return new Bounds(model.position,
            new Vector3(data.width, data.height, data.width));
    }
}

Spatial Queries

MethodPurposeBounds Used
tryGetBounds(Vector3, out byte)Get bound index for positionExpanded (+64)
tryGetNavigation(Vector3, out byte)Get navmesh index for AINon-expanded
checkSafe(byte)Validate bound indexNon-expanded
checkSafe(Vector3)Combined check: expanded bounds + valid heightExpanded
checkNavigation(Vector3)Is point on navmeshNon-expanded

Document history