Change Tracking
Every subscriber notification includes a ChangedFields bitmask that indicates exactly which fields differ from the previous state. Optionally, you can also request the full previous entity state.
ChangedFields bitmask
Section titled “ChangedFields bitmask”ChangedFields is a u64 bitmask where bit N is set if field N’s bytes differ from the previous value:
| Notification type | ChangedFields value |
|---|---|
Created | All bits set (u64.MaxValue). All fields are “new”. |
Bootstrap | All bits set (u64.MaxValue). All fields are “new”. |
Updated | Only bits for fields that actually changed. |
Deleted | Zero (0). No field data. |
ChangedFields is always populated, even when includePrevious is false. The bitmask is cheap to compute (the server already does a byte-level comparison during the flush).
Using HasChanged
Section titled “Using HasChanged”The source generator produces a Fields class with const int values for each field ordinal. Use these with the HasChanged method for type-safe field-level change detection:
await foreach (var change in players.SubscribeAsync(bootstrap: false, ct: ct)){ if (change.HasChanged(Player.Fields.Health)) { Console.WriteLine($"Health changed for {change.Entity?.Name}"); }
if (change.HasChanged(Player.Fields.PosX) || change.HasChanged(Player.Fields.PosY)) { Console.WriteLine($"Position changed to ({change.Entity?.PosX}, {change.Entity?.PosY})"); }}You can also inspect the raw bitmask:
Console.WriteLine($"Changed fields mask: 0x{change.ChangedFields:X}");Previous entity state
Section titled “Previous entity state”Subscribe with includePrevious: true to receive the previous entity state on Updated notifications:
await foreach (var change in players.SubscribeAsync( bootstrap: false, includePrevious: true, ct: ct)){ if (change.Type == NotificationType.Updated && change.HasChanged(Player.Fields.Health) && change.PreviousEntity is { } prev) { int delta = (int)change.Entity!.Value.Health - (int)prev.Health; Console.WriteLine($"{change.Entity?.Name} health {(delta > 0 ? "+" : "")}{delta}"); }}When PreviousEntity is populated
Section titled “When PreviousEntity is populated”| Notification type | PreviousEntity |
|---|---|
Created | Always null. There is no previous state for a new entity. |
Bootstrap | Always null. |
Updated | Populated if includePrevious is true. |
Deleted | Always null. |
Performance consideration
Section titled “Performance consideration”ChangedFields is essentially free: the server computes it during the flush as part of the byte-level comparison that determines whether a version bump occurs.
PreviousEntity adds the cost of serializing the full previous entity state into the notification. If you only need to know which fields changed (not the old values), use ChangedFields alone and leave includePrevious as false.