Skip to content

Unity Events Rewiring

TL;DR: Unity’s UnityEvent (used by Button.onClick, Slider.onValueChanged, CCK triggers, etc.) stores “persistent calls” as serialized data pointing at a specific Object + method name. At build time, any persistent call whose target is a WasmBehaviour is rewritten so the target becomes the replacement WasmRuntimeBehaviour and the method becomes one of a fixed set of trampolines that dispatch into WASM by method name.

From WasmBuildProcessor.RewireMap:

Original arg type (m_Mode)Setter methodTrigger methodSerialized arg field
UnityEngine.Objectset__InternalEventObjectArgTriggerScriptEventObjectm_ObjectArgument
intset__InternalEventIntArgTriggerScriptEventIntm_IntArgument
floatset__InternalEventFloatArgTriggerScriptEventFloatm_FloatArgument
stringset__InternalEventStringArgTriggerScriptEventStringm_StringArgument
boolset__InternalEventBoolArgTriggerScriptEventBoolm_BoolArgument

m_Mode values (Unity’s PersistentListenerMode):

ValueNameAction
0EventDefinedUse the event’s generic argument type. Rewritten to the matching setter+trigger pair, or to a void TriggerScriptEvent if no argument.
1VoidNo argument. Rewritten to a single TriggerScriptEvent("MethodName") call.
2ObjectRewritten to set__InternalEventObjectArg + TriggerScriptEventObject.
3Intset__InternalEventIntArg + TriggerScriptEventInt.
4Floatset__InternalEventFloatArg + TriggerScriptEventFloat.
5Stringset__InternalEventStringArg + TriggerScriptEventString.
6Boolset__InternalEventBoolArg + TriggerScriptEventBool.

For any mode that carries an argument, the build processor expands the single serialized call into two consecutive calls:

  1. Setter — set__InternalEventXxxArg(value) stores the hard-coded or event-defined argument on the runtime behaviour.
  2. Trigger — TriggerScriptEventXxx("MethodName") dispatches to the guest, which pulls the stored arg and invokes the guest method.

The setter call’s mode encodes the original typed argument (2–6). The trigger call is always mode 5 (String) because the method-name string is the trigger’s serialized argument.

  • Hardcoded: an argument baked into the serialized persistent call (e.g. you typed 42 in the inspector for an int event). isHardcoded = true; the setter receives that same baked value.
  • Event-defined: the argument comes from the runtime invocation (e.g. Slider.onValueChanged(float) passes the current value). isHardcoded = false; the setter’s arg field is zeroed at build time — the runtime fills it.

All rewritten calls set this to "WasmScripting.WasmRuntimeBehaviour, Assembly-CSharp". This is the fully-qualified name of the runtime behaviour host type; Unity uses it to resolve the method reflectively at runtime even when the original MonoScript no longer exists.

The attribute WasmScripting.ExternallyVisibleAttribute (CVR.CCK.Wasm/Scripting/Attributes/ExternallyVisibleAttribute.cs) marks a method as callable from outside the WASM module — i.e. by the host via TriggerScriptEvent(methodName). Since Preview.11-WASM it’s the sole route for host-initiated name-based dispatch.

Every method you wire to a UnityEvent persistent listener (Button.onClick, Toggle.onValueChanged, Slider.onValueChanged, animation events, CVRInteractable, etc.) must be public and carry [ExternallyVisible].

using WasmScripting;
public partial class Panel : WasmBehaviour
{
[ExternallyVisible]
public void OnButtonClicked() { /* wired to Button.onClick in inspector */ }
}
  • Any method referenced by a persistent UnityEvent listener, even if the listener args mode is zero-arg (UnityEngine.UI.Button.onClick).
  • Any method the host invokes by name — CVRInteractable method calls, CVRPointer events routed through TriggerScriptEvent, animation event callbacks, etc.
  • Unity lifecycle methods (Start, Update, LateUpdate, FixedUpdate, OnEnable, OnDisable, OnDestroy, the OnTrigger* / OnCollision* family). Found by enum-name scan in WasmBuildProcessor.ScanForUnityEvents.
  • Game events (OnPlayerJoined, OnPlayerLeft, OnPlayerRespawned, OnPropSpawned/Despawned, OnPlayerTriggerEnter/Stay/Exit, OnPlayerCollisionEnter/Stay/Exit, OnPropTrigger*, OnPropCollision*, OnPortalCreated/Destroyed, OnInstanceOwnerChange, OnInputReady, OnWorldPermissionsChanged). Found by ScanForGameEvents.
  • Delegate subscriptions (e.g. Networking.OnReceiveMessage += HandleMessage). The host calls these through the delegate pointer, not by name, so there’s no dispatcher lookup.
  • Direct same-VM calls between behaviours (other.RunMethod()). Plain C# — no host boundary crossed.

Omitting the attribute on a UnityEvent-targeted method produces no editor error and no build error. The rewire step still replaces the listener with TriggerScriptEvent("YourMethod"), but the guest-side dispatcher generated by the CCK toolchain has no entry for that name, so the call silently no-ops at runtime. Buttons appear wired but do nothing.

Only single-argument methods are rewireable:

public partial class Panel : WasmBehaviour
{
public void OnButtonClicked() { } // Void mode
public void OnSliderChanged(float v) { } // Float mode
public void OnNameEntered(string name) { } // String mode
public void OnToggled(bool on) { } // Bool mode
public void OnCountSet(int n) { } // Int mode
public void OnObjectPicked(UnityEngine.Object o) { } // Object mode
}

Multi-argument methods can’t be targeted by Unity’s persistent call system in the first place — call them from a single-arg handler that wraps the payload.

Inside WasmBuildProcessor.BuildScripts:

  1. Replace WasmBehaviour with WasmRuntimeBehaviour and remember the instance ID mapping.
  2. RemapWasmBehaviourReferences(...) — for any serialized object reference that pointed at the old behaviour, redirect to the new runtime behaviour.
  3. RewireUnityEvents(...) — recursively scan every Component for SerializedProperty entries of type PersistentCall, detect ones targeting a replaced instance ID, and rewrite per the table above.

The build processor emits log lines like:

[RerouteUnityEvents] -> Step 1: Panel.set__InternalEventIntArg
[RerouteUnityEvents] -> Step 2: Panel.TriggerScriptEventInt("OnCountSet")

Check the Unity console after a CCK build if you suspect a persistent call didn’t get rewired.

  • Dynamic UnityEvent.AddListener(lambda) — those aren’t persistent calls; they live at C# runtime only. The WASM runtime behaviour has no equivalent. Use persistent calls via the inspector, or implement your own event dispatch inside the guest.
  • Multi-arg methods from UI — not rewireable.
  • Non-serializable argument types (e.g. a user struct) — rewrite table only covers the five Unity-serializable primitives + Object.
  • CVR.CCK.Wasm/CCK/Editor/WasmBuildProcessor.cs — RewireUnityEvents, RecursiveRewire, RewireCall, RewireVoidTarget, RewireEventDefinedTarget, and the RewireMap dictionary that drives the mode → setter/trigger pair.
  • CVR-GameFiles/WasmScripting/WasmRuntimeBehaviour.cs — the _InternalEventXxxArg setters and TriggerScriptEvent / TriggerScriptEvent{String,Int,Float,Bool,Object} trampolines the rewire points at.
  • CVR.CCK.Wasm/Scripting/Attributes/ExternallyVisibleAttribute.cs — the attribute the guest-side dispatcher looks for.
  • Events — canonical Unity/game events the build processor scans for (different mechanism).
  • Runtime Lifecycle — the scripting_call_trigger_script_event* exports that receive these.
  • Authoring — recommended method signatures on your behaviour.