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¶
Screen—width,height(pixels),dpi,fullScreen. Usewidth/heightto position UI or check aspect ratio.Application—isFocused,isPlaying,platform. Checkplatformto 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 |
F1–F8 (+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."