Skip to content

Community Examples

TL;DR: A survey of user-built WASM projects shared in CVR’s testing community. Each section lists what the project demonstrates, where to get it, and (where relevant) which API calls need updating for the current CCK (Preview.27-WASM.27). Most scripts authored before Preview.27-WASM.26 use the old property-style CVR APIs (Player.Username, Prop.RootTransform, …) and will not compile against the refactored method-style surface (GetUsername(), GetRootTransform(), …).

  • World ID: w+acebfff3-ec94-4d05-bdc0-cd3e72b6eee4
  • Source: https://github.com/NotAKidoS/chilloutvr-gameboy
  • ROM folder: %LOCALAPPDATA%/ChilloutVR/LocalStorage/acebfff3-ec94-4d05-bdc0-cd3e72b6eee4/
  • Usage: Drop .gb files into the folder above. Press Insert in-game to cycle to the next detected ROM.

A full Game Boy emulator running inside WASM scripting. Shows what’s possible when you push the runtime: full CPU emulation, PPU rendering, MBC3 mapper, joypad mapping.

Demonstrates:

  • Raw ROM read via FileStorage.GetFiles() + file-content access (world-only + FileStorageReadRawFiles permission).
  • Continuous per-frame CPU emulation inside Update (within the 20 ms epoch deadline).
  • Texture2D output for the LCD display.
  • System.Diagnostics.Stopwatch used for timing — strictly outside the sandbox’s forbidden-namespace list, but Stopwatch is a pure timer and works in practice. The validator flags System.Diagnostics on principle; treat that specific warning as a false positive for Stopwatch-only usage.

Status: likely works with the current CCK provided permissions are requested. The repository’s Assets/test_UdonProgramSources/ folder name is historical — the scripts were adapted from a prior Udon (VRChat) port.

A port of the Optime GBA emulator. Ships with its own Proxies/ directory — manually-written WasmBehaviour, ExternallyVisibleAttribute, and type shims (Avatar, AvatarPoint, BufferReadWriter, FileStorage, LocalPlayer, Networking, Player, Prop, World) that predate the CCK’s own stubs.

Notes:

  • The included Proxies/WasmBehaviour.cs uses runtime reflection to dispatch TriggerScriptEvent. This requires CCKWasmProjectDescriptor.enableReflection = true, which enlarges the compiled .wasm significantly.
  • If you rebase onto the current CCK, delete Proxies/ and let the CCK’s source generator + stubs handle dispatch instead — the module gets smaller and you can leave reflection disabled.
  • Property-style API calls in the emulator code (e.g. player.Username) will need to move to the Get*() method style.

Status: older snapshot; needs refactor against current CCK for clean builds.

  • World ID: w+54dbf379-6456-4b8d-bfe4-a4c3a509a337
  • Source package: WasmExamplesWorld.unitypackage (requires ProBuilder + TextMeshPro + the latest WASM CCK).

Showcases:

  • Player join / leave / respawn event logging.
  • Networked text chat.
  • Prop destruction zone (lift + rotate + shrink animation then Prop.Destroy()).
  • Player clone (mirror-style render of the local player).
  • Mini world (scaled world-in-a-world with scale-correct camera).
  • Simple logging tricks.

The package contains seven scripts. Six still work cleanly; two need small edits for the current CCK.

ScriptStatusNotes
CameraDepthHelper.csOKEmpty behaviour marker; nothing to break.
NetworkManager.csFix neededUses sender.Username — refactor to sender.GetUsername(). Also requires the world to hold AccessUserIdentity only for GetUserId(); GetUsername() no longer needs that permission as of Preview.27-WASM.27.
PlayerClone.csOKAlready uses LocalPlayer.GetPosition() / GetRotation() / GetPlaySpaceScale().
PropDestroyer.csFix neededprop.RootTransformprop.GetRootTransform().
SimpleLogger.csMostly OKUses System.IO.Path.GetFileNameWithoutExtension(filePath) — pure string manipulation, not a filesystem call. Validator flags System.IO.Path as forbidden on principle; in practice this specific method works. Treat as a warning-level concern.
SmallWorld.csOKAlready uses GetPlaySpaceScale().
TextWall.csFix neededUses player.Username in four places — AddGlobalUserText, AddPlayerJoinedMessage, AddPlayerLeftMessage, AddPlayerRespawnedMessage. Each needs player.GetUsername().
NetworkManager.cs
- SimpleLogger.Info($"Received message from {sender.Username}: {message.Length} bytes");
+ SimpleLogger.Info($"Received message from {sender.GetUsername()}: {message.Length} bytes");
PropDestroyer.cs
- Transform rootTransform = prop.RootTransform;
+ Transform rootTransform = prop.GetRootTransform();
// TextWall.cs — apply to every Username reference
- InternalAddMessage($"\n<color=#4ECDC4><b>{player.Username}</b></color> {text}");
+ InternalAddMessage($"\n<color=#4ECDC4><b>{player.GetUsername()}</b></color> {text}");

The rest of each script compiles against the current CCK unchanged.

Cross-reference against the sandbox validator

Section titled “Cross-reference against the sandbox validator”

If you open Script Validator and paste an unmodified TextWall.cs, it reports:

  • `Player.Username` not found (4 hits). The closest-match hint is GetUsername.
  • Informational: Game events detected: OnPlayerJoined, OnPlayerLeft, OnPlayerRespawned.

Running the patched version shows only the expected informational events.

A compact pattern to gate a GameObject or a callback behind a list of user IDs. Useful for reward-only gear, admin overlays, or dev-only debug panels.

As posted, the snippet uses the old property-style Player.UserId, which no longer compiles against the current CCK:

if (whitelist == _localPlayer.UserId) { ... }

Player.UserId was refactored to GetUserId() in Preview.27-WASM.26. Since UserId still requires the AccessUserIdentity world permission (unlike GetUsername() which was released from the gate), the world has to request that permission first.

using UnityEngine;
using WasmScripting;
using CVR;
[DefaultExecutionOrder(-100)]
public partial class Whitelist : WasmBehaviour
{
[SerializeField] private string[] _whitelist;
[SerializeField] private GameObject _rewardObject;
public bool IsWhitelisted { get; private set; }
private Player _localPlayer;
private bool _permissionRequested;
private void Start()
{
_localPlayer = LocalPlayer.PlayerObject;
// GetUserId() requires AccessUserIdentity. Request it once, then re-check on change.
if (!WorldPermissions.CurrentPermissions.AccessUserIdentity && !_permissionRequested)
{
WorldPermissions.Request(new WorldPermissions { AccessUserIdentity = true });
_permissionRequested = true;
return;
}
WhitelistCheck();
}
public void OnWorldPermissionsChanged()
{
if (WorldPermissions.CurrentPermissions.AccessUserIdentity) WhitelistCheck();
}
private void WhitelistCheck()
{
if (_localPlayer == null) { Debug.LogError("Whitelist: _localPlayer null"); return; }
if (_whitelist == null || _whitelist.Length == 0) return;
string userId = _localPlayer.GetUserId();
if (string.IsNullOrEmpty(userId)) return;
foreach (string entry in _whitelist)
{
if (entry == userId)
{
IsWhitelisted = true;
RewardPlayer();
return;
}
}
IsWhitelisted = false;
}
private void RewardPlayer()
{
if (_rewardObject != null) _rewardObject.SetActive(true);
}
}

Changes from the original:

  • UserIdGetUserId() (method form).
  • Added a one-shot WorldPermissions.Request({ AccessUserIdentity = true }) so GetUserId() returns a real value rather than redaction.
  • Added OnWorldPermissionsChanged() to re-run the check once the user approves the prompt. If they deny, the whitelist silently resolves to “not whitelisted”.
  • Added explicit null / empty guards that short-circuit without spam-logging.
public partial class AdminPanel : WasmBehaviour
{
public Whitelist whitelist;
private void Start()
{
if (whitelist != null && whitelist.IsWhitelisted)
ShowAdminUi();
}
}

Whitelist.IsWhitelisted caches the result after the initial check; no repeated binding calls per frame.

Both the Gimmicks world’s NetworkManager / TextWall (when they use GetUsername()) and the Whitelist pattern (via GetUserId()) touch identity-gated APIs. Make sure the world requests the right flag:

  • GetUsername() — no permission needed as of Preview.27-WASM.27.
  • GetUserId() — requires AccessUserIdentity.

See World Permissions for the full request/approve flow.