---
title: "Drawing in 2D"
url: https://recursiveai.org/onegame/docs/2d/
description: "Draw sprites, sprite sheets and clips, shapes and text from view code with Draw2D, and move the 2D camera."
version: "1.5.1"
source: https://github.com/recursive-ai-oss/onegame/blob/15633bb497cc96b641d83e727c10a155e0f2f846/docs/2d.md
---

# Drawing in 2D

A 2D game draws from **view code**: an entity that implements `IView2D` gets a `Draw(Draw2D d)` call every frame, and what it draws appears under the UI. No render stage of your own. View code reads state and never writes it (assigning a synced property in `Draw` is a compile error).

```csharp
using System.Numerics;
using OneGame.Core;
using OneGame.Host;
using OneGame.UI;

[Entity]
public partial class Ship : Entity, IView2D
{
    [Synced] public partial Fixed X { get; private set; }
    [Synced] public partial Fixed Y { get; private set; }

    [View]
    public void Draw(Draw2D d) =>
        d.Sprite("art/ship.png", new Vector2(X.ToSingle(), Y.ToSingle()), scale: 2, tint: Color.White);
}
```

The path is loaded once through the game's asset service and cached, so calling it every frame is cheap, and an edited file shows up while the game runs ([assets.md](https://recursiveai.org/onegame/docs/assets/)). World units are pixels at zoom 1: +X right, +Y down, rotation in radians, clockwise.

## Sprites, shapes, text

Every call takes an optional `z` (higher draws later); ties keep call order. Sprites take `rotation`, `scale`, `tint`, `origin` (a fraction of the sprite, default its centre) and `flip`.

```csharp
[View]
public void Draw(Draw2D d)
{
    d.FillRect(new Rect(-400, 16, 800, 24), Color.Rgb(0x35506b));
    d.StrokeRect(new Rect(-400, 16, 800, 24), Color.Rgb(0x8fb8de), 2);
    d.Line(new Vector2(-120, 16), new Vector2(-120, -40), Color.Rgb(0xd0d0d0), 2);
    d.FillCircle(new Vector2(60, -8), 8, Color.Rgb(0xf2c14e));
    d.StrokeCircle(new Vector2(60, -8), 12, Color.Rgb(0xf2c14e), 1.5f);

    d.Text("hero", new Vector2(0, -24), 12, Color.White, new Vector2(0.5f, 1)); // in the world, anchored bottom-centre
    d.ScreenText("walk with Go", new Vector2(8, 8), 14, Color.Rgb(0xdddddd));     // in window pixels, for a HUD
}
```

Text is one line, unrotated, in the game's fonts. For anything that wraps or reacts to clicks, use the UI ([ui-guide.md](https://recursiveai.org/onegame/docs/ui-guide/)). Sprites can also come from a texture you already hold, a pixel rectangle of it, or a hand-built `Sprite` (`d.Sprite(in Sprite)`).

## Sprite sheets and clips

A sheet cuts one picture into equal frames; clips name runs of frames and play against view time (`d.Time`), so animation is smooth in play, identical in captures and replays, and frozen while the sim is paused.

```csharp
private SpriteSheet? _sheet;

[View]
public void Draw(Draw2D d)
{
    if (_sheet is null)
    {
        _sheet = SpriteSheet.Grid(d.Texture("art/hero.png", TextureFilter.Nearest), 16, 16);
        _sheet.AddClip("walk", 0, 4, 8);   // first frame, count, frames per second
        _sheet.AddClip("idle", 4, 2, 2);
    }

    var at = new Vector2(X.ToSingle(), 0);
    d.Sprite(_sheet, Speed == Fixed.Zero ? "idle" : "walk", at, scale: 2,
        flip: Speed < Fixed.Zero ? SpriteFlip.Horizontal : SpriteFlip.None, z: 1);
}
```

`d.Sprite(sheet, frame, ...)` draws a single frame by number. `SpriteSheet.Frames(texture, rects)` takes explicit rectangles instead of a grid. An unknown clip name throws `UnknownClipException`. To play a clip from a moment, pass `clipTime: d.Time - startedAt`.

## The camera

`d.Camera` is a rig that every `Draw2D` call shares:

```csharp
d.Camera.Zoom = 2;                                 // physical pixels per world unit
d.Camera.Bounds = new Rect(-400, -300, 800, 600);  // keeps the view inside this world rectangle
d.Camera.Smoothing = 0.15f;                        // follow time constant in seconds; 0 = exact
d.Follow(new Vector2(X.ToSingle(), 0));            // centre on a point (smoothed)
var world = d.ScreenToWorld(new Vector2(320, 200)); // a pixel to a world point
var pixel = d.WorldToScreen(new Vector2(60, -8));   // and back
```

`d.Camera.Snap(position)` jumps without smoothing; `Rotation` turns the view. Smoothing depends on how many frames were drawn, so keep it at 0 where captures must repeat exactly.

## Dropping down

When the happy path is not enough, the layers underneath are open. `d.Scene` is the frame's `Scene2D`, `d.Layer` the layer new sprites go on, and `d.Scene.AddLayer(name, parallax)` makes a scrolling background layer. `d.Renderer` is the `Renderer2D`, for textures made from pixels (`CreateTexture`), tile maps, custom pixel-shader materials and post-processing. Draw them from a render stage's `DrawOverUi`, alongside the entities' `Draw` calls. The engine-side contracts are in `docs/interfaces/render2d.md`.

## Look at it

```
onegame capture --out shots/main.png --size 720p --after 30 --json
```

Captures and the scenario `capture` expectation include everything drawn here. The control operation `draw_view` reports the last frame's sprite and text counts, the tick, and the camera, and converts between screens and worlds: `draw_view` with `{"screen":[160,100]}` returns the world point under that pixel, `{"world":[60,-8]}` the pixel of a world point. `tests/Draw2DGame` is a worked example: a walking hero with a clip, a follow camera, shapes and text, and a scenario with capture references.
