Game logic in s&box is written in C#. You attach behavior to GameObjects by creating Components — C# classes that inherit from Component. When you save a .cs file, s&box recompiles and hot-reloads your code automatically.
Components
A Component is the primary unit of game logic. Create one by inheriting from Component and overriding lifecycle methods.
public class MyComponent : Component
{
protected override void OnStart()
{
Log.Info( "Component started!" );
}
protected override void OnUpdate()
{
// Runs every frame
}
}
Add it to a GameObject in the scene editor, or in code:
var go = new GameObject();
var c = go.AddComponent<MyComponent>();
How game code is structured
Your project has a Code/ folder for runtime game code and an Editor/ folder for editor-only code. Libraries you install also live in Libraries/ and are compiled together with your game code.
The Scene object is your entry point to everything that exists at runtime. You can find GameObjects and Components through it:
// Find all active cameras in the scene
foreach ( var cam in Scene.GetAll<CameraComponent>() )
{
Log.Info( cam.GameObject.Name );
}
API whitelist
s&box restricts which .NET APIs game code can use. This prevents malicious code from running on players’ machines when they download and play community games.
When you use a blocked API, the compiler emits a SB1000 Whitelist Error. Editor code and library code are not restricted.
If you are building a standalone game, you can opt out of the whitelist — but you won’t be able to publish to the s&box platform while it’s disabled.
Many standard .NET APIs have s&box equivalents:
| Not allowed | Use instead |
|---|
Console.Log | Log.Info( message ) |
System.IO.* | The s&box Filesystem API |
If you encounter a false positive — an API that seems harmless but is blocked — report it on the issue tracker with the symbol as it appears in the error.
Security vulnerabilities in the whitelist system should be reported privately as described at facepunch.com/security. Do not report them publicly.
Common patterns cheat sheet
Debugging
| Task | Code |
|---|
| Log to console | Log.Info( $"Hello {username}" ); |
| Draw text on screen | DebugOverlay.ScreenText( new Vector2( 50, 50 ), "Hello" ); |
| Assert a condition | Assert.NotNull( obj, "Object was null!" ) |
| Task | Code |
|---|
| Get world position | var p = go.WorldPosition; |
| Set world position | go.WorldPosition = new Vector3( 10, 0, 0 ); |
| Get local position | var p = go.LocalPosition; |
GameObjects
| Task | Code |
|---|
| Find by name | Scene.Directory.FindByName( "Cube" ).First(); |
| Find by GUID | Scene.Directory.FindByGuid( guid ); |
| Create | var go = new GameObject(); |
| Destroy | go.Destroy() |
| Disable | go.Enabled = false; |
| Duplicate | var newGo = go.Clone(); |
| Add a tag | go.Tags.Add( "player" ); |
| Iterate children | foreach( var child in go.Children ) |
| Check if valid | if ( go.IsValid() ) |
Components
| Task | Code |
|---|
| Add component | var c = go.AddComponent<ModelRenderer>(); |
| Remove component | c.Destroy() |
| Disable component | c.Enabled = false; |
| Get owner GameObject | var go = c.GameObject; |
| Get a component | var c = go.GetComponent<ModelRenderer>(); |
| Get or add | var c = go.GetOrAddComponent<ModelRenderer>(); |
| Iterate all | foreach ( var c in go.Components.GetAll() ) |
| Check if valid | if ( c.IsValid() ) |
| Get all active in scene | foreach ( var c in Scene.GetAll<CameraComponent>() ) |
Code generation
s&box includes a [CodeGenerator] attribute that lets you wrap methods and properties to intercept calls. You decorate another attribute with [CodeGenerator] to specify what it wraps and which callback to invoke.
This is how the scene system implements Broadcast RPCs. You can use the same mechanism to build your own systems.
Wrapping a method
[CodeGenerator( CodeGeneratorFlags.WrapMethod | CodeGeneratorFlags.Instance, "OnRPCInvoked" )]
public class RPC : Attribute {}
public class MyObject
{
[RPC]
public void SendMessage( string message )
{
Log.Info( message );
}
internal void OnRPCInvoked( WrappedMethod m, params object[] args )
{
if ( IsServer )
{
// Send a networked message with the method name and args to all clients.
}
// Call the original method.
m.Resume();
}
}
Wrapping a property
[CodeGenerator( CodeGeneratorFlags.WrapPropertySet | CodeGeneratorFlags.Instance, "OnNetVarSet" )]
[CodeGenerator( CodeGeneratorFlags.WrapPropertyGet | CodeGeneratorFlags.Instance, "OnNetVarGet" )]
public class NetVar : Attribute {}
public class MyObject
{
[NetVar] public string Name { get; set; }
internal T OnWrapGet<T>( WrappedPropertyGet<T> p )
{
if ( MyNetVarTable.TryGetValue( p.PropertyName, out var netValue ) )
{
return (T)netValue;
}
return p.Value;
}
internal void OnWrapSet<T>( WrappedPropertySet<T> p )
{
if ( IsServer )
{
MyNetVarTable[p.PropertyName] = p.Value;
// Send a networked message setting this property for all clients.
}
p.Setter( p.Value );
}
}
CodeGeneratorFlags reference
The flags determine what gets wrapped and whether it applies to instance members, static members, or both. You can combine multiple flags on a single attribute and stack multiple [CodeGenerator] attributes on one attribute class.
| Flag | Description |
|---|
WrapMethod | Intercept method calls |
WrapPropertyGet | Intercept property reads |
WrapPropertySet | Intercept property writes |
Instance | Apply to instance members |
Static | Apply to static members |
The callbackName you pass to [CodeGenerator] can be an instance method or a static method. Use a . in the name to reference a static method on another class, e.g. "MyStaticClass.OnWrap".