Sync
Forms that compile (23)
Section titled “Forms that compile (23)”| Syntax | Description | Note |
|---|---|---|
[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 Pair pair; | Syncing a struct you declared | |
[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 | |
[UdonBehaviourSyncMode(BehaviourSyncMode.NoVariableSync)] | A mode with no synced variables | |
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 (15)
Section titled “Forms that don’t compile (15)”| Syntax | Description | Error | Reason | Alternative |
|---|---|---|---|---|
[UdonSynced] private Bag bag; (holding an array) | Syncing a struct that holds an array | TUKI0103 | not yet | Move the array out into its own field and sync each of them |
[UdonSynced] private Nothing nothing; (no fields) | Syncing a struct with nothing in it | TUKI0103 | by design | Give it the value you want to sync as a field |
[UdonSynced(UdonSyncMode.Linear)] private Tagged tagged; (holding a string) | Asking for interpolation on a struct that holds a type which cannot be interpolated | TUKI0106 | runtime | Drop the interpolation, or split out a struct holding only types that can be interpolated |
[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 |
[UdonSynced] private static int v; | Attach it to a static field | TUKI0110 | runtime | Remove static and make it an ordinary field |
[UdonSynced] private const int v = 3; | Attach it to a const field | TUKI0110 | runtime | Same as above |
[UdonSynced] private readonly int v; | Attach it to a readonly field | TUKI0110 | runtime | Remove readonly |
[UdonBehaviourSyncMode(Continuous)] and [UdonSynced] int[] | Syncing an array under the keep-sending method | TUKI0107 | runtime | Use Manual |
[UdonBehaviourSyncMode(Manual)] and [UdonSynced(Linear)] | Asking for interpolation under the send-on-call method | TUKI0107 | runtime | Use Continuous |
[UdonBehaviourSyncMode(None)] and [UdonSynced] | Holding a synced variable under the no-sync method | TUKI0107 | by design | Change the method, or remove the attribute |
[UdonBehaviourSyncMode(NoVariableSync)] with [UdonSynced] | Holding a synced variable under the mode with no synced variables | TUKI0107 | by design | Change the method, or remove the attribute |
Only fields can carry [UdonSynced].
Spellings that mean the default
Section titled “Spellings that mean the default”The following three, which turn up in ported code, compile as they are. Each means the same as not writing it at all, so leaving them in or taking them out changes nothing.
| Spelling | Meaning |
|---|---|
[UdonBehaviourSyncMode(BehaviourSyncMode.Any)] | Specifies no mode. The same as not writing it: the setting on the component is used as it is |
[UdonSynced(UdonSyncMode.None)] | Carries the value as it is. The same as writing [UdonSynced] on its own |
[UdonSynced(UdonSyncMode.NotSynced)] | Does not synchronize. The same as not writing [UdonSynced]: the field does not become a synced variable |
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”| What it decides | It decides, per synced object, that it belongs to this player right now. The subject is a GameObject, which you pass in to check, as in Networking.IsOwner(gameObject) |
| Why it is needed | Only the owner can assign to a synced variable. When a player who isn’t the owner assigns, that value is not sent to the other players |
| The first owner | The first player to join becomes master, and the owner of any object on which SetOwner() was never called is master. Only the rule deciding master is guaranteed; that the object’s owner is master is not |
| How many | The official documentation describes it as one. It is not guaranteed, though, and both a state with two owners and a state with no owner have 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 lower half of Figure 1 | This is the receiving side’s behaviour, and doesn’t happen on the owner |
OnDeserialization() | Called when a value is received. The owner doesn’t receive the values it sent, so it normally isn’t called there, but the official documentation nowhere states that it isn’t called on the owner, and cases of it firing on the owner have been reported. It cannot be used to decide whether you are the owner |
FieldChangeCallback | A mechanism that routes a received value through a property’s setter instead of putting it straight into the field. The owner receives no values, so this path never runs there (if the owner assigns to the property, the setter runs from that assignment itself. That is ordinary C# behaviour, unrelated to synchronization) |
| How you assign | Owner | Receiver |
|---|---|---|
Value = 1 (property) | The setter runs at the moment of assignment | The setter runs on receive |
backing = 1 (field) | Doesn’t run | — |
It looks as though the same work runs on both sides, but the paths differ. When the owner assigns directly to the field, 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 | It is dropped, and an error appears in the log |
| 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
There is no statement (it is generally treated as being called on every send. Keep it light on the assumption that it is,
and either way is fine).
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 |
| How often the sender can call events overall | Roughly 100 times per second. Determined dynamically and not configurable |
On the roughly 200 bytes for continuous sync, a VRChat staff member has stated roughly 256 bytes elsewhere, which conflicts with the figure in the official documentation. Estimating with the smaller one is safer.
| When exceeded (network events) | They queue on the sender rather than being dropped. Nothing is lost, but delivery is delayed |
| When exceeded (synced variables) | As in the table above: manual sync holds it and retries, continuous sync drops it |
| The overall limit | It is separate from the rate you specified yourself, so you can stay within your setting and still hit this one |
| The 200 bytes for continuous sync | It is a limit on all the synced variables in that program together. Every variable you add brings you closer to it |
| A single send | Not necessarily one on the wire either. Anything over 1024 bytes is split internally, so a send you intended as one can hit the rate limit |
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() | It runs on both the requester and the current owner, each locally. A split return value leaves a lasting discrepancy, so the verdict may use only what is certain to look the same to both (static information that doesn’t change at runtime, and the player identifiers passed as arguments. Synced variables do not qualify) |
| When the current owner has already left | It isn’t called; one is assigned automatically |
| While it is moving | Between the moment the requester switches over and the moment the other players learn of it, there appear to be two owners |
PlayerObject | An object created one per player who joins. Its owner is fixed to that player and cannot be moved to anyone else (the guarantee is scoped to while Start and OnDeserialization are running, and does not mean it is always correct) |
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 appears. The official documentation says nothing about when the value reverts, and write-ups split between a few frames later and unknown |
| 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”The most basic form.
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 go on a bool too.
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 go on a string too.
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 provides can be synchronized.
using UnityEngine;using Tsukimi;
public class SyncVector3 : TsukimiBehaviour{ [UdonSynced] private Vector3 point = new Vector3(1f, 2f, 3f);
void Start() { Debug.Log(point.x); // => 1 }}Syncing a struct you declared
Section titled “Syncing a struct you declared”A struct you declared can be synced too, as long as every field inside it is a type that can be synced on its own.
using UnityEngine;using Tsukimi;
public struct Pair { public int A; public float B; }
public class S_sync_struct : TsukimiBehaviour{ [UdonSynced] private Pair pair;
void Start() { pair.A = 3; pair.B = 1.5f; Debug.Log(pair.A); } // => 3}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 be synchronized too.
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”It specifies how to fill in between the values that arrive.
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 it sends
Section titled “When it sends”[UdonBehaviourSyncMode] decides that program’s sync method.
Manual sync
Section titled “Manual sync”Sets manual sync.
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”Sets continuous sync.
using UnityEngine;using Tsukimi;
[UdonBehaviourSyncMode(BehaviourSyncMode.Continuous)]public class AttributesSyncModeContinuous : TsukimiBehaviour{ [UdonSynced] private int value;
void Start() { value = 1; }}A mode with no synced variables
Section titled “A mode with no synced variables”This declares that the program has no synced variables.
using UnityEngine;using Tsukimi;using VRC.Udon.Common.Interfaces;
[UdonBehaviourSyncMode(BehaviourSyncMode.NoVariableSync)]public class AttributesSyncModeNoVariableSync : TsukimiBehaviour{ private int hits;
public override void Interact() { SendCustomNetworkEvent(NetworkEventTarget.All, nameof(Ping)); }
public void Ping() { hits = hits + 1; Debug.Log(hits); }}using UnityEngine;using Tsukimi;
[UdonBehaviourSyncMode(BehaviourSyncMode.NoVariableSync)]public class SyncNoVariableSyncWithSynced : TsukimiBehaviour{ [UdonSynced] private int count;
public override void Interact() { count = 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); // => 0 }}Per-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 at that moment.
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); // => 0 }}Ownership
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.
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); // => 1 }}Whether 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); // => 1 }}using 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”Line up values on the sending side and they arrive as the receiving side’s arguments.
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); }}