Skip to content

Getting started ​

This page builds a small program: a window, a background, a player sprite you move with the arrow keys, and a score label. It takes about ten minutes.

1. Make a window project ​

sh
thicket new mygame window
cd mygame

Edit thicket.json5. List the willow system and turn on the fs permission so the game can load images from disk:

json5
{
    entry: "app.tks",
    systems: ["willow"],
    window: { title: "My Game", width: 800, height: 600, bgColor: "#0E1E3C" },
    permissions: { fs: true },
}

A project with a window section is a window app. Without one it is a console app.

2. Put a sprite on screen ​

Put an image at assets/player.png. Replace app.tks with:

ts
import { Sprite } from "willow";

class MyGame {
    static main(): void {
        const player: Sprite = new Sprite("assets/player.png");
        player.setAnchor(0.5, 0.5);
        player.x = 400;
        player.y = 300;
        engine.root2d.add(player);
    }
}

Run it with thicket run. The image sits in the middle of the window.

  • new Sprite(path) loads the image and makes a node that draws it.
  • setAnchor(0.5, 0.5) moves the sprite's origin to its center, so x and y place the middle of the image.
  • engine.root2d.add(player) puts the sprite in the 2D tree. Nodes outside the tree are not drawn.

Don't name your class App

App is the built-in lifecycle class. A class of your own called App hides it in that file, and App.onFrame stops working. Name your entry class after your game.

3. Move it with the keyboard ​

App.onFrame runs a function once per drawn frame. Keyboard and Key are global, so there is nothing to import.

ts
import { Sprite } from "willow";

class MyGame {
    static main(): void {
        const player: Sprite = new Sprite("assets/player.png");
        player.setAnchor(0.5, 0.5);
        player.x = 400;
        player.y = 300;
        engine.root2d.add(player);

        const SPEED: float = 240;

        App.onFrame((dt: float): void => {
            if (Keyboard.isDown(Key.RIGHT)) {
                player.x += SPEED * dt;
            }
            if (Keyboard.isDown(Key.LEFT)) {
                player.x -= SPEED * dt;
            }
            if (Keyboard.isDown(Key.DOWN)) {
                player.y += SPEED * dt;
            }
            if (Keyboard.isDown(Key.UP)) {
                player.y -= SPEED * dt;
            }
        });
    }
}

dt is the time since the last frame, in seconds. Multiplying by it moves the player the same distance per second at any frame rate.

Keyboard.isDown is true while a key is held. Keyboard.isPressed is true only on the frame it goes down, which is what you want for a jump or a menu choice.

4. Add a background and a label ​

Nodes draw in tree order: a node added later draws on top. Add the background first, then the player, then the label.

ts
import { Sprite, Quad, Text, Font } from "willow";

class MyGame {
    static main(): void {
        const ground: Quad = new Quad(800, 120, Color.hex("#2E5E3A"));
        ground.y = 480;
        engine.root2d.add(ground);

        const player: Sprite = new Sprite("assets/player.png");
        player.setAnchor(0.5, 1.0);
        player.x = 400;
        player.y = 480;
        engine.root2d.add(player);

        const label: Text = new Text("Coins: 0", Font.system());
        label.fontSize = 24;
        label.color = Color.WHITE;
        label.x = 16;
        label.y = 16;
        engine.root2d.add(label);

        let coins: int = 0;
        const SPEED: float = 240;

        App.onFrame((dt: float): void => {
            if (Keyboard.isDown(Key.RIGHT)) {
                player.x += SPEED * dt;
            }
            if (Keyboard.isDown(Key.LEFT)) {
                player.x -= SPEED * dt;
            }
            if (Keyboard.isPressed(Key.SPACE)) {
                coins += 1;
                label.text = "Coins: " + coins;
            }
        });
    }
}
  • Quad is a solid rectangle. Color.hex("#2E5E3A") makes a colour from a hex string.
  • setAnchor(0.5, 1.0) puts the origin at the bottom middle of the sprite, so y is where its feet stand.
  • Font.system() is willow's bundled default font. Assigning label.text re-lays out the text on the next frame.

5. Game logic on fixed ticks ​

App.onFrame runs once per drawn frame, so it runs more often on a faster screen. For logic that should run at a steady rate, use App.onTick. Ticks run 60 times a second by default (App.tps).

ts
import { Sprite } from "willow";

class MyGame {
    static main(): void {
        const player: Sprite = new Sprite("assets/player.png");
        player.setAnchor(0.5, 0.5);
        player.x = 400;
        player.y = 300;
        player.interpolate = true;
        engine.root2d.add(player);

        const STEP: float = 4;

        App.onTick((): void => {
            if (Keyboard.isDown(Key.RIGHT)) {
                player.x += STEP;
            }
            if (Keyboard.isDown(Key.LEFT)) {
                player.x -= STEP;
            }
        });
    }
}

interpolate = true makes willow draw the sprite between its last two tick positions, so it moves smoothly even when the screen refreshes faster than the tick rate. See Lifecycle and time.

Next ​