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.
function onSerialize(stream: PlayerStateWriteStream): voidWrite this player's state for every other player to read: called 20 times a second, after onUpdate, in the client scripts and player-template scripts on the machine that simulates this player. A key keeps its value until written again; null removes it. Only changes are sent. onSerialize(stream) { stream.write('squash', this.squash) } A value is a finite number, a string of up to 64 characters, a boolean or an array of up to 4 numbers; a player's state holds up to 16 keys and 512 bytes. A write that breaks a rule throws. It comes from the player's own machine, so keep it to how the character looks and leave anything that matters (damage, score, cooldowns) to a server script.
function onDeserialize(player: PlayerRef, stream: PlayerStateReadStream): voidAnother player's state changed (their onSerialize), in client scripts and player-template scripts on every other machine. Delivered before the next onUpdate, and once for each player who already has state when the scripts start. Never for your own player. onDeserialize(player, stream) { const s = stream.read('squash') ?? 0 const body = this.game.getEntity(player.entityId) if (body) body.scale = [1 + s, 1 - s, 1 + s] } stream.read(key) answers the value or undefined, has(key) whether it is set, keys() every key. In a client script on a player-template map, player.entityId is their character as drawn on this screen: its scale, material and visibility are yours to set.

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'). In a room a server script's change reaches every player; a client script's change stays on that player's machine until a server script changes the same entity's tag.
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). A change while the game runs reaches every script and physics.overlapSphere / overlapBox; collisions, raycasts and camera culling keep the layer the entity was built with. See the physics guide for what that means in multiplayer.
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. In a client script the NavMeshAgent and health handles throw on every member, and neither component can be written (NPC steering and entity health live on the server). The rigidBody members that act on the simulation work on your own character's body only and throw for any other entity; position, rotation and the freeze flags stay readable on any body, and the body type throws, read or written. Patching a rigidBody component's fields (setComponent, or a field on the handle) follows the same rule; adding a body, removing one, writing it null or setting enabled decides its type and throws too, as does adding, removing or writing null (or anything but an object) to a characterController, boundaryBox or waterZone on an entity with a rigidBody or an NPC's navMeshAgent (the physics builds no body for those).
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. A misspelled component name throws, naming the component: addComponent('rigidbody', …) asks "did you mean rigidBody?".
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. A field in the patch replaces that field whole, except meshRenderer.materialSlots, which merges slot by slot: setComponent('meshRenderer', { materialSlots: { 1: { color: '#3b82f6' } } }) recolours material 1 of a model (its index in the model file) and leaves the model's other materials alone; null removes a slot's override. { materialAssetId } gives the slot a Material asset's look. meshRenderer.seeThrough draws the entity's silhouette through walls in one flat colour (a glowing player seen behind blocks): setComponent('meshRenderer', { seeThrough: { color: '#ffaa00', opacity: 0.6 } }), null for none. Only where something stands in front of it; in plain view its own look shows. Every mesh of it, a model's in its animated pose; hidden parts, layers and opacity-0 parts are left out. color defaults to '#ffffff', opacity (0 to 1) to 1; the object is replaced whole. meshRenderer.instanced: true draws the entity together with the other instanced meshes that look the same, one draw call for many copies (dropped items, creature parts); it still moves, hides, collides and is hit by raycasts on its own. Its colour and opacity may differ from the others'. A mesh that cannot share a draw (skinned, blended transparency, a custom shader) draws on its own. A misspelled component name throws, naming the component.
entity.removeComponent(type: string): voidRemove a component from this entity (cannot remove 'transform'). A misspelled component name throws, naming the component.
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 = …, and reaches players the same way).
entity.getChildren(): EntityRef[]Get references to all child entities.
entity.bodyType: 'static' | 'dynamic' | 'kinematic'Physics body type (read-only). Server scripts only: the room never sends a body's type to players, so reading it in a client script throws. (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.canSavePage(): booleanWhether game.savePage can offer the page as a file here. True only in a standalone build whose page was served from a website (http or https). False in the editor play-test, in a game published on Galatrix, for a standalone page opened from disk, and in an offline-room build. Use it to show or hide a Download button.
game.savePage(fileName: string): booleanOffer the page the player is playing as a file to save, named &lt;fileName&gt;.html. Path characters and a trailing .html are removed from the name; an empty name means the page's title. The page is read again from its own address, so the file is what that address serves: the whole game for a single-file HTML build, only index.html for a ZIP build. The script chooses the name, not the contents or the address. Returns true when the save was started; it finishes on its own. If the page cannot be read, a warning is logged and nothing is offered. Where canSavePage() is false it does nothing and returns false, and the editor logs why. Call it from something the player did, such as a HUD button's onClick, as for openURL: a browser may refuse a download the player did not ask for. One per press, as for saveFile.
game.getPageConfig(): Record<string, unknown> | nullThe page config: the JSON object set in Game Settings (or the Build window) as Page config, which a standalone build writes into its page as &lt;script type="application/json" id="game-config"&gt;. The owner of a built page can edit that block, and this returns what the page holds now. Each call returns a fresh copy, so changing it changes nothing else. Null in the editor play-test, in a game published on Galatrix, and when the block is missing, is not valid JSON, or is not an object. An empty config reads as {}. It is data only: read the fields a map expects and check their types, as the page's owner may have typed anything.
game.canFullscreen(): booleanWhether game.setFullscreen can put the page in full screen here. True only in a standalone build, in a browser that allows it (an iPhone's browser keeps full screen to videos). False in the editor play-test and in a game published on Galatrix. Use it to show or hide a full screen button.
game.isFullscreen(): booleanWhether the page is in full screen now (the player may leave it with Esc at any time).
game.setFullscreen(on: boolean): booleanPut the page in full screen, or take it out. Call it from something the player did, such as a HUD button's onClick: a browser refuses full screen otherwise. Returns true when it was asked of the browser; read isFullscreen() a moment later for the outcome. Where canFullscreen() is false it does nothing and returns false.
game.canSaveFile(): booleanWhether game.saveFile works here. True in the editor play-test, in a standalone build, and in a client script of a game published on Galatrix. False in a server script in a room, which runs on no player's browser. Use it to show or hide a button that saves a file.
game.saveFile(fileName: string, text: string): booleanOffer text to the player as a file to save, named fileName. Path characters and leading dots are removed from the name, and the file is .json or .txt: a name ending in neither gets .json in place of any other extension (a name with none gets .json added), so game.saveFile('my-world', text) offers my-world.json. The file is offered as application/json or text/plain to match. The text may be up to 32 MB as UTF-8; a larger one is refused with a warning. Call it from something the player did, such as a HUD button's onClick or a key press: otherwise it does nothing and returns false. One per press: a second save, page save or file picker from the same press is refused too. Returns true when the download began. Where canSaveFile() is false it does nothing, logs why, and returns false.
game.canOpenFile(): booleanWhether game.openFile works here. True wherever canSaveFile() is.
game.openFile(accept: string, onFile: (file: { name: string; text: string }) => void): booleanLet the player choose a file, and read it as text. Opens the browser's file picker for the extensions in accept, a list such as '.json,.txt' (extensions only). Call it from something the player did, such as a HUD button's onClick: otherwise it does nothing and returns false (one per press, as for saveFile). Returns true when the picker opened. The one file the player chooses is read as UTF-8 text, and onFile({ name, text }) is called with it in a later frame of this script. A file over 32 MB is refused with a warning, and a cancel calls nothing. The script only ever sees the file the player chose. Its text can be anything, so check it before using it: parse JSON inside a try, and check every field it should have.
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. Server scripts only, like healEntity and getEntityHealth: entity health stays in the room and is not sent to players, so all three throw in a client script.
game.healEntity(entityId: string, amount: number): voidHeal an entity with a health component, clamped to its max. Server scripts only.
game.getEntityHealth(entityId: string): { current: number; max: number } | nullCurrent + max HP of an entity with a health component, or null if it has none. Server scripts only.
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. A misspelled component name throws, naming the component.
game.setComponent(entityId: string, type: string, patch: any): voidEdit an existing component on any entity: merges the patch onto its current fields. As on the entity, meshRenderer.materialSlots merges slot by slot, and a misspelled component name throws.
game.removeComponent(entityId: string, type: string): voidRemove a component from any entity (cannot remove 'transform'). A misspelled component name throws, naming the component.
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.createTexture(width: number, height: number, options?: ScriptTextureFormat | ScriptTextureOptions): ScriptTextureA texture this script fills with pixels: fog of war, a minimap, a painted wall, an image received as data. It exists on the machine whose script made it, so in a room make it in a client script; a server script there throws. Its id goes wherever a texture asset id goes on something in the world: a meshRenderer's texture, a sprite, particles, or a custom shader's texture value. The script HUD cannot show it, since HUD images load from a URL. const paint = this.game.createTexture(64, 64) // 'rgba8': 4 bytes a pixel paint.setPixels(pixels) // Uint8Array, 64 x 64 x 4 this.entity.setComponent('meshRenderer', { customTextureAssetId: paint.id }) const fog = this.game.createTexture(128, 128, 'r8') // 1 byte a pixel, for a shader this.entity.setComponent('meshRenderer', { shaderUniforms: { uFog: fog.id } }) options is a format, 'rgba8' (the default, 4 bytes a pixel) or 'r8', or { format, filter } with filter 'linear' (the default) or 'nearest' for sharp pixels. 'rgba8' is a colour image, read like a texture asset (sRGB); 'r8' is data, read exactly as written (0 to 255 reads 0 to 1), so a mask or a height belongs in 'r8'. Sides are 1 to 2048. A game holds at most 256 at once and 64 MB of pixels (width x height x 4 for 'rgba8', x 1 for 'r8'); past either, createTexture throws naming the limit. dispose() gives one back, and they are freed with the entity that made them, when play stops and when the game closes.
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. From a server script in single-player runtimes only (the editor play-test and standalone builds). A client script's assignment throws everywhere, and in a room a server script's does nothing: every player there shares one clock. 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 a press that lifts within 300 ms without moving. 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. Reported while the finger is still down, and its lift reports no tap, so a map can use taps as clicks and long presses as holds. 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. The editor, a standalone build, single-player and a client script in multiplayer all run on the machine with the screen, so this works there. A server script in MULTIPLAYER runs where there are no fingers and reports nothing: in one, 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. A CLIENT script drives its own player, whose controls are on its machine: leave player out or pass players.getLocal(). Naming anyone else throws.
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. From a client script the mode changes on this player's screen only.
input.setMouseLook(scheme: MouseLookScheme | null): voidHow the mouse turns the built-in camera, per button, while that button is held on the game view: 'capture' — the cursor is hidden while held; turning has no limit. 'drag' — the cursor stays visible and moves with the drag; turning stops at the window edge. A left 'drag' keeps tap-to-use: a click without moving still uses the equipped tool. 'pinned' — the cursor stays visible where it was pressed; turning has no limit. 'none' — the button does not turn the camera (the default for a button left out). input.setMouseLook({ left: 'drag', right: 'capture' }) Once a scheme is set, clicking the game view no longer captures the cursor, so it stays free for the HUD. null goes back to the default: click to capture. Clicks on HUD elements never start a turn. In MULTIPLAYER a server script's scheme reaches every player, including those who join later; a client script's applies to its own screen. Ignored on touch screens (see setLookMode).
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. Server scripts only. A client script runs on the phone itself, so it reads getGestures/getPinch directly and acts on them; its call to this throws.
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. Pasted text (Ctrl+V / Cmd+V) arrives as if typed: its line breaks become spaces, other control characters are dropped, and one paste adds at most 1024 characters. A letter pressed with Ctrl or Cmd is a shortcut and types nothing (Ctrl+V adds no 'v'); AltGr (Ctrl+Alt) still types. Not while a page text field has focus.
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.
cursor.setImage(texture: string | null, hotspot?: [number, number], size?: number): voidDraw the cursor as an image: a texture asset's name or id. The system draws it, so it never trails the pointer. hotspot is the point of the image that clicks, in the image's own pixels ([0, 0] is its top-left corner, the default). size is the longest side on screen in CSS pixels, 32 by default, from 8 to 128 (browsers ignore bigger cursors). null puts the system cursor back. setVisible(false) still hides it, and setVisible(true) brings the image back. Only the player's own screen changes; in a server script it does nothing. this.cursor.setImage('AttackCursor', [4, 4]) // the tip of the sword clicks this.cursor.setImage(null) // the system cursor again

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 player is named — a cutscene script written for a room still runs in the play-test. Pass player.id or the player; a missing one throws. A client script can name only its own player.
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.setLookDirection(direction: [number, number, number]): voidTurn the built-in player's camera to face a world direction. The orbit carries on from there, and walking forward goes the new way at once. camera.setLookDirection([0, 0, -1]) // face -Z, level camera.setLookDirection(players.getFacing(id)) // look the way the body faces y tilts the view (negative looks down), clamped to the same range the mouse can reach. A zero, straight up or straight down direction throws: there is no heading to turn to. The body is not turned; use players.teleport with a yaw for that. Works in 'scriptable' mode too, so a cutscene can hand the camera back facing the right way. Pairs with players.getLookDirection, which reads the same direction. In MULTIPLAYER a server script's call reaches that player's screen a round trip later (everyone's, without forPlayer). Players who join later do not get it, so a call in onStart turns no one's camera in a room: turn each one in onPlayerJoined with camera.forPlayer(player.id). A client script's call turns its own camera at once, which is the one to use for a turn every frame.
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. In a client script, only on your own character — players.getLocal().entityId, on a map whose player template has a characterController. Any other entity throws, and so does isGrounded. A server script's call works in single-player runtimes and does nothing in a multiplayer room; move other characters there by setting their position.
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.setSpeed(entityId: string, speed: number, clipName?: string): voidChange the speed of the clip playing on an entity without restarting it, as play's speed would set it: 0 holds the pose where it is (a hit pause), 0.25 is slow motion, then set it back to the speed it was played at. With clipName, only while that clip is the one playing. Lasts until the next play or animator state change. A frozen clip still counts as playing. In a room a server script's change reaches every player and a client script's shows on its own screen only. The speed is a number of 0 or more; anything else throws.
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.
animation.setJointRotation(entityId: string, joint: string, rotation: [number, number, number, number] | [number, number, number] | null, options?: { mode?: 'add' | 'replace'; weight?: number }): voidTurn one joint of an entity's model from a script, over whatever its clips pose: a head that follows where it looks while the walk clip plays, an arm held out, a pose set outright. joint is one of getJoints(entityId). rotation is a quaternion [x, y, z, w], or 3 values = Euler angles in DEGREES, as entity.rotation takes them, in the joint's parent's axes. options.mode: 'add' (the default) turns the joint on top of the pose its clip gives it that frame, and the clip's own motion plays on inside the turn; 'replace' sets the joint's local rotation outright, clip or not. options.weight (0 to 1, default 1) blends from the clip's pose to that. null clears the joint. A turn lasts through clip changes and fades until it is cleared, the entity's model is swapped, the entity is destroyed or a scene loads. A client script's turn shows on its own player's screen only; a server script's reaches every player. It is drawn only: colliders do not follow. An unknown joint throws, naming the model's joints.
animation.getJoints(entityId: string): string[]The joints setJointRotation can turn on an entity's model: an imported model's named nodes (by the names its clips' tracks use: spaces as _, the characters [ ] . : / dropped), or a procedural mesh's bones. A model split into parts that each carry a copy of the skeleton lists each bone once, and turning it turns every copy. [] when there are none.

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. One still waiting when the scripts end (a map switch, a reconnect) never settles: the script stays where it awaited.
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). A server script's call reaches every player. A client script's call applies to that player only; if a server script later sets the same keys, its values replace them (the later call wins). 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. Sky keys: skyMode 'gradient' blends skyColor (straight up) to skyGradientBottom (straight down); skyGradientStops adds [position, colour] pairs between them (position 0-1, linear in elevation, 0.5 = the horizon; up to 32), e.g. a horizon that meets the fog: { skyMode:'gradient', skyColor:'#6f9cff', skyGradientBottom:'#c0d8ff', skyGradientStops:[[0.4, '#a8c4ff'], [0.5, '#c0d8ff']] }. Fog covers everything past fogFar; a mesh that must show beyond it (a sun, a moon, stars behind the hills) is drawn without it: this.entity.setComponent('meshRenderer', { fog: false }) (Fog off in the Inspector).

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 here is antialiasing: it is fixed when the WebGL context is created, so it is setAntialias's, and takes effect at the next start.
graphics.reset(): voidDrop this game's ceiling entirely — back to whatever the player and creator settled on (on an override map, a player's shadow Off included).
graphics.getPlayerShadows(): PlayerShadowSettings | nullThe player's own shadow choices from the platform's graphics settings (see PlayerShadowSettings). Answered wherever the multiplayer client runs a client script: rooms, and standalone builds made with Offline multiplayer room. null where those settings do not apply: the editor's play-test, ordinary standalone builds, and server scripts. Pairs with Scene Settings ▸ Shadows ▸ Override player settings, e.g. keep the map's shadows but pull them in for a player who picked a short distance: const p = graphics.getPlayerShadows(); if (p && p.distance === 'low') graphics.set({ shadowDistanceMax: 25 })
graphics.getAntialias(): { now: boolean; next: boolean } | nullAntialiasing (smoothed edges): now what this run was started with, next what the next start will use. A standalone build only, which is where a game's own choice applies: null everywhere else (a multiplayer room follows the player's platform settings, the editor's play-test its own viewport).
graphics.setAntialias(on: boolean): voidThe game's own antialiasing choice, kept on this device for this game alone and used from its next start (it is fixed with the WebGL context). Off is the default, the player's platform setting otherwise. A standalone build only; inert elsewhere. e.g. an options menu: graphics.setAntialias(true), then say "applies when the game is opened again" while getAntialias().next differs from .now.

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.completeAchievement(playerId: string, key: string): voidComplete one of this game's achievements for a player. It is saved to their account, and they see "Achievement unlocked" once. On a trigger zone at the top of a tower: onPlayerEnter(player) { this.players.completeAchievement(player.id, 'reached_the_top') } The key is one of your game's achievement keys (a game has up to 10). Calling it again for a player who already has it does nothing, so you can call it every time the condition holds. Server scripts only: a client script or a player template's script gets an error. Nothing is saved for guests, in the editor's play-test, in a Test Online session or in a standalone build; the play-test prints the completion to the console so you can check the trigger. A key your game does not have is reported once, as a warning in the game console (the backtick key).
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, enemy hits — are no-ops for that player. Falling out of the map (below the scene's kill height) still kills: the void is the map ending, not damage. 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 everyone's, and everyone who arrives later. The speed is a number of 0 or more; anything else throws.
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, and everyone who arrives later. The force is a number of 0 or more; anything else throws.
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): booleanWhether a player is flying. In a client script, your own player only: flight is not sent to other players, so another player's id throws.
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', or the id of one of this map's own entities or models: an entity is drawn as a held tool is (its renderer, colour, material slots and child parts), at the size it is drawn in the world; a model is drawn plain. 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'), or a shield at rest in the off hand: players.addAttachment(playerId, 'leftHand', game.findEntity('ShieldLook').id).
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.
players.getState(playerId: string, key: string): unknownThe latest value in a player's state stream (what their onSerialize wrote), or undefined. For your own player, what your scripts last wrote; for anyone else, the last value received. A server script reads the room's copy, which that player's machine wrote: treat it as untrusted.

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 ​

The script HUD. Pixel sizes (fontSize, width, height, margins) are authored against 1920 x 1080 and scale with the screen by sqrt(W/1920 * H/1080), about 0.4 on a phone. gameSettings.hudMinScale floors that scale; a script that places things by the scale then uses max(that scale, hudMinScale).

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.setVisible(visible: boolean, playerId?: string): voidShow or hide every script HUD element at once, for a game's "hide the GUI" key. Each element keeps its own visible flag, so showing the HUD again brings back exactly what was up. Messages, the hotbar, chat and other platform overlays are not script HUD elements and stay. A playerId hides it for that one player only. Lasts until play ends or the scene changes.
ui.isVisible(playerId?: string): booleanWhether the script HUD is shown (ui.setVisible), for everyone or for one player. True by default.
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 (Enter, or leaving it). 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): HudContainerHandleCreate 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. A scroll box is moved and read with scrollTo, scrollIntoView, getScroll and onScroll.
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 and not declared under Run-time states. 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 palette lacks is added when its block is declared under Run-time states; otherwise it is refused, and the warning names the STATE rather than the block, because the block itself is plainly there. A fence, a pane or a wall joins the blocks beside it: a wall and a pane join each other, a fence joins fences of its own stuff (wood, or nether brick), and a fence or a wall joins a gate at the ends of its line, not beside its face. A wall's arm rises the whole block where a block over it covers it, and a straight run or a crossing shows its post only where what stands on it asks for one: another wall's post, a torch, a sign, a banner, a pressure plate, or a block resting on its middle. A body meets a fence, a wall and a shut gate a block and a half high, so none is jumped over, and an open gate not at all. One whose state names its arms joins those sides and no others, whatever stands there: 'oak_fence@arms=north_south', 'glass_pane@arms=none'; such a wall keeps its post and its arms stop short of its top. A wall with 'post=false' stands without its post. A chorus plant (any of the six sides) and a tripwire name theirs the same way. A game with joining rules of its own works the sides out and writes them, rewriting a block's neighbours when it changes.
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). A client script writes only the grids that are each player's own, a server script only the others. onRocketHit(point) { this.blocks.fillSphere(point, 3, null) } // blow a hole in the world
blocks.setRegion(gridRef: string, min: [number, number, number], size: [number, number, number], cells: readonly number[] | Uint8Array | Uint16Array | Uint32Array | Int8Array | Int16Array | Int32Array, names: (string | null)[]): booleanWrite a whole box of cells in one call, each cell its own block: a world arriving from a server a column at a time, a level loaded in pieces, a big area rebuilt. min is the box's lowest corner in grid cells, size its width, height and depth. cells holds one number per cell — an array or a typed array (Uint8Array, Uint16Array), x fastest, then y, then z — and names[n] says which block the number n is: a palette name (with a state if it needs one, 'oak_log@axis=x'), or null / 'air' for air. One names table (up to 65,536 entries) can serve every call; only the numbers the cells use are looked at. The whole call is checked before anything is written: a number with no name, a name neither in the build's palette nor declared under Run-time states, a cells of the wrong length or a box over the cap (262,144 cells) is refused with a Debug warning and changes nothing, not even the states the call would have added. Cells that already hold their block are skipped. Meshes rebuild once per chunk from the next frame, and colliders before the next physics step, however many calls reach a chunk before then. The blocks are solid at once, without the moment of softness a block from set has: a box written over a player can trap them. Works in the play-test and standalone builds. In a multiplayer room a server script's call throws (the room would have to send the whole box to every player); a client script calls it on a grid that is each player's own. const names = ['air', 'stone', 'dirt', 'grass_block'] const cells = new Uint8Array(16 * 64 * 16) // all 0: air for (let z = 0; z < 16; z++) for (let x = 0; x < 16; x++) { for (let y = 0; y < 60; y++) cells[x + y * 16 + z * 16 * 64] = y < 56 ? 1 : y < 59 ? 2 : 3 } this.blocks.setRegion('World', [0, 0, 0], [16, 64, 16], cells, names)
blocks.clearRegion(gridRef: string, min: [number, number, number], max: [number, number, number]): booleanEmpty a box of cells (inclusive corners, like fill): the unload. A chunk left with no blocks leaves the grid, its meshes and its colliders, so the memory comes back, where filling it with air through fill keeps the chunk. On a grid lit by Script the light written in the box goes with it, cell for cell, whether or not its chunks keep blocks; the light round it stays. Capped at 262,144 cells; works where setRegion does.
blocks.getLight(gridRef: string, x: number, y: number, z: number): { sky: number; block: number } | nullA cell's light on a grid whose block grid asset sets Lighting: sky light and block light, each 0 to 15. Null when the grid's Lighting is None. With Engine lighting it is the light the engine works out; with Script lighting, what setLightRegion wrote (full sky where nothing was written). Blocks are drawn by it, and so is an entity whose meshRenderer has gridLight: true, by the light of the cell it stands in: the scene's light times the sky's share, or lit warm by the blocks' share, the brighter, sampled each frame with no rebuild. Drawn on each machine, never sent. const l = this.blocks.getLight('World', x, y, z) if (l) brightness = Math.max(l.sky, l.block) / 15
blocks.setLightRegion(gridRef: string, min: [number, number, number], size: [number, number, number], sky: number | readonly number[] | Uint8Array, block: number | readonly number[] | Uint8Array): booleanWrite the light of a box of cells on a grid whose Lighting is Script: a game that gets its light from a server writes it as it arrives. sky and block are each one level (0 to 15) for the whole box, or one level per cell, x fastest, then y, then z, like setRegion's cells. The chunks it reaches are drawn again; the light is never saved. A cell keeps the light written for it until setLightRegion writes it again or a clearRegion covers it: blocks set, filled or written over leave it as it was, so write a region's light after its blocks. Where nothing was written it is full sky. Capped at 262,144 cells. Works where setRegion does: the play-test and standalone builds, and in a room a client script on a grid that is each player's own. this.blocks.setLightRegion('World', [0, 0, 0], [16, 16, 16], skyLevels, blockLevels)
blocks.setBiomeRegion(gridRef: string, min: [number, number, number], size: [number, number, number], ids: number | readonly number[] | Uint8Array): booleanWrite biomes on a grid whose block grid asset has Biome tint on: grass, leaves and water are coloured by the biome they stand in. A biome covers a 4×4×4 of cells, so min and size count BIOME CELLS (cell x, y, z lies in biome cell floor(x / 4), ...), and ids holds one biome index (0 to 254, a row of the table setBiomeColors sets; 255 clears) per biome cell, x fastest, then y, then z, or one for all of them. A cell with no biome takes its colour from the cells round it that have one, or biome 0. Never saved. Capped at 262,144 biome cells. Works where setRegion does: the play-test and standalone builds, and in a room a client script on a grid that is each player's own. this.blocks.setBiomeRegion('World', [0, 0, 0], [4, 16, 4], biomeIds) // a 16x64x16 column of cells
blocks.setBiomeColumns(gridRef: string, min: [number, number], size: [number, number], ids: number | readonly number[] | Uint8Array): booleanWrite biomes by column of blocks on a grid with Biome tint on: each column takes one biome at every height, over what setBiomeRegion wrote there. min is the first column's [x, z] in cells, size the columns along x and z, and ids one biome index per column (0 to 254, a row of the table setBiomeColors sets; 255 hands the column back to setBiomeRegion's cells), x fastest, then z, or one for all of them. Biomes still blend over a block where two meet. One call draws again once, so a game that receives a chunk's biomes writes them in one go. Never saved. Capped at 262,144 columns. Works where setRegion does. this.blocks.setBiomeColumns('World', [cx * 16, cz * 16], [16, 16], biomeIds) // 256 ids, one a column
blocks.setBiomeColors(gridRef: string, table: { name: string; grass: string; foliage: string; water: string }[]): booleanThe colours each biome gives on a grid with Biome tint on: the table the ids of setBiomeRegion and setBiomeColumns index, each a name and three '#rrggbb' colours. Every tinted block is drawn again; a biome past the end of the table takes biome 0's colours. Never saved; works where setBiomeRegion does. this.blocks.setBiomeColors('World', [{ name: 'plains', grass: '#91BD59', foliage: '#77AB2F', water: '#3F76E4' }])
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; playerLocal?: boolean }[]Every block grid in the map, editable or not. playerLocal is true on a grid that is each player's own.

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.
room.getGameConfig(): Record<string, unknown>What this GAME is configured with - the config an admin set on the game itself in the dashboard (Maps, the map's card, Games), as a copy. Where getServerConfig() is per server, this is per game: one map published as several games, each pinned to its own server, reads a different object in each, with no server file involved. The values are the map's to define. onStart() { const game = this.room.getGameConfig() this.worldName = game.worldName ?? 'Overworld' } Server scripts only: {} on a client, in the editor play-test, in a standalone build, and on a game with no config. Read it once in onStart; it does not change while the room lives.
room.getWorldData(assetId?: string): Record<string, unknown>What the WORLD carries: the static data its author wrote in the World Editor with the blocks (mob areas, a story, NPC sites, a village list), as a copy. Where the map's own Data assets are per map and getServerConfig() is per server, this belongs to the stored block world itself: a map placing another world reads that world's data, and the data follows the world to whichever server seeds it. Nothing in play changes it; the things that DO change during play go in this.world. onStart() { const w = this.room.getWorldData() for (const area of w.mobAreas ?? []) this.spawnZone(area.box, area.mobs) } assetId picks the stored world when the map places several; without it, the first one. Server scripts only: {} on a client, in the editor play-test, in a standalone build, in a map with no stored world, and for a world whose author wrote none. Read it once in onStart.

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.
data.saveFailed(playerId?: string): booleanTrue while the saves are not being kept. A standalone build and the editor play-test save in the browser, which holds about 5 million characters per site; once the save is bigger, each write is refused. Values still read back in this session, but a reload brings back the last save that was kept. Turns false at the next write that fits. When true, tell the player and store less. In multiplayer, saves go to the game's data store every 10 seconds and when the player leaves, so this tells the last of those writes, not the set just made: true from a write that failed or went unanswered for 10 seconds until one succeeds, and for the whole session when the player's save could not be loaded at join. Pass the player; with none, it is true while any player's saves are not kept.

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, each retry after a short random wait, under half a second in all). 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.

net ​

WebSocket connections to the game's own backend. Standalone builds and the play-test only.

MemberDescription
net.connect(url: string, options?: { protocols?: string | string[] }): ScriptSocketOpen a socket. Throws for a bad URL, a scheme the rules refuse or a host nobody allowed; a connection that fails on the network reports through onError and onClose instead.
net.isAvailable(): booleanCan this runtime open sockets at all?
net.allowedHosts(): string[]The host list from Game Settings, for a server list screen.

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 } | ([number, number, number] | { x: number; y: number; z: number })[], options?: { count?: number // particles at each position (default 24, capped at 400 in all) 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) damping?: number // air drag (default 0); at 3 a particle is down to a twentieth of its speed in a second sizeOverLifetime?: boolean // shrink to nothing as it lives (default true); false keeps its size shape?: 'square' | 'round' // each particle's look (default 'square'); 'round' = soft dots }): 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. Give a list of positions for many small effects at once (a ring of motes, the splashes of one moment): they share one burst, count particles at each, all of them together capped at 400. Up to 64 bursts live at once; past that, the one nearest its end makes way for the new one.

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 on the machine with the screen — the editor, standalone, and a client script in a multiplayer room — 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 and game.savePage are 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. On a map with Scene Settings ▸ Shadows ▸ Override player settings, 'default' and 'hard' also lift a player's own shadow Off, so ask for them only when the player picks shadows in your options.
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
visiblebooleanfalse starts it hidden; visible = true (or set) shows it. Default true.

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
passLookbooleanOn a touch screen, the finger holding this button also turns the view, as a swipe on the right half does: a thumb holding Use (a bow drawn, a shield up) can aim at the same time. The button still gets its press, release and onPointer events. Swipe look only (the default look mode, not 'stick'); a mouse is unchanged. Default false.
visiblebooleanfalse starts it hidden; visible = true (or set) shows it. Default true.

HudContainerHandle ​

A container's handle: an element handle plus nesting and scrolling.

FieldTypeDescription
add(...children: HudElementHandle[]): HudContainerHandleHudContainerHandleNest existing HUD elements inside this container; its layout arranges them. Returns this handle.
scrollTo(x: number | null, y: number | null): voidvoidScroll to this position, in the HUD's px (clamped to the content). Pass null for an axis to keep it. In multiplayer it scrolls the box on the screens of the players who see it.
scrollIntoView(child: HudElementHandle | string): voidvoidScroll the least that shows this element (a child, or deeper) whole: a row chosen by the keys past the foot of a list comes into view. Nothing happens when it is already in view or not in this box.
getScroll(): HudScrollHudScrollWhere the box stands. In the play-test, a standalone build and a client script, the box as drawn (a scrollTo not yet drawn reads as its target). In a room's server script, the last position the player's screen reported, which it reports while the box has an onScroll; before any report, the last scrollTo, else zeros.
onScroll((scroll: HudScrollInfo) => void) | nullCalled as the box scrolls, by the player or by scrollTo / scrollIntoView, at most 5 times a second while it moves, and always with where it came to rest. Set to null to remove.

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.
onScroll(scroll: HudScrollInfo) => voidFires as the box scrolls, by the player or by scrollTo / scrollIntoView. See HudContainerHandle.onScroll.
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 disables the controls in the group (they draw greyed); its panels, images and labels still block.
blocksRaycastsbooleanCanvasGroup: false lets clicks pass through the group, everything in it included, 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.
visiblebooleanfalse starts it hidden; visible = true (or set) shows it. Default true.

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
visiblebooleanfalse starts it hidden; visible = true (or set) shows it. Default true.

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
keepSpacesboolean
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
filtering'point' | 'linear'
groupAlphanumber
interactableboolean
blocksRaycastsboolean
aspectRationumber
fitContent'none' | 'width' | 'height' | 'both'
minWidthnumber
minHeightnumber
flexGrownumber
passLookbooleanOn a button or an element with onPointer: the finger held on it also turns the view on a touch screen (see HudButtonOptions.passLook), e.g. useButton.passLook = true.
blendModeHudBlendModeHow the element is blended with what is drawn under it (see HudLabelOptions.blendMode), e.g. crosshair.set({ blendMode: 'difference' }); 'normal' draws it as usual again.
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). While a press is held, 'dragEnter' and 'dragExit' fire on each pointer-enabled element it moves onto and off (from the press, not only past the drag threshold; the pressed element itself on re-entering it), with info.sourceId the pressed element: a sweep across slots names every slot passed. The last one entered hears its 'dragExit' before the release's 'up'. 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', 'dragEnter', 'dragExit': the element pressed). info.button is the mouse button (0 left, 1 middle, 2 right) of a press, release, click or drag. 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 crossings to 25, and neither the hover/drag stream nor the crossings can 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), and a right click over it opens no browser menu.

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.
filtering'point' | 'linear'How it is drawn when scaled: 'point' keeps whole pixels (pixel art), 'linear' smooths. Unset: 'point' for a texture whose own filtering is Point, else 'linear'.
passLookbooleanWith an onPointer: on a touch screen the finger held on it also turns the view. See HudButtonOptions.passLook.
visiblebooleanfalse starts it hidden; visible = true (or set) shows it. Default true.
blendModeHudBlendModeHow it is blended with what is drawn under it, the world included. See HudLabelOptions.blendMode.

HudInputOptions ​

FieldTypeDescription
playerIdstringMULTIPLAYER: scope to ONE player (Roblox PlayerGui parity — see HudLabelOptions.playerId).
valuestringInitial text value.
placeholderstringPlaceholder shown when empty.
onValueChanged(value: string, info: HudInputInfo) => voidFires with the new string when the player commits the edit: Enter, or leaving the field. info.reason says which ('enter' | 'blur'), so Enter can be a form's Done. A callback that takes this second argument also hears Enter when the text is unchanged; a one-argument callback hears changes only. A multiline field keeps Enter as a new line, so it commits on leaving alone.
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.
visiblebooleanfalse starts it hidden; visible = true (or set) shows it. Default true.

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;s&gt; (struck through), &lt;color=#f00&gt;, &lt;size=24&gt;.
keepSpacesbooleanKeep runs of spaces and line breaks as written; by default they fold into one space. A label that does not wrap still does not wrap. Default false.
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
visiblebooleanfalse starts it hidden; visible = true (or set) shows it. Default true.
blendModeHudBlendModeHow it is blended with what is drawn under it, the world included. 'difference' draws white as the inverse of what is behind it: a crosshair dark over a bright sky, light over a dark cave. The other modes: 'multiply', 'screen', 'overlay', 'darken', 'lighten', 'color-dodge', 'color-burn', 'hard-light', 'soft-light', 'exclusion', 'hue', 'saturation', 'color', 'luminosity'. Inside a container it blends with what the container draws. Default 'normal'.
raycastTargetbooleanIntercept pointer rays — block clicks to whatever's behind it (raycastTarget). Default false.

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.
raycastTargetbooleanIntercept pointer rays — block clicks to whatever's behind it (raycastTarget). Default false.
passLookbooleanWith an onPointer: on a touch screen the finger held on it also turns the view. See HudButtonOptions.passLook.
blendModeHudBlendModeHow it is blended with what is drawn under it, the world included. See HudLabelOptions.blendMode.

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
visiblebooleanfalse starts it hidden; visible = true (or set) shows it. Default true.

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
visiblebooleanfalse starts it hidden; visible = true (or set) shows it. Default true.

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).
visiblebooleanfalse starts it hidden; visible = true (or set) shows it. Default true.

MouseLookScheme ​

Per-button mouse look. A button left out is 'none'.

FieldTypeDescription
leftMouseLookMode
rightMouseLookMode

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 position and rotation follow their reports every frame, and its scale is yours to set (their state reaches you in onDeserialize). 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.

PlayerShadowSettings ​

The shadow choices a player made in the platform's graphics settings on this device, as they are stored — not what is in effect (that is graphics.get()). A map that sets Scene Settings ▸ Shadows ▸ Override player settings reads this to fit its own shadows to the player.

FieldTypeDescription
shadows'soft' | 'hard' | 'off'The Shadows row: soft or hard filtering, or no shadows at all.
distance'high' | 'medium' | 'low' | 'player' | 'off'The Shadow distance row. 'player' is Only player: no shadow map, a disc under each character.

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
setLookDirection(direction: [number, number, number]): voidvoidTurn this player's built-in camera to face a direction — see ScriptCameraAPI.setLookDirection.
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

ScriptSocket ​

A WebSocket a script opened with net.connect. Set the handlers right after connecting; they are called the moment the browser delivers the event, also while the tab is hidden, and a throw inside one is logged without closing the socket. Frames arrive in order, binary ones as an ArrayBuffer and text ones as a string, at any size. The socket closes by itself when its entity is destroyed, when play stops and when another scene loads (unless the entity is kept by DontDestroyOnLoad); in a Galatrix room also when the player leaves the room or moves to another, and at every map switch, which starts the client scripts over, DontDestroyOnLoad or not.

FieldTypeDescription
onOpen(() => void) | nullThe connection is open; sending is possible from here on.
onMessage((data: ArrayBuffer | string) => void) | nullOne frame: an ArrayBuffer for binary, a string for text. Sending from inside this handler is fine.
onClose((code: number, reason: string, wasClean: boolean) => void) | nullThe connection ended: the close code and reason the other side sent (1000 = normal), and whether the handshake completed cleanly. Fires after onError for a failed connection.
onError((message: string) => void) | nullSomething went wrong (the server refused, the network dropped). onClose follows.
send(data: ArrayBufferView | ArrayBuffer | string): voidvoidSend one frame. A typed array or DataView is handed over as it is, without a copy, so reuse the buffer only after the call returns. Throws when the socket is not open.
close(code?: number, reason?: string): voidvoidClose the connection. Code 1000 and an empty reason by default; codes 3000-4999 are yours to define.
readyState'connecting' | 'open' | 'closing' | 'closed'
bufferedAmountnumberBytes queued by send() and not yet handed to the network.
urlstring
protocolstringThe subprotocol the server chose from protocols, or '' once open without one.

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.