Level Objects Loading
LevelObjects (1325 lines at Unturned/Level/LevelObjects.cs) manages the placement of world objects — rocks, buildings, vehicles, props, and everything placed in the level editor. It supports undo/redo through the IReun transaction interface, region-based activation with incremental visibility across frames, material palette overrides, per-object culling volumes, and a dual storage model for regular objects vs. buildable objects (player-placed structures converted to level objects). The object system is the most feature-rich of the Level sub-systems.
Source code location: Unturned/Level/LevelObjects.cs
Dual Storage Model
csharp
private static List<LevelObject>[,] _objects; // Editor-placed objects
private static List<LevelBuildableObject>[,] _buildables; // Converted player structuresLevelObject Fields
| Field | Type | Purpose |
|---|---|---|
transform | Transform | The instantiated game object |
asset | ObjectAsset | Asset definition (mesh, materials, interactable) |
placementOrigin | ELevelObjectPlacementOrigin | How the object was placed |
instanceID | uint | Unique instance ID for reverse lookup |
materialPaletteOverride | AssetReference<MaterialPaletteAsset> | Per-instance material swap |
materialOverrideIndex | int | Which palette entry to use |
ownedCullingVolume | CullingVolume | Per-instance visibility gate |
skybox | Transform | Distant LOD copy for skybox rendering |
ELevelObjectPlacementOrigin
| Origin | Meaning |
|---|---|
| MANUAL | Hand-placed in editor |
| PAINT | Paint brush tool placement |
| AUTOMATIC | Procedural generation |
| BUILDABLE | Converted from player structure |
| MIGRATION | Converted from older format |
LevelBuildableObject
csharp
public class LevelBuildableObject
{
public Transform transform { get; private set; }
public ItemStructureAsset asset { get; private set; } // or ItemBarricadeAsset
}Buildable objects are created when player-placed structures are saved into the level file. Unlike runtime StructureDrop/BarricadeDrop, these are static level geometry participating in the undo/redo system.
Instance ID System
csharp
private static uint availableInstanceID;
internal static Dictionary<uint, LevelObject> instanceIdToObject;
private static uint generateUniqueInstanceID()
{
// Search for gaps from removed objects (reuse deleted IDs)
for (uint candidate = 0; candidate < availableInstanceID; candidate++)
{
if (!instanceIdToObject.ContainsKey(candidate))
return candidate;
}
return availableInstanceID++;
}The counter never decrements — removed object IDs are reused via gap search. This prevents instance ID overflow on long-running editor sessions.
Region Activation
Incremental Visibility Tracker
csharp
private static RegionIncrementalVisibilityTracker regionTracker;
private static RegionIncrementalVisibilityTracker skyboxRegionTracker;
private static bool[,] _regions;Region activation is spread across frames:
- Each frame, one region is activated/deactivated based on player distance.
- Objects in activating regions call
SetIsActiveInRegion(true)— model shown, skybox hidden. - Objects in deactivating regions call
SetIsActiveInRegion(false)— model hidden, skybox shown (if landmark quality permits).
SetIsActiveInRegion
csharp
public void SetIsActiveInRegion(bool isActive)
{
if (isActive)
{
transform.gameObject.SetActive(true);
if (skybox != null)
skybox.gameObject.SetActive(false);
}
else
{
transform.gameObject.SetActive(false);
if (skybox != null && IsLandmarkQualitySufficient())
skybox.gameObject.SetActive(true);
}
}Undo/Redo — IReun System
Ring Buffer Architecture
csharp
private const int REUN_SIZE = 64;
private static IReun[] reun = new IReun[REUN_SIZE];
public static int step;
private static int frame;
public static void register(IReun reundoable)
{
// Shift right to make room at index 0
for (int i = REUN_SIZE - 1; i > 0; i--)
reun[i] = reun[i - 1];
reun[0] = reundoable;
// Track which frame this action belongs to
reundoable.frame = frame++;
step = 0; // Reset undo position
}Undo Operation
csharp
public static void undo()
{
int targetStep = step + 1;
for (int i = 0; i < REUN_SIZE; i++)
{
if (reun[i] != null && reun[i].frame == targetStep)
{
reun[i].undo();
step = targetStep;
return;
}
}
}IReun Implementations
| Implementation | Stored Data | undo() Behavior |
|---|---|---|
ReunObjectAdd | Position, rotation, scale, asset GUIDs | Removes the object from _objects and destroys transform |
ReunObjectRemove | Transform, asset GUIDs, region coordinates | Re-adds object at original position with original asset |
ReunObjectTransform | From/to position, rotation, scale | Moves object back to from-position; updates region if boundary crossed |
Object Operations
addObject
csharp
public static Transform addObject(Vector3 position, Quaternion rotation, Vector3 scale,
ObjectAsset asset, ELevelObjectPlacementOrigin origin)
{
byte x, y;
if (!Regions.tryGetCoordinate(position, out x, out y)) return null;
uint id = generateUniqueInstanceID();
LevelObject obj = new LevelObject(position, rotation, scale, asset, origin, id);
_objects[x, y].Add(obj);
instanceIdToObject[id] = obj;
if (_regions[x, y])
obj.SetIsActiveInRegion(true);
return obj.transform;
}removeObject
csharp
public static void removeObject(Transform target)
{
byte x, y;
if (!tryGetRegion(target, out x, out y)) return;
for (int i = 0; i < _objects[x, y].Count; i++)
{
if (_objects[x, y][i].transform == target)
{
LevelObject obj = _objects[x, y][i];
instanceIdToObject.Remove(obj.instanceID);
obj.destroy();
_objects[x, y].RemoveAt(i);
return;
}
}
}transformObject — Region Boundary Crossing
csharp
public static void transformObject(Transform target, Vector3 newPosition,
Quaternion newRotation, Vector3 newScale)
{
byte oldX, oldY, newX, newY;
if (!tryGetRegion(target, out oldX, out oldY)) return;
if (!Regions.tryGetCoordinate(newPosition, out newX, out newY)) return;
LevelObject obj = FindObjectByTransform(target, oldX, oldY);
if (obj == null) return;
obj.transform.position = newPosition;
obj.transform.rotation = newRotation;
obj.transform.localScale = newScale;
// Region boundary crossing
if (oldX != newX || oldY != newY)
{
_objects[oldX, oldY].Remove(obj);
_objects[newX, newY].Add(obj);
}
// Skybox update
if (obj.skybox != null)
{
obj.skybox.position = newPosition;
obj.skybox.rotation = newRotation;
}
// Culling volume update
if (obj.ownedCullingVolume != null)
obj.ownedCullingVolume.OnLevelObjectMoved();
}Material Palette Override System
csharp
public AssetReference<MaterialPaletteAsset> materialPaletteOverride;When set to a valid MaterialPaletteAsset, the object's renderers use the palette's material slots instead of the ObjectAsset's defaults. The override is applied at load time:
csharp
if (obj.materialPaletteOverride.isValid)
{
MaterialPaletteAsset palette = Assets.find(obj.materialPaletteOverride) as MaterialPaletteAsset;
if (palette != null && obj.materialOverrideIndex < palette.materials.Length)
{
Material[] mats = obj.materialOverrideIndex < palette.materials.Length
? new[] { palette.materials[obj.materialOverrideIndex] }
: obj.asset.defaultMaterials;
obj.renderer.sharedMaterials = mats;
}
}Enables duplicated house models with different paint schemes without duplicating the entire mesh.
Per-Object Culling Volume
csharp
public CullingVolume ownedCullingVolume;When assigned, visibility is gated by whether the player's camera is inside the volume. Used for objects that should only appear from specific vantage points (interior details visible only from inside a building, etc.).
Save/Load Format
Binary Format (SAVEDATA_VERSION_CURRENT = 12)
byte SAVEDATA_VERSION
uint32 availableInstanceID // Counter state for ID continuity
uint16 objectCount
for each object:
Guid assetGUID (16 bytes)
Vector3 position (12 bytes: 3x float)
Quaternion rotation (16 bytes: 4x float)
Vector3 scale (12 bytes: 3x float)
uint32 instanceID (4 bytes)
byte placementOrigin (1 byte)
// Version >= 11:
Guid materialPaletteOverrideGUID (16 bytes, or Guid.Empty)
// Version >= 12:
byte cullingOverrideFlags (1 byte)Total per object: 62+ bytes (variable with palette override).
preserveMissingAssets
csharp
public static CommandLineFlag preserveMissingAssets =
new CommandLineFlag(true, "-NoPreserveMissingObjects");When true (default), objects with missing assets get a placeholder transform for re-save. When -NoPreserveMissingObjects is set, missing-asset objects are zeroed on next save.
Buildable Objects
LevelBuildableObject instances are created from player-placed structures saved in the level:
csharp
public static Transform addBuildable(Vector3 position, Quaternion rotation, ushort id)
{
byte x, y;
if (!Regions.tryGetCoordinate(position, out x, out y)) return null;
ItemStructureAsset asset = Assets.find(EAssetType.ITEM, id) as ItemStructureAsset;
LevelBuildableObject buildable = new LevelBuildableObject(position, rotation, asset);
_buildables[x, y].Add(buildable);
buildable.enable(); // Instantiate model
return buildable.transform;
}