Skip to content

Script API reference

Auto-generated from packages/shared/src/scripting/ScriptAPI.ts. Do not edit by hand — update the JSDoc in that file and run pnpm --filter @galatrix/docs docs:gen. The editor’s script autocomplete is fed from the same file, so this page always matches it.

Scripts attach to entities and run on the authoritative server. You implement lifecycle hooks as top-level functions; the globals below (entity, game, players, …) are always available inside your script. See Scripting basics for a walk-through.

Lifecycle hooks

Implement any of these as top-level functions in your script.

HookCalled
function onAwake(): voidCalled once before any onStart, when the entity is initialized. Set up internal state here.
function onStart(): voidCalled once when the entity is first loaded (after every entity's onAwake).
function onUpdate(dt: number): voidCalled once per rendered FRAME (Update) — so it runs at the display's rate and dt varies. The place for cameras, input, UI and animation. Do NOT apply physics forces here: physics runs on a fixed step, so a force applied per-frame is multiplied by however many frames fall in one step (~3x at 60fps, ~7x at 144fps). Use onFixedUpdate for those.
function onFixedUpdate(dt: number): voidCalled exactly once per PHYSICS step, immediately before it runs (FixedUpdate). dt is always Time.fixedDeltaTime, whatever the framerate. This is where forces, impulses and velocity changes belong — one call, one step, so what you ask for is what gets integrated, identically in the editor, a standalone build and on the multiplayer server. Read HELD input here (input.getKey); per-frame edges (getKeyDown/getKeyUp) can be missed or double-seen by a fixed step — handle those in onUpdate (the same caveat applies).
function onEnable(): voidCalled when this entity becomes active (enabled).
function onDisable(): voidCalled when this entity becomes inactive (disabled).
function onDestroy(): voidCalled when this entity is about to be destroyed.
function onCollisionEnter(other: EntityRef): voidCalled once when this solid collider first touches another entity's solid collider (OnCollisionEnter).
function onCollisionStay(other: EntityRef): voidCalled every physics step while two solid colliders stay in contact (OnCollisionStay).
function onCollisionExit(other: EntityRef): voidCalled when two solid colliders separate (OnCollisionExit).
function onCollision(other: EntityRef): void
function onTriggerEnter(other: EntityRef): voidCalled when another entity enters this entity's trigger (a collider with isTrigger). OnTriggerEnter.
function onTriggerStay(other: EntityRef): voidCalled every physics step while another entity stays inside this trigger (OnTriggerStay).
function onTriggerExit(other: EntityRef): voidCalled when another entity exits this entity's trigger (OnTriggerExit).
function onPlayerEnter(player: PlayerRef): voidCalled when a player enters this entity's trigger zone.
function onPlayerLeave(player: PlayerRef): voidCalled when a player leaves this entity's trigger zone.
function onInteract(player: PlayerRef): voidCalled when a player presses the USE key (E / gamepad A) while standing inside this entity's trigger zone — a Roblox ProximityPrompt-style interaction. Fires with the specific player who pressed it, so it works per-player in multiplayer (the server delivers each player's press). Put an isTrigger collider on the object to define the activation zone.
function onDeath(): voidCalled when this entity's health reaches 0 (via game.damageEntity), just before it's removed (if destroyOnDeath). Use it to drop loot, play an effect, award score, etc.
function onPlayerRespawn(player: PlayerRef): voidCalled on EVERY scripted entity when a player dies and respawns (lava, a fall, an enemy, or players.kill). Use it to reset gameplay state to the level's starting condition — e.g. warp an NPC back to its spawn, re-open a door, reset a timer.
function onPlayerJoined(player: PlayerRef): voidCalled on EVERY scripted entity when a player joins the room — the place to configure a player for THIS map: lock their camera, hand out starter tools, set their speed or permissions. players.*, tools.give, and camera.forPlayer(player.id) are the usual verbs: onPlayerJoined(player) { this.tools.give(player.id, 'pickaxe-tool') this.camera.forPlayer(player.id).setZoomRange(0, 0) // first-person only this.players.setSpeed(player.id, 9) } In the editor play-test and a standalone build it fires once for the local player right after every onStart, so a map configured this way behaves identically before it is ever published. In multiplayer it fires on the server for each arriving player, AFTER they have a spawn position.
function onPlayerDataReady(player: PlayerRef): voidThat player's saved data has loaded (or failed open — the store reads empty). Server scripts only. Exactly once per join, always after onPlayerJoined; the place for data.get reads at join. A signed-out player's save lives in their own browser, so this waits for that to come back too — every player reaches this hook with whatever save they have.
function onPlayerLeft(player: PlayerRef): voidCalled on EVERY scripted entity when a player leaves the room, while their position and per-player data are still readable — save scores, drop their items, clean up their team slot. Not fired for the local player in the editor play-test (stopping the preview is not "leaving").
function onPlayerPropertyChanged(player: PlayerRef, key: string, value: unknown): voidA player's replicated property changed (players.setProperty). Fires on every scripted entity, in server scripts and in every client's scripts alike — the one hook that lets a client react to a fact about ANY player the moment the room states it (a skin change, a score, a ready flag). value is the new value, or undefined when the key was removed. Delivered before the next onUpdate, after the write has landed, so players.getProperty already answers the new value.
function onRoomPropertyChanged(key: string, value: unknown): voidA room property changed (room.setProperty) — same delivery and runtimes as onPlayerPropertyChanged.
function onPlayerKeyDown(player: PlayerRef, key: string): voidCalled on EVERY scripted entity when a player presses one of the keys the map asked for with input.watchKeys([...]). key is the canonical lowercase name ('r', 'space', 'arrowup'). This is the multiplayer answer to input.getKeyDown, which reads the machine the script runs on and therefore reads nothing in a room. Here the press arrives with the player who made it, so a key can drive per-player gameplay: onStart() { this.input.watchKeys(['r']) } onPlayerKeyDown(player, key) { if (key === 'r') this.reload(player.id) } Fires for the local player in the play-test and standalone, and for each player in multiplayer. Only watched keys are ever delivered.
function onPlayerKeyUp(player: PlayerRef, key: string): voidThe release half of onPlayerKeyDown — same rules, fired when a watched key comes back up. Use it for hold-to-charge, hold-to-aim, or anything that has to end when the player lets go. A player who disconnects mid-hold never sends the release, so treat leaving as a release too (onPlayerLeft).
function onPlayerJump(player: PlayerRef): voidCalled on EVERY scripted entity when a player jumps, once the map has asked for it with input.watchJump(). Fires on the jump itself — feet leaving the ground — not on the button: onStart() { this.input.watchJump() } onPlayerJump(player) { this.audio.play('woosh', player.position) } A press that produces no jump (already in the air, swimming, key still held from the last one) does not fire, and a jump from the touch button or a double tap does. Fires for the local player in the play-test and standalone, and for each player in multiplayer.
function onPlayerDied(player: PlayerRef, respawnIn: number): voidCalled on EVERY scripted entity when a player DIES — health reached zero, a kill zone, the kill plane, or players.kill(). This is the moment the respawn countdown starts; respawnIn is how many seconds until they are back (the map's respawnSeconds). Fires for server scripts in every runtime, and for every player's death in client scripts too, so a death overlay or an elimination feed is ordinary map code: onPlayerDied(player, respawnIn) { this.ui.showMessage(player.username + ' was eliminated') } The Died half of the pair — onPlayerRespawn fires when the countdown ends.
function onPlayerChat(player: PlayerRef, text: string): boolean | voidA player typed a line in the room's chat, on EVERY server-scripted entity, before the line is sent to anyone. Return true to keep it: the line is then a COMMAND for the script and nobody else sees it — the usual /home, /spawn, /sethome. Return nothing (or false) and the line goes out as chat. The first script to return true wins; the room's own rules (mutes, the rate limit) run first, so a muted player cannot command either. A GUEST cannot chat, but a line starting with / still reaches the scripts, with player.guest true, so guests can use commands; one no script keeps is refused as chat. Keep a command for accounts by checking player.guest: onPlayerChat(player, text) { if (text === '/spawn') { this.players.teleport(player.id, this.entity.position); return true } if (text === '/sethome' && player.guest) { this.ui.showMessage('Sign in to set a home.', player.id); return true } }
function onMessage(key: string, data: unknown): voidCalled when another script sends a message to this entity via events.send().
function onToolEquipped(player: PlayerRef): voidCalled when a player equips this tool entity.
function onToolUnequipped(player: PlayerRef): voidCalled when a player unequips this tool entity.
function onToolActivated(player: PlayerRef): voidCalled when a player activates (left-click) this tool entity.
function onToolDeactivated(player: PlayerRef): voidCalled when a player deactivates (releases click) this tool entity.
function onClientEvent(player: PlayerRef, key: string, data: unknown): voidCalled on EVERY scripted entity in a SERVER script when a client script sends events.toServer(key, data). player is the player whose machine sent it, stamped by the server from their session — a client cannot claim to be someone else. data crossed a network: treat it as untrusted input and validate before acting on it (range-check positions, verify the player can afford the purchase, …). onClientEvent(player, key, data) { if (key === 'fire') this.resolveShot(player.id, data.aim) } Arrives the tick after it was sent, in every runtime — the editor play-test queues it locally so the ordering a map is written against is the ordering it ships with.
function onClientInvoke(player: PlayerRef, key: string, data: unknown): unknownCalled in SERVER scripts to ANSWER a client script's events.invokeServer(key, data) — the request-response pair the fire-and-forget onClientEvent cannot express. A dedicated hook rather than "an onClientEvent that returns something" because events fan out to every handler while an invoke has exactly ONE answer: scripts are asked in dispatch order, and the first handler that returns a value other than undefined answers the request (return undefined to pass on keys that are not yours). The return value becomes what the client's promise resolves with — it must survive JSON and stay under the 16KB cap. Returning a Promise is supported: the reply is sent when it settles. onClientInvoke(player, key, data) { if (key === 'quote') return this.priceFor(player.id, data.item) } player is stamped from the sender's session and data is untrusted, exactly as in onClientEvent. Throwing rejects the caller's promise with the thrown message.
function onServerEvent(key: string, data: unknown): voidCalled on EVERY scripted entity in a CLIENT script when a server script sends events.toClient to this player (or to 'all'). The reply direction of onClientEvent, and the server's way to raise something on one player's screen that their own client scripts render: onServerEvent(key, data) { if (key === 'hitmarker') this.showHitmarker() } Same next-tick delivery as onClientEvent.

entity

MemberDescription
entity.id: stringThis entity's ID. (read-only)
entity.name: stringThis entity's name. (read-only)
entity.tag: stringThis entity's tag — settable at runtime: entity.tag = 'Enemy' or setTag('Enemy').
entity.position: [number, number, number]Get/set position.
entity.rotation: [number, number, number, number]Get/set rotation. READS return a quaternion [x, y, z, w]. WRITES accept a quaternion (re-normalized if its length drifted) — or 3 values ([x, y, z] / Vector3 / {x, y, z}), which always mean Euler angles in DEGREES (what the terminal get and the Inspector show). A 3-value write used to be taken as a broken quaternion that visibly scaled/skewed the mesh.
entity.eulerAngles: [number, number, number]Get/set the rotation as Euler angles in DEGREES (transform.eulerAngles).
entity.scale: [number, number, number]Get/set scale.
entity.color: stringGet/set color hex string.
entity.visible: booleanGet/set visibility.
entity.active: booleanGet/set active state. Inactive entities are hidden and skip physics/scripts.
entity.parent: ScriptEntityAPI | nullParent transform (live handle), or null if this is a root. Use it directly, e.g. this.transform.parent.position. For the raw id use parentId. (read-only)
entity.parentId: string | nullParent entity ID, or null if this is a root. (read-only)
entity.root: ScriptEntityAPITopmost ancestor transform (live handle); returns this entity if it has no parent. (read-only)
entity.layer: numberGet/set layer number (0-31).
entity.debugString: stringGet/set a free-form debug string shown above this entity when the editor's Debug Info overlay is enabled (Editor Settings → Debug Info). Use it to surface AI state, counters, etc. Example: this.entity.debugString = 'state: ' + this.state. Set to '' to clear.
entity.forward: [number, number, number]Forward direction (unit vector, world space, -Z). Read-only. (read-only)
entity.right: [number, number, number]Right direction (unit vector, world space, +X). Read-only. (read-only)
entity.up: [number, number, number]Up direction (unit vector, world space, +Y). Read-only. (read-only)
entity.translate(offset: [number, number, number]): voidMove entity by offset [dx, dy, dz] in world space.
entity.rotate(euler: [number, number, number]): voidRotate entity by euler angles [rx, ry, rz] in degrees.
entity.lookAt(target: [number, number, number]): voidPoint this entity toward a world position.
entity.distanceTo(point: [number, number, number]): numberDistance from this entity to a point.
entity.getComponent(type: string): any | nullGet a component by type name — a write-through handle to the live DATA component (collider/rigidBody/…), OR, if type is a SCRIPT class name, that script's LIVE instance (GetComponent<MyScript>()). null if neither. So enemy.getComponent('Health').takeDamage(5) works cross-entity. Special case: getComponent('NavMeshAgent') returns an agent-shaped NavMeshAgent handle — a.setDestination(pos), a.speed = 5, a.isStopped = true, a.warp(pos), a.destination, a.remainingDistance — the instance-shaped form of the global agent API. Special case: getComponent('rigidBody') additionally carries the LIVE simulation members that also live on Rigidbody — rb.velocity / rb.linearVelocity (get+set), rb.angularVelocity (rad/s), rb.AddForce(f, ForceMode.Impulse), rb.AddRelativeForce, rb.AddExplosionForce, rb.AddTorque, rb.GetPointVelocity(p), rb.friction, rb.ccd, rb.position/MovePosition, rb.Sleep()/WakeUp()/IsSleeping(). These are the physics.* verbs with this entity's id already bound; physics.getVelocity(id) still works unchanged. ⚠ rigidBody fields baked into the body at build time (mass, damping, gravityScale, freeze*, type…) THROW on write rather than silently not applying — see RigidBodyHandle.
entity.getScript(className: string): any | nullGet another script's LIVE instance on this entity by class name (GetComponent<MyScript>()) — read/ write its fields, call its methods. Same as getComponent(className); this is the explicit name.
entity.addComponent(type: string, data: any): voidAdd a component to this entity. Overwrites if already present.
entity.setComponent(type: string, patch: any): voidEdit an existing component: merges the patch onto its current fields (creates it if absent). e.g. setComponent('followTarget', { target: enemy.id }) — leaves offset/smoothing untouched.
entity.removeComponent(type: string): voidRemove a component from this entity (cannot remove 'transform').
entity.hasComponent(type: string): booleanTrue if this entity has the named DATA component, or a script of that class.
entity.setParent(parentId: string | null): voidSet (or clear with null) this entity's parent in the hierarchy.
entity.compareTag(tag: string): booleanCompare this entity's tag with a string.
entity.setTag(tag: string): voidSet this entity's tag at runtime (same as entity.tag = …).
entity.getChildren(): EntityRef[]Get references to all child entities.
entity.bodyType: 'static' | 'dynamic' | 'kinematic'Physics body type (read-only). (read-only)
entity.GetComponent(type: string): any | nullPascalCase alias for getComponent.
entity.AddComponent(type: string, data: any): voidPascalCase alias for addComponent.
entity.TryGetComponent(type: string): booleanTrue if the named component is present.
entity.SetActive(active: boolean): voidPascalCase alias for active = … (enable/disable).
entity.CompareTag(tag: string): booleanPascalCase alias for compareTag.

game

MemberDescription
game.getTime(): numberTime since scene started (seconds).
game.getRoomTime(): numberThe ROOM's clock, in seconds — the same number on the server and on every player's machine. getTime() is each runtime's own scene time: it starts when THAT machine loaded the map, so two players who joined a minute apart read numbers a minute apart. This one is the room's, so it is the clock to use for anything every player must agree about. What that buys: motion expressed as a FUNCTION OF TIME instead of an accumulation. A platform whose position is f(roomTime) is in the same place on every screen without a single byte being sent for it, and cannot drift — where offset += speed * dt accumulates a different error on every machine and needs correcting forever. // a slide every client agrees on, replicated by nobody const t = game.getRoomTime() const phase = (t * speed + seed) % (2 * span) entity.position = [base[0] + (phase < span ? phase : 2 * span - phase) - span / 2, base[1], base[2]] A client reads it from the room's own tick, extrapolated locally between ticks, so it advances smoothly rather than stepping 20 times a second. It trails the server by roughly half the round trip — a constant offset per player, not a growing one, which is what makes it usable for motion. In the editor play-test and a standalone build there is one process and no room, so this is scene time and matches getTime() exactly.
game.getDeltaTime(): numberDelta time for this tick (seconds).
game.findEntity(name: string): EntityRef | nullFind an entity by exact name. Returns null if not found.
game.findEntityById(id: string): EntityRef | nullFind an entity by ID. Returns null if not found.
game.findEntities(namePattern?: string): EntityRef[]Find every entity matching a name PATTERN: 'Fan' exact, 'Fan*' prefix (also FanWind, FanVis), 'Wind' suffix, 'an' contains. '%' works as the wildcard too. No argument, '' or '' = everything.
game.findEntitiesByTag(tag: string): EntityRef[]Find all entities with a given tag. Returns [] if no tag is given — use findEntities() for everything.
game.findEntityWithTag(tag: string): EntityRef | nullFind the first entity with a given tag. Returns null if not found.
game.spawnEntity(name: string, geometryType: string, position?: [number, number, number] | SpawnOptions, options?: SpawnOptions): EntityRefSpawn a bare primitive and return a live handle. The order is NAME, then KIND, then position. KIND is one of box, sphere, cylinder, capsule, plane, cone, torus, wedge (case-insensitive). An unknown kind — or arguments in the wrong order — throws and says which, rather than silently giving you a box. The position may be omitted to spawn at the origin, and accepts {x, y, z} as well as [x, y, z]. The result is solid by default — it spawns with a static rigidBody, so its mesh provides a collider — which is what scenery wants and exactly wrong for a pickup or a marker the player should walk through. Pass { solid: false } for those; options may go third if you are spawning at the origin. js game.spawnEntity('Crate', 'box', [0, 1, 0]) // solid game.spawnEntity('Coin', 'sphere', [0, 1, 0], { solid: false }) // walk through it Either way it carries no scripts: use instantiate with a prefab for anything that needs behaviour of its own. A room holds at most 10,000 runtime-spawned entities at once, counting instantiate. Past that a spawn is refused with a Debug warning and this returns a handle to nothing. Every spawn is replicated and replayed to every later joiner, so a room at that count has a leak: destroy what is no longer needed.
game.openURL(url: string): booleanOpen a web page in a new tab (also reachable as Application.OpenURL). Returns true if it was opened. STANDALONE BUILDS ONLY. In the editor play-test and in a game published on Galatrix this does nothing and returns false — a game hosted alongside other people's games must not be able to navigate a player somewhere on its own. In a standalone export the creator owns the page, so the call is theirs to make. The editor logs why it did nothing, so a creator testing a standalone feature is not left guessing. Only http, https and mailto are accepted; anything else (notably javascript:) is refused. Opens with noopener, so the new tab cannot reach back into the game. Browsers block window.open outside a user gesture, so call it from something the player did — a HUD button onClick, onInteract, a collision — not from onUpdate. A blocked popup returns false.
game.destroyEntity(id: string): voidDestroy an entity by ID.
game.dontDestroyOnLoad(target: string | { id: string }): booleanKeep an entity (and everything under it) alive across game.loadScene. Its scripts keep running with their state intact: onAwake/onStart do NOT run again in the new map. Root objects only — pass a child and it warns and does nothing, because a child's lifetime belongs to its parent. Mark the top-level parent and the whole subtree comes along. Returns true when the entity was marked. The mark lasts until the entity is destroyed or the game ends, so it carries across any number of later loadScene calls. Reloading the same map does not duplicate the survivor: it keeps its id and the incoming copy is skipped. onStart() { if (game.findEntities('MusicPlayer').length > 1) { game.destroyEntity(this.entity.id); return } game.dontDestroyOnLoad(this.entity.id) }
game.setActive(entityId: string, active: boolean): voidEnable or disable an entity. Inactive entities are hidden and skip physics/scripts.
game.damageEntity(entityId: string, amount: number): voidDamage an entity that has a health component. Fires its onDeath() at 0 HP and removes it (unless health.destroyOnDeath is false). No-op if the entity has no health component.
game.healEntity(entityId: string, amount: number): voidHeal an entity with a health component, clamped to its max.
game.getEntityHealth(entityId: string): { current: number; max: number } | nullCurrent + max HP of an entity with a health component, or null if it has none.
game.getComponent(entityId: string, type: string): any | nullGet a component from any entity by ID and type. Returns a live handle, or null if it has none. COLLIDERS ARE NOT ALWAYS COMPONENTS. An entity with a mesh and a rigidBody is solid WITHOUT a collider component — the runtime infers one from the mesh (see rigidBody.autoCollider). game.spawnEntity gives you exactly that pair, so a freshly spawned box is already solid while getComponent(id, 'collider') returns null and hasComponent(id, 'collider') reads false. Neither is a sign that the thing is not solid; they answer "is there a collider COMPONENT", which is a different question. Add one only to override the inferred shape — and note that adding one then REPLACES the auto-collider, so a collider smaller than the mesh makes the entity less solid than it was.
game.addComponent(entityId: string, type: string, data: any): voidAdd a component to any entity. Overwrites if already present.
game.setComponent(entityId: string, type: string, patch: any): voidEdit an existing component on any entity: merges the patch onto its current fields.
game.removeComponent(entityId: string, type: string): voidRemove a component from any entity (cannot remove 'transform').
game.hasComponent(entityId: string, type: string): booleanCheck if any entity has a component. Careful with 'collider': a mesh + rigidBody is solid with no collider component at all, so false here does NOT mean "not solid" — see getComponent.
game.getScript(entityId: string, className: string): any | nullGet another entity's script instance by class name (GetComponent<T>()). Returns null if not found.
game.getEntity(entityId: string): ScriptEntityAPI | nullGet a LIVE handle to any entity by id — read/write its transform, components, etc. Null if not found.
game.instantiate(prefabName: string, position?: [number, number, number], rotation?: [number, number, number, number] | [number, number, number]): EntityRef | nullSpawn a saved prefab by name at an optional position and rotation. Returns the root entity, or null when there is no prefab by that name. rotation REPLACES the root's authored rotation (children keep their parent-local poses, or the tree would twist apart). Three numbers are Euler DEGREES, four a quaternion — the same rule as every other rotation write. Applied before the entity is built, so mesh AND collider start at the final pose.
game.getAssetId(nameOrId: string, kind?: string): string | nullThe id of a project asset, found by its NAME — for the component slots that take an asset id (meshRenderer.customTextureAssetId, audioSource.audioAssetId, …). Write names in your scripts, not ids. An asset id belongs to the project it was created in: publish your game as an asset pack, import it somewhere else, and every id changes, so an id pasted into a script points at nothing there. A name survives the trip. const skin = game.getAssetId('Skin_Gold', 'texture') if (skin) this.entity.setComponent('meshRenderer', { customTextureAssetId: skin }) Pass kind whenever a name might exist in more than one kind ('texture', 'audio', 'model', 'prefab', 'material', 'shader', 'animationClip', 'font', 'blockGrid', 'data', 'uiCanvas', 'sceneTemplate', 'script', 'config'). Without it, every kind is searched in that order. An id passed in comes back unchanged, so it is safe to wrap a value that may already be one. Returns null when nothing matches. Duplicate names resolve to the NEWEST asset — see getAssetIds when you need them all.
game.getAssetIds(name: string, kind?: string): string[]Every asset id matching a name, oldest first ([] when none). Names are not unique — importing a pack twice, or two kits that both ship a "Coin", leaves real duplicates. Narrow with kind to compare like with like.
game.loadScene(nameOrIndex: string | number): voidSwitch the running game to another map/scene — by NAME or by build INDEX (0 = main map, 1..N = sub-maps in the list order). Tears down the current scene and loads the target fresh; the change applies at the end of the current tick. Equivalent to SceneManager.LoadScene(name | index).
game.getActiveScene(): stringName of the scene currently running.
game.getActiveSceneIndex(): numberBuild index of the scene currently running (0 = main map, 1..N = sub-maps). Equivalent to: SceneManager.GetActiveScene().buildIndex.
game.getSceneCount(): numberHow many maps this game has (main map + sub-maps). Equivalent to SceneManager.sceneCountInBuildSettings.
game.getSceneNameAt(index: number): stringName of the map at a build index (0 = main map), or '' if out of range.

time

MemberDescription
time.deltaTime: numberSeconds since the previous tick, SCALED by timeScale (Time.deltaTime). (read-only)
time.unscaledDeltaTime: numberSeconds since the previous tick, UNAFFECTED by timeScale (Time.unscaledDeltaTime). (read-only)
time.fixedDeltaTime: numberThe fixed physics timestep, independent of frame rate (Time.fixedDeltaTime). (read-only)
time.time: numberSeconds since the scene started, scaled by timeScale (Time.time). (read-only)
time.unscaledTime: numberSeconds since the scene started, UNAFFECTED by timeScale (Time.unscaledTime). (read-only)
time.frameCount: numberNumber of ticks since the scene started. (read-only)
time.smoothDeltaTime: numberdeltaTime smoothed over recent frames (Time.smoothDeltaTime) — use it to drive anything a single hitched frame shouldn't jolt. An approximation of that filter, not an identical curve. (read-only)
time.timeScale: numberThe simulation time scale (Time.timeScale) — a settable property. Assign it to pause or slow-mo: time.timeScale = 0 pauses (physics, NPCs, animations, and deltaTime freeze), = 1 resumes, = 0.5 is slow motion. Client-only: a no-op on the multiplayer server (it can't pause time for everyone). Scripts keep ticking while paused, so a pause menu can read input and set it back to 1 — drive that menu with unscaledDeltaTime, which ignores timeScale.

input

MemberDescription
input.getKey(key: string): booleanReturns true while the key is held down. Key names: 'w','a','s','d','space','shift','e','q','1'-'9','arrowup', etc. Reads the keyboard of the machine the script is running ON: the play-test and a standalone build answer for the one player sitting there, and a MULTIPLAYER room answers nothing at all, because the keyboards are on the players' machines and the script is on the server. For a key in a room, use watchKeys + onPlayerKeyDown, which say WHOSE key it was.
input.getKeyDown(key: string): booleanReturns true on the frame the key was first pressed. Local input — see getKey.
input.getKeyUp(key: string): booleanReturns true on the frame the key was released. Local input — see getKey.
input.watchKeys(keys: string[]): voidAsk for these keys to be reported for EVERY player, as onPlayerKeyDown / onPlayerKeyUp. This is how a key reaches a multiplayer room. getKey reads the machine the script runs on, and in a room that is the server, which has no keyboard — so a mechanic on R, Q or 1-9 that worked perfectly in play-test did nothing at all once the map was published. Watched keys close that: each client reports only the keys the map asked for, and the room turns them into per-player hooks. onStart() { this.input.watchKeys(['r', 'f']) } onPlayerKeyDown(player, key) { if (key === 'r') this.reload(player.id) } Call it from onStart. The set is the union of every script's request and lasts the map's lifetime; calling again adds to it, up to 32 distinct keys. Names are the same ones getKey takes ('r', 'space', 'arrowup', 'shift'), case-insensitive. Keys the map never asks for are never sent — a room reports the map's controls, not everything a player types. Nothing fires while a player is typing (chat, or any text field). A key already held when they start typing reports its release, so hold state cannot stick down because somebody stopped to talk. Movement keys need no watching: WASD, jump, sprint and E already drive the player and arrive as movement, onInteract and the tool hooks. Watch those only if you want the raw press as well. Works in every runtime. In the play-test and standalone the hooks fire for the local player, so one script behaves the same everywhere.
input.setCommandKeys(command: 'toggleCamera' | 'interact' | 'dropTool', keys: string | string[] | null): voidForce an engine COMMAND onto specific keys for this map — outranking each player's own Settings choice — or disable it here entirely. The commands are 'toggleCamera' (first/third person), 'interact' (vehicles, onInteract) and 'dropTool'. Pass a key or an array of keys (the getKey spelling: 'f', 'space', 'arrowup') to force the command onto exactly those; pass null to switch the command off in this map. In multiplayer every player's keyboard follows at once and late joiners get the current keys; the built-in prompts ("Press F to drive") name whichever key actually works. onStart() { this.input.setCommandKeys('interact', 'f') // this map's scheme needs F this.input.setCommandKeys('toggleCamera', null) // fixed camera — no toggle } Use it sparingly: players re-key commands in Settings for reach and accessibility, and a map should only take that choice away where its control scheme genuinely depends on the key. A map that just wants extra tool keys wants gameSettings.hotbarBinds instead — and hotbarBinds capture their keys above even this. Keys that may not carry a command (digits — the tool slots — Escape, Tab) are dropped; a forced list left empty by that keeps the player's key rather than disabling.
input.clearCommandKeys(command?: 'toggleCamera' | 'interact' | 'dropTool'): voidPut a command back on the player's own key — or every command, with no argument.
input.getCommandKeys(): Partial<Record<'toggleCamera' | 'interact' | 'dropTool', string[] | null>>This map's command-key overrides as they currently stand (a copy): forced keys per command, null for a disabled one, absent where the player's own key rules. NOT each player's effective key — their Settings live on their machines.
input.watchJump(): voidAsk to be told when a player JUMPS, as onPlayerJump(player). The companion of watchKeys, for the one control that is not really a key. It reports the jump that actually happened — the player was on the ground, the jump fired, they left it — not the moment the jump button went down. Those differ constantly: a press while already airborne, while swimming, or while held from the last jump produces no jump at all, and a jump can come from the touch button or a double tap, which no key would ever describe. onStart() { this.input.watchJump() } onPlayerJump(player) { this.players.setScore(player.id, this.players.getScore(player.id) + 1) } Call it from onStart. It applies to the whole map for its lifetime, like a watched key, and works in every runtime — in the play-test and standalone it fires for the local player. If what you want really is the button rather than the jump, watchKeys(['space']) is that. Covers the built-in player. A map that disables the built-in player and drives a characterController from its own script decides for itself what a jump is, so nothing here can see it — use watchKeys there.
input.getAxis(axis: 'horizontal' | 'vertical'): numberReturns a value from -1 to 1 for the given axis. 'horizontal' = A/D or Left/Right, 'vertical' = W/S or Up/Down.
input.getMouseButton(button: number): booleanReturns true if the mouse button is held. 0=left, 1=middle, 2=right.
input.getMouseButtonDown(button: number): booleanReturns true on the frame the mouse button was pressed.
input.getMouseButtonUp(button: number): booleanReturns true on the frame the mouse button was released.
input.getMousePosition(): [number, number]Where the cursor is over the game, [x, y] in 0-1 from the top-left, while the cursor is free (a panel open, cursor.unlock). Put a thing at the mouse with this — a tooltip, a drag ghost. Under pointer lock there is no cursor and the last free position stays. Meaningful in client scripts only: the room has no cursor.
input.getMouseDelta(): [number, number]Mouse movement since last frame [dx, dy] in pixels ('Mouse X'/'Mouse Y'). Works under pointer lock — use this for scripted look.
input.getPlayerSettings(): { lookSensitivity: number; showNameplates: boolean }The PLAYER's own settings, from the lobby — how this person chose to see and drive the game. lookSensitivity their mouse-look multiplier, 0.25 to 3, default 1 showNameplates whether they want usernames drawn over other players These are the player's choices, NOT the map's — a creator's settings live in Game Settings and travel with the map; these live on the player's own device and follow them between games. The built-in camera already applies lookSensitivity. A map that drives its own camera has to apply it itself, or the slider does nothing for anyone playing your game: const s = input.getPlayerSettings() this._yaw -= input.getMouseDelta()[0] * this.sensitivity * s.lookSensitivity Multiply, do not replace: your own sensitivity is the game's feel, theirs is a preference on top of it — which is exactly how the built-in camera composes the two. Reads the machine the script RUNS on, so it answers for the player in the play-test, in a standalone build, and in a client or player-template script. On the multiplayer SERVER there is no one player and no device, so it reports the neutral defaults (1 and true) rather than throwing.
input.getMouseScrollDelta(): [number, number]Mouse wheel scroll this frame [x, y]; y > 0 = scroll up (Input.mouseScrollDelta). Browser-scaled magnitude. Like every getter here it reads the machine the script RUNS on, so it reports the player's wheel in the play-test and in a standalone build, and nothing in a multiplayer room, where scripts run on the server and there is no mouse. For a wheel that switches weapons or zooms in a published game, set gameSettings.mouseWheel and let the engine do it on each player's own machine.
input.getTouchCount(): numberNumber of active touches this frame (Input.touchCount). 0 on a device with no touch.
input.getTouch(index: number): TouchInfo | nullThe active touch at index, or null if out of range (Input.GetTouch).
input.getTouches(): TouchInfo[]All active touches this frame (Input.touches). Empty array when none.
input.getScreenSize(): [number, number]Render surface size [width, height] in PIXELS (Screen.width/height). [0, 0] on a host with no canvas (the MP server). This is the scale factor between our 0-1 screen coords and pixels.
input.getMoveStick(): { active: boolean; origin: [number, number]; offset: [number, number]; forward: number; strafe: number }The built-in movement stick — the invisible dynamic joystick the engine steers the player with. origin is where the press landed and offset how far it has been dragged, both in CSS pixels from the canvas's top-left, so a HUD image can be positioned straight from them. This is a READING of the input already being applied, not a second control: draw the ring from it and the picture cannot disagree with the movement.
input.getLookStick(): { active: boolean; origin: [number, number]; offset: [number, number]; x: number; y: number }The built-in LOOK stick — the right-hand half of the touch scheme, which turns the camera while held. Same shape as getMoveStick, but the axes are in SCREEN terms: x positive = dragged right, y positive = dragged down. active is false on a desktop, where the mouse does this instead. Read-only, like getMoveStick: the engine is already turning the camera from it, so drawing a ring at origin with a knob at offset cannot drift out of step with what the camera is doing.
input.getLookDelta(): [number, number]Pixels the look finger moved this frame, [x, y] — the touch twin of getMouseDelta, and the swipe counterpart to getLookStick. Which one to read depends on the scheme, and a camera script wants BOTH: in the default 'swipe' mode the look stick never reports active (there is no joystick — the finger drags the world directly), so a scripted camera reading only getLookStick cannot be turned at all on a phone. Apply this straight, the way you apply a mouse delta; do not multiply it by deltaTime, because it is a distance already. Zero on a desktop, in 'stick' mode, and in multiplayer, where a map's scripts run on the server.
input.getGestures(): ScriptGesture[]What the fingers DID this frame — usually nothing, sometimes one thing. tap taps is 1, 2, 3… for single/double/triple. The single is reported first: waiting to find out whether a second is coming would delay every single tap by the double window. longPress a press held still for half a second. swipe a quick flick, with dir ('up'/'down'/'left'/'right'), dx/dy and speed in px/s. Every one carries x/y (pixels from the top-left) and half — 'move' or 'look', which side of the screen it belongs to. Use half rather than comparing x against half the screen width yourself: the engine owns where that split falls, and a swipe is tagged by where it STARTED, so a long turn that ends past the middle still counts as a look-half gesture. Checking the half is usually what you want — "double tap to jump" that ignores it also fires when a player re-plants their thumb on the stick. Each is handed over exactly once, on the frame it completed, so a slow game loop cannot miss one and a double read cannot see it twice. Empty on a desktop, where there are no fingers. Read where the SCRIPT runs. In the editor, a standalone build and single-player that is the machine with the screen, so this works; in MULTIPLAYER a map's scripts run on the server, which has no fingers and reports nothing. Use gestures for extras — a second way to do something a button already does — rather than for the only way to do it.
input.getPinch(): { active: boolean; scale: number; delta: number }The live two-finger pinch — a value to follow rather than an event to react to. scale is relative to where the fingers started (2 = twice as far apart), delta is the change since your last read, which is what a zoom wants to add up. Only counted on the LOOK half of the screen: in ordinary play a thumb is already sitting on each side, and treating any two fingers as a pinch would fire one constantly.
input.setMoveAxis(forward: number, strafe: number, player?: unknown): voidDrive the built-in player from your own controls — an on-screen joystick, an autorun pad, a cutscene. Held until you change it, like a key that stays down; setMoveAxis(0, 0) releases. Values are clamped to -1..1, forward positive is forward and strafe positive is LEFT (the same convention the keyboard axis uses). Merged with the keyboard and the built-in stick by magnitude, so nothing fights. player — WHOSE character. Omit it in single-player, the editor play-test and a standalone build, where there is one player and it can only mean them. In MULTIPLAYER a script runs on the server for a whole room, so a control has to say whose it is: pass the player your button handler was given, or the one onInteract handed you. Omitted there the call has nobody to move, and does nothing. The same rule applies to pressJump, pressInteract and setSprint.
input.pressJump(player?: unknown): voidJump once, as if the jump key were tapped — grounded and coyote rules still apply.
input.pressInteract(player?: unknown): voidPress E for the built-in player: get in or out of the nearest vehicle, and fire onInteract on any proximity zone they are standing in — the same edge the key produces, so a script watching getKeyDown('e') sees it too. On a phone this is the ONLY way to reach any of that. A vehicle nobody can enter and a prompt nobody can trigger are features the whole touch half of your audience does not have.
input.setSprint(on: boolean, player?: unknown): voidHold Shift for the built-in player, held until you set it back. getKey('shift') reads it too, so your own scripts and the engine agree about what the player is doing. Note what Shift DOES: it is a WALK modifier, not a sprint one — the built-in player moves at PLAYER_WALK_MULTIPLIER (0.55x) of normal speed while it is on. The name matches the sprint field the rest of the engine and the network protocol already use; the behaviour is the slow one. Label a button for it accordingly, or someone will press it to go faster and get the opposite.
input.setLookMode(mode: 'stick' | 'swipe'): voidSwap the touch look control: 'swipe' (the camera follows your finger and stops with it) or 'stick' (a joystick on the right that turns at a rate while held). Ignored where there is no touch. Works everywhere, including MULTIPLAYER: a map's scripts run on the server there, which has no touchscreen of its own, so the chosen mode travels to every client as replicated data (the same route the on-screen joystick's followStick fields take) and each phone's engine applies it. READING touch, by contrast, only works where a touchscreen is — see getGestures/getPinch.
input.setGesturePrefs(prefs: { doubleTapToJump?: boolean; pinchToZoom?: boolean }): voidTurn on engine-handled touch gesture behaviors: doubleTapToJump makes a quick double tap on the look half jump, pinchToZoom lets two fingers pull the third-person camera in and out. Declarative on purpose — in multiplayer these preferences replicate to every client and each phone's engine enacts them locally, which polling getGestures in a server-run script cannot do. Set once (onStart); fields you omit keep their current setting.
input.getAnyKey(): booleanTrue while ANY key or mouse button is held (Input.anyKey).
input.getAnyKeyDown(): booleanTrue on the frame any key or mouse button was first pressed (Input.anyKeyDown).
input.getInputString(): stringCharacters typed this frame, in order (Input.inputString). Printable text plus '\b' for backspace and '\n' for enter; empty when nothing was typed.
input.getDeviceInfo(): DeviceInfoDevice + display facts behind Input.mousePresent / touchSupported and Screen.dpi/orientation/ safeArea/fullScreen. Zeroed on a host with no DOM (the MP server).
input.getGamepads(): GamepadInfo[]Connected gamepads (Input.GetJoystickNames). Empty when none.

cursor

MemberDescription
cursor.lock(): voidLock the cursor to the game (pointer lock — hides it and feeds raw mouse to look). CursorLockMode.Locked.
cursor.unlock(): voidRelease the cursor so the player can move it freely. CursorLockMode.None.
cursor.setVisible(visible: boolean): voidShow or hide the cursor. Cursor.visible.
cursor.isLocked(): booleanTrue while the cursor is locked to the game.
cursor.isVisible(): booleanTrue while the cursor is visible.

camera

MemberDescription
camera.setType(type: 'custom' | 'scriptable' | 'default'): voidWho drives the camera — Roblox's Camera.CameraType. 'custom' / 'default' (two names for the SAME mode — 'custom' pairs with Roblox, 'default' says what it is): the engine drives. With the built-in player that's the player rig (orbit / follow / fixed / cinematic per the camera component) and pose writes are overwritten the next frame — detach first. With the built-in player DISABLED it's entity mode: the main Camera ENTITY is the camera's transform, and setPosition/setRotation/lookAt write through to that entity, so a single call moves the camera and it stays. 'scriptable': the script owns the render camera outright — nothing drives it until you switch back. The cutscene / kill-cam / orbit-menu pattern over the default player: camera.setType('scriptable'); camera.setPosition(...); … camera.setType('default'). Resets every play session. In multiplayer the server relays these to the client that renders the camera, so a cutscene behaves the same in a room as in the play-test.
camera.forPlayer(playerId: string): ScriptPlayerCameraAPIThe same camera writes, aimed at ONE player. A plain camera.setType(...) is a statement about the room: in multiplayer every player gets it, and it overrides any per-player shot already running. Use this when only one player should see a cutscene, a kill-cam or a menu orbit while everybody else keeps playing. Writes only. There is no reading another player's camera: the server has none to read, and a value invented for the answer would be worse than not offering it. In the editor play-test and in a standalone build there is exactly one player, so this targets them whatever id is passed — a cutscene script written for a room still runs in the play-test.
camera.getType(): 'custom' | 'scriptable'The current camera drive mode ('default' reads back as 'custom' — same mode).
camera.setPosition(position: [number, number, number]): voidMove the render camera to a world position (camera.transform.position). With the built-in player active this only sticks in 'scriptable' mode — see setType.
camera.setRotation(rotation: [number, number, number, number]): voidSet the render camera's rotation: a quaternion [x, y, z, w] (auto-normalized), or 3 values ([x,y,z]) = Euler DEGREES.
camera.lookAt(target: [number, number, number]): voidPoint the camera at a world position.
camera.setFov(degrees: number): voidSet the vertical field of view in degrees.
camera.getFov(): numberThe camera's current vertical field of view in degrees (Camera.fieldOfView). In multiplayer: what a script last set, else the map's main Camera fov. See the interface note.
camera.getRotation(): [number, number, number, number]The camera's current rotation as a quaternion [x, y, z, w] (camera.transform.rotation).
camera.getProjection(): CameraProjectionHow the camera projects: orthographic / orthographicSize / near+farClipPlane / aspect.
camera.setOrbitDistance(distance: number): voidChange the writable part of the projection. orthographicSize is the 2D zoom knob (HALF the vertical view height) — a pixel-perfect camera derives its own size and ignores it. Whether the camera is orthographic at all is authored on the camera component, not set from scripts. How far the third-person camera sits behind the built-in player, and the reader for it. Backs pinch-to-zoom; clamped to 0..40, where 0 is first person.
camera.getOrbitDistance(): number
camera.setShoulderOffset(offset: number): voidHow far the third-person camera sits to the SIDE of the player — the over-the-shoulder shot. The sign is the side the camera sits on, so flipping it swaps shoulders and 0 is centred behind them; clamped to ±3 metres, past which the player leaves the frame. The camera component authors the same number, but that is read once when the scene is built — this is the live one, so a map can let a player swap shoulders on a key: onUpdate() { if (this.input.getKeyDown('c')) this.camera.setShoulderOffset(-this.camera.getShoulderOffset()) } First person ignores it (the shot has no shoulder to sit over), so a swap made there simply applies when the player returns to third person. Best from a CLIENT script: which shoulder someone prefers is theirs alone, and a round trip to change it would be visible.
camera.getShoulderOffset(): number
camera.setZoomRange(min: number, max: number): voidZoom bounds for EVERYONE in the room — see forPlayer(...).setZoomRange for the per-player form, which is the usual one (call it in onPlayerJoined to make a map first-person only).
camera.setProjection(patch: { orthographicSize?: number; nearClipPlane?: number; farClipPlane?: number }): void
camera.getPosition(): [number, number, number]The camera's current world position. In multiplayer: the position a script last set, else the origin — never another player's view.
camera.getForward(): [number, number, number]The direction the camera is facing (unit vector) — handy for shooting / raycasts. In multiplayer: derived from the rotation a script set. Under a follow camera the server holds no pose — a room has one camera per player — and this reads back the documented -Z fallback.
camera.screenPointToRay(point?: [number, number]): { origin: [number, number, number]; direction: [number, number, number] }World ray through a screen point ([x, y] in 0-1 screen coords, top-left origin — the same space as input.getMousePosition / touches). Omit the point to use the current mouse position. Works for perspective AND orthographic cameras — aim clicks, place objects under the cursor, 2D picking: const r = this.camera.screenPointToRay(); const hit = this.physics.raycast(r.origin, r.direction). On the multiplayer server it is computed from the pose a script framed; with no pose to work from it falls back to the camera position + facing.
camera.screenToWorldPoint(point?: [number, number] | [number, number, number], distance?: number): [number, number, number]The world position under a screen point, distance units from the camera ( Camera.ScreenToWorldPoint — pass the distance as the point's 3rd element or the 2nd arg; default 10). With an orthographic camera this is THE way to get the mouse's 2D world position.
camera.worldToScreen(worldPoint: [number, number, number] | { x: number; y: number; z: number }): { x: number; y: number; z: number; behind: boolean }Project a world position to a screen point (the inverse of screenPointToRay). Returns x,y in 0-1 screen coords with a top-left origin (the same space as screenPointToRay's input, ready for HUD placement), z = distance in front of the camera in world units (negative when the point is behind the camera), and behind = z <= 0. Use it to pin a label/marker/off-screen arrow to a world entity; guard on behind (or z <= 0) to hide the marker when the target is behind you. On the multiplayer server (no camera) it returns { x: 0.5, y: 0.5, z: 0, behind: true }.
camera.shake(strength?: number, duration?: number): voidKick off a transient screen shake — a decaying random camera offset for duration seconds. Great for explosions, big hits, landings. strength is in world units (~0.2 subtle … ~0.8 violent); defaults strength 0.3, duration 0.3s. Re-calling restarts it (a new blast overrides a fading one). Client-only: a no-op on the multiplayer server (no camera).
camera.getEntity(): ScriptEntityAPI | nullThe Camera entity backing the active render camera (a live game-object handle, Camera.main.gameObject), or null when the built-in follow camera is active (it has no entity). Client-only: returns null on the multiplayer server, where there is no camera.

characterController

MemberDescription
characterController.move(entityId: string, motion: [number, number, number]): { grounded: boolean; collided: boolean }Move a characterController entity by a world-space motion for this frame (include your own gravity, yourself). Collide-and-slide + auto-step + ground-snap. Returns grounded + whether it hit anything.
characterController.isGrounded(entityId: string): booleanWhether the controller was on the ground as of its last move().

physics

MemberDescription
physics.applyForce(entityId: string, force: [number, number, number]): voidPush a dynamic body through its CENTRE OF MASS for one physics step — AddForce(ForceMode.Force). Call it every frame to push continuously; it does not linger. Newtons.
physics.applyForceAtPoint(entityId: string, force: [number, number, number], point: [number, number, number]): voidPush at a WORLD point rather than the centre of mass, for one step. The off-centre part becomes torque, so this is what tilts, rolls and spins a body — and it's the primitive a scripted vehicle is built on: a spring force at each wheel's contact point IS the suspension, and it's what gives weight transfer and body roll. applyForce cannot express any of that.
physics.applyImpulseAtPoint(entityId: string, impulse: [number, number, number], point: [number, number, number]): voidInstant velocity change at a WORLD point (AddForceAtPosition + ForceMode.Impulse) — a hit or kick that also spins the body. N·s.
physics.applyTorque(entityId: string, torque: [number, number, number]): voidSpin a body for one step (N·m) — anti-roll bars, air control, self-righting.
physics.applyTorqueImpulse(entityId: string, torque: [number, number, number]): voidInstant spin change (N·m·s).
physics.getPointVelocity(entityId: string, point: [number, number, number]): [number, number, number]How fast a WORLD point ON a body is really moving: v + ω × (point − centreOfMass). A spinning body's edge moves even when its centre doesn't, so suspension damping and tyre slip need THIS, not getVelocity. Scripts can't derive it — the centre of mass isn't otherwise reachable.
physics.getMass(entityId: string): numberBody mass in kg — spring and drive forces are meaningless until scaled to it.
physics.getCenterOfMass(entityId: string): [number, number, number]Body centre of mass, world space.
physics.setFriction(entityId: string, friction: number): voidSet the entity collider's friction at runtime — 0 = frictionless (ice), higher = grippier ( PhysicMaterial.dynamicFriction). Unlike the authored collider.friction (applied only when the collider is built), this changes it live — an icy floor, a sticky zone. Note: the physics engine uses a single friction coefficient, so there's no separate static vs dynamic friction.
physics.getFriction(entityId: string): numberThe entity collider's current friction coefficient.
physics.setRestitution(entityId: string, restitution: number): voidSet the entity collider's restitution (bounciness) at runtime, 0..1 — 0 = no bounce, 1 = fully elastic (the collider's bounciness). Live, unlike the authored collider.restitution.
physics.getRestitution(entityId: string): numberThe entity collider's current restitution (bounciness).
physics.sleep(entityId: string): voidForce a dynamic body to sleep NOW (Rigidbody.Sleep) — it stops simulating until a contact, an impulse, or wakeUp disturbs it. Authored counterpart: rigidBody.startAsleep.
physics.wakeUp(entityId: string): voidWake a sleeping body (Rigidbody.WakeUp).
physics.isSleeping(entityId: string): booleanWhether the body is currently asleep (Rigidbody.IsSleeping).
physics.setCcd(entityId: string, enabled: boolean): voidTurn continuous collision detection on/off for a body at runtime ( Rigidbody.collisionDetectionMode = Continuous/Discrete). Physics steps at a fixed 20 Hz, so a body moving faster than its own thickness per step can tunnel through a thin wall between steps; CCD sweeps its path and stops it at the surface instead. Enable it on projectiles / fast props. Authored counterpart: the rigidBody.ccd field (Inspector ▸ RigidBody ▸ Continuous Collision). Costs a little CPU, so it's off by default and best reserved for the few bodies that need it.
physics.setDamping(entityId: string, linear?: number, angular?: number): voidDrag (Rigidbody.linearDamping / angularDamping). Authored rigidBody.linearDamping and angularDamping only reach the body when it is BUILT — this is what changes it mid-play. Pass undefined for either to leave that one alone.
physics.getDamping(entityId: string): [number, number]Current [linear, angular] damping straight off the body.
physics.setGravityScale(entityId: string, scale: number): voidHow strongly world gravity pulls this body — 1 normal, 0 weightless, negative floats it up (there is no separate useGravity flag; useGravity = false is setGravityScale(id, 0)).
physics.getGravityScale(entityId: string): number
physics.setMass(entityId: string, mass: number): voidChange the body's EXACT mass mid-play (rb.mass = x), independent of collider size/density — the same authoritative model the authored rigidBody.mass uses. Ignored for values <= 0.
physics.setFreeze(entityId: string, freezePosition?: [boolean, boolean, boolean], freezeRotation?: [boolean, boolean, boolean]): voidFreeze/unfreeze axes at runtime (RigidbodyConstraints). true means FROZEN, matching the authored rigidBody.freezePosition / freezeRotation. Pass undefined to leave one alone.
physics.setBodyType(entityId: string, type: 'static' | 'dynamic' | 'kinematic'): voidSwap a body between dynamic / kinematic / static mid-play (rb.isKinematic = true). This is how a ragdoll activates, a carried prop goes inert, or an enemy freezes in place. Switching to kinematic stops the solver moving it — drive it with warpBody / rb.MovePosition from then on.
physics.getBodyType(entityId: string): 'static' | 'dynamic' | 'kinematic'
physics.setDetectCollisions(entityId: string, enabled: boolean): voidRigidbody.detectCollisions — turn the body's COLLIDERS off while it keeps simulating, so it still falls but passes through everything (no-clip / ghost / phase-through effects). Distinct from disabling the body, which would freeze it in place.
physics.getDetectCollisions(entityId: string): boolean
physics.isCcdEnabled(entityId: string): booleanWhether continuous collision detection is currently on for this body.
physics.applyImpulse(entityId: string, impulse: [number, number, number]): voidApply an impulse (instant velocity change) to a dynamic rigid body.
physics.setVelocity(entityId: string, velocity: [number, number, number]): voidSet the linear velocity of a dynamic rigid body.
physics.getVelocity(entityId: string): [number, number, number]Get the linear velocity of a rigid body.
physics.setAngularVelocity(entityId: string, velocity: [number, number, number]): voidSet the angular velocity of a dynamic rigid body.
physics.getAngularVelocity(entityId: string): [number, number, number]Get the angular velocity of a rigid body.
physics.warpBody(entityId: string, position: [number, number, number], rotation?: [number, number, number, number]): voidTeleport a dynamic/kinematic body to a world position (and optional rotation quaternion [x,y,z,w], default upright), zeroing its velocity — a respawn/reset for vehicles, crates, ragdolls, etc. The RESET is the point: use this when a body should arrive stopped and upright. A plain entity.position = p also moves a dynamic body now, but keeps its momentum and its current rotation — so a falling crate teleported that way is still falling when it lands.
physics.raycast(origin: [number, number, number], direction: [number, number, number], maxDistance?: number, layerMask?: number, ignoreEntities?: string[], triggers?: 'useGlobal' | 'ignore' | 'collide'): RaycastHit | nullCast a ray from origin in direction, returns the first hit or null. Optional layerMask is a bitmask of layers to hit — bit L set = hit layer L (e.g. 1 &lt;&lt; 3 for layer 3 only); omit/-1 = all layers. Optional ignoreEntities is a list of entity ids to skip (the ray passes through them) — pass [this.entity.id] to ignore yourself. There is no such list in a plain layer-mask query; this is a convenience on top of layerMask. triggers says whether TRIGGER colliders count as hits: 'useGlobal' (default) follows GameSettings.queriesHitTriggers, which is on unless the map turns it off; 'ignore' flies through every trigger — what a shot wants, so an invisible zone cannot stop it; 'collide' hits them even when the global is off.
physics.raycastAll(origin: [number, number, number], direction: [number, number, number], maxDistance?: number, layerMask?: number, ignoreEntities?: string[], triggers?: 'useGlobal' | 'ignore' | 'collide'): RaycastHit[]Cast a ray and return ALL hits along it, sorted nearest-first (Physics.RaycastAll). Optional layerMask filters which layers are hit (bit L set = layer L; omit/-1 = all). Optional ignoreEntities = entity ids to skip (e.g. [this.entity.id] for self). triggers as in raycast.
physics.sphereCast(origin: [number, number, number], radius: number, direction: [number, number, number], maxDistance?: number, layerMask?: number, ignoreEntities?: string[], triggers?: 'useGlobal' | 'ignore' | 'collide'): RaycastHit | nullThick raycast — sweep a sphere of radius along the ray and return the first entity it would touch (Physics.SphereCast), as a RaycastHit (see Value types) or null. More reliable than a thin ray for ground checks and projectiles: a ray slips through gaps and off ledge edges a real body wouldn't. distance is how far the sphere's CENTRE travelled before contact; point/normal are on the surface it hit. ⚠ distance is 0 when the sphere already overlaps a collider at origin (contact-at-start), and the returned hit sets overlapping: true so you can detect it — start the cast clear of geometry (or offset origin back along -direction) when you need the gap distance. layerMask and ignoreEntities (entity ids to skip, e.g. [this.entity.id]) as in raycast.
physics.overlapSphere(center: [number, number, number], radius: number, layerMask?: number): EntityRef[]All entities within radius of center (proximity query, by entity position — not exact collider intersection). Includes the caller; filter by tag/component as needed. layerMask filters by layer the same way raycast does (bit L = layer L; omit or -1 for every layer).
physics.overlapBox(center: [number, number, number], halfExtents: [number, number, number], orientation?: [number, number, number, number], layerMask?: number): EntityRef[]All entities inside a box at center with halfExtents (proximity query by entity position). An optional orientation quaternion makes it an oriented box; default is axis-aligned (OverlapBox). layerMask filters by layer as in raycast.
physics.setGravity(gravity: [number, number, number]): voidSet gravity for the physics world.
physics.getGravity(): [number, number, number]The physics world's current gravity (Physics.gravity). Reflects the map's own gameSettings.gravity, not a constant — read it before computing jump heights or drag.
physics.createRagdoll(position: [number, number, number], options?: RagdollSpawnOptions): RagdollHandleSpawn a physics ragdoll — a humanoid skeleton of jointed dynamic capsules that flops under gravity. position is the BASE (feet). Returns live limb handles; apply an impulse to root (the pelvis) to launch it. e.g. const r = physics.createRagdoll([x,y,z]); physics.applyImpulse(r.root.id, [0,5,8]).

animation

MemberDescription
animation.play(entityId: string, clipName: string, options?: { loop?: boolean; speed?: number; fade?: number }): voidPlay an animation clip on an entity. fade blends in from whatever clip is playing, over that many seconds, instead of cutting to the first frame.
animation.stop(entityId: string, clipName?: string): voidStop a specific animation clip (or all if clipName is omitted).
animation.isPlaying(entityId: string, clipName: string): booleanReturns true if the entity is currently playing the given clip.
animation.getClips(entityId: string): string[]Get list of available animation clip names on an entity.
animation.crossFade(entityId: string, clipName: string, duration?: number): voidCrossfade from the current animation to a new looping one over duration seconds (0.25 by default). The same as play with a fade.
animation.setBool(entityId: string, name: string, value: boolean): voidSet a bool parameter on an entity's animator controller (drives state transitions).
animation.setFloat(entityId: string, name: string, value: number): voidSet a float parameter (e.g. a speed value that crosses a transition threshold).
animation.setInt(entityId: string, name: string, value: number): voidSet an int parameter.
animation.setTrigger(entityId: string, name: string): voidPulse a trigger parameter — fires a transition once, then auto-resets.
animation.getFloat(entityId: string, name: string): numberRead a float (or bool-as-0/1) parameter.
animation.getBool(entityId: string, name: string): booleanRead a bool parameter.

timer

MemberDescription
timer.delay(delaySeconds: number, fn: () => void): numberCall fn after delaySeconds. Returns timer ID.
timer.interval(intervalSeconds: number, fn: () => void): numberCall fn every intervalSeconds. Returns timer ID.
timer.cancel(timerId: number): voidCancel a timer by ID.

math

MemberDescription
math.lerp(a: number, b: number, t: number): numberLinear interpolation from a to b by t (0-1).
math.clamp(value: number, min: number, max: number): numberClamp value between min and max.
math.remap(value: number, inMin: number, inMax: number, outMin: number, outMax: number): numberTake value from one range and return its equivalent position in another: math.remap(75, 0, 100, -1, 1) → 0.5. Output ranges may be inverted (outMin > outMax) to flip direction. Values outside the input range are EXTRAPOLATED, not clamped — wrap the call in math.clamp if you need it bounded. If inMin === inMax the result is outMin.
math.randomRange(min: number, max: number): numberRandom float between min and max.
math.randomInt(min: number, max: number): numberRandom integer between min and max (inclusive).
math.randomSeed(seed: number): voidSeed the script RNG that randomRange/randomInt/Random draw from. Same seed ⇒ same sequence in the editor play-test, the server and a standalone build — the basis for reproducible runs.
math.damp(current: number, target: number, lambda: number, dt: number): numberFrame-rate-independent approach: target + (current - target) * exp(-lambda * dt). Use this instead of lerp(current, target, 0.1) in an update loop, which converges faster the higher the frame rate. Larger lambda = snappier.
math.snap(value: number, increment: number): numberRound to the nearest multiple of increment (grid snapping). increment 0 returns value unchanged.
math.wrap(value: number, min: number, max: number): numberWrap value into [min, max) — the general form of repeat(), which only covers 0..length.
math.smootherstep(edge0: number, edge1: number, x: number): numberPerlin's smoothstep: same 0→1 shape but with a continuous second derivative, so motion driven by it has no visible kick at the ends.
math.distance(a: [number, number, number], b: [number, number, number]): numberDistance between two 3D points.
math.normalize(v: [number, number, number]): [number, number, number]Normalize a 3D vector to unit length.
math.dot(a: [number, number, number], b: [number, number, number]): numberDot product of two 3D vectors.
math.cross(a: [number, number, number], b: [number, number, number]): [number, number, number]Cross product of two 3D vectors.
math.add(a: [number, number, number], b: [number, number, number]): [number, number, number]Add two 3D vectors.
math.sub(a: [number, number, number], b: [number, number, number]): [number, number, number]Subtract vector b from a.
math.mul(v: [number, number, number], scalar: number): [number, number, number]Multiply a 3D vector by a scalar.
math.div(v: [number, number, number], scalar: number): [number, number, number]Divide a 3D vector by a scalar. Dividing by 0 returns [0,0,0] rather than Infinity.
math.negate(v: [number, number, number]): [number, number, number]Flip a vector's direction (−v).
math.scaleVec(a: [number, number, number], b: [number, number, number]): [number, number, number]Component-wise multiply, for non-uniform scaling (Vector3.Scale).
math.minVec(a: [number, number, number], b: [number, number, number]): [number, number, number]Component-wise minimum — the low corner of the AABB around both (Vector3.Min).
math.maxVec(a: [number, number, number], b: [number, number, number]): [number, number, number]Component-wise maximum — the high corner of the AABB around both (Vector3.Max).
math.length(v: [number, number, number]): numberLength (magnitude) of a 3D vector.
math.sqrLength(v: [number, number, number]): numberSquared length — skips the sqrt (sqrMagnitude). Compare against radius*radius.
math.sqrDistance(a: [number, number, number], b: [number, number, number]): numberSquared distance between two points — skips the sqrt. Compare against range*range.
math.smoothstep(edge0: number, edge1: number, x: number): numberSmooth-step interpolation (ease in-out).
math.deg2rad(degrees: number): numberConvert degrees to radians.
math.rad2deg(radians: number): numberConvert radians to degrees.
math.moveTowards(current: number, target: number, maxDelta: number): numberMove a value toward target by at most maxDelta.
math.lerpVec3(a: [number, number, number], b: [number, number, number], t: number): [number, number, number]Lerp a 3D vector from a to b by t.
math.clamp01(value: number): numberClamp value to [0, 1] (Mathf.Clamp01).
math.sign(value: number): numberSign of value: +1 for ≥0, −1 otherwise (Mathf.Sign — note +1 at 0, unlike Math.sign).
math.inverseLerp(a: number, b: number, value: number): numberWhere value falls between a and b, as 0..1 (Mathf.InverseLerp).
math.repeat(t: number, length: number): numberWrap t into [0, length) (Mathf.Repeat).
math.pingPong(t: number, length: number): numberBounce t between 0 and length (Mathf.PingPong).
math.approximately(a: number, b: number): booleanTrue if a and b are within a tiny epsilon (Mathf.Approximately).
math.deltaAngle(a: number, b: number): numberShortest signed difference between two angles in degrees, in [−180, 180] (Mathf.DeltaAngle).
math.lerpAngle(a: number, b: number, t: number): numberInterpolate between two angles in degrees the short way (Mathf.LerpAngle).
math.moveTowardsAngle(current: number, target: number, maxDelta: number): numberMove a degrees angle toward target by at most maxDelta, short way (Mathf.MoveTowardsAngle).
math.angle(a: [number, number, number], b: [number, number, number]): numberUnsigned angle in degrees between two vectors (Vector3.Angle).
math.project(v: [number, number, number], onto: [number, number, number]): [number, number, number]Project vector v onto onto (Vector3.Project).
math.reflect(v: [number, number, number], normal: [number, number, number]): [number, number, number]Reflect v off a surface with the given normal (Vector3.Reflect).
math.clampMagnitude(v: [number, number, number], max: number): [number, number, number]Clamp a vector's length to max (Vector3.ClampMagnitude).
math.moveTowardsVec3(current: [number, number, number], target: [number, number, number], maxDelta: number): [number, number, number]Move a point toward target by at most maxDelta (Vector3.MoveTowards).
math.dampVec3(current: [number, number, number], target: [number, number, number], lambda: number, dt: number): [number, number, number]Frame-rate-independent approach for a point — the vector form of {@link ScriptMathAPI.damp}.
math.smoothDamp(current: number, target: number, velocity: number[], smoothTime: number, deltaTime: number, maxSpeed?: number): numberCritically-damped spring (Mathf.SmoothDamp). velocity is a 1-element in/out array you keep between frames — e.g. this.vel = [0] in onStart, then pass this.vel each update; it is mutated. ⚠ Argument order differs from Mathf.SmoothDamp: deltaTime comes BEFORE the optional maxSpeed, because omitting dt (and silently getting a fixed 0.02 step) is the mistake worth preventing.
math.smoothDampVec3(current: [number, number, number], target: [number, number, number], velocity: number[], smoothTime: number, deltaTime: number, maxSpeed?: number): [number, number, number]Critically-damped spring for a point (Vector3.SmoothDamp). velocity is a 3-element in/out array you keep between frames ([0,0,0]); it is mutated. Same argument-order note as smoothDamp.
math.projectOnPlane(v: [number, number, number], normal: [number, number, number]): [number, number, number]Drop the part of v pointing into a surface, keep the part along it (Vector3.ProjectOnPlane). The slope primitive for character controllers: projectOnPlane(moveDir, groundNormal).
math.signedAngle(a: [number, number, number], b: [number, number, number], axis: [number, number, number]): numberAngle from a to b in degrees, signed by which side of axis the turn goes ( Vector3.SignedAngle). Pass [0,1,0] for a yaw turn. Unlike angle(), this tells you WHICH WAY.
math.geo: ScriptMathGeoAPIRay/point/segment queries against pure geometry — see {@link ScriptMathGeoAPI}.
math.curve: ScriptMathCurveAPIArc and spline interpolation — see {@link ScriptMathCurveAPI}.

events

MemberDescription
events.send(entityId: string, key: string, data?: unknown): voidSend a message to a specific entity's scripts.
events.broadcast(key: string, data?: unknown): voidSend a message to ALL entities' scripts.
events.sendToTag(tag: string, key: string, data?: unknown): voidSend a message to all entities with a given tag.
events.toServer(key: string, data?: unknown): voidCLIENT scripts only — send a request to the server's scripts, arriving as onClientEvent(player, key, data) with player stamped from this player's session. The client half of the cross-boundary pair (Roblox RemoteEvent:FireServer). Delivered next tick in every runtime. data must survive JSON (plain values, arrays, objects — no functions, no live handles) and stay under 16KB; a payload that cannot cross throws AT THE SEND, in the play-test too. Calling this from a server script throws — the server reaches clients with toClient. The server must treat what it receives as UNTRUSTED: a client script runs on the player's machine, so a request says what the player WANTS, and the server script decides what actually happens.
events.invokeServer(key: string, data?: unknown): Promise<unknown>CLIENT scripts only — toServer with an answer: send a request the server's scripts REPLY to, and await the reply. Arrives at server scripts as onClientInvoke(player, key, data); the first script (in dispatch order) whose handler returns a value other than undefined answers, and the promise resolves with that value. The promise rejects when the handler threw, when no server script answered the key, when the reply could not survive JSON or its size cap, or after a few seconds with no reply at all (dropped connection) — so always await it in try/catch: try { const price = await this.events.invokeServer('quote', { item: 'sword' }) } catch (e) { ... } Same rules as toServer everywhere else: payload and reply must survive JSON and stay under 16KB, requests share toServer's per-player rate budget, the server sees player stamped from this session and must treat data as untrusted. Delivered next tick; the reply takes at least one more. For fire-and-forget requests keep using toServer — an invoke costs a round trip.
events.toClient(player: PlayerRef | string, key: string, data?: unknown): voidSERVER scripts only — raise onServerEvent(key, data) in the client scripts of one player (pass the PlayerRef or id) or of everyone (pass 'all'). The reply direction of toServer (FireClient/FireAllClients). Same next-tick delivery and JSON payload rules. Calling this from a client script throws — a client script requests with toServer instead.

scene

MemberDescription
scene.getName(): stringGet the name of the current scene.
scene.getLayers(): string[]The project's layer names, index = layer id (0 = 'Default'), the project's Layers. Holes in the list are '' (unnamed). Backs LayerMask.NameToLayer / LayerToName / GetMask.
scene.loadScene(nameOrIndex: string | number): voidLoad a scene by name OR build index (0 = main map, 1..N = sub-maps). Triggers a full scene reload.
scene.setEnvironment(env: SceneEnvironment): voidChange the live ENVIRONMENT (sky/fog/lighting/day-night/shadows) at runtime — e.g. go dark when a boss spawns: scene.setEnvironment({ skyColor:'#0a0a14', ambientIntensity:0.1, fogEnabled:true, fogColor:'#05050a' }). Merges (set only what you want); broadcasts to all clients to re-apply. Per-map. Shadow keys: shadowsEnabled, shadowType ('soft'|'hard'), shadowMapSize (512-4096), shadowCameraSize (coverage radius around the viewer, 10-200), shadowCameraNear/Far — see the Lighting & shadows guide.

graphics

Render quality from the running game — for a game's own graphics options menu.

MemberDescription
graphics.get(): GraphicsQualityStateThe quality in effect right now, after all three ceilings. Read this to show a menu's current state.
graphics.set(quality: GraphicsQualityPatch): voidLower render quality for the rest of this session. Only the fields you pass change, so a menu with independent controls can move one without disturbing the others. Applies immediately — resolution, shadow filtering and the frame cap all change on the next frame. e.g. graphics.set({ renderScale: 0.5, shadowMode: 'off', fpsCap: 30 }) for a "Performance" option, and graphics.set({ renderScale: 1, shadowMode: 'default', fpsCap: 0 }) to go back to asking for everything (still capped by the player's and creator's settings). Values are EXACT and continuous — drive them straight from a slider if you want: graphics.set({ renderScale: pct / 100 }) with any percentage in range. See GraphicsQualityPatch for each field's range; anything outside clamps to the nearest end instead of failing. The one thing a script cannot change is antialiasing: it is fixed when the WebGL context is created, so it belongs to the player's own platform settings and only takes effect on the next game they open.
graphics.reset(): voidDrop this game's ceiling entirely — back to whatever the player and creator settled on.

players

MemberDescription
players.getAll(): PlayerRef[]Get all connected players.
players.getById(id: string): PlayerRef | nullGet a player by ID.
players.getLocal(): PlayerRef | nullThe local player (this client's own player) on client runtimes — editor play-test, standalone, and offline play. On the authoritative multiplayer server there is no single local player, so this returns null there: use getAll()/getById() or the onPlayer* hooks to act per-player instead.
players.getPosition(playerId: string): [number, number, number] | nullGet a player's current position. Returns null if player not found.
players.getVehicle(playerId: string): string | nullThe entity id of the vehicle the player is driving (its chassis), or null when on foot — so a camera script can detect driving: const car = players.getVehicle(players.getLocal().id). Resolve the id with getEntity(car) for its .position / .forward. Client-only (null on the MP server).
players.getLookDirection(playerId: string): [number, number, number] | nullWhere a player is LOOKING — their camera-forward as a unit vector, or null if they are unknown. This is the ray a weapon fires along, a laser sight draws, or a "what am I pointing at" prompt tests. Pair it with getEyePosition for the origin — never the player's own position, which is their waist: const from = this.players.getEyePosition(player.id) const dir = this.players.getLookDirection(player.id) const hit = this.physics.raycast(from, dir, 80) It is LIVE: it tracks the player's view every packet, so a weapon that keeps firing while the trigger is held follows where they are looking rather than freezing on the aim of the click that started the burst. (blocks.hitFromPlayer is the other half of this pair and deliberately still answers for the CLICK — mining acts on the block the player clicked.) Direction only, and the direction is all a client is trusted for: every origin the engine derives comes from the server's own idea of where that player is. Null for a player the game does not know — and briefly for one who does: a player who has only just joined has not sent a packet yet, so guard the call rather than assuming a vector.
players.getFacing(playerId: string): [number, number, number] | nullWhich way a player's BODY is turned — their forward as a unit vector on the ground plane, or null if they are unknown. Not the same as getLookDirection, and the difference is the whole reason this exists. That one is where the CAMERA points; this one is how the character is TURNED, which is what gets drawn. A player can look one way and stand another, so anything that cares about a body's shape rather than its aim wants this: a hit test built from the rig's parts, a shot-in-the-back check, a shove that should push someone the way they face. const f = this.players.getFacing(player.id) const right = [-f[2], 0, f[0]] // their right-hand side, no trigonometry needed Horizontal by construction — y is always 0, because a character stands upright however steeply the ground under them tilts. Pair it with getPosition, the waist the body is drawn around.
players.getShotLagMs(playerId: string): numberHow many milliseconds behind the room this player's SCREEN was when their input arrived — their network round trip plus the delay every client draws other players at. Server scripts, on maps with hitDetection: 'rewind' in Game Settings; everywhere else it answers 0, which is also the honest answer there (the play-test has no network, and a 'present' map asked not to rewind). This is the one number a hit test needs: a shooter aims at what they SEE, and what they see ran this far behind where everyone is now. Pass it to getPositionAt/getFacingAt to test the shot against the moment they fired: const lag = this.players.getShotLagMs(shooter.id) for (const p of this.players.getAll()) { const c = this.players.getPositionAt(p.id, lag) // where the shooter SAW them ... } Clamped by the map's rewindLimitMs (Game Settings, default 250), so it never reaches further back than the map allows.
players.getPositionAt(playerId: string, msAgo: number): [number, number, number] | nullA player's position msAgo milliseconds in the past — where they were, not where they are. 0 (or any value on a map without hitDetection: 'rewind') answers getPosition exactly, so a script written with this runs unchanged everywhere. Sample every player at the SAME msAgo within one shot — mixing instants puts targets in poses none of them ever shared. Null for a player the game does not know, like every players.* read.
players.getFacingAt(playerId: string, msAgo: number): [number, number, number] | nullgetFacing, sampled msAgo milliseconds in the past — which way their body was turned when the shooter saw them. Same rules as getPositionAt, same one-instant-per-shot rule.
players.getEyePosition(playerId: string): [number, number, number] | nullA player's EYE position — their position raised by the eye height, which is where an aim ray starts. getPosition is their waist, so a ray from there clips the floor on a downward shot and misses over a low wall. Follows gameSettings.playerHeight, so it stays inside the character's head on a map that made players taller or shorter. Null if the player is unknown.
players.getLookOrigin(playerId: string): [number, number, number] | nullWhere to START a ray so it lands under the player's CROSSHAIR — pair it with getLookDirection. const hit = this.physics.raycast( this.players.getLookOrigin(player.id), this.players.getLookDirection(player.id), 80) Not the same as the eyes, and that difference is the whole point. The crosshair is the centre of the screen, so what it sits over is whatever the CAMERA's centre ray meets — and a third-person camera is pitched at the player's head, above their eyes. A shot fired from the eyes along the aim lands that far below what the player was pointing at, at every range. This is the point on that line, so a ray from it hits exactly what the crosshair covered. Use getEyePosition instead when you want the player's actual eyes — what they can SEE from, for a line-of-sight check or a first-person effect. Falls back to the eye when no camera reported one.
players.setSpawnRule(rule: { pick?: 'first' | 'random' | 'farthest'; minDistance?: number }): voidHow this map hands out its spawn points — which pad an arriving or respawning player gets. A map with several spawn entities has to choose between them, and there is no answer that suits every game: a deathmatch wants arrivals scattered and away from whoever is already shooting, a tutorial or a race wants the first spawn every time in the order they were authored, a co-op map may want everyone together. So the map says, usually once in onStart: onStart() { this.players.setSpawnRule({ pick: 'random', minDistance: 20 }) } minDistance is a FILTER: prefer pads at least this far from any other player. When none qualifies — a crowded room, a small arena — it is dropped rather than enforced and the FARTHEST free pad wins, since a map that asked for distance is best served by as much of it as there is. 0 (the default) asks for none. pick is the CHOOSER among what survives the filter: 'first' — authored order, the same pad every time while it is free. The DEFAULT, and what the engine did before this rule existed, so no map changes behaviour by upgrading. 'random' — spread arrivals across the pads. What ten spawn points usually mean. 'farthest' — always the pad emptiest of players. Whatever the rule, nobody is ever spawned INSIDE another player; that is not negotiable and not part of it. Per room, and reset when the room switches sub-map, so one map's rule can never reach another's. On a single-player host (play-test, standalone) it is accepted and does nothing.
players.getMaxPlayers(): numberHow many players this room seats: the map's Game Settings "Max players" until a script changes it with setMaxPlayers. Server scripts only — the number is the room's, so a client is told to ask the server (share it with room.setProperty if a HUD wants it).
players.setMaxPlayers(max: number): voidReseat this room at runtime — a lobby that opens more chairs once a match is under way, a boss fight that closes the door at eight. Whole number, clamped to 1..100 like the Game Settings value. The matchmaker and the server browser see the new number within seconds and route new players by it. Lowering it below the current head count removes nobody: the room only stops admitting until enough players leave. Per room and reset when the room switches map (like setSpawnRule), never saved — a fresh room starts from the map's number again. onStart() { this.players.setMaxPlayers(4) } Server scripts only.
players.teleport(playerId: string, position: [number, number, number], yaw?: number): voidTeleport a player to a position, optionally aiming them. this.players.teleport(player.id, this.entity.position, 180) yaw is the facing in DEGREES — the direction the PLAYER faces, the same convention setRespawnPoint uses; omit it and they keep the way they were looking. Aiming moves the CAMERA with them (it falls in behind), so a pad that drops a player somewhere can point them down the course rather than wherever they last turned.
players.setRespawnPoint(playerId: string, position: [number, number, number] | null, yaw?: number): voidWhere this player wakes up after they die — a checkpoint, a base, a safe room. Until a map sets one they respawn at the map's spawn point, and null puts them back to that. This is the whole of what the engine knows about checkpoints: which pad claims a player, in what order, what it says and how it looks are the map's, so a course can have ordered stages, a lobby can send everyone home, and a race can clear progress at the finish line. onPlayerEnter(player) { this.players.setRespawnPoint(player.id, this.entity.position, 180) } yaw is the facing in DEGREES they wake up with — the direction the PLAYER faces, with the camera falling in behind them, so a pad turned down the course wakes them up looking down the course. (this.entity.eulerAngles[1] on the pad hands it its own.) Omit it and they keep the way they were looking. Their own point survives death for as long as they are in the game, and is forgotten when they leave. Server scripts only — a client cannot choose where anyone respawns.
players.getRespawnPoint(playerId: string): [number, number, number] | nullThe respawn point set for this player, or null when they still respawn at the map's spawn point — so a script can tell "has this player reached any checkpoint yet". Server scripts only: the point is not replicated, so a client script asking would be answered with a lie — it throws there.
players.teleportToScene(playerId: string, sceneName: string, data?: unknown): booleanMove a player to the game's OTHER map by name — in a published multiplayer game they leave this server and join one running that map (each map gets its own servers), carrying their health, backpack and the optional data payload with them. In the editor and standalone the whole scene loads instead, since a single player and their server are the same thing. Persistent values saved with data.set follow the player everywhere on their own. Returns false if no map has that name. e.g. a portal: players.teleportToScene(player.id, 'Dungeon', { fromDoor: 'north' })
players.getJoinData(playerId: string): unknownWhat teleportToScene sent along for this player (its data argument), or null if they did not arrive through a teleport. Readable for their whole session — check it in onStart/onUpdate to place arrivals: const from = players.getJoinData(p.id)
players.launch(playerId: string, velocity: [number, number, number]): voidLaunch/knock the player with an instant velocity — bounce pads, jump pads, knockback, explosion pushes. The player is a kinematic controller, so physics.applyImpulse(player.id, …) does NOTHING to it; use this instead. velocity[1] (Y) is the upward launch speed, velocity[0]/[2] (X/Z) a horizontal knockback that decays. e.g. a bounce pad: players.launch(player.id, [0, 18, 0]). In a CLIENT script this is the one movement write you keep, and only on your own player (players.getLocal().id) — that machine already simulates it, so the throw happens on the frame you ask for it instead of after a round trip, which is what a bounce pad wants. Throwing anyone else is refused: their character is simulated on their own machine. A room bounds a self-thrown arc by GameSettings.moveTolerance, so a map that launches this way must raise it.
players.kill(playerId: string): voidKill a player (respawn at spawn point).
players.damage(playerId: string, amount: number): voidDeal damage to a player (kills + respawns them if health reaches 0).
players.heal(playerId: string, amount: number): voidHeal a player by amount, clamped to their max health (the inverse of damage — for health pickups).
players.getHealth(playerId: string): numberGet a player's current health. This is the authoritative value that damage(), hazard zones, and falls reduce, and that respawn restores to max. Read it to drive a HUD health bar or a game-over check — do NOT keep your own player-HP variable (it desyncs from real damage sources).
players.getMaxHealth(playerId: string): numberGet a player's maximum health (default 100, or whatever setMaxHealth set).
players.getAccountLevel(playerId: string): number | nullA player's PLATFORM account level — the one on their profile, earned across every game. For a level badge in your HUD: onPlayerJoined(player) { const level = this.players.getAccountLevel(player.id) if (level !== null) this.ui.createLabel({ text: Level ${level}, playerId: player.id, position: [0.06, 0.06] }) } Not a level in YOUR game, and a map cannot award it. For progress that belongs to your game, keep your own with data.set / data.increment — that is per-game, and yours to balance and reset. Null for anyone with no account to read: a guest, an editor play-test, a standalone build, and the first moment of a signed-in player's session while the level is still being looked up. Guard it, and design the HUD to survive its absence — plenty of your players will be guests. The level is all a map gets. Coins are not readable here: a platform balance is real money, and economy.getCoins() is your own game's currency, which is yours to read and to pay out.
players.setMaxHealth(playerId: string, max: number): voidSet a player's maximum health AND restore them to full. Call it at game start to give the player a custom amount of HP (e.g. setMaxHealth(id, 200) for a tankier hero).
players.setInputEnabled(playerId: string, enabled: boolean): voidEnable or disable a player's movement input. Use it to freeze the player during a level transition, cutscene, or dialog (re-enable it before handing control back).
players.setInvulnerable(playerId: string, invulnerable: boolean): voidMake a player invulnerable (true) or vulnerable again (false). While invulnerable, damage() and kill() — and hazard zones, falls, enemy hits — are no-ops for that player. Use it for spawn protection, power-ups, or to stop deaths during a level-complete overlay / cutscene.
players.respawn(playerId: string): voidRespawn a player — at their own respawn point when a script has set one, else the spawn point.
players.setScore(playerId: string, score: number): voidSet a player's score (for leaderboard).
players.getScore(playerId: string): numberGet a player's score.
players.setSpeed(speed: number): voidSet player movement speed (units/sec). Pass a playerId first to target ONE player in multiplayer: setSpeed(player.id, 10) (e.g. from onInteract). Without an id it sets the local/current player.
players.setSpeed(playerId: string, speed: number): void
players.getSpeed(): numberGet current player movement speed.
players.setJumpForce(force: number): voidSet player jump force. Pass a playerId first to target ONE player in multiplayer: setJumpForce(player.id, 24) — a jump upgrade on one player. Without an id it sets everyone's.
players.setJumpForce(playerId: string, force: number): void
players.getJumpForce(): numberGet current player jump force.
players.setFlying(playerId: string, flying: boolean): voidFlight for one player: no gravity, the jump key rises and the walk key sinks, both at the player's move speed; horizontal movement is unchanged. Lasts until set false or they leave. Server scripts only; reaches the machine that simulates the character.
players.isFlying(playerId: string): boolean
players.setHidden(playerId: string, hidden: boolean): voidHide one player from everyone else: their character and nameplate are drawn on nobody else's screen (they still see themselves, still collide, still take hits). Server scripts only.
players.isHidden(playerId: string): boolean
players.getAvatar(playerId: string): AvatarConfig | nullA player's current avatar look (a copy): attachments, body parts, colors, scale. Change it through setAvatarPart / addAttachment; direct edits to the returned object do nothing. Returns null if the player is not found.
players.setAvatarPart(playerId: string, slot: BodyPartSlot, partId: string | null): voidSwap one of a player's body parts for another look — Roblox character-morph style. Slots: 'head' | 'torso' | 'leftArm' | 'rightArm' | 'leftLeg' | 'rightLeg'. partId is a part id such as 'pumpkinHead' or 'robotTorso'; pass null to restore that slot's normal look. Everyone in the room sees the change. Runtime-only: it never touches the player's saved profile avatar, and it resets when they leave. e.g. a Halloween morph pad: players.setAvatarPart(playerId, 'head', 'pumpkinHead').
players.addAttachment(playerId: string, socket: AttachmentSocket, assetId: string, offset?: AttachmentOffset): voidAttach an accessory to a named point on a player's body ('hat', 'bodyBack', 'leftHand', …) — one accessory per socket, replacing whatever was there. assetId is an accessory id such as 'crown' or 'jetpack'; offset optionally nudges position (rig units), rotationDeg, and scale. Runtime-only, visible to everyone, reset on leave. e.g. reward the race winner: players.addAttachment(winnerId, 'hat', 'crown').
players.removeAttachment(playerId: string, socket: AttachmentSocket): voidRemove whatever accessory occupies a socket on a player (the inverse of addAttachment).
players.resetAvatar(playerId: string): voidRestore a player's avatar to what they joined with (undo every setAvatarPart / addAttachment this game made).
players.setCharacterModel(playerId: string, model: string | { assetId: string; offset?: [number, number, number]; rotationDeg?: [number, number, number]; scale?: number } | null): voidSwitch which MODEL renders as this player's CHARACTER, for everyone -- the Roblox morph (player.Character = a different rig), by descriptor. Player-template maps only: the character there is the map's own art (a car, a marble, a creature), and this replaces that art with another model from the map's OWN model assets, on every screen, late joiners included. model is a model asset id or NAME, or an object with a local-transform override for model sets whose pivots disagree (most exported sets): players.setCharacterModel(id, 'Car7') players.setCharacterModel(id, { assetId: 'Car7', offset: [0, 0.24, -0.15], rotationDeg: [0, -90, 0], scale: 0.14 }) players.setCharacterModel(id, null) // back to the authored template art Server-only, like every appearance mutation; a model that is not in this map's assets is refused loudly. The choice is LIVE state (it does not survive the room) -- persist it in data and re-apply from onPlayerDataReady, the same as any other loadout. Physics is untouched: the collider stays the template's, exactly as documented for the template flavours.
players.getCharacterModel(playerId: string): { assetId: string; offset?: [number, number, number]; rotationDeg?: [number, number, number]; scale?: number } | nullThe character model this player currently renders with, or null for the authored template art. Readable on BOTH sides (clients read the replicated roster state), like getAvatar.
players.setReplicationFocus(playerId: string, focus: [number, number, number] | string | null): voidRepoint one player's REPLICATION FOCUS — the position their per-map replication distance is measured from (Scene Settings > Replication). Defaults to their character; repoint it for a spectator camera, a cutscene, or an overview map so the world stays live around what that player is actually watching. Takes a world position, an entity (id or name — the focus then follows it), or null to return to the character. Server scripts only; a no-op on maps that have not enabled replication culling, and in the editor play-test and standalone builds, which simulate everything locally. players.setReplicationFocus(id, [0, 40, 250]) // watch the arena from the stands players.setReplicationFocus(id, 'CameraTarget') // follow a cutscene dolly players.setReplicationFocus(id, null) // back to the character
players.getReplicationFocus(playerId: string): [number, number, number] | string | nullThe player's current replication-focus override: the pinned position, the followed entity's id, or null when the focus is their character (the default). The read half of setReplicationFocus, for the cutscene pattern that saves, moves and restores. Server scripts only, like the setter — the state lives on the room and is not replicated.
players.setProperty(playerId: string, key: string, value: unknown): voidSet a REPLICATED property on a player — a value every script in the room can read, on the server and on every client, and that a joiner sees from their first frame (Roblox attributes on a Player; Photon player properties). Pass null to remove it. Use it for the facts a room shares about a player: the skin they wear, their team, their score this round, a ready flag. Each change reaches every client as one small message, coalesced per tick. Values must be plain JSON (string, number, boolean, array, object), at most 1 KB each and 16 KB per player. Properties live as long as the room does — they are NOT saved. Persist what should outlive the session with data.set, and copy it into a property on join: onPlayerJoined(player) { this.players.setProperty(player.id, 'skin', this.data.get(player.id, 'skin') ?? 'classic') } Server scripts only — a client script asks with events.toServer. Listeners hear the change in onPlayerPropertyChanged(player, key, value).
players.getProperty(playerId: string, key: string): unknownRead a player's replicated property — any player's, on any runtime. undefined when unset.
players.getProperties(playerId: string): Record<string, unknown>Every replicated property of a player, as a copy.

vehicles

MemberDescription
vehicles.getAll(): string[]Every vehicle (chassis) entity id in the running scene.
vehicles.getSpeed(vehicleId: string): numberSigned forward speed (m/s) — positive when driving forward. 0 if unknown.
vehicles.isGrounded(vehicleId: string): booleanTrue if any wheel is touching the ground (gate throttle/steer on this).
vehicles.getConfig(vehicleId: string): Record<string, number> | nullThe EFFECTIVE tuning (component overrides merged over the engine defaults) — real numbers, e.g. { engineForce, brakeForce, maxSteerAngleDeg, steerRate, gripAssist, ... }. null if not a vehicle.
vehicles.setConfig(vehicleId: string, patch: Record<string, number | boolean>): voidOverride tuning at runtime (partial — only the given fields change; the physics reads them next step). e.g. vehicles.setConfig(car, { engineForce: 1400, gripAssist: 0 }).
vehicles.setInput(vehicleId: string, input: { throttle?: number; steer?: number; brake?: boolean; handbrake?: boolean }): voidDrive the car: throttle −1..1 (reverse..forward), steer −1..1 (right..left), brake, handbrake. Only the given fields change. Takes effect when the car has scriptedControl (or no built-in driver).
vehicles.setThrottle(vehicleId: string, throttle: number): voidConvenience: set throttle only (−1..1). Composes with setSteer/setBrake.
vehicles.setSteer(vehicleId: string, steer: number): voidConvenience: set steer only (−1..1, left positive).
vehicles.setBrake(vehicleId: string, brake: boolean): voidConvenience: set the service brake only (all wheels).
vehicles.setHandbrake(vehicleId: string, handbrake: boolean): voidConvenience: set the handbrake only — locks + slides the REAR wheels for e-brake turns / drifts.

ui

MemberDescription
ui.showMessage(text: string, playerId?: string): voidShow a message to all players (or specific player).
ui.showChat(text: string, playerId?: string): voidA line in the chat box, for one player or everyone: a welcome on join, the answer to a command. Nobody but the recipients sees it, and it is not chat (no sender, not logged). In the play-test and a standalone build, which have no chat box, it shows as a message.
ui.createLabel(options: HudLabelOptions): HudElementHandleCreate a text label on screen. Returns a handle to update/remove it.
ui.createBar(options: HudBarOptions): HudElementHandleCreate a progress/health bar on screen. Returns a handle.
ui.createPanel(options: HudPanelOptions): HudElementHandleCreate a background panel on screen. Returns a handle.
ui.createImage(options: HudImageOptions): HudElementHandleCreate an image on screen from a project texture asset (by name or id). Returns a handle.
ui.createButton(options: HudButtonOptions): HudElementHandleCreate a clickable button on screen; options.onClick fires when the player clicks it. Returns a handle.
ui.createSlider(options: HudSliderOptions): HudElementHandleCreate a slider; options.onValueChanged fires with the new number when the player drags it. Returns a handle.
ui.createScrollbar(options: HudScrollbarOptions): HudElementHandleCreate a scrollbar — a draggable sized handle; options.onValueChanged fires with the new 0..1 position as the player drags. Returns a handle.
ui.createToggle(options: HudToggleOptions): HudElementHandleCreate a checkbox toggle; options.onValueChanged fires with the new boolean. Returns a handle.
ui.createInput(options: HudInputOptions): HudElementHandleCreate a text input; options.onValueChanged fires with the new string on commit (blur/Enter). Returns a handle.
ui.createDropdown(options: HudDropdownOptions): HudElementHandleCreate a dropdown; options.onValueChanged fires with the newly-selected index when the player picks. Returns a handle.
ui.createContainer(options?: HudContainerOptions): HudElementHandle & { add(...children: HudElementHandle[]): HudElementHandle }Create a layout container (layout group + scroll rect). Its handle has add(...children) to nest existing HUD elements inside; the container's layout (vertical/horizontal/grid) + scroll arranges them.
ui.removeElement(id: string): voidRemove a HUD element by ID.

audio

MemberDescription
audio.play(sound: string, position?: [number, number, number], options?: PlaySoundOptions): voidPlay a sound once. sound is the NAME shown in the Project panel (its asset id also works). With a position it plays in 3D at that point, else flat at the listener. In multiplayer every player hears it, unless options.player names one.
audio.playOn(entityId: string): voidStart the audioSource on an entity — its looping/ambient sound. A spatial source is heard from the entity's position and follows it if it moves. Everyone in the room hears it, INCLUDING players who join afterwards, because this is a state change on the entity rather than a one-off cue.
audio.stopOn(entityId: string): voidStop an entity's audioSource. Also stops it for players who join later.
audio.setGroupVolume(group: 'master' | 'music' | 'sfx' | 'ui', volume: number, player?: string): voidSet a mixer group's volume (0+). Groups: 'master' | 'music' | 'sfx' | 'ui'. 'master' scales everything. Use it for music/SFX sliders and ducking (lower 'music' while dialogue plays). In multiplayer this applies to everyone, unless player names one.

tools

MemberDescription
tools.give(playerId: string, toolEntityId: string, slot?: number): string | nullGive a tool to a player. toolEntityId is an entity with a tool component. Without slot the tool lands in the next free slot, which is how a backpack fills by default: one after another, no gaps. Pass slot (0-based, so slot 0 is hotbar key 1) to put it somewhere specific and leave the slots before it EMPTY — a loadout of four weapons on 1-4 and building tools on 7-9, with a deliberate space between the two groups. A slot that is already taken is refused rather than shuffled, because two tools asking for the same slot is a bug in the map worth seeing. onPlayerJoined(player) { this.tools.give(player.id, rifle.id) // slot 1 this.tools.give(player.id, pickaxe.id, 6) // slot 7, leaving 5 and 6 blank } Once a gap exists, a later give with no slot appends AFTER everything rather than backfilling the hole — a gap you asked for is not free space for the next tool to wander into.
tools.remove(playerId: string, instanceId: string, keepGap?: boolean): voidRemove a tool from a player's backpack by instance ID. By default the tools after it close up, so the backpack stays gapless — what a backpack normally does. Pass keepGap true to leave the slot empty instead and let everything else stay exactly where the player learned it was: for a fixed loadout, a weapon lost mid-round should not slide the other three under different keys.
tools.equip(playerId: string, slotOrInstanceId: number | string): voidEquip a tool for a player by SLOT (0-based) — the same number give takes, not a position in the backpack list. The two are the same until a map leaves a gap. An empty slot equips nothing. A STRING is taken as the instance id give handed back, and the slot is looked up from it. That is the form to reach for when you are holding the tool rather than counting keyboard keys — and it survives a remove re-packing the backpack underneath you, which a remembered number does not.
tools.unequip(playerId: string): voidUnequip the player's current tool.
tools.activate(playerId: string): voidPull the equipped tool's trigger from a script — Roblox's Tool:Activate(). Fires that tool's onToolActivated exactly as a click does, so a scripted trigger and a real one run the same code. Refused for the same three reasons a click is: nothing in the hand, a dead player, or a tool whose canActivate is off. It does NOT hold the trigger down for you. A held-fire tool watches for the release, so pair it with deactivate — a lone activate leaves that tool believing the button is still down.
tools.deactivate(playerId: string): voidRelease the trigger again — Roblox's Tool:Deactivate(). Fires onToolDeactivated, and only when the trigger was actually down, so calling it twice is harmless.
tools.drop(playerId: string, instanceId?: string): booleanDrop a tool on the ground in front of a player — Roblox's Backspace drop. Without instanceId it drops whatever they are holding. The tool leaves their backpack, lands a stride ahead of them, and anyone who walks over it picks it up; it times out after a few minutes so an abandoned one does not lie there for the whole round. Refused, returning false, unless the tool's canBeDropped is on. That field is OFF by default — the opposite of Roblox — so that turning drops on is a decision a map makes rather than one it inherits. A script calling this is still subject to it: the field means "this tool may be dropped", not "a player may press the key".
tools.getBackpack(playerId: string): BackpackToolRef[]Get the player's backpack contents.
tools.getEquipped(playerId: string): BackpackToolRef | nullGet the player's currently equipped tool, or null.
tools.clearBackpack(playerId: string): voidClear a player's entire backpack.
tools.getHotbarBinds(): HotbarBind[]The hotbar key binds as they currently stand — the same shape as gameSettings.hotbarBinds: each entry names a slot (0-based), the extra keys that equip it, and/or the label its corner shows instead of the number. Starts as whatever the map's Game Settings authored; reflects the last setHotbarBinds after a script has changed them. Returns a copy — edit it and pass it back.
tools.setHotbarBinds(binds: HotbarBind[]): voidReplace the hotbar key binds for every player — re-keying the bar mid-game, the way a build phase might put tools on B/N/M and a combat phase put weapons back on letters near the left hand. The whole array replaces the whole set (start from getHotbarBinds() to change one entry), and the same rules as the Game Settings field apply: the number keys always keep working, digits cannot be re-bound, several keys may share a slot, labels are trimmed to 4 characters, and junk entries are dropped rather than throwing. In multiplayer every player's keyboard and hotbar update at once, and players who join later get the current binds. Pass [] to clear every custom bind. this.tools.setHotbarBinds([{ slot: 8, keys: ['c', 'x'], label: 'C' }])

blocks

MemberDescription
blocks.raycast(origin: [number, number, number], dir: [number, number, number], maxDist?: number, opts?: BlockRaycastOptions): BlockHit | nullFirst block along a world-space ray, across every block grid (nearest wins). Read-only — works on static grids too. maxDist defaults to 8 world units. By default the ray stops on ANY block, which is what a mining or building tool wants: a tuft of grass is a block you can break. Pass { solidOnly: true } for a ray that asks "is anything in the way" — a bullet, a line of sight — and it passes through water and the walk-through blocks (plants, carpets, torches, rails, crops, lava) exactly as a player walks through them.
blocks.hitFromPlayer(playerId: string, maxDist?: number, opts?: BlockRaycastOptions): BlockHit | nullThe block a player is LOOKING AT — eye position and aim handled for you. The aim is the camera ray the player last activated a tool with, so use it inside onToolActivated (the mining pattern above). Null when the player has not activated a tool yet, or nothing is in reach.
blocks.get(gridRef: string, x: number, y: number, z: number): string | nullThe block name at a grid cell, or null for air. gridRef = the grid entity's id or name. The NAME only — a growing crop reads as 'wheat' at every stage. Use getState for the stage. A multiplayer room in Large world mode (Game Settings) holds only the regions near its players: a cell in a region it has not loaded reads as null too. A write there is applied at once and kept when the region arrives.
blocks.getState(gridRef: string, x: number, y: number, z: number): string | nullThe block at a grid cell WITH its state: 'wheat@age=3', or plain 'wheat' for a block whose state says nothing. Null for air, exactly like get. A block's state properties decide what it looks like — how grown a crop is, which way a log lies — and get deliberately drops them so that a script can key a table on the block's name. This is the other half: what it returns can be handed straight back to set, and the properties are sorted, so the same state is always the same string. const here = this.blocks.getState(grid, x, y, z) // 'wheat@age=3' this.blocks.set(grid, x, y, z, 'wheat@age=4') // one stage on
blocks.set(gridRef: string, x: number, y: number, z: number, block: string | null): booleanSet one cell: a palette block name places, null (or 'air') breaks. True when applied; false — with a Debug warning saying why — when the grid is static, unknown, or the name is not in its palette. A name may carry a STATE — 'wheat@age=3', or the 'wheat[age=3]' spelling every schematic uses — and then it places that state and nothing else. Without one, the first block of that name in the palette wins: what a script asking for 'oak_log' wants, and what a script growing a crop must not have. A state the build never seeded is refused, and the warning names the STATE rather than the block, because the block itself is plainly there.
blocks.fill(gridRef: string, min: [number, number, number], max: [number, number, number], block: string | null): booleanFill a box of cells (inclusive corners) with one block or air — arena resets, cleared areas. Capped (262,144 voxels) so one call cannot freeze a room; larger worlds are authored in the editor.
blocks.fillSphere(center: [number, number, number], radius: number, block: string | null): numberCarve or fill a SPHERE of cells at a WORLD point, across every grid — the explosion shape. Unlike fill, this takes world coordinates and needs no grid reference: a blast happens at a point in the world and takes whatever blocks are near it. null removes them (a crater), a palette name fills them. Reach for this rather than looping set over a sphere of cells. Each set is applied on its own — its own collider rebuild, its own network broadcast — so a radius-3 crater costs a room 123 of each; this is one per grid however big the sphere is. Capped at 262,144 cells, like fill. Returns how many cells were written (0 when nothing was in range, or no grid in range allows runtime edits — the reason is logged to Debug). onRocketHit(point) { this.blocks.fillSphere(point, 3, null) } // blow a hole in the world
blocks.isEditable(gridRef: string): booleanWhether a grid allows runtime edits (its component's "Players Can Edit" switch).
blocks.list(): { entityId: string; name: string; editable: boolean }[]Every block grid in the map, editable or not.

room

MemberDescription
room.log(...args: unknown[]): voidLog to EVERY player's room console. Multiple arguments join with spaces; objects are JSON-stringified.
room.forPlayer(playerId: string): { log(...args: unknown[]): void }The same log, aimed at ONE player — the same shape as camera.forPlayer(id). Use it for anything that is only that player's business: why their action was refused, what state the map thinks they are in, a value you are chasing for one reporter. In a busy room a broadcast line about one player is noise for everyone else. In the editor play-test and standalone there is exactly one player, so this targets them whatever id is passed — a script written for a room still reads the same in the play-test.
room.setProperty(key: string, value: unknown): voidSet a REPLICATED room property — one value every script in the room reads the same, on the server and on every client, and that a joiner sees from their first frame (Roblox attributes on Workspace; Photon room properties). Pass null to remove it. The place for the facts a round shares: the match phase, the countdown's end time, today's seed, the current course. Plain JSON values, at most 1 KB each and 16 KB for the whole room; live for the room's lifetime, never saved. Server scripts only. Listeners hear it in onRoomPropertyChanged. One key the platform reads: chatCommands, a list of strings naming the map's /commands for the chat box's Tab completion - the name, then a word per argument: player completes a name in the room, give|take|set one of those words, anything else is typed. ['home', 'tpa player', 'pay player &lt;amount&gt;']. The platform's own (/account, /ping) complete on every map.
room.getProperty(key: string): unknownRead a room property, on any runtime. undefined when unset.
room.getProperties(): Record<string, unknown>Every room property, as a copy.
room.getServerConfig(): Record<string, unknown>What this SERVER is configured with for maps — the map section of the node's config file (config/nodes/<box>/<name>.json in the deployment), as a copy. One map can run differently on different servers: a hard-mode server, another spawn for a server with its own world, a seed, a rule set. The values are the map's to define; the engine only carries them. onStart() { const cfg = this.room.getServerConfig() this.difficulty = cfg.difficulty ?? 'normal' if (cfg.spawn) this.players.setSpawnRule({ pick: 'first' }) // and see "Where players spawn" } Server scripts only: {} on a client, in the editor play-test, in a standalone build, and on a server whose file has no map section — so a map reads defaults everywhere it runs. Read it once in onStart; it does not change while the room lives.

data

Generic per-player data store for any custom values.

MemberDescription
data.get(playerId: string, key: string): unknownGet a value for a player. Returns undefined if not set.
data.set(playerId: string, key: string, value: unknown): voidSet a value for a player.
data.increment(playerId: string, key: string, amount?: number): numberIncrement a numeric value. Returns the new value.
data.decrement(playerId: string, key: string, amount?: number): numberDecrement a numeric value (clamped to 0). Returns the new value.
data.getAll(playerId: string): Record<string, unknown>Get all data for a player as a record.
data.has(playerId: string, key: string): booleanCheck if a key exists for a player.
data.delete(playerId: string, key: string): voidDelete a key for a player.

world

Shared world data: records of the whole persistent world (a boss defeated, a bridge repaired), revisioned so competing writes cannot both win. Server scripts only.

MemberDescription
world.load(key: string): Promise<WorldRecord | null>The record, or null when none exists. Rejects when the store could not answer.
world.save(key: string, value: unknown, revision?: number): Promise<number>Write a value. With revision, only when the record is still at it (0 = only when it does not exist yet), rejecting with 'conflict' otherwise; without, regardless. Resolves the new revision.
world.update(key: string, fn: (value: unknown, revision: number) => unknown, attempts?: number): Promise<WorldRecord | null>Read, change, write, retried on a conflict (attempts, default 5). fn gets a copy of the value (undefined when there is none) and the revision, and returns the value to write; returning undefined writes nothing. Resolves the record as written, or as it stands when nothing was written.
world.delete(key: string, revision?: number): Promise<boolean>Remove the record. With revision, only when still at it. Resolves true.
world.keys(prefix?: string): Promise<string[]>Every key, or those starting with prefix, sorted.

economy

Currency management.

MemberDescription
economy.getCoins(playerId: string): numberGet a player's coin balance.
economy.setCoins(playerId: string, amount: number): voidSet a player's coin balance.
economy.addCoins(playerId: string, amount: number): numberAdd coins to a player. Returns new balance.
economy.removeCoins(playerId: string, amount: number): numberRemove coins from a player (clamped to 0). Returns new balance.
economy.canAfford(playerId: string, coins: number): booleanCheck if player can afford a cost.
economy.getCrystals(playerId: string): numberGet a player's crystal balance.
economy.setCrystals(playerId: string, amount: number): voidSet a player's crystal balance.
economy.addCrystals(playerId: string, amount: number): numberAdd crystals to a player. Returns new balance.
economy.removeCrystals(playerId: string, amount: number): numberRemove crystals (clamped to 0). Returns new balance.

agent

NavMesh agent control (pathfinding + steering) — drive AI from your script.

MemberDescription
agent.setDestination(entityId: string, position: [number, number, number]): voidPath to and steer toward a world position.
agent.getDestination(entityId: string): [number, number, number] | nullThe agent's current destination, or null.
agent.stop(entityId: string): voidStop the agent in place (keeps the destination; resume() continues).
agent.resume(entityId: string): voidResume movement after stop().
agent.isStopped(entityId: string): booleanWhether the agent is currently stopped.
agent.resetPath(entityId: string): voidClear the destination + path; the agent coasts to a halt.
agent.rebuildNavMesh(): voidRebuild the NavMesh walkability grid from the current scene (call after spawning/destroying an obstacle at runtime) and invalidate every agent's path so they repath. No entity id — it is global.
agent.hasPath(entityId: string): booleanWhether the agent has an active destination.
agent.remainingDistance(entityId: string): numberRemaining distance to the destination (Infinity when there is none).
agent.getVelocity(entityId: string): [number, number, number]The agent's current velocity (x, y, z).
agent.setSpeed(entityId: string, speed: number): voidOverride the agent's move speed at runtime (units/sec, NavMeshAgent.speed).
agent.setAcceleration(entityId: string, accel: number): voidOverride acceleration at runtime (units/s², NavMeshAgent.acceleration).
agent.setAngularSpeed(entityId: string, degPerSec: number): voidOverride turn rate at runtime (degrees/sec, NavMeshAgent.angularSpeed).
agent.setAngularAcceleration(entityId: string, degPerSec2: number): voidOverride the turn-rate ramp at runtime (degrees/sec²; 0 = instant). A Galatrix extra.
agent.setStoppingDistance(entityId: string, dist: number): voidOverride stopping distance at runtime (units, NavMeshAgent.stoppingDistance).
agent.setAvoidancePriority(entityId: string, priority: number): voidOverride avoidance priority at runtime (0 most important … 99 least, NavMeshAgent.avoidancePriority).
agent.setObstacleAvoidanceType(entityId: string, type: 'none' | 'low' | 'med' | 'good' | 'high'): voidOverride avoidance quality at runtime; 'none' disables avoidance (NavMeshAgent.obstacleAvoidanceType).
agent.setRadius(entityId: string, radius: number): voidOverride the agent radius at runtime (NavMeshAgent.radius).
agent.setAutoBraking(entityId: string, enabled: boolean): voidToggle arrival auto-braking at runtime (NavMeshAgent.autoBraking).
agent.setAutoRepath(entityId: string, enabled: boolean): voidToggle periodic auto-repath at runtime (NavMeshAgent.autoRepath).
agent.setUpdateRotation(entityId: string, enabled: boolean): voidWhen false, your script controls the agent's facing instead of the engine (NavMeshAgent.updateRotation).
agent.setAreaMask(entityId: string, mask: number): voidSet which nav areas this agent may traverse (NavMeshAgent.areaMask, a bitmask; -1 = all).
agent.setStopped(entityId: string, stopped: boolean): voidStop or resume the agent without clearing its path (settable NavMeshAgent.isStopped).
agent.setVelocity(entityId: string, velocity: [number, number, number]): voidDrive the agent's velocity directly for one step (NavMeshAgent.velocity setter).
agent.move(entityId: string, offset: [number, number, number]): voidMove the agent by a relative offset, clamped to the navmesh (NavMeshAgent.Move).
agent.knockback(entityId: string, direction: [number, number, number] | { x: number; y: number; z: number }, force: number): voidKnock the agent back: launch it along direction (a Vector3 or [x,y,z]; y ignored) at force units/sec for a brief decaying window that overrides steering (a mini stun), then it resumes its path. For melee/explosion recoil. Halts against walls/navmesh edges so a hit can't punt it off the map. force ≤ 0 clears any active knockback.
agent.getSpeed(entityId: string): numberCurrent move-speed cap (NavMeshAgent.speed).
agent.getAcceleration(entityId: string): numberCurrent acceleration (NavMeshAgent.acceleration).
agent.getAngularSpeed(entityId: string): numberCurrent turn rate in degrees/sec (NavMeshAgent.angularSpeed).
agent.getAngularAcceleration(entityId: string): numberCurrent turn-rate ramp in degrees/sec² (0 = instant). A Galatrix extra.
agent.getStoppingDistance(entityId: string): numberCurrent stopping distance (NavMeshAgent.stoppingDistance).
agent.getRadius(entityId: string): numberCurrent agent radius (NavMeshAgent.radius).
agent.getAvoidancePriority(entityId: string): numberCurrent avoidance priority 0-99 (NavMeshAgent.avoidancePriority).
agent.getObstacleAvoidanceType(entityId: string): 'none' | 'low' | 'med' | 'good' | 'high'Current avoidance quality (NavMeshAgent.obstacleAvoidanceType).
agent.getAutoBraking(entityId: string): booleanWhether arrival auto-braking is on (NavMeshAgent.autoBraking).
agent.getAutoRepath(entityId: string): booleanWhether periodic auto-repath is on (NavMeshAgent.autoRepath).
agent.getUpdateRotation(entityId: string): booleanWhether the engine owns facing (NavMeshAgent.updateRotation).
agent.getAreaMask(entityId: string): numberThe agent's nav-area bitmask (NavMeshAgent.areaMask; -1 = all).
agent.getDesiredVelocity(entityId: string): [number, number, number]The velocity the agent WANTS this tick, before avoidance/accel limiting (desiredVelocity).
agent.getSteeringTarget(entityId: string): [number, number, number] | nullThe next corner the agent steers toward (NavMeshAgent.steeringTarget), or null.
agent.getNextPosition(entityId: string): [number, number, number]The simulated position the agent will move to (NavMeshAgent.nextPosition).
agent.getPathCorners(entityId: string): [number, number, number][]The current path's corners as world points (NavMeshAgent.path).
agent.getPathStatus(entityId: string): 'complete' | 'partial' | 'invalid'Route status: 'complete' | 'partial' | 'invalid' (NavMeshAgent.pathStatus).
agent.pathPending(entityId: string): booleanWhether a path is still being computed — always false here, pathing is synchronous (pathPending).
agent.isPathStale(entityId: string): booleanWhether the current path is stale — always false on a static navmesh (isPathStale).
agent.isOnNavMesh(entityId: string): booleanWhether the agent stands on a walkable navmesh cell (NavMeshAgent.isOnNavMesh).
agent.isOnOffMeshLink(entityId: string): booleanWhether the agent is mid-jump across an off-mesh / jump link (NavMeshAgent.isOnOffMeshLink).
agent.hasAgent(entityId: string): booleanWhether an agent with this id exists (has a navMeshAgent component the engine is tracking). Call this before other agent.* methods — they return benign defaults for unknown/deleted entities.
agent.getMovementState(entityId: string): 'idle' | 'walk'The animation movement state the engine chose ('idle' | 'walk').
agent.calculatePath(entityId: string, target: [number, number, number]): { corners: [number, number, number][]; status: 'complete' | 'partial' | 'invalid'; links: number[] }Compute a path to a target WITHOUT moving the agent (NavMeshAgent.CalculatePath). links lists the indices of corners that are off-mesh-link LANDINGS (the segment entering them is a jump).
agent.raycast(entityId: string, target: [number, number, number]): { blocked: boolean; point: [number, number, number] }Trace a straight navmesh line toward a target (NavMeshAgent.Raycast).
agent.warp(entityId: string, position: [number, number, number]): voidTeleport the agent to a position and clear its current path.

Agent-less navmesh queries (static NavMesh.* — sample/raycast/path against the grid).

MemberDescription
nav.samplePosition(position: [number, number, number]): [number, number, number] | nullNearest walkable point on the navmesh to position (NavMesh.SamplePosition), or null.
nav.findClosestEdge(position: [number, number, number]): { point: [number, number, number]; distance: number } | nullClosest navmesh boundary (wall edge) point to position (NavMesh.FindClosestEdge), or null.
nav.calculatePath(from: [number, number, number], to: [number, number, number]): { corners: [number, number, number][]; status: 'complete' | 'partial' | 'invalid'; links: number[] }Compute a path between two world points without an agent (NavMesh.CalculatePath). links lists the indices of corners that are off-mesh-link LANDINGS — the segment from corner i-1 into corner i is a jump/drop across a gap, not a walked step, so a custom movement controller should arc that segment instead of ground-steering it.
nav.raycast(from: [number, number, number], to: [number, number, number]): { blocked: boolean; point: [number, number, number] }Trace a straight navmesh line (NavMesh.Raycast); blocked = it hit a wall, point = last walkable spot.
nav.isWalkable(position: [number, number, number]): booleanWhether a world point is on the walkable navmesh.

particles

One-shot particle effects (explosions, hit sparks) at a world point. Client-side visual.

MemberDescription
particles.burst(position: [number, number, number] | { x: number; y: number; z: number }, options?: { count?: number // number of particles (default 24, capped ~400) color?: string // start color hex (default '#ffaa33') endColor?: string // fade-to color over lifetime size?: number // particle size (default 0.25) speed?: number // outward speed (default 6) lifetime?: number // seconds each particle lives (default 0.8) gravity?: number // Y acceleration (default -4) blending?: 'normal' | 'additive' // additive = glowy (default) }): voidSpawn a self-disposing particle burst at a world position — explosions, hit sparks, pickups — without authoring a prefab. All options are optional; the burst cleans itself up once the particles fade.

storage

NOT a save file, despite the name. Scratch memory for ONE script instance, held in RAM and gone when play stops — nothing here survives a reload, and no other script can see it. data.* is the store that persists (per player, across sessions). Plain instance fields (this.count = 0) do the same job as this and read better; the one thing they cannot do is stay out of the Inspector's public-field list.

Type: Record<string, unknown>

debug

Debug logging — debug.log/warn/error (Debug.Log/LogWarning/LogError route here too). Shows in the editor Console panel while play-testing (and standalone's browser console); STRIPPED from the published platform. Use it to trace map logic. (console.log is separate — native console, ships.)

MemberDescription
debug.log(...args: unknown[]): void
debug.warn(...args: unknown[]): void
debug.error(...args: unknown[]): void
debug.setEnabled(on: boolean): voidTurn debug logging on/off at runtime — affects standalone + the published platform (the editor is always on). Backed by the map's gameSettings.debugMode.
debug.enabled: booleanWhether debug logging is currently active for this runtime. (read-only)
debug.drawLine(start: unknown, end: unknown, color?: unknown, duration?: number): voidDraw a world-space debug line (backs Debug.DrawRay/DrawLine). color = a hex string/number or a Color; duration in seconds (0 = one frame). Drawn in the editor + standalone, gated like the log sink.

Vector3

Vector3 (also a [x,y,z] array): new Vector3(x,y,z), v.x, v.magnitude, Vector3.Distance(a,b).

Type: typeof Vector3

Quaternion

Quaternion (also [x,y,z,w]): Quaternion.Euler(0,90,0), Quaternion.LookRotation(dir), Quaternion.Slerp.

Type: typeof Quaternion

Color

Color (also [r,g,b,a], 0..1): new Color(1,0,0), Color.red, Color.fromHex('#ff8800').

Type: typeof Color

Mathf

Mathf: Mathf.Clamp/Lerp/Sin/PI/Deg2Rad/PingPong/….

Type: typeof Mathf

Random

Random: Random.Range(a,b), Random.value, Random.insideUnitSphere (seeded → deterministic).

Type: RandomApi

Value types

The object shapes returned or accepted by the API above.

BackpackToolRef

FieldTypeDescription
instanceIdstring
toolEntityIdstring
namestring
toolTypestring
slotIndexnumber

BlockHit

What a block raycast answers with — everything a mining/building script needs to act.

FieldTypeDescription
entityIdstringThe grid that was hit; pass it straight back into blocks.get / set / fill.
cell[number, number, number]The solid cell that was hit, in GRID CELLS. blocks.set(hit.entityId, ...hit.cell, null) breaks it.
place[number, number, number] | nullWhere a placed block goes: the empty cell in front of the hit face, or the hit cell ITSELF when what was hit is built through rather than against — a snow layer, a tuft of grass, water, as in Minecraft. Null when the ray began inside the block (nothing sensible to build on).
blockstringThe palette name of the hit block ('stone', 'oak_planks', ...). Namespace-free, and accepted back as-is by blocks.set.
distancenumberWorld-space distance to the hit, for reach checks.

BlockRaycastOptions

What a block ray may stop on.

FieldTypeDescription
solidOnlybooleanOnly stop on blocks that collide: the ray passes through water and every walk-through block.

CameraProjection

How the camera projects the world — the read side of Camera.orthographic / orthographicSize / nearClipPlane / farClipPlane / aspect.

FieldTypeDescription
orthographicbooleanTrue when the camera is orthographic (2D games, the pixel-perfect pipeline).
orthographicSizenumberHALF the vertical view height in world units (not the full height). 0 on a perspective camera.
nearClipPlanenumberDistance to the near clip plane.
farClipPlanenumberDistance to the far clip plane.
aspectnumberViewport width / height.

DeviceInfo

Device + display capabilities, read live from the host each frame.

FieldTypeDescription
mousePresentbooleanA fine pointer (mouse/trackpad) is present — Input.mousePresent.
touchSupportedbooleanThe display accepts touch — Input.touchSupported. True of any 2-in-1 laptop, so it answers "is touch possible", NOT "is this a phone". For the latter use touchPrimary.
touchPrimarybooleanA finger is the PRIMARY way this device is pointed at — a phone or a tablet. CSS (pointer: coarse), which a desktop with a touchscreen answers false because its main input is still a mouse. This is the test the built-in touch controls gate on, so an on-screen pad that uses it appears exactly where the engine is listening for one.
maxTouchPointsnumberSimultaneous touch points the hardware reports (0 = no touch). >1 ⇒ multi-touch.
dpinumberPixels per inch — Screen.dpi (0 when unknown).
orientation'portrait' | 'landscape'Screen.orientation, collapsed to the two that matter on the web.
fullScreenbooleanScreen.fullScreen.
safeArea[number, number, number, number]Screen.safeArea as [x, y, width, height] in PIXELS, bottom-left origin — the display area not covered by a notch or rounded corner. Equals the full screen rect when there are no insets.
isEditorbooleanTrue when running inside the editor's play-test rather than a published/standalone game (Application.isEditor).
runtime'editor' | 'standalone' | 'platform'WHICH runtime is executing this script: 'editor' — the editor's play-test 'standalone' — an exported build the creator hosts themselves 'platform' — a game published on Galatrix (single-player or multiplayer) The distinction is about who owns the page. In a standalone build the creator owns it, so the game may do things a stranger's game on a shared platform may not — game.openURL is gated on exactly this.
deviceType'desktop' | 'mobile' | 'tablet'The kind of machine, for laying out a game rather than for gating input. 'desktop' — a mouse is the primary pointer 'tablet' — coarse pointer, screen's short edge ≥ 600 CSS px 'mobile' — coarse pointer, smaller Read this to CHOOSE A LAYOUT (bigger buttons on a phone, a wider HUD on a desktop). To decide whether to draw touch controls at all, use touchPrimary — that is the exact test the engine's own touch layer gates on, so a pad drawn from it appears precisely where the engine is listening.

EntityRef

FieldTypeDescription
idstring
namestring
tagstring
compareTag(tag: string): booleanbooleanCompare this ref's tag. The other in onCollision/onTriggerEnter is an EntityRef; for the player it is tagged 'Player', so onTriggerEnter(other){ if (other.compareTag('Player')) … }.
parentIdstring | null
layernumber
position[number, number, number]
rotation[number, number, number, number]
scale[number, number, number]
colorstring
visibleboolean
activebooleanWhether this entity is active (enabled). Inactive entities are hidden and skip updates.
bodyType'static' | 'dynamic' | 'kinematic'

GamepadInfo

A connected gamepad (Input.GetJoystickNames + joystick axes/buttons).

FieldTypeDescription
indexnumberSlot index — the joystick number, 0-based.
namestringHardware id string (GetJoystickNames entry).
axesnumber[]Axis values −1..1, in the browser's standard-gamepad order (0/1 = left stick, 2/3 = right stick).
buttonsboolean[]Pressed state per button, in the browser's standard-gamepad order (0 = A/cross, 1 = B/circle, …).

GraphicsQualityPatch

What graphics.set accepts — the same knobs as the editor's Game Settings ▸ Quality section. Every field is optional; only the ones you pass change. These are EXACT continuous values, not the steps the platform's own settings screen happens to offer: renderScale: 0.63 and fpsCap: 45 are as valid as any round number, so a game is free to drive them from a slider. Each range below is enforced by clamping, never by rejecting — a value outside it lands on the nearest end rather than failing, so a menu cannot break the renderer with a bad number.

FieldTypeDescription
renderScalenumber0.25 – 1. Multiplier on the render pixel ratio; 1 = native. The single biggest lever.
fpsCapnumberFrames per second, 10 or more. 0 = uncapped.
shadowMode'default' | 'hard' | 'off''default' keeps the map's own shadow settings, 'hard' forces cheap single-tap filtering, 'off' stops every light casting.
shadowMapSizeMaxnumberPixels, 256 or more. A CEILING on shadow map resolution, not a size to use — a map already below it is left alone.
shadowDistanceMaxnumberWorld units, 10 – 200. A ceiling on how far shadows are drawn around the viewer.
playerBlobShadowbooleanDraw a soft disc under the player's own character in place of a cast shadow. REQUIRES shadowMode: 'off' in the same game — "in place of" is the trade it exists for, a quad instead of a whole shadow pass, and with shadows still on it would stack a disc under a character that is casting a real one. Set on its own it does nothing. Every player in the room, not only the local one, fading out past a distance from the camera. For a disc under an NPC, a prop or a vehicle, put the blobShadow component on that entity instead — it is a property of the map rather than a quality step.
maxShadowCastingLightsnumberHow many entity lights may cast shadows at once. 0 = none of them; the environment sun is exempt, being the scene's main light.
particleScalenumber0.1 – 1. Multiplier on every particle system's budget.
detailDistanceScalenumber0.1 – 1. Grass and terrain detail draw distance, and how far streamed voxel builds are drawn.
rttIntervalnumber1 – 60, whole frames. Render in-world screens and reflection probes every Nth frame.
anisotropyMaxnumber1 – 16. A CEILING on anisotropic texture filtering, applied over each texture's own Aniso Level — 1 turns it off everywhere, and a texture already below the ceiling is left alone. Anisotropy costs extra texture reads on surfaces seen at a grazing angle (floors, roads, walls running away from the camera), so it is worth the same "performance mode" button as renderScale.

GraphicsQualityState

The render quality actually in effect, after every ceiling has been applied. What a game's own options menu should display: if the player capped something lower in their platform Display settings, these are the values they are really getting, not the ones the menu asked for.

FieldTypeDescription
renderScalenumberResolution multiplier on the render pixel ratio. 1 = native.
fpsCapnumberFrame cap, or 0 for uncapped.
shadowMode'default' | 'hard' | 'off''default' = the map's own shadow settings, 'hard' = cheap single-tap filtering, 'off' = no shadows.
particleScalenumberMultiplier on every particle system's budget. 1 = as authored.
detailDistanceScalenumberMultiplier on grass/detail draw distance and block-grid visual streaming. 1 = as authored.
rttIntervalnumberOff-screen camera textures and the reflection probe render every Nth frame. 1 = every frame.
anisotropyMaxnumberThe ceiling on anisotropic texture filtering, or 0 for none — every texture keeps its own Aniso Level. There is no single number to report otherwise: anisotropy is per texture, and this caps it.
playerBlobShadowbooleanA soft disc is drawn under every player's character instead of a cast shadow. What the platform's Shadow Distance ▸ Only player step turns on, together with shadowMode: 'off'.

HudBarOptions

FieldTypeDescription
playerIdstringMULTIPLAYER: scope to ONE player (Roblox PlayerGui parity — see HudLabelOptions.playerId).
valuenumber
position[number, number]
anchor[number, number]
barColorstring
barBgColorstring
widthnumber
heightnumber

HudButtonOptions

FieldTypeDescription
playerIdstringMULTIPLAYER: scope to ONE player (Roblox PlayerGui parity — see HudLabelOptions.playerId).
textstring
onClick(player?: unknown) => voidFired when it is clicked. In MULTIPLAYER the argument is WHO clicked — the same player object onInteract hands over — because a room's HUD is many people's, and a handler that cannot tell them apart cannot do anything per-player. Undefined on a single-player host, where there is only one.
position[number, number]
anchor[number, number]
widthnumber
heightnumber
fontSizenumber
fontColorstring
bgColorstring
borderRadiusnumber
highlightedColorstringBackground while hovered / pressed (a colour-tint transition). Unset → an automatic brightness tint.
pressedColorstring
imageAssetIdstringSprite Swap (other Button transition): texture asset ids for the button's normal, hovered, pressed and disabled art. A state with no sprite falls back to the base one; with no sprites at all the colour behaviour above is unchanged.
highlightedImageAssetIdstring
pressedImageAssetIdstring
disabledImageAssetIdstring

HudContainerOptions

FieldTypeDescription
playerIdstringMULTIPLAYER: scope to ONE player (Roblox PlayerGui parity — see HudLabelOptions.playerId).
layout'vertical' | 'horizontal' | 'grid' | 'none'Child arrangement (LayoutGroup). 'vertical'/'horizontal' = flex column/row; 'grid' uses columns; 'none' = children position themselves absolutely inside. Default 'vertical'.
gapnumberGap between children (px).
paddingnumberInner padding (px).
align'start' | 'center' | 'end' | 'stretch'Cross-axis alignment of children (flex align-items).
justify'start' | 'center' | 'end' | 'between' | 'around'Main-axis distribution of children (flex justify-content).
columnsnumberColumn count when layout='grid'.
scroll'none' | 'x' | 'y' | 'both'Overflow scrolling (ScrollRect) — give the container a fixed width/height to scroll within.
clipbooleanClip children to the container box (RectMask2D) — composes with borderRadius for rounded clips.
position[number, number]
anchor[number, number]
widthnumber
heightnumber
bgColorstring
borderRadiusnumber
opacitynumber
groupAlphanumberCanvasGroup: fade the whole group (multiplies opacity) — animate it to fade a panel in/out.
interactablebooleanCanvasGroup: false makes the group non-interactive (its children can't be clicked).
blocksRaycastsbooleanCanvasGroup: false lets clicks pass through the group to whatever is behind it.
fitContent'none' | 'width' | 'height' | 'both'Content Size Fitter: shrink-wrap the container to its children on the chosen axis.
aspectRationumberAspect Ratio Fitter: keep this width : height ratio.
minWidthnumberLayout Element: min size + flex-grow weight when this container is nested in another layout group.
minHeightnumber
flexGrownumber
parentstringNest this container inside another container.

HudDropdownOptions

FieldTypeDescription
playerIdstringMULTIPLAYER: scope to ONE player (Roblox PlayerGui parity — see HudLabelOptions.playerId).
optionsstring[]The choices shown in the dropdown.
onValueChanged(index: number) => voidFires with the newly-selected index (0-based) when the player picks an option.
valuenumberInitially-selected index (default 0).
position[number, number]
anchor[number, number]
widthnumber
fontSizenumber
fontColorstring
bgColorstring

HudElementHandle

FieldTypeDescription
idstring
set(props: Record<string, unknown>): voidvoidUpdate one or more properties of this HUD element.
remove(): voidvoidRemove this HUD element from the screen.
tween(props: Record<string, unknown>, seconds: number, ease?: UiEase, onDone?: () => void): voidvoidAnimate numeric properties to new values over seconds — a menu sliding in, a prompt fading out, a bar easing to its new fill. Without this every one of those is a script stepping a number each frame and remembering to stop, which is why most UI just pops. panel.tween({ opacity: 0 }, 0.3) // fade out panel.tween({ position: [0.5, 0.1] }, 0.4, 'easeOut') // slide up panel.tween({ width: 320, height: 180 }, 0.2, 'easeInOut', () => panel.remove()) Numbers and number arrays (position, anchor) interpolate; anything else — text, a colour — is applied immediately, since there is no sensible half-way value. Tweening a property that is already animating replaces that one, so two fades cannot fight. Default easing is 'easeOut'.
stopTween(): voidvoidStop this element's tweens where they are (values keep whatever they reached).
textstring
fontSizenumber
fontColorstring
fontWeight'normal' | 'bold'
textAlign'left' | 'center' | 'right'
richTextboolean
outlineColorstring
outlineWidthnumber
textShadowboolean
verticalAlign'top' | 'middle' | 'bottom'
valuenumber
barColorstring
bgColorstring
highlightedColorstring
pressedColorstring
visibleboolean
imageType'simple' | 'sliced' | 'tiled' | 'filled'
fillAmountnumber
fillMethod'horizontal' | 'vertical' | 'radial'
fillClockwiseboolean
preserveAspectboolean
sliceBordernumber | [number, number, number, number]
tileSizenumber
raycastTargetboolean
groupAlphanumber
interactableboolean
blocksRaycastsboolean
aspectRationumber
fitContent'none' | 'width' | 'height' | 'both'
minWidthnumber
minHeightnumber
flexGrownumber
onPointer((event: string, info: HudPointerInfo) => void) | nullPointer events — assign a handler to receive pointer + drag events over this element. The event name is the first argument: 'enter' | 'exit' | 'down' | 'up' | 'click', plus the drag cycle 'beginDrag' | 'drag' | 'endDrag' and 'drop' (fired on the element under the cursor at release). The second argument ({@link HudPointerInfo}) says WHO and WHERE: info.player is the player whose pointer it is (like a button's onClick), info.elementId this element, and for a drag-and-drop pair info.targetId (on 'endDrag': the element released over) / info.sourceId (on 'drop': the element that was dragged). const slot = ui.createPanel(...); slot.onPointer = (e, info) =&gt; { if (e === 'drop') moveItem(info.sourceId, info.elementId) }. Handlers that only take the event keep working. 'drag' is throttled to 5 per second on the client, and the hover/drag stream can never crowd out a press, release or drop. Set to null to remove. Opting in makes the element receive pointer events (decorative UI stays click-through).

HudImageOptions

FieldTypeDescription
playerIdstringMULTIPLAYER: scope to ONE player (Roblox PlayerGui parity — see HudLabelOptions.playerId).
texturestringA project TEXTURE asset, by name or id — resolved client-side to the embedded image bytes.
urlstringRaw image src escape hatch (used when texture is absent).
position[number, number]
anchor[number, number]
widthnumber
heightnumber
mask'circle' | 'rounded'Clip to a shape (Mask): 'circle' = round crop, 'rounded' = borderRadius corners.
borderRadiusnumberCorner radius (px) when mask is 'rounded'. Default 8.
imageType'simple' | 'sliced' | 'tiled' | 'filled'Image Type — 'simple' (default) / 'sliced' (9-slice, corners keep their size while the middle stretches) / 'tiled' (repeat) / 'filled' (reveal only fillAmount via fillMethod — cooldowns, fill bars).
sliceBordernumber | [number, number, number, number]9-slice inset in px: a uniform number, or [top,right,bottom,left]. Default 12.
tileSizenumber'tiled': repeat tile size in px (default = the sprite's natural size).
fillMethod'horizontal' | 'vertical' | 'radial''filled': fill direction. Default 'horizontal'.
fillAmountnumber'filled': 0-1 fraction shown (Image.fillAmount). Default 1.
fillClockwiseboolean'filled': reverse the fill origin/sweep (radial: counter-clockwise; linear: from the far edge).
preserveAspectbooleanKeep the sprite's aspect ratio (letterbox) instead of stretching to the rect (Preserve Aspect).
raycastTargetbooleanIntercept pointer rays — block clicks to whatever's behind it (raycastTarget). Default false.

HudInputOptions

FieldTypeDescription
playerIdstringMULTIPLAYER: scope to ONE player (Roblox PlayerGui parity — see HudLabelOptions.playerId).
valuestringInitial text value.
placeholderstringPlaceholder shown when empty.
onValueChanged(value: string) => voidFires with the new string when the player commits the edit (blur / Enter).
position[number, number]
anchor[number, number]
widthnumber
fontSizenumber
fontColorstring
bgColorstring
contentType'standard' | 'integer' | 'decimal' | 'alphanumeric' | 'email' | 'password'Content type (InputField) — filters typing / sets the field kind.
characterLimitnumberMax characters (Character Limit).
multilinebooleanRender a multi-line textarea instead of a single line.

HudLabelOptions

FieldTypeDescription
textstring
playerIdstringMULTIPLAYER: a player ID to scope this element to (Roblox PlayerGui parity). Pass the id you get from players.getLocal().id, players.getAll()[i].id, or a server hook like onInteract(playerId) — the server then sends this element only to THAT player, so only they see it. It is an id string, not a username or index. Leave it out — or pass any falsy value (undefined, null, or '') — for a GLOBAL HUD shown to everyone (Roblox StarterGui). In single-player / standalone there is one local player, so it just renders. Every ui.createX option accepts playerId; label is the common case.
position[number, number]
anchor[number, number]
fontSizenumber
fontColorstring
fontWeight'normal' | 'bold'
textAlign'left' | 'center' | 'right'
wrapbooleanWrap into multiple lines within maxWidth px instead of one nowrap line.
maxWidthnumber
fontFamilystringCSS font-family stack, e.g. "Georgia, serif" or "monospace". Default system-ui.
richTextbooleanParse rich-text markup in the text: &lt;b&gt;, &lt;i&gt;, &lt;u&gt;, &lt;color=#f00&gt;, &lt;size=24&gt;.
outlineColorstringDraw a filled outline behind the text in this colour (Outline effect).
outlineWidthnumberOutline thickness in px (default 1).
textShadowbooleanDrop shadow, on by default for readability; set false to remove it.
verticalAlign'top' | 'middle' | 'bottom'Vertical placement within the element's box (needs a height/frame to show).
bestFitbooleanBest Fit: shrink the font so the text fits the label's box, between bestFitMin and bestFitMax (or fontSize). Needs a bounded box — set width AND height for it to have an effect.
bestFitMinnumber
bestFitMaxnumber
widthnumberBox size in px — used by Best Fit as the bounded rect the text shrinks to fit.
heightnumber

HudPanelOptions

FieldTypeDescription
playerIdstringMULTIPLAYER: scope to ONE player (Roblox PlayerGui parity — see HudLabelOptions.playerId).
position[number, number]
anchor[number, number]
widthnumber
heightnumber
bgColorstring
borderRadiusnumber
opacitynumber
visiblebooleanStart hidden — for UI a script reveals later (a joystick ring, a prompt). Default true.

HudScrollbarOptions

FieldTypeDescription
playerIdstringMULTIPLAYER: scope to ONE player (Roblox PlayerGui parity — see HudLabelOptions.playerId).
valuenumberInitial handle position 0..1 (default 0).
onValueChanged(value: number) => voidFires with the new 0..1 position as the player drags the handle.
handleSizenumberHandle length as a fraction of the track 0..1 (Scrollbar.size). Default 0.2.
direction'horizontal' | 'vertical'Scrollbar axis. Default 'horizontal'.
position[number, number]
anchor[number, number]
widthnumber
heightnumber
bgColorstring
barColorstring

HudSliderOptions

FieldTypeDescription
playerIdstringMULTIPLAYER: scope to ONE player (Roblox PlayerGui parity — see HudLabelOptions.playerId).
valuenumberInitial value (within [min, max]).
onValueChanged(value: number) => voidFires with the new number when the player drags the slider.
minnumber
maxnumber
stepnumber
position[number, number]
anchor[number, number]
widthnumber
bgColorstring
barColorstring

HudToggleOptions

FieldTypeDescription
playerIdstringMULTIPLAYER: scope to ONE player (Roblox PlayerGui parity — see HudLabelOptions.playerId).
valuebooleanInitial on/off state.
onValueChanged(value: boolean) => voidFires with the new boolean when the player toggles it.
labelstringOptional label shown next to the checkbox.
position[number, number]
anchor[number, number]
fontSizenumber
fontColorstring
barColorstring
groupstringA ToggleGroup id — toggles sharing it act like radio buttons (exactly one on).

PlayerRef

FieldTypeDescription
idstring
usernamestring
position[number, number, number]
entityIdstringOn a player-template map, this player's CHARACTER as a GameObject on the machine the script runs on — Roblox's Player.Character. In a client script every player has one: your own is the template entity you simulate (push it with physics.applyForce(entityId, …)); another player's is a local copy of the template's visual, posed from their reports, that you may dress — game.getEntity(player.entityId).setComponent('meshRenderer', { customTextureAssetId }) from onPlayerPropertyChanged is how a replicated skin reaches every screen. Patches stay local; its transform is re-posed every frame. Absent on the server and on maps with the built-in player.
guestbooleanTrue when this player is a GUEST, with no account (onPlayerChat only): a script may keep some commands for players who can be told apart from one visit to the next. Absent on other hooks.
role'admin' | 'moderator'The platform's staff role of this player (onPlayerChat only): 'admin' or 'moderator', absent for everyone else. Decided by the platform from the account, never by the client, so a script may keep some commands for staff (/god). Absent on other hooks.

PlaySoundOptions

Extras for a one-shot: how loud, how fast, which bus, and whether only one player hears it.

FieldTypeDescription
volumenumber0+, default 1.
pitchnumberPlayback rate, default 1. Above 1 is higher and faster.
group'master' | 'music' | 'sfx' | 'ui'Mixer bus, default 'sfx'.
minDistancenumberHow far the sound carries, for a POSITIONAL one-shot (ignored without a position). The same three settings the audioSource component has, and the same meanings — Min/Max Distance and the rolloff mode. minDistance is full volume: the sound does not begin to fall off until this far away. That is the one to raise for anything big. An explosion heard at 20 m through the default 1 m reference is at roughly 5% volume, which no amount of volume fixes — the curve takes it straight back. A blast wants a minDistance around its own blast radius, so everyone it affected hears it in full. rolloffMode: 'logarithmic' (default, natural, stays faintly audible past maxDistance) or 'linear' (silent exactly at maxDistance).
maxDistancenumber
rolloffMode'logarithmic' | 'linear'
playerstringPlay it for ONE player only. Everyone else hears nothing — for a sound that is about them (a pickup chime, a private warning) rather than about the world.

RagdollHandle

What physics.createRagdoll returns — live handles to the spawned limbs.

FieldTypeDescription
partsEntityRef[]Every limb, parent-first (parts[0] is the pelvis).
rootEntityRef | nullThe pelvis (root) limb — apply impulses/forces here to move the whole ragdoll. Null if spawn failed.
idsstring[]The entity ids of every limb.
destroy(): voidvoidRemove the entire ragdoll (destroys every limb).

RagdollSpawnOptions

Tuning for physics.createRagdoll.

FieldTypeDescription
heightnumberTotal height in world units (a normal humanoid ≈ 1.8). Default 1.8.
massnumberTotal mass spread across the limbs. Default 70.
colorstringLimb colour (hex). Default a neutral skin grey.
tagstringTag applied to every limb. Default 'Ragdoll'.
layernumberCollision layer (0-31) for every limb. Default 0.

RaycastHit

FieldTypeDescription
entityIdstringThe entity id of the collider that was hit.
entityNamestringThe name of the hit entity (convenience — same as game.getEntity(entityId)?.name).
point[number, number, number]World-space point of contact [x, y, z].
normal[number, number, number]World-space surface normal at the contact [x, y, z] (unit length).
distancenumberHow far along the ray/sweep the hit is: for raycast the distance from origin to point; for sphereCast how far the sphere's CENTRE travelled before contact. 0 when the cast already overlaps a collider at origin (see overlapping) — a shape starting inside/touching geometry reports contact-at-start, so distance is a free-space gap ONLY when the cast begins clear.
overlappingbooleansphereCast only: true when the sphere was already touching/overlapping this collider at origin, so distance is 0 because of contact-at-start — NOT a measured gap. Lets you tell "already inside it" apart from a genuine zero gap (the reason a raw distance reads as hit/no-hit only). When true, offset origin back along -direction and cast again for the real gap, or treat it as blocked. normal still points away from a shallow contact surface (useful for backing off); on a DEEP overlap the normal degrades toward zero. Undefined for raycast.

ScriptMathCurveAPI

math.curve — interpolation along an arc or a spline, for the cases straight lerp gets visibly wrong.

FieldTypeDescription
slerpVec3(a: [number, number, number], b: [number, number, number], t: number): [number, number, number][number, number, number]Interpolate through the arc between two vectors instead of across it (Vector3.Slerp); magnitude interpolates linearly. lerpVec3 on two directions shortens the vector mid-way.
catmullRom(p0: [number, number, number], p1: [number, number, number], p2: [number, number, number], p3: [number, number, number], t: number): [number, number, number][number, number, number]Catmull-Rom spline point at t (0..1) on the p1→p2 segment; p0/p3 are the neighbouring waypoints that shape the tangents. The curve passes THROUGH its control points — use it for camera rails.

ScriptMathGeoAPI

math.geo — ray/point/segment queries against pure geometry. Unlike physics.raycast, which hits COLLIDERS, these hit maths: use them for AI, targeting, gizmos and anything with no body in the scene.

FieldTypeDescription
closestPointOnSegment(p: [number, number, number], a: [number, number, number], b: [number, number, number]): [number, number, number][number, number, number]Nearest point to p on the segment a→b (clamped to the segment, not the infinite line).
rayPlane(origin: [number, number, number], dir: [number, number, number], planePoint: [number, number, number], planeNormal: [number, number, number]): MathRayHit | nullMathRayHit | nullWhere a ray meets an infinite plane, or null if parallel or behind the origin.
raySphere(origin: [number, number, number], dir: [number, number, number], center: [number, number, number], radius: number): MathRayHit | nullMathRayHit | nullWhere a ray meets a sphere, or null if it misses. An origin INSIDE the sphere returns the exit point.
rayAABB(origin: [number, number, number], dir: [number, number, number], boxMin: [number, number, number], boxMax: [number, number, number]): MathRayHit | nullMathRayHit | nullWhere a ray meets an axis-aligned box, or null if it misses. An origin inside the box gives distance 0 (matching Bounds.IntersectRay). Corners may be passed in either order.
pointInBox(p: [number, number, number], boxMin: [number, number, number], boxMax: [number, number, number]): booleanbooleanIs the point inside (or on) an axis-aligned box? Corners may be passed in either order.

ScriptPlayerCameraAPI

The write half of the camera API, scoped to a single player. See ScriptCameraAPI.forPlayer.

FieldTypeDescription
setType(type: 'custom' | 'scriptable' | 'default'): voidvoid
setPosition(position: [number, number, number]): voidvoid
setRotation(rotation: [number, number, number, number]): voidvoid
lookAt(target: [number, number, number]): voidvoid
setFov(degrees: number): voidvoid
setOrbitDistance(distance: number): voidvoid
setZoomRange(min: number, max: number): voidvoidBound how far THIS player may zoom the third-person camera (Roblox CameraMinZoomDistance / CameraMaxZoomDistance). setZoomRange(0, 0) is LockFirstPerson: the camera pins to the eyes and scroll/pinch cannot pull it back. setZoomRange(0, 40) restores the default. Enforced inside the camera itself, so every zoom writer — wheel, pinch, vehicle framing, scripts — obeys it.
shake(strength?: number, duration?: number): voidvoid

SpawnOptions

What a physics.raycast / raycastAll / sphereCast hit returns. null (or an empty array for raycastAll) means nothing was hit within maxDistance. Options for game.spawnEntity.

FieldTypeDescription
solidbooleanWhether the spawn collides with anything. Defaults to true, matching CreatePrimitive: the entity gets a static rigidBody and its mesh provides the collider. false spawns the mesh alone — the entity is visible, raycasts and triggers still see it, but nothing walks into it.

TouchInfo

A single active touch. position/deltaPosition are in 0-1 screen coords (top-left origin), consistent with the rest of the input API (a swipe across the screen ≈ 1.0). Multiply by sensitivity for look/drag.

FieldTypeDescription
fingerIdnumberStable id for this finger across frames (Touch.fingerId).
position[number, number]Touch position this frame, [x, y] in 0-1 screen coords (top-left origin).
deltaPosition[number, number]Movement since last frame, [dx, dy] in 0-1 screen units (Touch.deltaPosition).
phase'began' | 'moved' | 'stationary' | 'ended' | 'canceled'Lifecycle this frame (TouchPhase, lowercased): a touch is 'began' the frame it lands, 'moved'/'stationary' while held, then 'ended' (lifted) or 'canceled'.
tapCountnumberNumber of taps in quick succession (Touch.tapCount).
pressurenumberFinger pressure 0..1 (Touch.pressure / maximumPossiblePressure=1). 1 when the device can't measure it — hardware without pressure support always reports 1.0.
radiusnumberContact radius in PIXELS (Touch.radius). 0 when the device doesn't report a contact size.
deltaTimenumberSeconds since this finger last moved (Touch.deltaTime).

WorldRecord

A shared world record as read: its value and the revision it was read at.

FieldTypeDescription
valueunknown
revisionnumberCounts up on every write. Pass it back to save() so a write lands only on what you read.