Skip to content

Authoring a WASM Script

TL;DR: Open Unity with the CCK Wasm package. Create a C# script via Assets > Create > CVR Wasm Behavior (C#) and subclass WasmBehaviour. Add a CCKWasmProjectDescriptor to your avatar/prop/world root and either drop the component on any child or add the script to the descriptor’s includedScripts list. Methods named after the UnityEvents or GameEvents enums are auto-detected and dispatched at runtime.

  • Unity version matching the CVR CCK.
  • The CVR.CCK.Wasm package imported into your project.
  • Internet — on first build the pipeline downloads .NET 10 preview, WASI SDK 25, and the CCK WasmModule into %LOCALAPPDATA%/ChilloutVR/CVRBuildTools/.

Unity menu: Assets > Create > CVR Wasm Behavior (C#).

Template (CVR.CCK.Wasm/CCK/Editor/Templates/WasmBehaviorTemplate.cs.template):

using UnityEngine;
using WasmScripting;
public partial class NewWasmBehavior : WasmBehaviour
{
void Start() { }
void Update() { }
}

Constraints on the class:

  • Must inherit WasmBehaviour (directly or transitively).
  • Cannot be abstract if you want instances on GameObjects (WasmBuildProcessor skips abstract types from the descriptor list).
  • partial is required — both the WasmBehaviour subclass and any custom class you mark [WasmSerialized] must be partial. The build pipeline emits a generated partial half that plugs into the serializer. A non-partial WasmBehaviour class compiles but won’t serialize correctly.
  • File-scoped namespace or single top-level type per file works; the CCK pipeline only compiles the paths it collects, not an entire Unity asmdef.

The build processor finds events by method name. Define a public or private method whose name matches a value in either enum and it will be dispatched automatically.

void Awake() { }
void Start() { }
void Update() { }
void LateUpdate() { }
void FixedUpdate() { }
void OnEnable() { }
void OnDisable() { }
void OnDestroy() { }
void OnTriggerEnter(Collider other) { }
void OnCollisionEnter(Collision hit) { }
void OnPlayerJoined(Player p) { }
void OnPlayerLeft(Player p) { }
void OnPropSpawned(Prop p) { }
void OnInstanceOwnerChange() { }
void OnWorldPermissionsChanged() { }

These are explicitly stubbed out on WasmBehaviour so they never dispatch, even if you define them:

  • OnServerInitialized
  • OnConnectedToServer
  • OnGUI
  • OnApplicationQuit
  • OnApplicationFocus(bool)
  • OnApplicationPause(bool)

Source: CVR.CCK.Wasm/Scripting/WasmBehaviour.cs (the #if !CVR_SCRIPTING_ENVIRONMENT block).

Public fields serialize by default. Use the attributes in WasmScripting:

using WasmScripting;
public partial class Score : WasmBehaviour
{
public int points; // serialized
[NonWasmSerialized]
public float uiJiggleTimer; // NOT serialized (transient UI state)
[WasmSerialized]
private int hiddenCombo; // private, BUT serialized
[WasmSerialized]
private CustomBag savedBag; // custom class must also carry [WasmSerialized]
}

Defaults serialized without annotation:

  • C# primitives (int, bool, string, float, …),
  • UnityEngine.Object references,
  • Arrays,
  • IList<T>,
  • IDictionary<K,V>.

Source: CVR.CCK.Wasm/Scripting/Attributes/SerializationAttributes.cs.

More detail: Serialization.

A root GameObject (the avatar/prop/world root) must carry a CCKWasmProjectDescriptor. Add it from the inspector menu Add Component > ChilloutVR Editor > CCK Wasm Project Descriptor.

Descriptor fields (CVR.CCK.Wasm/CCK/CCKWasmProjectDescriptor.cs):

FieldPurpose
enableDebuggingBuild in Debug config (slower, includes symbols). Also toggled automatically in Play Mode.
enableReflectionOmit <IlcDisableReflection>true</IlcDisableReflection> from the generated csproj. Enables runtime reflection at the cost of a much bigger .wasm.
includedScripts[]C# scripts compiled into the module even if no component using them is present in the hierarchy. Each entry: isActive + MonoScript reference.
projectDefines[]Custom preprocessor defines (isActive + define). Names are auto-sanitized to valid identifiers.
externalProjectPathPath to your own .csproj; if set, the pipeline invokes dotnet publish on it directly (skipping csproj generation) and includes its output as the module.

Two ways to surface a script to the pipeline:

  1. Component pickup — drop your WasmBehaviour subclass onto any GameObject inside the root. The build processor scans the hierarchy.
  2. Descriptor list — add the MonoScript to includedScripts. Use this for utility types or callbacks you invoke dynamically but never add as a component.

The generated csproj always defines one of the following based on the content type being built:

  • CVR_SCRIPTING_CONTEXT_AVATAR
  • CVR_SCRIPTING_CONTEXT_PROP
  • CVR_SCRIPTING_CONTEXT_WORLD

Plus any custom defines from projectDefines.

Inside the script environment, CVR_SCRIPTING_ENVIRONMENT is additionally defined (see CVR.CCK.Wasm/Scripting/WasmBehaviour.cs:10). Use it to branch code between “in-editor Unity tooling” and “compiled-for-WASM” — for example, hiding editor-only types behind #if !CVR_SCRIPTING_ENVIRONMENT.

Direct, intra-script calls work as normal C# method calls. Calls that cross from Unity’s UnityEvent system (e.g. Button.onClick) into your behaviour are handled automatically — the build processor rewrites persistent calls to route through TriggerScriptEvent(...). See Unity Events Rewiring.

  • All WasmBehaviour subclasses and [WasmSerialized] custom classes must be partial. Required by the serialization generator.
  • Scripts on Prefabs outside the main scene are not built into the WASM module (CCK limitation). Place referenced objects in the main scene and leave them inactive until needed; instantiate them from there.
  • Crawling Prefabs outside a scene throws permission errors — the scope resolver tags prefab contents as ScopeContext.None and every access is rejected.
  • ScriptableObject fields are not supported. Store shared data as serialized fields on a WasmBehaviour component instead.
  • [DefaultExecutionOrder] orders only among WasmBehaviours within the same VM. It does not change when your scripts run relative to Unity’s or CVR’s built-in systems. For “after-everything” semantics use PostUpdate / PostLateUpdate / PostFixedUpdate — see Events → Execution order.
  • You cannot animate fields on a WasmBehaviour via an Animator. Drive state by calling methods from script.
  • StartCoroutine and async patterns run on the single WASM thread — they don’t yield back to Unity between awaits. Use Update / FixedUpdate for periodic work.
  • Root has CCKWasmProjectDescriptor.
  • All referenced MonoScript types inherit WasmBehaviour and are partial.
  • No usage of disallowed APIs (UnityEngine.Input, Application, System.IO, System.Net, etc.) — see Not Exposed.
  • No usage of disallowed Unity events above.
  • State you want to persist into play is on fields using default serialization rules or [WasmSerialized].
  • Any referenced runtime-instantiated GameObject is already in the main scene (disabled is fine).
  • No ScriptableObject fields on the WasmBehaviour.
  • CVR.CCK.Wasm/Scripting/WasmBehaviour.cs — the MonoBehaviour base class WASM scripts inherit from (also contains the empty stubs for OnGUI / OnApplicationQuit / OnApplicationFocus / OnApplicationPause / OnServerInitialized / OnConnectedToServer).
  • CVR.CCK.Wasm/Scripting/Attributes/ExternallyVisibleAttribute, SerializationAttributes (WasmSerialized, NonWasmSerialized), WasmRuntimeInitializeOnLoadMethod.
  • CVR.CCK.Wasm/CCK/CCKWasmProjectDescriptor.cs — descriptor fields (enableDebugging, enableReflection, includedScripts, projectDefines, externalProjectPath).
  • CVR.CCK.Wasm/CCK/Editor/Templates/WasmBehaviorTemplate.cs.template — the template used by Assets > Create > CVR Wasm Behavior (C#).
  • Events — full event catalog with signatures.
  • Permissions — why a call might throw WasmAccessDeniedException.
  • Build Pipeline — what happens when you press the CCK build button.