Skip to content

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 structures

LevelObject Fields

FieldTypePurpose
transformTransformThe instantiated game object
assetObjectAssetAsset definition (mesh, materials, interactable)
placementOriginELevelObjectPlacementOriginHow the object was placed
instanceIDuintUnique instance ID for reverse lookup
materialPaletteOverrideAssetReference<MaterialPaletteAsset>Per-instance material swap
materialOverrideIndexintWhich palette entry to use
ownedCullingVolumeCullingVolumePer-instance visibility gate
skyboxTransformDistant LOD copy for skybox rendering

ELevelObjectPlacementOrigin

OriginMeaning
MANUALHand-placed in editor
PAINTPaint brush tool placement
AUTOMATICProcedural generation
BUILDABLEConverted from player structure
MIGRATIONConverted 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:

  1. Each frame, one region is activated/deactivated based on player distance.
  2. Objects in activating regions call SetIsActiveInRegion(true) — model shown, skybox hidden.
  3. 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

ImplementationStored Dataundo() Behavior
ReunObjectAddPosition, rotation, scale, asset GUIDsRemoves the object from _objects and destroys transform
ReunObjectRemoveTransform, asset GUIDs, region coordinatesRe-adds object at original position with original asset
ReunObjectTransformFrom/to position, rotation, scaleMoves 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;
}

Document history