How games are organized
Every Rally game has the same services at the top of its tree. Where you put an object decides whether players receive it and whether a script in it runs. This page explains what goes where, how scripts find each other, and a layout that holds up as a game grows.
The services at a glance
Section titled “The services at a glance”| Service | What goes in it | Sent to players? | Scripts that run here |
|---|---|---|---|
| Workspace | The world: parts, models, spawn points, terrain, lights and sounds. Characters appear here. | Yes, except server Scripts | Scripts |
| Players | Filled in by the game: one Player per person |
Yes, but each PlayerGui only to its player |
— |
| Lighting | Its own properties: sky color, sun, ambient light, fog, shadows | Yes | — |
| ReplicatedStorage | Remote events, shared ModuleScripts, objects both sides need, shared game state | Yes | — |
| ServerScriptService | Your server Scripts | No | Scripts |
| ServerStorage | Templates to clone into the world, server-only ModuleScripts | No | — |
| StarterGui | ScreenGuis (and LocalScripts inside them), copied to each player when they join | Yes | LocalScripts, in each player’s copy |
| StarterPlayer | Character defaults, and StarterPlayerScripts for LocalScripts | Yes | LocalScripts in StarterPlayerScripts |
You can’t add, move or delete services. In Studio, each one is a top-level row in the
Explorer. In code, import them by name from @rally/sdk — workspace, Players, Lighting,
ReplicatedStorage, ServerScriptService, ServerStorage, StarterGui and StarterPlayer —
or look one up with game.getService('Players').
What goes in each service
Section titled “What goes in each service”Workspace
Section titled “Workspace”Everything that exists in the world: parts, models, SpawnLocations, terrain, and the lights,
sounds, animations and constraints that belong to them. When players spawn, their characters
appear here too, and each player’s device keeps its own camera here.
A Script inside an object in Workspace runs on the server, and script.parent gives it the object
it’s in. That’s handy for self-contained objects such as a kill brick or a door. Server Scripts
are never sent to players, even here.
Workspace’s own properties set world-wide physics: gravity (196.2 studs/s² by default) and
fallenPartsDestroyHeight (-500 by default: unanchored parts that fall below it are destroyed, and
characters die).
Players
Section titled “Players”The game adds a Player here for each person who joins and removes it when they leave. You can’t
put anything in it in Studio, but you can set its properties: respawnTime (seconds before a
character comes back, 3 by default) and characterAutoLoads (whether characters spawn on their own,
true by default). See Players and characters.
Lighting
Section titled “Lighting”The look of the sky and light. Set skyColor, ambient, brightness, sunDirection,
fogColor, fogEnd (0 means no fog) and shadows in Studio’s Properties, or change them from
a script — for example, to make the sky darker as a round goes on.
ReplicatedStorage
Section titled “ReplicatedStorage”Things the server and every player’s device both need:
- RemoteEvents and RemoteFunctions, so clients and the server can talk. They have to be somewhere both sides can see, and here is the usual place. See Multiplayer.
- Shared ModuleScripts: settings, names and helper functions that both server and client scripts import.
- Shared state that every player should see, such as a
Folderwhose attributes hold the current round and time left. Late joiners get it automatically. - Templates both sides use, such as an effect a LocalScript clones.
ServerScriptService
Section titled “ServerScriptService”Your server Scripts. Many games have just one here — a Main or Server script that starts
everything else. Nothing in ServerScriptService is ever sent to players.
ServerStorage
Section titled “ServerStorage”Server-only storage. Nothing in it is sent to players and nothing in it is drawn or simulated, so it’s the place for:
- Templates the server clones into the world: enemies, pickups, map pieces.
- Server-only ModuleScripts: game rules, round logic, saving data.
- BindableEvents that let your server scripts talk to each other.
StarterGui
Section titled “StarterGui”On-screen UI. Put ScreenGuis here, with frames, labels and buttons inside them. When a player
joins, the game copies every ScreenGui into that player’s PlayerGui, where only they see it, and
any LocalScripts inside the copies start running on their device. The copy happens once, when they
join — not again when their character respawns.
Studio draws the UI in StarterGui over the viewport so you can see and arrange it; Hide UI in the toolbar hides it. See User interface and the UI editor.
StarterPlayer
Section titled “StarterPlayer”Defaults for new characters — characterWalkSpeed (16), characterJumpPower (50) and
characterMaxHealth (100) — applied each time a character spawns. Inside it,
StarterPlayerScripts holds the LocalScripts that run on every player’s device when they join.
Who can see what
Section titled “Who can see what”Players receive Workspace, Players, Lighting, ReplicatedStorage, StarterGui and StarterPlayer,
including the code of the LocalScripts in them. They never receive ServerScriptService,
ServerStorage, server Scripts (wherever they are), or other players’ PlayerGui.
Treat everything players receive as public. A player can read anything that reaches their device, including your LocalScripts’ code, so keep secrets, answers and game rules on the server.
How scripts reference each other
Section titled “How scripts reference each other”A ModuleScript shares code with export, and other scripts import it by its place in the tree.
There are two ways to write the path.
By tree path, starting with @game/ and the service, with names separated by /:
import { ROUND_SECONDS } from '@game/ReplicatedStorage/Shared/Config';import * as Rounds from '@game/ServerStorage/Modules/Rounds';Relative to the script’s own folder, starting with ./ (the same folder) or ../ (the folder
above):
// In ServerStorage/Modules/Rounds — Coins is the ModuleScript next to it:import { clearAll, spawnAll } from './Coins';
// In ReplicatedStorage/Client/Hud — go up to ReplicatedStorage, then into Shared:import { REMOTES } from '../Shared/Config';“The script’s own folder” means its parent. A script whose ModuleScripts are inside it — Rounds
with a Timer module as its child — imports them as ./Rounds/Timer.
A few rules:
- Only ModuleScripts can be imported. Importing a Script or LocalScript is a compile error.
- LocalScripts — and the modules they import — can’t import ModuleScripts in ServerStorage or ServerScriptService, since their code would be sent to players. Studio reports an error.
- Scripts can import
@rally/sdk,@game/...paths and relative paths. There are no other packages. - Names are paths, so they can’t contain
/. Studio won’t let you type one. - A ModuleScript must have a name of its own among the scripts next to it. Two scripts with the same name in the same place, when one is a ModuleScript, stop the game from compiling — an import couldn’t tell them apart. Rename one.
- Renaming or moving a ModuleScript changes its path. Update the imports that point at it.
The editor checks imports as you type, so a wrong path is underlined right away. Scripts and modules explains how modules are built into scripts.
Keep shared settings in one module
Section titled “Keep shared settings in one module”Put the numbers you’ll want to tune, and the names both sides must agree on, in one ModuleScript in ReplicatedStorage. A typo in a remote event’s name then becomes an editor error instead of a bug you find while playing:
// Tuning numbers and names that both the server and players' devices use.// Change them here and every script that imports this file picks them up.
export const ROUND_SECONDS = 60;export const INTERMISSION_SECONDS = 10;export const COIN_VALUE = 1;
/** RemoteEvents in ReplicatedStorage/Remotes. */export const REMOTES = { /** Server → everyone: `(text: string)` for the status line. */ announce: 'Announce',} as const;Keep secrets out of it: anything a LocalScript imports reaches players.
A structure for a real game
Section titled “A structure for a real game”Here’s how Last Stand at Outpost 9 — a four-player co-op survival game built in Studio, with weapons, perks, doors to buy and waves of enemies — lays out its tree:
Workspace├─ Building Floors, Walls, Roof, Fence, Props: the level, grouped in Folders├─ Interactables Doors, WallBuys, Perks, Windows, Crates, …: Models tagged with attributes├─ Lights├─ NavNodes points enemies find their way between├─ Spawns└─ TerrainLighting a dark sky and fogReplicatedStorage├─ Remotes RemoteEvents: Fire, Reload, Interact, Announce, …├─ GameState a Folder whose attributes are the public game state (round, phase, …)├─ Shared ModuleScripts both sides import: Config, Net, Weapons, Perks, MathUtil└─ Client ModuleScripts only the client script imports: Hud, Gunplay, Look, …ServerScriptService└─ Server the one server Script: starts every systemServerStorage├─ Templates enemy Models, cloned into the world└─ Modules server-only ModuleScripts: Rounds, Zombies, Combat, Persistence, …StarterGui└─ HUD one ScreenGui with every on-screen elementStarterPlayer└─ StarterPlayerScripts └─ Client the one LocalScript: starts the client modulesWhy it works:
- One entry script per side.
Serverimports the server modules and starts them in order;Clientdoes the same for the client modules. Start-up order is easy to follow, and each module’s state exists exactly once — two scripts importing the same module would each get their own copy. - Names and numbers in one place.
Shared/Netholds the remote event names andShared/Configevery tuning number, so both sides always agree. - Rules stay on the server. Only the
Serverscript importsServerStorage/Modules, so none of that code ever reaches players — Studio wouldn’t let a LocalScript import it anyway. - The level is data. Each door, perk machine and wall weapon is a Model with attributes such as
kindandcost. Scripts find them by those attributes, so you can add a door or change a price in Studio without touching code.
The same layout, small
Section titled “The same layout, small”Here’s the pattern as a complete small game: coins appear on marker parts each round, and the player who grabs the most wins. Besides the scripts, it uses these objects:
Workspace├─ SpawnLocation├─ CoinSpots invisible Parts marking where coins appear└─ Coins an empty Folder the coins go inReplicatedStorage├─ Remotes│ └─ Announce RemoteEvent├─ Shared│ └─ Config ModuleScript (above)└─ Client └─ Hud ModuleScriptServerScriptService└─ Main ScriptServerStorage├─ Templates│ └─ Coin a Part: the coin to clone└─ Modules ├─ Coins ModuleScript └─ Rounds ModuleScriptStarterGui└─ HUD ScreenGui └─ Status TextLabelStarterPlayer└─ StarterPlayerScripts └─ Main LocalScriptThe server’s entry script sets up players and starts the round loop:
// The game's one server Script: sets up players, then starts the systems in ServerStorage/Modules.import { createLeaderstats, Players } from '@rally/sdk';import { scores } from '@game/ServerStorage/Modules/Coins';import * as Rounds from '@game/ServerStorage/Modules/Rounds';
Players.playerAdded.connect((player) => { const stats = createLeaderstats(player, { Coins: 0 }); scores.set(player, stats.Coins);});Players.playerRemoving.connect((player) => scores.delete(player));
Rounds.start();The server modules do the work. Coins clones the template onto every marker and scores
pickups; Rounds runs the loop and announces what’s happening:
// Server only: spawns coins on the spots in Workspace/CoinSpots and scores pickups.import { Character, Folder, getChild, type NumberValue, Part, type Player, Players, ServerStorage, workspace,} from '@rally/sdk';import { COIN_VALUE } from '@game/ReplicatedStorage/Shared/Config';
const template = getChild(getChild(ServerStorage, 'Templates', Folder), 'Coin', Part);const coins = getChild(workspace, 'Coins', Folder);const spots = getChild(workspace, 'CoinSpots', Folder) .getChildren() .filter((spot): spot is Part => spot instanceof Part);
/** Each player's coin counter (their leaderstats value), set up by the Main script. */export const scores = new Map<Player, NumberValue>();
export function spawnAll(): void { for (const spot of spots) { const coin = template.clone(); coin.position = spot.position; coin.touched.connect((other) => { if (!(other instanceof Character) || coin.destroyed) return; const player = Players.getPlayerFromCharacter(other); const score = player && scores.get(player); if (!score) return; score.value += COIN_VALUE; coin.destroy(); }); coin.parent = coins; }}
export function clearAll(): void { coins.clearAllChildren();}// Server only: the round loop. Each round spawns coins, counts down, then picks a winner.import { Folder, getChild, Players, RemoteEvent, ReplicatedStorage, task } from '@rally/sdk';import { INTERMISSION_SECONDS, REMOTES, ROUND_SECONDS } from '@game/ReplicatedStorage/Shared/Config';import { clearAll, scores, spawnAll } from './Coins';
const announce = getChild(getChild(ReplicatedStorage, 'Remotes', Folder), REMOTES.announce, RemoteEvent);
async function playRound(): Promise<void> { for (const score of scores.values()) score.value = 0; spawnAll(); for (let t = ROUND_SECONDS; t > 0; t--) { announce.fireAllClients(`Grab the coins! ${t}`); await task.wait(1); } clearAll(); let winner = ''; let best = 0; for (const [player, score] of scores) { if (score.value > best) { best = score.value; winner = player.displayName; } } announce.fireAllClients(winner ? `${winner} wins with ${best}!` : 'Nobody scored this time.'); await task.wait(INTERMISSION_SECONDS);}
export function start(): void { task.spawn(async () => { for (;;) { if (Players.getPlayers().length === 0) { await task.wait(1); continue; } await playRound(); } });}Main and Rounds both import Coins, and they share one scores map: everything Main
imports is built into one program. On the player’s side, the entry LocalScript starts the client
modules:
// The game's one LocalScript: runs on each player's device and starts the client modules.import { task, waitForLocalPlayer } from '@rally/sdk';import * as Hud from '@game/ReplicatedStorage/Client/Hud';
task.spawn(async () => { const me = await waitForLocalPlayer(); await Hud.init(me);});// Client only: shows what the server announces in the HUD's Status label.import { Folder, type Player, PlayerGui, RemoteEvent, ReplicatedStorage, ScreenGui, TextLabel, waitForChild,} from '@rally/sdk';import { REMOTES } from '@game/ReplicatedStorage/Shared/Config';
export async function init(me: Player): Promise<void> { const gui = await waitForChild(me, 'PlayerGui', PlayerGui); const hud = await waitForChild(gui, 'HUD', ScreenGui); const status = await waitForChild(hud, 'Status', TextLabel); const remotes = await waitForChild(ReplicatedStorage, 'Remotes', Folder); const announce = await waitForChild(remotes, REMOTES.announce, RemoteEvent); announce.onClientEvent.connect((text) => { status.text = String(text); });}The client code waits for everything it uses with waitForChild, because on a player’s device
objects arrive from the server over time. The server code uses getChild, because when server
scripts start, the world is already loaded.
Tips for keeping a game tidy
Section titled “Tips for keeping a game tidy”- Group with Folders and Models. Folders organize; Models group parts that belong together and move as one. In Studio, select objects and press Ctrl+G (⌘+G on a Mac) to group them into a Model.
- Give things you look up from code unique names among their siblings.
findFirstChildreturns the first match, so two parts calledDoorin one folder is a bug waiting to happen. - Mark things with attributes, not names. A
kindattribute or a folder likeCoinSpotslets scripts find everything of one sort, however many you add. - Keep markers invisible. Parts that only mark positions can have
transparency1 andcanCollideandcanTouchturned off. You can still find and select them in the Explorer.
Next steps
Section titled “Next steps”- Scripts and modules — script kinds, imports and how modules are built.
- Instances and the tree — finding, creating and changing objects in code.
- Building worlds — Studio’s tools for laying out the Workspace.