Skip to content

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 is a u64 bitmask where bit N is set if field N’s bytes differ from the previous value:

Notification typeChangedFields value
CreatedAll bits set (u64.MaxValue). All fields are “new”.
BootstrapAll bits set (u64.MaxValue). All fields are “new”.
UpdatedOnly bits for fields that actually changed.
DeletedZero (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).

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}");

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}");
}
}
Notification typePreviousEntity
CreatedAlways null. There is no previous state for a new entity.
BootstrapAlways null.
UpdatedPopulated if includePrevious is true.
DeletedAlways null.

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.