Sync
Forms that compile (21)
Section titled “Forms that compile (21)”| Syntax | Description |
|---|---|
[UdonSynced] private int value; | Syncing an integer |
[UdonSynced] private bool flag; | Syncing a boolean |
[UdonSynced] private string label; | Syncing a string |
[UdonSynced] private Vector3 point; | Syncing a struct |
[UdonSynced] private Mode mode; | Syncing an enum |
[UdonSynced] private int[] values; | Syncing an array |
[UdonSynced(UdonSyncMode.Linear)] | Sync with interpolation |
[UdonSynced(UdonSyncMode.Smooth)] | Smooth interpolation |
[UdonBehaviourSyncMode(BehaviourSyncMode.Manual)] | Manual sync |
[UdonBehaviourSyncMode(BehaviourSyncMode.Continuous)] | Continuous sync |
public override void OnDeserialization() | When a value arrives |
[FieldChangeCallback(nameof(Value))] | Per-variable notification |
Value = 1; | Assignment on the owner’s side |
public override void OnPreSerialization() | Just before sending |
public override void OnPostSerialization(SerializationResult result) | After sending |
Networking.IsOwner(gameObject) | Whether you are the owner |
Networking.SetOwner(Networking.LocalPlayer, gameObject) | Taking ownership |
public override void OnOwnershipTransferred(VRCPlayerApi player) | When ownership transfers |
public override bool OnOwnershipRequest(...) | Whether to accept the request |
[NetworkCallable] | Permission on the callee side |
SendCustomNetworkEvent(NetworkEventTarget.All, ...) | Calling with arguments |
Forms that don’t compile (6)
Section titled “Forms that don’t compile (6)”| Syntax | Description | Error | Reason | Instead |
|---|---|---|---|---|
[UdonSynced] private Pair pair; | Sync a struct you defined yourself | TUKI0103 | undecided | Sync each component separately |
[UdonSynced] private int[][] grid; | Sync an array of arrays | TUKI0103 | runtime | Use one flat array and compute the index (arrays) |
[UdonSynced] private GameObject target; | Sync a reference to an object | TUKI0103 | runtime | Wire the same reference for everyone in the inspector, or number the targets and sync the number |
[UdonSynced(UdonSyncMode.Linear)] private string label; | Specify interpolation on a type that can’t interpolate | TUKI0106 | runtime | Drop the interpolation |
[UdonSynced] public int Value { get; set; } | Attach it to a property | CS0592 | runtime | Attach it to the backing field and connect it to the property with [FieldChangeCallback] |
[UdonSynced] int v = 1; | Attach it to a local variable (inside a method) | CS7014 | runtime | Make it a field |
Only fields can carry [UdonSynced].
How sync works
Section titled “How sync works”The official documentation guarantees only two things: the first player to join becomes master,
and the owner of a PlayerObject is fixed to that player. Whether a value arrives, arrives in send order,
and arrives exactly once are all unguaranteed. The diagrams below are not guarantees; they follow the
official documentation’s description.
What ownership is
Section titled “What ownership is”Ownership is a marker set per synced object, saying “this belongs to this player right now.” The target is
a GameObject, checked by passing the GameObject to Networking.IsOwner(gameObject).
Ownership matters because only the owner can assign to a synced variable. If a non-owner player assigns to it, that value is not sent to other players.
The first player to join becomes master, and the owner of an object that has never had SetOwner()
called on it is master. Of this, only how master is decided is guaranteed — that such an object’s owner
is master is not guaranteed.
The official documentation says one object has one owner. This isn’t guaranteed, though: states with two owners and states with no owner have both been reported.
Figure 1 What happens when a value is synced (manual sync)
Section titled “Figure 1 What happens when a value is synced (manual sync)”sequenceDiagram
autonumber
participant O as Owner
participant N as Network
participant P as Receiver
O->>O: Assign to a synced variable
O->>O: Call RequestSerialization()
O->>O: OnPreSerialization() is called
O->>N: Send all of that program's synced variables together at once
O->>O: OnPostSerialization() is called
N->>P: Receive
P->>P: Assign the received values to the variables one by one
P->>P: FieldChangeCallback is called on each assignment
P->>P: OnDeserialization() is called once all assignments are done
Things to note about sync
Section titled “Things to note about sync”| Where | Note |
|---|---|
RequestSerialization() | It doesn’t send once per call. However many times you call it, only one send happens next |
| Send | It can be lost in transit. The sender is not notified if it’s lost |
OnPostSerialization() | This only tells you a send was attempted. Whether the other side received it is unknown |
| Assignment on the receiving side | The order isn’t fixed. To see a state where multiple variables are all in place, wait for OnDeserialization() |
What runs on the owner’s side
Section titled “What runs on the owner’s side”The bottom half of figure 1 is the receiver’s behaviour. It doesn’t happen on the owner.
OnDeserialization() is called when a value is received. The owner doesn’t receive the value it sent,
so it’s normally not called. But the official documentation doesn’t state that it “isn’t called on the owner,”
and cases of it being called on the owner have been reported. It can’t be used to decide whether you’re the owner.
FieldChangeCallback is a mechanism that routes a received value through the property’s setter instead of writing the field directly.
The owner doesn’t receive a value, so this path doesn’t run for it. But if the owner assigns to the property,
that assignment itself runs the setter. This is plain C# behaviour, unrelated to sync.
| How you assign | Owner | Receiver |
|---|---|---|
Value = 1 (property) | The setter runs at the point of assignment | The setter runs on receipt |
backing = 1 (field) | Doesn’t run | — |
It looks like the same code runs on both sides, but the paths differ. If the owner assigns to the field directly, the processing is skipped only on the owner’s side (Assignment on the owner’s side).
Figure 2 Manual sync and continuous sync
Section titled “Figure 2 Manual sync and continuous sync”Attaching [UdonBehaviourSyncMode(BehaviourSyncMode.Manual)] gives manual sync,
and attaching [UdonBehaviourSyncMode(BehaviourSyncMode.Continuous)] gives continuous sync.
Figure 1 is the manual sync case.
sequenceDiagram
participant O as Owner
participant N as Network
O->>O: Assign to a synced variable
alt Manual sync
O->>O: Call RequestSerialization()
O->>N: Send
O->>O: Assign to a synced variable again
Note over O: Assigning alone doesn't send it
else Continuous sync
O->>N: Send
O->>N: Send (repeats even if the value hasn't changed)
Note over O: RequestSerialization() has no effect
end
Manual sync (Manual) | Continuous sync (Continuous) | |
|---|---|---|
| When it sends | When the owner calls RequestSerialization() | Automatic. Sends repeatedly even if the value hasn’t changed |
| When assigning to a synced variable | Not sent. The value stays with the owner until RequestSerialization() is next called | Left as is, it rides along on the next send |
When calling RequestSerialization() | A send is scheduled | Nothing happens. It doesn’t add a send |
| When sends back up | Held and resent | Dropped, and an error is logged |
| Limit per send | Roughly 280,496 bytes | Roughly 200 bytes |
| What it suits | Values sent only on change, like a score or a state | Values that keep changing, like position or rotation |
Whether OnPreSerialization() and OnPostSerialization() are called under continuous sync isn’t
documented officially. Commonly they’re treated as called on every send. Continuous sync sends repeatedly,
so under that assumption they’re called many times per second. Keeping them light under that assumption works either way.
Rough figures for send volume and count
Section titled “Rough figures for send volume and count”The official documentation gives numbers for only the following 3. Even in the original text they’re rough figures marked “about” or “roughly,” and can change with VRChat updates.
| What it measures | Rough figure |
|---|---|
| Amount sendable per second | About 11 kilobytes |
| Limit per send | Roughly 280,496 bytes for manual sync, roughly 200 bytes for continuous sync |
| Times a single network event can be called | 5 per second by default. Write it as [NetworkCallable(100)] to raise it up to 100 |
What happens when you exceed the limit differs by what you’re sending. For network events, the excess isn’t discarded — it queues on the sender. It won’t disappear, but arrival is delayed. Synced variables behave as in the table above: manual sync holds and resends, continuous sync drops it.
The 200 bytes for continuous sync is a limit on the total of all synced variables placed in that program. Adding more variables brings you closer to the limit.
A single send isn’t necessarily one send on the network. Past 1024 bytes it’s split internally, so a send you intended as one call can hit the call-count limit you set.
The following figures aren’t in the official documentation.
- How many times per second continuous sync sends (secondary sources say “about 10 times,” but it isn’t stated whether that’s measured or estimated)
- The upper bound on string length (this can only be stated as “whatever fits the limit above, at 2 bytes per character”)
- The upper bound on array element count
Figure 3 Ownership transfer
Section titled “Figure 3 Ownership transfer”Ownership is held by each player individually as “who is the owner right now.” When you transfer it, the caller switches over first, and it reaches other players with a delay.
sequenceDiagram
participant R as Requester
participant O as Current owner
participant P as Other player
R->>R: Call SetOwner(self)
R->>R: OnOwnershipRequest() is called
O->>O: The same OnOwnershipRequest() is called here too
R->>R: If accepted, the owner becomes self on the spot
R->>R: OnOwnershipTransferred() is called
R-->>P: Reaches other players
P->>P: The owner as seen by other players also becomes the requester
P->>P: OnOwnershipTransferred() is called
OnOwnershipRequest() is called on both the requester and the current owner. If the return values split, a mismatch
remains, so use something that looks the same from both sides for the decision, such as a synced variable. If the current owner has already left,
it isn’t called, and ownership is assigned automatically.
Between the requester switching over and it reaching other players, it looks like there are two owners.
A PlayerObject is an object created one per player who joins. Its owner is fixed to that player
and can’t be transferred to anyone else. But the guaranteed scope is during the execution of Start and OnDeserialization;
it doesn’t mean this is always true.
Figure 4 Two players take ownership at the same time
Section titled “Figure 4 Two players take ownership at the same time”sequenceDiagram
participant X as First player
participant N as Network
participant Y as Second player
X->>X: Call SetOwner(self)
Y->>Y: Call SetOwner(self)
X->>X: The owner as seen by the first player is the first player
Y->>Y: The owner as seen by the second player is the second player
X->>N: Assign to a synced variable and send
Y->>N: Assign to a synced variable and send
N->>N: Whichever arrives first becomes the owner
N->>X: Receive the winning value
N->>Y: Receive the winning value
The losing assignment disappears the moment the winning value is received. Both succeed on their own side, so taking ownership isn’t mutual exclusion.
Other things that happen
Section titled “Other things that happen”- Ownership can change due to a collision or a pickup. Nothing is called when this happens
- When the owner leaves, ownership is assigned automatically. There’s no rule for who it’s assigned to.
OnOwnershipRequest()isn’t called, and the order relative to the leave notification isn’t fixed either - The state in figure 4 can fail to resolve. It appears to happen more in environments with lower network latency
Common misunderstandings
Section titled “Common misunderstandings”| Misunderstanding | Actually |
|---|---|
Since there’s one owner, calling SetOwner() acts as mutual exclusion | If two players call SetOwner() at the same time, both become the owner on their own side. It’s not mutual exclusion |
If OnDeserialization() was called, you’re not the owner | There are records of OnDeserialization() being called on the owner. It can’t be used to decide whether you’re the owner |
| A value assigned while not the owner reverts immediately | No error occurs. The value reverts only when the true owner next sends. If the owner never sends, that value stays as is |
| A player who joins partway through still receives past network events | Synced variable values are sent, but network events are not resent |
| Right after joining, nothing has been called yet | OnDeserialization() is called at the point of joining. If it plays sounds or animations, they all play at once the moment you join |
Synced variables
Section titled “Synced variables”A field with [UdonSynced] becomes the target of sync.
Syncing an integer
Section titled “Syncing an integer”This is the most basic form. Attach it to an int field.
using UnityEngine;using Tsukimi;
public class AttributesSynced : TsukimiBehaviour{ [UdonSynced] private int value;
void Start() { value = 1; RequestSerialization(); Debug.Log(value); // => 1 }}Syncing a boolean
Section titled “Syncing a boolean”It can also be attached to a bool. If nothing is set, it starts as false.
using UnityEngine;using Tsukimi;
public class SyncBool : TsukimiBehaviour{ [UdonSynced] private bool flag;
void Start() { Debug.Log(flag); // => false }}Syncing a string
Section titled “Syncing a string”It can also be attached to a string. The example reads back the value set by the initializer as is.
using UnityEngine;using Tsukimi;
public class SyncString : TsukimiBehaviour{ [UdonSynced] private string label = "a";
void Start() { Debug.Log(label); // => "a" }}Syncing a struct
Section titled “Syncing a struct”Structs the runtime has can be synced. A struct you defined yourself cannot (see the table below).
using UnityEngine;using Tsukimi;
public class SyncVector3 : TsukimiBehaviour{ [UdonSynced] private Vector3 point = new Vector3(1f, 2f, 3f);
void Start() { Debug.Log(point.x); // => a runtime value }}Syncing an enum
Section titled “Syncing an enum”An enum’s contents are an integer, so it can be synced as is.
using UnityEngine;using Tsukimi;
public class SyncEnum : TsukimiBehaviour{ private enum Mode { Off, On }
[UdonSynced] private Mode mode;
void Start() { Debug.Log((int)mode); // => 0 }}Syncing an array
Section titled “Syncing an array”Arrays can also be synced. But an array of arrays cannot (see the table below).
using UnityEngine;using Tsukimi;
public class ArraysFieldSynced : TsukimiBehaviour{ [UdonSynced] private int[] values = new int[4];
void Start() { values[0] = 10; RequestSerialization(); }}Sync with interpolation
Section titled “Sync with interpolation”This specifies filling in between arriving values. The types it can attach to are limited: float compiles, string doesn’t.
using UnityEngine;using Tsukimi;
public class AttributesSyncedInterpolated : TsukimiBehaviour{ [UdonSynced(UdonSyncMode.Linear)] private float value;
void Start() { Debug.Log(value); // => 0 }}using UnityEngine;using Tsukimi;
public class R_sync_interp : TsukimiBehaviour{ [UdonSynced(UdonSyncMode.Linear)] private string label = "a";
void Start() { Debug.Log(label); }}Smooth interpolation
Section titled “Smooth interpolation”Like Linear, this fills in between arriving values, but the interpolation method differs.
using UnityEngine;using Tsukimi;
public class SyncSmooth : TsukimiBehaviour{ [UdonSynced(UdonSyncMode.Smooth)] private float amount;
void Start() { Debug.Log(amount); // => 0 }}When to send
Section titled “When to send”[UdonBehaviourSyncMode] decides that program’s sync method.
Manual sync
Section titled “Manual sync”This sets manual sync. It sends when RequestSerialization() is called.
using UnityEngine;using Tsukimi;
[UdonBehaviourSyncMode(BehaviourSyncMode.Manual)]public class AttributesSyncModeManual : TsukimiBehaviour{ [UdonSynced] private int value;
void Start() { value = 1; RequestSerialization(); }}Continuous sync
Section titled “Continuous sync”This sets continuous sync. It sends even without calling RequestSerialization().
using UnityEngine;using Tsukimi;
[UdonBehaviourSyncMode(BehaviourSyncMode.Continuous)]public class AttributesSyncModeContinuous : TsukimiBehaviour{ [UdonSynced] private int value;
void Start() { value = 1; }}Sync events
Section titled “Sync events”These are methods called as part of sync. The base declares them, so write them with override.
When a value arrives
Section titled “When a value arrives”Called after a synced value arrives.
using UnityEngine;using Tsukimi;
public class SyncOnDeserialization : TsukimiBehaviour{ [UdonSynced] private int value;
public override void OnDeserialization() { Debug.Log(value); }
void Start() { Debug.Log(0); }}
// Output// 0Per-variable notification
Section titled “Per-variable notification”When a value arrives for this variable via sync, it doesn’t write the field directly — it goes through the specified property’s setter.
using UnityEngine;using Tsukimi;
public class AttributesFieldChangeCallback : TsukimiBehaviour{ [UdonSynced, FieldChangeCallback(nameof(Value))] private int backing;
public int Value { get { return backing; } set { backing = value; } }
void Start() { Value = 1; Debug.Log(Value); // => 1 }}Assignment on the owner’s side
Section titled “Assignment on the owner’s side”Assigning to the property runs the setter right away. This is plain C# behaviour, unrelated to sync. It doesn’t run when you assign to the field directly.
using UnityEngine;using Tsukimi;
public class SyncFieldChangeCallbackOnWrite : TsukimiBehaviour{ [UdonSynced, FieldChangeCallback(nameof(Value))] private int backing;
public int Value { get { return backing; } set { backing = value; Debug.Log(100 + value); // => 101 } }
void Start() { Value = 1; backing = 2; Debug.Log(backing); // => 2 }}Just before sending
Section titled “Just before sending”An entry point for assembling the value to send, right before it’s sent.
using UnityEngine;using Tsukimi;
public class SyncOnPreSerialization : TsukimiBehaviour{ [UdonSynced] private int value;
public override void OnPreSerialization() { value = 1; }
void Start() { RequestSerialization(); Debug.Log(value); // => 0 }}After sending
Section titled “After sending”Lets you receive whether the send succeeded.
using UnityEngine;using Tsukimi;using VRC.Udon.Common;
public class SyncOnPostSerialization : TsukimiBehaviour{ public override void OnPostSerialization(SerializationResult result) { Debug.Log(result.success); }
void Start() { Debug.Log(0); }}
// Output// 0Ownership
Section titled “Ownership”Forms for checking the owner or transferring ownership.
Whether you are the owner
Section titled “Whether you are the owner”Lets you check whether you’re the owner of that object.
using UnityEngine;using Tsukimi;using VRC.SDKBase;
public class SyncIsOwner : TsukimiBehaviour{ [UdonSynced] private int value;
void Start() { if (Networking.IsOwner(gameObject)) { value = 1; RequestSerialization(); } }}Taking ownership
Section titled “Taking ownership”Makes the player in the first argument the owner. The example passes self (Networking.LocalPlayer).
using UnityEngine;using Tsukimi;using VRC.SDKBase;
public class SyncSetOwner : TsukimiBehaviour{ void Start() { Networking.SetOwner(Networking.LocalPlayer, gameObject); }}When ownership transfers
Section titled “When ownership transfers”Called after ownership transfers.
using UnityEngine;using Tsukimi;using VRC.SDKBase;
public class SyncOnOwnershipTransferred : TsukimiBehaviour{ public override void OnOwnershipTransferred(VRCPlayerApi player) { Debug.Log(0); }
void Start() { Debug.Log(1); }}
// Output// 1Whether to accept the request
Section titled “Whether to accept the request”Returning true accepts the request; returning false declines it.
using UnityEngine;using Tsukimi;using VRC.SDKBase;
public class SyncOnOwnershipRequest : TsukimiBehaviour{ public override bool OnOwnershipRequest(VRCPlayerApi requester, VRCPlayerApi newOwner) { return true; }
void Start() { Debug.Log(1); // => 1 }}Network events
Section titled “Network events”A form that sends a method call rather than a variable’s value.
Permission on the callee side
Section titled “Permission on the callee side”Only a method with this marker can be called over the network.
using UnityEngine;using Tsukimi;using VRC.SDK3.UdonNetworkCalling;
public class AttributesNetworkCallable : TsukimiBehaviour{ [NetworkCallable] public void Receive(int n) { Debug.Log(n); }
void Start() { Debug.Log(1); }}
// Output// 1using UnityEngine;using Tsukimi;using VRC.Udon.Common.Interfaces;
public class R_sync_no_callable : TsukimiBehaviour{ public void Receive(int n) { Debug.Log(n); }
void Start() { SendCustomNetworkEvent(NetworkEventTarget.All, nameof(Receive), 1); }}Calling with arguments
Section titled “Calling with arguments”Values listed on the sending side land in the receiving side’s arguments. Up to 8 arguments can be written.
using UnityEngine;using Tsukimi;using VRC.Udon.Common.Interfaces;using VRC.SDK3.UdonNetworkCalling;
public class ArraysSendAsNetworkEvent : TsukimiBehaviour{ private int[] scores;
void Start() { scores = new int[] { 1, 2, 3 }; SendCustomNetworkEvent(NetworkEventTarget.All, nameof(Receive), scores); }
[NetworkCallable] public void Receive(int[] values) { Debug.Log(values.Length); }}