Skip to main content
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

AttributeEvent nameWhen it fires
[EditorEvent.Frame]tool.frameEvery editor frame
[EditorEvent.Hotload]hotloadedAfter a hot reload completes
[Event.Streamer.JoinChat]stream.joinWhen a viewer joins the streamer chat
[Event.Streamer.LeaveChat]stream.leaveWhen a viewer leaves the streamer chat
[Event.Streamer.ChatMessage]stream.messageWhen 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

1

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" ) { }
    }
}
2

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}" );
    }
}
3

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:
StageWhen it runs
Stage.StartUpdateStart of each frame update
Stage.FinishUpdateEnd of each frame update
Stage.StartFixedUpdateStart of each fixed update
Stage.PhysicsStepDuring the physics step (fixed update)
Stage.FinishFixedUpdateEnd of each fixed update
Stage.UpdateBonesAfter bone transforms are computed
Stage.InterpolationWhen transforms are interpolated
Stage.SceneLoadedOnce, 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() { }
}

Build docs developers (and LLMs) love