Change notifications: BindOnChange
BindOnChange attaches a callback to a SyncVar, so code runs exactly when the value changes instead of comparing it every frame — with flags deciding which of the several moments a value can change actually fire it.
When to use this
- Reacting to state arriving from the server: update a label, play a reaction, swap a material.
- Reacting to changes on the entity itself regardless of side — with
ExecuteOnServeradded.
When not to
- One-shot events that carry their own data (a hit, a shout) — an RPC; a callback only tells you a value changed.
- Per-frame view updates — read
InterpolatedValuein the view; callbacks fire at the send rate, not per frame.
Minimal example
Lamp.cs
using LiteEntitySystem;
public class Lamp : EntityLogic
{
public SyncVar<bool> IsOn;
public SyncVar<byte> Brightness;
public Lamp(EntityParams entityParams) : base(entityParams) { }
protected override void RegisterRPC(ref RPCRegistrator r)
{
base.RegisterRPC(ref r);
// instance form: callback is a method of this entity
r.BindOnChange(this, ref IsOn, OnIsOnChanged);
// static form: no closure over the instance, works for virtual methods
r.BindOnChange<Lamp, byte>(ref Brightness,
static (lamp, prevBrightness) => lamp.ApplyBrightness());
}
private void OnIsOnChanged(bool wasOn)
{
// IsOn already holds the NEW value; wasOn is the previous one
ApplyBrightness();
}
private void ApplyBrightness() { /* update the view */ }
}
How it works
Where bindings are declared
Bindings live in the RegisterRPC override, next to RPC registration, and are set up once per class — like RPCs, they belong to the class, not to an instance. The override must start with base.RegisterRPC(ref r), or base-class bindings are lost.
The previous-value argument
The callback receives the value the field held before the change; the field itself already holds the new one. That asymmetry is deliberate — it lets a callback diff old against new (if (wasOn != IsOn)) without keeping a shadow copy.
Choosing timings
By default a callback fires only when a value arrives from the server (ExecuteOnSync). A value can change at other moments too, and each is opt-in:
| Flag | Fires when |
|---|---|
ExecuteOnSync |
A state update from the server changes the value (default) |
ExecuteOnServer |
Server code changes the value |
ExecuteOnPrediction |
Client code changes the value during prediction, including re-simulation |
ExecuteOnRollbackReset |
Rollback resets the value to the server's, before OnRollback |
ExecuteAlways |
All of the above |
Combine what you need: ExecuteOnSync | ExecuteOnServer is the usual choice for logic that must run on both sides — the example project's controller uses exactly this to notice possession changes.
Suppressing a notification
SetValueWithoutOnSyncNotification(value) writes a field without triggering its local callback. Use it when a callback would fight the code doing the write — for instance when applying a value the callback itself derived.
Behavior details
Only ExecuteOnServer matters here; without it a server-side write fires nothing. The callback runs synchronously inside the assignment, before the change is serialized.
Warning
Common mistakes
- Reading the callback argument as the new value — it is the previous one; the field already holds the new value.
- Expecting a client callback for a value the client itself predicted — that needs
ExecuteOnPrediction; the default fires only on server sync. - Spawning sounds or particles in an
ExecuteOnPredictioncallback — rollback replays them on every correction; gate withInNormalState. - Assuming a callback fires once per change — with sampling, several intermediate values may collapse into one; and rollback can re-fire prediction callbacks for the same tick.