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 ​