Entity Synchronization
Altruist provides delta-based synchronization — only changed properties are sent over the network. Mark an entity with [Synchronized] and the framework handles everything automatically.
Automatic Sync with [Synchronized]
Add [Synchronized] to any world object that implements ISynchronizedEntity. The framework syncs it every tick — no manual code needed.
[Synchronized] // Sync at engine tick rate (e.g., 25Hz)
[WorldObject("player")]
public class PlayerEntity : WorldObject3D, ISynchronizedEntity
{
public string ClientId { get; set; } = "";
[Synced(0, SyncAlways: true)]
public string Name { get; set; } = "";
[Synced(1)]
public float[] Position { get; set; } = [0, 0, 0];
[Synced(2)]
public int Level { get; set; } = 1;
[Synced(3)]
public int Health { get; set; } = 100;
public PlayerEntity(Transform3D t) : base(t) { }
}
That's it. Each engine tick, the framework handles sync automatically:
Sync Frequency
Control how often the entity syncs:
[Synchronized] // Every engine tick (25Hz)
[Synchronized(10)] // 10 times per second
[Synchronized(10, SyncUnit.Hz)] // Same as above
[Synchronized(1, SyncUnit.Seconds)] // Once per second
[Synchronized(5, SyncUnit.Ticks)] // Every 5 ticks
Note:
Position data changes every tick — use [Synchronized] (full tick rate). Stats like health or level change rarely — use [Synchronized(1, SyncUnit.Seconds)] to reduce bandwidth.
The [Synced] Attribute
Mark individual properties to include in sync:
[Synced(0)] // Sync when changed
[Synced(1, SyncAlways: true)] // Always include (even if unchanged)
[Synced(2, oneTime: true)] // Sync once then stop
[Synced(3, syncFrequency: 5)] // Sync every 5th tick at most
| Parameter | Description |
|---|---|
bitIndex | Unique index within the class (incremental) |
SyncAlways | Always included when any other property changes |
oneTime | Synced once (e.g., entity name on first appearance) |
syncFrequency | Max once per N ticks (0 = every tick with changes) |
Manual Sync
You can sync any ISynchronizedEntity manually without [Synchronized]:
// Sync only changed properties
await _router.Synchronize.SendAsync(someEntity);
// Force send ALL properties (useful on login or after teleport)
await _router.Synchronize.SendAsync(someEntity, forceAllAsChanged: true);
Use manual sync for:
- Force-sync on login — send full state to a newly connected client
- One-off sync after a trade — ensure both players see updated inventory
- Non-player entities — sync an NPC or object that doesn't need per-tick auto-sync
Note:
[Synchronized] is opt-in convenience for automatic per-tick sync. The manual SendAsync API is always available on any ISynchronizedEntity, with or without the attribute.
What Gets Sent
The SyncPacket contains only changed properties:
{
"messageCode": 3,
"header": { "sender": "server", "receiver": "client-id" },
"message": {
"entityType": "PlayerEntity",
"data": {
"Position": [10.5, 20.3, 0],
"Health": 95
}
}
}
Properties that haven't changed since the last sync are omitted. If nothing changed, no packet is sent.
Inheritance
Synced properties in base classes are automatically included. The bitIndex in derived classes is offset from the base:
public class BaseEntity : ISynchronizedEntity
{
public string ClientId { get; set; } = "";
[Synced(0, SyncAlways: true)]
public string Id { get; set; } = "";
[Synced(1)]
public float[] Position { get; set; } = [0, 0];
}
[Synchronized]
public class PlayerEntity : BaseEntity
{
// bitIndex 0 here becomes global index 2 (after base's 0, 1)
[Synced(0)]
public int Health { get; set; }
[Synced(1)]
public string Name { get; set; } = "";
}
Client Type
[MessagePackObject]
public class SyncPacket
{
[Key(0)][JsonPropertyName("messageCode")] public uint MessageCode { get; set; } // 3
[Key(1)][JsonPropertyName("entityType")] public string EntityType { get; set; } = "";
[Key(2)][JsonPropertyName("data")] public Dictionary<string, object?> Data { get; set; } = new();
}