Skip to content

Input, time & camera

Read the local player's controls, track time, and work with the camera and screen. Everything here is inherently per‑client — it's about this player's machine.

Input — keyboard & mouse

Poll input in Update. Input reads the local player only (the one whose client is running the script).

protected override void Update()
{
    if (Input.GetKeyDown("space")) Jump();          // fired the frame it goes down
    if (Input.GetKey("w")) Move(transform.forward); // held this frame

    float h = Input.GetAxis("Horizontal");          // -1..1, smoothed (WASD / stick)
    float v = Input.GetAxis("Vertical");

    if (Input.GetMouseButtonDown(0)) Fire();        // 0 left, 1 right, 2 middle
    Vector3 m = Input.mousePosition;                // pixel position of the cursor
}
Read
GetKey(name) · GetKeyDown · GetKeyUp held / pressed‑this‑frame / released
GetButton(name) · GetButtonDown · GetButtonUp named input buttons
GetAxis(name) · GetAxisRaw analog axes ("Horizontal", "Vertical", …)
GetMouseButton(i) · GetMouseButtonDown/Up mouse buttons (0/1/2)
mousePosition · anyKey · anyKeyDown cursor & "did anything happen"

Input is local & desktop‑oriented

Input reflects the player physically at the keyboard/mouse. It is desktop‑only — it does not see VR controllers. To read one control the same way on desktop and VR, use named actions (Controls.*) below — that's the recommended way. To read a VR controller's raw buttons/sticks, use VR.*.

Your project needs Input Handling set to “Both”

Input.* uses Unity's classic input class, which throws in a project set to Input System Package only — so a script that reads input dies in play mode even though it works in the game. The SDK checks this on load and offers to fix it, or run Social Scape ▸ SDK ▸ Validate Project Settings any time. Unity must be restarted for the change to take effect. Controls.* and VR.* are unaffected.

Named actions (Controls.*)

The portable way to read input: instead of polling a specific key or a specific controller button, you name an action (e.g. "Fire") once, bind it to whatever you like on each device, and read it by name — the script never cares which device the player is holding.

1 — Declare the actions. Add an Input Actions component (Add Component ▸ Social Scape ▸ Input Actions) to any object in your world. Give each action a name, then add bindings. One action can hold several bindings and any of them fires it — so "Fire" can be the left mouse button on desktop and the right trigger in VR at the same time:

Action name Example bindings (add as many as you want)
Fire <Keyboard>/f · <Mouse>/leftButton · <XRController>{RightHand}/trigger
Throttle <Keyboard>/w · <Gamepad>/rightTrigger · <XRController>{RightHand}/{Primary2DAxis}/y
Move <Keyboard>/wasd (2D Vector composite) · <XRController>{LeftHand}/thumbstick

2 — Read them by name in your script — device‑agnostic, poll in Update:

protected override void Update()
{
    if (Controls.GetButtonDown("Fire")) Shoot();          // pressed this frame (any bound device)
    if (Controls.GetButton("Boost")) ApplyBoost();        // held this frame

    float throttle = Controls.GetAxis("Throttle");        // analog: trigger 0..1, axis −1..1
    Vector2 move   = Controls.GetVector("Move");          // 2D: thumbstick / WASD composite
}
Read
Controls.GetButton(name) held this frame
Controls.GetButtonDown(name) · GetButtonUp(name) pressed / released this frame
Controls.GetAxis(name) analog value (a 2D action returns its x)
Controls.GetVector(name) 2D value (a 1D action returns (value, 0))
Controls.IsDefined(name) is a named action with this name currently active? (degrade gracefully)

Why this over Input.* / VR.*

Input.* is desktop‑only and VR.* reads a specific controller button — with a named action you write the gameplay once and bind it per device, so the same world works for a desktop player and a VR player with no if (VR.IsPresent()) branching. Names are case‑insensitive; keep them unique across your world (a duplicate name means the last one loaded wins).

On a grabbable, prefer OnUseDown / OnUseUp over polling

An SSInteractable now calls OnUseDown() and OnUseUp() on its script, so a held action needs no polling and no holder check — the callbacks only ever run on the machine whose input fired them, for the object that received the press:

public void OnUseDown() => StartDraw();
public void OnUseUp()   => Release();

Reach for Controls.* when the input is not tied to holding an object (a world-wide hotkey, a vehicle control), or when you need the analog value while held — and then mind this:

Driving a HELD object? Check the holder is you

Interactable.IsHeld() is true when anyone holds the object, but Controls.* reads this player's devices. So the obvious pairing is wrong:

if (Interactable.IsHeld() && Controls.GetButton("Draw"))   // ❌ on a bystander's client, THEIR
    Draw();                                                //    trigger draws someone else's bow

Gate on the holder actually being the local player — Interactable.GetHolder() returns the holder's account id, and Player.IsLocal(id) answers the question. The bug is invisible in single-player testing and obvious the moment two people are in the room, which is the worst combination to ship with.

Reads are local — never the source of networked truth

Like Input.* and VR.*, a Controls.* read reflects this player's own devices; the server and other players read it as zero. Use it to drive this player's local feel; to make something everyone sees, turn the read into authoritative state — a [Synced] var, a World.Set*, or an RPC. See Synced variables.

VR — controllers

Read the local player's VR controller buttons, triggers and thumbsticks. hand is 0 (left) or 1 (right) — you can also pass "left"/"right". Poll in Update.

protected override void Update()
{
    if (!VR.IsPresent()) return;                    // desktop / no headset → reads are neutral anyway

    bool grab    = VR.GetButton(1, "grip");         // right grip held this frame
    float pull   = VR.GetAxis(1, "trigger");        // right trigger, 0..1 analog
    Vector2 move = VR.GetStick(0);                  // left thumbstick, x/y each −1..1

    if (VR.GetButton(0, "primary")) Jump();         // left X / A
}

The names are logical, not physical — the XR runtime maps them to the same control on every supported headset, so "primary" is the lower face button on both a Quest and an Index controller. You never branch on the headset model.

Buttons — VR.GetButton(hand, name)bool

Name Quest / Rift‑style Index What it is
"trigger" index trigger (click) trigger (click) the trigger, as a button
"grip" grip button grip (squeeze) the side grip
"primary" X (left) / A (right) A lower face button
"secondary" Y (left) / B (right) B upper face button
"thumbstickClick" click the stick in click the stick in thumbstick press
"menu" menu / ☰ menu the application‑menu button

Axes — VR.GetAxis(hand, name)float

Name Range
"trigger" 0..1 how far the trigger is pulled
"grip" 0..1 how hard the grip is squeezed (0 or 1 on controllers with a digital grip)
"thumbstickX" −1..1 thumbstick left/right
"thumbstickY" −1..1 thumbstick up/down

Thumbstick — VR.GetStick(hand)Vector2

VR.GetStick(hand) returns both axes at once (x left/right, y up/down, each −1..1) — the same values as GetAxis(hand, "thumbstickX"/"thumbstickY"), convenient for locomotion:

Vector2 s = VR.GetStick(0);
transform.position += (transform.right * s.x + transform.forward * s.y) * speed * Time.deltaTime;

Cross‑controller: what's guaranteed, and the fallbacks

The catalog above is the guaranteed set on modern controllers — Quest (all), Index, Rift, WMR, and other stick‑based controllers. Older wand‑style controllers (e.g. the original Vive wand) physically have no A/B/X/Y face buttons and no thumbstick — they use a trackpad and a menu/grip. On those, "primary"/"secondary"/"thumbstickClick" and the stick read as false/0, while "trigger", "grip" and "menu" still work. If your world must run everywhere, key core actions off trigger/grip and treat the face buttons and stick as enhancements — or gate them behind a world interaction that works for all input styles.

VR reads are local & cosmetic — never the source of networked truth

Like the other VR.* and Player.* VR reads, controller input is client‑local: the server and other players read it as zero. Use it to drive this player's local feel (locomotion, aiming, a held tool). To make something everyone sees, turn the local read into authoritative state — a [Synced] var, a World.Set*, or an RPC. See Synced variables.

Time — timing & frame‑rate independence

Multiply per‑frame motion by Time.deltaTime so it runs the same on every machine regardless of frame rate.

transform.position += transform.forward * speed * Time.deltaTime;   // metres per second
Time.deltaTime seconds since last frame — multiply movement by this
Time.fixedDeltaTime physics step length (use in FixedUpdate) — 0.02 s (50 Hz) platform‑wide, identical on every client and the game server, never changed at runtime
Time.time seconds since the world started
Time.unscaledTime / unscaledDeltaTime ignores timeScale (UI/menus)
Time.timeScale global speed (1 normal, 0 paused, 0.5 slow‑mo)
Time.frameCount · realtimeSinceStartup frame index / wall‑clock

No lockstep / deterministic physics across clients

The 0.02 s step is the same everywhere, but physics is not run in lockstep and is not deterministic across clients — don't assume two machines simulate identical results. Sync the authoritative state ([Synced] vars / World.Set* / RPCs) instead of expecting peers to converge on their own. Note too that an Update() accumulator loses simulated time under a frame hitch (a long frame's deltaTime is capped), whereas FixedUpdate catches up — count physics steps in FixedUpdate, not accumulated Update deltas.

Stopwatch measures elapsed real time precisely (mini‑games, timers):

Stopwatch sw = Stopwatch.StartNew();
// ... do something ...
long ms = sw.ElapsedMilliseconds;
sw.Restart();

Delays — use a coroutine, not a busy loop

To wait, yield return new WaitForSeconds(2f); inside a coroutine — never spin in a while loop counting Time.time (it blocks the frame and the VM will throttle/kill it). See Basics.

Camera — the player's viewpoint

Camera.main is the local player's camera. Convert between world and screen space and cast rays from the cursor:

Camera cam = Camera.main;

// Click-to-select: ray from the cursor into the world
Ray ray = cam.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray.origin, ray.direction, out hit, 100f))
    Debug.Log("Clicked " + hit.collider.gameObject.name);

// Place a UI marker over a world object
Vector3 screen = cam.WorldToScreenPoint(target.position);
bool onScreen = screen.z > 0;
Camera.main the active camera
ScreenPointToRay(px) · ViewportPointToRay ray from screen/viewport point
ScreenToWorldPoint · WorldToScreenPoint convert spaces
WorldToViewportPoint · ViewportToWorldPoint 0..1 viewport coords
fieldOfView · nearClipPlane · farClipPlane · orthographic lens settings

Don't fight the player's camera

The VR/desktop rig owns the camera. Read from it freely (raycasts, projection); avoid moving it or changing FOV during play unless your world genuinely needs a custom view — it can cause discomfort in VR.

Getting a handle to a camera you placed

Camera.main returns only the player's view. To drive a camera you added to the world (e.g. adjust orthographicSize), declare a public Camera field on your script and assign it in the Inspector — it arrives as a live handle with the full instance API. GetComponent<Camera>() is blocked by design and returns null, and Camera.allCameras includes the system Main Camera unfiltered — so use the serialized field rather than either of those.

Screen & Application

  • Screenwidth, height (pixels), dpi, fullScreen. Use width/height to position UI or check aspect ratio.
  • ApplicationisFocused, isPlaying, platform. Check platform to branch desktop vs VR behaviour.

Quick reference

Group Calls
Named actions (desktop+VR, local) Controls.GetButton/GetButtonDown/GetButtonUp(name) · Controls.GetAxis(name) · Controls.GetVector(name) · Controls.IsDefined(name) — declared on a Social Scape ▸ Input Actions component
Keys Input.GetKey/Down/Up · GetButton/Down/Up · GetAxis · GetAxisRaw
Mouse Input.GetMouseButton/Down/Up · mousePosition · anyKey · anyKeyDown
VR controllers (raw, local) VR.GetButton(hand, name) · VR.GetAxis(hand, name) · VR.GetStick(hand) — names: trigger·grip·primary·secondary·thumbstickClick·menu
Time Time.deltaTime · fixedDeltaTime · time · timeScale · unscaledTime · frameCount
Stopwatch StartNew · ElapsedMilliseconds · Restart · Stop · Reset
Camera Camera.main · ScreenPointToRay · WorldToScreenPoint · ScreenToWorldPoint · fieldOfView
Screen/App Screen.width/height/dpi · Application.platform/isFocused

→ Feeds naturally into Physics (cursor raycasts) and UI (screen‑space placement).

Keys the platform already uses

Your bindings share the keyboard with the platform. Nothing stops you binding these, but the platform's action fires too — so a player pressing your key may also open a menu or wave. Avoid them, or accept the double meaning deliberately:

Keys Platform action
Esc Menu / overlay / cancel
Y, Enter Open chat / send chat
R Radial (expressions/actions) menu
V Push‑to‑talk
P, F, Ctrl+F1 Camera summon · shutter (and F is also the default seat exit) · screenshot
F1F8 (+Shift) Hand gestures
C, Z Crouch / prone
J, K, L Combat action · ragdoll · recover
Left Alt Free the cursor (click world UI precisely)
WASD, Space, Shift, Q, E Locomotion (and vehicle defaults while seated)
Mouse L/R/M + wheel Use · grab/throw · rotate held · carry distance

While a player is typing (chat or any world text field), your named actions read their rest value and key triggers hold their fire — the platform gates them for you, so you never have to special-case "the player was chatting."