How Object Sync Works
This page explains how objects are synced in sync and stateMachine rooms, and who can modify what. Its content is the same for every game engine. For each engine’s settings and APIs, see the Unity Client and the Godot Client.
Overview
| Term | Meaning | Example with 10 enemies |
|---|---|---|
| Shared object | An object created by the room whose state all participants see identically | The 10 enemies |
| Player object | An object a player spawns and owns. The owner modifies its values | Each player’s character |
| Object ID | An ID that identifies a single entity in the room. Issued by the server | 10 different IDs, one per enemy |
| Prefab ID | An ID that represents the kind of object. Used to choose which Prefab (a scene in Godot) the client displays | The same ID for all 10 |
| Sync property (field) | A value delivered to participants | Position, HP |
The kind of object decides who can modify its values.
| Kind | Who modifies values | Example |
|---|---|---|
| Player object | The owner | Each player’s character |
| Shared object that lends control | The participant who has control (control) | A box you carry, a paddle |
| Server-managed shared object | Only the rules. Participants send operations | Enemies, doors |
| Simulated object | Only the rules. The owner sends inputs (ticks) | A character moved by inputs |
Data arrives in the following order.
- When you join the room, all objects and values at that point (a snapshot) arrive, and the client creates the instances to display.
- After that, only changed values arrive, and the display is updated.
Send Order
RPCs, spawns, events, properties and other commands may be called at any time after connecting. Those called before the snapshot is applied, or inside spawn notifications, are sent as soon as they can be, in call order. The order of commands is kept across all objects and room-level calls.
Received messages carry the room server’s clock. The client uses it to keep a time shared by all participants, and moves other players’ objects smoothly according to the time they were received.
Position and Rotation
Position and rotation have standard fields, so you can sync them without a sync schema. On the client, select only the axes the server allows to sync.
| Server declaration | Axes you can sync | Step size (default) |
|---|---|---|
RoomSync.Position | Position X / Y / Z | 0.01 (game distance units) |
RoomSync.Rotation | Rotation X / Y / Z | 360 / 4096 degrees |
RoomSync.Transform | Both of the above | Same as above |
- Position is handled in world coordinates, and rotation as Euler angles. Scale is not synced.
- Other clients show the pose from slightly in the past (0.1 seconds by default), interpolating between the updates received before and after. Positions farther than a set distance can be reached without interpolation (for respawns, etc.).
- When updates stop, the object stops at the last received pose. It does not predict future positions.
- Values are rounded to the step size before sending. Jitter smaller than the step size is not sent.
Custom Values
For custom values such as HP or an open/closed state, declare them as a sync schema in the rules and use the generated .realtimeschema on the client. For how to write the declaration, see Sync Schema.
- On the writing side, reading a value returns the value you last wrote (even before it is sent). For example, adding 1 to HP twice between sends adds 2.
- You can also read only the values received from the room (confirmed).
- Passing received values to the display (Property bindings in Unity,
property_bindingsin Godot) only changes the display. The server’s value does not change. Change server-managed values by sending operations.
Control
For objects the server declares as “lending control”, only the participant who has obtained control (lease) can send values. Use this for carrying a box, moving a paddle, and so on.
- Acquire requests are processed asynchronously, so you cannot necessarily operate the object right after requesting.
- When another participant has control, the request is rejected with
LeaseBusy. A rejected request is not retried automatically. - Control has an expiry, and the client can renew it automatically before it expires.
- You can enable movement and input handling only while you can operate the object: while you have control of a shared object, or while you own a player object or simulated object.
- A request that waits for control ends in one of the following results.
| Result | Meaning |
|---|---|
| Succeeded | You have control |
| Refused | LeaseBusy: another participant has control. Forbidden: an object whose control cannot be acquired (a server-managed object, or another player’s object) |
| Abandoned | The request was withdrawn (control released, the display instance disabled, the object removed) |
- When the display instance is disabled, control is released automatically.
- There are also APIs to acquire, renew and release control manually (
Acquire,Renew,Release). They only return the room’s answer; they neither enable the handling above nor renew automatically. - Each change of controller (acquire, release or expiry) starts a new period. Values sent in an earlier period (arriving late over UDP) are not applied, and do not cause an error.
Send Operations to the Server
Clients cannot rewrite the values of server-managed objects (such as an enemy’s HP). Instead, send a request saying “perform operation method on this object” (Interact). The request reaches the rules’ Interact (on_interact in GDScript), and the rules validate it and update the values.
method is a number the rules use to tell kinds of operations apart. When the rules name it with a [RoomAction] enum (## @room_action in GDScript), pass the generated constant. You cannot send additional arguments to a room run by rules.
An operation request (and acquiring, renewing and releasing control) completes once. The completion status is one of:
| Status | Meaning |
|---|---|
| Succeeded | Accepted |
| Refused | Refused. The reason is the result code (table below) |
| NotSent | Could not be sent |
| Abandoned | No answer will come (disconnected, or rejoined the room on reconnection) |
An interaction that removes its object (picking up an item) still completes as Succeeded: the room removes the object and then answers.
| Result code | Value | Meaning |
|---|---|---|
Success | 0 | Accepted, and the change was delivered to everyone |
MissingObject | 1 | The target object does not exist |
Forbidden | 2 | The operation is not allowed for this object |
LeaseBusy | 3 | Another participant has control |
StaleEpoch | 4 | An old request from before the control state changed |
InvalidOperation | 5 | An operation not accepted in the current state, or a rules execution error |
AlreadyApplied | 6 | The rules reported it as already applied. The client treats it as a success |
StaleRequest | 7 | The same request ID has already been processed |
| Rejection reason from the rules | 100 or more | The value the rules returned with Step.Ignore |
When the rules reject with Step.Ignore, the numeric reason arrives as is. To distinguish it from the room’s results, use values of 100 or more for your rules’ own rejection reasons (error enum).
Player Objects
Objects owned by a player, such as the character the player controls, are created by a spawn request from the client. There are two ways to spawn.
| Method | Behavior |
|---|---|
Create immediately (world.Instantiate in Unity, world.instantiate in Godot) | Returns the instance immediately, usable as your own. Movement and RPCs are held until the room confirms the spawn and are sent after confirmation. Positions you move to before confirmation are kept. Synced values are available after confirmation |
Create after confirmation (world.Spawn / world.spawn) | Waits for the room’s confirmation, then creates the instance |
If the room rejects the spawn (the ownership limit of 64 is exceeded, or the Prefab is not registered in a room that has an object catalog), the rejection is reported, and an instance created immediately is destroyed.
- Ownership can be transferred to another participant (player objects only). Anyone can take ownership of objects with
"ownership": "takeover"in the object catalog. - When the owner leaves, their player objects are destroyed. Objects with
"keepOnLeave": truein the object catalog are handed over to the master client. - Each change of owner starts a new ownership period. Values sent in an earlier period (arriving late over UDP) are not applied, even after the same participant gets ownership back.
RPC
Calls a method of an object on other clients.
| Target | Who receives it |
|---|---|
| All | Everyone, including the calling player |
| Others | Everyone except the calling player |
| Owner | The object’s owner |
| AllBuffered / OthersBuffered | Same as above, plus players who join later. Discarded when the object is destroyed or the calling player leaves |
| One player | The participant with the specified connection ID |
- The RPC ID is determined from the method name. Renaming the method changes the ID, so update the object catalog and the client together.
- To restrict who may call which RPC, set an object catalog. With one, only the owner may call the IDs in
methods, and RPCs called on other players’ objects go inpublicMethods. - Arguments are up to 4096 bytes in total. The last argument can receive the caller’s connection ID and similar information.
- Received RPCs are called in arrival order, after receive processing finishes. You can send RPCs and modify values from inside an RPC.
- You cannot send RPCs to shared objects. Use Send Operations to the Server instead.
Room Events
Send notifications not tied to an object with room events. Any participant can send them. Targets are All / Others, their Buffered variants, or a single player. Buffered events are discarded when the sending player leaves.
Custom Properties
Values attached to the room or to players. They also reach players who join later.
- Supported value types are the same as RPC arguments. Writing
nulldeletes the property. - A write takes effect when it has gone through the room and reached everyone. The same applies on the writing player’s side, so everyone sees values in the same order.
- You can write only your own player properties. When a player leaves, that player’s properties are deleted.
- Keys are up to 64 bytes, with up to 1024 entries for the whole room.
Master Client
The room makes one of the participants the master client. The first player to join becomes the master, and when the master leaves, the role moves to the earliest-joined remaining player. The master can appoint another participant as the master.
If you want the server to decide game progression, use stateMachine rules instead of the master client so that modified clients cannot affect it.
Interest Group
Divide objects into groups and deliver updates only to players subscribed to them. The owner sets the group, and group 0 reaches everyone.
- Groups filter value updates and All / Others RPCs to that object. Spawns, destroys, and changes of owner or group reach everyone.
- When you subscribe, the current values of the group’s objects arrive immediately.
- Subscriptions carry over across reconnections. Up to 256 groups per connection.
Ticks and Prediction
When the room rules define a tick and simulated objects, the server moves objects of that kind from inputs (ticks). The client sends inputs instead of writing positions.
- The input is the “current state” of 32 integer slots. It is sent every room tick. The room reads slots with a range clamped to it.
- Inputs are sent only while you own a simulated object. Each send carries the inputs of the last 3 ticks, so a lost UDP datagram is covered.
- The client advances the tick number attached to inputs ahead of the room, so that inputs arrive slightly (2 ticks by default) before the room processes them. It adjusts its pace automatically within ±10% from the clock the room sends.
- Even the owner can neither write values of a simulated object nor destroy it. It is removed by the rules’
Despawnor when the owner leaves.
When the rules have prediction code (prediction), the compiler emits prediction code for the client. Once registered, it works as follows.
- Simulated objects you own show the result of each input tick immediately, without waiting for a round trip.
- Each time the room’s results arrive, the client recomputes from them up to the current tick (rollback). When a prediction was off, the gap is closed over about 0.1 seconds so that it is hard to notice.
- Other players’ characters are shown by interpolating the room’s results.
- Objects spawned in a prediction (predicted spawns) are shown first, and the same instance takes over when the room’s object arrives. Objects despawned by the prediction stop rendering until the room despawns them.
- Each input records how far behind other objects were shown on screen at that moment. The room uses it to test hits as the shooter saw them (lag compensation). For an accurate value, pass the server time to the client, and keep the view delay equal to the position interpolation delay (0.1 seconds by default).
UDP
Sending periodic updates such as positions over UDP reduces latency when the connection is congested. Things that must arrive, such as spawns, RPCs, events, and properties, stay on WebSocket.
- The client registers with the room and keeps the registration alive. When the room responds, UDP is used; after 10 seconds without a response, the client returns to WebSocket.
- Values sent over UDP, by the room and by clients alike, are resent over WebSocket every second in case of loss. Even if UDP stops getting through for a while, values become current within about one second once communication recovers.
- Before ownership is handed over or control is released, the values sent over UDP are resent over WebSocket. The new writer receives the latest state over WebSocket before it starts writing.
- State updates carry numbers in the order they were sent. Even if UDP and WebSocket deliver them out of order, an older value never overwrites a newer one. The room state used for prediction is also assembled by these numbers, not by arrival order.
- Values sent right before an RPC or similar are sent over WebSocket, so they arrive before the RPC.
- UDP is not available in web browsers (Unity WebGL, Godot web exports). They work over WebSocket only.
Object Catalog
If you want the room to check player object types, fields, RPCs and ownership requests, set the criteria as JSON in syncSetting.objectCatalog of the Realtime namespace settings. The keys are Prefab IDs.
Without it, the room checks none of these: any Prefab can be spawned as a player object with the fields declared at spawn, anyone may call any RPC, and anyone may take ownership. With it, only the registered Prefabs, fields and RPCs get through.
{
"1": {
"fields": 63,
"mode": "player",
"methods": [1],
"publicMethods": [2],
"ownership": "takeover",
"keepOnLeave": true
}
}| Item | Description |
|---|---|
fields | A bitmask of the synced fields. Position X/Y/Z are 1/2/4, rotation X/Y/Z are 8/16/32 |
mode | "player" for player objects |
methods | IDs of RPCs the owner can call |
publicMethods | IDs of RPCs any participant can call on other players’ objects |
ownership | "fixed" (default) or "takeover" (anyone can take ownership) |
keepOnLeave | If true, the object is not destroyed when the owner leaves but is handed over to the master client. Available only for Prefabs registered in the catalog |
Instead of writing it by hand, you can generate the catalog JSON from the editor (Unity, Godot). Prefabs the rules define (shared objects and simulated objects) are not included in the generated JSON. If the catalog and the rules both define the same Prefab ID, the room fails to initialize.
Object Lifetime and Reconnection
| Target | On disconnect | On reconnect |
|---|---|---|
| Shared objects created by the server | Remain in the room | Redisplayed from the snapshot with their current values |
| Your player objects | Destroyed (handed over to the master if keepOnLeave) | Recreate them if needed |
| Your player properties, Buffered RPCs and events | Deleted | Set them again if needed |
| Room properties | Remain in the room | Arrive in the snapshot |
| Control you held | Released | Acquire it again if needed |
| Your connection ID | Becomes invalid | A new ID is issued |
On every reconnection, the display is rebuilt from the snapshot you receive. The client does not need to replay rules or operation history. Requests that were waiting before the reconnection complete as Abandoned.
When you reconnect with the same userId, the old connection is treated as having left.
Relation to Room Termination Conditions
A room terminates when no participant has communicated for 1 minute. In sync and stateMachine rooms, Pings that keep the connection alive do not count as communication for this purpose.
The client sends only when values change. Keeping the UDP registration alive does not count either. So in games with scenes where everyone waits without operating, design the game so that the room continues during that time, for example by sending operations periodically.
Estimated Send Volume
Sync data does not send field names; it sends only the numbers and integer values of changed fields. The client sends changes in batches at a fixed interval (default 20 times per second). Writing the same value repeatedly, or returning to the previously sent value, does not increase the send volume.
Narrowing the position step size and synced axes to what you need reduces sends caused by small jitter.
Troubleshooting
These checks apply to every engine. For engine-specific checks, see the Unity Client and the Godot Client.
| Symptom | What to check |
|---|---|
| Objects are not displayed | Whether you received the snapshot after connecting. Whether the arriving Prefab IDs are registered in the client’s catalog |
| Value magnitudes or positions are off | Whether the rules (GSL) and the sync schema were generated together from the same rule definition. If not, the client logs a “generated from other rules” warning |
| Operations are refused | Check the rejection notice for the refused command, the target ID and the reason (Forbidden: no permission, MissingObject: no such object, LimitReached: a limit, MethodNotAllowed: an RPC the catalog does not list, and so on) |
| Operations such as attacks are not reflected | Check the request’s result. See the list of result codes |
| RPCs don’t arrive | If you set an object catalog, whether the RPC ID is in its methods (or publicMethods when called from other players). Renaming the method changes the ID |
| Spawned objects disappear | Check the spawn rejection result. Whether you exceeded the ownership limit (64). If you set an object catalog, whether the Prefab is registered with "mode": "player" |
| UDP is not used | Whether the room’s port is open for UDP. UDP is not available in web browsers |