Skip to content

UdonSharp ↔ CVR WASM Mapping

TL;DR: UdonSharp compiles a restricted C# subset to VRChat’s Udon bytecode. CVR WASM scripting compiles a broader C# surface to WebAssembly via NativeAOT and runs it through Wasmtime. Both subclass a base behaviour; both expose a Player-like API, player/prop/world events, and networked messaging. They diverge sharply on state synchronization (Udon auto-syncs fields; WASM requires explicit messages) and supported language features (WASM handles generics, LINQ, inheritance, try/catch, and binary serialization; Udon does not). This page is a porting reference for creators coming from VRChat.

Reference sources:

UdonSharpCVR WASM
Base classUdonSharpBehaviour : MonoBehaviourWasmBehaviour : MonoBehaviour (must be partial)
RuntimeUdon VM (custom, stack-based)Wasmtime 34 with epoch preemption
Language subsetC# subset; no generics, no delegates (mostly), no LINQ, no try/catchFull C# via NativeAOT (generics, LINQ, try/catch, async on single thread)
Compile outputUdon bytecode assembly (.uasset)Assets/WasmModule.wasm
Per-object script countOne UdonSharpBehaviour per componentOne VM per content root (avatar/prop/world), many behaviours share it
ReflectionEmulated via compiler-generated stubsDisabled by default (<IlcDisableReflection>true</IlcDisableReflection>); opt-in per project
ThreadingSingle-threadSingle-thread (WASM threads disabled)

Related on the CVR side: Architecture · Runtime Lifecycle · Authoring.

// UdonSharp (VRChat)
using UdonSharp;
using UnityEngine;
using VRC.SDKBase;
using VRC.Udon;
public class Foo : UdonSharpBehaviour
{
public void Start() { }
public void Update() { }
}
// CVR WASM (equivalent shape)
using UnityEngine;
using WasmScripting;
public partial class Foo : WasmBehaviour
{
public void Start() { }
public void Update() { }
}

Key difference: the partial keyword is required on WasmBehaviour subclasses (the source generator emits a companion partial for serialization plumbing). See Authoring.

ConceptUdonSharpCVR WASM
Local playerNetworking.LocalPlayerVRCPlayerApiCVR.LocalPlayer.PlayerObjectCVR.Player (world-only)
Remote playersVRCPlayerApi.GetPlayers(array)CVR.Player.GetAllPlayers() / GetRemotePlayers()
Player display nameplayer.displayNameplayer.GetUsername()
Persistent user IDplayer.userId (deprecated in public; not stable across sessions)player.GetUserId() — requires AccessUserIdentity world permission
Network IDplayer.playerId (int)player.GetNetworkId() (short)
World / sceneVRC.SDKBase.Utilities.IsValid(...)Scene is always “Self” scope from a world script; world getters on CVR.World
Is owner?Networking.IsOwner(gameObject) or Networking.GetOwner(gameObject) == Networking.LocalPlayerNo per-GameObject ownership. Use Networking.GetInstanceOwner() == LocalPlayer.PlayerObject for instance-level authority, or WasmUtils.GetOwnerContext() == CVRScriptOwnerContext.Self for avatar/prop-level
Transfer ownershipNetworking.SetOwner(player, gameObject)No first-class equivalent. Design around instance-owner authority + OnInstanceOwnerChange.
LogDebug.Log(...)Debug.Log(...) (bound via DebugLinksManual)

Related: CVR Host Functions → Player · Permissions.

UdonSharp eventCVR WASM equivalentNotes
Start, Update, LateUpdate, FixedUpdateSame namesWASM also exposes PostUpdate, PostLateUpdate, PostFixedUpdate for ordering after Unity + CVR systems (see Events → Execution order)
OnEnable, OnDisable, OnDestroySame names
OnPlayerJoined(VRCPlayerApi)OnPlayerJoined(CVR.Player)Parameter type differs
OnPlayerLeft(VRCPlayerApi)OnPlayerLeft(CVR.Player)
OnPlayerRespawn(VRCPlayerApi)OnPlayerRespawned(CVR.Player)Note the -ed suffix
OnPlayerTriggerEnter(VRCPlayerApi)OnPlayerTriggerEnter(CVR.Player)Single-arg in CVR — does not tell you which trigger; use Unity’s OnTriggerEnter(Collider) for per-trigger dispatch. See Events → Player events.
OnTriggerEnter(Collider)SameUnity physics event, identical semantics
OnCollisionEnter(Collision)Same
OnPickup, OnDrop (VRCPickup)No direct equivalentCVR interaction is wired via CVRPointer / CVRInteractable + persistent UnityEvents
OnPreSerialization, OnDeserializationNo equivalentUdon has auto-sync; CVR uses explicit Networking.SendMessage / OnReceiveMessage
OnOwnershipTransferred(VRCPlayerApi)OnInstanceOwnerChange(CVR.Player)CVR tracks instance-level ownership, not per-GameObject
OnInputJump(bool, UdonInputEventArgs)CVRInput — read CVRInput.GetButtonDown(CVRButton.Jump) in Update, or subscribe to OnInputReady
OnAvatarEyeHeightChangedNo direct equivalentRead LocalPlayer.GetCurrentHeight() in a world script and diff it yourself
OnStationEntered(VRCPlayerApi)No direct equivalentCVR uses chair / seat components, not a built-in station API
VRCPlayerApi.IsUserInVR()No direct binding; use LocalPlayer.GetPlaySpaceOffset() as a heuristic — it’s Vector3.zero unconditionally in desktop and a non-zero offset once a VR user moves in their playspace. Source: PlayerSetup.GetPlayerPlaySpaceOffset returns Vector3.zero when isUsingVr == false. Edge case: a stationary VR user at their exact tracking centre also reads zero.

Full CVR event catalog: Events.

This is the biggest divergence.

// UdonSharp
using UdonSharp;
using UnityEngine;
using VRC.SDKBase;
[UdonBehaviourSyncMode(BehaviourSyncMode.Manual)]
public class Counter : UdonSharpBehaviour
{
[UdonSynced(UdonSyncMode.None)] public int count;
public override void OnDeserialization() { UpdateUi(); }
public void Increment()
{
if (!Networking.IsOwner(gameObject))
Networking.SetOwner(Networking.LocalPlayer, gameObject);
count++;
RequestSerialization();
}
}

Udon handles the wire format, serialization schedule, and late-joiner sync for you.

// CVR WASM — explicit messaging
using System;
using UnityEngine;
using WasmScripting;
using CVR;
public partial class Counter : WasmBehaviour
{
[WasmSerialized] private int count;
private const byte MSG_COUNT = 1;
void Start() { Networking.OnReceiveMessage += OnNet; }
void OnDestroy() { Networking.OnReceiveMessage -= OnNet; }
public void OnClicked()
{
Player owner = Networking.GetInstanceOwner();
Player me = LocalPlayer.PlayerObject;
if (owner != me) return; // instance-owner authority
count++;
Broadcast();
}
private void Broadcast()
{
var w = new BufferReaderWriter(8);
w.Write(MSG_COUNT);
w.Write(count);
Networking.SendMessage(w.Buffer.Slice(0, w.Length), null, SendType.Reliable);
}
private void OnNet(Player sender, Span<byte> msg)
{
var r = new BufferReaderWriter(msg);
r.Read(out byte tag);
if (tag != MSG_COUNT) return;
if (sender != Networking.GetInstanceOwner()) return;
r.Read(out count);
}
}

You design the protocol, you choose the SendType, you verify sender authority. The CVR team considers this a feature — predictable bandwidth, no surprise churn — but it’s more boilerplate. See Networked Counter example, BufferReaderWriter, Networking.

UdonSharpCVR WASM
GranularityPer-GameObjectPer-instance (one “instance owner” for the whole session)
Take ownershipNetworking.SetOwner(player, gameObject)Can’t; the instance owner is assigned by CVR’s matchmaker
Release ownershipImplicit on disconnectImplicit on disconnect; new owner is chosen by CVR
QueryNetworking.GetOwner(gameObject) / IsOwner(gameObject)Networking.GetInstanceOwner()
Handoff eventOnOwnershipTransferred(VRCPlayerApi newOwner)OnInstanceOwnerChange(Player newOwner)

For avatar-scoped scripts, “Self” owner context means you’re the wearer; the equivalent question “is this my prop?” maps to WasmUtils.GetOwnerContext() == CVRScriptOwnerContext.Self. See Permissions.

// UdonSharp
SendCustomNetworkEvent(NetworkEventTarget.All, "MyEvent");
SendCustomNetworkEvent(NetworkEventTarget.Owner, "MyOwnerOnlyEvent");
// CVR WASM — wrap the event name yourself
var w = new BufferReaderWriter(32);
w.Write((byte)MSG_CUSTOM_EVENT);
w.Write("MyEvent");
Networking.SendMessage(w.Buffer.Slice(0, w.Length),
target: null, // null = broadcast to all others
sendType: SendType.Reliable);

To target only the owner, resolve their short network ID and pass new short[] { owner.GetNetworkId() } as target.

UdonSharpCVR WASM
Input.GetKey(KeyCode.Space) (in U# runs, though VRC restricts)CVR.CVRInput.GetButtonDown(CVRButton.Jump) / .WasPressedThisFrame(...)
VRC input events (OnInputJump, OnInputMoveHorizontal, …)Read CVRInput.Movement, CVRInput.Look, CVRInput.InteractRight, etc. from Update
Input set via UdonInputEventArgs.floatValueWrite with CVRInput.SetMovement(...) — only materializes from world scripts or avatar-on-wearer

See CVR Input for the double-buffered memory model and the write-gating rules.

UdonSharpCVR WASM
Persistent world saveNone built-in; abuse URL/VRCString via external servicesWasmScripting.FileStorage — world-only, per-world quota, encrypted on disk
Per-player persistenceNone built-inSame as above; partition by Player.GetUserId() inside your save file
In-session state[UdonSynced] fields[WasmSerialized] on private fields + Networking.SendMessage for cross-client sync

CVR’s storage is a real advantage for single-world persistence. See File Storage and the Idle / RPG examples.

UdonSharpCVR WASM
Web requestsVRCImageDownloader / VRCStringDownloader — limited, domain-gatedHTTP is gated by the HttpApiAllowed + HttpAllowedDomains world permissions. See World Permissions.
Load textures from URLVRCImageDownloader.DownloadImageGo through the permissioned HTTP API then assign to Texture2D
Raw socketsNot possibleNot possible
FeatureUdonSharpCVR WASM
Generics (List<T>, Dictionary<K,V>)Partial — built-ins only, no custom genericsFully supported (via NativeAOT)
LINQNot supportedFully supported
try / catch / finallyLimited / disabledFully supported; exceptions cross the WASM boundary back to the host
async / awaitNot supportedSupported at C# level but single-threaded (continuations on same invocation)
Custom classes (non-behaviour)Via [System.Serializable] with specific rulesVia [WasmSerialized] on the class; fields serialize per normal rules
InterfacesNot supportedSupported
InheritanceOnly direct UdonSharpBehaviour subclass (no intermediate abstract types)Arbitrary inheritance chain, including abstract intermediates
structPartialFully supported incl. readonly structs and record structs
Delegates / Action / FuncNot supportedSupported
Unsafe / pointersNot supportedNot supported (blocked by WASM — no raw pointers anyway)
ReflectionEmulated for GetProgramVariable / SendCustomEvent onlyDisabled by default (IlcDisableReflection=true); opt-in per project
Span<T> / ReadOnlySpan<T>Not supportedFully supported; used throughout the sandbox

Practical rule: if you hit a limitation in UdonSharp, chances are the direct C# version works in WASM. The opposite is rarer.

UdonSharp attributeCVR WASM equivalent
[UdonSynced] / [UdonSynced(UdonSyncMode.Linear)] / [UdonSynced(UdonSyncMode.Smooth)]No equivalent — explicit networking
[UdonBehaviourSyncMode(BehaviourSyncMode.Manual)]N/A
[FieldChangeCallback("PropertyName")]Implement yourself: wrap the field in a property with setter side-effects
[RecursiveMethod]Not needed — WASM supports ordinary recursion
[SerializeField]Same — works as in Unity
[HideInInspector], [Header], [Tooltip], [Range], [Space]Supported but stripped from the WASM module unless UNITY_EDITOR_ATTRIBUTES is added to project defines. See Available Attributes.
[ExternallyVisible]Required on any method the host calls by name — UnityEvent persistent listeners (Button.onClick etc.), CVRInteractable, animation events. Omitting it silently no-ops the call. Lifecycle events (Start, Update, …) and game events (OnPlayerJoined, OnInstanceOwnerChange, …) are picked up by enum-name scans and don’t need it. See Unity Events Rewiring.

Udon:

[UdonBehaviourSyncMode(BehaviourSyncMode.Manual)]
public class Counter : UdonSharpBehaviour
{
[UdonSynced] public int count;
public override void OnDeserialization() => display.text = $"{count}";
public void Bump()
{
if (!Networking.IsOwner(gameObject))
Networking.SetOwner(Networking.LocalPlayer, gameObject);
count++;
RequestSerialization();
}
}

CVR (owner-authoritative):

public partial class Counter : WasmBehaviour
{
[WasmSerialized] private int count;
public TMPro.TMP_Text display;
private const byte MSG = 1;
void Start() { Networking.OnReceiveMessage += OnNet; Refresh(); }
void OnDestroy() { Networking.OnReceiveMessage -= OnNet; }
public void Bump()
{
if (Networking.GetInstanceOwner() != LocalPlayer.PlayerObject) return;
count++;
Broadcast();
Refresh();
}
void OnNet(System.Player sender, System.Span<byte> msg) { /* verify + apply */ }
void Broadcast() { /* pack + Networking.SendMessage */ }
void Refresh() { display.text = $"{count}"; }
}

Compare side-by-side: Networked Counter example.

Udon:

VRCPlayerApi[] players = new VRCPlayerApi[80];
int n = VRCPlayerApi.GetPlayers(players).Length;
for (int i = 0; i < n; i++) Debug.Log(players[i].displayName);

CVR:

Player[] players = Player.GetAllPlayers();
if (players != null)
for (int i = 0; i < players.Length; i++)
Debug.Log(players[i].GetUsername());

Note: Player.GetAllPlayers() is world-only. Use it from a world script; from an avatar script rely on game events (OnPlayerJoined / OnPlayerLeft) to maintain your own list.

Udon has built-in stations (VRCStation). CVR does not. You use CVR’s native chair/seat components (CCK CVRSeat) wired to WasmBehaviour via UnityEvents. No code inside your WASM script manages the sit itself — you just listen for the UnityEvent.

  • Udon’s sync smoothing[UdonSynced(UdonSyncMode.Linear)] and Smooth interpolate synced floats. No CVR equivalent; interpolate yourself using local timing + received state.
  • VRChat’s interactable layer (OnInteract, OnPickup, OnPickupUseDown) — CVR uses CVRPointer / CVRInteractable CCK components wired through persistent UnityEvents.
  • Stations — no CVR built-in. Use CCK seat components.
  • VRChat’s quest-compat concept — WASM scripts ship identically across platforms; no per-platform variants.
  • URL whitelist loading — CVR’s HTTP path uses world permissions + domain whitelist; the shape is different enough that you’ll rewrite the download layer.
  • OnPlayerHeldItemPickupDrop — no equivalent.
  • FileStorage for per-world persistent save data.
  • Broad C# language surface (generics, LINQ, async, full try/catch).
  • Fine-grained events like PostUpdate / PostLateUpdate / PostFixedUpdate that run after all Unity + CVR systems.
  • BufferReaderWriter — zero-allocation binary packing for network / disk.
  • WorldPermissions API — user-approved capability tier (identity / HTTP / storage quota).
  • CVRInput with write access (world + avatar-on-wearer) — drive the local player’s input programmatically.
  • One VM per content root (not per component) — lets a world have hundreds of coordinated behaviours sharing state without VM-per-component overhead.
  • Source-generator-driven serialization — tighter than Udon’s reflection-based sync.

A literal UdonSharp → WASM code translator is not practical. The runtimes differ in fundamental ways:

  • Udon’s per-GameObject ownership has no clean WASM equivalent; the “right” answer depends on whether you want instance-owner authority, spawner authority, or distributed peer-to-peer.
  • [UdonSynced] needs a design choice for each field (broadcast cadence, delta vs. snapshot, late-joiner handling) that the translator can’t guess.
  • Udon’s disabled features (generics, LINQ, try/catch) mean Udon code doesn’t exercise them; going the other direction risks breakage.

What is practical:

  • This mapping page — a porting guide.
  • Side-by-side examples — the examples demonstrate patterns you’d recognize from Udon (click counter, turn-based, networked state, file storage).
  • Validator coverage — the script validator flags the most common UdonSharp-to-WASM drift: property access (player.displayName vs player.GetUsername()), ownership calls, unbound types.
  • Architecture — how the WASM runtime is put together.
  • Permissions — three-axis access control.
  • Events — full event catalog with signatures.
  • Networking — sending and receiving messages.
  • Examples — ready-to-test patterns for common gameplay needs.
  • Not Exposed — what the sandbox denies on the CVR side.