terminalCode Example

Sandbox: ISpawner interface — custom spawner pattern with ISpawnEvents

calendar_today May 4, 2026 schedule ~1 min read person patrickjr verified 50

ISpawner Interface — Custom Spawner Pattern

The ISpawner interface defines the contract for anything that can be spawned via the spawn menu. Implementations include PropSpawner, EntitySpawner, DuplicatorSpawner, and MountSpawner.

CSHARP
public interface ISpawner
{
    string DisplayName { get; }
    string Icon { get; }

    // Raw data to reconstruct this spawner (cloud ident, JSON, etc.)
    string Data { get; }

    BBox Bounds { get; }

    // Async loading — await this before calling Spawn
    Task<bool> Loading { get; }

    // The unspawned prefab, if available (null for props/duplicator)
    GameObject Prefab => null;

    // Draw a preview ghost at the given transform
    void DrawPreview( Transform transform, Material overrideMaterial );

    // Actually spawn at the given transform — host only
    // Returns root GameObjects so they can be added to undo
    Task<List<GameObject>> Spawn( Transform transform, Player player );

    // Optional: populate a right-click context menu
    void PopulateContextMenu( MenuPanel menu, string ident, string metadata ) { }
}

PropSpawner example

CSHARP
public class PropSpawner : ISpawner
{
    public Task<List<GameObject>> Spawn( Transform transform, Player player )
    {
        var depth = -Bounds.Mins.z;
        transform.Position += transform.Up * depth; // Place on surface

        var go = new GameObject( false, "prop" );
        go.Tags.Add( "removable" );
        go.WorldTransform = transform;

        var prop = go.AddComponent<Prop>();
        prop.Model = Model;

        Ownable.Set( go, player.Network.Owner );

        // Add fallback collider if model has no physics parts
        if ( (Model.Physics?.Parts?.Count ?? 0) == 0 )
        {
            var collider = go.AddComponent<BoxCollider>();
            collider.Scale = Model.Bounds.Size;
            collider.Center = Model.Bounds.Center;
            go.AddComponent<Rigidbody>();
        }

        go.NetworkSpawn( true, null );
        return Task.FromResult( new List<GameObject> { go } );
    }
}

ISpawnEvents — intercept spawning

CSHARP
public interface ISpawnEvents : ISceneEvent<ISpawnEvents>
{
    public class SpawnData
    {
        public ISpawner Spawner { get; init; }
        public Transform Transform { get; set; }
        public PlayerData Player { get; init; }
        public bool Cancelled { get; set; } // Set true to block the spawn
    }

    public class PostSpawnData
    {
        public ISpawner Spawner { get; init; }
        public Transform Transform { get; init; }
        public PlayerData Player { get; init; }
        public List<GameObject> Objects { get; init; }
    }

    void OnSpawn( SpawnData e ) { }
    void OnPostSpawn( PostSpawnData e ) { }
}

Usage pattern in tools

CSHARP
// Fire pre-spawn event (limits system listens here to cancel if over limit)
var spawnData = new Global.ISpawnEvents.SpawnData { Spawner = spawner, Transform = dest, Player = player.PlayerData };
Scene.RunEvent<Global.ISpawnEvents>( x => x.OnSpawn( spawnData ) );
if ( spawnData.Cancelled ) return;

var objects = await spawner.Spawn( dest, player );

// Fire post-spawn event (limits system tracks objects here)
Scene.RunEvent<Global.ISpawnEvents>( x => x.OnPostSpawn( new Global.ISpawnEvents.PostSpawnData
{
    Spawner = spawner, Transform = dest, Player = player.PlayerData, Objects = objects
} ) );

Key points

Was this helpful?