Skip to content

Gotchas

Most C# you write here just works. This page is the short list of places it doesn't — the things that cost real time if you walk into them blind. If you're porting an existing world, read this before you start; it's mostly the stuff that generates confusing errors an hour in.

Cross-script references are loose

A reference to another script behaves like a normal component reference — you can call its public methods and fields, and reach the object's transform and gameObject right through it (otherHand.transform.localPosition works). The one rule to remember: only public members cross the boundary, so keep whatever you call from another script public. Bad member names are ordinary compile errors, not silent failures. Full story on Talking to other scripts.

Lock a grabbable with the interactable, not the collider

When you want to stop something being picked up — a cue that's already in someone's hands, a door that's sealed for now — use the interactable's own lock:

Interactable.SetLocked(true);     // blocks grabbing; stays solid; every player sees it
Interactable.SetLocked(false);
Interactable.ForceDrop();          // make the current holder let go

Disabling the object's collider is the obvious-looking shortcut and it half-works, but it also switches off every other collision the object has, and it isn't networked — so other players won't be in the same state you are. SetLocked is the thing that's actually meant for this, and it replicates.

A documented call that seems to do nothing

NexusVM has no reflection fallback: an unrecognised call logs Unknown bridge function and returns null — no compile error, no exception, just silence. Check the console for that line first.

  • No such line? It was bound and it ran; the bug is in your surrounding logic.
  • Line present? That is a missing binding — a bug on our side. Report it rather than redesigning around it: a shipped world is sandboxed, so any workaround needing first-party code cannot ship anyway.
  • Neither, but nothing happens? You are probably on the wrong side. Input.*, Controls.* and Screen.* are local to one player; HTTP.*, AI.* and Player.GetUsername are server-only and no-op in editor play mode.

The networking attributes need using SocialScape.SDK;

NexusBehaviour is in NexusVM.Unity, but [Synced], [ServerRpc] and [ClientRpc] are in SocialScape.SDK. A script with only the first using builds fine — right up until you add an attribute, and then it fails with CS0246: The type or namespace name 'SyncedAttribute' could not be found. It reads like a broken SDK and it is a missing using. Add both; see Networking.

If it still fails after adding the using, the script is probably inside a folder with its own .asmdef (third-party assets ship these routinely). That puts it in a separate assembly where auto-referencing does not apply — add NexusVM.Unity.Runtime and SocialScape.SDK to that asmdef's references, or move the script to a folder with no .asmdef above it.

Networking.LocalPlayer is a struct — test it with IsValid(), never != null

PlayerAPI is a struct, so if (Networking.LocalPlayer != null) does not compile. Use IsValid():

var me = Networking.LocalPlayer;
if (!me.IsValid()) return;        // ✅ no player yet (offline tester, or before spawn)
// if (me != null)                // ❌ does not compile — it is a struct

And do NOT guard on the id being empty — it never is

With no player, GetPlayerId() returns an all-zeros GUID (00000000-0000-0000-0000-000000000000) — not null, not "". So the obvious guard silently passes:

string id = Networking.LocalPlayer.GetPlayerId();
if (id == "") return;                       // ❌ never true — falls through with a bogus id
Player.GetHeadPosition(id);                 //    … which then reads as a valid lookup

Gate on IsValid() once and keep the result, rather than testing the id afterwards. This bites hardest in the offline tester, where there is no networked player at all, so every id you fetch is the zero GUID and every downstream read quietly returns a default instead of failing.

[Synced] fields must be public

A synced variable is public by nature — it's read on every other player's machine. Declare one private and it won't compile. If you'd rather nothing else wrote to it, just leave it public and don't write to it from elsewhere; there's no private synced variable.

Numbers behave, with two small exceptions

The full numeric surface works the way you'd expect — every cast, uint/ulong unsigned math, int.MaxValue / float.NaN constants, int.TryParse(s, out n), Convert.ToInt32(...), compound *=/+= (on numbers and on Color/Vector3). See Numbers, casts & parsing for the details. A few edges to know:

  • char prints as its number. $"{someChar}" shows the character code ("65"), not the letter — treat char as a number.
  • decimal is a double. It compiles but carries no extra precision.

Some things only run live

The in-editor test player covers a lot — triggers, interactables, motion, your own logic all run — but anything that needs other people has nobody to talk to offline, so it quietly no-ops. That's Player.* reads of other players, voice, avatars, synced variables actually replicating, and RPCs. It's expected, not a bug; test those in a real session. Each API's page notes which calls are live-only.

Things that do work like normal C# (so just use them)

Worth saying, because the sandbox is less strict than people assume and second-guessing it wastes time:

  • Several declarations on one line — float h, s, v; — is fine.
  • if (gameObject) / if (!target) truthiness works on objects (it means "not null"). Numbers, strings and Vector3 still need a real comparison — if (count > 0), not if (count).
  • TextMeshPro works, not just legacy Text — type a field TextMeshProUGUI (or TMP_Text) and set .text / .fontSize / .color.
  • MeshFilter.mesh swaps a mesh at runtime; transform.eulerAngles, Color.HSVToRGB / RGBToHSV, Image.fillAmount, and Collider.enabled are all there.
  • foreach (var x in someArray) infers the element type — even for an array of another script, so foreach (var cue in cues) over a PoolCue[] lets you call cue._Method() on each one.

When you're unsure, write it the normal Unity way first. Most of the time it compiles, and the API reference that ships with the SDK settles the rest.