terminalCode Example
Sandbox: ISpawner interface — custom spawner pattern with ISpawnEvents
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
- Loading is a Task<bool> — always await it before calling Spawn or accessing Bounds
- EntitySpawner tries local/installed resources first, then falls back to cloud: Entity ??= await Cloud.Load<ScriptedEntity>( Path, true )
- The "removable" tag on spawned objects is used by the cleanup system
- Ownable.Set(go, player.Network.Owner) should always be called for player-spawned objects
Was this helpful?