2D games
A 2D game is a side view: players run left and right, jump, and fall, like a classic platformer. It uses the same Studio, the same objects and the same scripting API as a 3D game. Everything simply happens on a flat plane.
Make a 2D game
Section titled “Make a 2D game”On the Games page, select Create a game. Under Dimension, pick 2D, or leave it on
Auto: then a description that asks for 2D (such as “a 2D platformer” or “a side-scroller”)
starts the game in 2D, and anything else starts in 3D. Under Start from, pick the Coin Dash
template (or Lost Crypt, a top-down dungeon), or Blank to start from a strip of ground and
a spawn point. Scripts can read the dimension from workspace.dimension, which is '2d' or '3d'.
Switch between 2D and 3D
Section titled “Switch between 2D and 3D”A game can switch any time:
- Ask the AI. Rally’s AI (in Studio or Build live) and your own AI app can switch the game when
what you want needs the other one. They use the
set_dimensiontool. - Switch it yourself. On the game’s page, open Manage, and under Dimension choose 3D or 2D. Owners can switch from there; collaborators can ask the AI.
Switching keeps every object where it is and changes how the world works: in 2D, position.z
becomes a draw layer and collisions ignore depth, so a 3D level usually needs rearranging (the AI
does that after it switches). Everyone editing the game reloads into the new view, and Build live
worlds restart. Players keep the published version, in its own dimension, until you publish again.
Restoring an old version brings back the dimension it was published in, and a remix starts in the
dimension of the version it copies.
What changes in 2D
Section titled “What changes in 2D”| 3D | 2D | |
|---|---|---|
| Where things move | Along X, Y and Z. | Along X (left and right) and Y (up and down). |
position.z |
A position like any other. | Only decides what’s drawn in front. Higher Z is closer to the player. |
| Rotation | Around X, Y and Z. | Around Z only. rotation.x and rotation.y are ignored for parts. |
| Collisions | In 3D. | On the plane. Depth is ignored: parts collide whatever their Z. |
| Characters | Run in any direction, turn to face where they go. | Run left and right, face left or right. |
| Default controls | W A S D or arrows to move, Space to jump, mouse to look. | A D or ← → to run, Space, W or ↑ to jump. |
| Touch screens | Joystick and Jump button. | The same; pushing the joystick up also jumps. |
| Camera | Orbits behind the character, with perspective. | Looks straight at the plane from the side, with no perspective, and follows the character. |
| Shadows | The sun casts shadows. | None. Everything is lit from the front. |
Units are the same in both: distances are in studs, and a character is a capsule 5 studs tall and 2 studs wide.
Shapes
Section titled “Shapes”shape |
In 2D |
|---|---|
box |
A rectangle. |
sphere |
A circle whose diameter is size.x. |
capsule |
A rectangle with round ends. |
cylinder |
Looks and collides like a box. Use box instead. |
A part’s depth (size.z) doesn’t matter: every part is drawn as a flat plate at its position.z.
Draw order
Section titled “Draw order”position.z is a layer number. Characters are drawn just in front of layer 0:
- Z of 0 is the play layer: ground, platforms, coins and hazards. Characters draw in front of it.
- Negative Z is the background: hills, clouds, buildings in the distance.
- Z of 1 or more draws in front of characters: foreground leaves, bars, a tunnel’s front wall.
Because collisions ignore depth, a background part at Z = -10 still blocks characters unless you
tell it not to. For every background and foreground part, turn off canCollide (and canTouch,
since nothing needs to detect it).
The camera
Section titled “The camera”The 2D camera looks at the plane from the side and follows your character, aiming a little above it so players see more of what’s ahead and above. It tracks sideways quickly and up and down more gently, so jumps don’t jerk the view.
workspace.currentCamera.distance is half the visible height, in studs. The default is 14, so
the screen shows 28 studs from top to bottom (the width depends on the window). Players can zoom
with the mouse wheel between 4 and 60. A LocalScript can set distance itself, to anything from 2
to 200.
For a fixed view, such as a single-screen arena, set cameraType to 'scriptable' and put the
camera’s position where the middle of the screen should be. distance still sets how much
shows; lookAt and fieldOfView don’t matter in 2D.
This LocalScript zooms the camera out while the player is inside one of the parts in a
CameraZones folder. Give each zone a zoom number attribute (the half-height to show), and make
the zones invisible and non-colliding:
import { BasePart, Folder, RunService, task, type Tween, TweenService, type Vector3, waitForChild, waitForLocalPlayer, workspace,} from '@rally/sdk';
const NORMAL = 14;
/** Whether `p` is inside the zone's rectangle (ignoring depth and rotation). */function inside(zone: BasePart, p: Vector3): boolean { return ( Math.abs(p.x - zone.position.x) <= zone.size.x / 2 && Math.abs(p.y - zone.position.y) <= zone.size.y / 2 );}
task.spawn(async () => { const me = await waitForLocalPlayer(); const zones = await waitForChild(workspace, 'CameraZones', Folder); let want = NORMAL; let tween: Tween | null = null;
RunService.renderStepped.connect(() => { const camera = workspace.currentCamera; const character = me.character; if (!camera || !character) return; let zoom = NORMAL; for (const zone of zones.getChildren()) { const z = zone.getAttribute('zoom'); if (zone instanceof BasePart && typeof z === 'number' && inside(zone, character.position)) zoom = z; } if (zoom === want) return; want = zoom; tween?.cancel(); tween = TweenService.create(camera, { time: 0.8, easing: 'sineInOut' }, { distance: zoom }); tween.play(); });});Plan jumps
Section titled “Plan jumps”Characters reach full speed at once, jump straight up at jumpPower studs per second, and fall
under workspace.gravity. That makes jumps easy to plan:
- Height: about
jumpPower² ÷ (2 × gravity)studs. - Time in the air: about
2 × jumpPower ÷ gravityseconds. - Distance: about
walkSpeed × time in the airstuds.
With the defaults (walk speed 16, jump power 50, gravity 196.2), a running jump rises about 6 studs
and carries about 8 studs forward. Leave some margin: gaps a little shorter and ledges a little
lower than the limit feel fair. Set characterWalkSpeed and characterJumpPower on
StarterPlayer, and gravity on Workspace.
Build a 2D level in Studio
Section titled “Build a 2D level in Studio”Studio shows a 2D game from the side, the way players see it. See Build your world for every tool; in 2D:
- Scroll zooms at the cursor. Right-drag, middle-drag or W A S D pan, and Q / E zoom out and in.
- Move drags along X and Y, Rotate turns around Z, and Scale resizes width and height. Depth stays as it is.
- To put a part on another layer, type its Z in
positionin Properties.
-
Lay out the ground and platforms at Z 0 with Part. Long, flat boxes make good floors.
-
Add a SpawnLocation where players should start.
-
Add hazards and pickups at Z 0, with
canCollideoff so characters pass through them and scripts gettouched. -
Add scenery at negative Z with
canCollideandcanTouchoff. Bigger, paler parts further back read as distance. -
Select Play to try your jumps, and adjust.
Sprites
Section titled “Sprites”Set a part’s image to the URL of a picture and it’s drawn on the part, stretched to the part’s
width and height. Transparent areas of a PNG stay see-through, so a character-shaped picture on a
box looks like a cut-out. Match the part’s size to the picture’s proportions to avoid stretching.
The part still collides as its shape, not as the picture.
Terrain
Section titled “Terrain”Terrain works in 2D too: its middle row of heights becomes a hilly ground line that characters walk on, drawn as solid ground below the line, just behind layer 0.
Scripting in 2D
Section titled “Scripting in 2D”The API is the same as in 3D. Things to keep in mind:
- Ignore Z in gameplay. Raycasts and
getPartsInRadiuswork on the plane, whatever the layer.distanceTodoes count Z, so to measure from a character (always at Z 0) to a part on another layer, flatten it first:character.position.distanceTo(part.position.withZ(0)). - Rotate with
rotation.z. A full turn isnew Vector3(0, 0, 360). - Velocities have no Z. An unanchored part’s
velocity.zis always 0, and its Z (its layer) never changes. - Hinges always turn around Z, whatever their
axis, and constraint pivots ignore Z. - Which way a character faces:
rotation.yis 0 when it faces right (+X) and 180 when it faces left.
This pair of scripts throws a fireball the way the character faces when the player presses
F. Add a RemoteEvent named Shoot to ReplicatedStorage:
import { RemoteEvent, ReplicatedStorage, task, UserInputService, waitForChild } from '@rally/sdk';
task.spawn(async () => { const shoot = await waitForChild(ReplicatedStorage, 'Shoot', RemoteEvent); UserInputService.inputBegan.connect((input, gameProcessed) => { if (!gameProcessed && input.key === 'KeyF') shoot.fireServer(); });});import { Character, Color3, getChild, Part, type Player, Players, RemoteEvent, ReplicatedStorage, task, Vector3, workspace,} from '@rally/sdk';
const shoot = getChild(ReplicatedStorage, 'Shoot', RemoteEvent);const lastShot = new Map<Player, number>();
shoot.onServerEvent.connect((player) => { const character = player.character; if (!character || character.health <= 0) return; const now = task.time(); if (now - (lastShot.get(player) ?? -Infinity) < 0.4) return; // cooldown lastShot.set(player, now);
// 1 when facing right, -1 when facing left. const facing = Math.cos((character.rotation.y * Math.PI) / 180) >= 0 ? 1 : -1; const fireball = new Part(); fireball.shape = 'sphere'; fireball.size = new Vector3(1.2, 1.2, 1.2); fireball.color = Color3.fromHex('#ff7a1a'); fireball.material = 'neon'; fireball.anchored = false; fireball.restitution = 0.6; // bounces along the ground fireball.position = character.position.add(new Vector3(facing * 2, 0.5, 1)); // Z 1: in front fireball.velocity = new Vector3(facing * 50, 15, 0); fireball.touched.connect((other) => { if (other instanceof Character && other !== character) other.takeDamage(25); }); fireball.parent = workspace; task.delay(3, () => fireball.destroy());});
Players.playerRemoving.connect((player) => lastShot.delete(player));Parallax scrolling
Section titled “Parallax scrolling”Background layers that drift slower than the camera make a 2D world feel deep. The camera’s
position follows the view, so a LocalScript can move scenery a little each frame. This one moves
each part in a Scenery folder by a share of the camera’s movement that grows with its distance
behind the play layer:
import { BasePart, Folder, RunService, task, Vector3, waitForChild, workspace } from '@rally/sdk';
task.spawn(async () => { const scenery = await waitForChild(workspace, 'Scenery', Folder); const layers = scenery .getChildren() .filter((c): c is BasePart => c instanceof BasePart) // Z -20 or further back follows the camera at half speed; Z 0 doesn't move. .map((part) => ({ part, home: part.position, share: Math.min(0.5, Math.max(0, -part.position.z / 40)) }));
RunService.renderStepped.connect(() => { const camera = workspace.currentCamera; if (!camera) return; for (const { part, home, share } of layers) { part.position = new Vector3(home.x + camera.position.x * share, home.y, home.z); } });});It’s a LocalScript, so the scenery only moves on each player’s own screen, and costs nothing on the network.
The Coin Dash template
Section titled “The Coin Dash template”Coin Dash is a complete 2D side-scroller to learn from and build on. Players run right, grab coins, dodge spikes, ride a lift and touch the flag. Each run’s coins go on a high-score board.
What’s in it:
| Where | What |
|---|---|
Workspace › Start |
The SpawnLocation. |
Workspace › Terrain |
A Folder of parts: ground, steps, ledges, a wall and the Lift, a platform that slides back and forth. Its travelX and seconds attributes set how far and how fast. This is an ordinary Folder, not a Terrain object. |
Workspace › Hazards |
Spikes that kill on touch (canCollide off). |
Workspace › Coins |
Coins that come back 12 seconds after someone takes them. |
Workspace › Scenery |
Hills at Z -10 and clouds at Z -6, with collisions off. |
Workspace › Goal |
The flag. Touching it ends the run and records it. |
| ReplicatedStorage | Toast, a RemoteEvent for messages, and HighScores, StringValues that hold the board. |
StarterGui › HUD |
The coin counter, the board and the message banner. |
ServerScriptService › main |
The game’s rules. |
StarterPlayerScripts › hud |
The HUD, and coins that bob on each player’s screen. |
It also changes a few settings: walk speed 22 and jump power 60 on StarterPlayer, a respawn time
of 1 second on Players, fallenPartsDestroyHeight of -30 on Workspace, and no shadows on
Lighting.
Templates are published without their TypeScript, so main and hud open as compiled JavaScript.
They work as they are, but they’re hard to edit. To make the game your own, replace them with your
own scripts: Build a 2D platformer walks you through it,
starting from this template.
Top-down games: the Lost Crypt template
Section titled “Top-down games: the Lost Crypt template”A 2D game is normally seen from the side: characters run along X and jump. Lost Crypt shows how to make a top-down game instead, where you walk in four directions:
- Workspace has
gravity0, so nothing falls. - Its
movementLocalScript turns off the built-in controls (UserInputService.defaultControls = false), reads WASD, the arrow keys or the touch joystick, and moves your character itself, using raycasts so you slide along walls instead of walking through them. The server still checks every move, as it does in any game. - Floors are parts with collisions off (behind everything, at Z -2); walls, pillars and locked doors collide.
Its keys and doors are data: parts in Keys and Doors with a key attribute (Red, Blue,
Gold), and enemies appear at the markers in EnemySpawns (kind: slime, bat or skeleton).