Understanding the Component System
Learn how scenes, GameObjects, and Components work in s&box — properties, lifecycle hooks, and execution order.
s&box no longer uses the old Source-style Entity model — the engine moved to a scene system inspired by Unity and Godot, where everything is a GameObject with a Transform, and behaviour comes from Components attached to those GameObjects. Your gameplay code is almost always a class deriving from Sandbox.Component, with [Property] fields exposed to the editor and lifecycle methods you override. Scenes are JSON files on disk; GameObjects can be nested into hierarchies; Components are the reusable building blocks of your game.
GameObject: the world object
A GameObject has a Transform (position, rotation, scale), Tags (used for collision filtering, camera include/exclude, and arbitrary game logic), Children (a list of nested GameObjects), and a list of Components. Children inherit their parent's transform, so moving the parent moves them with it. You create one with `var go = new GameObject();`, find one with `Scene.Directory.FindByName("Cube").First()`, and destroy it with `go.Destroy()`. World position is exposed as `go.WorldPosition` and parent-relative position as `go.LocalPosition`.
var enemy = new GameObject();
enemy.Name = "Enemy";
enemy.WorldPosition = new Vector3( 100, 0, 50 );
enemy.Tags.Add( "enemy" );
var renderer = enemy.AddComponent<ModelRenderer>();
renderer.Model = Model.Load( "models/dev/box.vmdl" );
renderer.Tint = Color.Red;Component: where your code lives
A Component is a C# class deriving from Sandbox.Component that you attach to a GameObject. You add one in the editor by selecting the GameObject and clicking 'Add Component', or in code with go.AddComponent<T>(). Components access their owner via the GameObject property and the scene via Scene. You can query siblings with GetComponent<T>(), and ancestors/descendants with GetComponentInParent<T>() / GetComponentInChildren<T>(). Removing a component is component.Destroy().
[Property] exposes fields to the editor
Mark a property with [Property] and it appears in the Inspector for designers to tweak, and is serialized into the scene JSON. [RequireComponent] will auto-create a referenced sibling component if missing. Other useful attributes include [Range(min, max)] for sliders, [TextArea] for multi-line strings, and [Sync] (covered in the networking guide) for replicated state. Properties are also visible in prefab overrides, which makes them the primary knob designers use.
using Sandbox;
public sealed class Health : Component
{
[Property, Range( 1, 1000 )] public float Max { get; set; } = 100f;
[Property] public float Current { get; set; } = 100f;
[RequireComponent] public ModelRenderer Renderer { get; set; }
public void TakeDamage( float amount )
{
Current = MathF.Max( 0, Current - amount );
if ( Current <= 0 ) GameObject.Destroy();
}
}Lifecycle: OnAwake, OnStart, OnUpdate, OnDestroy
Override the methods you need: OnAwake runs once when the component is created (after deserialization), OnStart runs the first time the component becomes enabled and is guaranteed to fire before the first OnFixedUpdate, OnUpdate runs every frame, OnFixedUpdate runs every fixed timestep (use this for player movement and physics), OnPreRender runs after animation but before drawing, OnEnabled / OnDisabled fire on toggle, and OnDestroy runs when the component or its GameObject is destroyed. There is also an async OnLoad you can override to defer scene-load completion (the loading screen waits for it).
using Sandbox;
public sealed class PatrolBot : Component
{
private Vector3 _origin;
protected override void OnAwake()
{
Log.Info( $"{GameObject.Name} awoke" );
}
protected override void OnStart()
{
_origin = WorldPosition;
}
protected override void OnFixedUpdate()
{
var offset = MathF.Sin( Time.Now ) * 100f;
WorldPosition = _origin + Vector3.Forward * offset;
}
protected override void OnDestroy()
{
Log.Info( $"{GameObject.Name} destroyed" );
}
}Don't rely on cross-object execution order
Within a single component the lifecycle is deterministic, but the order in which the same callback runs across different GameObjects is not. If you need a global tick that happens once per frame at a known time — game managers, score, round logic — derive from GameObjectSystem<T> instead of putting it on a per-object component. Systems hook into specific scene stages (Stage.StartUpdate, Stage.PhysicsStep, etc.) and run with predictable timing.