Instances and the tree
Everything in a Rally game is an instance in one tree: parts, models, scripts, UI, players and the services at the top. This page covers the things you do with instances in code — find them, read and change them, create, copy and remove them.
What an instance has
Section titled “What an instance has”Every instance has:
- a class, which decides what it is and what it can do.
classNamegives it as a string ('Part'), andisA('BasePart')checks it, counting parent classes too. - a name and a parent, which place it in the tree.
- properties, such as a part’s
positionandcolor, that the engine knows about. - attributes: your own named values, on any instance.
- events you can connect to, such as
childAddedanddestroying.
All of them share the members of the base class Instance. The API reference
lists every class.
Parents and children
Section titled “Parents and children”An instance’s parent is the instance it’s inside. Setting parent moves it — and an instance is
part of the game while it’s somewhere under game:
import { Folder, Part, print, workspace } from '@rally/sdk';
const folder = new Folder();folder.name = 'Map';folder.parent = workspace;
const door = new Part();door.name = 'Door';door.parent = folder; // now in the game, at Workspace › Map › Door
print(door.getFullName()); // Workspace.Map.Doorprint(door.isDescendantOf(workspace)); // trueprint(folder.getChildren().length); // 1getChildren()returns the direct children, in order. It’s a copy, so it’s safe to change the tree while you loop over it.getDescendants()returns everything inside, at any depth.isDescendantOf(other)andisAncestorOf(other)check how two instances are related.getFullName()gives a readable path likeWorkspace.Map.Door— handy in log messages.- Setting
parent = nulltakes an instance out of the game without destroying it. You can put it back later.
Services are fixed: you can’t move or destroy workspace, Players or any other service.
A new instance is named after its class — a new Part is called Part — until you set name.
Names don’t have to be unique, but the lookups below return the first match, so give anything your
scripts look up by name a name of its own among its siblings.
Names also make up the paths scripts import from (@game/ReplicatedStorage/Config), so in Studio a
name can’t be empty, can’t contain /, and can be at most 100 characters.
Finding objects
Section titled “Finding objects”| To find… | Use |
|---|---|
| a child that must exist, typed | getChild(parent, name, Class) from @rally/sdk — throws if it’s missing or another class |
| a child that may not have arrived yet | await waitForChild(parent, name, Class, timeoutSeconds?) from @rally/sdk |
a child, or undefined |
parent.findFirstChild(name) |
| a descendant at any depth | parent.findFirstChild(name, true) |
| the first child of a class | findFirstChildOfClass('Part') (exact class) or findFirstChildWhichIsA('BasePart') (subclasses count) |
| an ancestor | findFirstAncestor(name) or findFirstAncestorWhichIsA('Model') |
| a service by name | game.getService('Players') |
| the player a character belongs to | Players.getPlayerFromCharacter(character) |
getChild and waitForChild take the class you expect and return the object typed as that class,
so TypeScript lets you use its properties right away. findFirstChild returns a plain Instance;
check its class with instanceof before you use it:
import { BasePart, Folder, getChild, Part, print, workspace } from '@rally/sdk';
// getChild: the child must exist and be the right class, or it throws a clear error.const map = getChild(workspace, 'Map', Folder);const door = getChild(map, 'Door', Part); // typed as Part
// findFirstChild: returns undefined when there's nothing by that name. Check the class yourself.const maybeFloor = map.findFirstChild('Floor');if (maybeFloor instanceof Part) print('floor is', maybeFloor.size);
// Search the whole subtree.const deep = workspace.findFirstChild('Door', true);print('found', deep); // Workspace.Map.Door
// Every part in the Map folder, at any depth.const parts = map.getDescendants().filter((d): d is BasePart => d instanceof BasePart);print(parts.length, 'parts in the map; the door is', door.size);When something’s wrong, getChild says exactly what: Workspace has no child 'Gate' (expected a Part), or Workspace.Map.Door is a Folder, expected a Part.
Get it, or wait for it?
Section titled “Get it, or wait for it?”On the server, getChild is usually right. By the time server scripts start, the whole world
you built in Studio is loaded. Things other scripts create while the game runs — a player’s
leaderstats, say — are the exception: wait for those.
On a player’s device, wait. Objects arrive from the server over the network, and a LocalScript
can start before what it needs has arrived. Use waitForChild inside task.spawn (or an async
event handler):
import { Folder, NumberValue, print, ReplicatedStorage, task, waitForChild } from '@rally/sdk';
task.spawn(async () => { const state = await waitForChild(ReplicatedStorage, 'GameState', Folder); const timeLeft = await waitForChild(state, 'TimeLeft', NumberValue); timeLeft.getPropertyChangedSignal('value').connect(() => { print(`${timeLeft.value} seconds left`); });});waitForChild waits as long as it takes. If you pass timeoutSeconds, it fails with an error
instead once that much time has passed — useful while you’re building, to catch a misspelled name.
Properties
Section titled “Properties”Properties use camelCase names — position, anchored, canCollide — the same names Studio’s
Properties panel shows. They’re typed and checked when you set them:
- A value of the wrong type throws:
Part.anchored expects true or false, got string "yes". - A choice that isn’t allowed throws and lists the allowed ones:
Part.shape expects one of 'box', 'sphere', 'cylinder', 'capsule', got string "triangle". - A number out of range is clamped: setting
transparencyto 5 gives 1. - Read-only properties throw if you set them, such as
Player.userId,Player.character,Character.velocityandWorkspace.dimension.
Vectors and colors — Vector3, Vector2 and Color3 — are immutable values. To change a part’s
position you build a new vector and assign it; changing one in place isn’t possible:
import { Color3, Part, print, Vector3, workspace } from '@rally/sdk';
const door = workspace.findFirstChild('Door', true);if (!(door instanceof Part)) throw new Error('No Door part in the Workspace');
// Vectors and colors are immutable: build a new value and assign it.door.position = door.position.add(new Vector3(0, 2, 0));door.position = door.position.withY(10);door.color = Color3.fromRGB(255, 90, 95);door.transparency = 5; // clamped to 1print(door.transparency); // 1
// React when a property changes.door.getPropertyChangedSignal('transparency').connect(() => { door.canCollide = door.transparency < 0.5;});door.transparency = 0.2;print(door.canCollide); // trueChange events fire only when a value really changes — setting a property to the value it already
has does nothing. Data types covers Vector3, Color3 and the other
value types.
Attributes
Section titled “Attributes”Attributes are your own data on any instance: a door’s price, a pad’s jump power, a player’s team.
An attribute holds a string, number, boolean, Vector3, Vector2 or Color3, and replicates to
players just like a property.
setAttribute(name, value)sets one; setting it toundefinedremoves it.getAttribute(name)reads one —undefinedif it isn’t set. Its type is any of the allowed types, so check it withtypeofbefore you use it.getAttributes()returns them all as an object.attributeChangedfires with the name of any attribute that changes;getAttributeChangedSignal(name)fires for one.
Names use letters, digits and _, don’t start with a digit, and are at most 100 characters. In
Studio, add attributes at the bottom of the Properties panel under Attributes › Add
attribute (string, number, boolean, Vector3 or Color3).
Attributes let you set up a world in Studio that scripts read, instead of hard-coding values. These jump pads each take their power from an attribute:
import { Character, Folder, getChild, Part, StarterPlayer, task, workspace } from '@rally/sdk';
// Every part in JumpPads launches players. A pad's `jumpPower` attribute sets how high;// pads without one use 100. Add pads in Studio without touching this script.const pads = getChild(workspace, 'JumpPads', Folder);
for (const pad of pads.getChildren()) { if (!(pad instanceof Part)) continue; const power = pad.getAttribute('jumpPower'); const jumpPower = typeof power === 'number' ? power : 100; pad.touched.connect((other) => { if (!(other instanceof Character)) return; other.jumpPower = jumpPower; task.delay(1, () => { if (!other.destroyed) other.jumpPower = StarterPlayer.characterJumpPower; }); });}Creating objects
Section titled “Creating objects”Create an instance with new and the class from @rally/sdk, set its properties, then set its
parent:
import { Color3, Part, Vector3, workspace } from '@rally/sdk';
const crate = new Part();crate.name = 'Crate';crate.size = new Vector3(3, 3, 3);crate.position = new Vector3(0, 20, 0);crate.color = Color3.fromHex('#b5835a');crate.material = 'wood';crate.anchored = false; // let physics drop itcrate.parent = workspace; // set the parent last: now it's in the gameSetting the parent last means nothing sees the object half set up — not physics, not touched
events, not other scripts listening for new children. Instance.new('Part', workspace) also
works, by class name, but returns a plain Instance.
Scripts can create these classes: Part, MeshPart, SpawnLocation, Model, Folder,
Terrain, PointLight, Sound, Animation, the constraints (WeldConstraint,
HingeConstraint, SpringConstraint, RopeConstraint), the value objects (NumberValue,
StringValue, BoolValue, ObjectValue), RemoteEvent, RemoteFunction, BindableEvent, and
the UI classes (ScreenGui, Frame, TextLabel, TextButton, TextBox, ImageLabel). The
engine makes Player, Character, PlayerGui and Camera itself, and scripts can only be added
in Studio.
Cloning
Section titled “Cloning”clone() makes a deep copy of an instance: its properties, its attributes and everything inside
it. The copy has no parent until you give it one. References inside the copied tree — a model’s
primaryPart, say — point at the copies. Event connections aren’t copied.
Keep templates in ServerStorage, where players can’t see them, and clone them into the world when you need them:
import { Folder, getChild, Model, ServerStorage, task, Vector3, workspace } from '@rally/sdk';
const template = getChild(ServerStorage, 'CoinTemplate', Model);const coins = getChild(workspace, 'Coins', Folder);
function spawnCoin(at: Vector3): Model { const coin = template.clone(); // a deep copy with no parent coin.pivotTo(at); // move the whole model coin.parent = coins; task.delay(30, () => coin.destroy()); // clean up after 30 seconds return coin;}
for (let i = 0; i < 5; i++) spawnCoin(new Vector3(i * 6, 3, 10));Children whose archivable property is off are left out of copies.
Removing objects
Section titled “Removing objects”destroy()removes an instance and everything inside it for good. Their event connections are disconnected, and they can’t be put back: settingparentafterwards throws. When the server destroys something, it disappears for every player.destroyedistrueonce an instance has been destroyed. Check it in code that runs later, like a delayed callback.destroyingfires just before an instance is destroyed.clearAllChildren()destroys everything inside an instance, keeping the instance itself.parent = nullremoves an instance without destroying it, so you can put it back.
The engine removes some things for you: unanchored parts that fall below the Workspace’s
fallenPartsDestroyHeight are destroyed, and a player’s character is destroyed when they leave.
Folders and Models
Section titled “Folders and Models”A Folder organizes. It has no properties of its own and does nothing in the world — use folders
to group things for yourself and for your scripts, like Checkpoints or Remotes.
A Model groups parts that belong together — a car, a house, an enemy — so you can move them as one:
primaryPart: the part that counts as the model’s position, if you set one.getPivot(): the primary part’s position, or the center of the model’s parts if it has none.pivotTo(position): moves every part so the pivot lands onposition.translateBy(offset): moves every part byoffset.parts(): every part in the model, at any depth.getBoundingBox(): thecenterandsizeof the box around its parts.
These move parts but don’t rotate them, and the bounding box ignores rotation. In Studio, select some objects and press Ctrl+G (⌘+G on a Mac) to group them into a Model.
import { getChild, Model, print, RunService, Vector3, workspace } from '@rally/sdk';
const cart = getChild(workspace, 'Cart', Model);print('cart is', cart.getBoundingBox().size, 'around', cart.getPivot());
// Slide the whole cart 4 studs per second along X.RunService.heartbeat.connect((dt) => { cart.translateBy(new Vector3(4 * dt, 0, 0));});Value objects
Section titled “Value objects”NumberValue, StringValue, BoolValue and ObjectValue each hold one value — a number, a
string, a boolean, or a reference to another instance. They’re instances, so they sit in the tree
where you can see them, and they replicate to players.
Use them for state that several scripts or every player should see: a round timer, a team score, a
player’s leaderstats. The server changes the value, and every player’s copy follows:
import { Folder, getChild, NumberValue, ReplicatedStorage, task } from '@rally/sdk';
const state = getChild(ReplicatedStorage, 'GameState', Folder);const timeLeft = getChild(state, 'TimeLeft', NumberValue);
task.spawn(async () => { while (timeLeft.value > 0) { await task.wait(1); timeLeft.value -= 1; // every player's copy updates }});The Clock LocalScript earlier on this page prints the value each time it changes.
Value objects or attributes? Both replicate and both have change events. Attributes are lighter —
no extra object — and are best for data about an object. Value objects show up in the Explorer,
can hold a reference to another instance, and are what leaderstats uses.
Every instance in the game has an id that’s the same on the server and on every player’s device.
game.getInstanceById(id) finds it again. You rarely need ids: remote events can send instances
directly, and they arrive as the same instance on the other side.
Next steps
Section titled “Next steps”- Services — the services and their most useful members.
- Events and signals — react to changes in the tree.
- Players and characters — the instances the game makes for each player.