Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Getting Started

This guide walks you through creating your first Epitaph game, from an empty directory to a running window.


Prerequisites

  • Rust toolchain (stable) – needed to build ns and your game
  • ns CLI installed globally:
cargo install --path crates/ns

Scaffold a New Project

The fastest way to start is with ns init:

ns init my-game
cd my-game

This creates a complete project:

my-game/
  Cargo.toml          Rust project (depends on ns-core, ns-audio, ns-shell-minifb)
  build.toml          Build manifest for scripts/assets
  .gitignore
  src/
    main.rs           Game runner (engine + audio + shell setup)
  scripts/
    main.ep           Entry-point Epitaph script

What gets generated

src/main.rs sets up the engine, audio backend, and shell. It uses debug vs release the same way as ns-dothack-app: in debug, scripts and assets load from ./scripts and ./assets when those folders exist; in release (or when PAKs exist), it uses game.toml paths and scripts.pak / game.pak.

Audio is wired to the engine (not the shell) via engine.set_audio(...) and the AudioRuntime script plugin. The shell only handles the window and input.

See the template generated by ns init in crates/ns/src/init.rs (MAIN_RS) for the full mount_scripts / try_mount_assets helpers using PackProvider (not PakProvider) and DirProvider.

scripts/main.ep is a minimal hello-world game:

sector game

access <gfx>
access <color>
access <window>

phase init() {
    window.open(640, 480, "My Game")
}

phase tick(dt) {
}

phase draw() {
    gfx.fill_screen(30, 30, 60)
    gfx.draw_text("Hello, NetSlum!", 10, 10, color.White)
}

Note: The generated Cargo.toml pulls ns-core, ns-audio, ns-assets, and ns-shell-minifb from github.com/Toyz/ns. To use a local checkout instead, replace the git = "..." entries with path = "../path-to-ns/crates/..." paths.


Project Layout

After running ns build, the project looks like:

my-game/
  Cargo.toml
  build.toml
  game.toml           Generated runtime config
  scripts.pak         Compiled bytecode archive
  src/
    main.rs
  scripts/
    main.ep

If you add an [assets] section to build.toml, you also get a game.pak.


Your First Script

Here is a more complete example with input handling:

sector game

access <gfx>
access <color>
access <input>
access <window>
access <sys>

let message = "Hello, NetSlum!"
let x = 80
let y = 60

phase init() {
    window.open(640, 480, "My Game")
}

phase tick(dt) {
    sustain ev = window.poll_event() {
        when ev == :close_requested {
            sys.exit()
        }
    }

    let cmd = input.poll()
    when cmd == :left  { x = x - 2 }
    when cmd == :right { x = x + 2 }
    when cmd == :up    { y = y - 2 }
    when cmd == :down  { y = y + 2 }
}

phase draw() {
    gfx.fill_screen(20, 20, 40)
    gfx.draw_text(message, x, y, color.White)
}

What this does

  1. sector game – declares the module name. All phases are qualified as game.init, game.tick, game.draw.
  2. access <gfx> – imports the graphics module so you can call gfx.fill_screen, gfx.draw_text, etc.
  3. access <color> – imports color presets like color.White and constructors like color.rgb(r, g, b), color.rgba(...), and color.hex("#rrggbb").
  4. phase init() – runs once when the engine starts.
  5. phase tick(dt) – runs on a fixed timestep (default 60 Hz). Drains window.poll_event() so :close_requested can call sys.exit(), then polls input and moves the text.
  6. phase draw() – runs every render frame. Clears the screen and draws the message.

The Game Contract

Every Epitaph game must provide exactly three phases in sector game:

PhaseSignatureWhen it runs
game.initphase init()Once at startup
game.tickphase tick(dt)Every simulation frame (dt = seconds per tick)
game.drawphase draw()Every render frame

The engine validates that all three exist before starting. If any is missing, you get a load error.

Important: tick vs draw

The engine uses a fixed-timestep loop:

  • game.tick runs at a fixed rate (default 60 Hz). Use it for game logic, physics, input handling, and state updates.
  • game.draw runs once per frame after all pending ticks. Use it for rendering.

Only draw commands issued during game.draw appear on screen. Any gfx.* calls made in init or tick are collected but never rendered.


Compile and Run

Build and run in one step

ns run

Runs cargo run without packing first. In debug, your game loads .ep files from ./scripts (and ./assets if present) so iteration is fast. ns run sets Cargo’s working directory to the folder that contains build.toml (so it works when your shell is elsewhere). If that folder is part of a Cargo workspace and cargo run would pick the wrong crate, use ns run -p your-package.

ns run --release

Runs ns build (scripts + assets PAKs, game.toml), then cargo run --release. The release binary reads packed scripts and assets.

Build only

ns build
ns build --full              # also run `cargo build` after packing
ns build --full --release    # same with `cargo build --release`
ns build --full -p my-game   # workspace: `cargo build -p my-game`

Compiles every .ep file under scripts/ into bytecode, bundles them into scripts.pak, processes assets (if configured), and writes game.toml. With --full, ns then builds your game crate so the binary matches the new PAKs.

Run separately

cargo run              # debug → dev-mode paths (./scripts, ./assets)
cargo run --release    # release → PAKs / game.toml paths

The generated src/main.rs picks script and asset sources based on that split (see ns init template).


Compile a Single Script

For quick iteration you can compile one file without a full build:

ns compile scripts/main.ep -o main.epc

This produces a standalone .epc (Epitaph Pre-Compiled) bytecode file.


Next Steps