Skip to content

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.

Every instance has:

  • a class, which decides what it is and what it can do. className gives it as a string ('Part'), and isA('BasePart') checks it, counting parent classes too.
  • a name and a parent, which place it in the tree.
  • properties, such as a part’s position and color, that the engine knows about.
  • attributes: your own named values, on any instance.
  • events you can connect to, such as childAdded and destroying.

All of them share the members of the base class Instance. The API reference lists every class.

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.Door
print(door.isDescendantOf(workspace)); // true
print(folder.getChildren().length); // 1
  • getChildren() 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) and isAncestorOf(other) check how two instances are related.
  • getFullName() gives a readable path like Workspace.Map.Door — handy in log messages.
  • Setting parent = null takes 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.

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:

ServerScriptService/Find.server.ts
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.

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):

StarterPlayer/StarterPlayerScripts/Clock.client.ts
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 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 transparency to 5 gives 1.
  • Read-only properties throw if you set them, such as Player.userId, Player.character, Character.velocity and Workspace.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:

ServerScriptService/Props.server.ts
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 1
print(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); // true

Change 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 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 to undefined removes it.
  • getAttribute(name) reads one — undefined if it isn’t set. Its type is any of the allowed types, so check it with typeof before you use it.
  • getAttributes() returns them all as an object.
  • attributeChanged fires 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:

ServerScriptService/JumpPads.server.ts
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;
});
});
}

Create an instance with new and the class from @rally/sdk, set its properties, then set its parent:

ServerScriptService/Create.server.ts
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 it
crate.parent = workspace; // set the parent last: now it's in the game

Setting 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.

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:

ServerScriptService/Coins.server.ts
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.

  • destroy() removes an instance and everything inside it for good. Their event connections are disconnected, and they can’t be put back: setting parent afterwards throws. When the server destroys something, it disappears for every player.
  • destroyed is true once an instance has been destroyed. Check it in code that runs later, like a delayed callback.
  • destroying fires just before an instance is destroyed.
  • clearAllChildren() destroys everything inside an instance, keeping the instance itself.
  • parent = null removes 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.

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 on position.
  • translateBy(offset): moves every part by offset.
  • parts(): every part in the model, at any depth.
  • getBoundingBox(): the center and size of 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.

ServerScriptService/Cart.server.ts
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));
});

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:

ServerScriptService/Clock.server.ts
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.