Skip to content

Talking to other scripts

A world is usually many scripts on many objects. There are two ways for one script to drive another.

Declare a field typed as the other script's class, grab it with GetComponent<T>(), then call its public methods and read/write its public fields directly:

public class PoolCue : NexusBehaviour
{
    public PoolStateManager state;   // a reference to another script's class

    void Start()
    {
        // Or wire it in the inspector instead of GetComponent.
        if (state == null) state = gameObject.GetComponent<PoolStateManager>();
    }

    void OnHit()
    {
        state._OnCueHit(2);              // call a public method
        int remaining = state.ballsLeft; // read a public field
        state.ballsLeft = remaining - 1; // write a public field
    }
}

And the other script just exposes public members:

public class PoolStateManager : NexusBehaviour
{
    public int ballsLeft = 15;
    public void _OnCueHit(int power) { /* ... */ }
}

Notes:

  • Leading underscores are fine. _OnCueHit, _StartGame, etc. are callable across scripts — the _ is just a naming convention, not "private".
  • Engine lifecycle methods are not callable across scripts (Start, Update, OnTriggerEnter, …) — call your own methods instead.
  • Get a reference by GetComponent<TheScript>(), GetComponentInChildren<TheScript>(), GetComponentInParent<TheScript>(), or a public field you assign in the inspector.
  • Arrays of scripts work. With a public PoolCue[] cues; field, both foreach (var cue in cues) cue._Hit(); and cues[0]._Hit(); call across to each script.
  • The plural getters don't collect script types. GetComponents<T>(), GetComponentsInChildren<T>(), and GetComponentsInParent<T>() only gather built-in Unity components — for your own script classes they return null, so .Length is 0 and a foreach over the result runs zero times, silently (no error, no warning). The foreach/index examples above work because they iterate an inspector-assigned array field, not a plural getter. To loop over several scripts, use a public PoolCue[] cues; field wired in the inspector, or grab each child individually with the singular GetComponentInChildren<T>().
  • A value read from another script is loosely typed. A cross-script member read (anything other than transform/gameObject) has compile-time type object, so using it directly in a relational (<, <=, >, >=) or arithmetic expression fails to compile with "Comparison operators require numeric operands"if (other.LocalSeat >= 0) won't build. Assign it to a local of the concrete type first: int seat = other.LocalSeat; if (seat >= 0). (== and != do compare inline for any type.) A read resolves both public fields and read-only get-only properties (public int Score { get; }) — each returns the real value.

A script reference gives you the other script's public methods and fields, and — since a script is a component — its transform and gameObject as well. So this all works:

Vector3 p = otherHand.transform.localPosition;   // reach the object's transform through the ref
otherHand.gameObject.SetActive(false);            // and its gameObject

From transform and gameObject you can get to anything else on that object, so those two are the door to the rest. What a reference won't expose is the other script's private internals — so keep whatever you call across scripts public. A wrong member name is a normal compile error you'll catch as you type; there's nothing subtle to chase here.

You cannot read another script's static or const — reach it through an instance

Each script is compiled on its own, so another script's static and const members do not exist from where you are standing. Only an instance reaches across.

// ✗ refused — MaxPoints is a const on a different script
int cap = MarkerInkParticle.MaxPoints;

// ✓ expose it as an instance member and go through a reference
MarkerInkParticle particle = gameObject.GetComponent<MarkerInkParticle>();
int cap = (int)particle.GetMaxPoints();

This is now a compile error that names the line. It did not used to be: the C# editor resolves the sibling class perfectly well, so the script looked correct, compiled, and then produced no value at all at runtime. The empty value travelled — a method declared int handed it back — and the world failed later, somewhere else, with a message about a null on a line that was completely innocent. If you have a constant two scripts both need, either duplicate it or expose it as a method on one of them.

Resolve on use, not once at startup

A reference you grab in Start() can be null and stay null for the whole session. Scripts on the same object initialize in no guaranteed order, and GetComponentInParent<T>() only returns a script that has already initialized — so an early call finds nothing, the field stays empty, and every later use silently does nothing.

Resolve lazily instead, from one small helper you call at the top of each entry point:

public class MarkerTip : NexusBehaviour
{
    MarkerInk ink;

    void EnsureRefs()
    {
        if (ink == null) ink = gameObject.GetComponentInParent<MarkerInk>();
    }

    void OnUsed()   { EnsureRefs(); if (ink != null) ink._StartStroke(); }
    void OnDropped(){ EnsureRefs(); if (ink != null) ink._EndStroke(); }
}

The same rule applies to anything the world does not have yet at startup — most commonly the local player:

// ✗ the world loads BEFORE the player spawns; an empty id captured here sticks forever
void Start() { myId = Networking.LocalPlayer.GetId(); }

// ✓ ask when you actually need it
string MyId() { return Networking.LocalPlayer.GetId(); }

It behaves differently in the SDK than in the live game

In the SDK's editor testing, scripts run immediately so there is no player yet — you will see this. In the live game your script's Start() is held until the local player exists, so the same code can appear to work there and fail in the SDK. Resolving on use is correct in both.

2. SendMessage (fire-and-forget)

If you only need to trigger something (no return value), send a message by method name to every script on an object:

otherObject.SendMessage("_StartGame");        // this object's scripts
otherObject.BroadcastMessage("_Reset");        // + all children
otherObject.SendMessageUpwards("_Scored", 1);  // + all parents

The receiver exposes a matching public method:

public void _StartGame() { /* ... */ }
public void _Scored(int points) { /* ... */ }

SendMessage never returns a value and calls every matching script on the target. Prefer a typed reference (option 1) when you need a return value, a field, or a single specific script.

Which to use

You want to… Use
Call a method and read the result typed reference — state.GetScore()
Read or write another script's field typed reference — state.ballsLeft
Just trigger an event, maybe on several scripts SendMessage / BroadcastMessage

→ For replicating a script's own state to other players, see Synced Variables.