# 2D games
A 2D game uses the same renderer, physics and ECS as a 3D one, laid out
flat:
- The game lives in the XY plane: +X is right, +Y is up.
- An orthographic camera at +Z looks down -Z.
- Sprites are flat `Quad` meshes with an unlit material.
- Physics stays 3D, with bodies kept in the plane.
Start from the template, a side-view platformer with a tile map, a
player, a goal block and a test:
```bash
rusting new . "Side" --template 2d
```
[Tutorial 2](tutorials/02-coin-run-cli.md) builds a complete 2D platformer
from it.
## The camera
A 2D camera is a `camera` with an `Orthographic` projection.
`vertical_size` is how many metres of the world the view shows top to
bottom; the width follows the window's shape:
```json
"camera": {"projection": {"Orthographic": {"vertical_size": 12.0, "near": 0.1, "far": 100.0}}, "active": true, "priority": 10, "viewport": null}
```
Place it at a positive Z, such as `[0, 1, 10]`, with no rotation. To
follow the player, make it a child of the player, as the template does.
## Sprites
A sprite is an object with a `mesh_renderer` that draws the `Quad`
primitive. The quad is 1 x 1 m in XY and faces +Z; scale the object to
size it. Use an `Unlit` material, so lights do not shade it, and turn
shadows off:
```json
"mesh_renderer": {
"mesh": {"BuiltinPrimitive": "Quad"},
"material": {"Inline": {
"model": "Unlit",
"alpha_mode": {"Mask": {"cutoff": 0.5}},
"base_color": [1.0, 1.0, 1.0, 1.0],
"base_color_texture": "../assets/sprites/hero.png"
}},
"cast_shadows": false,
"receive_shadows": false
}
```
- `base_color_texture` is relative to the scene file. Import the image
first, as described in [Assets](assets.md); `rusting asset generate
<project> sprite "star 2"` makes a placeholder.
- `Mask` cuts out pixels with alpha below the cutoff, which suits pixel
art. `Blend` draws soft edges.
- Draw order follows Z: a sprite at z = 0.1 draws over one at z = 0.
- `base_color` tints the texture.
To show one frame of a sprite sheet, scale and shift the texture
coordinates: each point samples `uv * uv_scale + uv_offset`. For frame
`i` of a strip of 4 frames side by side, use `"uv_scale": [0.25, 1.0]`
and `"uv_offset": [i * 0.25, 0.0]`. No `GameScene` call changes the frame
yet, so flipbook animation needs a separate material for each frame
(`create_material` and `set_material`).
## Tile maps
`rusting.tile_map` draws a grid of square tiles written as text rows, top
row first. The object's position is the top-left corner of the grid;
rotation and scale are not applied.
```json
"rusting.tile_map": {
"tile_size": 1.0,
"rows": [
"..........",
"....BB....",
"##########"
],
"tiles": {
"#": {"color": [0.25, 0.55, 0.3, 1.0], "texture": null, "solid": true},
"B": {"color": [0.55, 0.35, 0.2, 1.0], "texture": "tiles/bricks.png", "solid": true}
}
}
```
- Each character looks up its kind in `tiles`. Characters with no kind,
such as `.`, are empty cells.
- `texture` is relative to `assets/`, not to the scene. Pixels with alpha
below 0.5 are cut out, and `color` tints the texture.
- `solid` tiles block CPU bodies. Each run of solid tiles in a row
becomes one fixed box collider.
- Tiles are spawned when the map loads or changes and are never saved.
Change the map, not its tiles.
In the editor, select the map: the Inspector's Tile Painter section has a
brush palette and four tools that work in the Scene View. Paint (D),
Rectangle (R), Line (L) and Fill (B) use Godot's keys. Fill stays inside
the grid's used area.
Game code reads and writes cells by world position:
```rust
let feet = [x, y - 0.6, 0.0];
if scene.tile("Level", feet) == Some('B') {
// break the brick under the player
scene.set_tile("Level", feet, '.');
}
```
`tile` returns `.` for an empty cell or one past the grid. `set_tile`
grows the grid as needed. The tiles and colliders rebuild on the next
fixed step.
## Platformer movement
`rusting.platformer_controller` runs and jumps a body in the XY plane. It
reads the `player.left`, `player.right` and `player.jump` actions; see
[Input](input.md) for the default keys and gamepad buttons.
```json
"rusting.platformer_controller": {"run_speed": 5.0, "jump_speed": 8.0, "gravity": 20.0, "collision_mask": 4294967295, "air_jumps": 1}
```
- Give the body a `Kinematic` CPU `rigid_body` and a `Box` collider. The
template's player is 0.6 m wide and 1 m tall
(`"half_extents": [0.3, 0.5, 0.3]`).
- `air_jumps` is the number of extra jumps in the air: 1 is a double
jump.
- A jump pressed up to 0.1 s before landing, or 0.1 s after running off a
ledge, still jumps. Hitting a ceiling stops the rise.
- A moving platform carries a body that stands on it.
- `collision_mask` picks the collision layers the body stops against.
- `scene.dash(name, velocity, seconds)` moves it at a fixed velocity with
no gravity for that long. Walls still stop it.
For a top-down game, use `rusting.player_controller` with a camera that
looks down, as the `top-down` and `twin-stick` templates do. Those games
are 3D scenes seen from above.
## 2D physics
The CPU solver is 3D. To keep dynamic bodies in the plane, add
`rusting.axis_lock`:
```json
"rusting.axis_lock": {"linear": [false, false, true], "angular": [true, true, false]}
```
The body then moves only in X and Y and turns only around Z. A `Sphere`
acts as a circle, a `Box` as a rectangle and a `Capsule` as a 2D capsule,
as long as everything it touches is in the same plane.
For other outlines, add `rusting.polygon` next to the collider:
| `Polygon` | the convex hull of `points` | any |
| `Chain` | a thin wall along each segment; `closed` joins the ends | `Fixed` only |
```json
"rusting.polygon": {"points": [[-1.0, 0.0], [1.0, 0.0], [0.0, 1.0]], "depth": 1.0, "closed": true}
```
`points` are in metres in the object's XY plane, counterclockwise.
`depth` is the thickness along Z, centred on the object. A `Chain` makes
slopes and curved ground. In Rust, `Polygon2d::mesh()` gives a mesh of
the same outline to draw.
GPU bodies and the other physics features work in 2D too; see the
[Physics guide](physics.md).
## HUD and screen space
The HUD is the same in 2D and 3D: `rusting.hud` places text and buttons
at a screen anchor, and `follow` pins a label to an object. See
[Menus and UI](menus-and-ui.md). `scene.on_screen(point)` gives where a
world point shows in the view, and `scene.pointer_ray()` gives the world
ray under the cursor, for clicking on sprites.
## Test it
Scenarios work the same as in 3D. The template's `tests/run.json` holds
right, taps jump and checks the player's position:
```json
{"tick": 1, "press": "player.right"},
{"tick": 5, "tap": "player.jump"},
{"tick": 7, "within": 30, "expect": {"entity": "Player", "path": "/transform/position/1", "greater_than": 1.5}}
```
Run it with `rusting test Side Side/tests/run.json`.