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. Setterset__InternalEventXxxArg(value) stores the hard-coded or event-defined argument on the runtime behaviour.
  2. TriggerTriggerScriptEventXxx("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.csRewireUnityEvents, 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.