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 modeFlag 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
| Method | Purpose | Bounds Used |
|---|---|---|
tryGetBounds(Vector3, out byte) | Get bound index for position | Expanded (+64) |
tryGetNavigation(Vector3, out byte) | Get navmesh index for AI | Non-expanded |
checkSafe(byte) | Validate bound index | Non-expanded |
checkSafe(Vector3) | Combined check: expanded bounds + valid height | Expanded |
checkNavigation(Vector3) | Is point on navmesh | Non-expanded |
