# Draw a 2D game

Use sprites for image artwork, vectors for shapes, and text for instructions
and scores. This example draws all three and lets you collect a circle with
keyboard movement.

Start with the `2d` starter, which includes the `controllers-topdown` plugin:

```sh
karty new --yes --template 2d my-game
```

Keep the package's archive URL and checksum in `karty.toml`. The first build
restores its source under ignored `.karty/packages/controllers-topdown/`.
For an existing project, run `karty package import controllers-topdown@0.0.1`
first; see [reusable packages](../design/packages.html).

## Add a sprite

Create a project from the [guide home](index.html). Download
[player.png](assets/player.png) and save it as `assets/textures/player.png`.
The CLI imports that image and generates `assets.TexturePlayer` on the next
build. Use the generated ID when creating the sprite.

## Draw and play

Keep the project resolution at 960 by 540. Replace `src/main.go` with this
scene. Change the imports if your project has a different name.

```go
package main

import (
    "github.com/karty-game/controllers-topdown"
    "example.com/my-game/.karty/assets"
    "example.com/my-game/.karty/engine"
)

const speed = 180 // logical pixels per second

type Game struct {
    engine.Game
    player *engine.Sprite2D
    goal *engine.Shape2D
    label *engine.Text2D
    controller *controllers.Planar
    won bool
}

//go:wasmexport karty_register
func main() {
    options := controllers.DefaultTopDown2DOptions()
    options.Speed, options.RunSpeed = speed, speed
    controller, err := controllers.NewTopDown2D(options)
    if err != nil { panic(err) }
    game := &Game{controller: controller}
    engine.Run(engine.Hooks{
        OnStart: game.start,
        OnUpdate: game.update,
        OnKeyDown: func(key engine.Key) { game.key(key, true) },
        OnKeyUp: func(key engine.Key) { game.key(key, false) },
        OnInputReset: game.controller.Reset,
    })
}

func (game *Game) start() {
    // Draw the background behind the sprite, goal, and text.
    background := game.NewShape2D(engine.Rectangle(960, 540), engine.NewVec2(0, 0))
    background.SetFill(engine.RGBA(12, 21, 35, 255))
    background.SetLayer(-1)
    game.player = game.NewSprite2D(assets.TexturePlayer, engine.NewVec2(160, 280))
    game.goal = game.NewShape2D(engine.Circle(18), engine.NewVec2(760, 350))
    game.goal.SetFill(engine.RGBA(255, 200, 99, 255))
    game.label = game.NewText2D("WASD / arrows: collect the gold circle", engine.NewVec2(32, 24))
    game.label.SetFontSize(20)
    game.label.SetLayer(1)
}

func (game *Game) key(key engine.Key, down bool) {
    if key == engine.KeyR && down {
        game.controller.Reset()
        game.won = false
        game.player.Translate(160-game.player.Position().X, 280-game.player.Position().Y)
        game.goal.SetAlpha(1)
        game.label.SetText("WASD / arrows: collect the gold circle")
    }
    if control, ok := movementKey(key); ok { game.controller.Key(control, down) }
}

func (game *Game) update(engine.Frame) {
    if game.won { return }
    delta := game.controller.Step(controllers.Vec2{X: float64(game.player.Position().X), Y: float64(game.player.Position().Y)})
    dx, dy := float32(delta.X), float32(delta.Y)
    if dx != 0 || dy != 0 {
        x := min(max(game.player.Position().X+dx, 16), 944)
        y := min(max(game.player.Position().Y+dy, 80), 524)
        game.player.Translate(x-game.player.Position().X, y-game.player.Position().Y)
    }
    // Our game collects the goal within 32 pixels of its centre.
    dx = game.player.Position().X - game.goal.Position().X
    dy = game.player.Position().Y - game.goal.Position().Y
    if dx*dx + dy*dy < 32*32 {
        game.won = true
        game.goal.SetAlpha(0)
        game.label.SetText("Collected! Press R to restart")
    }
}
func movementKey(key engine.Key) (controllers.Control, bool) {
	switch key {
	case engine.KeyW: return controllers.Forward, true
	case engine.KeyS: return controllers.Backward, true
	case engine.KeyA: return controllers.Left, true
	case engine.KeyD: return controllers.Right, true
	case engine.KeyArrowUp: return controllers.ForwardAlt, true
	case engine.KeyArrowDown: return controllers.BackwardAlt, true
	case engine.KeyLeft: return controllers.LeftAlt, true
	case engine.KeyRight: return controllers.RightAlt, true
	case engine.KeyShiftLeft: return controllers.RunLeft, true
	case engine.KeyShiftRight: return controllers.RunRight, true
	}
	return 0, false
}

```

Run `karty dev`. Move with WASD or the arrow keys, reach the circle, then press
R to try again. `OnInputReset` clears held keys when the game loses focus.
The preset normalizes diagonal input and converts `speed = 180` pixels per
second into three pixels per fixed 60 Hz update. Camera following, pointer
targets and zoom are available on the same preset when your game needs them.

## Change the drawing

Positions use canvas pixels. The upper left is `(0, 0)`; X grows right and Y
grows down. Sprite and circle positions mark their centres. Rectangle and text
positions mark their upper-left corners.

`NewSprite2D` draws the imported image. `NewShape2D` draws the rectangle and
circle without image files. `NewText2D` draws the instruction, and `SetText`
changes it when the goal is collected. Higher layers draw above lower ones.

Try another PNG, change the circle's radius or color, or move the goal. Use
setters such as `Translate`, `SetFill`, and `SetText` to update existing
entities instead of creating them again each frame.

![A 2D sprite, vector goal, and text drawn by the example.](assets/2d-scene.jpg)

## Add feedback and share

[Sound and video](media.html) add effects or music. [Build and share](share.html)
packages your game for the browser or desktop.

[KartUI](ui.html) is optional when you need menus or reusable screen layouts.
Its components work with both 2D and 3D games.
