Unity Client

Components and settings for displaying and operating shared objects in Unity

In sync and stateMachine rooms, the objects in the room are displayed as Unity GameObjects. This page explains the Unity-side settings and APIs, organized by what you want to do.

If you are migrating from Photon (PUN2), the Migration Guide from Photon has a mapping table.

If this is your first time, working through the Tutorial first makes the role of each setting easier to grasp. For how to write server-side rules, see the Room Definition Reference.

Overview

TermMeaningExample with 10 enemies
Shared objectAn object created by the room whose state all participants see identicallyThe 10 enemies
Player objectAn object a player spawns and owns. The owner modifies its valuesEach player’s character
Object IDAn ID that identifies a single entity in the room. Issued by the server10 different IDs, one per enemy
Prefab IDAn ID that represents the kind of object. Used to choose which Prefab Unity displaysThe same ID for all 10
Sync propertyA value delivered to participantsPosition, HP

In Unity, you use the following three components. The namespace is Gs2.Realtime.Sync.

ComponentWhere to put itRole
NetworkWorldOne per sceneCreates, updates, and destroys GameObjects from the received state, and sends changes in batches
NetworkEntityThe root of each synced PrefabAssociates a GameObject with a shared object. Handles reading and writing values, reflecting them in the display, and control
NetworkTransformPrefabs that sync position and rotationSends the movement of objects you can operate, and interpolates to the received position on other clients

To send operations to objects that the server updates, also add SharedObject.

Data arrives in the following order.

  1. When you join the room, all objects and values at that point (a snapshot) arrive, and NetworkWorld creates the GameObjects.
  2. After that, only changed values arrive, and the display is updated.

Connect to a Room

NetworkWorld does not connect or log in. Connect with the GS2 SDK’s RealtimeSession and pass it to NetworkWorld in the following order. For concrete code, see the connection script in the tutorial.

  1. Connect with RealtimeSession and get MyConnectionId (your connection ID).
  2. Call world.Attach(connectionId, send). send is a function that receives the data to send and pushes it onto the send queue.
  3. Pass the data received in session.OnMessageWithMetadata to world.Receive(payload, metadata.LifeTimeMilliSeconds). Register it before starting session.Dispatch() so you don’t miss the snapshot sent right after joining.
  4. When disconnected, call world.Detach(). All displayed GameObjects are destroyed.

LifeTimeMilliSeconds is the room server’s clock. When you pass it, you can use world.ServerTime (seconds), a time shared by all participants in the room, and other players’ objects move smoothly according to the time they were received. world.Receive(payload) alone also works.

For how to send state over UDP, see Send State over UDP.

send receives a data array and its valid length, and returns true if it could push the data onto the send queue. The array is reused, so copy the valid range before queuing it. If sending fails, do not discard the changes; disconnect, reconnect, and rebuild the display from the snapshot.

RelayRealtimeSession wraps messages in a different format, so it cannot be used for this purpose.

Register Prefabs

Register the Prefabs that display objects received from the server, one per Prefab ID.

  1. Create a Prefab for display and add NetworkEntity to its root. If you also sync position and rotation, add NetworkTransform as well.
  2. In the Project window, create a catalog with Create > GS2 > Realtime > Prefab Catalog.
  3. In the catalog’s Prefabs, register the Prefab ID (Id) and the Prefab for display.
  4. Specify the catalog in NetworkWorld > Prefab Catalog in the scene.

Register every Prefab ID the server might send. An error occurs if an unregistered ID arrives. The Prefab root needs a NetworkEntity.

Shared objects are created by the room. When a player creates their own object, use world.Instantiate instead of Unity’s Instantiate. GameObjects created with Unity’s Instantiate are not shared with other participants.

If you want the room to check spawns and RPCs, set an object catalog.

Sync Position and Rotation

The SDK has standard fields for position and rotation, so you can sync them without a sync schema asset. In NetworkTransform, select only the axes the server allows to sync.

Server declarationAxes you can sync in UnityStep size (default)
RoomSync.PositionPosition X / Y / Z0.01 (Unity distance units)
RoomSync.RotationRotation X / Y / Z360 / 4096 degrees
RoomSync.TransformBoth of the aboveSame as above

The NetworkTransform settings are as follows.

ItemDescription
Position Axes / Rotation AxesThe axes to sync. Selecting an axis the server does not allow causes an error. If only position is allowed, set Rotation Axes to None
TargetThe Transform to move. If empty, its own Transform
InterpolationThe interpolation speed when you are not passing the server time. If 0, it moves to the received value immediately
Teleport DistanceMoves without interpolation to positions farther than this distance (for respawns, etc.). If 0, it always interpolates
Interpolation DelayWhen passing the server time, how many seconds in the past the displayed pose is (default 0.1). Interpolates between the updates received before and after

Position is handled in world coordinates, and rotation as Euler angles. When all three rotation axes are synced, rotation is interpolated as a Quaternion. Axes you don’t select keep each client’s local value. Scale is not synced. When updates stop, the object stops at the last received pose; it does not predict future positions.

For objects you can operate, move the Transform as usual in your movement script, and NetworkTransform rounds the values to the step size and sends them. Jitter smaller than the step size is not sent.

Display Custom Values

For custom values such as HP or an open/closed state, declare them as RoomSchema on the server side and use the generated sync schema (.realtimeschema) in Unity. For how to write the declaration, see Sync Schema.

  1. Put the generated .realtimeschema under Assets. It is imported as a sync schema asset.
  2. Specify that asset in the Prefab’s NetworkEntity > Schema.
  3. Add an entry with NetworkEntity > Property bindings > Add binding, and select the value with Field.
  4. Register the target in Apply to, and select a Dynamic event that takes the received value as its argument.

The event argument type corresponds to the sync schema type (int / bool / float). For example, you can connect a bool to a GameObject’s SetActive(bool), or a float to a Slider’s value. The event is called right after joining and every time the value changes.

From scripts, you can read and write values with the following NetworkEntity methods.

MethodDescription
GetInt / GetBool / GetFloat(name)Read the received value
SetInt / SetBool / SetFloat(name, value)Send a value. Requires write permission for that object

To write sending and receiving together in a script, put a component that implements INetworkObservable on the Prefab. It corresponds to Photon’s OnPhotonSerializeView.

public sealed class Health : MonoBehaviour, INetworkObservable
{
    public float Value;
    // Called on the owner, just before changes are sent in a batch
    public void OnNetworkWrite(NetworkEntity entity) => entity.SetFloat("Health", Value);
    // Called on other clients when a value arrives
    public void OnNetworkRead(NetworkEntity entity, uint changed)
    {
        if (entity.HasChanged("Health", changed))
            Value = entity.GetFloat("Health");
    }
}

Property bindings only pass received values to the display. Moving the Slider locally does not change the server’s value. Change server-managed values by sending operations to the server.

Send Values with Control

For objects the server declares as “lending control”, only the player who has obtained control (lease) can send values. Use this for carrying a box, moving a paddle, and so on.

MemberDescription
RequestControl() / ReleaseControl()Request to acquire / release control. Can also be registered to a Button’s On Click
Acquire On EnableAutomatically requests control when enabled
Auto RenewAutomatically renews control before it expires (enabled by default)
Controlled BehavioursScripts enabled only while you have control (movement and input handling). Do not register networking components
HasControl / Control ChangedWhether you can operate it now / the event fired when that changes
LastResult / Command CompletedThe result of an acquire or release request

Acquire requests are processed asynchronously, so you cannot necessarily operate the object right after requesting. When another player has control, the request is rejected with LeaseBusy. A rejected request is not retried automatically, so call RequestControl() again if needed.

When the GameObject is disabled, control is released automatically.

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” with SharedObject.Interact(method). The request reaches the rules’ Interact, and the rules validate it and update the values.

var target = GetComponent<SharedObject>();
var request = target.Interact(1); // 0 if it could not be sent

method is a number the rules use to tell kinds of operations apart. You cannot send additional arguments.

Receive the result of the request with NetworkWorld.Client.CommandCompleted. The arguments are the object ID, the request ID returned by Interact, and the result.

world.Client.CommandCompleted += (objectId, request, result) =>
{
    Debug.Log($"Request {request} for #{objectId}: {result}");
};
ResultValueMeaning
Success0Accepted, and the change was delivered to everyone
MissingObject1The target object does not exist
Forbidden2The operation is not allowed for this object
LeaseBusy3Another player has control
StaleEpoch4An old request from before the control state changed
InvalidOperation5An operation not accepted in the current state, or a rules execution error
AlreadyApplied6The rules reported it as already applied
StaleRequest7The same request ID has already been processed
Rejection reason from the rulesAny value of 1 or moreThe 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 results above, use values of 1000 or more for rejection reasons in your rules.

Create Player Objects

Create objects owned by a player, such as the character the player controls, with world.Instantiate. It corresponds to Photon’s PhotonNetwork.Instantiate.

var entity = world.Instantiate(prefabId, position, rotation);
entity.Rpc(nameof(Weapon.Equip), RpcTarget.AllBuffered, 3); // Usable right after creation
  • You can use the returned NetworkEntity immediately as your own (IsMine). Movement and RPCs are held until the room confirms the spawn and are sent after confirmation. Positions you move to before confirmation are kept. IsSpawned becomes true once confirmed.
  • Position and rotation are set on the axes synced by the Prefab’s NetworkTransform.
  • If the room rejects the spawn (the ownership limit is exceeded, or the Prefab is not registered in a room that has an object catalog), world.SpawnFailed is called and the instance is destroyed.
  • To wait for spawn confirmation before creating the GameObject, use world.Spawn(prefabId, position, rotation, entity => ...).
MemberDescription
IsMineWhether you can modify it (the owner of a player object, or the holder of lease control)
OwnerIdThe owner’s connection ID. For shared objects, the holder of control (0 if none)
Destroy()Destroys your own player object
TransferOwnership(connectionId)Transfers ownership to another participant (Photon’s TransferOwnership)
RequestOwnership()Takes ownership (Photon’s RequestOwnership). In a room with an object catalog, only for objects with "ownership": "takeover"
OwnershipChangedThe event fired when the owner changes

When the owner leaves, their player objects are destroyed. Objects with "keepOnLeave": true in the object catalog are handed over to the master client.

RPC

Add [NetworkRpc] to a method of a Prefab component to make it callable from other clients. It corresponds to Photon’s [PunRPC] and photonView.RPC.

public sealed class Weapon : MonoBehaviour
{
    [NetworkRpc]
    void Fire(Vector3 direction, int damage, NetworkRpcInfo info)
    {
        // info.Sender is the connection ID of the calling player (set by the server)
    }
}

entity.Rpc(nameof(Weapon.Fire), RpcTarget.Others, direction, 10);
entity.RpcTo(nameof(Weapon.Fire), connectionId, direction, 10); // Send to one player only
TargetWho receives it
RpcTarget.AllEveryone, including the calling player
RpcTarget.OthersEveryone except the calling player
RpcTarget.OwnerThe object’s owner
RpcTarget.AllBuffered / OthersBufferedSame as above, plus players who join later. Discarded when the object is destroyed or the calling player leaves
RpcTo(method, connectionId, ...)The one specified player
  • The RPC ID is determined from the method name (you can also fix it with [NetworkRpc(id)]). The NetworkEntity Inspector shows the IDs of the RPCs on the Prefab.
  • 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 in publicMethods ([NetworkRpc(AnyCaller = true)] puts them there when the catalog is generated).
  • Supported argument types are bool, integer types, char, float, double, string, byte[], enum, Vector2/3/4, Vector2Int/3Int, Quaternion, and Color, up to 4096 bytes in total. If the last argument is NetworkRpcInfo, you receive the caller. If the method returns IEnumerator, it runs as a coroutine.
  • 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. They correspond to Photon’s RaiseEvent and OnEvent.

public sealed class Scoreboard : MonoBehaviour
{
    void OnEnable() => world.AddEventTarget(this);
    void OnDisable() => world.RemoveEventTarget(this);

    [NetworkEvent]
    void Goal(int team, NetworkRpcInfo info) { /* ... */ }
}

world.RaiseEvent("Goal", RpcTarget.All, 1);
world.RaiseEventTo("Goal", connectionId, 1);

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. Arguments are encoded according to the declaration of the registered handler method. To handle raw bytes without a handler, use RaiseRawEvent and EventReceived.

Custom Properties

Values attached to the room or to players. They also reach players who join later. They correspond to Photon’s room and player custom properties.

world.SetRoomProperty("stage", "forest");
world.SetPlayerProperty("team", 2);   // You can write only your own properties
world.RoomPropertyChanged += key => { /* ... */ };
if (world.TryGetPlayerProperty<int>(connectionId, "team", out var team)) { /* ... */ }
  • Supported value types are the same as RPC arguments (enums are stored as integers). Writing null deletes 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.
  • 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.

MemberDescription
world.MasterClientId / IsMasterClientThe master’s connection ID / whether you are the master
world.SetMasterClient(connectionId)Changes the master. Only the master can call it
world.MasterClientChangedThe event fired when the master changes

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. It corresponds to Photon’s PhotonView.Group and SetInterestGroups.

entity.SetGroup(5);          // The owner sets the object's group (0 reaches everyone)
world.SubscribeGroup(5);     // Receive updates for group 5
world.UnsubscribeGroup(5);
  • 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.

Control Objects Moved by Ticks

When the room rules use DefineTickRate and DefineSimulatedPrefab, the server moves objects of that kind from inputs (ticks). The client sends inputs instead of writing positions.

// Spawn your character (a simulated prefab)
var me = world.Instantiate(5, Vector3.zero, Quaternion.identity);

// Set the current input every frame
void Update()
{
    world.SetInput(0, Mathf.RoundToInt(Input.GetAxisRaw("Horizontal")));
    world.SetInput(1, Mathf.RoundToInt(Input.GetAxisRaw("Vertical")));
    world.SetInput(2, Input.GetButton("Fire1"));
    // Aim direction for Room.Raycast (x 1000)
    world.SetInput(3, Mathf.RoundToInt(aim.x * 1000));
    world.SetInput(4, Mathf.RoundToInt(aim.y * 1000));
}
  • The input is the “current state” of 32 integer slots. Set it with SetInput(slot, int), SetInput(slot, bool) or SetInput(slot, float, precision); it is sent every room tick. To set it per tick, use the InputSampling event.
  • 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. Where UDP is unavailable (WebGL, for example), inputs go over WebSocket (UDP).
  • A working sample is in the SDK at Assets/Scenes/Samples/Realtime/Arena (predicted movement, lag-compensated shots and predicted rocket spawns).
  • NetworkWorld advances the tick number attached to inputs (InputTick) slightly ahead of the room, so that inputs arrive InputLeadTicks (2 ticks by default) before the room processes them. It adjusts its pace automatically within ±10% from the Clock the room sends.
  • Objects show the room’s results. IsMine is true for the owner, but it can neither write values nor Destroy them; they are removed by the rules’ Despawn or when the owner leaves.

Predict Your Own Character

When the rules have code marked with [Predict] (prediction), the compiler emits <RuleClass>Prediction.cs. Add it to your Unity project and register it.

TickRoomPrediction.Register(world);  // for a rule class named TickRoom; before or after Attach
  • Once registered, 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. When a prediction was off, NetworkTransform closes the gap over about CorrectionTime (0.1 seconds by default) so that it is hard to notice.
  • NetworkObject.Values keeps the values received from the room. Read predicted values from PredictedValues. Objects with IsPredicted set to true are predicted.
  • Other players’ characters are shown by interpolating the room’s results, as before.
  • Objects spawned in a prediction (predicted spawns) are announced by NetworkWorld.PredictedSpawned. When the room’s object arrives, the same instance gets its NetworkEntity.Object. Objects despawned by the prediction stop rendering until the room despawns them (NetworkObject.PredictedRemoved).
  • 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). Keep NetworkWorld.ViewDelay equal to NetworkTransform.InterpolationDelay (0.1 seconds by default). ViewDelay can also be set in the Inspector. For an accurate value, pass the server time with world.Receive(bytes, milliseconds) (connect to a room).

Send State over UDP

Sending periodic updates such as positions over UDP reduces latency when the connection is congested. It corresponds to Photon’s unreliable sending. Things that must arrive, such as spawns, RPCs, events, and properties, stay on WebSocket.

var udp = new RealtimeUdpChannel(session);  // Create after connecting
// Every frame
udp.Poll((bytes, milliseconds) => world.Receive(bytes, milliseconds));
world.Unreliable = udp.Active ? udp.TrySend : (TrySend)null;
  • Poll registers with the room and keeps the registration alive. When the room responds, Active becomes true; after 10 seconds without a response, it returns to false. While it is false, data is sent over WebSocket.
  • Values sent over UDP are resent every second in case of loss, so even if a value is lost, it becomes correct within about one second at most.
  • Values sent right before an RPC or similar are sent over WebSocket, so they arrive before the RPC.
  • UDP is not available on WebGL. It works 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
  }
}
ItemDescription
fieldsA 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
methodsIDs of RPCs the owner can call (the example above assumes IDs fixed like [NetworkRpc(1)]. Check IDs derived from names in the Inspector or with Copy Object Catalog JSON)
publicMethodsIDs of RPCs any participant can call on other players’ objects
ownership"fixed" (default) or "takeover" (anyone can take ownership)
keepOnLeaveIf 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 it with Copy Object Catalog JSON in the catalog asset’s right-click menu. When you use shared objects with stateMachine, register them together with the Prefabs the rules define.

Object Lifetime and Reconnection

TargetOn disconnectOn reconnect
Shared objects created by the serverRemain in the roomRedisplayed from the snapshot with their current values
Your player objectsDestroyed (handed over to the master if keepOnLeave)Recreate them if needed
Your player properties, Buffered RPCs and eventsDeletedSet them again if needed
Room propertiesRemain in the roomArrive in the snapshot
Control you heldReleasedAcquire it again if needed
Your connection IDBecomes invalidA new ID is issued

On every reconnection, rebuild the display from the snapshot you receive. The client does not need to replay rules or operation history.

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.

NetworkWorld 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. NetworkWorld sends changes in batches at the Send Rate interval (default 20 times per second; you can also change it with world.SendRate). 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

SymptomWhat to check
Objects are not displayedWhether you received the snapshot after connecting. Whether the arriving Prefab IDs are registered in the catalog
You get an Unknown prefab ID errorWhether the Prefab ID in the server rules matches the catalog’s Id
Select a field from the schema is displayedWhether you specified the generated sync schema in Schema for the Prefab that uses custom values
You get a field error even though you sync position onlyWhether NetworkTransform’s Rotation Axes is None
Nothing is sent when you moveWhether you have acquired control. Whether the movement script is registered in Controlled Behaviours
Value magnitudes or positions are offWhether the rules (GSL) and the sync schema were generated together from the same C# definition
Values don’t change with Inspector eventsWhether you selected a Dynamic event instead of one that passes a fixed value
Operations such as attacks are not reflectedCheck the result in CommandCompleted. See the list of results
RPCs don’t arriveIf 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
Objects created with world.Instantiate disappearCheck the result in SpawnFailed. Whether you exceeded the ownership limit (64). If you set an object catalog, whether the Prefab is registered with "mode": "player"
UDP does not become ActiveWhether the room’s port is open for UDP. UDP is not available on WebGL

Distribute the rules (GSL) and the sync schema as a pair generated together from the same C# definition.