Tutorial: Build the Demo Campaign
This tutorial builds a small example campaign called demo from scratch, one file at a time, then validates and boots it. By the end you will have written every kind of entity a small game needs and watched the engine load it.
Introduction
Isometry games are pure data. You do not write code to make a game; you write YAML entity files and package them into a .zip. This page walks the whole path: a starting point, a map made of tiles, a playable hero, an action the hero can take, an AI-driven enemy, a waypoint marker, and finally the validate-and-boot step that proves it all hangs together.
Each step shows a real file, so the files you write form a single coherent, working campaign.
Prerequisites
You should have worked through these pages first:
- Install and run — get the engine running.
- Your first campaign — the smallest campaign that loads.
- Campaign structure — how a campaign folder is laid out.
- Entities and KeyRefs — how entities reference each other.
Note: Entity YAML files always use clean keys —
name,min,max,default. The engine has internal aliases (name_and so on) but those never appear in campaign files. Writingname_in a file is an error.
How a campaign fits together
A campaign is a folder of YAML files. Each file holds one or more entity types, keyed by an entity key you choose. Entities point at each other with KeyRefs: a field whose value is another entity's key.
There are exactly 33 entity types. You will not use all of them here, but you will touch the ones that form the spine of any game: Main, Map, Actor, Action, Strategy, and a handful of supporting entities they reference. For definitions of each term, see the glossary.
Create a folder named demo and put each file below inside it. Keep file names as shown; the engine reads every .yaml in the folder regardless of name, but matching these keeps things findable.
Step 1: Define the entry point
Every campaign needs exactly one Main entity. It names the player's actor and the starting map — nothing more is required.
Main.yaml:
Main:
demoMain:
actor: heroActor
map: demoMap
Both actor and map are KeyRefs. They point at entities you have not written yet; you will fill them in over the next steps. The keys heroActor and demoMap are referenced again later — use them exactly.
Step 2: Build the tileset and map
A map is drawn from tiles. You define the tiles once in a TileSet, lay them out in text grids, wire those grids into a TileMap, then wrap it all in a Map.
Define the tiles
A TileSet points at one texture and lists the Tile entities cut from it. Each Tile records its grid index, a one-character symbol you will use in the layout files, and navigation/obstacle references to a Face entity — which parts of the tile players can walk on and which block them. Here a shared roof (the top of the tile) is what grass is walkable on and what the wall blocks against.
tileset.yaml:
TileSet:
demoTileSet:
columns: 8
texture: /assets/basic.png
tiles:
- grass
- water
- wall
Face:
roof:
northeast: true
northwest: true
Tile:
grass:
symbol: G
index: 0
navigation: roof
water:
symbol: W
index: 46
wall:
symbol: X
index: 52
obstacle: roof
columns is how many tiles wide the texture is, used to turn an index into a position in the image. Grass is walkable; walls block movement. A Face marks which parts of a tile are walkable or solid, using the directions northeast, east, southeast, southwest, west, and northwest (the top of a tile is northeast + northwest).
Draw the layers
A TileMap stacks Layer entities. Each layer's source points at a plain-text file under the campaign folder where every character is a tile symbol and a space is empty.
tilemap.yaml:
TileMap:
demoTileMap:
tileset: demoTileSet
layers:
- demoFloor
- demoWater
- demoWalls
Layer:
demoFloor:
source: /demoMap/layer0
ysort: false
demoWater:
source: /demoMap/layer1
ysort: true
demoWalls:
source: /demoMap/layer2
ysort: true
ysort controls whether tiles on that layer sort against actors by depth — leave it off for the flat floor, on for things actors walk behind.
Create the grid files at demoMap/layer0, demoMap/layer1, and demoMap/layer2 (no file extension). A floor file is rows of the grass symbol:
GGGGGGGG
GGGGGGGG
GGGGGGGG
Each G becomes one grass tile; spaces leave a gap.
Assemble the map
The Map ties the tilemap to a spawn point (where the player appears), the NPC deployments, and optional scenery. Spawn is a KeyRef to a Vector, which you define in the same file.
map.yaml:
Map:
demoMap:
name: Demo Map
tilemap: demoTileMap
spawn: demoSpawn
deployments:
- guardDeploy
- merchantDeploy
- patrollerDeploy
background:
- skyParallax
- starsParallax
audio:
- bgMusic
Vector:
demoSpawn:
x: 128
y: 16
background and audio are optional; you can drop them while testing. The deployments are written in Step 5. For the full map field list, see Build a map.
Step 3: Create the hero actor
The hero is an Actor — a character in the world. Actors are the richest entity type, but no field is required; you add only what your character needs. The hero needs a look (a Sprite), trackable values (Resource entities), and things it can do (Skill entities). Its hitbox is derived automatically from the sprite.
Give it a sprite
A Sprite points at a texture, an AnimationSet, a Vector for size, and an Inset for margin. Note the YAML key is animation_set, with an underscore.
sprite.yaml:
Sprite:
heroSprite:
animation_set: heroAnimSet
texture: /assets/astronaut_sprite.png
size: spriteSize
margin: spriteMargin
Vector:
spriteSize:
x: 64
y: 64
Inset:
spriteMargin:
top: 16
right: 8
bottom: 16
left: 8
size is the pixel size of one frame; margin is the per-edge padding around the art — it seats the feet on the tile and trims the hitbox to the body.
Animate it
An AnimationSet lists Animation entities by name. Each Animation has eight compass-direction fields (N, NE, E, SE, S, SW, W, NW), each an array of frame indices into the sprite sheet. Add an optional sound to play when the animation runs.
animation.yaml:
AnimationSet:
heroAnimSet:
name: Hero Animations
animations:
- idle
- run
- tool
Animation:
idle:
S:
- 30
N:
- 35
E:
- 5
W:
- 0
run:
S:
- 22
- 23
- 24
- 23
tool:
sound: attackSound
S:
- 21
A full character fills in all eight directions; the trimmed example above shows the shape. A single-frame array like "S": [30] is a still pose; a multi-frame array like "S": [22, 23, 24, 23] cycles.
Track resources
A Resource is a numeric value on the actor — health, mana, gold. Each has a default, a min, and a max.
resources.yaml:
Resource:
health:
name: Health
default: 20
min: 0
max: 20
icon: /assets/icons/health.png
description: Hit points. Reach 0 and you're done.
menu: healthMenu
mana:
name: Mana
default: 10
min: 0
max: 10
gold:
name: Gold
default: 5
min: 0
max: 999
For visibility and rolled values such as a luckMeasure (Measure, expression 1d20), see Add resources and measures.
Assemble the hero
Now wire it together. The hero references its sprite, resources, skills, and a group, and sets movement and vision fields. Its hitbox is derived automatically from the sprite, so there is nothing to author.
heroActor.yaml:
Actor:
heroActor:
name: Hero
speed: 1.0
sprite: heroSprite
base: 8
public:
- health
private:
- health
- mana
- gold
skills:
- attackSkill
- healSkill
perception: 25
salience: 1
menu: heroMenu
resources:
- health
- mana
- gold
measures:
- luckMeasure
triggers:
- deathTrigger
timers:
- regenTimer
group: playerGroup
Vector:
heroStartPos:
x: 200
y: 100
base is the footprint circle in pixels, speed the movement rate, perception the vision range, and salience how easily others spot this actor. public and private list which resources other players and the owner can see. The skills array is capped at nine entries. For a full walk of actor fields, see Create an actor.
Step 4: Define an action and a skill
An Action changes the game when it runs. Its only required field is do, the name of one of the 75 built-in action functions. The attack calls minus_resource_target, which subtracts a resource from whatever the hero targets.
The parameters convention
An action passes data to its function through Parameter entities. The parameters field is a flat array of Parameter keys, and you define those parameters in a sibling top-level Parameter block. Each parameter is a key and a value, both strings.
actions.yaml:
Action:
attackAction:
name: Attack
time: 1.1
animation: tool
do: minus_resource_target
parameters:
- attackResourceParam
- attackDamageParam
Parameter:
attackResourceParam:
key: resource
value: health
attackDamageParam:
key: expression
value: (1d6)-1
Here time is the cast duration in seconds and animation names the clip to play. The damage is a dice expression, (1d6)-1.
Warning: Never nest parameters inline like
"parameters": [{ "Parameter": {...} }]. Theparametersfield is always a flat array of keys, with the definitions in a siblingParameterblock.
Actions can branch. A Condition compares two values; an action's if runs the then action on a true result and the else action otherwise. The isDead condition checks @health <= 0:
Condition:
isDead:
left: '@health'
operator: <=
right: '0'
The @ token reads a resource on the acting actor. For the full set of functions you can put in do, see the action function reference and Define actions.
Wrap the action in a skill
A Skill is what appears on the hero's action bar. It points at a start action and an end action. The attack skill is named Punch.
skills.yaml:
Skill:
attackSkill:
name: Punch
start: attackAction
end: attackAction
icon: /assets/icons/sword.png
Step 5: Add an NPC with AI
An NPC is just an Actor with a Strategy — the AI controller. A strategy holds a list of Behavior entities; the engine checks each behavior's goals (a list of conditions) and runs its action when the goals are met.
Write the AI chain
The guard uses guardStrategy, which scans for a target, then pursues it. The behaviors reach back into the conditions and actions you already have.
strategies.yaml:
Strategy:
guardStrategy:
behaviors:
- guardBehavior
- followBehavior
followStrategy:
behaviors:
- followBehavior
Behavior:
guardBehavior:
goals:
- hasTarget
action: scanAction
followBehavior:
goals:
- isNearTarget
action: pursueAction
The supporting actions move the NPC. scanAction runs target_nearest to pick a target, and pursueAction runs move_to_target. Add these to actions.yaml:
Action:
scanAction:
name: Scan
do: target_nearest
pursueAction:
name: Pursue
do: move_to_target
The matching goal conditions go in your Condition block:
Condition:
hasTarget:
left: '@has_target'
operator: '='
right: '1'
isNearTarget:
left: '@distance_to_target'
operator: '>'
right: '1'
Note: For movement to a fixed point rather than a target, the
set_destination_selffunction takes adestinationparameter — a KeyRef to aVector. The chase AI here does not use it, but it is the building block for patrols toward set points. See Add NPC AI.
Define the guard actor
The guard is a leaner actor than the hero: one resource, an enemy group, and a strategy. It reuses the hero's sprite.
guardActor.yaml:
Actor:
guardActor:
name: Guard
speed: 0.5
sprite: heroSprite
base: 8
public:
- health
perception: 15
salience: 1
resources:
- health
group: enemyGroup
strategy: guardStrategy
This example uses three NPCs that show different strategies: guardActor uses guardStrategy, merchantActor uses patrolStrategy, and patrollerActor uses followStrategy. Match those assignments — they are not interchangeable.
Place the NPCs
A Deployment puts an actor on the map at a Vector location. The map's deployments array (Step 2) references these.
deployments.yaml:
Deployment:
guardDeploy:
location: guardPos
actor: guardActor
merchantDeploy:
location: merchantPos
actor: merchantActor
patrollerDeploy:
location: patrollerPos
actor: patrollerActor
Vector:
guardPos:
x: 128
y: -48
merchantPos:
x: 160
y: 32
patrollerPos:
x: 288
y: 0
Step 6: Add a waypoint
A Waypoint is a named marker on a map, shown to the player to point at a place of interest. It uses the clean key name — never the internal alias name_.
waypoints.yaml:
Waypoint:
townCenter:
name: Town Center
location: townCenterPos
icon: /assets/icons/sun.png
map: demoMap
description: The center of the demo town.
Vector:
townCenterPos:
x: 300
y: 200
location is a Vector KeyRef and map ties the marker to a specific map. For menus and dialogs that round out the player-facing layer, see Menus, dialogs, and waypoints.
A note on dialogs
A Dialog is a titled text panel. Its text can show live actor values: {{@resource}} reads the viewed actor, {{$resource}} reads the caller. A status dialog:
Dialog:
statusDialog:
title: Status Report
text: 'Health: {{@health}} / 20
Mana: {{@mana}} / 10
Gold: {{$gold}}'
Both title and text are required.
Step 7: Validate and boot
Before a campaign loads, the engine validates every entity against its schema and resolves every KeyRef. A typo in a key or a missing required field stops the load with a clear message. See Validation for how to read the output.
Validate the folder
You can check the campaign before packaging it. Point the engine at the demo folder with --validate; it runs the full validator and exits, printing the result. (A folder is validated without an archive, so asset-path checks are skipped — those run when you validate a .zip.)
isometry --validate=demo
A passing run prints Campaign validation passed and exits 0. A failure prints a summary of errors and exits 1.
Package the campaign
Package the folder into a .zip whose single root folder is named demo/, matching the campaign name:
zip -r demo.zip demo
The packaged .zip is the unit the engine loads; it must sit beside the binary at runtime.
Boot it headless
Place demo.zip next to the binary, then boot directly into a hosted session. The --network=host mode runs the full load-and-validate pipeline; --network=none is currently a no-op and loads no campaign.
isometry --headless --campaign=demo --network=host --port=5000 --username=p --secret=p --log-level=INFO
Logs write to log.txt in the binary's directory. A clean boot shows two markers:
Campaign validation passed
Successfully created Entity
Campaign validation passed means every schema and KeyRef checked out. Successfully created Entity lines confirm the engine built your actors and map from the data. If you see a validation error instead, it names the entity and field at fault — fix it and re-run. For hosting and the network flags in depth, see Hosting.
You have now built and booted a full example campaign: an entry point, a tiled map, a playable hero, an action, an AI-driven enemy, and a waypoint — all in data.