Skip to content

Sync

SyntaxDescriptionNote
[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
SyntaxDescriptionErrorReasonAlternative
[UdonSynced] private Bag bag; (holding an array)Syncing a struct that holds an arrayTUKI0103not yetMove the array out into its own field and sync each of them
[UdonSynced] private Nothing nothing; (no fields)Syncing a struct with nothing in itTUKI0103by designGive 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 interpolatedTUKI0106runtimeDrop the interpolation, or split out a struct holding only types that can be interpolated
[UdonSynced] private int[][] grid;Sync an array of arraysTUKI0103runtimeUse one flat array and compute the index (arrays)
[UdonSynced] private GameObject target;Sync a reference to an objectTUKI0103runtimeWire 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 interpolateTUKI0106runtimeDrop the interpolation
[UdonSynced] public int Value { get; set; }Attach it to a propertyCS0592runtimeAttach 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)CS7014runtimeMake it a field
[UdonSynced] private static int v;Attach it to a static fieldTUKI0110runtimeRemove static and make it an ordinary field
[UdonSynced] private const int v = 3;Attach it to a const fieldTUKI0110runtimeSame as above
[UdonSynced] private readonly int v;Attach it to a readonly fieldTUKI0110runtimeRemove readonly
[UdonBehaviourSyncMode(Continuous)] and [UdonSynced] int[]Syncing an array under the keep-sending methodTUKI0107runtimeUse Manual
[UdonBehaviourSyncMode(Manual)] and [UdonSynced(Linear)]Asking for interpolation under the send-on-call methodTUKI0107runtimeUse Continuous
[UdonBehaviourSyncMode(None)] and [UdonSynced]Holding a synced variable under the no-sync methodTUKI0107by designChange the method, or remove the attribute
[UdonBehaviourSyncMode(NoVariableSync)] with [UdonSynced]Holding a synced variable under the mode with no synced variablesTUKI0107by designChange the method, or remove the attribute

Only fields can carry [UdonSynced].

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.

SpellingMeaning
[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

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 it decidesIt 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 neededOnly 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 ownerThe 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 manyThe 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
WhereNote
RequestSerialization()It doesn’t send once per call. However many times you call it, only one send happens next
SendIt 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 sideThe order isn’t fixed. To see a state where multiple variables are all in place, wait for OnDeserialization()
The lower half of Figure 1This 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
FieldChangeCallbackA 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 assignOwnerReceiver
Value = 1 (property)The setter runs at the moment of assignmentThe 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).

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 sendsWhen the owner calls RequestSerialization()Automatic. Sends repeatedly even if the value hasn’t changed
When assigning to a synced variableNot sent. The value stays with the owner until RequestSerialization() is next calledLeft as is, it rides along on the next send
When calling RequestSerialization()A send is scheduledNothing happens. It doesn’t add a send
When sends back upHeld and resentIt is dropped, and an error appears in the log
Limit per sendRoughly 280,496 bytesRoughly 200 bytes
What it suitsValues sent only on change, like a score or a stateValues 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).

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 measuresRough figure
Amount sendable per secondAbout 11 kilobytes
Limit per sendRoughly 280,496 bytes for manual sync, roughly 200 bytes for continuous sync
Times a single network event can be called5 per second by default. Write it as [NetworkCallable(100)] to raise it up to 100
How often the sender can call events overallRoughly 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 limitIt 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 syncIt is a limit on all the synced variables in that program together. Every variable you add brings you closer to it
A single sendNot 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

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 leftIt isn’t called; one is assigned automatically
While it is movingBetween the moment the requester switches over and the moment the other players learn of it, there appear to be two owners
PlayerObjectAn 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.

  • 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
MisunderstandingActually
Since there’s one owner, calling SetOwner() acts as mutual exclusionIf 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 ownerThere 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 immediatelyNo 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 eventsSynced variable values are sent, but network events are not resent
Right after joining, nothing has been called yetOnDeserialization() is called at the point of joining. If it plays sounds or animations, they all play at once the moment you join

A field with [UdonSynced] becomes the target of sync.

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
}
}

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
}
}

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"
}
}

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
}
}

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
}

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
}
}

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();
}
}

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); }
}

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
}
}

[UdonBehaviourSyncMode] decides that program’s sync method.

Sets manual sync.

using UnityEngine;
using Tsukimi;
[UdonBehaviourSyncMode(BehaviourSyncMode.Manual)]
public class AttributesSyncModeManual : TsukimiBehaviour
{
[UdonSynced] private int value;
void Start()
{
value = 1;
RequestSerialization();
}
}

Sets continuous sync.

using UnityEngine;
using Tsukimi;
[UdonBehaviourSyncMode(BehaviourSyncMode.Continuous)]
public class AttributesSyncModeContinuous : TsukimiBehaviour
{
[UdonSynced] private int value;
void Start()
{
value = 1;
}
}

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;
}
}

These are methods called as part of sync. The base declares them, so write them with override.

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
}
}

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
}
}

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
}
}

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
}
}

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
}
}

Forms for checking the owner or transferring ownership.

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();
}
}
}

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);
}
}

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
}
}

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
}
}

A form that sends a method call rather than a variable’s value.

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);
}
}

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);
}
}