menu_bookDocumentation

s&box Source Generator: IUpdateSubscriber, WrappedPropertySet, and How [Sync]/[Rpc.*] Work at Compile Time

calendar_today May 4, 2026 schedule ~2 min read person patrickjr verified 50

s&box Source Generator: How [CodeGenerator] Works for [Sync] and [Rpc.*]

The Sandbox.Generator Roslyn source generator transforms your C# code at compile time. Understanding this explains why [Sync] and [Rpc.] work without any runtime reflection overhead.

What the Generator Does

For every property with [CodeGenerator( CodeGeneratorFlags.WrapPropertySet, "CallbackName" )], the generator rewrites the setter to call CallbackName( new WrappedPropertySet<T> { ... } ) instead of the original setter body. The original setter body is moved into a lambda stored in the WrappedPropertySet.

For every method with [CodeGenerator( CodeGeneratorFlags.WrapMethod, "CallbackName" )], the generator rewrites the method to call CallbackName( new WrappedMethod { Resume = () => { original body } }, args... ).

IUpdateSubscriber, IFixedUpdateSubscriber, IPreRenderSubscriber

The generator also automatically adds subscriber interfaces to components that override specific methods:

This is how the scene knows which components to call each frame — it maintains HashSetEx<Component> sets for each subscriber type and only iterates components that implement the interface. Critical implication: If you override OnUpdate() in your component, the generator adds IUpdateSubscriber to your class. The scene then adds your component to updateComponents. If you do NOT override OnUpdate(), your component is never added to the update set — zero overhead.

WrappedPropertySet and WrappedPropertyGet

These structs carry the property value, getter/setter delegates, member identity hash, and attribute array to the callback:

CSHARP
// What [Sync] generates for a property setter:
// Original: public float Health { get; set; }
// Generated setter calls:
__sync_SetValue( new WrappedPropertySet<float>
{
    Value = value,
    Setter = (v) => { field = v; },
    Getter = () => Health,
    MemberIdent = <hash of "MyComponent.Health">,
    PropertyName = "Health",
    TypeName = "MyComponent",
    Attributes = __Health__Attrs
} );

Attribute Caching

The generator creates a static PropertyNameAttrs field that caches all attributes on the property as an array. This avoids repeated reflection calls when the network system needs to check SyncFlags.

Multiple Wrappers on One Property

Multiple [CodeGenerator] attributes on the same property are applied in priority order (highest priority first). This allows [Sync] and [Change] to both wrap the same property setter without conflict.

Was this helpful?