> For the complete documentation index, see [llms.txt](/llms.txt)


# 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 {#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 Unity displays | The same ID for all 10 |
| Sync property | A value delivered to participants | Position, HP |

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

| Component | Where to put it | Role |
| --- | --- | --- |
| `NetworkWorld` | One per scene | Creates, updates, and destroys GameObjects from the received state, and sends changes in batches |
| `NetworkEntity` | The root of each synced Prefab | Associates a GameObject with a shared object. Handles reading and writing values, reflecting them in the display, and control |
| `NetworkTransform` | Prefabs that sync position and rotation | Sends 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 {#connection}

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](#connect).

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](#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 {#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`](#player-objects) 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](#catalog).

## Sync Position and Rotation {#transform}

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 declaration | Axes you can sync in Unity | Step size (default) |
| --- | --- | --- |
| `RoomSync.Position` | Position X / Y / Z | 0.01 (Unity distance units) |
| `RoomSync.Rotation` | Rotation X / Y / Z | 360 / 4096 degrees |
| `RoomSync.Transform` | Both of the above | Same as above |

The NetworkTransform settings are as follows.

| Item | Description |
| --- | --- |
| **Position Axes** / **Rotation Axes** | The axes to sync. Selecting an axis the server does not allow causes an error. If only position is allowed, set Rotation Axes to `None` |
| **Target** | The Transform to move. If empty, its own Transform |
| **Interpolation** | The interpolation speed when you are not passing the server time. If 0, it moves to the received value immediately |
| **Teleport Distance** | Moves without interpolation to positions farther than this distance (for respawns, etc.). If 0, it always interpolates |
| **Interpolation Delay** | When 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 {#bindings}

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](#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.

| Method | Description |
| --- | --- |
| `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`.

```csharp
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](#interaction).

## Send Values with Control {#authority}

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.

| Member | Description |
| --- | --- |
| `RequestControl()` / `ReleaseControl()` | Request to acquire / release control. Can also be registered to a Button's On Click |
| **Acquire On Enable** | Automatically requests control when enabled |
| **Auto Renew** | Automatically renews control before it expires (enabled by default) |
| **Controlled Behaviours** | Scripts enabled only while you have control (movement and input handling). Do not register networking components |
| `HasControl` / **Control Changed** | Whether you can operate it now / the event fired when that changes |
| `LastResult` / **Command Completed** | The 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 {#interaction}

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.

```csharp
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.

```csharp
world.Client.CommandCompleted += (objectId, request, result) =>
{
    Debug.Log($"Request {request} for #{objectId}: {result}");
};
```

| Result | 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 player 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 |
| `StaleRequest` | 7 | The same request ID has already been processed |
| Rejection reason from the rules | Any value of 1 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 results above, use values of 1000 or more for rejection reasons in your rules.

## Create Player Objects {#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`.

```csharp
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 => ...)`.

| Member | Description |
| --- | --- |
| `IsMine` | Whether you can modify it (the owner of a player object, or the holder of lease control) |
| `OwnerId` | The 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"` |
| `OwnershipChanged` | The 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](#master).

## RPC {#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`.

```csharp
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
```

| Target | Who receives it |
| --- | --- |
| `RpcTarget.All` | Everyone, including the calling player |
| `RpcTarget.Others` | Everyone except the calling player |
| `RpcTarget.Owner` | The object's owner |
| `RpcTarget.AllBuffered` / `OthersBuffered` | Same 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](#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](#interaction) instead.

## Room Events {#events}

Send notifications not tied to an object with room events. They correspond to Photon's `RaiseEvent` and `OnEvent`.

```csharp
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 {#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.

```csharp
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 {#master}

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.

| Member | Description |
| --- | --- |
| `world.MasterClientId` / `IsMasterClient` | The master's connection ID / whether you are the master |
| `world.SetMasterClient(connectionId)` | Changes the master. Only the master can call it |
| `world.MasterClientChanged` | The 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 {#interest-groups}

Divide objects into groups and deliver updates only to players subscribed to them. It corresponds to Photon's `PhotonView.Group` and `SetInterestGroups`.

```csharp
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 {#tick}

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

```csharp
// 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](#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 {#predict}

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

```csharp
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](#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](#connection)).

## Send State over UDP {#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.

```csharp
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 {#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.

```json
{
  "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 (the example above assumes IDs fixed like `[NetworkRpc(1)]`. Check IDs derived from names in the Inspector or with **Copy Object Catalog JSON**) |
| `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 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 {#lifetime}

| 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, 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 {#idle}

A room [terminates when no participant has communicated for 1 minute](#room-termination-conditions). 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 {#size}

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 {#troubleshooting}

| Symptom | What to check |
| --- | --- |
| Objects are not displayed | Whether you received the snapshot after connecting. Whether the arriving Prefab IDs are registered in the catalog |
| You get an `Unknown prefab ID` error | Whether the Prefab ID in the server rules matches the catalog's **Id** |
| `Select a field from the schema` is displayed | Whether 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 only | Whether NetworkTransform's **Rotation Axes** is `None` |
| Nothing is sent when you move | Whether you have acquired control. Whether the movement script is registered in **Controlled Behaviours** |
| Value magnitudes or positions are off | Whether the rules (GSL) and the sync schema were generated together from the same C# definition |
| Values don't change with Inspector events | Whether you selected a **Dynamic** event instead of one that passes a fixed value |
| Operations such as attacks are not reflected | Check the result in `CommandCompleted`. See the [list of results](#interaction) |
| 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 |
| Objects created with `world.Instantiate` disappear | Check 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 `Active` | Whether 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.



