Handbook
The user guide for NJORD Engine, an AI-first focused engine. NJORD Engine lets you build 3D games in a single desktop app. Promt and describe what you want or use the in built functions to set up a game, test it instantly and export a double-clickable game.
This is a living document and the source for the docs on the NJORD website. It describes the editor (NJORD Studio) and the games it produces. Sections marked (coming soon) are on the roadmap.
Getting started
- Open NJORD Engine.
- File ▸ New project… creates a new game from a starter scene (a floor, a
player and a goal). Set:
- Project name , the folder name (letters, digits,
-,_). - Game name , the title shown in-game and used for the exported
.exe. You can change it later by right-clicking the game at the top of the asset tree and choosing Rename.
- Project name , the folder name (letters, digits,
- To open an existing game, use File ▸ Change project.
A game is a folder of plain, human-readable files (the world, gameplay rules, visual style, and your imported models/textures). You never edit those by hand, the editor does it for you, but it means your projects are portable and version-control friendly.
The interface
The window has three areas:
- Asset tree (left) , every object, light, material and file in your game, shown as a hierarchy. Select something here to edit it.
- Viewport (center) , the live 3D view of your game, powered by the real engine. What you see is what you ship.
- Properties / Prompt (right) , the inspector for the selected object, and the AI prompt panel.
Use View ▸ Swap tree and prompt sides to flip the left/right panels. Panels can be resized and minimized; the layout is remembered.
Editing the raw files. You normally never touch the underlying files, but you can: Window ▸ Edit project files... opens a text/code editor in the viewport that lists your game's files (world, behaviors, style, dialog, materials, ...) with JSON syntax highlighting and a Markdown preview. Edit, then Save (Ctrl+S); JSON is checked before it is written so a typo cannot corrupt the level, and the editor and preview re-sync with what you saved. Close leaves; unsaved changes ask first.
A thin status bar runs along the bottom of the window. Prompts run in the background, so the prompt never locks up: send a request and keep working, or send several and they run at once (for example a new model, a level edit and a dialogue pass in parallel). Each running request shows in the status bar as a chip with a live timer and its current step; on the right, the bar shows how many generated models are waiting for approval (the review queue) as dots you can click to jump to the next one.
The menu bar groups the main actions: File (new/open/save/package), Edit (undo/redo/reset camera/settings), Assets (add lights, import, create objects), View, and Help.
Moving around the viewport
The editor camera orbits around your scene:
- Orbit , drag with the left or middle mouse button.
- Pan , drag with the right mouse button.
- Zoom , scroll wheel.
- Frame selection , select an object and press F to center the camera on it. Press F while the Assets tree is focused to also reveal it there: the tree opens the folders on its path and scrolls to the row (handy after you pick something in the viewport and want to find it in the tree).
- Reset camera , Edit ▸ Reset camera.
The viewport has a ground grid and render modes (solid and wireframe) to help you place things precisely. The transform gizmo (Move/Rotate/Scale, W/E/R) works on any selected object, including mesh-less markers and a parented child (e.g. a trigger null under an object): its handles sit on the object in the view and edits are written in the child's own (parent-relative) space. A World / Local toggle in the viewport overlay (top-right) sets whether Move/Rotate follow the world axes or the object's own orientation (Local follows its parent); Scale is always along the object's own axes.
Native viewport (fast preview, experimental)
The center viewport normally runs the engine inside the app window. For heavier scenes you can open the SAME view in a native window, which renders much faster:
- Window ▸ Open in native viewport (fast preview) opens a separate native window on the current game. Selecting, moving/rotating/scaling (with the gizmo), the grid and live material edits stay in sync both ways between it and the editor.
- Window ▸ Embed native viewport (experimental) does the same but keeps the native window pinned over the viewport area so it looks embedded.
Limitations (it is still experimental): the embedded window floats on top rather than truly clipping into the panel, so a dropdown or modal that overlaps the viewport can be hidden behind it (close the native window, or use the detached "fast preview" mode, if that gets in the way). It needs the desktop app with the native viewer built. This is an early step toward making the native view the default editor viewport.
Building a scene
Promt or add objects from the Assets menu:
- Create cube / cylinder / sphere / cone / plane , primitive shapes, handy for blocking out a level.
- Create empty , an invisible transform-only object, useful as a group/parent (see Grouping and hierarchy).
- Create player / spawn point / goal , gameplay objects (see below).
- Add point / spot / directional light and Add sun.
New objects appear in the asset tree and in the viewport. Select one to move it and edit its Properties (transform, color/material, collision, gameplay).
The tree is organized under your game: World (the scene , the camera, environment, sun, your objects and lights), Materials, Models, Textures, and the data files. Right-click an object for Rename, Duplicate and Delete.
Transforming objects (gizmos)
Select an object and use the on-screen gizmo to move, rotate or scale it. Switch tools with the keyboard:
- W , Move
- E , Rotate
- R , Scale
Drag the colored handles to transform along the X/Y/Z axes. Drag the yellow cube in the centre for the free / uniform version of the active tool: free move in the screen plane, uniform scale (whole object), or free trackball rotation. Hold Ctrl while dragging to snap (a 0.5 grid for move, 0.25 steps for scale, 15-degree steps for rotate). You can also type exact values in the Transform section of the Properties panel. Changes are saved with your project; use Ctrl+Z / Ctrl+Y to undo/redo. The Properties panel's sections are collapsible; turn on Settings ▸ Workspace ▸ Accordion sections to keep only one open at a time.
The player
Your game has one player: a first-class object you can select and edit like any other (every game has one). Its Properties include:
- Model , the mesh the player uses. Pick an imported model from the dropdown
(a
.glb/.gltf/.obj), or a primitive. An animated glTF plays automatically. - Facing , if your model runs the wrong way (some rigs face the "wrong" direction), set Facing to Backwards 180° (or 90° left/right) to turn the visual model. This only rotates the model, not the controls.
- Animations , map the model's clips to gameplay states (see below).
- Spawn at , the spawn-point marker where the player starts and respawns.
- Transform ▸ Size , scales the player model and its standing height. For the collision shape itself, use Properties ▸ Collider (see "Player collider" below) to set an explicit box that matches your model; otherwise the player uses a built-in fixed size. Place the player so it sits on the floor and it will rest in the same spot in-game.
The player faces the direction it moves (third-person: forward is where the camera looks).
Animations
When you set the player's Model to an animated glTF, NJORD drives it with a locomotion system:
- It blends idle ▸ walk ▸ run by the player's actual speed, crossfades to jump in the air, and plays die on death, all smoothly.
- It plays attack as a one-shot when you swing a melee (left mouse): the swing takes over the whole body for its duration, then returns to locomotion.
- Hold Shift to sprint (run).
Mapping clips
A model often ships several named clips. In the player's Animations section, pick which clip plays for each state (Idle / Walk / Run / Jump / Attack / Die). Each dropdown lists the clips found in the model. "Auto" matches by keyword (idle/walk/run/jump/attack/die), so well-named clips work with no setup. A model without an attack clip simply plays no swing animation; the attack still lands.
Tip: export from your model tool as a single
.glbthat contains all the clips. FBX does not load, use glTF/GLB. If a model has no idle clip, the player holds a neutral pose when standing still.
Shoot while running
The proper way to shoot while moving is a shoot clip per stance, so the legs in the clip always match the movement , no upper/lower "mask" needed. Name them with a stance word plus a shoot word and NJORD picks the right one automatically from the player's speed:
Idle_Shoot(orAim) , idle legs + firing arms , used when standing.Walk_Shoot, walking legs + firing arms , used while walking.Run_and_Shoot, running legs + firing arms , used while running.
Map any one of them as the Attack clip (or just name them by convention and Auto finds them). While you fire, the full-body clip for your current stance plays (legs match, movement speed unchanged); between shots the plain locomotion plays.
If a stance has no matching shoot clip, that stance keeps its normal
locomotion and just shows the muzzle flash (no arm animation) , NJORD will not
slam in a running-legs clip while you stand or walk. So with only Run_and_Shoot,
running shots animate fully while standing/walking shots keep their idle/walk
legs. Add Idle_Shoot + Walk_Shoot (e.g. generate them in Meshy) to get firing
arms in those stances too.
A pure attack clip (e.g. Shoot, Punch, with no locomotion word in the
name) just plays full-body as-is, standing or moving , the right choice for melee.
This replaces the earlier bone-mask / leg-freeze approaches (a Bevy animation mask collapsed the model on some rigs; both are disabled). Matching the clip to the stance is robust and needs no per-bone masking.
Tip: map the states explicitly (Idle/Walk/Run/Jump/Die to the plain locomotion clips, Attack to the shoot clip); locomotion matching skips action clips so a clip named like
Run_and_Shootis never mistaken for the Run state.
Tuning the locomotion feel ([animation])
How the player switches between idle / walk / run, and how snappy the blends
are, is data-driven in style.toml under [animation]. Every field is optional;
the defaults reproduce the engine's built-in behaviour, so a game without the
section animates exactly as before.
move_threshold(default0.6) , smoothed ground speed (world units/second) at or above which the model animates as walking; below it, idle.run_threshold(default6.75) , speed at or above which it animates as running instead of walking.crossfade_seconds(default~0.0714) , the locomotion crossfade time constant: roughly how quickly a state change blends in. Lower is snappier, higher is smoother/floatier. (It is an exponential blend, so this is a feel constant, not an exact transition duration.)speed_smoothing_seconds(default0.1) , how quickly the measured speed that drives the state choice is eased, which damps flicker when the speed sits right on a walk/run boundary.
[animation]
move_threshold = 0.4
run_threshold = 9.0
crossfade_seconds = 0.12 # softer, floatier blends
Limitations (first cut): one global crossfade time (not yet per-state), and this tuning applies to the player locomotion; enemy models use a fixed blend.
Custom animations (play any clip from a rule)
The states above are engine-driven (the engine decides when to play them). To
trigger any other clip yourself, use a play_animation behavior action in
behaviors.json:
{ "trigger": { "type": "on_interact", "target": "chest" },
"action": { "type": "play_animation", "target": "chest", "clip": "open" } }
targetis the entity id to animate. Omit it to animate the player, where the clip plays as a one-shot over locomotion (like attack).clipis matched case-insensitively against the model's clip names.- By default it plays once and returns to the idle loop (or the player's
locomotion). Add
"looping": trueto keep it playing (e.g. a spinning fan).
So known states (idle/walk/run/jump/attack/die) live in the engine, and any extra movement you invent lives in your behavior rules, no engine changes.
Importing models and textures
- Assets ▸ Import model… copies a
.glb,.gltfor.objinto your game'smodels/folder. It then appears in the Models node and in any Model dropdown. - Assets ▸ Import texture… copies an image into
textures/.
Imported assets live inside your game folder, so projects stay self-contained.
Unused models/textures are flagged "(unused)" in the tree and can be deleted
(they move to a .trash folder, so deletes are undoable).
Assets area (parking to the side). A newly imported or AI-generated model is dropped into an assets area to the right of your scene (a lane starting at about x = 24, like a 3ds Max asset shelf), not into the middle of the active scene. Generate or import several and they line up in a tidy grid there instead of piling on top of each other. Drag one into place when you want to use it.
Generating models with AI (Meshy)
The 3D Model agent (see The AI prompt and agents) generates a textured but
static mesh via Meshy, it has no skeleton or animation. The result is a
normal .glb in your models/ folder, so you can rig and animate it outside the
engine and bring the result back in:
- Open the model's
.glbfrommodels/in Blender (right-click the model in the tree ▸ open externally), or upload it to Mixamo (free auto-rig + animations for humanoid characters) or Meshy's own rigging. - Add a skeleton and animation clips, then export a single
.glb(with all the clips) back over the file inmodels/. - The editor live-reloads on save and the rigged glTF's clips play automatically (see Animations).
So "make a character that runs" is two steps today: generate the mesh, then rig it externally and save it back.
SOLO review before it enters the scene. A generated model does not silently
land in your level. It opens in SOLO mode: the rest of the scene is hidden so
you see just the new model, with a review bar at the bottom of the viewport.
Choose Approve to keep it, or Reject to remove it (the .glb file stays in
models/, so nothing is lost, it is just not placed). If one run produces several
models they queue up and are reviewed one at a time (the bar shows how many are
left). Press S to toggle solo on the reviewed model, or on any selected entity,
to inspect it alone. The Iterate button (refine an existing model) is greyed
out for now, it needs a provider that supports refine (Tripo or Hyper3D), which is
coming. Limitation (current cut): the model is technically added to the scene while
you review it (hidden), so leaving solo without choosing keeps it placed; a stricter
"not placed until approved" pass comes with the multi-provider work.
External editing: right-click a model or texture to open it in an external program (set your preferred apps in Settings). Saving in that program live-reloads the asset in the editor.
Materials and textures
- Assets ▸ Create material makes a reusable, named material. Its Properties
panel has a Type picker (see below) and a Channels section: Albedo and
Emissive are a color OR an image (toggle), Roughness/Metallic are
values, and there are image-only map slots (Metal/Rough, Normal, Depth). Each
texture field takes an existing texture or a + button that imports an image
from your disk , the file is copied into the game's
textures/folder (renamed if the name clashes) and referenced. Assign the material to objects via their Material dropdown , this replaces the object's inline color/metallic/roughness/etc. so the material fully applies (an object's inline fields otherwise win over a named material). - Material type (lighting model). A material can pick how it reacts to light:
PBR (realistic metallic/roughness, the default), Toon (cel-shaded flat
bands), Unlit (self-lit, ignores lighting , signs/screens/holograms),
Transparent (flat alpha-blend see-through; pair with Opacity), Glass
(refractive , bends light for oil/glass/water; tune Refraction weight, IOR
index and Thickness), or SSS (subsurface/translucent , skin, wax, leaves,
jade , light scatters through; tune Translucency weight + Thickness). Glass
and SSS also take a Volume scale (how far light travels before it is absorbed)
and Volume tint (the color it picks up , thick-glass green, skin flesh tone).
Absent = the level's global shading; per material, so a scene can mix them. Values in
0..1(roughness, metallic, opacity, translucency, refraction) are sliders. Note: real-time refraction (Glass) is screen-space and can look flat; it is not offline glass. Volume tint (for Glass/SSS) only shows when Volume scale is a finite number , with no scale there is no absorption, so a tint alone does nothing (setting a tint auto-applies a default scale). - Preview + look-dev (SOLO). A material's Preview material (solo) button (or S / Edit ▸ Solo with an object OR a material selected) isolates it on a clean stage , a material/model workbench. Entering SOLO frames the object size-aware (it fills ~60% of the viewport, whatever its size), and particle FX are hidden on the stage. In SOLO you can only orbit + zoom (no move/rotate/scale), and a Look-dev panel (top-left) lets you judge the surface under different lighting presets (Studio / Outdoor / Night / Neutral), rotate the light, set its intensity, and toggle a background color. The top-right overlay gains a UV toggle that shows a UV checkerboard on the surface (inspect UV layout, stretching and seams; StandardMaterial surfaces only). Preview spheres use a UV sphere (clean latitude/longitude UVs + smooth shading). (A rotatable HDRI/IBL environment is planned.)
- Texture maps (per channel). Beyond a flat value, a material can take image maps: albedo (base color), a metallic/roughness map (Bevy packs both into one texture, so one map drives both channels), an emissive map (+ an intensity), a normal map (surface detail without geometry; needs mesh tangents), and a depth map for parallax (fake displacement , carved/raised detail; tune its scale). Tiling U V repeats the textures across the surface (2 = twice as dense, 0.5 = zoomed in) , the two fields are the horizontal (U) and vertical (V) repeat, so you can stretch a texture on one axis (leave V empty for uniform). Textures use Repeat so they tile seamlessly. Note: true vertex displacement / subdivision surface is done in your 3D app (Blender) and baked into the exported mesh; the engine uses parallax to fake it. Every texture slot has a ... browse button (import from disk), an × clear button (with a confirm), and , once a texture is set , an Edit in external application → link that opens it in your image editor; saving there writes the file in place and the viewport live-reloads (a quick fade masks the rebuild).
- Plain objects can also use an inline color/roughness/metallic/emissive without a named material (the same styled sliders as a real material, for a quick basic look). For textures, maps and material types (PBR / toon / glass / SSS ...), press Create material from this in Properties ▸ Material: it turns the object's inline look into a reusable named material, assigns it, and opens it for editing.
- glTF models bring their own baked materials, but you can tint them: Properties ▸ Material ▸ Tint multiplies the model's base color (white = unchanged), so it shifts a textured model or recolors an untextured one (e.g. a generated Meshy model). Applies on reload.
Lights, sun and environment
- Add point, spot, directional lights or a sun from the Assets menu. Lights show as icons in the editor and are hidden in the game.
- Select a light to edit its color, intensity, range and (for spot) cone angle.
- The Sun and Environment nodes (under World) control the global sun and the scene environment. The Sun node sets its Azimuth (which way) + Elevation (height, 0 = horizon, 90 = overhead), color, intensity, and , when the sky is on , the visible sun disc Size + Softness (edge blur).
- Shadows are soft by default: Environment ▸ Shadows ▸ Softness picks
soft(Gaussian PCF) orhard; the shadow map resolution ([lighting] shadow_map_size, default 2048) controls sharpness. Cascades ([lighting] shadow_cascades, 1..4, default 4) split the shadow map across distance: fewer cascades render faster (a perf lever for large open scenes) at the cost of crispness far away. Ambient occlusion (SSAO) adds soft contact shadows (WebGPU only; forces MSAA off). - Cast shadows (per object) , each object has a Cast shadows toggle in
Properties ▸ Visibility (or
cast_shadows: falseinworld.json). Turn it off on dense or small clutter (props, debris, foliage) so it is skipped in the shadow maps: a performance win in busy scenes. The object still receives shadows and light, so it does not look unlit. Works on primitive/box and OBJ meshes and on imported glTF models (the setting reaches the model's meshes once it loads). Applies on reload. - Sky , Environment ▸ Sky turns on a sky dome. Pick a Source:
Gradient (a Zenith + Horizon color and an optional sun disc), Skydome
(your own painted image) or HDRI (an equirectangular
.hdr/.exrfor realism). Skydome/HDRI take an equirectangular 2:1 image you browse for; Rotation spins the sky. For the Gradient sky, place the sun with Rotation (which way) + Sun height (0 = horizon, 90 = overhead, 180 = the opposite horizon); a low sun warms the disc + horizon toward orange (a sunrise/sunset look). Sky fill is a cheap IBL: with the sky on it tints the scene's ambient from the sky colors so shadowed surfaces pick up sky light (set0to just SEE a painted sky and light the scene with normal lights instead). Best for outdoor levels; leave it off for interiors or space. Note: a 6-face skybox cube, true environment-map reflections (EnvironmentMapLight) and baked GI are planned next; the dome is not yet a reflection probe.
Sound
Sounds live in an audio.toml next to your level, mapping a sound id to a
file in your game folder:
[sounds]
collect = "sounds/coin.ogg"
hurt = "sounds/ow.ogg"
win = "sounds/fanfare.ogg"
lose = "sounds/buzzer.ogg"
death = "sounds/thud.ogg"
Drop the audio files (.ogg or .wav) in your game folder (e.g. sounds/).
Two ways a sound plays:
- Built-in moments , the ids
collect(pickup),hurt(took non-lethal damage),win,lose,death(the death sequence),shoot(fire a ranged weapon),hit(a shot hits a wall or target),jump,reload(a reload starts, on R or when the magazine empties) andempty(dry-fire click on an empty magazine) play automatically when those happen, just by being defined (each is silent until you map it). - Behavior rules , a
play_soundaction (in your behavior rules, which the AI prompt can author) plays any id you choose, on any trigger.
Background music , add a music line (an audio file path) to audio.toml and
it loops while the game runs (stopped while editing):
music = "sounds/theme.ogg"
[sounds]
collect = "sounds/coin.ogg"
Set per-sound volume with a [volumes] table (and music_volume for the
music), 1.0 = full:
music = "sounds/theme.ogg"
music_volume = 0.6
[volumes]
collect = 0.8
The player's Master volume (Options menu) scales everything on top. Limitations (first cut): one-shot sound effects + one looping music track, no music crossfade yet.
Gameplay: goals, collectibles, enemies
Spawn point , where the player starts/respawns. Set the player's Spawn at to use it.
Goal , reach it to win the level.
Collectible , toggle Collectible on any object (Properties ▸ Gameplay) and give it a Score value. The player picks it up on touch.
Checkpoint , toggle Checkpoint to move the respawn point when touched.
Enemy / AI , in Properties ▸ AI / Enemy, set Behavior to Chase (hunts the player; set Speed/Damage/Health, and Knockback = how hard a touch shoves the player off, 0 = no shove), Shooter (a ranged enemy that keeps its distance and fires projectiles at the player; Damage = HP per shot), or Patrol (moves between waypoints). AI objects are shown in red in the tree. A Shooter takes the player's shots/melee just like a chaser. Place several shooters and each automatically gets a bit of variety (a different hold distance, fire rate and move speed, and it strafes/circles the player), and they push apart instead of stacking, so a squad reads as distinct enemies, not one AI.
Attacking (melee or ranged) , the player attacks with the attack key (left mouse by default). Select the player and open Properties ▸ Combat to set it up (or edit
style.toml [combat]directly). By default it is a melee swing that damages enemies within Melee range. Turn on Ranged (shooter) to fire a projectile along the player's facing instead, damaging the first enemy it hits then despawning. Tune Damage (per hit), Cooldown (fire rate), and the projectile speed / life / size. The attack animation (if the model has one) plays for both. Set Muzzle R/U/F to move where shots spawn relative to the player (Right / Up / Forward in world units), so they leave the weapon instead of the head, e.g. lower the Up value and add a little Right for a hip-fired gun. Ranged shots converge on the crosshair (screen centre), correcting for the camera being offset from the player, so what you point at is what you hit. Hold the right mouse button to aim: the camera pulls in over the shoulder and looks along your aim line, so you can aim fully up or down with the mouse (limited only by the camera's min/max pitch); the player turns to face your aim and a centered crosshair appears. If up-aim feels capped, raise max_pitch in the camera style (radians; ~1.2 gives a steep upward look).Pickups , make any object something the player collects on touch. Select it and open Properties ▸ Pickup, then turn on a type: Weapon, Health (heals by the set amount, capped at max) or Ammo (adds to the active weapon's reserve). Each despawns when picked up. A Weapon pickup: on touch the player equips it (the pickup disappears) and the ranged attack switches to its stats, so a melee game becomes a shooter once you grab a gun, and you can place stronger weapons later in a level. Each weapon sets Name, Damage, Fire rate, Proj. speed, Range (projectile life), Proj. size and Spread (scatter in degrees: 0 = dead straight, higher = less accurate). So one weapon can shoot straighter and farther than another. Keep Spread at 0 for player weapons so they are accurate (spread is opt-in for a deliberately inaccurate gun). Set Blast radius above 0 for an explosive weapon (a rocket): the shot detonates on impact and deals its damage with falloff to everything in range, so a near miss beside an enemy still hurts it. The blast hurts you too (falloff from the centre, respecting the brief invulnerability window), so firing a rocket at your own feet is a real risk, but a point-blank shot has to travel a short distance from the muzzle before it can self-damage, so it will not instantly kill you as it spawns. Proj. size controls the visible projectile, so a rocket can read as a big glowing ball. Ammo uses a magazine + reserve model: Mag size is rounds per magazine (0 = unlimited, no reloading) and Reserve is the spare rounds you carry. Each shot spends a round from the magazine; an empty magazine auto-reloads and R reloads manually, refilling the magazine from the reserve (so 6/30 with a reserve pulls 24 rounds). The corner readout shows magazine / capacity and the reserve. The equipped weapon resets at the start of each level.
Screen shake , the camera gives a short jolt on impactful moments (an explosion, the player's death) for game feel. Automatic, no setup; it decays quickly and only affects the in-game camera (Play).
Health & lives , the player has health (set Max health on the player) shown by a built-in health bar (bottom-left, green then amber then red as it drops) , this is automatic, every game shows it, no
ui.jsonneeded. On top of health, lives are how many times you can die before game over: set them in Game settings ▸ Lives. Lives = 0 means unlimited , a health-only game (a typical FPS): the player always respawns and only aloserule (or a goal) ends the match. Any positive number is a life count that ticks down to a game over. (A game can still author its ownui.jsonHUD, e.g. lives hearts, on top of the built-in bar.) Each death also flashes a message (default "You died") , set Game settings ▸ Death msgs to your own text, or several separated by | to show a random one each time (e.g.You died | Wasted | Try again). This is separate from the final game-over Lose text. Each enemy also shows a small billboarded health bar floating over its head (green then amber then red as its HP drops), so you can read an enemy's health as you fight it. This renders in the shipped native game as well as the editor's Play (it is a runtime presentation system, not editor-only).Driving (racing) , select the player and turn on Properties ▸ Vehicle ▸ Drive a vehicle to make the player drive an arcade car instead of walking: forward/back = accelerate / brake / reverse, left/right = steer (only while moving, and inverted in reverse). It has momentum (it coasts to a stop , tune with Grip) and slides along walls. It also drifts: hold Space for the handbrake to break the back loose. Tune the slide with Side grip (sideways hold in corners , high = kart-like and planted, low = loose and drifty) and Handbrake grip (the side grip while Space is held , low = big slides). A hard drift automatically kicks up grey tyre smoke and lays dark skid marks on the road at the rear wheels (the marks hold, then fade after a few seconds); AI racers do the same. Toggle each off with Drift smoke and Skid marks in the Vehicle properties (both on by default). A drift also builds a drift score shown top-right while driving: the number climbs during a slide (the live combo shows as
+N) and banks into the total shortly after you straighten out, so linking slides keeps a combo going. It resets each time you press Play. Set Max speed / Accel / Brake / Reverse / Turn rate to taste, and give the player a car model. Driving uses a chase camera that stays behind the car and follows where it points, so forward is up the screen. Press C to cycle the camera views (see below); this works on foot too. If the car looks like it drives sideways, its model's nose is not aligned with the drive direction: set the player'smodel_yaw(try 90 / 180 / 270).Game camera views (cycle with C) , the in-game camera is a LIST of views the player cycles with the C key. Select the Game camera (in the tree under your game) and open Properties ▸ Views: Add view for each camera, give it a Mode (Follow = third-person behind the player, or First-person = at the head/cockpit). A Follow view has Distance, Height and Side (a lateral over-the-shoulder offset), plus Look at (the point it aims at, as right / up / forward from the player , raise Forward to look down the track); a First-person view has Eye height, Forward and Side (place it in a cockpit or out of the model). Both have a Look angle (tilt the view further, + down / - up) and a FOV. Look through puts the editor view at the camera to preview what the player sees (with the game's aspect-ratio letterbox); move the mouse to jump back out. The selected camera is highlighted in the viewport. In Play, C steps through the cameras in order. With no views defined the engine uses a built-in third-person + first-person pair (the classic C toggle). Saved to
style.toml [[camera.views]]; applies on reload.Time limit (time-attack) , set
[rules] time_limit(seconds) instyle.toml(or ask the AI for "a 60-second time limit") to make a beat-the-clock level: a corner M:SS countdown ticks down and the game is lost at zero.0(the default) = no limit.Race track (generated) , the easiest way to get a clean track is to ask the AI for one (e.g. "make an oval race track, 3 laps"): it emits a single track object and the engine generates a correct looping road plus evenly-spaced, oriented gates (a start/finish + the ordered checkpoints), so nothing comes out skewed or overlapping. The track's
sizeis the loop's [x, z] extent,widththe road width, andgatesthe number of gates besides the start/finish. Set Vehicle ▸ Laps (or let the prompt do it) to finish after N laps.Race track (manual gates) , or build a course by hand and mark gate objects as Race gates to make a lap.
Racing opponents , add cars and set their AI ▸ Racer (or ask the AI for "3 opponents") to make them drive around the track's gates. They follow the same loop as you; face them toward the first gate with the object's rotation. Each opponent shows its name on the finish leaderboard , that is just its object name, so rename it in the tree (for example "Rival" or "Blue Team") to give it a proper racer name (unnamed ones fall back to "CPU 1", "CPU 2", ...). Select an object, open Properties ▸ Gameplay ▸ Race gate (track) and give it a Gate order: 0 is the start/finish line (place it where the player starts, facing gate 1), then 1, 2, 3, ... around the loop in driving order. The player must pass the gates in order (so they cannot cut the track) and re-reach the start/finish to complete a lap; passing a gate also moves the respawn point there, so falling off puts you back at the last gate. Because the order is enforced, driving backward never counts (you would reach gates out of turn), so there is no separate "wrong way" rule to configure. Set Properties ▸ Vehicle ▸ Laps to the number of laps that finishes (wins) the race (0 = endless, laps are just counted). The engine shows a Lap X / N readout in the corner, with each lap's time under it (
L1 31.2, the fastest lap marked*) plus the running current lap. You can also place these race stats anywhere on a custom HUD (ui.json): a text element'sbindacceptslap,position(your standing among the AI racers, 1 = leading) andtimer(the elapsed race time), alongside the existinghealth/score/lives/speed. Alapbar doubles as a race-progress bar. A gate is a single object (no child posts to build): pick its Shape in Properties ▸ Gameplay , Line (a wide thin gate across the road), Box (an oriented zone), Ring (a flat circle on the ground) or Sphere (a 3D ball you fly through, for a flight game). The engine draws the shape in both the editor and while playing, so you can see and place the gates. Its Scale sets the size (default is a ~10-wide gate). Tip: lay a looping road of floor pieces first, then drop the gates around it.Moving platform , a solid object that moves along a route and carries the player standing on it (lifts, moving ground). Make one by giving a solid object waypoints but no enemy behavior (with a Chase/Patrol behavior the same waypoints make a moving enemy instead). Author the route in Properties ▸ Waypoints (add/remove points, type each XYZ), or ask the AI ("a platform that moves up and down"). With the object selected, the route is drawn in the viewport and you can drag the waypoint spheres on the ground to reposition them (the first point is the object's own position, moved with the normal gizmo). The same applies to a Patrol enemy's route.
Hazard (spikes/lava) , an object that hurts the player while they stand in or touch it. Turn on Properties ▸ Gameplay ▸ Hazard and set the Damage; like enemy contact, an invulnerability window stops instant death. A hit also knocks the player back off the hazard , tune the shove with Knockback (0 = damage only). Use a flat floor tile for a lava pool (make it non-solid to wade in) or a cube for spikes. Give a hazard waypoints to make it a moving hazard (sweeping spikes/saw). Give it a Spin (see below) to make a rotating saw/blade (combine both for a saw that sweeps AND spins). Author it in the UI or via the AI prompt ("a pool of lava that damages the player", "a spinning saw blade that patrols the corridor"). In the editor the damage area shows as a red box (matching exactly where a hit lands), so you can see and size it; a moving hazard's red box follows it.
Spin (rotating objects) , any object can spin continuously in Play. Select it and set Properties ▸ Transform ▸ Spin °/s (degrees per second about X, Y and Z): a saw blade spins about X or Z, a turntable or fan about Y. The spin is purely visual, the collision/damage box stays put, so size the object to cover its spinning reach. Absent or all-zero = no spin.
One-way platform , a platform the player can jump up through from below and land on top of when falling, and that never blocks sideways movement. Mark a thin object (
one_way: true, e.g. a flat floor tile). Great for layered, ascending platforming. Author via the AI prompt orworld.json.Custom collider , by default an object's collision box is derived from its mesh, so a model (a tree, a rock) gets a coarse cube. Turn on Properties ▸ Collider ▸ Custom collider to set a tight box (half-extents + offset, scaling with the object); it shows as an orange box in the viewport. The box overrides the mesh collider for solid/ground collision, and it collides even if Solid is off (the collider IS the collision shape). Keep the offset at 0 to sit on the object; a Y offset floats the box. Shape options (sphere/capsule) and a low-poly collision mesh are planned.
Enemy hit shape , a skinned enemy (an animated humanoid model) is hit against per-bone hitboxes built automatically from its skeleton: a shot is tested against the live bone positions, so a hit lands where you actually aimed on the moving body. Each region is weighted: a headshot deals 2x, a torso hit 1x, and an arm/leg graze 0.5x. This is the same idea as an Unreal Physics Asset / Unity per-bone colliders. A model without a skeleton (a static mesh) falls back to a single body CAPSULE (feet to head) so grazing a tall character still registers; you can CONTROL that capsule with a Custom collider (above) , its X/Z half is the hit RADIUS, its Y half the HEIGHT, its offset the placement (the orange box shows it). Press F4 while playing to SEE the per-bone hitboxes as colored spheres on each enemy , red = head (2x), amber = torso (1x), blue = arm/leg (0.5x) , so you can check where shots register.
Tuning the per-bone hitboxes (sidecar file) , the auto boxes are a good default, but you can OVERRIDE them per model with a sidecar file next to the
.glb:models/<name>.hitboxes.json. It applies to every enemy using that model (author once, reuse everywhere , like an Unreal Physics Asset). List the boxes you want; each binds to a bone and sets its region / damage / size:{ "boxes": [ { "bone": "Head", "region": "head", "damage_mult": 2.5, "radius": 0.18 }, { "bone": "Spine02", "region": "torso", "damage_mult": 1.0 }, { "bone": "LeftLeg", "region": "limb", "damage_mult": 0.4 } ] }Only
boneis required;region(head/torso/limb),damage_mult,radius(world units) andoffset([x,y,z]in the bone's frame) fall back to the region default when omitted. A model with a sidecar uses only the boxes you list (so you fully control the shape); a model without one keeps the auto boxes. The file is optional and hot-swappable , edit it and reload. Use F4 in Play to see the result. (Editing the boxes visually in the editor is a later step.)Shootable buttons / switches / explosives , make any object react to being SHOT with an
on_shotbehavior rule (inbehaviors.json, which the AI prompt can author for you):{ "trigger": { "type": "on_shot", "target": "gate_button" }, "action": { "type": "despawn", "target": "gate" } }, shoot the button to remove the gate. It composes with any action (win,set_state,play_sound,start_dialog, ...), so a shootable target can win the level, open a door, arm a trap, and so on. Give the target a Custom collider so the shot has a box to hit. Fires once.Explosive barrels / area damage , the
explodebehavior action deals damage in a radius instead of to one target. Everything withinradiusof the blast centre takes flatdamage(no falloff) and dies exactly like a shot; an expanding fireball + a ring of impact bursts + anexplosionsound play at the centre. The blast respects cover: a target behind , or under , a wall takes no damage even inside the radius (line-of-sight from the blast centre), so a rocket in a corner hurts the exposed side but not what the corner hides.centeris an entity id whose live position is used (omit it to blast at the player), andhurt_player(defaulttrue) can be setfalsefor a blast that only clears enemies. Combine it withon_shoton the same barrel id for a classic shootable explosive barrel:{ "trigger": { "type": "on_shot", "target": "barrel_a" }, "action": { "type": "explode", "center": "barrel_a", "radius": 4, "damage": 100 } }, shoot the barrel, the blast takes out the crowd around it. Add a second rule thatdespawns the barrel if it should disappear. Map anexplosionsound inaudio.tomlfor the bang.Breakable / destructible props , give any object a
breakablenumber (its hit points) to make it a crate, barrel or wall that the player can shoot to pieces: each shot deals its damage, and when the hit points reach zero the object shatters , it despawns with a burst of debris sparks and abreaksound (map one inaudio.toml). Give it a Collider to size the box shots test against. Unlike a one-shoton_shotbutton, a breakable takes as many hits as its hit points allow. If the same object ALSO has anon_shotbehavior, that behavior fires when it breaks (not on every hit), so abreakableexplosive barrel needs a few shots before itexplodes. Set it in Properties or inworld.json("breakable": 20).Triggered traps (falling rock, dropping gate) , the
movebehavior action slides an entity to a new position when triggered, for classic traps: a rock that falls when the player passes, a portcullis that drops, a sliding wall, a crusher. It is distinct from a hazard (a still damage volume, spikes/lava) and from waypoints (a continuous patrol):moveis a ONE-SHOT motion fired by a trigger.targetis the entity to move;tois[x, y, z], absolute unlessrelativeis true (then an offset from where it is now, so "drop 8 down" isto: [0, -8, 0], relative: true);durationis the seconds it takes (default 0.5, a quick drop); andimpact_damagehurts the player once if the moving entity reaches or overlaps them (a falling rock crushes for that much). Omitimpact_damage(or set 0) for a harmless move like a door sliding open. Example, a boulder that drops on the player when they reach a spot:{ "trigger": { "type": "on_reach", "target": "trip", "radius": 2 }, "action": { "type": "move", "target": "boulder", "to": [0, -8, 0], "relative": true, "duration": 0.4, "impact_damage": 40 } }. The AI prompt can author this from "a rock that falls and hurts you when you walk under it". LIMITATION: the move is a straight interpolation (no physics/bounce), andimpactdamage is a proximity hit, not a per-frame crush.The easy way , the Trap toggle. Rather than prompt the (fiddly) behaviour, select the object, place it up high, and in Properties ▸ Gameplay turn on "Trap: falls on player" and set the Trap damage. The studio wires the correct behaviour for you AND drops a trigger null (
<id>_trigger) on the ground directly under the object: an empty marker (editor-only, hidden in Play) parented under the object so it belongs to it and stays with it. It is drawn as a wireframe zone (visible edges only, no solid mesh) sized by its scale, and you can move, rotate and scale it with the gizmo to reshape/resize where the trap fires. The object falls when the player walks into that zone, so it triggers where you expect, not the moment Play starts. Move the object higher for a bigger drop. Toggle the trap off to remove both the behaviour and the null.Zone shape. A goal and a trigger null are ZONES with a selectable shape in Properties ▸ Zone shape: Plane (a flat pad on the ground, height ignored), Circle (a ground ring, height ignored), Cube (a box, the default), or Sphere (a 3D ball). Each is drawn as an always-on wireframe in that shape and sized by its scale (radius for circle/sphere; width/depth/height for plane/cube), from the Zone-shape panel or the scale gizmo. The
on_reachfires over exactly that volume, so pickplanefor a doormat/pressure plate,circlefor a ground ring,spherefor a zone you can enter from any height. (Race gates keep their ownShapeunder Race gate: line/box/ring/sphere.) A Position: World / Parent toggle in the same panel picks whether the zone is fixed in the world or moves with its object (a trap plate under its boulder stays attached; the world position is kept when you switch). Parented triggers compose their parent's full transform (position, rotation AND scale), so anon_reachon a child null under a big scaled object fires exactly where the zone is drawn.Particle effects (fire, smoke, sparks, dust) , attach a continuous particle effect to any object in Properties ▸ Effects ▸ Particles: Fire (a torch/campfire flame), Smoke (a drifting plume), Sparks (a broken wire / grinding metal), or Dust. It emits while the game runs, sized and animated by the engine. Purely cosmetic (no gameplay effect). Tweak it in the same panel: Color (tint), and Size / Rate / Speed / Spread / Lifetime multipliers (1 = the preset default, so 2 = twice as big / fast / many / long), plus Start: Always on, or On land , the effect holds off until the object's trap fall (
move) hits the ground, so a dropped rock kicks up dust/smoke the moment it lands. For a one-shot burst tied to an event, the AI can add an fx behaviour ({ "type": "fx", "effect": "sparks"|"fire"|"smoke"|"dust"|"explosion", "at": "<id>" }), e.g. sparks when a switch is shot or an explosion puff on reach, at an entity or the player. (For area DAMAGE, useexplode, notfx;fxonly draws.) To turn a continuous emitter on or off from a trigger, usefx_start/fx_stop({ "type": "fx_start", "target": "<id>" }): a fountain that switches on when a lever is shot, a fire that ignites when the player reaches it, or smoke that stops on a timer. The target needs its own Particles effect. Custom sprite (billboards): pick a Material (one of your named materials, with a soft PNG + alpha) and the particles render as camera-facing billboards , flat sprites that always face you (the standard fire/smoke/snow look) , using the material's texture + color, tinted by Color and rotated by Spin. Emit from a Point or a Plane area (sized by the object's scale, for rain/snow/fog over a region), and set a Direction to throw particles that way (e.g.0, -1, 0for snow FALLING, shown as an arrow in the viewport). Size random varies each particle's size (fake variety from one sprite), Gravity pulls down (+) / up (-). Example SNOW: a wide empty up high,Dust+ Plane + Direction0,-1,0+ a snowflake Material + Size random. With no Material, the built-in glow.Player collider , the player now has the same Collider section. By default the player uses a built-in size (a fixed radius + standing height), shown as a green cage when selected; that size does NOT scale with the model, so on a scaled or imported character the green cage will not match the art. Turn on Custom collider to set an explicit box you size and place (orange, scales + offsets with the player): its X/Z half becomes the wall/ground collision radius and its Y half + offset sets what the player is blocked by, so the box and the model finally line up. The character still stands on the ground at its normal height (the box is the wall-collision shape). Applies on reload.
Forgiving jump , the platformer jump has built-in feel helpers (no setup needed): coyote time (you can still jump for a split second after walking off an edge), jump buffering (a jump pressed just before landing fires the instant you touch down), and variable height (tap for a short hop, hold for a full one). Tune via
jump_height/gravityinstyle.toml [movement].
More complex logic (triggers, conditions, actions, dialog) lives in the game's behavior rules, which the AI prompt can author for you.
NPCs, dialogue and quests
Characters can talk, and conversations can hand out quests. This lives in a
dialog.json file alongside your level. Author it three ways: the structured
editor in Edit > Dialogue & quests... (Dialogs / NPCs / Quests tabs), by
asking the AI prompt (see The AI prompt and agents), or by editing the file by
hand. All three validate against your level on save.
- Speech bubbles (barks). Bind a world entity to short lines that show in a
bubble over its head. With
ambienton, the character babbles to itself all the time; otherwise the bubble only appears when the player comes near, to draw them over. - Talking. Give an NPC a
dialogand walk up to it: a Press E to talk prompt appears, and E opens the conversation. Pick replies with the number keys. - Quests. A dialog choice can start or complete a quest, or set a flag that your behaviors react to. Open the quest log in-game with J; it lists active and completed quests.
The pieces (in dialog.json): dialogs (the conversation trees), npcs (bind
an entity by id to a dialog and/or a bark), and quests (id + title). A
choice may carry set_state, start_quest or complete_quest.
Grouping and hierarchy
Make one object follow another so they move together: in Properties ▸ Hierarchy, set Follows to a parent. The child nests under the parent in the tree. A common pattern is to group props under an empty and have the empty follow, e.g., the player or a moving platform.
Multi-part models are grouped for you. When you prompt the level designer to build something made of several pieces (a car, an animal, a machine), it creates an empty (null) parent and puts every part under it, with the parts positioned relative to the null. So the whole model is one group: select the empty and move/rotate it and every part follows. The parts stay non-solid (visual composition); the null is the handle you grab.
Testing your game (Play)
Press Play to run the game right inside the editor. While playing, Pause freezes it in place (the sim stops, nothing resets , inspect or tweak, then Play to resume), and Stop returns to editing and resets the level (the player and enemies go back to their spawns). Pressing Esc pauses (your play state is kept); use Stop to reset. In-game controls:
| Action | Key |
|---|---|
| Move | W A S D |
| Sprint | Shift |
| Jump | Space |
| Look | Mouse |
| Attack | Left mouse button |
| Interact | E |
| Release cursor | Esc |
The AI prompt and agents
The right-hand Prompt panel is how you build with words. Describe what you want and the engine routes it to the right specialist agent, you do not pick by hand (the Auto mode). There are manual Level and 3D Model modes too, for when you want to force one.
If a request is genuinely ambiguous, the orchestrator (in Auto mode) may ask you one short question instead of guessing (e.g. which object you mean, or what style or size). Just answer in your next message, it remembers what you originally asked and continues with the full picture. It replies in your own language.
The agents:
- Level designer , edits the level (geometry, enemies, collectibles, lights,
triggers, win/lose). Additive: it keeps what is already there. It can also turn
on the game mode a request implies (e.g. "make a race track, 3 laps" turns
on driving + laps; "a shooting arena" turns on ranged combat). For safety it may
only change gameplay fields (vehicle, combat, player health/lives/respawn), never
the rest of
style.toml, so you can still tweak look/feel yourself afterwards. - Models , generates a single 3D model from a description (via a 3D provider such as Meshy) and drops it into the scene. See Importing models and textures.
- Dialogue & quests , writes conversations, NPC barks and quests
(
dialog.json). See NPCs, dialogue and quests. - Extension , when you ask for something the engine cannot do yet, it does
not fail or guess; it drafts a reviewable spec for the game's
extended/folder.
Approval before it enters your game. Generated content does not apply silently. A new model opens in SOLO review (Approve / Reject, see Importing models and textures). Generated text , a story/dialogue update or a feature spec , opens in a review panel in the viewport: you read and can edit it (with JSON syntax highlighting, or a Markdown preview), then Approve to write and apply it, or Reject to discard it (nothing changes until you approve). Pending reviews show as dots on the right of the status bar. Quick property edits (a colour, size or material value) are the exception, they apply live and are not gated, so tweaking stays instant.
Approval BEFORE a paid model is generated (save your credits). 3D model generation costs money at your provider, so you can gate the OUTGOING request too. Turn on Confirm before generating in Settings: before NJORD sends a model prompt to Meshy/Tripo/etc., it shows you the exact prompt it is about to send, and you can edit it, confirm to spend the generation, or cancel for free. This is the opposite end from SOLO review (which gates the result AFTER it arrives), so between the two you never spend credits blindly and never get a model dropped in without your say-so.
Stopping a running request. Each in-flight AI request shows a small filled square (a Stop button) in the corner of its entry in the prompt panel. Click it to cancel just that request (handy if a local model is grinding or you changed your mind); other queued/running requests keep going. A cancelled request is dropped cleanly, nothing is applied.
Getting good results (how you phrase it matters most):
- For one detailed object, say "high-poly" or "detailed" (and a style, e.g. "realistic", "low-poly"), so it is generated as a rich mesh by your 3D provider. A plain request can come back blocky.
- A whole layout or area (a parking lot, a street, a room) is built by the level designer from placed geometry, describe the layout; it is not one generated mesh.
- For a staircase or ramp you can walk up, ask for one and say it should be walkable. A generated staircase model is a single mesh whose plain collider is a solid box the full height (a wall), so the engine makes it climbable with a ramp: a sloped, invisible walkable surface over the model that you glide up. You get the nice-looking stairs and can still walk them. (Tiny kerbs under ~0.35 units are auto-stepped and need no ramp.) You can tune it yourself in Properties: the Walkable ramp section toggles the slope and picks which way it climbs (a 4-way +X / −X / +Z / −Z), and the Collider section sizes the box (shown orange in the viewport) to match the model, shrink it if the box is bigger than the stairs. A green arrow in the viewport shows which way the ramp leads. The ramp is solid from the back and sides (you can only go up the slope, not through it). The climb direction is along the world X/Z axes, so keep a walkable staircase axis-aligned (unrotated) for now.
Each agent has a built-in instruction set that ships with the engine. You do not edit
those directly; instead Edit ▸ AI agents lets you add your OWN instructions on top,
a per-agent customization that is appended to the built-in prompt, so it extends and
overrides the defaults where they conflict (e.g. "always use low-poly", "make enemies
faster", "prefer a night mood"). Leave an agent blank to use the defaults. Your
customizations are saved with the project (in agent-addons.json) and take effect on Save,
so a project can carry its own agent tuning, and you can share it without giving away the
engine's built-in prompts. You need an Anthropic API key (or a local provider) in
Settings for the agents, and a 3D-provider key for model generation.
Local and offline AI (Local Agent)
You are not tied to a cloud provider. Settings ▸ AI providers is a master-detail panel: pick a capability on the left (Orchestrator, Code & logic, Story & game design, Mesh, Rigging & animation, and reserved Audio / Image slots), configure it on the right. Each text capability (orchestrator, code, story) has its own Provider picker:
- Anthropic (cloud) , the default; pick a model and use your Anthropic key.
- Local / OpenAI-compatible , point NJORD at your own endpoint: private,
offline, no API cost. This works with any server that speaks the OpenAI
/v1/chat/completionsAPI, including Ollama (http://localhost:11434) and LM Studio (http://localhost:1234), as well as OpenAI itself. Enter the Endpoint URL and type the Model name your server exposes (e.g.llama3.1,qwen2.5-coder). A key is only needed if your endpoint requires one; local servers usually ignore it.
Setting up Ollama or LM Studio (step by step).
- Install and start the server: Ollama listens on
http://localhost:11434, LM Studio onhttp://localhost:1234(start its local server from LM Studio's Developer tab). Pull/download at least one model first (e.g.ollama pull llama3.1). - In Settings ▸ AI providers, pick a capability (e.g. Orchestrator), set Provider to Local / OpenAI-compatible, and put that URL in Endpoint URL.
- Click Detect models (also labelled Show available models): NJORD queries your server and lists what it finds. Loaded models (already in memory, ready to answer fast) are shown with their memory size and are clickable to select; models that are installed but not loaded are listed below, greyed out. Click a loaded model to fill the Model field, or just type the name yourself.
- If you have an NVIDIA GPU, NJORD also shows a VRAM readout (used / total) so you can see how much headroom a model leaves. Bigger models are smarter but use more VRAM and are slower; a model that does not fit spills to system RAM and crawls.
A small local model may produce less reliable world/behavior JSON than a strong cloud model; the detect list + VRAM readout help you pick the largest model that still fits your card.
Each capability is configured independently, so you can, for example, run the orchestrator on a small fast local model while the code agent uses a strong cloud model. The provider and endpoint are remembered between sessions. Note that a small local model may produce less reliable world/behavior JSON than a strong cloud model, so if a generation comes back malformed, try a larger local model or a cloud provider. The text agents (orchestrator, code, story) use these settings; 3D model generation uses its own Mesh/Rigging provider keys.
The game front-end: splash, menu, pause
A packaged NJORD game launches with a proper front-end (this appears in the exported game, not in the editor's Play):
- Splash screens , the built-in NJORD splash, then any custom splash images you add, each shown for a few seconds (skip with Enter/Space/Esc).
- Main menu , New game (Enter), Load game (L), Options (O), or Quit (Q/Esc).
- Pause , press Esc in-game to pause; Esc/Enter resumes, S saves, O opens Options, Q quits.
Options (volume, resolution, controls)
Press O from the main menu or the pause menu to open Options. Use Up/Down to pick a row, Left/Right to change a value, Enter to rebind a control (then press the new key), and Esc to go back. You can set:
- Master volume , scales all game sound.
- Resolution , cycles through common window sizes.
- FPS counter , toggle a small frames-per-second + frame-time readout in the top-right (off by default). The frame time makes stutter visible: a steady ms with occasional spikes is hitching, not a low average.
- Controls , rebind forward / back / left / right / jump / sprint / interact to any key.
Settings are saved to settings.json next to the game's exe and re-applied next
launch. (First cut: keyboard rebinds only, no mouse-button or gamepad rebind or
fullscreen toggle yet. Like save/load, Options is in the packaged game, not the
editor's Play.)
Save and load
In the packaged game, press S in the pause menu to save, and L on the
main menu to load. The save (savegame.json, next to the game's exe) captures
the player's progression: position, score, lives, health, state flags, quests and
which collectibles you have already picked up (so they stay collected on load).
Limitations (first cut): it assumes the current level (multi-level "which level"
is not saved yet). Save/load is the packaged game only, not the editor's Play.
Custom splash screens
Add intro images (DOOM/Quake style) in your game's style.toml:
[shell]
njord_splash = true # show the NJORD splash first (default)
splash_images = ["splash/intro.png"] # your images, shown in order
splash_seconds = 2.0 # seconds per splash
Put the images in your game folder (e.g. splash/intro.png).
Window and resolution
Control the packaged game's window in style.toml:
[window]
width = 1280 # resolution
height = 720
vsync = true # cap to the monitor refresh (smooth, low input lag)
fullscreen = false # borderless fullscreen
Keep vsync = true unless you have a reason not to, rendering uncapped feels
laggy even at very high frame rates. In the editor, pressing Play shows a
frame at this aspect ratio (dimming the rest) so you see exactly what the
packaged game will show.
Frame-rate independent physics. The gameplay simulation (movement, gravity,
jumping, enemies, platforms) runs at a fixed 60 Hz internally, separate from how
fast the screen draws. That means jump height and movement feel exactly the same
on a 60 Hz laptop and a 144 Hz monitor, and the game behaves reproducibly (the
groundwork for future multiplayer). Rendering is smoothly interpolated between
simulation steps, so motion stays fluid at any refresh rate above 60 fps. There
is nothing to configure; the fixed tick rate is not yet exposed in style.toml.
LAN co-op (experimental)
Two people on the same local network can play a level together, each controlling their own avatar and seeing the other move and jump in real time. This is an early, movement-only slice built on rollback netcode (the same technique fighting games use), so it feels responsive even with a little network latency. It is native-only and opt-in: the normal single-player game and the editor are completely unaffected.
What you need
- A level with two
role: "player"entities (the built-ingames/coop-001is set up for this: two heroes on a flat arena). - The level marked as co-op: in the editor's Game settings ▸ Mode, pick
Co-op (2 players, LAN) (this writes
style.toml[mode] co_op = true). - Both machines on the same LAN, running the same build of the game on the same OS/architecture (the simulation must match bit-for-bit).
Marking a level co-op, and testing it solo. A co-op level is explicit: set
Game settings ▸ Mode to Co-op (2 players, LAN), which saves [mode] co_op = true to the level's style.toml. A co-op level must have exactly two
role: "player" objects (validation warns if the count or the flag disagree).
When you test a co-op level solo in the editor (press Play, no network
session), you control only the first player (the follow camera frames it too),
and that player has the full single-player feature set , shooting, melee,
pickups, health, death and respawn, behaviors, explosions, race and the HUD all act
on it, exactly as in a one-player level. The other players are not spawned in a
solo run , they are just spawn SLOTS for remote peers, so single-player has one real
player and no idle dummy bodies standing around (which would clutter the level and
steal the enemies' aim). They come to life only when a real peer joins over LAN.
In real LAN play each machine controls its own avatar (on-foot movement only, see
Limits below). A normal single-player level (one player, co_op = false) is
completely unchanged (byte-for-byte).
Build the co-op-capable binary. Co-op is behind a cargo feature, so build the engine with it enabled:
cargo build -p njord-engine --features net --release
Start menu (normal play). Launch the game with just the level, on each machine:
njord-engine games/coop-001
You get an in-game start menu with three choices (mouse or keyboard):
- Single Player (
1/ Enter) , play the level solo, exactly as a non-net launch (no session is created). - Host (LAN) (
2/H) , host a LAN game and wait for a joiner. - Join (LAN) (
3/J) , open the join browser: hosts found on the LAN are listed automatically; use Up/Down to select one and Enter to connect. If broadcast discovery is blocked on your network, type the host's IP directly in the Type IP row (digits and.) and press Enter. Esc goes back.
The start menu also shows the level's authored mode (Single-player, Co-op, Deathmatch, ...), since the level decides the mode, not the menu. Host one machine, Join from the other, and once both are in, both avatars come alive.
CLI-flag shortcut (headless / dev). The old --host / --join <ip> flags
still work and now skip the menu, booting straight into that mode , handy for
scripted or headless testing. On the first machine (the host):
njord-engine games/coop-001 --host
On the second machine (the joiner), pass the host's LAN IP address:
njord-engine games/coop-001 --join 192.168.1.20
Both must point at the same level folder. Optional flags: --port <n> to
change the UDP game port (default 47001; both machines must use the same one) and
--input-delay <n> to trade a couple of frames of delay for fewer corrections
(default 2). Find the host's IP with ipconfig (Windows) on the host machine.
While the peers connect you see a "Waiting for player..." message; once both
are in, both avatars come alive. If the peer cannot be reached within ~25 seconds
you get a "could not reach peer" message, and if the other player drops mid-game
you get "peer disconnected" , in both cases press Esc to quit.
Windows Firewall. The first time you host or join, Windows will pop up a firewall prompt to allow the game to use the network , allow it (private networks) or the two machines cannot exchange data. Co-op uses UDP on the game port (default 47001) plus a small discovery port (47000).
Limits (this slice).
- LAN only, exactly 2 players. No internet play, no matchmaking yet.
- Both peers need the same build and the same OS/architecture.
- On-foot movement co-op only. Under a live LAN session the gameplay systems
(combat, pickups, death, behaviors, race) act on no networked avatar, so co-op
play is on-foot MOVEMENT only; use a plain movement level (like
coop-001) for a LAN session. A SOLO test of a co-op combat level (likecoop-002) does give player 1 the full feature set , that is the intended way to build/preview one. - No in-game pause during a co-op session.
- The host must be reachable by the joiner (same subnet, firewall allowed).
- Both machines should sustain ~60 fps. The simulation runs at a fixed 60 Hz on both peers (rollback netcode). A machine that cannot hold ~60 fps falls behind and makes the whole session stutter for everyone, so a very weak GPU is a poor peer.
Troubleshooting. If the game window opens and immediately closes on a machine, look
for a njord-crash.log file next to the exe , it records the real error (often a GPU
driver / wgpu backend issue on older hardware). As a quick workaround you can force a
backend before launching, e.g. PowerShell $env:WGPU_BACKEND="dx12" (or gl / vulkan)
then run the exe.
This is a foundation, not the finished multiplayer feature; expect it to grow (more players, internet play via Steam, synced combat) in later versions.
Versus: Team Deathmatch, Elimination & Capture the Flag (experimental)
The same LAN rollback foundation also drives versus (PvP) play, where the two
avatars fight instead of cooperating. Team Deathmatch is the first versus mode: a
timed frag match between teams, with a live scoreboard and a deterministic team winner
(both machines agree, bit-for-bit, on who won and when). Like co-op it is native-only,
opt-in (--features net) and leaves single-player + the editor untouched.
Authoring a versus level. In the editor's Game settings ▸ Mode, pick Team Deathmatch (PvP). That reveals the versus tuning:
- Teams , how many sides (default 2, e.g. red vs blue). Avatars are split across
teams by join order (
handle % teams). - Match (s) , the match clock in seconds (0 = no clock, ends only on the score limit).
- Score limit , frags that end the match early when a team reaches it (0 = none).
- Rounds , for round-based modes (Elimination); unused by Deathmatch.
Place at least two role: "player" objects on opposing spawns, split evenly
across the teams (validation warns if the count is below two or does not divide into
the teams). Turn on [combat] ranged so avatars shoot. The built-in
games/versus-001 is a ready 1v1 Deathmatch arena.
Playing. Build and host/join exactly as for co-op (--features net,
--host / --join <ip>), pointing both machines at the versus level. Each machine
drives its own avatar and can shoot the other. PvP bullets have no per-hit
invulnerability , a burst drops an opponent fast; only a respawn grants a brief
window of immunity. A killed avatar respawns at its own spawn after a short beat.
Hold Tab for the scoreboard (each avatar's team, kills and deaths, with team
totals); a small readout shows the clock and your own K/D. A billboarded health bar
floats over every OTHER player's head (green then amber then red as their health drops,
tinted by team), so you can read an ally's or opponent's state at a glance; your own
avatar has none (you have the corner HUD). When the clock runs out (or a team hits the
score limit) the match ends and shows VICTORY / DEFEAT / DRAW for your team.
Testing PvP alone in the editor. You do not need two machines to check that a versus
level's combat works. In the editor's Play, a versus level's friendly fire is live:
your shots damage the OTHER role: "player" avatar (it stands idle as a placeholder for
the remote peer), so you can walk up, shoot it, watch its health bar drop and drop it at
zero. This is the single-player preview of PvP , there is no editor respawn or
scoreboard for the idle avatar (those belong to the hosted net match); it just confirms
that a shot connects and deals damage before you host. In Single and Co-op levels
friendly fire stays off (co-op is cooperative), so a player shot never hurts another
player there.
Elimination (round-based). Pick Elimination (PvP) in Game settings ▸ Mode to
run a best-of-rounds last-team-standing match instead of a timed frag race. There
is no respawn inside a round: when you die you stay out until the round ends. The
moment one team is completely wiped, the surviving team wins the round; if both teams
fall on the same tick the round is a draw (nobody scores it). After a short
intermission everyone respawns for the next round. The first team to the majority
(rounds / 2 + 1, e.g. 2 of 3) takes the match, and the scoreboard shows the round
number and each team's round wins (Tab), ending on VICTORY / DEFEAT / DRAW.
Set Rounds in the versus tuning (0 or 1 = a single round). games/elim-001 is a ready
1v1 best-of-3 Elimination arena.
Capture the Flag (grab, carry, capture). Pick Capture the Flag (PvP) in Game
settings ▸ Mode to run a flag match. Each team needs a flag base: place an object
with marker = "flag" and set its team (0, 1, ...) behind each team's spawn (colour
it to match the team). Then: grab the enemy flag (touch it), carry it back to your
own base (a floating coloured sphere shows where each flag is), and , while your OWN flag
is still home , capture it there to score. If you are killed while carrying, the
flag drops where you fell; any teammate can touch a dropped flag to return it home
(denying the steal). The first team to the Score limit captures wins (or, with a match
clock, the most captures when time runs out); the scoreboard (Tab) shows each team's
captures. games/ctf-001 is a ready 1v1 first-to-3-captures arena. CTF, like
Deathmatch, uses endless respawn.
Limits (this slice). LAN only, same build/OS/arch, as co-op above. Team Deathmatch, Elimination and Capture the Flag are all fully wired, deterministic versus modes.
Packaging: Build and export
File ▸ Package game (Build & export)… builds a portable, double-clickable
copy of your current game. It first asks where to save (a folder picker , pick
anywhere, e.g. straight onto a USB stick), then writes a <Your game>/ folder there:
<Your game>.exe, double-click to play (named after your game, with the NJORD icon).Play.bat, a friendly launcher.- your game's data and assets.
README.txt, the controls.
The first build can take a few minutes; later ones are fast. When it finishes, the folder opens automatically. Hand it to anyone, no install or dev tools required.
Custom exe icon: drop an icon.ico in your game's folder and it is embedded
into the packaged .exe (shown in Explorer and the taskbar). Without one, the
default NJORD icon is used.
Settings
Edit ▸ Settings:
- API keys , NJORD uses your own AI keys (BYOK) for the prompt and content generation. Keys are stored securely in your OS credential manager, never in the project. The Orchestrator provider can also be set to a local/offline model (Ollama, LM Studio, or any OpenAI-compatible endpoint), see Local and offline AI under The AI prompt and agents.
- Confirm before generating , when on, a 3D model prompt is shown for you to edit/confirm/cancel before it is sent to the (paid) provider, see Approval BEFORE a paid model is generated under The AI prompt and agents.
- External editors , choose which program opens models and images for right-click external editing.
Controls reference
Editor
| Action | Control |
|---|---|
| Orbit camera | Left / middle drag |
| Pan camera | Right drag |
| Zoom | Scroll wheel |
| Frame selection | F |
| Reveal selection in the Assets tree (open folders + scroll) | F (with the tree focused) |
| Snap to a view (Top/Front/Side…) | Click an axis on the orientation gizmo, top-right |
| Move / Rotate / Scale tool | W / E / R |
| Gizmo World / Local space | Toggle in the viewport overlay (top-right) |
| Snap while dragging a gizmo | Hold Ctrl |
| Duplicate selected object | Ctrl+D / Cmd+D |
| Undo / Redo | Ctrl+Z / Ctrl+Y |
| Save | Ctrl+S |
In-game
| Action | Control |
|---|---|
| Move | W A S D |
| Sprint | Shift |
| Jump | Space |
| Look | Mouse |
| Attack / shoot | Left mouse button |
| Aim (ranged games) | Right mouse button |
| Reload (ranged games) | R |
| Camera: first/third person | C |
| Talk to a nearby NPC | E |
| Dialog choices | Number keys 1-9 |
| Quest log | J |
| Animation debug overlay (editor) | F3 |
| Per-bone hitbox overlay (editor, Play) | F4 |
| Pause (packaged game) | Esc |
| Options (packaged game) | O (main/pause menu) |
The move/jump/sprint/interact/attack keys are the defaults. A game can set them
with a [controls] section in style.toml (key names like W, Space,
ShiftLeft, E, or Mouse:Left for attack), and the player can rebind the
keyboard actions in the in-game Options menu (saved to settings.json).
NJORD is a product of NJORD Studio AB.