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
nsand 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.tomlpullsns-core,ns-audio,ns-assets, andns-shell-minifbfromgithub.com/Toyz/ns. To use a local checkout instead, replace thegit = "..."entries withpath = "../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
sector game– declares the module name. All phases are qualified asgame.init,game.tick,game.draw.access <gfx>– imports the graphics module so you can callgfx.fill_screen,gfx.draw_text, etc.access <color>– imports color presets likecolor.Whiteand constructors likecolor.rgb(r, g, b),color.rgba(...), andcolor.hex("#rrggbb").phase init()– runs once when the engine starts.phase tick(dt)– runs on a fixed timestep (default 60 Hz). Drainswindow.poll_event()so :close_requested can callsys.exit(), then polls input and moves the text.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:
| Phase | Signature | When it runs |
|---|---|---|
game.init | phase init() | Once at startup |
game.tick | phase tick(dt) | Every simulation frame (dt = seconds per tick) |
game.draw | phase 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.tickruns at a fixed rate (default 60 Hz). Use it for game logic, physics, input handling, and state updates.game.drawruns 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
- Language Reference – learn the full syntax
- Standard Library – math, strings, bytes, JSON, and more
- Engine API – graphics, audio, input, save, window management
- Build System – asset pipeline, plugins,
game.tomlconfig