The event system lets components and systems subscribe to named events that the engine fires during gameplay. Subscriptions are declared with C# attributes — the engine discovers them at assembly registration time and dispatches calls automatically.
There are two complementary patterns: attribute-based events (named string events dispatched via Event.Run) and interface-based events (scene lifecycle events declared as interfaces such as ISceneLoadingEvents).
Attribute-based events
Subscribing to a built-in event
Decorate any method with an [EventAttribute] subclass to subscribe to a named event. The engine calls all matching methods when that event fires.
using Sandbox;
public class MySystem : GameObjectSystem
{
public MySystem( Scene scene ) : base( scene ) { }
// Called every frame during tool/editor context
[EditorEvent.Frame]
void OnFrame()
{
// runs each editor frame
}
}
Components that inherit from Component are registered and unregistered with the event system automatically when they are enabled and disabled. You do not need to call Event.Register manually for components.
Controlling dispatch order
The Priority property on any EventAttribute controls the order listeners are called. Lower numbers run first. The default is 0.
[Event( "my.game.tick", Priority = -1 )]
void RunBeforeEverythingElse()
{
// executes before all listeners at priority 0 or higher
}
Built-in named events
| Attribute | Event name | When it fires |
|---|
[EditorEvent.Frame] | tool.frame | Every editor frame |
[EditorEvent.Hotload] | hotloaded | After a hot reload completes |
[Event.Streamer.JoinChat] | stream.join | When a viewer joins the streamer chat |
[Event.Streamer.LeaveChat] | stream.leave | When a viewer leaves the streamer chat |
[Event.Streamer.ChatMessage] | stream.message | When a chat message arrives |
Interface-based scene events
For scene lifecycle events, implement one of the built-in interfaces. The engine discovers registered objects that implement these interfaces and calls the relevant methods at the right point in the frame or load cycle.
Scene loading
using Sandbox;
public class GameManager : GameObjectSystem, ISceneLoadingEvents
{
public GameManager( Scene scene ) : base( scene ) { }
// Called before loading starts
void ISceneLoadingEvents.BeforeLoad( Scene scene, SceneLoadOptions options )
{
Log.Info( "About to load scene" );
}
// Async hook — the engine waits for this task before continuing
async Task ISceneLoadingEvents.OnLoad( Scene scene, SceneLoadOptions options )
{
await LoadAssetsAsync();
}
// Called after all loading is complete
void ISceneLoadingEvents.AfterLoad( Scene scene )
{
Log.Info( "Scene ready" );
}
}
Scene startup
ISceneStartup is intended for GameObjectSystem types, because components haven’t been spawned yet when these methods fire.
public class ServerBootstrap : GameObjectSystem, ISceneStartup
{
public ServerBootstrap( Scene scene ) : base( scene ) { }
// Host only — called before the scene file is loaded
void ISceneStartup.OnHostPreInitialize( SceneFile scene ) { }
// Host only — called after the scene is loaded
void ISceneStartup.OnHostInitialize() { }
// Called on clients after they receive the initial scene from the server
// Not called on the dedicated server
void ISceneStartup.OnClientInitialize() { }
}
Physics events
public class PhysicsListener : GameObjectSystem, IScenePhysicsEvents
{
public PhysicsListener( Scene scene ) : base( scene ) { }
void IScenePhysicsEvents.PrePhysicsStep() { /* before physics runs */ }
void IScenePhysicsEvents.PostPhysicsStep() { /* after physics runs */ }
void IScenePhysicsEvents.OnOutOfBounds( Rigidbody body )
{
body.GameObject.Destroy();
}
void IScenePhysicsEvents.OnFellAsleep( Rigidbody body )
{
Log.Info( $"{body.GameObject.Name} fell asleep" );
}
}
Collision events
Implement ISceneCollisionEvents on a component to receive per-frame collision callbacks.
public class DamageOnHit : Component, ISceneCollisionEvents
{
[Property] public float Damage { get; set; } = 10f;
void ISceneCollisionEvents.OnCollisionStart( Collision collision )
{
if ( collision.Other.GameObject.Components.TryGet<Health>( out var health ) )
health.TakeDamage( Damage );
}
void ISceneCollisionEvents.OnCollisionStop( CollisionStop collision ) { }
void ISceneCollisionEvents.OnCollisionUpdate( Collision collision ) { }
void ISceneCollisionEvents.OnCollisionHit( Collision collision ) { }
}
Dispatching a custom event
Define an attribute
Create an EventAttribute subclass that encodes your event’s string name.namespace MyGame;
public static class GameEvent
{
/// <summary>Fired when a player scores a point.</summary>
public class PlayerScoredAttribute : EventAttribute
{
public PlayerScoredAttribute() : base( "mygame.player.scored" ) { }
}
}
Subscribe to it
Decorate a method in any registered class with your attribute.public class ScoreDisplay : Component
{
[GameEvent.PlayerScored]
void OnPlayerScored( string playerName, int newScore )
{
Log.Info( $"{playerName} scored! Total: {newScore}" );
}
}
Fire the event
Call Event.Run with the event name and any arguments from wherever the event originates.// No arguments
Event.Run( "mygame.player.scored" );
// With arguments — listeners must accept matching parameter types
Event.Run( "mygame.player.scored", playerName, newScore );
Argument count and types must match exactly between the Event.Run call and every subscriber method. A mismatch throws an ArgumentException at runtime.
GameObjectSystem
GameObjectSystem is the base class for per-scene singleton systems — global managers that live for the lifetime of the scene. Unlike components, systems are instantiated automatically when the scene is created and disposed when it ends. Use them for systems that should always exist regardless of which GameObjects are present.
using Sandbox;
// This system is automatically created for every scene
public class ScoreManager : GameObjectSystem<ScoreManager>
{
public int Score { get; private set; }
public ScoreManager( Scene scene ) : base( scene )
{
// Hook into a scene tick stage
Listen( Stage.StartUpdate, 0, Tick, "ScoreManager Tick" );
}
void Tick()
{
// Runs every frame at StartUpdate stage
}
public void AddScore( int amount )
{
Score += amount;
Log.Info( $"Score: {Score}" );
}
}
Access a system from anywhere using the static Current property (available on GameObjectSystem<T> subclasses):
ScoreManager.Current.AddScore( 10 );
// or:
var mgr = Scene.GetSystem<ScoreManager>();
Tick stages
Use Listen( Stage, int order, Action, string ) inside the constructor to hook into the scene update loop:
| Stage | When it runs |
|---|
Stage.StartUpdate | Start of each frame update |
Stage.FinishUpdate | End of each frame update |
Stage.StartFixedUpdate | Start of each fixed update |
Stage.PhysicsStep | During the physics step (fixed update) |
Stage.FinishFixedUpdate | End of each fixed update |
Stage.UpdateBones | After bone transforms are computed |
Stage.Interpolation | When transforms are interpolated |
Stage.SceneLoaded | Once, after the scene finishes loading |
The order parameter controls priority within a stage — lower numbers run first (default is 0).
Manually registering objects
Components are registered automatically. For plain C# objects that need to receive events, call Event.Register and Event.Unregister yourself.
public class MyPlainClass
{
public MyPlainClass()
{
Event.Register( this );
}
~MyPlainClass()
{
Event.Unregister( this );
}
[EditorEvent.Frame]
void Tick() { }
}