Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Flint Engine

A CLI-first, AI-agent-optimized 3D game engine written in Rust.

Flint is a general-purpose 3D game engine designed from the ground up to provide an excellent interface for AI coding agents, while maintaining effective workflows for human developers. Unlike existing engines that optimize for GUI-driven workflows, Flint prioritizes programmatic interaction, introspection, and validation.

The Core Idea

Current game engines are built around visual editors, drag-and-drop workflows, and GUI-heavy tooling. These become friction points when AI agents attempt to make changes programmatically — the agent ends up fighting against abstractions designed for human spatial reasoning and visual feedback loops.

Flint inverts this: the primary interface is CLI and code, with visual tools focused on validating results rather than creating them.

Every scene is a TOML file you can read, diff, and version. Every operation is a composable CLI command. Every piece of engine state is queryable as structured data. The viewer exists to answer one question: “Did the agent do what I asked?”

Built with Flint

FlintKart — a low-poly kart racer built with Flint

FlintKart: a kart racing game built as a standalone project using Flint’s game project architecture — custom schemas, scripts, and assets layered on top of the engine via git subtree.

Visual Showcase

PBR rendering with shadows, materials, and models

The Luminarium showcase scene: Cook-Torrance PBR shading with cascaded shadows, textured walls and floors, glTF models, and emissive materials.

Wireframe debug visualization

Wireframe debug mode (F1) reveals mesh topology — one of seven built-in debug visualizations for inspecting geometry, normals, depth, UVs, and material properties.

What It Looks Like

Create a scene, add entities, query them, and view the result — all from the command line:

# Initialize a project
flint init my-game

# Create a scene and populate it
flint scene create levels/tavern.scene.toml --name "The Tavern"
flint entity create --archetype room --name "main_hall" --scene levels/tavern.scene.toml
flint entity create --archetype door --name "front_door" --parent "main_hall" --scene levels/tavern.scene.toml

# Query what you've built
flint query "entities where archetype == 'door'" --scene levels/tavern.scene.toml

# Validate against constraints
flint validate levels/tavern.scene.toml --fix --dry-run

# See it in 3D with PBR rendering
flint serve levels/tavern.scene.toml --watch

# Walk around in first person
flint play levels/tavern.scene.toml

Current Status

The engine supports:

  • Entity CRUD via CLI with archetype-based creation
  • Scene serialization in human-readable TOML
  • Query language for filtering and inspecting entities
  • Schema system for component and archetype definitions
  • Constraint validation with auto-fix capabilities
  • Asset management with content-addressed storage and glTF import
  • PBR renderer with Cook-Torrance shading, cascaded shadow mapping, and glTF mesh rendering
  • GPU skeletal animation with glTF skin/joint import, vertex skinning, and crossfade blending
  • egui inspector with entity tree, component editing, and constraint overlay
  • Hot-reload viewer that watches for file changes
  • Headless rendering for CI and automated screenshots
  • Physics simulation via Rapier 3D with kinematic character controller
  • First-person gameplay with WASD movement, mouse look, jumping, and sprinting
  • Game loop with fixed-timestep accumulator for deterministic physics
  • Spatial audio via Kira with 3D positioned sounds, ambient loops, and event-driven triggers
  • Property animation with TOML-defined keyframe clips (Step, Linear, CubicSpline interpolation)
  • Skeletal animation with glTF skin import, GPU bone matrix skinning, and crossfade blending
  • Rhai scripting with entity/input/audio/animation APIs, event callbacks, and hot-reload
  • Interactable entities with HUD prompts, proximity detection, and scripted behaviors
  • AI asset generation with pluggable providers (Flux textures, Meshy 3D models, ElevenLabs audio), style guides, batch scene resolution, model validation, and build manifests
  • Billboard sprites with camera-facing quads and sprite sheet animation
  • GPU particle system with instanced rendering, per-emitter pooling, alpha/additive blending, and configurable emission shapes
  • Extensible input system with config-driven bindings for keyboard, mouse, and gamepad with runtime rebinding
  • Data-driven UI system with TOML-defined layouts, style classes, anchor-based positioning, flow layouts, and runtime scripting API
  • Game project architecture for standalone games that include the engine as a git subtree

See the Roadmap for the full development history.

Who Is This For?

  • AI agent developers building game content programmatically
  • Technical game developers who prefer code over visual editors
  • Tooling enthusiasts who want to compose game development operations
  • Rust game developers looking for a deterministic, introspectable engine

Reading This Guide

  • Start with Why Flint? to understand the motivation
  • Follow the Getting Started guide to build from source and create your first project
  • Explore Core Concepts to learn about the engine’s systems
  • Check the Architecture section if you want to understand the codebase
  • Browse the API Reference for per-crate Rust documentation

Quick Reference

A scannable cheat sheet for daily Flint development.

CLI Commands

CommandDescription
flint init <name>Initialize a new project
flint scene create <path>Create a new scene file
flint scene listList scene files
flint scene infoShow scene metadata
flint entity createCreate an entity in a scene
flint entity deleteDelete an entity from a scene
flint query "<expr>"Query entities (e.g., "entities where archetype == 'door'")
flint schema <name>Inspect a component or archetype schema
flint validate <scene>Validate scene against constraints (--fix to auto-fix)
flint edit <file>Unified interactive editor (auto-detects file type)
flint play <scene>First-person gameplay with physics + scripting
flint render <scene> -o out.pngHeadless render to PNG
flint gen <spec> -o out.glbRun procgen spec to produce mesh/texture
flint asset generate <type>AI asset generation (texture, model, audio)
flint asset import <file>Import file into asset catalog
flint prefab view <template>Preview a prefab template in the viewer

Keyboard Shortcuts

Player (flint play)

KeyAction
WASDMove
MouseLook around
SpaceJump
ShiftSprint
EInteract
Left ClickFire
RReload
1 / 2Weapon slots
F1Cycle debug mode (PBR → Wireframe → Normals → Depth → UV → Unlit → Metal/Rough)
F4Toggle shadows
F5Toggle bloom
F6Toggle post-processing pipeline
F11Toggle fullscreen
EscapeRelease cursor / Exit

Scene Viewer (flint edit <scene.toml>)

KeyAction
Left-clickSelect entity / pick gizmo axis
Left-dragOrbit camera (or drag gizmo)
Right-dragPan camera
ScrollZoom
Ctrl+SSave scene
Ctrl+ZUndo position change
Ctrl+Shift+ZRedo position change
F1Cycle debug mode
F2Toggle wireframe overlay
F3Toggle normal arrows
F4Toggle shadows

Spline Editor (flint edit <scene.toml> --spline)

KeyAction
Left-clickSelect control point
Left-dragMove control point
Alt+dragMove vertically (Y)
Middle-dragOrbit
Right-dragPan
Tab / Shift+TabCycle control points
IInsert point
DeleteRemove point
Ctrl+SSave spline
Ctrl+ZUndo

File Type Auto-Detection (flint edit)

ExtensionOpens
.scene.toml, .chunk.tomlScene viewer
.procgen.tomlProcgen previewer (or texture pipeline editor)
.terrain.tomlTerrain editor
.glb, .gltfModel previewer (orbit camera)

Common TOML Snippets

Minimal Entity

[entities.my_thing]
archetype = "furniture"

[entities.my_thing.transform]
position = [0, 1, 0]
rotation = [0, 45, 0]
scale = [1, 1, 1]

PBR Material

[entities.my_thing.material]
base_color = [0.8, 0.2, 0.1]
roughness = 0.6
metallic = 0.0
emissive = [1.0, 0.4, 0.1]
emissive_strength = 2.0

Physics Body

[entities.wall.collider]
shape = "box"
size = [10.0, 4.0, 0.5]

[entities.wall.rigidbody]
body_type = "static"

Particle Emitter (Fire)

[entities.fire.particle_emitter]
emission_rate = 40.0
max_particles = 200
lifetime_min = 0.3
lifetime_max = 0.8
speed_min = 1.5
speed_max = 3.0
direction = [0, 1, 0]
gravity = [0, 2.0, 0]
size_start = 0.15
size_end = 0.02
color_start = [1.0, 0.7, 0.1, 0.9]
color_end = [1.0, 0.1, 0.0, 0.0]
blend_mode = "additive"
shape = "sphere"
shape_radius = 0.15
autoplay = true

Post-Processing

[post_process]
bloom_enabled = true
bloom_intensity = 0.04
bloom_threshold = 1.0
vignette_enabled = true
vignette_intensity = 0.3
exposure = 1.0

UI Layout

# ui/hud.ui.toml
[ui]
name = "HUD"
style = "ui/hud.style.toml"

[elements.score_panel]
type = "panel"
anchor = "top-right"
class = "hud-panel"

[elements.score_text]
type = "text"
parent = "score_panel"
class = "score-value"
text = "0"

UI Style

# ui/hud.style.toml
[styles.hud-panel]
width = 160
height = 50
bg_color = [0.0, 0.0, 0.0, 0.6]
rounding = 6
padding = [10, 8, 10, 8]
x = -10
y = 10

[styles.score-value]
font_size = 28
color = [1.0, 1.0, 1.0, 1.0]
text_align = "center"
width_pct = 100

Script Attachment

[entities.npc.script]
source = "npc_behavior.rhai"
enabled = true

[entities.npc.interactable]
prompt_text = "Talk"
range = 3.0
interaction_type = "talk"

Audio Source

[entities.campfire.audio_source]
file = "audio/fire_crackle.ogg"
volume = 0.8
loop = true
spatial = true
min_distance = 1.0
max_distance = 15.0

Prefab Instance

[prefabs.player]
template = "kart"
prefix = "player"

[prefabs.player.overrides.kart.transform]
position = [0, 0, 0]

Top Scripting Functions

FunctionReturnsDescription
self_entity()i64ID of the entity this script is attached to
get_entity(name)i64Look up entity by name (-1 if not found)
get_field(id, comp, field)DynamicRead a component field
set_field(id, comp, field, val)Write a component field
get_position(id)#{x,y,z}Entity position
set_position(id, x, y, z)Set entity position
distance(a, b)f64Distance between two entities
is_action_pressed(action)boolCheck if action is held
is_action_just_pressed(action)boolCheck if action pressed this frame
delta_time()f64Seconds since last frame
play_sound(name)Play a sound effect
play_clip(id, clip)Play an animation clip
raycast(ox,oy,oz, dx,dy,dz, dist)Map/()Cast a ray, get hit info
move_character(id, dx, dy, dz)#{x,y,z,grounded}Collision-corrected movement
spawn_entity(name)i64Create a new entity
load_scene(path)Transition to a new scene
push_state("paused")Push a game state (e.g., pause)
pop_state()Pop to previous game state
persist_set(key, val)Store data across scene transitions
load_ui(path)i64Load a .ui.toml document (returns handle)
ui_set_text(id, text)Set element text content
ui_show(id) / ui_hide(id)Toggle element visibility
ui_set_style(id, prop, val)Override a style property at runtime

Render Command Quick Examples

# Basic screenshot
flint render scene.toml -o shot.png --schemas schemas

# Framed hero shot
flint render scene.toml -o hero.png --distance 20 --pitch 30 --yaw 45 --target 0,1,0 --no-grid

# Debug views
flint render scene.toml -o wireframe.png --debug-mode wireframe
flint render scene.toml -o normals.png --debug-mode normals
flint render scene.toml -o depth.png --debug-mode depth

# Post-processing control
flint render scene.toml -o bloom.png --bloom-intensity 0.08
flint render scene.toml -o raw.png --no-postprocess

Why Flint?

The Problem

Game engines today — Unity, Unreal, Godot — are designed around visual editors. You drag objects into scenes, connect nodes in graphs, click through property inspectors. These workflows are excellent for humans using a mouse, but they create friction in two growing scenarios:

  1. AI agents building game content. When an AI coding agent needs to place a door in a scene, it shouldn’t need to simulate mouse clicks on a GUI. It should issue a command and get structured feedback.

  2. Automation and CI pipelines. Validating a scene, running regression tests on visual output, or batch-processing hundreds of entities — these tasks fight against editor-centric architectures.

The core tension: existing engines treat programmatic access as a secondary concern. The API exists, but it’s bolted onto a system designed for spatial interaction. Scene formats are binary or semi-readable. Introspection is limited. Determinism is not guaranteed.

The Thesis

Flint starts from the opposite assumption: the primary interface is CLI and code. Visual tools are for validation, not creation.

This doesn’t mean Flint is hostile to humans. It means every operation flows through a composable, scriptable interface first. If you can do it in the CLI, you can automate it. If you can automate it, an AI agent can do it. The viewer is the place where a human confirms: “Yes, that’s what I wanted.”

What This Enables

For AI agents

An agent working with Flint has a clean contract:

  • Issue CLI commands, get structured JSON/TOML responses
  • Query any aspect of engine state with a SQL-inspired language
  • Validate work against declarative constraint rules
  • Produce visual artifacts (headless renders) for verification

No simulated GUI interaction. No screen scraping. No ambiguous visual state.

For humans

A developer working with Flint gets:

  • Scene files that are human-readable TOML, easily diffable in git
  • A query language for exploring what’s in a scene without opening an editor
  • Constraint rules that serve as living documentation of what a “correct” scene looks like
  • A hot-reload viewer that updates in real-time as files change

For teams

A team using Flint gets:

  • Deterministic builds — same inputs always produce identical outputs
  • Text-based formats that merge cleanly in version control
  • Structured output for CI pipelines and automated testing
  • A shared vocabulary between human developers and AI tools

Comparison

AspectTraditional EnginesFlint
Primary interfaceGUI editorCLI
Scene formatBinary or semi-textTOML (fully text)
Programmatic APISecondaryPrimary
IntrospectionLimitedFull (query language)
Deterministic buildsGenerally noYes
AI-agent optimizedNoYes
ValidationRuntime errorsDeclarative constraints

The Name

Flint is a tool for starting fires. Simple, reliable, fundamental. Strike it and something sparks into existence. That’s the idea: minimal friction between intent and result.

Design Principles

Flint’s architecture follows six principles that guide every design decision. They are listed in priority order — when principles conflict, higher-ranked ones win.

1. CLI-First

Every operation is expressible as a composable command. There is no operation that requires a GUI. The CLI is the source of truth for what the engine can do.

This means:

  • All commands accept flags for output format (--format json, --format toml)
  • Commands compose via pipes and standard shell tooling
  • Batch operations are first-class, not afterthoughts
  • The viewer is a consumer of state, not a producer of it

2. Introspectable

You can query any aspect of engine state as structured data. Nothing is hidden behind opaque handles or binary blobs.

# What entities exist?
flint query "entities where archetype == 'door'"

# What does a door look like?
flint schema door

# What would this change break?
flint validate levels/tavern.scene.toml --fix --dry-run

The query language is the same whether you’re exploring interactively or writing constraint rules. Learn it once, use it everywhere.

3. Deterministic

Same inputs always produce identical outputs. No hidden state, no ambient randomness, no order-dependent behavior.

  • Entity IDs are stable across save/load cycles
  • Procedural generation uses explicit seeds
  • Build manifests record exact asset hashes
  • Headless renders are reproducible for regression testing

4. Text-Based

Scene and asset formats are human-readable, machine-parseable, and diffable. TOML is the primary format throughout.

[entities.front_door]
archetype = "door"
parent = "main_hall"

[entities.front_door.transform]
position = [5, 0, 0]

[entities.front_door.door]
style = "hinged"
locked = false

This isn’t just about readability — it’s about collaboration. Text files merge cleanly in version control. Diffs are meaningful. AI agents can read and write them directly.

5. Constraint-Driven

Declarative rules define what a valid scene looks like. The engine validates against these rules and can optionally auto-fix violations.

Constraints serve multiple roles:

  • Validation — catch errors before they become runtime bugs
  • Documentation — constraints describe what “correct” means
  • Automation — auto-fix rules handle routine corrections
  • Communication — constraints are a shared contract between human and AI

6. Hybrid Workflows

Humans and AI agents collaborate effectively on the same project. Neither workflow is an afterthought.

The typical loop:

  1. An AI agent creates or modifies scene content via CLI
  2. Constraints validate the changes automatically
  3. A human reviews the result in the viewer
  4. Feedback flows back to the agent as structured data

This principle ensures Flint doesn’t optimize so hard for agents that humans can’t use it, or so hard for humans that agents can’t automate it.

CLI-First Workflow

Flint’s primary interface is the command line. Every engine operation — creating entities, querying scenes, validating constraints, importing assets, generating content — is a composable CLI command. Visual tools exist to validate results, not to create them.

Why CLI-First?

Traditional game engines center on visual editors: drag a mesh into a viewport, tweak a slider, click Save. This works well for a single human at a desk, but it creates friction for:

  • Automation — you can’t script a drag-and-drop operation
  • Reproducibility — a sequence of mouse clicks isn’t version-controllable
  • AI agents — they see text, not pixels
  • CI/CD — headless servers have no windows to click in
  • Collaboration — binary project files don’t merge cleanly in git

Flint inverts the priority: text-first, visual-second. The CLI is the engine’s native language.

Composable Commands

Every command reads structured input and produces structured output. This means standard shell patterns work naturally:

# Create a scene with several entities
flint scene create levels/dungeon.scene.toml --name "Dungeon Level 1"
flint entity create --archetype room --name "entrance" --scene levels/dungeon.scene.toml
flint entity create --archetype door --name "iron_gate" --parent "entrance" --scene levels/dungeon.scene.toml

# Query and filter with standard tools
flint query "entities where archetype == 'door'" --scene levels/dungeon.scene.toml --format json

# Validate and capture results
flint validate levels/dungeon.scene.toml --format json

# Render a preview image for review
flint render levels/dungeon.scene.toml --output preview.png --width 1920 --height 1080

Structured Output

Commands support --format json and --format toml output modes, making their results machine-readable. This enables pipelines like:

# Count entities of each archetype
flint query "entities" --scene levels/tavern.scene.toml --format json | jq 'group_by(.archetype) | map({archetype: .[0].archetype, count: length})'

# Check if validation passes (exit code 0 = clean, 1 = violations)
flint validate levels/tavern.scene.toml --format json && echo "Scene is valid"

JSON output follows consistent schemas, so tools can parse results reliably across engine versions.

Batch Operations

Because every operation is a command, building complex scenes is just a script:

#!/bin/bash
SCENE="levels/tavern.scene.toml"

flint scene create "$SCENE" --name "The Rusty Flagon"

# Build the structure
for room in main_hall kitchen storage; do
    flint entity create --archetype room --name "$room" --scene "$SCENE"
done

# Add doors between rooms
flint entity create --archetype door --name "kitchen_door" --parent "main_hall" --scene "$SCENE"
flint entity create --archetype door --name "storage_door" --parent "kitchen" --scene "$SCENE"

# Validate the whole thing
flint validate "$SCENE" --fix

This script is version-controllable, reproducible, and can run in CI.

The Viewer as Validator

The flint serve --watch viewer and flint play command are verification tools, not authoring tools. They answer the question: “Does the scene I built look correct?”

# Edit the TOML in your text editor, viewer updates automatically
flint serve levels/tavern.scene.toml --watch

# Walk through the scene to verify physics, audio, and interactions
flint play levels/tavern.scene.toml

The viewer hot-reloads when the scene file changes. Edit TOML, save, see the result — no GUI interaction required.

Headless Rendering for CI

Scenes can be rendered to PNG without a window, enabling automated visual validation:

flint render levels/tavern.scene.toml --output screenshots/tavern.png --width 1920 --height 1080

This is the foundation for visual regression testing in CI pipelines — render a baseline, then compare future renders against it.

Contrast with GUI Engines

AspectGUI EngineFlint
Primary inputMouse clicks, drag-and-dropCLI commands, TOML files
AutomationLimited (editor scripting plugins)Native (every operation is a command)
Version controlBinary project filesText TOML files, clean git diffs
AI agent supportScreenshot parsing, GUI automationStructured text I/O, query introspection
Headless operationUsually not supportedFirst-class (render, validate, query)
ReproducibilityManual steps, screenshotsScripts, exit codes, structured output

This doesn’t mean Flint is text-only. It means the text interface is complete — anything you can do in the viewer, you can do (and automate) from the command line.

Further Reading

AI Agent Interface

Flint is designed from the ground up to be an excellent interface for AI coding agents. Where traditional engines optimize for human spatial reasoning and visual feedback, Flint optimizes for text-based reasoning, structured data, and automated validation.

The Problem with GUI Engines

AI agents working with traditional game engines face fundamental friction:

  • Screenshot parsing — agents must interpret rendered pixels to understand scene state, an unreliable and lossy process
  • GUI automation — clicking buttons and dragging sliders through accessibility APIs or screenshot analysis is brittle
  • Binary formats — proprietary project files can’t be read, diffed, or generated as text
  • Implicit state — engine state lives in inspector panels, viewport selections, and undo histories that agents can’t access

Flint eliminates all of these friction points.

Structured Input and Output

Every Flint command accepts text input and produces structured text output:

# JSON output for machine parsing
flint query "entities where archetype == 'door'" --scene levels/tavern.scene.toml --format json

# Exit codes signal success (0) or failure (1)
flint validate levels/tavern.scene.toml --format json
echo $?  # 0 = valid, 1 = violations found

An agent can create entities, modify scenes, and inspect state entirely through text — no screenshots, no pixel coordinates, no GUI automation.

Query-Based Introspection

The query language gives agents programmatic access to scene state. Instead of reading a screenshot to count doors, an agent can:

# How many doors are in this scene?
flint query "entities where archetype == 'door'" --scene levels/tavern.scene.toml --format json | jq length

# Is this door locked?
flint query "entities where door.locked == true" --scene levels/tavern.scene.toml --format json

# What components does the player entity have?
flint query "entities where archetype == 'player'" --scene levels/tavern.scene.toml --format json

Queries return structured data that agents can parse, reason about, and use to plan their next action.

Constraint Validation as Feedback

Constraints provide an automated feedback loop. An agent doesn’t need a human to check its work — it can validate programmatically:

# Agent creates some entities...
flint entity create --archetype door --name "secret_door" --scene levels/tavern.scene.toml

# Then checks if the scene is still valid
flint validate levels/tavern.scene.toml --format json

If validation fails, the JSON output tells the agent exactly what’s wrong and how to fix it. The --fix --dry-run mode even previews what auto-fixes would apply. This creates a tight create-validate-fix loop that agents can execute without human intervention.

Schema Introspection

Agents can discover what components and archetypes are available without reading documentation:

# What fields does the 'door' component have?
flint schema door

# What components does the 'player' archetype include?
flint schema player

This means an agent can learn the engine’s data model at runtime, then use that knowledge to create valid entities.

Headless Rendering

Visual verification without a window:

# Render the scene to an image file
flint render levels/tavern.scene.toml --output preview.png --width 1920 --height 1080

An agent (or its supervisor) can render a preview image to check that the scene looks correct, without opening a GUI. This enables visual regression testing in CI and supports workflows where an agent builds a scene, renders a preview, and a human reviews the image.

TOML as Scene Format

Scenes are plain TOML text files. An agent can:

  • Read a scene file directly as text
  • Write entity data by editing TOML
  • Diff changes with standard tools (git diff)
  • Generate entire scenes programmatically
  • Merge changes from multiple agents without conflicts (each entity is a distinct TOML section)

No proprietary binary formats, no deserialization libraries, no SDK required.

AI Asset Generation

Phase 5 extends the agent interface to asset creation. Agents can generate textures, 3D models, and audio through CLI commands:

# Generate a texture using AI
flint asset generate texture -d "rough stone wall with mortar lines" --style medieval_tavern

# Batch-generate all missing assets for a scene
flint asset resolve my_scene.scene.toml --strategy ai_generate --style medieval_tavern

Style guides ensure generated assets maintain visual consistency, and model validation checks results against constraints — the same automated feedback loop that works for scene structure now works for asset quality.

Further Reading

Installation

Flint is built from source using the Rust toolchain. There are no pre-built binaries yet.

Prerequisites

  • Rust (stable, 1.75+) — install from rustup.rs
  • Git — for cloning the repository
  • A GPU with Vulkan, Metal, or DX12 support (for the renderer and viewer)

Build from Source

Clone the repository and build in release mode:

git clone https://github.com/chrischaps/flint.git
cd flint
cargo build --release

The binary is at target/release/flint (or target/release/flint.exe on Windows).

Verify Installation

cargo run --bin flint -- --version

You should see the Flint version string.

Running Without Installing

You can run Flint directly through Cargo without installing it system-wide:

cargo run --bin flint -- <command>

For example:

cargo run --bin flint -- init my-game
cargo run --bin flint -- serve demo/showcase.scene.toml --watch

Running Tests

To verify everything is working:

cargo test

This runs the full test suite across all crates.

Optional: Add to PATH

To use flint directly without cargo run:

cargo install --path crates/flint-cli

Or copy the release binary to a directory on your PATH.

What’s Next

With Flint built, follow Your First Project to create a scene from scratch.

Your First Project

This guide walks through creating a Flint project and building a simple scene using only CLI commands.

Initialize a Project

flint init my-tavern

This creates a project directory with the standard structure:

my-tavern/
├── schemas/
│   ├── components/
│   │   ├── transform.toml
│   │   ├── bounds.toml
│   │   └── door.toml
│   ├── archetypes/
│   │   ├── room.toml
│   │   ├── door.toml
│   │   ├── furniture.toml
│   │   └── character.toml
│   └── constraints/
│       └── basics.toml
├── levels/
└── assets/

The schemas/ directory contains default component definitions, archetype bundles, and constraint rules. You’ll modify and extend these as your project grows.

Create a Scene

flint scene create my-tavern/levels/tavern.scene.toml --name "The Rusty Flint Tavern"

This creates an empty scene file:

[scene]
name = "The Rusty Flint Tavern"
version = "1.0"

Add Rooms

Build out the space with room entities:

flint entity create --archetype room --name "main_hall" \
    --scene my-tavern/levels/tavern.scene.toml \
    --schemas my-tavern/schemas \
    --props '{"transform":{"position":[0,0,0]},"bounds":{"min":[-7,0,-5],"max":[7,4,5]}}'

The --archetype room flag tells Flint to create an entity with the components defined in schemas/archetypes/room.toml (transform + bounds). The --props flag provides the specific values.

Add a kitchen connected to the main hall:

flint entity create --archetype room --name "kitchen" \
    --parent "main_hall" \
    --scene my-tavern/levels/tavern.scene.toml \
    --schemas my-tavern/schemas \
    --props '{"transform":{"position":[0,0,-9]},"bounds":{"min":[-4,0,-3],"max":[4,3.5,3]}}'

The --parent flag establishes a hierarchy — the kitchen is a child of the main hall.

Add a Door

flint entity create --archetype door --name "front_entrance" \
    --parent "main_hall" \
    --scene my-tavern/levels/tavern.scene.toml \
    --schemas my-tavern/schemas \
    --props '{"transform":{"position":[0,0,5]},"door":{"style":"hinged","locked":false}}'

Query Your Scene

See what you’ve built:

flint query "entities" --scene my-tavern/levels/tavern.scene.toml

Filter for specific archetypes:

flint query "entities where archetype == 'door'" --scene my-tavern/levels/tavern.scene.toml

Inspect the Scene File

The scene is plain TOML. Open my-tavern/levels/tavern.scene.toml and you’ll see:

[scene]
name = "The Rusty Flint Tavern"
version = "1.0"

[entities.main_hall]
archetype = "room"

[entities.main_hall.transform]
position = [0, 0, 0]

[entities.main_hall.bounds]
min = [-7, 0, -5]
max = [7, 4, 5]

[entities.kitchen]
archetype = "room"
parent = "main_hall"

[entities.kitchen.transform]
position = [0, 0, -9]

[entities.kitchen.bounds]
min = [-4, 0, -3]
max = [4, 3.5, 3]

[entities.front_entrance]
archetype = "door"
parent = "main_hall"

[entities.front_entrance.transform]
position = [0, 0, 5]

[entities.front_entrance.door]
style = "hinged"
locked = false

Everything is readable, editable, and diffable. You can modify this file directly — the CLI isn’t the only way to edit scenes.

View It

Launch the hot-reload viewer:

flint serve my-tavern/levels/tavern.scene.toml --watch --schemas my-tavern/schemas

A window opens showing your scene as colored boxes:

  • Blue wireframes for rooms
  • Orange boxes for doors
  • Green boxes for furniture
  • Yellow boxes for characters

The viewer hot-reloads — any change to the scene file (from the CLI, a text editor, or an AI agent) updates the view instantly.

Camera controls:

InputAction
Left-dragOrbit
Right-dragPan
ScrollZoom
SpaceReset camera
RForce reload
EscapeQuit

What’s Next

Your First Scene

A Flint scene is a TOML file describing entities, their components, and their relationships. This page explains the scene format by building one from scratch.

Scene Structure

Every scene file has two sections: metadata and entities.

# Metadata
[scene]
name = "My Scene"
version = "1.0"
description = "An optional description"

# Entities
[entities.my_entity]
archetype = "room"

[entities.my_entity.transform]
position = [0, 0, 0]

The [scene] table holds metadata. Everything under [entities.*] defines the objects in your world.

Entities

An entity is a named thing in the scene. Its name is the key under [entities]:

[entities.main_hall]
archetype = "room"

Entities can optionally have:

  • An archetype — a schema-defined bundle of components
  • A parent — another entity this one is attached to
  • Components — data tables nested under the entity

Components

Components are data attached to entities. They’re defined as nested TOML tables:

[entities.main_hall]
archetype = "room"

[entities.main_hall.transform]
position = [0, 0, 0]

[entities.main_hall.bounds]
min = [-7, 0, -5]
max = [7, 4, 5]

The transform and bounds components are defined by schema files in schemas/components/. The schema tells Flint what fields are valid and what types they are.

Parent-Child Relationships

Entities form hierarchies through the parent field:

[entities.main_hall]
archetype = "room"

[entities.main_hall.transform]
position = [0, 0, 0]

[entities.kitchen]
archetype = "room"
parent = "main_hall"

[entities.kitchen.transform]
position = [0, 0, -9]

The kitchen is a child of the main hall. In the viewer, child transforms are relative to their parent.

A Complete Example

Here’s a small but complete scene — a room with a door and a table:

[scene]
name = "Simple Room"
version = "1.0"

[entities.room]
archetype = "room"

[entities.room.transform]
position = [0, 0, 0]

[entities.room.bounds]
min = [-5, 0, -5]
max = [5, 3, 5]

[entities.door]
archetype = "door"
parent = "room"

[entities.door.transform]
position = [0, 0, 5]

[entities.door.door]
style = "hinged"
locked = false
open_angle = 90.0

[entities.table]
archetype = "furniture"
parent = "room"

[entities.table.transform]
position = [0, 0, 0]

[entities.table.bounds]
min = [-0.6, 0, -0.6]
max = [0.6, 0.8, 0.6]

Editing Scenes

You can edit scene files in three ways:

  1. CLI commandsflint entity create, flint entity delete, etc.
  2. Text editor — open the TOML file directly
  3. Programmatically — any tool that can write TOML

All three approaches produce the same result. The flint serve --watch viewer detects changes from any source and reloads automatically.

Validating Scenes

Run the constraint checker to verify your scene is well-formed:

flint validate levels/my-scene.scene.toml --schemas schemas

This checks your scene against the rules defined in schemas/constraints/. See Constraints for details.

What’s Next

  • Entities and ECS explains the entity-component system
  • Schemas covers how components and archetypes are defined
  • Scenes goes deeper into the scene system internals

Querying Entities

Flint includes a SQL-inspired query language for filtering and inspecting entities. Queries let you search scenes by archetype, component values, or nested field data.

Basic Syntax

All queries follow the pattern:

entities where <condition>

The simplest query returns all entities:

flint query "entities" --scene levels/tavern.scene.toml

Filtering by Archetype

The most common filter — find entities of a specific type:

# Find all doors
flint query "entities where archetype == 'door'" --scene levels/tavern.scene.toml

# Find all rooms
flint query "entities where archetype == 'room'" --scene levels/tavern.scene.toml

Comparison Operators

OperatorMeaningExample
==Equalarchetype == 'door'
!=Not equalarchetype != 'room'
>Greater thantransform.position.y > 5.0
<Less thandoor.open_angle < 90
>=Greater or equalaudio_source.volume >= 0.5
<=Less or equalcollider.friction <= 0.3
containsString containsname contains 'wall'

Querying Component Fields

Access component fields with dot notation:

# Find locked doors
flint query "entities where door.locked == true" --scene levels/tavern.scene.toml

# Find entities above a certain height
flint query "entities where transform.position.y > 2.0" --scene levels/tavern.scene.toml

# Find loud audio sources
flint query "entities where audio_source.volume > 0.8" --scene levels/tavern.scene.toml

Output Formats

Query results can be formatted for different consumers:

# Human-readable (default)
flint query "entities where archetype == 'door'" --scene levels/tavern.scene.toml

# JSON for scripting and AI agents
flint query "entities where archetype == 'door'" --scene levels/tavern.scene.toml --format json

# TOML for configuration workflows
flint query "entities where archetype == 'door'" --scene levels/tavern.scene.toml --format toml

Combining with Shell Tools

JSON output composes with standard tools:

# Count doors
flint query "entities where archetype == 'door'" --scene levels/tavern.scene.toml --format json | jq length

# Get just the names
flint query "entities where archetype == 'door'" --scene levels/tavern.scene.toml --format json | jq '.[].name'

# Find entities with a specific parent
flint query "entities" --scene levels/tavern.scene.toml --format json | jq '.[] | select(.parent == "main_hall")'

Further Reading

The Scene Viewer

The Flint viewer is a real-time 3D window for validating scenes. It renders your scene with full PBR shading and shadows, and provides an egui inspector panel for browsing entities and editing component properties.

Launching the Viewer

flint serve levels/tavern.scene.toml --watch --schemas schemas

The --watch flag enables hot-reload: edit the scene TOML file, and the viewer re-parses and re-renders automatically. The entire file is re-parsed on each change (not incremental), which keeps the implementation simple and avoids synchronization issues.

Camera Controls

The viewer uses an orbit camera that rotates around a focus point:

InputAction
Left-dragOrbit around focus (or drag gizmo axis when hovering)
Right-dragPan the view
ScrollZoom in/out
SpaceReset camera
RForce reload
EscapeQuit / cancel gizmo drag

Transform Gizmo

When you select an entity in the inspector, a translate gizmo appears at its position with colored axis arrows and plane handles:

  • Red arrow — drag to move along X axis
  • Green arrow — drag to move along Y axis
  • Blue arrow — drag to move along Z axis
  • Plane handles (small squares at axis intersections) — drag to move in two axes simultaneously

The gizmo uses constraint-plane dragging: for single-axis movement, it automatically picks the plane most perpendicular to your camera view. Inactive axes dim while dragging to clearly show the active constraint.

Editing Shortcuts

InputAction
Ctrl+SSave scene to disk
Ctrl+ZUndo position change
Ctrl+Shift+ZRedo position change
EscapeCancel current gizmo drag

All position changes are tracked in an undo/redo stack. Saving writes the modified positions back to the scene TOML file.

The Inspector Panel

The egui-based inspector panel (on the left side of the viewer) provides:

  • Entity tree — hierarchical list of all entities in the scene, reflecting parent-child relationships
  • Component editor — select an entity to view and edit its component values; position fields are editable via drag-value widgets with color-coded labels (red/green/blue matching the gizmo axes)
  • Constraint overlay — validation results from flint-constraint, highlighting any rule violations

Rendering Features

The viewer renders scenes with the same PBR pipeline used by the player:

  • Cook-Torrance physically-based shading
  • Cascaded shadow mapping from directional lights
  • glTF mesh rendering with material support
  • Debug rendering modes (cycle with F1)
  • Shadow toggle (F4)
  • Fullscreen toggle (F11)

Playing a Scene

To experience a scene in first-person with physics, use play instead of serve:

flint play levels/tavern.scene.toml

See the CLI Reference for full play command details and controls.

Headless Rendering

For CI pipelines and automated screenshots, render to PNG without opening a window:

flint render levels/tavern.scene.toml --output preview.png --width 1920 --height 1080

Entities and ECS

Flint uses an Entity-Component-System (ECS) architecture, built on top of the hecs crate. This page explains how entities, components, and IDs work in Flint.

What Is ECS?

In an ECS architecture:

  • Entities are unique identifiers (not objects with methods)
  • Components are pure data attached to entities
  • Systems are logic that operates on entities with specific component combinations

Flint’s twist: components are dynamic. Instead of being Rust structs compiled into the engine, they’re defined at runtime as TOML schema files and stored as toml::Value. This means you can define new component types without recompiling the engine.

Entity IDs

Every entity gets a stable EntityId — a 64-bit integer that:

  • Is unique within a scene
  • Never gets recycled (monotonically increasing)
  • Persists across save/load cycles
  • Is deterministic (the same scene always produces the same IDs)

Internally, Flint maintains a bidirectional map (BiMap) between EntityId values and hecs Entity handles. This allows efficient lookup in both directions.

#![allow(unused)]
fn main() {
// From flint-core
pub struct EntityId(pub u64);
}

When loading a saved scene, the ID counter is adjusted to be higher than any existing ID, preventing collisions when new entities are created.

Named Entities

While entity IDs are the internal identifier, entities in Flint are also named. The name is the key in the scene file:

[entities.front_door]     # "front_door" is the name
archetype = "door"

Names must be unique within a scene. They’re used in:

  • CLI commands: --name "front_door"
  • Parent references: parent = "main_hall"
  • Query results
  • Constraint violation messages

Components as Dynamic Data

In most ECS implementations, components are Rust structs:

#![allow(unused)]
fn main() {
// NOT how Flint works
struct Transform { position: Vec3, rotation: Vec3 }
}

In Flint, components are toml::Value maps, defined by schema files:

# schemas/components/transform.toml
[component.transform]
description = "Position and rotation in 3D space"

[component.transform.fields]
position = { type = "vec3", default = [0, 0, 0] }
rotation = { type = "vec3", default = [0, 0, 0] }
scale = { type = "vec3", default = [1, 1, 1] }

This design trades some type safety and performance for flexibility — archetypes and components can be defined, modified, and extended without touching Rust code.

Parent-Child Relationships

Entities can form hierarchies. A child entity references its parent by name:

[entities.kitchen]
archetype = "room"
parent = "main_hall"

The ECS layer tracks these relationships, enabling:

  • Hierarchical transforms (child positions are relative to parent)
  • Tree queries (“find all children of main_hall”)
  • Cascading operations (deleting a parent removes children)

Archetypes

An archetype is a named bundle of components that defines an entity “type”:

# schemas/archetypes/door.toml
[archetype.door]
description = "A door entity"
components = ["transform", "door"]

[archetype.door.defaults.door]
style = "hinged"
locked = false

When you create an entity with --archetype door, Flint ensures it has the required components and fills in defaults for any missing values.

Archetypes are not rigid types — an entity can have components beyond what its archetype specifies. The archetype defines the minimum set.

Working with Entities via CLI

# Create an entity
flint entity create --archetype door --name "vault_door" \
    --scene levels/dungeon.scene.toml \
    --schemas schemas \
    --props '{"transform":{"position":[0,0,0]},"door":{"locked":true}}'

# Delete an entity
flint entity delete --name "vault_door" --scene levels/dungeon.scene.toml

# List entities in a scene
flint query "entities" --scene levels/dungeon.scene.toml

# Filter by archetype
flint query "entities where archetype == 'door'" --scene levels/dungeon.scene.toml

Further Reading

  • Schemas — how components and archetypes are defined
  • Scenes — how entities are serialized to TOML
  • Queries — how to filter and inspect entities

Schemas

Schemas define the structure of your game world. They specify what components exist, what fields they contain, and how they bundle together into archetypes. Schemas are TOML files stored in the schemas/ directory of your project.

Component Schemas

A component schema defines a reusable data type. Components live in schemas/components/:

# schemas/components/door.toml
[component.door]
description = "A door that can connect spaces"

[component.door.fields]
style = { type = "enum", values = ["hinged", "sliding", "rotating"], default = "hinged" }
locked = { type = "bool", default = false }
open_angle = { type = "f32", default = 90.0, min = 0.0, max = 180.0 }

Field Types

TypeDescriptionExample
boolBooleantrue / false
i3232-bit integer42
f3232-bit float3.14
stringText string"hello"
vec33D vector (array of 3 floats)[1.0, 2.0, 3.0]
enumOne of a set of string values"hinged"
entity_refReference to another entity by name"main_hall"

Field Constraints

Fields can include validation constraints:

open_angle = { type = "f32", default = 90.0, min = 0.0, max = 180.0 }
required_key = { type = "entity_ref", optional = true }
  • default — value used when not explicitly set
  • min / max — numeric range bounds
  • optional — whether the field can be omitted (defaults to false)
  • values — valid options for enum types

Built-in Components

Flint ships with several built-in component schemas:

Transform

# schemas/components/transform.toml
[component.transform]
description = "Position and rotation in 3D space"

[component.transform.fields]
position = { type = "vec3", default = [0, 0, 0] }
rotation = { type = "vec3", default = [0, 0, 0] }
scale = { type = "vec3", default = [1, 1, 1] }

Bounds

# schemas/components/bounds.toml
[component.bounds]
description = "Axis-aligned bounding box"

[component.bounds.fields]
min = { type = "vec3", default = [0, 0, 0] }
max = { type = "vec3", default = [10, 4, 10] }

Door

# schemas/components/door.toml
[component.door]
description = "A door that can connect spaces"

[component.door.fields]
style = { type = "enum", values = ["hinged", "sliding", "rotating"], default = "hinged" }
locked = { type = "bool", default = false }
open_angle = { type = "f32", default = 90.0, min = 0.0, max = 180.0 }

Material

# schemas/components/material.toml
[component.material]
description = "PBR material properties"

[component.material.fields]
texture = { type = "string", default = "", optional = true }
roughness = { type = "f32", default = 0.5, min = 0.0, max = 1.0 }
metallic = { type = "f32", default = 0.0, min = 0.0, max = 1.0 }
color = { type = "vec3", default = [1.0, 1.0, 1.0] }
emissive = { type = "vec3", default = [0.0, 0.0, 0.0] }

Rigidbody

# schemas/components/rigidbody.toml
[component.rigidbody]
description = "Physics rigid body"

[component.rigidbody.fields]
body_type = { type = "enum", values = ["static", "dynamic", "kinematic"], default = "static" }
mass = { type = "f32", default = 1.0, min = 0.0 }
gravity_scale = { type = "f32", default = 1.0 }

Collider

# schemas/components/collider.toml
[component.collider]
description = "Physics collision shape"

[component.collider.fields]
shape = { type = "enum", values = ["box", "sphere", "capsule"], default = "box" }
size = { type = "vec3", default = [1.0, 1.0, 1.0] }
friction = { type = "f32", default = 0.5, min = 0.0, max = 1.0 }

Character Controller

# schemas/components/character_controller.toml
[component.character_controller]
description = "First-person character controller"

[component.character_controller.fields]
move_speed = { type = "f32", default = 5.0, min = 0.0 }
jump_force = { type = "f32", default = 7.0, min = 0.0 }
height = { type = "f32", default = 1.8, min = 0.1 }
radius = { type = "f32", default = 0.4, min = 0.1 }
camera_mode = { type = "enum", values = ["first_person", "orbit"], default = "first_person" }

Sprite

# schemas/components/sprite.toml
[component.sprite]
description = "Billboard sprite rendered as a camera-facing quad"

[component.sprite.fields]
texture = { type = "string", default = "", description = "Sprite sheet texture name" }
width = { type = "f32", default = 1.0, min = 0.01, description = "World-space width" }
height = { type = "f32", default = 1.0, min = 0.01, description = "World-space height" }
frame = { type = "i32", default = 0, min = 0, description = "Current frame index" }
frames_x = { type = "i32", default = 1, min = 1, description = "Columns in sprite sheet" }
frames_y = { type = "i32", default = 1, min = 1, description = "Rows in sprite sheet" }
anchor_y = { type = "f32", default = 0.0, description = "Vertical anchor (0=bottom, 0.5=center)" }
fullbright = { type = "bool", default = true, description = "Bypass PBR lighting" }
visible = { type = "bool", default = true }

The sprite component is used for billboard sprites that always face the camera. See Rendering: Billboard Sprites for details on the rendering pipeline.

Archetype Schemas

Archetypes bundle components together with defaults. They live in schemas/archetypes/:

# schemas/archetypes/room.toml
[archetype.room]
description = "A room or enclosed space"
components = ["transform", "bounds"]

[archetype.room.defaults.bounds]
min = [0, 0, 0]
max = [10, 4, 10]

The components array lists which component schemas an entity of this archetype requires. The defaults section provides values used when a component field isn’t explicitly set.

Built-in Archetypes

ArchetypeComponentsDescription
roomtransform, boundsAn enclosed space
doortransform, doorA door entity
furnituretransform, boundsA piece of furniture
charactertransformA character or NPC
walltransform, bounds, materialA wall surface
floortransform, bounds, materialA floor surface
ceilingtransform, bounds, materialA ceiling surface
pillartransform, bounds, materialA structural pillar
playertransform, character_controller, rigidbody, colliderPlayer-controlled entity

Introspecting Schemas

Use the CLI to inspect schema definitions:

# Show a component schema
flint schema door --schemas schemas

# Show an archetype schema
flint schema room --schemas schemas

This outputs the component fields, types, defaults, and constraints — useful for both humans exploring the schema and AI agents discovering what fields are available.

Creating Custom Schemas

To add a new component:

  1. Create a file in schemas/components/:
# schemas/components/health.toml
[component.health]
description = "Hit points and damage tracking"

[component.health.fields]
max_hp = { type = "i32", default = 100, min = 1 }
current_hp = { type = "i32", default = 100, min = 0 }
armor = { type = "f32", default = 0.0, min = 0.0, max = 1.0 }
  1. Reference it in an archetype:
# schemas/archetypes/enemy.toml
[archetype.enemy]
description = "A hostile NPC"
components = ["transform", "health"]

[archetype.enemy.defaults.health]
max_hp = 50
current_hp = 50
  1. Use it in a scene:
[entities.goblin]
archetype = "enemy"

[entities.goblin.transform]
position = [10, 0, 5]

[entities.goblin.health]
max_hp = 30
current_hp = 30
armor = 0.1

No engine recompilation needed — schemas are loaded at runtime from the TOML files.

Game Project Schemas

Games can define their own schemas that extend or override the engine’s built-in schemas. The --schemas flag accepts multiple paths, with later paths taking priority:

# From a game project root (engine at engine/)
cargo run --manifest-path engine/Cargo.toml --bin flint-player -- \
  scenes/arena.scene.toml \
  --schemas engine/schemas \
  --schemas schemas

In this example, engine/schemas/ provides the engine’s built-in components (transform, material, rigidbody, etc.) and the game’s own schemas/ adds game-specific components (health, weapon, enemy AI). If both directories define a component with the same name, the game’s definition wins.

Game Project Directory Structure

Game projects live in their own repositories with the engine included as a git subtree:

my_game/                    (standalone git repo)
├── engine/                 (git subtree ← Flint repo)
│   ├── crates/
│   ├── schemas/            (engine schemas)
│   └── Cargo.toml
├── schemas/
│   ├── components/
│   │   ├── health.toml
│   │   ├── weapon.toml
│   │   └── enemy_ai.toml
│   └── archetypes/
│       ├── enemy.toml
│       └── pickup.toml
├── scripts/
│   ├── enemy_ai.rhai
│   ├── weapon.rhai
│   └── hud.rhai
├── scenes/
│   └── level_1.scene.toml
├── sprites/
│   └── imp.png
└── audio/
    ├── shotgun.ogg
    └── imp_death.ogg

This separation keeps game-specific data out of the engine directory, allowing multiple games to share the same engine schemas while defining their own components and archetypes. See Building a Game Project for the full setup guide.

Further Reading

Scenes

A scene in Flint is a TOML file that describes a collection of entities and their data. Scenes are the primary unit of content — they’re what you load, save, query, validate, and render.

File Format

Scene files use the .scene.toml extension and have two sections:

# Metadata
[scene]
name = "The Rusty Flint Tavern"
version = "1.0"
description = "A showcase scene demonstrating Flint engine capabilities"

# Entity definitions
[entities.main_hall]
archetype = "room"
# ...

The [scene] Table

FieldRequiredDescription
nameyesHuman-readable scene name
versionyesFormat version (currently “1.0”)
descriptionnoOptional description
input_confignoPath or name of an input config file for this scene (see Input System)

The [entities.*] Tables

Each entity is a table under [entities], keyed by its unique name:

[entities.front_door]
archetype = "door"
parent = "main_hall"

[entities.front_door.transform]
position = [0, 0, 5]

[entities.front_door.door]
style = "hinged"
locked = false
open_angle = 90.0

Top-level fields:

  • archetype — the archetype schema name (optional but recommended)
  • parent — name of the parent entity (optional)

Component tables are nested under the entity. Each component name (e.g., transform, door, bounds) corresponds to a schema in schemas/components/.

Scene Operations

Creating a Scene

flint scene create levels/tavern.scene.toml --name "The Tavern"

Listing Scenes

flint scene list

Getting Scene Info

flint scene info levels/tavern.scene.toml

Loading and Saving

The flint-scene crate handles serialization. Scenes are loaded into the ECS world as entities with dynamic components, and saved back to TOML with stable ordering.

When a scene is loaded:

  1. The TOML is parsed into a scene structure
  2. Each entity definition creates an ECS entity with a stable EntityId
  3. Parent-child relationships are established
  4. The entity ID counter is adjusted to be above any existing ID (preventing collisions on subsequent creates)

When a scene is saved:

  1. All entities are serialized to their TOML representation
  2. Component data is written as nested tables
  3. Parent references use entity names (not internal IDs)

Reload Behavior

Scene reload is a full re-parse. When flint serve --watch detects a file change:

  1. The entire scene file is re-read and re-parsed
  2. The old world state is replaced with the new one
  3. The renderer picks up the new state on the next frame

This approach is simple and correct — there’s no incremental diffing that could get out of sync. For the scene sizes Flint targets, re-parsing is fast enough.

Scene as Source of Truth

A key design decision: the scene file is the source of truth, not the in-memory state. This means:

  • You can edit the file with any text editor
  • AI agents can write TOML directly
  • Git diffs show exactly what changed
  • No hidden state lives only in memory

The CLI commands (entity create, entity delete) modify the scene file, and the in-memory world loads from that file. The viewer watches the file, not the internal state.

Example: The Showcase Scene

The demo scene demo/showcase.scene.toml demonstrates the full format:

[scene]
name = "The Rusty Flint Tavern"
version = "1.0"
description = "A showcase scene demonstrating Flint engine capabilities"

# Rooms - rendered as blue wireframe boxes
[entities.main_hall]
archetype = "room"

[entities.main_hall.transform]
position = [0, 0, 0]

[entities.main_hall.bounds]
min = [-7, 0, -5]
max = [7, 4, 5]

# Doors - rendered as orange boxes
[entities.front_entrance]
archetype = "door"
parent = "main_hall"

[entities.front_entrance.transform]
position = [0, 0, 5]

[entities.front_entrance.door]
style = "hinged"
locked = false
open_angle = 90.0

# Furniture - rendered as green boxes
[entities.bar_counter]
archetype = "furniture"
parent = "main_hall"

[entities.bar_counter.transform]
position = [-4, 0, 0]

[entities.bar_counter.bounds]
min = [-1.5, 0, -3]
max = [0, 1.2, 3]

# Characters - rendered as yellow boxes
[entities.bartender]
archetype = "character"
parent = "main_hall"

[entities.bartender.transform]
position = [-5, 0, 0]

This scene defines 4 rooms, 4 doors, 9 pieces of furniture, and 6 characters — all in readable, diffable TOML.

Prefabs

Prefabs are reusable entity group templates that reduce scene file duplication. A prefab defines a set of entities in a .prefab.toml file, and scenes instantiate them with variable substitution and optional overrides.

Defining a Prefab

Prefab files live in the prefabs/ directory and follow the same entity format as scenes, with a [prefab] metadata header:

[prefab]
name = "kart"
description = "Racing kart with body, wheels, and driver"

[entities.kart]

[entities.kart.transform]
position = [0, 0, 0]

[entities.kart.model]
asset = "kart_body"

[entities.wheel_fl]
parent = "${PREFIX}_kart"

[entities.wheel_fl.transform]
position = [-0.4, 0.15, 0.55]

[entities.wheel_fl.model]
asset = "kart_wheel"

All string values containing ${PREFIX} are substituted with the instance prefix at load time. Entity names are automatically prefixed (e.g., kart becomes player_kart when the prefix is "player").

Using Prefabs in a Scene

Scenes reference prefabs in a [prefabs] section:

[prefabs.player]
template = "kart"
prefix = "player"

[prefabs.player.overrides.kart.transform]
position = [0, 0, 0]

[prefabs.ai1]
template = "kart"
prefix = "ai1"

[prefabs.ai1.overrides.kart.transform]
position = [3, 0, -5]

Each prefab instance specifies:

  • template — the prefab name (matches the .prefab.toml filename without extension)
  • prefix — substituted for ${PREFIX} in all string values and prepended to entity names
  • overrides — per-entity component field overrides (deep-merged with the template)

Override Deep Merge

Overrides are merged at the field level, not the component level. If a prefab template defines a component with five fields and an override specifies one field, only that one field is replaced — the other four are preserved from the template.

Path Resolution

The loader searches for prefab templates in:

  1. <scene_directory>/prefabs/
  2. <scene_directory>/../prefabs/

This means a prefabs/ directory at the project root is found when loading scenes from scenes/.

Previewing Prefabs

Use the CLI to visually inspect a prefab template:

flint prefab view prefabs/kart.prefab.toml --schemas schemas

Splines

Splines define smooth paths through 3D space using Catmull-Rom interpolation. They’re used for track layouts, camera paths, and procedural geometry generation.

Spline Component

Attach a spline to an entity with the spline component:

[entities.track_path]

[entities.track_path.spline]
source = "oval_plus.spline.toml"

The engine loads the .spline.toml file, samples it into a dense point array stored as the spline_data ECS component, and makes it available for script queries via the Spline API.

Spline Meshes

The spline_mesh component generates geometry by sweeping a rectangular cross-section along a spline:

[entities.road_surface]

[entities.road_surface.spline_mesh]
spline = "track_path"
width = 12.0
height = 0.3
offset_y = -0.15

[entities.road_surface.material]
base_color = [0.3, 0.3, 0.3]
roughness = 0.8

One spline can feed multiple mesh entities (road surface, walls, guardrails) with different cross-section dimensions and materials.

Further Reading

Queries

Flint’s query system provides a SQL-inspired language for filtering and inspecting entities. Queries are parsed by a PEG grammar (pest) and executed against the ECS world.

Grammar

The query language is defined in crates/flint-query/src/grammar.pest:

query     = { resource ~ (where_clause)? }
resource  = { "entities" | "components" }
where_clause = { "where" ~ condition }
condition = { field ~ operator ~ value }
field     = { identifier ~ ("." ~ identifier)* }
operator  = { "==" | "!=" | "contains" | ">=" | "<=" | ">" | "<" }
value     = { string | number | boolean }

Whitespace is ignored between tokens. The where keyword is case-insensitive.

Resources

Two resource types can be queried:

ResourceDescription
entitiesReturns entity data (name, archetype, components)
componentsReturns component definitions from the schema registry

Operators

OperatorDescriptionValue Types
==Exact equalitystring, number, boolean
!=Not equalstring, number, boolean
>Greater thannumber
<Less thannumber
>=Greater than or equalnumber
<=Less than or equalnumber
containsSubstring matchstring

Field Paths

Fields use dot notation to access nested values:

PatternMeaning
archetypeThe entity’s archetype name
nameThe entity’s name
door.lockedThe locked field of the door component
transform.positionThe position field of the transform component

Examples:

# Top-level entity properties
flint query "entities where archetype == 'door'"
flint query "entities where name contains 'wall'"

# Component fields
flint query "entities where door.locked == true"
flint query "entities where audio_source.volume > 0.5"
flint query "entities where material.roughness >= 0.8"
flint query "entities where collider.shape == 'box'"

Value Types

TypeSyntaxExamples
StringSingle or double quotes'door', "wall"
NumberIntegers or decimals, optional negative42, 3.14, -1.5
BooleanUnquoted keywordstrue, false

Use in Constraints

Queries power the constraint system. Each constraint rule includes a query field that selects which entities the constraint applies to:

[[constraint]]
name = "doors_have_transform"
query = "entities where archetype == 'door'"
severity = "error"
message = "Door '{name}' is missing a transform component"

[constraint.kind]
type = "required_component"
archetype = "door"
component = "transform"

The query selects all door entities, and the constraint checks that each one has a transform component. See Constraints for details.

CLI Usage

# Basic query
flint query "entities where archetype == 'door'" --scene levels/tavern.scene.toml

# JSON output for machine consumption
flint query "entities" --scene levels/tavern.scene.toml --format json

# TOML output
flint query "entities where door.locked == true" --scene levels/tavern.scene.toml --format toml

# Specify schemas directory
flint query "entities" --scene levels/tavern.scene.toml --schemas schemas

Limitations

  • Conditions are currently single-clause (one field-operator-value comparison per query at the parser level)
  • Boolean combinators (and, or, not) are part of the grammar design but not yet implemented in the parser
  • Queries operate on in-memory ECS state, not directly on TOML files
  • Performance is linear in entity count (queries scan all entities matching the resource type)

Further Reading

Constraints

Constraints are declarative validation rules that define what a correct scene looks like. They live in TOML files under schemas/constraints/ and are checked by flint validate.

Constraint File Format

Each constraint file can contain multiple [[constraint]] entries:

[[constraint]]
name = "doors_have_transform"
description = "Every door must have a transform component"
query = "entities where archetype == 'door'"
severity = "error"
message = "Door '{name}' is missing a transform component"

[constraint.kind]
type = "required_component"
archetype = "door"
component = "transform"
FieldDescription
nameUnique identifier for the constraint
descriptionHuman-readable explanation of what the rule checks
queryFlint query that selects which entities this constraint applies to
severity"error" (blocks) or "warning" (advisory)
messageViolation message. {name} is replaced with the entity name

Constraint Kinds

required_component

Ensures that entities matching the query have a specific component:

[constraint.kind]
type = "required_component"
archetype = "door"
component = "transform"

Use case: every door must have a position in the world.

required_child

Ensures that entities have a child entity of a specific archetype:

[constraint.kind]
type = "required_child"
archetype = "room"
child_archetype = "door"

Use case: every room must have at least one door.

value_range

Checks that a numeric field falls within a valid range:

[constraint.kind]
type = "value_range"
field = "door.open_angle"
min = 0.0
max = 180.0

Use case: door angles must be physically possible.

reference_valid

Checks that an entity reference field points to an existing entity:

[constraint.kind]
type = "reference_valid"
field = "door.target_room"

Use case: a door’s target room must actually exist in the scene.

query_rule

The most flexible kind — validates that a query returns the expected number of results:

[constraint.kind]
type = "query_rule"
rule_query = "entities where archetype == 'player'"
expected = "exactly_one"

Use case: a playable scene must have exactly one player entity.

Auto-Fix Strategies

Some constraint violations can be fixed automatically. The fix section defines how:

  • set_default — set a missing field to its schema default
  • add_child — create a child entity with the required archetype
  • remove_invalid — remove entities that violate the constraint
  • assign_from_parent — copy a field value from the parent entity

Auto-fix runs in a loop: fix violations, re-validate, fix new violations. Cycle detection prevents infinite loops.

CLI Usage

# Check a scene for violations
flint validate levels/tavern.scene.toml

# JSON output for parsing
flint validate levels/tavern.scene.toml --format json

# Preview what auto-fix would change
flint validate levels/tavern.scene.toml --fix --dry-run

# Apply auto-fixes
flint validate levels/tavern.scene.toml --fix

# Specify a schemas directory
flint validate levels/tavern.scene.toml --schemas schemas

The exit code is 0 if all constraints pass, 1 if any errors are found. Warnings do not affect the exit code.

Real Example

From schemas/constraints/basics.toml:

[[constraint]]
name = "doors_have_transform"
description = "Every door must have a transform component"
query = "entities where archetype == 'door'"
severity = "error"
message = "Door '{name}' is missing a transform component"

[constraint.kind]
type = "required_component"
archetype = "door"
component = "transform"

[[constraint]]
name = "rooms_have_bounds"
description = "Every room must have a bounds component"
query = "entities where archetype == 'room'"
severity = "error"
message = "Room '{name}' is missing a bounds component"

[constraint.kind]
type = "required_component"
archetype = "room"
component = "bounds"

[[constraint]]
name = "door_angle_range"
description = "Door open angle must be between 0 and 180 degrees"
query = "entities where archetype == 'door'"
severity = "warning"
message = "Door '{name}' has an open_angle outside the valid range"

[constraint.kind]
type = "value_range"
field = "door.open_angle"
min = 0.0
max = 180.0

Further Reading

Assets

Flint uses a content-addressed asset system with SHA-256 hashing. Every imported file is identified by its content hash, which means identical files are automatically deduplicated and any change to a file produces a new, distinct hash.

Content Addressing

When you import a file, Flint computes its SHA-256 hash and stores it under a content-addressed path:

.flint/assets/<first-2-hex>/<full-hash>.<ext>

This means:

  • Deduplication — importing the same file twice stores it only once
  • Change detection — if a source file changes, its hash changes, and the new version is stored separately
  • Integrity — the hash verifies the file hasn’t been corrupted

Asset Catalog

The asset catalog is a searchable index of all imported assets. Each entry tracks:

  • Name — a human-friendly identifier (e.g., tavern_chair)
  • Hash — the SHA-256 content hash
  • Type — asset type (mesh, texture, material, etc.)
  • Tags — arbitrary labels for organization and filtering
  • Source path — where the file was originally imported from

Importing Assets

Use the CLI to import files into the asset store:

# Import a glTF model with name and tags
flint asset import models/chair.glb --name tavern_chair --tags furniture,medieval

# Browse the catalog
flint asset list --type mesh

# Check asset references in a scene
flint asset resolve levels/tavern.scene.toml --strategy strict

glTF/GLB Import

The flint-import crate provides full glTF/GLB support, extracting:

  • Meshes — vertex positions, normals, texture coordinates, and indices
  • Materials — PBR properties (base color, roughness, metallic, emissive)
  • Textures — embedded or referenced image files

Imported meshes are rendered by flint-render with full PBR shading.

Resolution Strategies

When a scene references assets, Flint can resolve them using different strategies:

StrategyBehavior
strictAll referenced assets must exist in the catalog. Missing assets are errors.
placeholderMissing assets are replaced with placeholder geometry. Useful during development.
ai_generateMissing assets are generated via AI providers (Flux, Meshy, ElevenLabs) and stored.
human_taskMissing assets produce task files for manual creation by an artist.
ai_then_humanGenerate with AI first, then produce review tasks for human approval.

The ai_generate, human_task, and ai_then_human strategies are part of the AI Asset Generation pipeline.

Asset Sidecar Files

Each asset in the catalog has a .asset.toml sidecar file storing metadata:

[asset]
name = "tavern_chair"
type = "mesh"
hash = "sha256:a1b2c3..."
source_path = "models/chair.glb"
tags = ["furniture", "medieval"]

Runtime Catalog Resolution

The player can load the asset catalog at startup for name-based asset resolution. When an entity references an asset by name, the resolution chain is:

  1. Look up the name in the AssetCatalog
  2. If found, resolve the content hash
  3. Load from the ContentStore path (.flint/assets/<hash>)
  4. Fall back to file-based loading if not in the catalog

This allows scenes to reference both pre-imported and AI-generated assets by name without hardcoding file paths.

Further Reading

Rendering

Flint uses wgpu 23 for cross-platform GPU rendering, providing physically-based rendering (PBR) with a Cook-Torrance BRDF, cascaded shadow mapping, and full glTF mesh support.

PBR Shading

The renderer implements a metallic-roughness PBR workflow based on the Cook-Torrance specular BRDF:

  • Base color — the surface albedo, optionally sampled from a texture
  • Roughness — controls specular highlight spread (0.0 = mirror, 1.0 = diffuse)
  • Metallic — interpolates between dielectric and metallic response
  • Emissive — self-illumination for light sources and glowing objects

Materials are defined in scene TOML via the material component, matching the fields in schemas/components/material.toml.

Shadow Mapping

Directional lights cast shadows via cascaded shadow maps. Multiple shadow cascades cover different distance ranges from the camera, giving high-resolution shadows close up and broader coverage at distance.

Toggle shadows at runtime with F4.

Camera Modes

The renderer supports two camera modes that share the same view/projection math:

ModeUsageControls
OrbitScene viewer (serve)Left-drag to orbit, right-drag to pan, scroll to zoom
First-personPlayer (play)WASD to move, mouse to look, Space to jump, Shift to sprint

The camera mode is determined by the entry point: serve uses orbit, play uses first-person. Both produce the same view and projection matrices.

glTF Mesh Rendering

Imported glTF models are rendered with their full mesh geometry and materials. The flint-import crate extracts meshes, materials, and textures from .glb/.gltf files, which the renderer draws with PBR shading.

Skinned Mesh Pipeline

For skeletal animation, the renderer provides a separate GPU pipeline that applies bone matrix skinning in the vertex shader. This avoids the 32-byte overhead of bone data on static geometry.

How it works:

  1. flint-import extracts joint indices and weights from glTF skins alongside the mesh data
  2. flint-animation evaluates keyframes and computes bone matrices each frame (local pose -> global hierarchy -> inverse bind matrix)
  3. The renderer uploads bone matrices to a storage buffer and applies them in the vertex shader

Key types:

  • SkinnedVertex — extends the standard vertex with joint_indices: [u32; 4] and joint_weights: [f32; 4] (6 attributes total vs. 4 for static geometry)
  • GpuSkinnedMesh — holds the vertex/index buffers, material, and a bone matrix storage buffer with its bind group
  • Skinned pipeline uses bind groups 0–3: transform, material, lights, and bones (storage buffer, read-only, vertex-visible)

Skinned meshes also cast shadows through a dedicated vs_skinned_shadow shader entry point that applies bone transforms before depth rendering.

Billboard Sprites

Billboard sprites are camera-facing quads used for 2D elements in 3D space — enemies, pickups, particle effects, and environmental details. They always face the camera, like classic Doom-style sprites.

The BillboardPipeline is a separate rendering pipeline from PBR, optimized for flat textured quads:

  • No vertex buffer — quad positions are generated procedurally from vertex_index (4 vertices per sprite)
  • Per-sprite uniform buffer — each sprite gets its own instance data (position, size, frame, anchor)
  • Binary alpha — the fragment shader uses discard for transparent pixels (avoids order-independent transparency complexity)
  • Sprite sheet animation — supports multi-frame sprite sheets via frame, frames_x, and frames_y fields
  • Render order — billboard sprites render after skinned meshes in the pipeline

Sprite Component

Attach a sprite to any entity with the sprite component:

[entities.imp]
archetype = "enemy"

[entities.imp.transform]
position = [10, 0, 5]

[entities.imp.sprite]
texture = "imp_spritesheet"
width = 1.5
height = 2.0
frames_x = 4
frames_y = 1
frame = 0
anchor_y = 0.0
fullbright = true
FieldTypeDefaultDescription
texturestring""Sprite sheet texture name (from sprites/ directory)
widthf321.0World-space width of the quad
heightf321.0World-space height of the quad
framei320Current frame index in the sprite sheet
frames_xi321Number of columns in the sprite sheet
frames_yi321Number of rows in the sprite sheet
anchor_yf320.0Vertical anchor point (0.0 = bottom, 0.5 = center)
fullbrightbooltrueIf true, bypasses PBR lighting (always fully lit)
visiblebooltrueWhether the sprite is rendered

Design Decisions

Billboard sprites use a separate pipeline rather than extending the PBR pipeline. This keeps the PBR shaders clean and allows sprites to opt out of lighting entirely (fullbright = true). The discard-based alpha approach is simple and avoids the significant complexity of order-independent transparency, at the cost of no partial transparency (pixels are either fully opaque or fully transparent).

Post-Processing

The renderer includes an HDR post-processing pipeline that applies bloom, tonemapping, and vignette as a final pass. See Post-Processing for full details.

When post-processing is active, all scene pipelines (PBR, skinned, billboard, particle, skybox) render to an Rgba16Float HDR intermediate buffer. A composite fullscreen pass then applies exposure, ACES tonemapping, gamma correction, and optional vignette to produce the final sRGB output.

Configure post-processing per-scene via the [post_process] TOML block, or override with CLI flags (--no-postprocess, --bloom-intensity, --bloom-threshold, --exposure).

PBR Materials

PBR material showcase — varying roughness and metallic values

PBR materials with varying roughness and metallic values. Left to right: rough dielectric, smooth dielectric, rough metal, polished metal.

Debug Visualization

The renderer provides six debug visualization modes, cycled with F1:

ModeDescription
PBRStandard Cook-Torrance shading (default)
WireframeEdge lines only, no fill
NormalsWorld-space surface normals mapped to RGB
DepthLinearized depth as grayscale
UV CheckerUV coordinates as a procedural checkerboard
UnlitAlbedo color only, no lighting
Metal/RoughMetallic (red channel) and roughness (green channel)

Additional debug overlays:

  • Wireframe overlay (F2 in viewer, --wireframe-overlay in render) — draws edges on top of solid shading
  • Normal arrows (F3 in viewer, --show-normals in render) — draws face-normal direction arrows

Wireframe debug mode

Wireframe debug mode showing mesh topology.

Normal visualization

Normal debug mode mapping world-space normals to RGB channels.

Viewer vs Headless

The renderer operates in two modes:

Viewer mode (flint serve --watch) opens an interactive window with:

  • Real-time PBR rendering
  • egui inspector panel (entity tree, component editor, constraint overlay)
  • Hot-reload: edit the scene TOML and the viewer updates automatically
  • Debug rendering modes (cycle with F1)

Headless mode (flint render) renders to a PNG file without opening a window — useful for CI pipelines and automated screenshots:

flint render levels/tavern.scene.toml --output preview.png --width 1920 --height 1080

Technology

The rendering stack uses winit 0.30’s ApplicationHandler trait pattern (not the older event-loop closure style). wgpu 23 provides the GPU abstraction, selecting the best available backend (Vulkan, Metal, or DX12) at runtime.

Further Reading

Post-Processing

Flint includes an HDR post-processing pipeline that transforms the raw scene render into polished final output with bloom, SSAO, fog, volumetric lighting, tonemapping, and vignette effects.

How It Works

Instead of rendering directly to the screen, the scene is drawn to an intermediate HDR buffer (Rgba16Float format) that can store values brighter than 1.0. A series of fullscreen passes then process this buffer:

Scene render         SSAO        Volumetric    Bloom chain       Composite pass
(PBR, skinned,   →  depth-based  shadow-based  downsample    →   exposure
 billboard,         AO texture   god rays      upsample          ACES tonemapping
 particles,                                                      gamma correction
 skybox)                                                         fog + vignette
     ↓                  ↓            ↓              ↓                ↓
 Rgba16Float       AO texture   vol texture    bloom texture    sRGB surface
 HDR buffer        (darkening)  (light shafts) (bright halos)  (final output)

All scene pipelines — PBR, skinned mesh, billboard sprite, particle, and skybox — render to the HDR buffer when post-processing is active. The PBR shader’s built-in tonemapping is automatically disabled so it outputs linear HDR values for the composite pass to process.

Bloom

Bloom creates the soft glow around bright light sources — emissive materials, fire particles, bright specular highlights. The implementation uses the technique from Call of Duty: Advanced Warfare:

  1. Threshold — pixels brighter than bloom_threshold are extracted
  2. Downsample — a 5-level mip chain progressively halves the resolution using a 13-tap filter
  3. Upsample — each mip level is upsampled with a 9-tap tent filter and additively blended back up the chain
  4. Composite — the final bloom texture is mixed into the scene at bloom_intensity strength

Bloom enabled — bright sources produce soft halos

Post-processing enabled: bloom creates halos around emissive surfaces and bright lights.

Bloom disabled — same scene with raw PBR output

Post-processing disabled: the same scene rendered with shader-level tonemapping only.

The mip chain depth is calculated as floor(log2(min(width, height))) - 3, capped at 5 levels, ensuring the smallest mip is at least 8x8 pixels.

SSAO (Screen-Space Ambient Occlusion)

SSAO darkens crevices, corners, and areas where surfaces meet, adding depth and realism to a scene without requiring extra light sources. The implementation samples the depth buffer around each pixel to estimate how much ambient light would be blocked by nearby geometry.

FieldTypeDefaultDescription
ssao_enabledbooltrueEnable SSAO
ssao_radiusf320.5Sample radius in world units (larger = wider darkening)
ssao_intensityf321.0Occlusion strength (higher = darker crevices)

Fog

Distance-based fog blends a configurable fog color into the scene based on pixel depth. Height-based falloff can be layered on top so fog is thicker near the ground and thins out at higher elevations.

FieldTypeDefaultDescription
fog_enabledboolfalseEnable distance fog
fog_color[f32; 3][0.7, 0.75, 0.82]Fog color (linear RGB)
fog_densityf320.02Exponential density factor
fog_startf325.0Distance where fog begins
fog_endf32100.0Distance where fog reaches full opacity
fog_height_enabledboolfalseEnable height-based falloff
fog_height_fallofff320.1How quickly fog thins with altitude
fog_height_originf320.0World Y where fog is thickest

Volumetric Lighting (God Rays)

Volumetric lighting simulates light scattering through participating media (dust, fog, haze), producing visible shafts of light (god rays). The effect ray-marches from each pixel toward the camera, sampling the shadow map at each step to determine whether that point in space is lit or in shadow.

How it works

  1. For each screen pixel, reconstruct its world position from the depth buffer
  2. March volumetric_samples steps along the view ray from the pixel back toward the camera
  3. At each step, project the position into shadow-map space and sample the cascaded shadow map
  4. Accumulate light contribution where the sample is not in shadow, applying exponential decay
  5. The resulting volumetric texture is additively blended into the scene during the composite pass

Because volumetric lighting depends on the shadow map, it requires at least one directional light with shadows enabled. The effect is disabled when shadows are off.

Per-light configuration

Each directional light can control its volumetric contribution independently via its light component:

[entities.sun.light]
type = "directional"
direction = [0.4, 0.6, 0.05]
color = [1.0, 0.92, 0.75]
intensity = 6.0
volumetric_intensity = 4.0               # god ray brightness (0 = no rays)
volumetric_color = [1.0, 0.88, 0.6]      # tint for the light shafts
Light fieldTypeDefaultDescription
volumetric_intensityf320.0Per-light god ray strength (0 = disabled for this light)
volumetric_color[f32; 3]light colorTint color for the shafts from this light

Global scene settings

The [post_process] block controls the overall volumetric pass:

FieldTypeDefaultDescription
volumetric_enabledboolfalseEnable volumetric lighting
volumetric_samplesu3232Ray-march steps per pixel (higher = smoother, more expensive)
volumetric_densityf321.0Scattering density multiplier
volumetric_max_distancef32100.0Maximum ray-march distance from camera
volumetric_decayf320.98Exponential decay per step (closer to 1.0 = shafts extend further)

Example: dungeon window

[post_process]
volumetric_enabled = true
volumetric_samples = 64
volumetric_density = 30.0
volumetric_max_distance = 15.0
volumetric_decay = 0.998
exposure = 2.5

[entities.sun.light]
type = "directional"
direction = [0.4, 0.4, 0.05]
color = [1.0, 0.92, 0.75]
intensity = 6.0
volumetric_intensity = 4.0
volumetric_color = [1.0, 0.88, 0.6]

High volumetric_density with a short volumetric_max_distance and decay close to 1.0 produces thick, concentrated shafts — good for dusty interiors. For outdoor haze, use lower density and longer distance.

Dither

Subtle dithering reduces color banding in gradients (skies, fog falloff, smooth surfaces). A blue-noise pattern is applied during the composite pass.

FieldTypeDefaultDescription
dither_enabledboolfalseEnable dithering
dither_intensityf320.03Dither strength (subtle values like 0.02–0.05 work best)

Scene Configuration

Add a [post_process] block to your scene TOML to configure per-scene settings:

[post_process]
bloom_enabled = true
bloom_intensity = 0.04
bloom_threshold = 1.0
ssao_enabled = true
ssao_radius = 0.5
ssao_intensity = 1.0
fog_enabled = true
fog_density = 0.02
fog_color = [0.7, 0.75, 0.82]
volumetric_enabled = false
vignette_enabled = true
vignette_intensity = 0.3
exposure = 1.0

All fields are optional — omitted values use their defaults.

CLI Flags

Override post-processing settings from the command line:

# Disable all post-processing
flint render scene.toml --no-postprocess

# Adjust bloom
flint render scene.toml --bloom-intensity 0.08 --bloom-threshold 0.8

# Adjust exposure
flint render scene.toml --exposure 1.5

# SSAO
flint render scene.toml --ssao-radius 0.5 --ssao-intensity 1.0

# Fog
flint render scene.toml --fog-density 0.02 --fog-color 0.7,0.75,0.82 --fog-height-falloff 0.1

# Volumetric lighting
flint render scene.toml --volumetric-density 1.0 --volumetric-samples 32

# Dither
flint render scene.toml --dither-intensity 0.03

# Combine flags
flint play scene.toml --bloom-intensity 0.1 --exposure 1.2 --volumetric-density 5.0
FlagDescription
--no-postprocessDisable the entire post-processing pipeline
--no-shadowsDisable shadow mapping (also disables volumetric)
--bloom-intensity <f32>Override bloom intensity
--bloom-threshold <f32>Override bloom brightness threshold
--exposure <f32>Override exposure multiplier
--ssao-radius <f32>Override SSAO sample radius
--ssao-intensity <f32>Override SSAO strength
--fog-density <f32>Override fog density (0 disables fog)
--fog-color <r,g,b>Override fog color
--fog-height-falloff <f32>Enable height fog with given falloff
--volumetric-density <f32>Override volumetric density (0 disables)
--volumetric-samples <u32>Override volumetric ray-march steps
--dither-intensity <f32>Override dither strength

CLI flags take precedence over scene TOML settings.

Runtime Toggles

During gameplay (flint play / flint edit), toggle post-processing effects with keyboard shortcuts:

KeyAction
F5Toggle bloom on/off
F6Toggle entire post-processing pipeline on/off
F7Toggle SSAO on/off
F8Toggle fog on/off
F9Toggle dither on/off
F10Toggle volumetric lighting on/off

When post-processing is toggled off at runtime, the PBR shader’s built-in ACES tonemapping and gamma correction are automatically restored as a fallback path. This means the scene always looks correct regardless of the pipeline state.

Shader Integration

When the post-processing pipeline is active, the engine sets enable_tonemapping = 0 in the PBR uniform buffer, forcing shaders to output raw linear HDR values. The composite pass then applies:

  1. SSAO — darkens ambient-occluded areas using the AO texture
  2. Volumetric — additively blends god ray light shafts
  3. Exposure — multiplies all color values by the exposure setting
  4. ACES tonemapping — maps HDR values to displayable range using the ACES filmic curve
  5. Fog — blends fog color based on depth and optional height falloff
  6. Gamma correction — converts linear light to sRGB
  7. Dither — applies subtle noise to reduce banding
  8. Vignette — darkens screen edges for a cinematic look

When post-processing is disabled (via --no-postprocess or F6), the shader handles tonemapping and gamma internally. This dual-path design ensures backward compatibility with scenes that don’t use post-processing.

Design Decisions

  • Rgba16Float for the HDR buffer provides sufficient precision for bloom extraction without the memory cost of Rgba32Float
  • Progressive downsample/upsample (rather than a single Gaussian blur) produces wide, natural-looking bloom cheaply
  • 1x1 black fallback texture when bloom is disabled avoids conditional bind group creation
  • Resize handlingPostProcessResources are recreated on window resize since the HDR texture and bloom mip chain are resolution-dependent
  • Extended shadow depth — the shadow frustum’s depth range is extended beyond the camera frustum so off-screen casters (ceilings, walls behind the camera) are captured in the shadow map, which is critical for correct volumetric shafts in enclosed spaces

Further Reading

Audio

Flint’s audio system provides spatial 3D sound via the flint-audio crate, built on Kira 0.11. Sounds can be positioned in 3D space with distance attenuation, played as ambient loops, or triggered by game events like collisions.

Spatial Audio

Spatial sounds are attached to entities via the audio_source component. The sound’s volume attenuates with distance from the listener (the player camera):

  • min_distance — full volume within this radius
  • max_distance — silence beyond this radius
  • Volume falls off smoothly between the two

The listener position and orientation are updated each frame to match the first-person camera, so sounds pan and attenuate as you move through the scene.

Ambient Loops

Non-spatial sounds play on the main audio track at constant volume regardless of listener position. Set spatial = false on an audio_source to use this mode — useful for background music, ambient atmosphere, and UI sounds.

Audio Schemas

Three component schemas define audio behavior:

audio_source (audio_source.toml) — a sound attached to an entity:

FieldTypeDefaultDescription
filestringPath to audio file (relative to scene directory)
volumef321.0Playback volume (0.0–2.0)
pitchf321.0Playback speed/pitch (0.1–4.0)
loopboolfalseLoop the sound continuously
spatialbooltrue3D positioned (uses entity transform)
min_distancef321.0Distance at full volume
max_distancef3225.0Distance at silence
autoplaybooltrueStart playing on scene load

audio_listener (audio_listener.toml) — marks which entity receives audio:

FieldTypeDefaultDescription
activebooltrueWhether this listener is active

audio_trigger (audio_trigger.toml) — event-driven sounds:

FieldTypeDefaultDescription
on_collisionstringSound to play on collision start
on_interactstringSound to play on player interaction
on_enterstringSound when entering a trigger volume
on_exitstringSound when exiting a trigger volume

Dynamic Parameter Sync

Audio source parameters (volume and pitch) can be changed at runtime via set_field() and the engine automatically syncs changes to the playing audio each frame. This enables dynamic audio effects like engine RPM simulation or distance-based volume curves:

#![allow(unused)]
fn main() {
// Adjust engine sound pitch based on speed
let rpm_ratio = speed / max_speed;
set_field(engine_sound, "audio_source", "pitch", 0.5 + rpm_ratio * 1.5);
set_field(engine_sound, "audio_source", "volume", 0.3 + rpm_ratio * 0.7);
}

Changes are applied with a 16ms tween for smooth transitions (no clicks or pops).

Scene Transition Behavior

When a scene transition occurs (via load_scene() or reload_scene()), all playing sounds are explicitly stopped with a short fade-out before the old scene is unloaded. This prevents audio bleed between scenes — sounds from the previous scene won’t continue playing into the new one.

Architecture

The audio system has three main components:

  • AudioEngine — wraps Kira’s AudioManager, handles sound file loading, listener positioning, and spatial track creation. Sounds route through spatial tracks for 3D positioning or the main track for ambient playback.
  • AudioSync — bridges TOML audio_source components to Kira spatial tracks. Discovers new audio entities each frame and updates spatial positions from entity transforms.
  • AudioTrigger — maps game events (collisions, interactions) to AudioCommands that play sounds at specific positions.

The system implements the RuntimeSystem trait, ticking in the update() phase of the game loop (not fixed_update(), since audio doesn’t need fixed-timestep processing).

Graceful Degradation

AudioManager::new() can fail on headless machines or CI environments without an audio device. The engine wraps the manager in Option and silently skips audio operations when unavailable. This means scenes with audio components work correctly in all environments — you just won’t hear anything.

Adding Audio to a Scene

# A crackling fire with spatial falloff
[entities.fireplace]
archetype = "furniture"

[entities.fireplace.transform]
position = [5.0, 0.5, 3.0]

[entities.fireplace.audio_source]
file = "audio/fire_crackle.ogg"
volume = 0.8
loop = true
spatial = true
min_distance = 1.0
max_distance = 15.0

# Background tavern ambience (non-spatial)
[entities.ambience]

[entities.ambience.audio_source]
file = "audio/tavern_ambient.ogg"
volume = 0.3
loop = true
spatial = false

Supported audio formats: OGG, WAV, MP3, FLAC (via Kira’s symphonia backend).

Scripting Integration

Audio can be controlled from Rhai scripts using deferred commands. The script API produces ScriptCommand values that the player processes after the script update phase:

FunctionDescription
play_sound(name)Play a non-spatial sound at default volume
play_sound(name, volume)Play a non-spatial sound at the given volume (0.0–1.0)
play_sound_at(name, x, y, z, volume)Play a spatial sound at a 3D position
stop_sound(name)Stop a playing sound
#![allow(unused)]
fn main() {
// In a Rhai script:
fn on_interact() {
    play_sound("door_open");                          // Non-spatial
    play_sound_at("glass_clink", 5.0, 1.0, 3.0, 0.8); // Spatial at position
}
}

Sound names match files in the audio/ directory. All .ogg, .wav, .mp3, and .flac files are automatically loaded at startup.

Further Reading

  • Scripting — full scripting API including audio functions
  • Animation — animation system that can trigger audio events
  • Physics and Runtime — the game loop and event bus that drives audio triggers
  • Schemas — component and archetype definitions

Animation

Flint’s animation system provides two tiers of animation through the flint-animation crate: property tweens for simple transform animations defined in TOML, and skeletal animation for character rigs imported from glTF files with GPU vertex skinning.

Tier 1: Property Animation

Property animations are the simplest form — animate any transform property (position, rotation, scale) or custom float field over time using keyframes. No 3D modeling tool required; clips are defined entirely in TOML.

Animation Clips

Clips are .anim.toml files stored in the demo/animations/ directory:

# animations/door_swing.anim.toml
name = "door_swing"
duration = 0.8

[[tracks]]
interpolation = "Linear"

[tracks.target]
type = "Rotation"

[[tracks.keyframes]]
time = 0.0
value = [0.0, 0.0, 0.0]

[[tracks.keyframes]]
time = 0.8
value = [0.0, 90.0, 0.0]

[[events]]
time = 0.0
event_name = "door_creak"

Interpolation Modes

ModeBehavior
StepJumps instantly to the next keyframe value
LinearLinearly interpolates between keyframes
CubicSplineSmooth interpolation with in/out tangents (matches glTF spec)

Track Targets

Each track animates a specific property:

TargetDescription
PositionEntity position [x, y, z]
RotationEntity rotation in euler degrees [x, y, z]
ScaleEntity scale [x, y, z]
CustomFloatAny numeric component field (specify component and field)

Animation Events

Clips can fire game events at specific times — useful for triggering sounds (footstep at a specific frame), spawning particles, or notifying scripts. Events fire once per loop cycle.

Attaching an Animation

Add an animator component to any entity in your scene:

[entities.platform]
archetype = "furniture"

[entities.platform.transform]
position = [2.0, 0.5, 3.0]

[entities.platform.animator]
clip = "platform_bob"
autoplay = true
loop = true
speed = 1.0

The animation system scans for .anim.toml files at startup and matches clip names to animator components.

Tier 2: Skeletal Animation

For characters and complex articulated meshes, skeletal animation imports bone hierarchies from glTF files and drives them with GPU vertex skinning.

Pipeline

glTF file (.glb)
  ├── Skin: joint hierarchy + inverse bind matrices
  ├── Mesh: positions, normals, UVs, joint_indices, joint_weights
  └── Animations: per-joint translation/rotation/scale channels
         │
         ▼
  ┌──────────────────────┐
  │   flint-import        │  Extract skeleton, clips, skinned vertices
  └──────────┬───────────┘
             │
  ┌──────────▼───────────┐
  │   flint-animation     │  Evaluate keyframes → bone matrices each frame
  └──────────┬───────────┘
             │
  ┌──────────▼───────────┐
  │   flint-render        │  Upload bone matrices → vertex shader skinning
  └──────────────────────┘

How It Works

  1. Importflint-import extracts the skeleton (joint hierarchy, inverse bind matrices) and animation clips (per-joint keyframe channels) from glTF files
  2. Evaluate — each frame, flint-animation samples the current clip time to produce local joint poses, walks the bone hierarchy to compute global transforms, and multiplies by inverse bind matrices to get final bone matrices
  3. Render — bone matrices are uploaded to a GPU storage buffer. The skinned vertex shader transforms each vertex by its weighted bone influences

Skinned Vertices

Skeletal meshes use a separate SkinnedVertex type with 6 attributes (vs. 4 for static geometry), avoiding 32 bytes of wasted bone data on every static vertex in the scene:

AttributeTypeDescription
positionvec3Vertex position
normalvec3Vertex normal
colorvec4Vertex color
uvvec2Texture coordinates
joint_indicesuvec4Indices of 4 influencing bones
joint_weightsvec4Weights for each bone (sum to 1.0)

Crossfade Blending

Smooth transitions between skeletal clips (e.g., idle to walk) use crossfade blending controlled by the animator component:

[entities.character.animator]
clip = "idle"
playing = true
loop = true
blend_target = "walk"      # Crossfade into this clip
blend_duration = 0.3       # Over 0.3 seconds

Blending uses slerp for rotation quaternions and lerp for translation/scale, producing smooth pose interpolation.

Skeleton Schema

The skeleton component references a glTF skin:

[entities.character.skeleton]
skin = "Armature"           # Name of the glTF skin

Entities with both animator and skeleton components use the skeletal animation path. Entities with only animator use property tweens.

Animator Schema

The animator component controls playback for both tiers:

FieldTypeDefaultDescription
clipstring“”Current animation clip name
playingboolfalseWhether the animation is playing
autoplayboolfalseStart playing on scene load
loopbooltrueLoop when the clip ends
speedf321.0Playback speed (-10.0 to 10.0)
blend_targetstring“”Clip to crossfade into
blend_durationf320.3Crossfade duration in seconds

Architecture

  • AnimationPlayer — clip registry and per-entity playback state for property tweens
  • AnimationSync — bridges ECS animator components to property animation playback, auto-discovers new entities each frame
  • SkeletalSync — bridges ECS to skeletal animation, manages per-entity skeleton state and bone matrix computation
  • AnimationSystem — top-level RuntimeSystem implementation that ticks both tiers

Animation runs in update() (variable-rate), not fixed_update(), because smooth interpolation benefits from matching the rendering frame rate rather than the physics tick rate.

Scripting Integration

Animations can be controlled from Rhai scripts by writing directly to the animator component. The AnimationSync system picks up changes on the next frame:

FunctionDescription
play_clip(entity_id, clip_name)Start playing a named animation clip
stop_clip(entity_id)Stop the current animation
blend_to(entity_id, clip, duration)Crossfade to another clip over the given duration
set_anim_speed(entity_id, speed)Set animation playback speed
#![allow(unused)]
fn main() {
// In a Rhai script:
fn on_interact() {
    let me = self_entity();
    play_clip(me, "door_swing");
}

fn on_init() {
    let me = self_entity();
    blend_to(me, "idle", 0.3);  // Smooth transition to idle
}
}

Further Reading

  • Scripting — full scripting API including animation functions
  • Audio — audio system that responds to animation events
  • Rendering — the skinned mesh GPU pipeline
  • Physics and Runtime — the game loop that drives animation
  • File Formats.anim.toml format reference

Terrain

Flint’s terrain system provides heightmap-based outdoor environments via the flint-terrain crate. Rolling hills, mountains, valleys, and open landscapes are defined by a grayscale heightmap image and textured with up to four blended surface layers controlled by an RGBA splat map.

How It Works

A single terrain component on an entity defines the entire terrain surface:

Heightmap PNG          flint-terrain              flint-render
  grayscale    ──►  Chunked mesh generation  ──►  TerrainPipeline
  257x257           positions/normals/UVs          PBR lighting
                    triangle indices               splat-map blending
                                                   cascaded shadows

Splat Map PNG                                    flint-physics
  RGBA channels  ──►  4-layer texture blend      Rapier trimesh
  R=grass G=dirt       tiled from world pos       collision collider
  B=rock  A=sand

The heightmap is a grayscale PNG (8-bit or 16-bit) where black is the lowest point and white is the highest. The terrain is divided into chunks for efficient rendering — each chunk is an independent draw call with its own vertex and index buffers.

Adding Terrain to a Scene

Create an entity with the terrain archetype:

[entities.ground]
archetype = "terrain"

[entities.ground.transform]
position = [-128, 0, -128]

[entities.ground.terrain]
heightmap = "terrain/heightmap.png"
splat_map = "terrain/splatmap.png"
layer0_texture = "terrain/grass.png"
layer1_texture = "terrain/dirt.png"
layer2_texture = "terrain/rock.png"
layer3_texture = "terrain/sand.png"
width = 256.0
depth = 256.0
height_scale = 50.0
texture_tile = 16.0

The transform.position sets the world-space origin of the terrain. The heightmap is placed starting at that position, extending width units along X and depth units along Z. Heights range from 0 to height_scale units along Y.

Heightmap

The heightmap is a grayscale PNG image. Each pixel encodes a height value:

  • 8-bit grayscale — 256 height levels
  • 16-bit grayscale — 65,536 height levels (recommended for large terrains)

The heightmap resolution determines mesh detail. A 257x257 image with chunk_resolution = 64 produces a 4x4 grid of chunks, each with 65x65 vertices (16,641 vertices per chunk, 24,576 triangles per chunk).

Heights are sampled with bilinear interpolation for smooth surfaces, even with lower-resolution heightmaps.

Creating Heightmaps

Any image editor that outputs grayscale PNGs works. Common approaches:

  • Photoshop/GIMP — paint or use noise filters, export as grayscale PNG
  • World Machine / Gaea — procedural terrain generation tools
  • Python + Pillow — generate programmatically with noise functions
  • Real-world data — USGS elevation data converted to grayscale

The dimensions should ideally be (N * chunk_resolution) + 1 for clean chunk boundaries (e.g., 257, 513, 1025).

Splat Map

The splat map is an RGBA PNG that controls how four texture layers blend across the terrain surface:

ChannelLayerTypical Use
R (red)Layer 0Grass
G (green)Layer 1Dirt
B (blue)Layer 2Rock
A (alpha)Layer 3Sand

At each pixel, the RGBA weights are normalized so they always sum to 1.0. A pixel with (255, 0, 0, 0) shows pure grass; (128, 128, 0, 0) shows a 50/50 grass-dirt blend.

If no splat map is provided, the terrain uses the default white texture uniformly.

Creating Splat Maps

Splat maps can be painted manually in any image editor that supports RGBA channels, or generated algorithmically based on height and slope:

  • Low flat areas — grass (red channel)
  • Mid elevations — dirt (green channel)
  • Steep slopes / high peaks — rock (blue channel)
  • Very low areas — sand (alpha channel)

Texture Tiling

Layer textures are tiled across the terrain surface based on world position, not the terrain UV. The texture_tile field controls how many times the texture repeats per 100 world units:

texture_tileRepetitions per 100 unitsGood for
4.04xLarge rock formations
12.012xGeneral ground cover
24.024xFine detail (grass blades)

Higher values produce finer detail but may show visible tiling at a distance. Future updates will add detail textures and triplanar mapping to mitigate this.

Component Schema

FieldTypeDefaultDescription
heightmapstringPath to grayscale PNG (relative to scene directory)
widthf32256.0World-space extent along X axis
depthf32256.0World-space extent along Z axis
height_scalef3250.0Maximum height in world units
chunk_resolutioni3264Vertices per chunk edge (higher = more detail)
texture_tilef3216.0Texture tiling factor per 100 world units
splat_mapstring“”Path to RGBA splat map PNG
layer0_texturestring“”Layer 0 texture (splat R channel)
layer1_texturestring“”Layer 1 texture (splat G channel)
layer2_texturestring“”Layer 2 texture (splat B channel)
layer3_texturestring“”Layer 3 texture (splat A channel)
metallicf320.0PBR metallic value for terrain surface
roughnessf320.85PBR roughness value for terrain surface

Physics Collision

Terrain automatically gets a trimesh physics collider via Rapier. The mesh geometry is exported as vertices and triangle indices, then registered as a fixed (immovable) rigid body. This means:

  • Characters walk on the terrain surface naturally
  • Objects collide with the terrain
  • Raycasts hit the terrain for line-of-sight checks

The collider shape exactly matches the rendered mesh, so what you see is what you collide with.

Height Sampling from Scripts

The terrain_height(x, z) function is available in Rhai scripts to query the terrain height at any world position:

#![allow(unused)]
fn main() {
fn on_update() {
    let me = self_entity();
    let pos = get_position(me);

    // Get terrain height at entity's XZ position
    let ground_y = terrain_height(pos.x, pos.z);

    // Snap entity to terrain surface
    set_position(me, pos.x, ground_y + 0.5, pos.z);
}
}

This is useful for:

  • NPC placement — keep characters on the ground
  • Projectile impact — detect when a projectile hits terrain
  • Camera clamping — prevent the camera from going below ground
  • Vegetation scattering — place objects at correct heights

The function uses bilinear interpolation on the heightmap data, matching the rendered surface exactly. It returns 0.0 if no terrain is loaded.

Rendering

Terrain uses its own TerrainPipeline with full PBR lighting — the same Cook-Torrance BRDF, cascaded shadow maps, point lights, and spot lights as regular scene geometry. Terrain both casts and receives shadows.

The rendering order places terrain early in the pass (after the skybox, before entity geometry) to fill the depth buffer for efficient occlusion culling of objects behind hills.

When post-processing is active, the terrain outputs linear HDR values like all other scene geometry. The composite pass handles tonemapping, bloom, fog, and other effects.

Scene Transitions

Terrain is fully cleared and reloaded during scene transitions. When load_scene() is called:

  1. Current terrain draw calls and physics collider are removed
  2. New scene is loaded
  3. New terrain (if any) is generated, uploaded to GPU, and registered with physics
  4. The terrain_height() callback is updated to use the new heightmap

Architecture

The terrain system is split across crates to maintain clean dependency boundaries:

  • flint-terrain — pure data crate (no GPU dependency). Generates chunked mesh geometry from heightmap data. Outputs raw positions, normals, UVs, and indices.
  • flint-renderTerrainPipeline and terrain_shader.wgsl. Assembles GPU vertex buffers from terrain data, handles splat-map texture blending and PBR lighting.
  • flint-physics — reuses existing register_static_trimesh() for collision. No terrain-specific physics code needed.
  • flint-scriptterrain_height(x, z) Rhai function via callback pattern.

This separation means flint-terrain can be used independently for tools, CLI commands, or headless processing without pulling in the GPU stack.

Limitations

  • One terrain per scene — currently only the first terrain entity is loaded
  • No LOD — all chunks render at full resolution regardless of distance
  • No runtime deformation — terrain is static after loading
  • CPU-side simulation — no GPU compute for terrain generation
  • Fixed PBR parameters — metallic and roughness are uniform across the entire terrain surface

See the Terrain Roadmap for planned features including LOD, sculpting, auto-splatting, triplanar mapping, and more.

Further Reading

Particles

Flint’s particle system provides GPU-instanced visual effects through the flint-particles crate. Fire, smoke, sparks, dust motes, magic effects — any volumetric visual that needs hundreds or thousands of small, short-lived elements.

Note: Particle effects are dynamic simulations that accumulate over time. Use flint play to see them in action — headless flint render captures a single frame and won’t show accumulated particles.

How It Works

Each entity with a particle_emitter component owns a pool of particles simulated on the CPU and rendered as camera-facing quads via GPU instancing. The pipeline is:

TOML component                CPU simulation              GPU rendering
particle_emitter   ──►  ParticleSync reads config  ──►  ParticlePipeline
  emission_rate         spawn/integrate/kill             instanced draw
  gravity               pack into instance buffer        storage buffer
  color_start/end       (swap-remove pool)               alpha or additive

Unlike billboard sprites (which are individual ECS entities), particles are pooled per-emitter — a single entity can own thousands of particles without overwhelming the ECS.

Adding Particles to a Scene

Add a particle_emitter component to any entity:

[entities.campfire]
[entities.campfire.transform]
position = [0, 0.2, 0]

[entities.campfire.particle_emitter]
emission_rate = 40.0
max_particles = 200
lifetime_min = 0.3
lifetime_max = 0.8
speed_min = 1.5
speed_max = 3.0
direction = [0, 1, 0]
spread = 20.0
gravity = [0, 2.0, 0]
size_start = 0.15
size_end = 0.02
color_start = [1.0, 0.7, 0.1, 0.9]
color_end = [1.0, 0.1, 0.0, 0.0]
blend_mode = "additive"
shape = "sphere"
shape_radius = 0.15
autoplay = true

Emission Shapes

The shape field controls where new particles spawn relative to the emitter:

ShapeFieldsDescription
point(none)All particles spawn at the emitter origin
sphereshape_radiusRandom position within a sphere
coneshape_angle, shape_radiusParticles emit in a cone around direction
boxshape_extentsRandom position within an axis-aligned box

Blend Modes

ModeUse CaseDescription
alphaSmoke, dust, fogStandard alpha blending — particles fade naturally
additiveFire, sparks, magicColors add together — bright, glowing effects

Additive blending is order-independent, making it ideal for dense effects. Alpha blending looks best for soft, diffuse effects.

Value Over Lifetime

Particles interpolate linearly between start and end values over their lifetime:

  • size_start / size_end — particles can grow (smoke expanding) or shrink (sparks dying)
  • color_start / color_end — RGBA transition. Set color_end alpha to 0 for fade-out

Sprite Sheet Animation

For textured particles (flame sprites, explosion frames), use sprite sheets:

[entities.explosion.particle_emitter]
texture = "explosion_sheet.png"
frames_x = 4
frames_y = 4
animate_frames = true   # Auto-advance frames over particle lifetime

With animate_frames = true, each particle plays through the sprite sheet from birth to death.

Bursts and Duration

For one-shot effects (explosions, impacts), combine bursts with limited duration:

[entities.explosion.particle_emitter]
emission_rate = 0.0      # No continuous emission
burst_count = 50         # 50 particles on each burst
duration = 0.5           # Emitter runs for 0.5 seconds
looping = false          # Don't repeat
autoplay = true          # Fire immediately

For periodic bursts (fountain, heartbeat), set looping = true with a duration.

Component Schema

FieldTypeDefaultDescription
emission_ratef3210.0Particles per second (0 = burst-only)
burst_counti320Particles fired on each burst/loop start
max_particlesi32256Pool capacity (max 10,000)
lifetime_minf321.0Minimum particle lifetime in seconds
lifetime_maxf322.0Maximum particle lifetime in seconds
speed_minf321.0Minimum initial speed
speed_maxf323.0Maximum initial speed
directionvec3[0,1,0]Base emission direction (local space)
spreadf3215.0Random deviation angle in degrees
gravityvec3[0,-9.81,0]Acceleration applied per frame (world space)
dampingf320.0Velocity decay per second
size_startf320.1Particle size at birth
size_endf320.0Particle size at death
color_startvec4[1,1,1,1]RGBA color at birth
color_endvec4[1,1,1,0]RGBA color at death
texturestring“”Sprite texture (empty = white dot)
frames_xi321Sprite sheet columns
frames_yi321Sprite sheet rows
animate_framesboolfalseAuto-advance frames over lifetime
blend_modestring“alpha”"alpha" or "additive"
shapestring“point”"point", "sphere", "cone", "box"
shape_radiusf320.5Radius for sphere/cone shapes
shape_anglef3230.0Half-angle for cone shape (degrees)
shape_extentsvec3[0.5,0.5,0.5]Half-extents for box shape
world_spacebooltrueParticles detach from emitter transform
durationf320.0Emitter duration (0 = infinite)
loopingbooltrueLoop when duration expires
playingboolfalseCurrent playback state
autoplaybooltrueStart emitting on scene load

Scripting Integration

Particles can be controlled from Rhai scripts:

FunctionDescription
emit_burst(entity_id, count)Fire N particles immediately
start_emitter(entity_id)Start continuous emission
stop_emitter(entity_id)Stop emission (existing particles finish)
set_emission_rate(entity_id, rate)Change emission rate dynamically
#![allow(unused)]
fn main() {
// Rhai script: burst of sparks on impact
fn on_collision() {
    let me = self_entity();
    emit_burst(me, 30);
}

// Rhai script: toggle emitter with interaction
fn on_interact() {
    let me = self_entity();
    let playing = get_field(me, "particle_emitter", "playing");
    if playing {
        stop_emitter(me);
    } else {
        start_emitter(me);
    }
}
}

Architecture

  • ParticlePool — swap-remove array for O(1) particle death, contiguous alive iteration
  • ParticleSync — bridges ECS particle_emitter components to the simulation, auto-discovers new emitters each frame
  • ParticleSystem — top-level RuntimeSystem that ticks simulation in update() (variable-rate, not fixed-step)
  • ParticlePipeline — wgpu render pipeline with alpha and additive variants, storage buffer for instances

The particle system runs after animation (emitter transforms may be animated) and before the renderer refresh. Instance data is packed contiguously and uploaded to a GPU storage buffer for efficient instanced drawing.

Recipes

Fire

emission_rate = 40.0
gravity = [0, 2.0, 0]
color_start = [1.0, 0.7, 0.1, 0.9]
color_end = [1.0, 0.1, 0.0, 0.0]
blend_mode = "additive"
shape = "sphere"
shape_radius = 0.15

Smoke

emission_rate = 8.0
gravity = [0, 0.5, 0]
damping = 0.3
size_start = 0.1
size_end = 0.6
color_start = [0.4, 0.4, 0.4, 0.3]
color_end = [0.6, 0.6, 0.6, 0.0]
blend_mode = "alpha"

Sparks

emission_rate = 15.0
speed_min = 3.0
speed_max = 6.0
spread = 45.0
gravity = [0, -9.81, 0]
size_start = 0.03
size_end = 0.01
color_start = [1.0, 0.9, 0.3, 1.0]
color_end = [1.0, 0.3, 0.0, 0.0]
blend_mode = "additive"

Dust Motes

emission_rate = 5.0
speed_min = 0.05
speed_max = 0.2
spread = 180.0
gravity = [0, 0.02, 0]
damping = 0.5
size_start = 0.02
size_end = 0.02
color_start = [1.0, 1.0, 0.9, 0.5]
color_end = [1.0, 1.0, 0.9, 0.0]
shape = "box"
shape_extents = [2.0, 1.0, 2.0]

Further Reading

  • Scripting — full scripting API including particle functions
  • Animation — animate emitter transforms with property tweens
  • Rendering — the GPU pipeline that draws particles
  • Physics and Runtime — the game loop that drives particle simulation

Physics and Runtime

Flint’s runtime layer transforms static scenes into interactive, playable experiences. The flint-runtime crate provides the game loop infrastructure, and flint-physics integrates the Rapier 3D physics engine for collision detection and character movement.

The Game Loop

The game loop uses a fixed-timestep accumulator pattern. Physics simulation steps at a constant rate (1/60s by default) regardless of how fast or slow the rendering runs. This ensures deterministic behavior across different hardware.

The loop structure:

  1. Tick the clock — advance time, accumulate delta into the physics budget
  2. Process input — read keyboard and mouse state into InputState
  3. Fixed-step physics — while enough time has accumulated, step the physics simulation
  4. Character controller — apply player movement based on input and physics state
  5. Update audio — sync listener position to camera, process trigger events, update spatial tracks
  6. Advance animation — tick property tweens and skeletal playback, write updated transforms to ECS, upload bone matrices to GPU
  7. Run scripts — execute Rhai scripts (on_update, event callbacks), process deferred commands (audio, events)
  8. Render — draw the frame with the current entity positions, HUD overlay (crosshair, interaction prompts)

The RuntimeSystem trait provides a standard interface for systems that plug into this loop. Physics, audio, animation, and scripting each implement RuntimeSystem with initialize(), fixed_update(), update(), and shutdown() methods.

Physics with Rapier 3D

The flint-physics crate wraps Rapier 3D and bridges it to Flint’s TOML-based component system:

  • PhysicsWorld — manages Rapier’s rigid body set, collider set, and simulation pipeline
  • PhysicsSync — reads rigidbody and collider components from entities and creates corresponding Rapier bodies. Static bodies for world geometry (walls, floors, furniture), kinematic bodies for the player.
  • CharacterController — kinematic first-person movement with gravity, jumping, ground detection, and sprint

Physics Schemas

Three component schemas define physics properties:

Rigidbody (rigidbody.toml) — determines how an entity participates in physics:

  • body_type: "static" (immovable world geometry), "dynamic" (simulated), or "kinematic" (script-controlled)
  • mass, gravity_scale

Collider (collider.toml) — defines the collision shape:

  • shape: "box", "sphere", or "capsule"
  • size: dimensions of the collision volume
  • friction: surface friction coefficient

Character Controller (character_controller.toml) — first-person movement parameters:

  • move_speed, jump_force, height, radius, camera_mode

The player archetype (player.toml) bundles these together with a transform for a ready-to-use player entity.

Adding Physics to a Scene

To make a scene playable, add physics components to entities:

# The player entity
[entities.player]
archetype = "player"

[entities.player.transform]
position = [0, 1, 0]

[entities.player.character_controller]
move_speed = 6.0
jump_force = 7.0

# A wall with a static collider
[entities.north_wall]
archetype = "wall"

[entities.north_wall.transform]
position = [0, 2, -10]

[entities.north_wall.collider]
shape = "box"
size = [20, 4, 0.5]

[entities.north_wall.rigidbody]
body_type = "static"

Then play the scene:

flint play my_scene.scene.toml

Raycasting

The physics system provides raycasting for line-of-sight checks, hitscan weapons, and interaction targeting. PhysicsWorld::raycast() casts a ray through the Rapier collision world and returns the first hit:

#![allow(unused)]
fn main() {
pub struct EntityRaycastHit {
    pub entity_id: EntityId,
    pub distance: f32,
    pub point: [f32; 3],
    pub normal: [f32; 3],
}
}

The function resolves Rapier collider handles back to Flint EntityIds through the collider-to-entity map maintained by PhysicsSync. An optional exclude_entity parameter lets callers exclude a specific entity (typically the shooter) from the results.

Raycasting is exposed to scripts via the raycast() function — see Scripting: Physics API for the script-level interface and examples.

Input System

The InputState struct provides a config-driven, device-agnostic input layer. It tracks keyboard, mouse, and gamepad state each frame and evaluates logical actions from physical bindings.

How It Works

All input flows through a unified Binding model:

  • Keyboard keys (Key { code }) — any winit KeyCode name (e.g., "KeyW", "Space", "ShiftLeft")
  • Mouse buttons (MouseButton { button }) — "Left", "Right", "Middle", "Back", "Forward"
  • Mouse delta (MouseDelta { axis, scale }) — raw mouse movement for camera look
  • Mouse wheel (MouseWheel { axis, scale }) — scroll wheel input
  • Gamepad buttons (GamepadButton { button, gamepad }) — any gilrs button name (e.g., "South", "RightTrigger")
  • Gamepad axes (GamepadAxis { axis, gamepad, deadzone, scale, invert, threshold, direction }) — analog sticks and triggers with full processing pipeline

Actions have two kinds:

  • Button — discrete on/off (pressed/released). Any binding value >= 0.5 counts as pressed.
  • Axis1d — continuous analog value. All binding values are summed.

Input Configuration Files

Bindings are defined in TOML files with a layered loading model:

version = 1
game_id = "doom_fps"

[actions.move_forward]
kind = "button"
[[actions.move_forward.bindings]]
type = "key"
code = "KeyW"
[[actions.move_forward.bindings]]
type = "gamepad_axis"
axis = "LeftStickY"
direction = "negative"
threshold = 0.35
gamepad = "any"

[actions.fire]
kind = "button"
[[actions.fire.bindings]]
type = "mouse_button"
button = "Left"
[[actions.fire.bindings]]
type = "gamepad_button"
button = "RightTrigger"
gamepad = "any"

[actions.look_x]
kind = "axis1d"
[[actions.look_x.bindings]]
type = "mouse_delta"
axis = "x"
scale = 2.0
[[actions.look_x.bindings]]
type = "gamepad_axis"
axis = "RightStickX"
deadzone = 0.15
scale = 1.0
gamepad = "any"

Config Layering

Configs are loaded with deterministic precedence (later layers override earlier):

  1. Engine built-in defaults — hardcoded WASD + mouse baseline (always present)
  2. Game default config<game_root>/config/input.toml (checked into the repo)
  3. User overrides~/.flint/input_{game_id}.toml (per-player remapping, written at runtime)
  4. CLI override--input-config <path> flag (one-off testing/debugging)

Scenes can also reference an input config via the input_config field in the [scene] table.

Default Action Bindings

When no config files are present, the built-in defaults provide:

ActionDefault BindingKind
move_forwardWButton
move_backwardSButton
move_leftAButton
move_rightDButton
jumpSpaceButton
interactEButton
sprintLeft ShiftButton
weapon_11Button
weapon_22Button
reloadRButton
fireLeft Mouse ButtonButton

Games can define any number of custom actions in their config files. Scripts access them with is_action_pressed("custom_action").

Gamepad Support

Gamepad input is handled via the gilrs crate. The player polls gamepad events each frame and routes them through the same binding system as keyboard/mouse:

  • Buttons are matched by gilrs Debug names: South, East, North, West, LeftTrigger, RightTrigger, DPadUp, etc.
  • Axes support deadzone filtering, scale, invert, and optional threshold for button-like behavior
  • Multi-gamepad is supported via GamepadSelector::Any (first match) or GamepadSelector::Index(n) (specific controller)
  • Disconnected gamepads are automatically cleaned up

Runtime Rebinding

Bindings can be remapped at runtime through the rebind_action() API:

  1. Call begin_rebind_capture(action, mode) to enter capture mode
  2. The next physical input (key press, mouse click, or gamepad button/axis) becomes the new binding
  3. The mode determines conflict resolution:
    • Replace — clear all existing bindings, set the new one
    • Add — append to the binding list (allows multiple inputs for one action)
    • Swap — remove this binding from any other action, assign to target
  4. User overrides are automatically saved to ~/.flint/input_{game_id}.toml

Runtime Physics Updates

The physics system handles several runtime updates beyond the core simulation:

  • Sensor flag updates — when game logic marks an entity as dead, its collider can be set to a sensor (non-solid) so other entities pass through it
  • Kinematic body sync — script-controlled position changes are written back to Rapier kinematic bodies each frame
  • Collision event drain — the ChannelEventCollector collects collision and contact events each physics step; these are drained and dispatched as script callbacks (on_collision, on_trigger_enter, on_trigger_exit)

Further Reading

  • Scripting — Rhai scripting system for game logic
  • Audio — spatial audio with Kira
  • Animation — property tweens and skeletal animation
  • Rendering — the PBR rendering pipeline
  • Schemas — component and archetype definitions including physics schemas
  • CLI Reference — the play command and player binary

Scripting

Flint’s scripting system provides runtime game logic through Rhai, a lightweight embedded scripting language. Scripts can read and write entity data, respond to game events, control animation and audio, and hot-reload while the game is running.

Overview

The flint-script crate integrates Rhai into the game loop:

  • ScriptEngine — compiles and runs .rhai scripts, manages per-entity state (scope, AST, callbacks)
  • ScriptSync — discovers entities with script components, handles hot-reload by watching file timestamps
  • ScriptSystem — implements RuntimeSystem for game loop integration, running in update() (variable-rate)

Scripts run each frame during the update() phase, after physics and before rendering. This gives them access to the latest physics state while allowing their output to affect the current frame’s visuals.

Script Component

Attach a script to any entity with the script component:

[entities.my_door]
archetype = "door"

[entities.my_door.script]
source = "door_interact.rhai"
enabled = true
FieldTypeDefaultDescription
sourcestring""Path to .rhai file (relative to the scripts/ directory)
enabledbooltrueWhether the script is active

Script files live in the scripts/ directory next to your scene file.

Event Callbacks

Scripts define behavior through callback functions. The engine detects which callbacks are defined in each script’s AST and only calls those that exist:

CallbackSignatureWhen It Fires
on_initfn on_init()Once when the script is first loaded
on_updatefn on_update()Every frame. Use delta_time() for frame delta
on_collisionfn on_collision(other_id)When this entity collides with another
on_trigger_enterfn on_trigger_enter(other_id)When another entity enters a trigger volume
on_trigger_exitfn on_trigger_exit(other_id)When another entity exits a trigger volume
on_actionfn on_action(action_name)When an input action fires (e.g., "jump", "interact")
on_interactfn on_interact()When the player presses Interact near this entity
on_draw_uifn on_draw_ui()Every frame after on_update, for 2D HUD draw commands

The on_interact callback is sugar for the common pattern of proximity-based interaction. It automatically checks the entity’s interactable component for range (default 3.0) and enabled (default true) before firing.

API Reference

All functions are available globally in every script. Entity IDs are passed as i64 (Rhai’s native integer type).

Entity API

FunctionReturnsDescription
self_entity()i64The entity ID of the entity this script is attached to
this_entity()i64Alias for self_entity()
get_entity(name)i64Look up an entity by name. Returns -1 if not found
entity_exists(id)boolCheck whether an entity ID is valid
entity_name(id)StringGet the name of an entity
has_component(id, component)boolCheck if an entity has a specific component
get_component(id, component)MapGet an entire component as a map (or () if missing)
get_field(id, component, field)DynamicRead a component field value
set_field(id, component, field, value)Write a component field value
get_position(id)MapGet entity position as #{x, y, z}
set_position(id, x, y, z)Set entity position
get_rotation(id)MapGet entity rotation (euler degrees) as #{x, y, z}
set_rotation(id, x, y, z)Set entity rotation (euler degrees)
distance(a, b)f64Euclidean distance between two entities
set_parent(child_id, parent_id)Set an entity’s parent in the hierarchy
get_parent(id)i64Get the parent entity ID (-1 if none)
get_children(id)ArrayGet child entity IDs as an array
get_world_position(id)MapWorld-space position as #{x, y, z} (accounts for parent transforms)
set_material_color(id, r, g, b, a)Set the material base color (RGBA, 0.0–1.0)
find_entities_with(component)ArrayAll entity IDs that have the given component
entity_count_with(component)i64Count of entities with the given component
spawn_entity(name)i64Create a new entity. Returns its ID or -1 on failure
despawn_entity(id)Remove an entity from the world

Input API

FunctionReturnsDescription
is_action_pressed(action)boolWhether an action is currently held
is_action_just_pressed(action)boolWhether an action was pressed this frame
is_action_just_released(action)boolWhether an action was released this frame
action_value(action)f64Analog value for Axis1d actions (0.0 if not bound)
mouse_delta_x()f64Horizontal mouse movement this frame
mouse_delta_y()f64Vertical mouse movement this frame

Action names are defined by input configuration files and are fully customizable per game. The built-in defaults include: move_forward, move_backward, move_left, move_right, jump, interact, sprint, weapon_1, weapon_2, reload, fire. Games can define arbitrary custom actions in their input config TOML files and query them from scripts with is_action_pressed("custom_action").

Input bindings support keyboard, mouse, and gamepad devices. See Physics and Runtime: Input System for the config file format and layered loading model.

Time API

FunctionReturnsDescription
delta_time()f64Seconds since last frame
total_time()f64Total elapsed time since scene start

Audio API

Audio functions produce deferred commands that the player processes after the script update phase:

FunctionDescription
play_sound(name)Play a non-spatial sound at default volume
play_sound(name, volume)Play a non-spatial sound at the given volume (0.0–1.0)
play_sound_at(name, x, y, z, volume)Play a spatial sound at a 3D position
stop_sound(name)Stop a playing sound

Sound names match the audio files loaded from the audio/ directory (without extension).

Animation API

Animation functions write directly to the animator component on the target entity. The AnimationSync system picks up changes on the next frame:

FunctionDescription
play_clip(entity_id, clip_name)Start playing a named animation clip
stop_clip(entity_id)Stop the current animation
blend_to(entity_id, clip, duration)Crossfade to another clip over the given duration
set_anim_speed(entity_id, speed)Set animation playback speed

Coordinate System

Flint uses a Y-up, right-handed coordinate system:

  • Forward = -Z (into the screen)
  • Right = +X
  • Up = +Y

Euler angles are stored as (pitch, yaw, roll) in degrees, applied in ZYX order. Positive yaw rotates counter-clockwise when viewed from above (i.e., turns left).

Use the direction helpers (forward_from_yaw, right_from_yaw) to convert a yaw angle into a world-space direction vector. These encode the coordinate convention so scripts don’t need to compute the trig manually.

Math API

FunctionReturnsDescription
PI()f64The constant π (3.14159…)
TAU()f64The constant τ = 2π (6.28318…)
deg_to_rad(degrees)f64Convert degrees to radians
rad_to_deg(radians)f64Convert radians to degrees
forward_from_yaw(yaw_deg)MapForward direction vector #{x, y, z} for a given yaw in degrees
right_from_yaw(yaw_deg)MapRight direction vector #{x, y, z} for a given yaw in degrees
wrap_angle(degrees)f64Normalize an angle to [0, 360)
clamp(val, min, max)f64Clamp a value to a range
lerp(a, b, t)f64Linear interpolation between a and b
random()f64Random value in [0, 1)
random_range(min, max)f64Random value in [min, max)
sin(x)f64Sine (radians)
cos(x)f64Cosine (radians)
abs(x)f64Absolute value
sqrt(x)f64Square root
floor(x)f64Floor
ceil(x)f64Ceiling
min(a, b)f64Minimum of two values
max(a, b)f64Maximum of two values
atan2(y, x)f64Two-argument arctangent (radians)

Event API

FunctionDescription
fire_event(name)Fire a named game event
fire_event_data(name, data)Fire an event with a data map payload

Log API

FunctionDescription
log(msg)Log an info-level message
log_info(msg)Alias for log()
log_warn(msg)Log a warning
log_error(msg)Log an error

Physics API

Physics functions provide raycasting and camera access for combat, line-of-sight checks, and interaction targeting:

FunctionReturnsDescription
raycast(ox, oy, oz, dx, dy, dz, max_dist)Map or ()Cast a ray from origin in direction. Returns hit info or () if nothing hit
move_character(id, dx, dy, dz)Map or ()Collision-corrected kinematic movement. Returns #{x, y, z, grounded}
get_collider_extents(id)Map or ()Collider shape dimensions (see below)
get_camera_position()MapCamera world position as #{x, y, z}
get_camera_direction()MapCamera forward vector as #{x, y, z}
set_camera_position(x, y, z)Override camera position from script
set_camera_target(x, y, z)Override camera look-at target from script
set_camera_fov(fov)Override camera field of view (degrees) from script

The raycast() function automatically excludes the calling entity’s collider from results. On a hit, it returns a map with these fields:

FieldTypeDescription
entityi64Entity ID of the hit object
distancef64Distance from origin to hit point
point_x, point_y, point_zf64World-space hit position
normal_x, normal_y, normal_zf64Surface normal at hit point

move_character performs collision-corrected kinematic movement using Rapier’s shape-sweep. The entity must have rigidbody and collider components. The returned map contains the corrected position and a grounded flag:

#![allow(unused)]
fn main() {
fn on_update() {
    let me = self_entity();
    let dt = delta_time();
    let result = move_character(me, 0.0, -9.81 * dt, 5.0 * dt);
    if result != () {
        set_position(me, result.x, result.y, result.z);
        if result.grounded {
            // Can jump
        }
    }
}
}

get_collider_extents returns the collider shape dimensions. The returned map varies by shape:

  • Box: #{shape: "box", half_x, half_y, half_z}
  • Capsule: #{shape: "capsule", radius, half_height}
  • Sphere: #{shape: "sphere", radius}

Returns () if the entity has no collider.

Example: Hitscan weapon

#![allow(unused)]
fn main() {
fn fire_weapon() {
    let cam_pos = get_camera_position();
    let cam_dir = get_camera_direction();
    let hit = raycast(cam_pos.x, cam_pos.y, cam_pos.z,
                      cam_dir.x, cam_dir.y, cam_dir.z, 100.0);
    if hit != () {
        let target = hit.entity;
        if has_component(target, "health") {
            let hp = get_field(target, "health", "current_hp");
            set_field(target, "health", "current_hp", hp - 25);
        }
    }
}
}

Spline API

Query spline entities for path-following, track layouts, and procedural placement:

FunctionReturnsDescription
spline_closest_point(spline_id, x, y, z)Map or ()Nearest point on spline to query position. Returns #{t, x, y, z, dist_sq}
spline_sample_at(spline_id, t)Map or ()Sample spline at parameter t (0.0–1.0). Returns #{x, y, z, fwd_x, fwd_y, fwd_z, right_x, right_y, right_z}

The t parameter wraps for closed splines. The returned forward and right vectors are normalized and can be used for orientation.

Particle API

FunctionDescription
emit_burst(entity_id, count)Fire N particles immediately
start_emitter(entity_id)Start continuous emission
stop_emitter(entity_id)Stop emission (existing particles finish their lifetime)
set_emission_rate(entity_id, rate)Change emission rate dynamically

See Particles for full component schema and recipes.

Post-Processing API

Control the HDR post-processing pipeline at runtime from scripts:

FunctionDescription
set_vignette(intensity)Set vignette intensity (0.0 = none, 1.0 = heavy)
set_bloom_intensity(intensity)Set bloom strength (0.0 = none)
set_exposure(value)Set exposure multiplier (1.0 = default)

These overrides are applied each frame and combine with the scene’s [post_process] baseline settings. Useful for dynamic effects like speed vignetting, boost bloom, or exposure flashes.

Audio Filter API

FunctionDescription
set_audio_lowpass(cutoff_hz)Set master bus low-pass filter cutoff frequency (Hz)

The low-pass filter affects all audio output. Pass 20000.0 for no filtering, lower values for a muffled effect. Useful for speed-dependent audio (e.g., wind rush at high speed) or dramatic transitions.

Scene Transition API

Load new scenes, manage game state, and persist data across transitions:

FunctionReturnsDescription
load_scene(path)Begin transition to a new scene
reload_scene()Reload the current scene
current_scene()StringPath of the current scene
transition_progress()f64Progress of the current transition (0.0–1.0)
transition_phase()StringCurrent transition phase ("idle", "exiting", "loading", "entering")
is_transitioning()boolWhether a scene transition is in progress
complete_transition()Advance to the next transition phase

Scene transitions follow a lifecycle: Idle -> Exiting -> Loading -> Entering -> Idle. During the Exiting and Entering phases, on_draw_ui() still runs so scripts can draw fade effects using transition_progress(). Call complete_transition() to advance phases — this gives scripts full control over transition timing and visuals.

Two additional callbacks fire during transitions:

CallbackSignatureWhen It Fires
on_scene_enterfn on_scene_enter()After a new scene is loaded and ready
on_scene_exitfn on_scene_exit()Before the current scene is unloaded

Game State Machine API

A pushdown automaton for managing game states (playing, paused, custom):

FunctionReturnsDescription
push_state(name)Push a named state onto the stack
pop_state()Pop the top state (returns to previous)
replace_state(name)Replace the top state
current_state()StringName of the current (top) state
state_stack()ArrayAll state names from bottom to top
register_state(name, config)Register a custom state template

Built-in state templates:

  • "playing" — all systems run (default)
  • "paused" — physics, scripts, animation, particles, and audio are paused; rendering runs; on_draw_ui() still fires (for pause menus)
  • "loading" — all systems paused

Persistent Data API

Key-value store that survives scene transitions:

FunctionReturnsDescription
persist_set(key, value)Store a value
persist_get(key)DynamicRetrieve a value (or () if not set)
persist_has(key)boolCheck if a key exists
persist_remove(key)Remove a key
persist_clear()Clear all persistent data
persist_keys()ArrayList all keys
persist_save(path)Save store to a TOML file
persist_load(path)Load store from a TOML file

Data-Driven UI API

Load and manipulate TOML-defined UI documents at runtime:

FunctionReturnsDescription
load_ui(path)i64Load a UI document (.ui.toml). Returns a handle
unload_ui(handle)Unload a UI document
ui_set_text(element_id, text)Set the text content of a UI element
ui_show(element_id)Show a hidden UI element
ui_hide(element_id)Hide a UI element
ui_set_visible(element_id, visible)Set element visibility
ui_set_color(element_id, r, g, b, a)Set element text/foreground color
ui_set_bg_color(element_id, r, g, b, a)Set element background color
ui_set_style(element_id, property, value)Override a single style property
ui_reset_style(element_id)Remove all style overrides
ui_set_class(element_id, class_name)Change an element’s style class
ui_exists(element_id)boolCheck if a UI element exists
ui_get_rect(element_id)MapGet resolved position/size as #{x, y, width, height}

UI documents are defined with paired .ui.toml (layout) and .style.toml (styling) files, following an HTML/CSS/JS-like separation of concerns. See File Formats for the format specification.

UI Draw API

The draw API lets scripts render 2D overlays each frame via the on_draw_ui() callback. Draw commands are issued in screen-space coordinates (logical points, not physical pixels) and rendered by the engine through egui.

Draw Primitives

FunctionDescription
draw_text(x, y, text, size, r, g, b, a)Draw text at position
draw_text_ex(x, y, text, size, r, g, b, a, layer)Draw text with explicit layer
draw_rect(x, y, w, h, r, g, b, a)Draw filled rectangle
draw_rect_ex(x, y, w, h, r, g, b, a, rounding, layer)Filled rectangle with corner rounding and layer
draw_rect_outline(x, y, w, h, r, g, b, a, thickness)Rectangle outline
draw_circle(x, y, radius, r, g, b, a)Draw filled circle
draw_circle_outline(x, y, radius, r, g, b, a, thickness)Circle outline
draw_line(x1, y1, x2, y2, r, g, b, a, thickness)Draw a line segment
draw_sprite(x, y, w, h, name)Draw a sprite image
draw_sprite_ex(x, y, w, h, name, u0, v0, u1, v1, r, g, b, a, layer)Sprite with custom UV coordinates, tint, and layer

Query Functions

FunctionReturnsDescription
screen_width()f64Logical screen width in points
screen_height()f64Logical screen height in points
measure_text(text, size)MapApproximate text size as #{width, height}
find_nearest_interactable()Map or ()Nearest interactable entity info, or () if none in range

find_nearest_interactable() returns a map with entity (ID), prompt_text, interaction_type, and distance fields when an interactable entity is within range.

Layer Ordering

All _ex draw variants accept a layer parameter which must be an integer (0, 1, -1), not a float. Using 0.0 instead of 0 will cause a “Function not found” error because Rhai does not implicitly convert between float and int. Commands are sorted by layer before rendering:

  • Negative layers render behind (background elements)
  • Layer 0 is the default
  • Positive layers render in front (foreground elements)

Coordinate System

Coordinates are in egui logical points, not physical pixels. On high-DPI displays, logical points differ from pixels by the scale factor. Use screen_width() and screen_height() for layout calculations — they return the correct logical dimensions.

Sprite Loading

Sprite names map to image files in the sprites/ directory (without extension). Supported formats: PNG, JPG, BMP, TGA. Textures are lazy-loaded on first use and cached for subsequent frames.

Data-Driven UI System

For structured interfaces like menus, HUDs, and dialog boxes, Flint provides a data-driven UI system that separates layout, style, and logic into distinct files. The procedural draw_* API above continues to work alongside it for dynamic elements like minimaps or particle trails.

The pattern is:

  • Layout (.ui.toml) — element tree with types, hierarchy, anchoring, and default text/images
  • Style (.style.toml) — named style classes with visual properties (colors, sizes, fonts, padding)
  • Logic (.rhai) — scripts load UI documents and manipulate elements at runtime

File Format: .ui.toml

[ui]
name = "Race HUD"
style = "ui/race_hud.style.toml"   # Path to companion style file

[elements.speed_panel]
type = "panel"
anchor = "bottom-center"
class = "hud-panel"

[elements.speed_label]
type = "text"
parent = "speed_panel"
class = "speed-text"
text = "0"

[elements.lap_counter]
type = "text"
anchor = "top-right"
class = "lap-text"
text = "Lap 1/3"
FieldTypeDefaultDescription
typestring"panel"Element type: panel, text, rect, circle, image
anchorstring"top-left"Screen anchor for root elements (see below)
parentstringParent element ID (child inherits position from parent)
classstring""Style class name from the companion .style.toml
textstring""Default text content (for text elements)
srcstring""Image source path (for image elements)
visiblebooltrueInitial visibility

Anchor points: top-left, top-center, top-right, center-left, center, center-right, bottom-left, bottom-center, bottom-right

File Format: .style.toml

[styles.hud-panel]
width = 200
height = 60
bg_color = [0.0, 0.0, 0.0, 0.6]
rounding = 8
padding = [12, 8, 12, 8]
layout = "stack"

[styles.speed-text]
font_size = 32
color = [1.0, 1.0, 1.0, 1.0]
text_align = "center"
width_pct = 100

[styles.lap-text]
font_size = 24
color = [1.0, 0.85, 0.2, 1.0]
width = 120
height = 30
x = -10
y = 10

Style properties:

PropertyTypeDefaultDescription
x, yfloat0Offset from anchor point or parent
width, heightfloat0Fixed dimensions in logical points
width_pct, height_pctfloatPercentage of parent width/height (0–100)
height_autoboolfalseAuto-size height from children extent
color[r,g,b,a][1,1,1,1]Primary color (text color, shape fill)
bg_color[r,g,b,a][0,0,0,0]Background color (panels)
font_sizefloat16Text font size
text_alignstring"left"Text alignment: left, center, right
roundingfloat0Corner rounding for panels/rects
opacityfloat1.0Element opacity multiplier
thicknessfloat1Stroke thickness for outlines
radiusfloat0Circle radius
layerint0Render layer (negative = behind, positive = in front)
padding[l,t,r,b][0,0,0,0]Interior padding (left, top, right, bottom)
layoutstring"stack"Child flow: stack (vertical) or horizontal
margin_bottomfloat0Space below element in flow layout

Rhai API: Data-Driven UI

FunctionReturnsDescription
load_ui(layout_path)i64Load a .ui.toml document. Returns a handle (-1 on error)
unload_ui(handle)Unload a previously loaded UI document
ui_set_text(element_id, text)Change an element’s text content
ui_show(element_id)Show an element
ui_hide(element_id)Hide an element
ui_set_visible(element_id, visible)Set element visibility
ui_set_color(element_id, r, g, b, a)Override primary color
ui_set_bg_color(element_id, r, g, b, a)Override background color
ui_set_style(element_id, prop, value)Override any style property by name
ui_reset_style(element_id)Clear all runtime overrides
ui_set_class(element_id, class)Switch an element’s style class
ui_exists(element_id)boolCheck if an element exists in any loaded document
ui_get_rect(element_id)Map or ()Get resolved screen rect as #{x, y, w, h}

Element IDs are the TOML key names from the layout file (e.g., "speed_label", "lap_counter"). Functions search all loaded documents when resolving an element ID.

Example: Menu with Data-Driven UI

# ui/main_menu.ui.toml
[ui]
name = "Main Menu"
style = "ui/main_menu.style.toml"

[elements.title]
type = "text"
anchor = "top-center"
class = "title"
text = "MY GAME"

[elements.menu_panel]
type = "panel"
anchor = "center"
class = "menu-container"

[elements.btn_play]
type = "text"
parent = "menu_panel"
class = "menu-item"
text = "Play"

[elements.btn_quit]
type = "text"
parent = "menu_panel"
class = "menu-item"
text = "Quit"
#![allow(unused)]
fn main() {
// scripts/menu.rhai
let menu_handle = 0;
let selected = 0;

fn on_init() {
    menu_handle = load_ui("ui/main_menu.ui.toml");
}

fn on_update() {
    // Highlight selected item
    if selected == 0 {
        ui_set_color("btn_play", 1.0, 0.85, 0.2, 1.0);
        ui_set_color("btn_quit", 0.6, 0.6, 0.6, 1.0);
    } else {
        ui_set_color("btn_play", 0.6, 0.6, 0.6, 1.0);
        ui_set_color("btn_quit", 1.0, 0.85, 0.2, 1.0);
    }

    if is_action_just_pressed("move_forward") { selected = 0; }
    if is_action_just_pressed("move_backward") { selected = 1; }

    if is_action_just_pressed("interact") {
        if selected == 0 { load_scene("scenes/level1.scene.toml"); }
    }
}
}

When to Use Each UI Approach

ApproachBest For
Data-driven (.ui.toml + .style.toml)Menus, HUD panels, dialog boxes, score displays — anything with stable structure
Procedural (draw_* API)Crosshairs, damage flashes, debug overlays, dynamic effects — anything computed per-frame
Both togetherLoad a HUD layout for structure, use draw_* for dynamic overlays on top

Hot-Reload

The script system checks file modification timestamps each frame. When a .rhai file changes on disk:

  1. The file is recompiled to a new AST
  2. If compilation succeeds, the old AST is replaced and the new version takes effect immediately
  3. If compilation fails, the old AST is kept and an error is logged — the game never crashes from a script typo

This enables a fast iteration workflow: edit a script in your text editor, save, and see the result in the running game without restarting.

Interactable System

The interactable component marks entities that the player can interact with at close range. It works together with scripting to create interactive objects:

[entities.tavern_door]
archetype = "door"

[entities.tavern_door.interactable]
prompt_text = "Open Door"
range = 3.0
interaction_type = "use"
enabled = true

[entities.tavern_door.script]
source = "door_interact.rhai"
FieldTypeDefaultDescription
prompt_textstring"Interact"Text shown on the HUD when in range
rangef323.0Maximum interaction distance from the player
interaction_typestring"use"Type of interaction: use, talk, examine
enabledbooltrueWhether this interactable is currently active

When the player is within range of an enabled interactable entity, the HUD displays a crosshair and the prompt_text. Pressing the Interact key (E) fires the on_interact callback on the entity’s script.

The find_nearest_interactable() function scans all interactable entities each frame to determine which (if any) to highlight. The HUD prompt fades in and out based on proximity.

Example: Interactive Door

#![allow(unused)]
fn main() {
// scripts/door_interact.rhai

let door_open = false;

fn on_interact() {
    let me = self_entity();
    door_open = !door_open;

    if door_open {
        play_clip(me, "door_swing");
        play_sound("door_open");
        log("Door opened");
    } else {
        play_clip(me, "door_close");
        play_sound("door_close");
        log("Door closed");
    }
}
}

Example: Flickering Torch

#![allow(unused)]
fn main() {
// scripts/torch_flicker.rhai

fn on_update() {
    let me = self_entity();
    let t = total_time();

    // Flicker the emissive intensity with layered sine waves
    let flicker = 0.8 + 0.2 * sin(t * 8.0) * sin(t * 13.0 + 0.7);
    set_field(me, "material", "emissive_strength", clamp(flicker, 0.3, 1.0));
}
}

Example: NPC Bartender

#![allow(unused)]
fn main() {
// scripts/bartender.rhai

fn on_init() {
    let me = self_entity();
    play_clip(me, "idle");
    log("Bartender ready to serve");
}

fn on_interact() {
    let me = self_entity();
    let player = get_entity("player");
    let dist = distance(me, player);

    // Face the player
    let my_pos = get_position(me);
    let player_pos = get_position(player);
    let angle = atan2(player_pos.x - my_pos.x, player_pos.z - my_pos.z);
    set_rotation(me, 0.0, angle * 57.2958, 0.0);

    // React
    play_sound("glass_clink");
    blend_to(me, "wave", 0.3);
    log("Bartender waves at you");
}
}

Architecture

on_init ──► ScriptEngine.call_inits()
                │
                ▼
            per-entity Scope + AST
                │
                ▼
on_update ──► ScriptEngine.call_updates()
                │
                ▼
events ────► ScriptEngine.process_events()
                │                    │
                ▼                    ▼
        ECS reads/writes      ScriptCommands
        (via ScriptCallContext)  (PlaySound, FireEvent, Log)
                                     │
on_draw_ui ► ScriptEngine            ▼
                │              PlayerApp processes
                ▼              deferred commands
          DrawCommands
          (Text, Rect, Circle,
           Line, Sprite)
                │
                ▼
          egui layer_painter()
          renders 2D overlay

Each entity gets its own Rhai Scope, preserving persistent variables between frames. The Engine is shared across all entities. World access happens through a ScriptCallContext that holds a raw pointer to the FlintWorld — valid only during the call batch, cleared immediately after.

Example: Combat HUD

For game-specific UI, use a dedicated hud_controller entity with a script component. The entity has no physical presence in the world — it exists only to run the HUD script:

[entities.hud_controller]

[entities.hud_controller.script]
source = "hud.rhai"
#![allow(unused)]
fn main() {
// scripts/hud.rhai

fn on_draw_ui() {
    let sw = screen_width();
    let sh = screen_height();

    // Crosshair
    let cx = sw / 2.0;
    let cy = sh / 2.0;
    draw_line(cx - 10.0, cy, cx + 10.0, cy, 0.0, 1.0, 0.0, 0.8, 2.0);
    draw_line(cx, cy - 10.0, cx, cy + 10.0, 0.0, 1.0, 0.0, 0.8, 2.0);

    // Health bar
    let player = get_entity("player");
    if player != -1 && has_component(player, "health") {
        let hp = get_field(player, "health", "current_hp");
        let max_hp = get_field(player, "health", "max_hp");
        let pct = hp / max_hp;

        draw_rect(20.0, sh - 40.0, 200.0, 20.0, 0.2, 0.2, 0.2, 0.8);
        draw_rect(20.0, sh - 40.0, 200.0 * pct, 20.0, 0.8, 0.1, 0.1, 0.9);
        draw_text(25.0, sh - 38.0, `HP: ${hp}/${max_hp}`, 14.0, 1.0, 1.0, 1.0, 1.0);
    }

    // Interaction prompt
    let interact = find_nearest_interactable();
    if interact != () {
        let prompt = interact.prompt_text;
        let tw = measure_text(prompt, 18.0);
        draw_text(cx - tw.width / 2.0, cy + 40.0, `[E] ${prompt}`, 18.0, 1.0, 1.0, 1.0, 0.9);
    }
}
}

This pattern keeps all game-specific HUD logic in scripts rather than engine code. The engine provides only the generic draw primitives.

Further Reading

2D Sprites

Flint’s 2D sprite system provides GPU-instanced flat-quad rendering for side-scrollers, top-down games, and any project that needs sprite-based visuals. It sits alongside the existing 3D billboard sprite pipeline but uses an orthographic camera and XY-plane alignment instead of camera-facing quads.

How It Works

2D sprites render as axis-aligned quads on the XY plane using a dedicated Sprite2dPipeline in flint-render. The pipeline follows the same storage-buffer instancing pattern as the particle system — one draw call per texture batch, quads generated from vertex_index in the shader, no vertex buffers needed.

Scene TOML              CPU collection              GPU rendering
sprite component  ──►  Collect instances   ──►  Sprite2dPipeline
  mode = "sprite2d"    batch by texture           instanced draw
  texture, layer       sort by layer              storage buffer
  tint, flip           pack Sprite2dInstanceGpu    orthographic proj

Key differences from 3D billboard sprites:

BillboardSprite2D
OrientationAlways faces cameraFixed on XY plane
CameraPerspective (3D scene)Orthographic
Z-orderingDepth bufferLayer field (integer)
Depth writeYes (binary alpha discard)No (layer-based sorting)
Use caseItems/NPCs in 3D world2D games

Setting Up a 2D Scene

A 2D scene needs an orthographic camera and sprites set to sprite2d mode:

[scene]
name = "My 2D Game"

[camera]
projection = "orthographic"
ortho_height = 10.0            # visible height in world units
position = [0.0, 0.0, 50.0]   # Z offset doesn't affect ortho rendering
target = [0.0, 0.0, 0.0]

[entities.player]
archetype = "sprite"
[entities.player.transform]
position = [0.0, 2.0, 0.0]
[entities.player.sprite]
mode = "sprite2d"
texture = "player.png"
width = 1.0
height = 2.0
layer = 10
tint = [1.0, 1.0, 1.0, 1.0]

[entities.background]
archetype = "sprite"
[entities.background.transform]
position = [0.0, 0.0, 0.0]
[entities.background.sprite]
mode = "sprite2d"
texture = "sky.png"
width = 20.0
height = 12.0
layer = 0

The layer field controls draw order — higher layers render in front. Unlike depth-buffer sorting, this gives you explicit, deterministic control over what overlaps what.

Sprite Component

The sprite component works for both billboard and sprite2d modes. Set mode = "sprite2d" to switch to flat rendering.

FieldTypeDefaultDescription
texturestring""Sprite texture file (empty = solid color from tint)
widthf321.0World-space width
heightf321.0World-space height
modestring"billboard""billboard" (3D camera-facing) or "sprite2d" (flat XY quad)
layeri320Z-order layer for 2D sorting (higher = in front)
tintvec4[1,1,1,1]RGBA color multiplier
flip_xboolfalseMirror horizontally
flip_yboolfalseMirror vertically
source_rectvec4[0,0,0,0]Source rectangle in pixels [x, y, w, h]; [0,0,0,0] = full texture
anchor_yf320.0Vertical anchor (0 = bottom, 0.5 = center)
framei320Current sprite sheet frame index
frames_xi321Columns in sprite sheet
frames_yi321Rows in sprite sheet
fullbrightbooltrueBypass PBR lighting (always true for 2D)
visiblebooltrueShow/hide the sprite

Texture Atlases

For sprite sheets and texture packing, Flint supports .atlas.toml sidecar files that define named regions within a texture:

# sprites/player_sheet.atlas.toml
[atlas]
texture = "player_sheet.png"

[atlas.regions]
idle_0  = [0,   0, 64, 64]
idle_1  = [64,  0, 64, 64]
run_0   = [0,  64, 64, 64]
run_1   = [64, 64, 64, 64]
run_2   = [128, 64, 64, 64]
run_3   = [192, 64, 64, 64]

Each region is [x, y, width, height] in pixel coordinates. The AtlasRegistry loads all .atlas.toml files from a directory and resolves region names to UV rectangles at runtime.

Sprite Sheet Animation

The flint-animation crate provides frame-based sprite animation through .sprite.toml clip files and the sprite_animator component.

Defining Clips

Animation clips live in .sprite.toml files alongside your scene:

# animations/character.sprite.toml

[animation.idle]
texture = "character_sheet.png"
frame_size = [64, 64]
frames = [
    { col = 0, row = 0, duration_ms = 300 },
    { col = 1, row = 0, duration_ms = 300 },
]
loop_mode = "loop"

[animation.run]
texture = "character_sheet.png"
frame_size = [64, 64]
frames = [
    { col = 0, row = 1, duration_ms = 100 },
    { col = 1, row = 1, duration_ms = 100 },
    { col = 2, row = 1, duration_ms = 100 },
    { col = 3, row = 1, duration_ms = 100 },
]
loop_mode = "loop"

[animation.jump]
texture = "character_sheet.png"
frame_size = [64, 64]
frames = [
    { col = 0, row = 2, duration_ms = 150 },
    { col = 1, row = 2, duration_ms = 200 },
    { col = 2, row = 2, duration_ms = 300 },
]
loop_mode = "once"

Each frame specifies a column and row in the sprite sheet, plus a duration in milliseconds. A single file can contain multiple named clips.

Loop Modes

ModeBehavior
loopRepeats indefinitely from the start
ping_pongPlays forward then backward, repeating
oncePlays once and stops on the last frame

Attaching Animation

Add a sprite_animator component alongside the sprite component:

[entities.player]
archetype = "animated_sprite"
[entities.player.transform]
position = [0.0, 2.0, 0.0]
[entities.player.sprite]
mode = "sprite2d"
texture = "character_sheet.png"
width = 2.0
height = 2.0
[entities.player.sprite_animator]
clip = "idle"
playing = true
speed = 1.0

The animated_sprite archetype bundles transform, sprite (mode = sprite2d), and sprite_animator together.

Sprite Animator Component

FieldTypeDefaultDescription
clipstring""Active animation clip name
playingboolfalseWhether animation is playing
speedf321.0Playback speed multiplier (0–10)

How Playback Works

SpriteAnimSync runs each frame during the animation update:

  1. Sync from world — reads sprite_animator.clip and sprite_animator.playing from the ECS, detects changes
  2. Advance — steps playback time by delta * speed, finds the current frame in the clip
  3. Write back — computes the source rectangle from the frame’s col/row and writes sprite.source_rect back to the ECS

The source rectangle tells the GPU which portion of the sprite sheet to display. This happens automatically — scripts only need to set the clip name and playing state.

Scripting Integration

Control sprite animation from Rhai scripts:

FunctionDescription
sprite_play(entity_id, clip_name)Start playing a clip
sprite_stop(entity_id)Stop playback
sprite_set_speed(entity_id, speed)Set playback speed
sprite_is_playing(entity_id)Check if currently playing

The on_animation_end(clip_name) callback fires when a once clip finishes:

#![allow(unused)]
fn main() {
// Rhai script: character animation controller
fn on_init() {
    let me = self_entity();
    sprite_play(me, "idle");
}

fn on_update() {
    let me = self_entity();
    let vx = 0.0;

    if action_held("move_right") { vx = 1.0; }
    if action_held("move_left")  { vx = -1.0; }

    if vx != 0.0 {
        sprite_play(me, "run");
        set_field(me, "sprite", "flip_x", vx < 0.0);
    } else {
        sprite_play(me, "idle");
    }
}

fn on_animation_end(clip) {
    if clip == "jump" {
        sprite_play(self_entity(), "idle");
    }
}
}

Architecture

  • Sprite2dPipeline (flint-render) — wgpu render pipeline with orthographic projection, storage buffer instancing, per-texture batching
  • Sprite2dInstanceGpu — 80-byte per-instance struct: position, size, UV rect, tint, flip flags
  • TextureAtlas / AtlasRegistry (flint-render) — parses .atlas.toml files, resolves region names to pixel rectangles
  • SpriteAnimClip (flint-animation) — frame sequence with per-frame duration and loop mode
  • SpriteAnimSync (flint-animation) — bridges ECS sprite_animator components to playback state, writes source_rect back each frame

Further Reading

  • Touch Input — mobile-friendly input for 2D games
  • Animation — property tweens and skeletal animation
  • Scripting — full scripting API including sprite animation functions
  • Rendering — the GPU pipeline architecture

Touch Input

Flint’s input system extends seamlessly to touchscreens through touch zones — named screen regions that map to the same action bindings used by keyboard and gamepad. A single input config can drive a game on desktop and mobile without code changes.

How It Works

Touch input integrates into the existing InputState system in flint-runtime:

Touch event (OS)         Normalized tracking         Action evaluation
TouchStart(id, x, y) ──► TouchPoint { id, pos,  ──► binding_value(TouchZone)
TouchMove(id, x, y)      start_pos, phase,          checks zone containment
TouchEnd(id)              start_time }               produces action values

Touch coordinates are normalized to [0..1] range (0,0 = top-left, 1,1 = bottom-right), making zone definitions resolution-independent. Tap detection uses physics: a touch qualifies as a tap when elapsed time < 300ms and movement distance < 20 pixels.

Touch Zones

Touch zones are named rectangular screen regions. Five built-in zones cover the most common layouts:

ZoneRegionCommon Use
full_screenEntire screenGlobal taps, swipes
left_halfLeft 50%Move left, D-pad left
right_halfRight 50%Move right, D-pad right
top_halfTop 50%Look up, jump
bottom_halfBottom 50%Look down, crouch

Zones are defined as normalized rectangles (x, y, width, height). For example, left_half is (0.0, 0.0, 0.5, 1.0).

Input Configuration

Touch zones use the same InputConfig TOML format as keyboard and gamepad bindings. A single action can have multiple binding types:

# input.toml
version = 1
game_id = "my_game"

[actions.move_left]
kind = "button"
[[actions.move_left.bindings]]
type = "key"
code = "KeyA"
[[actions.move_left.bindings]]
type = "touch_zone"
zone = "left_half"

[actions.move_right]
kind = "button"
[[actions.move_right.bindings]]
type = "key"
code = "KeyD"
[[actions.move_right.bindings]]
type = "touch_zone"
zone = "right_half"

[actions.jump]
kind = "button"
[[actions.jump.bindings]]
type = "key"
code = "Space"

Touch zone bindings support a scale field (default 1.0) that multiplies the action value, just like gamepad axis bindings.

Binding Format

[[actions.my_action.bindings]]
type = "touch_zone"
zone = "left_half"     # Zone name (one of the 5 built-in zones)
scale = 1.0            # Optional: action value multiplier

Mouse-as-Touch Emulation

By default, Flint emulates touch input from mouse clicks on desktop. Left-click-and-drag produces touch events as finger ID 0, letting you test touch-based games without a touchscreen.

  • Enabled by default (emulate_touch_from_mouse = true)
  • Automatically disabled when a real touch event arrives
  • Left mouse button maps to finger 0
  • Mouse position maps to touch position

This means touch-zone bindings work immediately on desktop during development.

Tap Detection

Taps are detected automatically when a touch ends:

  • Duration < 300ms (from touch start to touch end)
  • Distance < 20 pixels (from start position to end position)

Taps are available for one frame after detection and are consumed on read.

Scripting API

Touch state is accessible from Rhai scripts via these functions:

Touch Tracking

FunctionReturnsDescription
touch_count()i64Number of currently active touches
touch_x(index)f64Normalized X position (0–1) of touch at index
touch_y(index)f64Normalized Y position (0–1) of touch at index
is_touching(id)boolWhether the given touch ID is currently active
touch_just_started(id)boolWhether the touch ID just became active this frame
touch_just_ended(id)boolWhether the touch ID just ended this frame

Tap Detection

FunctionReturnsDescription
tap_count()i64Number of taps detected this frame
tap_x(index)f64Normalized X position of tap at index
tap_y(index)f64Normalized Y position of tap at index

Example: Touch-Driven Movement

#![allow(unused)]
fn main() {
// scripts/touch_controller.rhai
fn on_update() {
    let me = self_entity();
    let speed = 5.0;
    let dt = delta_time();

    // Action-based movement works with both keyboard and touch
    if action_held("move_left") {
        let pos = get_position(me);
        set_position(me, pos.x - speed * dt, pos.y, pos.z);
    }
    if action_held("move_right") {
        let pos = get_position(me);
        set_position(me, pos.x + speed * dt, pos.y, pos.z);
    }

    // Direct tap handling for jumping
    if tap_count() > 0 {
        // Jump on any tap
        fire_event("jump");
    }
}

fn on_draw_ui() {
    // Visualize active touches (useful for debugging)
    let w = screen_width();
    let h = screen_height();

    for i in 0..touch_count() {
        let tx = touch_x(i) * w;
        let ty = touch_y(i) * h;
        draw_circle(tx, ty, 20.0, 1.0, 1.0, 1.0, 0.4);
    }
}
}

Design Philosophy

The touch system is intentionally minimal. Rather than providing virtual joysticks, gesture recognizers, or complex multi-touch state machines, it gives you:

  1. Zone-based action bindings — works with the existing input config system
  2. Raw touch state — positions, phases, and tap detection exposed to scripts
  3. Mouse emulation — desktop testing without hardware

Game-specific touch UI (virtual D-pads, on-screen buttons, swipe gestures) belongs in scripts, not the engine. The engine provides the primitives; scripts compose them into the interaction model that fits each game.

Further Reading

AI Asset Generation

Flint includes an integrated AI asset generation pipeline through the flint-asset-gen crate. The system connects to external AI services to produce textures, 3D models, and audio from text descriptions, while maintaining visual consistency through style guides and validating results against constraints.

Overview

The pipeline follows a request-enrich-generate-validate-catalog flow:

Description + Style Guide
        │
        ▼
  Prompt Enrichment (palette, materials, constraints)
        │
        ▼
  GenerationProvider (Flux / Meshy / ElevenLabs / Mock)
        │
        ▼
  Post-generation Validation (geometry, materials)
        │
        ▼
  Content-Addressed Storage + Asset Catalog

Providers

Flint uses a pluggable GenerationProvider trait. Each provider handles one or more asset types:

ProviderAsset TypesServiceDescription
FluxTexturesFlux APIAI image generation for PBR textures
Meshy3D ModelsMeshy APIText-to-3D model generation (GLB output)
ElevenLabsAudioElevenLabs APIAI sound effect and voice generation
MockAllLocalGenerates minimal valid files for testing without network access

The GenerationProvider trait defines the interface:

#![allow(unused)]
fn main() {
pub trait GenerationProvider: Send {
    fn name(&self) -> &str;
    fn supported_kinds(&self) -> Vec<AssetKind>;
    fn health_check(&self) -> Result<ProviderStatus>;
    fn generate(&self, request: &GenerateRequest, style: Option<&StyleGuide>, output_dir: &Path) -> Result<GenerateResult>;
    fn submit_job(&self, request: &GenerateRequest, style: Option<&StyleGuide>) -> Result<GenerationJob>;
    fn poll_job(&self, job: &GenerationJob) -> Result<JobPollResult>;
    fn download_result(&self, job: &GenerationJob, output_dir: &Path) -> Result<GenerateResult>;
    fn build_prompt(&self, request: &GenerateRequest, style: Option<&StyleGuide>) -> String;
}
}

The Mock provider generates solid-color PNGs, minimal valid GLB files, and silence WAV files — useful for testing workflows and CI pipelines without API keys or network access.

Style Guides

Style guides enforce visual consistency across generated assets. They are TOML files that define a palette, material constraints, geometry constraints, and prompt modifiers:

# styles/medieval_tavern.style.toml
[style]
name = "medieval_tavern"
description = "Weathered medieval fantasy tavern"
prompt_prefix = "Medieval fantasy tavern style, low-fantasy realism"
prompt_suffix = "Photorealistic textures, warm candlelight tones"
negative_prompt = "modern, sci-fi, neon, plastic, chrome"
palette = ["#8B4513", "#A0522D", "#D4A574", "#4A4A4A", "#2F1B0E"]

[style.materials]
roughness_range = [0.6, 0.95]
metallic_range = [0.0, 0.15]
preferred_materials = ["aged oak wood", "rough-hewn stone", "hammered wrought iron"]

[style.geometry]
max_triangles = 5000
require_uvs = true
require_normals = true

When a style guide is active, the provider enriches the generation prompt by prepending the prompt_prefix, appending palette colors and material descriptors, and adding the prompt_suffix. The negative_prompt tells AI services what to avoid.

Style guides are searched in styles/ then .flint/styles/ by name (e.g., medieval_tavern finds styles/medieval_tavern.style.toml).

Semantic Asset Definitions

The asset_def component describes what an entity needs in terms of assets, expressed as intent rather than file paths:

[entities.tavern_wall]
archetype = "wall"

[entities.tavern_wall.asset_def]
name = "tavern_wall_texture"
description = "Rough stone wall with mortar lines, medieval tavern interior"
type = "texture"
material_intent = "rough stone"
wear_level = 0.7
tags = ["wall", "interior", "medieval"]
FieldTypeDescription
namestringAsset name identifier
descriptionstringWhat this asset is for (used as the generation prompt)
typestringAsset type: texture, model, or audio
material_intentstringMaterial intent (e.g., “aged wood”, “rough stone”)
wear_levelf32How worn/damaged (0.0 = pristine, 1.0 = heavily worn)
size_classstringSize class: small, medium, large, huge
tagsarrayTags for categorization

These definitions let the batch resolver automatically generate all assets a scene needs.

Batch Resolution

The flint asset resolve command can resolve an entire scene’s asset needs at once using different strategies:

StrategyBehavior
strictAll assets must already exist in the catalog. Missing assets are errors.
placeholderMissing assets get placeholder geometry.
ai_generateMissing assets are generated via AI providers and stored in the catalog.
human_taskMissing assets produce task files for manual creation.
ai_then_humanGenerate with AI first, then produce review tasks for human approval.
# Generate all missing assets for a scene using AI
flint asset resolve my_scene.scene.toml --strategy ai_generate --style medieval_tavern

# Create task files for a human artist
flint asset resolve my_scene.scene.toml --strategy human_task --output-dir tasks/

Model Validation

After generating a 3D model, Flint can validate it against a style guide’s constraints. The validator imports the GLB file through the same import_gltf() pipeline used by the player, then checks:

  • Triangle count against geometry.max_triangles
  • UV coordinates present if geometry.require_uvs is set
  • Normals present if geometry.require_normals is set
  • Material properties against materials.roughness_range and materials.metallic_range
flint asset validate model.glb --style medieval_tavern

Each check reports Pass, Warn, or Fail status.

Build Manifests

Build manifests track the provenance of all generated assets in a project. They record which provider generated each asset, what prompt was used, and the content hash:

flint asset manifest --assets-dir assets --output build/manifest.toml

The manifest scans .asset.toml sidecar files for provider properties to identify which assets were AI-generated vs. manually created. This is useful for auditing, reproducing builds, and tracking which assets need regeneration when style guides change.

Configuration

Flint uses a layered configuration system for API keys and provider settings:

Global config (~/.flint/config.toml):

[providers.flux]
api_key = "your-flux-key"
enabled = true

[providers.meshy]
api_key = "your-meshy-key"
enabled = true

[providers.elevenlabs]
api_key = "your-elevenlabs-key"
enabled = true

[generation]
default_style = "medieval_tavern"

Project config (.flint/config.toml): overrides global settings for this project.

Environment variables: override both config files:

  • FLINT_FLUX_API_KEY
  • FLINT_MESHY_API_KEY
  • FLINT_ELEVENLABS_API_KEY

The layering order is: global config < project config < environment variables.

CLI Commands

CommandDescription
flint asset generate <type> -d "<prompt>"Generate a single asset
flint asset generate texture -d "stone wall" --style medieval_tavernGenerate with style guide
flint asset generate model -d "wooden chair" --provider meshyGenerate with specific provider
flint asset resolve <scene> --strategy ai_generateBatch-generate all missing scene assets
flint asset validate <file> --style <name>Validate a model against style constraints
flint asset manifestGenerate a build manifest of all generated assets
flint asset regenerate <name> --seed 42Regenerate an existing asset with a new seed
flint asset job status <id>Check status of an async generation job
flint asset job listList all generation jobs

Runtime Catalog Integration

The player can optionally load the asset catalog at startup for runtime asset resolution. When an entity references an asset by name, the resolution chain is:

  1. Look up the name in the AssetCatalog
  2. If found, resolve the content hash
  3. Load from the ContentStore path (.flint/assets/<hash>)
  4. Fall back to file-based loading if not in the catalog

This means scenes can seamlessly reference both pre-imported and AI-generated assets by name, without hardcoding file paths.

Further Reading

Building a Tavern

The finished tavern — atmospheric interior with NPCs, fireplace, and furniture

This tutorial walks through building a complete tavern scene from scratch using Flint’s CLI. By the end, you’ll have a walkable tavern with physics, audio, animation, scripted NPCs, and interactive objects.

1. Initialize the Project

flint init tavern-game
cd tavern-game

This creates a project directory with schemas/ containing default component and archetype definitions.

2. Create the Scene

flint scene create levels/tavern.scene.toml --name "The Rusty Flagon"

3. Build the Rooms

Create the tavern’s three rooms using parent-child hierarchy:

SCENE="levels/tavern.scene.toml"

# Main hall
flint entity create --archetype room --name "main_hall" --scene $SCENE
flint entity create --archetype room --name "kitchen" --scene $SCENE
flint entity create --archetype room --name "storage" --scene $SCENE

Now edit the TOML directly to set positions and dimensions. Each room needs a transform and bounds:

[entities.main_hall]
archetype = "room"

[entities.main_hall.transform]
position = [0.0, 0.0, 0.0]

[entities.main_hall.bounds]
size = [15.0, 4.0, 12.0]

4. Add Physics Colliders

For the scene to be walkable, surfaces need physics colliders. Add walls, floor, and ceiling:

[entities.floor]
archetype = "wall"

[entities.floor.transform]
position = [0.0, -0.25, 0.0]

[entities.floor.collider]
shape = "box"
size = [20.0, 0.5, 20.0]

[entities.floor.rigidbody]
body_type = "static"

[entities.north_wall]
archetype = "wall"

[entities.north_wall.transform]
position = [0.0, 2.0, -10.0]

[entities.north_wall.collider]
shape = "box"
size = [20.0, 4.0, 0.5]

[entities.north_wall.rigidbody]
body_type = "static"

Repeat for all walls. Static rigidbodies are immovable world geometry that the player collides with.

5. Create the Player

The player entity bundles a character controller, transform, and audio listener:

[entities.player]
archetype = "player"

[entities.player.transform]
position = [0.0, 1.0, 5.0]

[entities.player.character_controller]
move_speed = 6.0
jump_force = 7.0
height = 1.8
radius = 0.3

6. Add Furniture

Place objects throughout the tavern:

[entities.bar_counter]
archetype = "furniture"

[entities.bar_counter.transform]
position = [-3.0, 0.5, -2.0]
scale = [3.0, 1.0, 0.8]

[entities.bar_counter.collider]
shape = "box"
size = [3.0, 1.0, 0.8]

[entities.bar_counter.rigidbody]
body_type = "static"

[entities.fireplace]
archetype = "furniture"

[entities.fireplace.transform]
position = [5.0, 0.5, -8.0]

[entities.fireplace.material]
emissive = [1.0, 0.4, 0.1]
emissive_strength = 2.0

7. Add Audio

Attach spatial sounds to entities:

[entities.fireplace.audio_source]
file = "audio/fire_crackle.ogg"
volume = 0.8
loop = true
spatial = true
min_distance = 1.0
max_distance = 15.0

[entities.ambience]

[entities.ambience.audio_source]
file = "audio/tavern_ambient.ogg"
volume = 0.3
loop = true
spatial = false

Place audio files (OGG, WAV, MP3, or FLAC) in the audio/ directory next to the scene.

8. Add Animations

Create animation clips in animations/:

# animations/platform_bob.anim.toml
name = "platform_bob"
duration = 4.0

[[tracks]]
interpolation = "CubicSpline"

[tracks.target]
type = "Position"

[[tracks.keyframes]]
time = 0.0
value = [2.0, 0.5, 3.0]

[[tracks.keyframes]]
time = 2.0
value = [2.0, 1.5, 3.0]

[[tracks.keyframes]]
time = 4.0
value = [2.0, 0.5, 3.0]

Attach an animator to the entity:

[entities.platform.animator]
clip = "platform_bob"
autoplay = true
loop = true
speed = 1.0

9. Add Interactable Objects

Make the door interactive with a script:

[entities.front_door]
archetype = "door"

[entities.front_door.transform]
position = [0.0, 1.0, -5.0]

[entities.front_door.interactable]
prompt_text = "Open Door"
range = 3.0
interaction_type = "use"

[entities.front_door.script]
source = "door_interact.rhai"

Create the script in scripts/door_interact.rhai:

#![allow(unused)]
fn main() {
let door_open = false;

fn on_interact() {
    let me = self_entity();
    door_open = !door_open;

    if door_open {
        play_clip(me, "door_swing");
        play_sound("door_open");
    } else {
        play_clip(me, "door_close");
        play_sound("door_close");
    }
}
}

10. Add NPCs

Create NPC entities with scripts for behavior:

[entities.bartender]
archetype = "npc"

[entities.bartender.transform]
position = [-3.0, 0.0, -3.0]

[entities.bartender.interactable]
prompt_text = "Talk to Bartender"
range = 3.0
interaction_type = "talk"

[entities.bartender.script]
source = "bartender.rhai"

11. Validate and Test

# Check the scene against constraints
flint validate levels/tavern.scene.toml

# View in the scene viewer with hot-reload
flint serve levels/tavern.scene.toml --watch

# Walk through the tavern in first person
flint play levels/tavern.scene.toml

12. The Finished Result

The demo/phase4_runtime.scene.toml in the Flint repository is a complete implementation of this tavern, with:

  • Three rooms (main hall, kitchen, storage) with physics colliders on all surfaces
  • A bar counter, tables, fireplace, and barrels
  • Four NPCs: bartender, two patrons, and a mysterious stranger with scripted behaviors
  • Spatial audio: fire crackle, ambient tavern noise, door sounds, glass clinks
  • Property animations: bobbing platform, door swings
  • Interactable doors and NPCs with HUD prompts
  • Footstep sounds synced to player movement
# Try the finished demo
cargo run --bin flint -- play demo/phase4_runtime.scene.toml

Further Reading

Writing Constraints

This guide walks through authoring constraint rules for your Flint project. Constraints are declarative TOML rules that define what a valid scene looks like, checked by flint validate.

Anatomy of a Constraint File

Constraint files live in schemas/constraints/ and contain one or more [[constraint]] entries:

[[constraint]]
name = "unique_identifier"
description = "Human-readable explanation"
query = "entities where <condition>"
severity = "error"
message = "Violation message for '{name}'"

[constraint.kind]
type = "<kind>"
# kind-specific fields...
  • name — unique identifier, used in logs and JSON output
  • description — what the rule checks (for documentation)
  • query — which entities this constraint applies to
  • severity"error" fails validation, "warning" is advisory
  • message — shown when violated. {name} is replaced with the entity name

Choosing the Right Kind

required_component — “Entity X must have component Y”

The most common kind. Use when an archetype needs a specific component:

[[constraint]]
name = "doors_have_transform"
description = "Every door must have a transform"
query = "entities where archetype == 'door'"
severity = "error"
message = "Door '{name}' is missing a transform component"

[constraint.kind]
type = "required_component"
archetype = "door"
component = "transform"

value_range — “Field X must be between A and B”

Validates that a numeric field is within bounds:

[[constraint]]
name = "door_angle_range"
description = "Door open angle must be between 0 and 180"
query = "entities where archetype == 'door'"
severity = "warning"
message = "Door '{name}' has an invalid open_angle"

[constraint.kind]
type = "value_range"
field = "door.open_angle"
min = 0.0
max = 180.0

required_child — “Entity X must have a child of archetype Y”

Enforces parent-child relationships:

[[constraint]]
name = "rooms_have_door"
description = "Every room needs at least one exit"
query = "entities where archetype == 'room'"
severity = "error"
message = "Room '{name}' has no door"

[constraint.kind]
type = "required_child"
archetype = "room"
child_archetype = "door"

reference_valid — “This reference field must point to an existing entity”

Checks referential integrity:

[[constraint]]
name = "door_target_exists"
description = "Door target room must exist"
query = "entities where archetype == 'door'"
severity = "error"
message = "Door '{name}' references a non-existent target"

[constraint.kind]
type = "reference_valid"
field = "door.target_room"

query_rule — “This query must return the expected count”

The most flexible kind, for arbitrary rules:

[[constraint]]
name = "one_player"
description = "Playable scenes need exactly one player"
query = "entities where archetype == 'player'"
severity = "error"
message = "Scene must have exactly one player entity"

[constraint.kind]
type = "query_rule"
rule_query = "entities where archetype == 'player'"
expected = "exactly_one"

Auto-Fix Strategies

Add a [constraint.fix] section to enable automatic repair:

[[constraint]]
name = "rooms_have_bounds"
query = "entities where archetype == 'room'"
severity = "error"
message = "Room '{name}' needs bounds"

[constraint.kind]
type = "required_component"
archetype = "room"
component = "bounds"

[constraint.fix]
strategy = "set_default"

Available strategies:

  • set_default — add the missing component with schema defaults
  • add_child — create a child entity of the required archetype
  • remove_invalid — remove entities that violate the rule
  • assign_from_parent — copy a value from the parent entity

Testing Constraints

Always test with --dry-run first to preview changes:

# See what violations exist
flint validate levels/tavern.scene.toml --schemas schemas

# Preview auto-fix changes without applying
flint validate levels/tavern.scene.toml --fix --dry-run

# Apply fixes
flint validate levels/tavern.scene.toml --fix

JSON output gives machine-readable results for CI:

flint validate levels/tavern.scene.toml --format json

Organizing Constraint Files

Group related constraints into files by topic:

schemas/constraints/
├── basics.toml          # Fundamental rules (transform required, etc.)
├── physics.toml         # Physics constraints (collider sizes, mass ranges)
├── audio.toml           # Audio constraints (volume ranges, spatial settings)
└── gameplay.toml        # Game-specific rules (one player, door connectivity)

All .toml files in schemas/constraints/ are loaded automatically.

Cascade Detection

When auto-fix modifies one entity, it might cause a different constraint to fail. Flint handles this by running fix-validate cycles. If a cycle is detected (the same violation keeps appearing), the fixer stops and reports the issue.

Further Reading

Importing Assets

This guide walks through importing external files into Flint’s content-addressed asset store.

Basic Import

Import a glTF model with the flint asset import command:

flint asset import models/chair.glb --name tavern_chair --tags furniture,medieval

This does three things:

  1. Hashes the file (SHA-256) and stores it under .flint/assets/<hash>/
  2. Extracts mesh, material, and texture data (for glTF/GLB files)
  3. Writes a .asset.toml sidecar with metadata

Content-Addressed Storage

Every imported file is stored by its content hash:

.flint/
└── assets/
    ├── a1/
    │   └── a1b2c3d4e5f6...  (the actual file)
    └── f7/
        └── f7a8b9c0d1e2...  (another file)

The first two hex characters of the hash form a subdirectory, preventing any single directory from having too many entries. Identical files are automatically deduplicated — importing the same model twice stores it only once.

glTF/GLB Import

For 3D models, the importer extracts structured data:

$ flint asset import models/tavern_door.glb --name tavern_door
Imported: 3 mesh(es), 2 texture(s), 2 material(s)
Asset 'tavern_door' registered.
  Hash: sha256:a1b2c3...
  Type: Mesh
  Sidecar: assets/meshes/tavern_door.asset.toml

Extracted data includes:

  • Meshes — vertex positions, normals, texture coordinates, indices, and optionally joint indices/weights for skeletal meshes
  • Materials — PBR properties (base color, roughness, metallic, emissive)
  • Textures — embedded or referenced image files
  • Skeletons — joint hierarchy and inverse bind matrices (if the model has skins)
  • Animations — per-joint keyframe channels (translation, rotation, scale)

Sidecar Metadata

Each imported asset gets an .asset.toml file in the assets/ directory:

[asset]
name = "tavern_chair"
type = "mesh"
hash = "sha256:a1b2c3d4e5f6..."
source_path = "models/chair.glb"
format = "glb"
tags = ["furniture", "medieval"]

For AI-generated assets, the sidecar also records provenance:

[asset.properties]
prompt = "weathered wooden tavern chair"
provider = "meshy"

Tagging and Organization

Tags help organize and filter assets:

# Import with tags
flint asset import models/barrel.glb --name barrel --tags furniture,storage,medieval

# Filter by tag
flint asset list --tag medieval

# Filter by type
flint asset list --type mesh

# Get details on a specific asset
flint asset info tavern_chair

Asset Catalog

The catalog is built by scanning all .asset.toml files in the assets/ directory. It provides indexed lookup by name, type, and tag:

# List all assets
flint asset list

# JSON output for scripting
flint asset list --format json

Resolving References

Check that a scene’s asset references are satisfied:

# Strict mode --- all references must exist
flint asset resolve levels/tavern.scene.toml --strategy strict

# Placeholder mode --- missing assets replaced with fallback geometry
flint asset resolve levels/tavern.scene.toml --strategy placeholder

# AI generation --- missing assets created by AI providers
flint asset resolve levels/tavern.scene.toml --strategy ai_generate --style medieval_tavern

Supported Formats

FormatTypeImport Support
.glb, .gltf3D ModelFull (mesh, material, texture, skeleton, animation)
.png, .jpg, .bmp, .tga, .hdrTextureHash and catalog
.wav, .ogg, .mp3, .flacAudioHash and catalog
OtherGenericHash and catalog (type guessed from extension)

Further Reading

Headless Rendering

Flint can render scenes to PNG images without opening a window. This enables automated screenshots, visual regression testing, and CI pipeline integration.

The flint render Command

flint render levels/tavern.scene.toml --output preview.png

This loads the scene, renders a single frame with PBR shading and shadows, and writes the result to a PNG file.

Camera Options

Control the camera position with orbit-style parameters:

flint render levels/tavern.scene.toml \
    --output preview.png \
    --width 1920 --height 1080 \
    --distance 30 \
    --yaw 45 \
    --pitch 30
FlagDefaultDescription
--output <path>render.pngOutput file path
--width <px>1920Image width in pixels
--height <px>1080Image height in pixels
--distance <units>(auto)Camera distance from origin
--yaw <degrees>(auto)Horizontal camera angle
--pitch <degrees>(auto)Vertical camera angle
--target <x,y,z>(auto)Camera look-at point (comma-separated)
--fov <degrees>(auto)Field of view in degrees
--no-gridfalseDisable ground grid
--schemas <path>schemasPath to schemas directory (repeatable)

Post-Processing Flags

Control post-processing from the command line:

# Disable all post-processing (raw shader output)
flint render scene.toml --no-postprocess --output raw.png

# Custom bloom settings
flint render scene.toml --bloom-intensity 0.08 --bloom-threshold 0.8

# Adjust exposure
flint render scene.toml --exposure 1.5
FlagDefaultDescription
--no-postprocessfalseDisable entire post-processing pipeline
--bloom-intensity <f32>0.04Bloom mix strength
--bloom-threshold <f32>1.0Minimum brightness for bloom
--exposure <f32>1.0Exposure multiplier

Debug Rendering Flags

Render debug visualizations for diagnostics:

# Wireframe view
flint render scene.toml --debug-mode wireframe --output wireframe.png

# Surface normals
flint render scene.toml --debug-mode normals --output normals.png

# Other modes: depth, uv, unlit, metalrough
flint render scene.toml --debug-mode depth --output depth.png

# Wireframe overlay on solid geometry
flint render scene.toml --wireframe-overlay --output overlay.png

# Normal arrows
flint render scene.toml --show-normals --output arrows.png

# Raw linear output (no tonemapping)
flint render scene.toml --no-tonemapping --output linear.png
FlagDefaultDescription
--debug-mode <mode>(none)wireframe, normals, depth, uv, unlit, metalrough
--wireframe-overlayfalseDraw wireframe edges over solid shading
--show-normalsfalseDraw face-normal direction arrows
--no-tonemappingfalseDisable tonemapping for raw linear output
--no-shadowsfalseDisable shadow mapping
--shadow-resolution <px>1024Shadow map resolution per cascade

CI Pipeline Integration

Headless rendering works on machines without a display. Use it in CI to catch visual regressions:

# Example GitHub Actions step
- name: Render preview
  run: |
    cargo run --bin flint -- render levels/tavern.scene.toml \
      --output screenshots/tavern.png \
      --width 1920 --height 1080

- name: Upload screenshot
  uses: actions/upload-artifact@v4
  with:
    name: screenshots
    path: screenshots/

Visual Regression Testing

A basic visual regression workflow:

  1. Baseline — render a reference image and commit it:

    flint render levels/tavern.scene.toml --output tests/baseline/tavern.png
    
  2. Test — after changes, render again and compare:

    flint render levels/tavern.scene.toml --output tests/current/tavern.png
    # Compare with your preferred tool (ImageMagick, pixelmatch, etc.)
    
  3. Update — if the change is intentional, update the baseline:

    cp tests/current/tavern.png tests/baseline/tavern.png
    

Since Flint’s renderer is deterministic for a given scene file and camera position, identical inputs produce identical outputs.

Rendering Multiple Views

Script multiple renders for different angles:

#!/bin/bash
SCENE="levels/tavern.scene.toml"

for angle in 0 90 180 270; do
    flint render "$SCENE" \
        --output "screenshots/view_${angle}.png" \
        --yaw $angle --pitch 25 --distance 25 \
        --width 1920 --height 1080
done

Rendering Pipeline Details

Headless rendering uses the same wgpu PBR pipeline as the interactive viewer:

  • Cook-Torrance BRDF with roughness/metallic workflow
  • Cascaded shadow mapping for directional light shadows
  • glTF mesh rendering with full material support
  • Skinned mesh rendering with bone matrix upload (for skeletal meshes)

The only difference from interactive rendering is that the output goes to a texture-to-buffer copy instead of a swapchain surface.

Further Reading

AI Agent Workflow

This guide covers how AI coding agents interact with Flint to build game scenes programmatically. It describes the agent interaction loop, error handling patterns, and best practices.

The Agent Interaction Loop

An agent building a scene follows a create-validate-query-render cycle:

┌──────────────────────────────────────────────────┐
│                                                  │
│   1. Discover ──► 2. Create ──► 3. Validate ─┐   │
│        ▲                                     │   │
│        │          4. Query ◄─── 5. Fix ◄─────┘   │
│        │              │                          │
│        └──────────────┤                          │
│                       ▼                          │
│                  6. Render ──► Human Review       │
│                                                  │
└──────────────────────────────────────────────────┘

Step 1: Discover Available Schemas

Before creating entities, the agent learns what’s available:

# List available archetypes
flint schema --list-archetypes --schemas schemas

# Inspect a specific archetype
flint schema player --schemas schemas

# Inspect a component
flint schema collider --schemas schemas

This tells the agent what fields exist, their types, and their defaults.

Step 2: Create Scene and Entities

# Create the scene file
flint scene create levels/dungeon.scene.toml --name "Dungeon Level 1"

# Create entities
flint entity create --archetype room --name "entrance" \
    --scene levels/dungeon.scene.toml

flint entity create --archetype door --name "iron_gate" \
    --parent "entrance" \
    --scene levels/dungeon.scene.toml

Or the agent can write TOML directly — often faster for complex scenes:

[scene]
name = "Dungeon Level 1"

[entities.entrance]
archetype = "room"

[entities.entrance.transform]
position = [0.0, 0.0, 0.0]

[entities.entrance.bounds]
size = [10.0, 4.0, 10.0]

[entities.iron_gate]
archetype = "door"
parent = "entrance"

[entities.iron_gate.transform]
position = [0.0, 1.5, -5.0]

Step 3: Validate

Check the scene against constraints:

flint validate levels/dungeon.scene.toml --format json --schemas schemas

JSON output example:

{
  "valid": false,
  "violations": [
    {
      "constraint": "doors_have_transform",
      "entity": "iron_gate",
      "severity": "error",
      "message": "Door 'iron_gate' is missing a transform component"
    }
  ]
}

The agent parses this JSON, understands what’s wrong, and proceeds to fix it.

Step 4: Query to Verify

After fixing violations, the agent can query to confirm the scene state:

# Verify the door now has a transform
flint query "entities where archetype == 'door'" \
    --scene levels/dungeon.scene.toml --format json

# Count entities
flint query "entities" \
    --scene levels/dungeon.scene.toml --format json | jq length

Step 5: Fix and Iterate

If validation fails, the agent can either:

  • Auto-fix — let Flint handle it:

    flint validate levels/dungeon.scene.toml --fix --dry-run --format json
    flint validate levels/dungeon.scene.toml --fix
    
  • Manual fix — edit the TOML to add missing data

Step 6: Render for Review

Generate a preview image for human (or vision-model) review:

flint render levels/dungeon.scene.toml --output preview.png \
    --width 1920 --height 1080 --distance 25 --yaw 45

AI Asset Generation

Agents can generate assets alongside scene construction:

# Generate textures for the scene
flint asset generate texture \
    -d "rough dungeon stone wall, torch-lit" \
    --style medieval_tavern \
    --name dungeon_wall_texture

# Generate a 3D model
flint asset generate model \
    -d "iron-bound wooden door, medieval dungeon" \
    --provider meshy \
    --name iron_door_model

# Batch-generate all missing assets for the entire scene
flint asset resolve levels/dungeon.scene.toml \
    --strategy ai_generate \
    --style medieval_tavern

Semantic asset definitions in the scene file guide batch generation:

[entities.wall_section.asset_def]
name = "dungeon_wall_texture"
description = "Rough stone dungeon wall with moss and cracks"
type = "texture"
material_intent = "rough stone"
wear_level = 0.8

Error Handling Patterns

Exit Codes

All Flint commands use standard exit codes:

  • 0 — success
  • 1 — error (validation failure, missing file, etc.)
flint validate levels/dungeon.scene.toml --format json
if [ $? -ne 0 ]; then
    echo "Validation failed, fixing..."
    flint validate levels/dungeon.scene.toml --fix
fi

JSON Error Output

Error details are always available in JSON:

flint validate levels/dungeon.scene.toml --format json 2>/dev/null

Idempotent Operations

Most Flint operations are idempotent — running them twice produces the same result. This is important for agents that may retry failed operations.

Example: Agent Building a Complete Scene

Here’s a complete agent workflow script:

#!/bin/bash
set -e
SCENE="levels/generated.scene.toml"
SCHEMAS="schemas"

# 1. Create scene
flint scene create "$SCENE" --name "Agent-Generated Level"

# 2. Build structure
flint entity create --archetype room --name "main_room" --scene "$SCENE"
flint entity create --archetype room --name "side_room" --scene "$SCENE"
flint entity create --archetype door --name "connecting_door" \
    --parent "main_room" --scene "$SCENE"

# 3. Add player
flint entity create --archetype player --name "player" --scene "$SCENE"

# 4. Validate (will likely fail --- no transforms yet)
flint validate "$SCENE" --schemas "$SCHEMAS" --format json || true

# 5. Auto-fix what we can
flint validate "$SCENE" --schemas "$SCHEMAS" --fix

# 6. Verify
ENTITY_COUNT=$(flint query "entities" --scene "$SCENE" --format json | jq length)
echo "Scene has $ENTITY_COUNT entities"

# 7. Render preview
flint render "$SCENE" --output preview.png --schemas "$SCHEMAS" \
    --width 1920 --height 1080

echo "Scene built successfully. Preview: preview.png"

Best Practices

  • Always validate after creating entities — catch problems early
  • Use JSON output — easier to parse than text
  • Use --dry-run before --fix — preview changes before applying
  • Write TOML directly for complex scenes — faster than many CLI commands
  • Use semantic asset definitions — let batch resolution handle asset generation
  • Render previews — visual verification catches issues that validation can’t

Further Reading

Building a Game Project

This guide walks through setting up a standalone game project that uses Flint’s engine schemas while defining its own game-specific components, scripts, and assets.

Setting Up a Game Repository

Game projects live in their own git repositories with the Flint engine included as a git subtree. This gives you a single clone with everything needed to build and play, while keeping game and engine commits separate.

1. Create the repository

mkdir my_game && cd my_game
git init
mkdir schemas schemas/components schemas/archetypes scripts scenes sprites audio

2. Add the engine as a subtree

git remote add flint-engine https://github.com/chrischaps/Flint.git
git subtree add --prefix=engine flint-engine main --squash

The --squash flag collapses engine history into one commit, keeping your game history clean. Full engine history stays in the Flint repo.

3. Create convenience scripts

play.bat — launch any scene by name:

@echo off
set SCENE=%~1
if "%SCENE%"=="" set SCENE=level_1
cargo run --manifest-path engine\Cargo.toml --bin flint-player -- scenes\%SCENE%.scene.toml --schemas engine\schemas --schemas schemas %2 %3 %4

build.bat — build the engine in release mode:

@echo off
cargo build --manifest-path engine\Cargo.toml --release

Directory Structure

my_game/                         (your git repo)
├── engine/                      (git subtree ← Flint repo)
│   ├── crates/
│   ├── schemas/                 (engine schemas: transform, material, etc.)
│   └── Cargo.toml
├── schemas/
│   ├── components/              # Game-specific component definitions
│   │   ├── health.toml
│   │   ├── weapon.toml
│   │   └── enemy_ai.toml
│   └── archetypes/              # Game-specific archetype bundles
│       ├── enemy.toml
│       └── pickup.toml
├── scripts/                     # Rhai game logic scripts
│   ├── player_weapon.rhai
│   ├── enemy_ai.rhai
│   └── hud.rhai
├── scenes/                      # Scene files
│   └── level_1.scene.toml
├── sprites/                     # Billboard sprite textures
├── audio/                       # Sound effects and music
├── play.bat                     # Convenience launcher
└── build.bat                    # Engine build script

Multi-Schema Layering

The key to the game project pattern is the --schemas flag, which accepts multiple paths. Schemas load in order, with later paths overriding earlier ones:

cargo run --manifest-path engine\Cargo.toml --bin flint-player -- ^
  scenes\level_1.scene.toml ^
  --schemas engine\schemas ^
  --schemas schemas

This loads:

  1. Engine schemas from engine/schemas/ — built-in components like transform, material, rigidbody, collider, character_controller, sprite, etc.
  2. Game schemas from schemas/ — game-specific components like health, weapon, enemy_ai

If both directories define a component with the same name, the game’s version takes priority.

Defining Game Components

Create component schemas in schemas/components/:

# schemas/components/health.toml
[component.health]
description = "Hit points for damageable entities"

[component.health.fields]
max_hp = { type = "i32", default = 100, min = 1 }
current_hp = { type = "i32", default = 100, min = 0 }
# schemas/components/weapon.toml
[component.weapon]
description = "Weapon carried by the player"

[component.weapon.fields]
name = { type = "string", default = "Pistol" }
damage = { type = "i32", default = 10 }
fire_rate = { type = "f32", default = 0.5 }
ammo = { type = "i32", default = 50 }
max_ammo = { type = "i32", default = 100 }

Defining Game Archetypes

Bundle game components with engine components:

# schemas/archetypes/enemy.toml
[archetype.enemy]
description = "A hostile NPC with health and a sprite"
components = ["transform", "health", "sprite", "collider", "rigidbody", "script"]

[archetype.enemy.defaults.health]
max_hp = 50
current_hp = 50

[archetype.enemy.defaults.sprite]
fullbright = true

[archetype.enemy.defaults.rigidbody]
body_type = "kinematic"

[archetype.enemy.defaults.collider]
shape = "box"
size = [1.0, 2.0, 1.0]

Writing the Scene

Reference game archetypes in your scene file just like engine archetypes:

[scene]
name = "Level 1"

[entities.player]
archetype = "player"

[entities.player.transform]
position = [0, 1, 0]

[entities.player.character_controller]
move_speed = 8.0

[entities.player.health]
max_hp = 100
current_hp = 100

[entities.enemy_1]
archetype = "enemy"

[entities.enemy_1.transform]
position = [10, 0, 5]

[entities.enemy_1.sprite]
texture = "enemy"
width = 1.5
height = 2.0

[entities.enemy_1.script]
source = "enemy_ai.rhai"

[entities.hud_controller]

[entities.hud_controller.script]
source = "hud.rhai"

Script-Driven Game Logic

All game-specific behavior lives in Rhai scripts. The engine provides generic APIs (entity, input, audio, physics, draw) and your scripts implement game rules:

#![allow(unused)]
fn main() {
// scripts/hud.rhai

fn on_draw_ui() {
    let sw = screen_width();
    let sh = screen_height();

    // Crosshair
    let cx = sw / 2.0;
    let cy = sh / 2.0;
    draw_line(cx - 8.0, cy, cx + 8.0, cy, 0.0, 1.0, 0.0, 0.8, 2.0);
    draw_line(cx, cy - 8.0, cx, cy + 8.0, 0.0, 1.0, 0.0, 0.8, 2.0);

    // Health display
    let player = get_entity("player");
    if player != -1 && has_component(player, "health") {
        let hp = get_field(player, "health", "current_hp");
        let max_hp = get_field(player, "health", "max_hp");
        draw_text(20.0, sh - 30.0, `HP: ${hp}/${max_hp}`, 16.0, 1.0, 1.0, 1.0, 1.0);
    }
}
}

Running the Game

# Via convenience script
.\play.bat level_1

# Via the standalone player directly
cargo run --manifest-path engine\Cargo.toml --bin flint-player -- ^
  scenes\level_1.scene.toml --schemas engine\schemas --schemas schemas

Asset Resolution

Scripts, audio, and sprite paths are resolved relative to the game project root. When a scene lives in scenes/, the engine looks for:

  • Scripts in scripts/
  • Audio in audio/
  • Sprites in sprites/

Engine Subtree Workflow

The engine at engine/ is a full copy of the Flint repo. You can edit engine code directly, and manage updates with standard git subtree commands:

# Pull latest engine changes
git subtree pull --prefix=engine flint-engine main --squash

# Push engine edits back to the Flint repo
git subtree push --prefix=engine flint-engine main

Engine edits are normal commits in your game repo. The subtree commands handle splitting and merging the engine/ prefix.

Further Reading

  • Schemas — component and archetype schema system
  • Scripting — full Rhai scripting API
  • Rendering — billboard sprites and PBR pipeline
  • CLI Reference — the play command and --schemas flag

Deploying to Android

Flint games can be packaged as Android APKs and run on physical devices. The engine uses the same wgpu/Vulkan rendering, Rhai scripting, and TOML scene format on mobile — no code changes needed. Touch input, orthographic cameras, and 2D sprites work identically across desktop and Android.

Prerequisites

1. Android SDK

Install via Android Studio or the standalone sdkmanager:

  • compileSdk: API level 34
  • Build Tools: 34.x
  • NDK: latest version

2. Rust Android Target

rustup target add aarch64-linux-android

3. cargo-ndk

cargo install cargo-ndk

4. Environment Variables

export ANDROID_HOME=/path/to/android/sdk
export ANDROID_NDK_HOME=$ANDROID_HOME/ndk/<version>

Building an APK

From the Flint engine root:

# Quick build + install to a connected device
./scripts/android-build.sh /path/to/your/game

# Or manual Gradle build
cd android
./gradlew assembleDebug -PgameDir=/path/to/your/game

The -PgameDir parameter points to your game project directory (the one containing scene files, schemas, scripts, etc.).

What the Build Does

The Gradle build runs two tasks:

  1. cargoNdkBuild — Cross-compiles the flint-android crate as a native shared library (libflint_android.so) for ARM64 (arm64-v8a) using cargo ndk. The library is placed in app/src/main/jniLibs/.

  2. copyGameAssets — Copies your game’s assets into the APK:

    • Scene files (*.scene.toml, *.sprite.toml, *.anim.toml)
    • Directories: schemas/, scripts/, sprites/, textures/, models/, audio/, animations/
    • Engine schemas are copied separately into engine/schemas/
    • Generates asset_manifest.txt listing every bundled file path

Asset Manifest

Android’s NDK AAssetDir_getNextFileName() only enumerates files within a directory — it does not list subdirectories. To work around this, the Gradle build generates an asset_manifest.txt listing every relative file path. At runtime, the extractor reads this manifest and copies each file individually.

How It Runs

When the APK launches:

  1. android_main() initializes Android logging (visible in logcat)
  2. Asset extraction copies all bundled files from the APK to internal storage so that std::fs code works unchanged — no virtual filesystem needed
  3. Version marker (.asset_version) prevents redundant extraction on subsequent launches
  4. Schema loading loads engine schemas then game schemas (same merge order as desktop)
  5. Scene discovery finds the first *.scene.toml file in the extracted assets
  6. Player event loop starts the game using the same PlayerApp as desktop, with Android surface integration

Architecture Decisions

Extract, Don’t Abstract

Rather than building a virtual filesystem that intercepts all file reads, Flint extracts APK assets to internal storage at startup. This means every std::fs::read_to_string(), image::open(), gltf::import(), and Rhai script load works exactly as on desktop. The extraction happens once, takes a fraction of a second for typical game assets.

NativeActivity over GameActivity

Flint uses Android’s built-in NativeActivity (hasCode = "false" in the manifest) rather than Google’s Jetpack GameActivity. NativeActivity has zero Java dependencies — the entire app is Rust.

GPU Limits

Mobile GPUs may not support desktop-default wgpu limits. The Android entry point uses wgpu::Limits::downlevel_defaults() as a base, then overrides max_texture_dimension_2d with the adapter’s actual reported capability.

Platform API Level

The minimum API level is 26 (Android 8.0), required for:

  • AAudio — the audio backend used by Kira
  • Vulkan — the graphics backend used by wgpu

Surface Lifecycle

Android can pause and resume apps at any time. Flint handles this gracefully:

  • suspended() — drops the window and surface, preserves GPU device and all game state
  • resumed() — recreates the window and surface, continues rendering

The first resumed() call performs full initialization (GPU device, pipelines, scene loading). Subsequent calls only recreate the surface.

Game Project Structure

A typical game project targeting Android:

my_game/
  scenes/
    main.scene.toml       # Entry scene (first .scene.toml found)
  schemas/
    components/           # Game-specific components
    archetypes/           # Game-specific archetypes
  scripts/
    player.rhai
    hud.rhai
  sprites/
    player_sheet.png
  animations/
    character.sprite.toml
  audio/
    music.ogg
  input.toml              # Touch + keyboard bindings

The build copies this entire structure into the APK. Engine schemas are bundled separately, and the schema merge order (engine then game) is preserved.

Testing on Desktop

Touch-zone bindings work on desktop via mouse emulation (enabled by default). Left-click-and-drag emulates a single finger touch, so you can test the full touch interaction model without a device.

# Test your touch-enabled game on desktop
flint play scenes/main.scene.toml --schemas schemas

Debugging

Logcat

All log::info!, log::warn!, and log::error! calls from Rust appear in Android logcat with the tag flint:

adb logcat -s flint:*

Common Issues

IssueCauseFix
Black screen on launchScene file not found in extracted assetsCheck asset_manifest.txt includes your scene
Crash on surface creationGPU doesn’t support required featuresCheck adb logcat for wgpu errors; ensure Vulkan device
No audioAPI level < 26AAudio requires Android 8.0+
Touch not respondingWindowEvent::Touch not reaching input stateVerify process_touch_* calls in PlayerApp

Further Reading

CLI Reference

Flint’s CLI is the primary interface for all engine operations. Below is a reference of available commands.

Commands

CommandDescription
flint init <name>Initialize a new project
flint entity createCreate an entity in a scene
flint entity deleteDelete an entity from a scene
flint scene createCreate a new scene file
flint scene listList scene files
flint scene infoShow scene metadata and entity count
flint query "<query>"Query entities with the Flint query language
flint schema <name>Inspect a component or archetype schema
flint validate <scene>Validate a scene against constraints
flint asset importImport a file into the asset store
flint asset listList assets in the catalog
flint asset infoShow details for a specific asset
flint asset resolveCheck asset references in a scene
flint asset generateGenerate an asset using AI providers
flint asset validateValidate a generated model against style constraints
flint asset manifestGenerate a build manifest of all generated assets
flint asset regenerateRegenerate an existing asset with new parameters
flint asset job statusCheck status of an async generation job
flint asset job listList all generation jobs
flint edit <file>Unified interactive editor (auto-detects file type)
flint play <scene>Play a scene with first-person controls and physics
flint render <scene>Render a scene to PNG (headless)
flint gen <spec>Run a procedural generation spec to produce meshes or textures
flint prefab view <template>Preview a prefab template in the viewer

The play Command

Launch a scene as an interactive first-person experience with physics:

flint play demo/phase4_runtime.scene.toml
flint play levels/tavern.scene.toml --schemas schemas --fullscreen
FlagDescription
--schemas <path>Path to schemas directory (repeatable; later paths override earlier). Default: schemas
--fullscreenLaunch in fullscreen mode
--input-config <path>Input config overlay path (highest priority, overrides all other layers)

Player Controls (Defaults)

These are the built-in defaults. Games can override any binding via input config files (see Physics and Runtime: Input System).

InputAction
WASDMove
MouseLook around
Left ClickFire (weapon)
SpaceJump
ShiftSprint
EInteract with nearby object
RReload
1 / 2Select weapon slot
EscapeRelease cursor / Exit
F1Cycle debug rendering mode (PBR → Wireframe → Normals → Depth → UV → Unlit → Metal/Rough)
F4Toggle shadows
F5Toggle bloom
F6Toggle post-processing pipeline
F11Toggle fullscreen

Gamepad controllers are also supported when connected. Bindings for gamepad buttons and axes can be configured in input config TOML files.

The play command requires the scene to have a player archetype entity with a character_controller component. Physics colliders on other entities define the walkable geometry.

Game Project Pattern

Games that define their own schemas, scripts, and assets use multiple --schemas paths. Game projects typically live in their own repositories with the engine included as a git subtree at engine/. The engine schemas come first, then the game-specific schemas overlay on top:

# From a game project root (engine at engine/)
cargo run --manifest-path engine/Cargo.toml --bin flint-player -- \
  scenes/level_1.scene.toml \
  --schemas engine/schemas \
  --schemas schemas

This loads the engine’s built-in components (transform, material, rigidbody, etc.) from engine/schemas/, then adds game-specific components (health, weapon, enemy AI) from the game’s own schemas/. See Schemas: Game Project Schemas for directory structure details and Building a Game Project for the full workflow.

Standalone Player Binary

The player is also available as a standalone binary for distribution:

cargo run --bin flint-player -- demo/phase4_runtime.scene.toml --schemas schemas

# With game project schemas (from a game repo with engine subtree)
cargo run --manifest-path engine/Cargo.toml --bin flint-player -- \
  scenes/level_1.scene.toml --schemas engine/schemas --schemas schemas

The render Command

Render a scene to a PNG image without opening a window:

flint render demo/phase3_showcase.scene.toml --output hero.png --schemas schemas
flint render scene.toml -o shot.png --distance 20 --pitch 30 --yaw 45 --target 0,1,0 --no-grid
FlagDefaultDescription
--output <path> / -orender.pngOutput file path
--width <px>1920Image width
--height <px>1080Image height
--distance <f32>(auto)Camera distance from target
--yaw <deg>(auto)Horizontal camera angle
--pitch <deg>(auto)Vertical camera angle
--target <x,y,z>(auto)Camera look-at point
--fov <deg>(auto)Field of view
--no-gridfalseDisable ground grid
--debug-mode <mode>(none)wireframe, normals, depth, uv, unlit, metalrough
--wireframe-overlayfalseWireframe edges on solid geometry
--show-normalsfalseNormal direction arrows
--no-tonemappingfalseRaw linear output
--no-shadowsfalseDisable shadow mapping
--shadow-resolution <px>1024Shadow map resolution per cascade
--no-postprocessfalseDisable post-processing
--bloom-intensity <f32>0.04Bloom strength
--bloom-threshold <f32>1.0Bloom brightness threshold
--exposure <f32>1.0Exposure multiplier
--ssao-radius <f32>0.5SSAO sample radius
--ssao-intensity <f32>1.0SSAO intensity (0 = disabled)
--fog-density <f32>0.02Fog density (0 = disabled)
--fog-color <r,g,b>0.7,0.75,0.82Fog color
--fog-height-falloff <f32>0.1Fog height falloff
--schemas <path>schemasSchemas directory (repeatable)

The edit Command

Unified interactive editor that auto-detects file type and opens the appropriate tool:

flint edit levels/demo.scene.toml              # Scene viewer (hot-reload)
flint edit levels/demo.scene.toml --spline     # Spline/track editor
flint edit models/character.glb                # Model previewer (orbit camera)
flint edit models/character.glb --watch        # Model previewer with file watching
flint edit specs/oak_tree.procgen.toml         # Procgen previewer (mesh/texture)
flint edit specs/stone_wall.procgen.toml       # Texture pipeline editor (if pipeline pattern)
flint edit terrain.terrain.toml                # Terrain editor

File Type Detection

ExtensionToolDescription
.scene.toml, .chunk.tomlScene viewerHot-reload, egui inspector, gizmos
.procgen.toml (pipeline pattern)Texture pipeline editorNode graph for texture specs
.procgen.toml (other)Procgen previewerLive preview of generated mesh/texture
.terrain.tomlTerrain editorHeightmap terrain editing
.glb, .gltfModel previewerOrbit camera, animation playback

Common Flags

FlagDefaultDescription
--schemas <path>schemasSchemas directory (repeatable)
--width <px>(auto)Window width
--height <px>(auto)Window height
--no-gridfalseDisable ground grid
--watchfalseWatch for file changes
--seed <u64>(auto)Override seed (procgen)
--no-inspectorfalseHide egui inspector (scene)
--auto-orbitfalseAuto-orbit camera (model/procgen)

Model Previewer Flags

FlagDefaultDescription
--distance <f32>(auto)Camera orbit distance
--yaw <deg>(auto)Horizontal camera angle
--pitch <deg>(auto)Vertical camera angle
--target <x,y,z>(auto)Camera look-at point
--fov <deg>(auto)Field of view
--no-animatefalseDisable animation playback
--clip <name>(none)Start with a specific animation clip
--anim-speed <f32>1.0Animation playback speed multiplier
--render <path>(none)Render to PNG instead of opening a window

Scene Viewer Controls

InputAction
Left-clickSelect entity / pick gizmo axis
Left-dragOrbit camera (or drag gizmo if axis selected)
Right-dragPan camera
ScrollZoom
Ctrl+SSave scene to disk
Ctrl+ZUndo position change
Ctrl+Shift+ZRedo position change
EscapeCancel gizmo drag
F1Cycle debug mode
F2Toggle wireframe overlay
F3Toggle normal arrows
F4Toggle shadows

When an entity is selected, a translate gizmo appears with colored axis arrows (red = X, green = Y, blue = Z) and plane handles. Click and drag an axis or plane to move the entity. Position changes can be undone/redone and saved back to the scene file.

Spline Editor Controls (--spline)

InputAction
Left-clickSelect control point
Left-dragMove control point on constraint plane
Alt + dragMove control point vertically (Y axis)
Middle-dragOrbit camera
Right-dragPan camera
ScrollZoom
Tab / Shift+TabCycle through control points
IInsert a new control point after selected
DeleteRemove selected control point
Ctrl+SSave spline to disk
Ctrl+ZUndo

Legacy aliases: flint serve, flint preview, flint gen-preview, flint tex-edit, flint terrain-edit, and flint spline-edit still work but route through flint edit.

The asset generate Command

Generate assets using AI providers:

flint asset generate texture -d "rough stone wall" --style medieval_tavern
flint asset generate model -d "wooden chair" --provider meshy --seed 42
flint asset generate audio -d "tavern ambient noise" --duration 10.0
FlagDescription
-d, --descriptionGeneration prompt (required)
--nameAsset name (derived from description if omitted)
--providerProvider to use: flux, meshy, elevenlabs, mock
--styleStyle guide name (e.g., medieval_tavern)
--width, --heightImage dimensions for textures (default: 1024x1024)
--seedRandom seed for reproducibility
--tagsComma-separated tags
--outputOutput directory (default: .flint/generated)
--durationAudio duration in seconds (default: 3.0)

Generated assets are automatically stored in content-addressed storage and registered in the asset catalog with a .asset.toml sidecar. Models are validated against style constraints after generation.

The gen Command

Run a procedural generation spec to produce meshes (GLB) or textures (PNG):

flint gen specs/oak_tree.procgen.toml -o tree.glb
flint gen specs/stone_wall.procgen.toml -o wall.png
flint gen specs/oak_tree.procgen.toml --dry-run
flint gen specs/oak_tree.procgen.toml --seed 42 -o tree.glb
flint gen specs/oak_tree.procgen.toml --batch 10 --seed-start 0
FlagDefaultDescription
-o, --output <path>(derived from spec)Output file or directory
--seed <u64>(from spec)Override the spec’s seed
--dry-runfalsePrint estimated cost without generating
--format <fmt>(auto)Force output format: glb or png
--batch <N>(none)Generate N variants with sequential seeds
--seed-start <u64>0Starting seed for batch generation
--registerfalseStore output in content store with provenance
--forcefalseRegenerate even if cached
--validatefalseValidate output after generation
--strictfalseTreat warnings as failures
--style-guide <path>(none)Style guide TOML for validation constraints

The prefab view Command

Preview a prefab template in the interactive viewer:

flint prefab view prefabs/kart.prefab.toml --schemas engine/schemas --schemas schemas
FlagDefaultDescription
--prefix <string>"preview"Prefix for ${PREFIX} substitution
--schemas <path>schemasSchemas directory (repeatable)

This command loads the .prefab.toml template, performs variable substitution, builds a synthetic scene from the expanded entities, and launches the viewer for visual inspection. Useful for verifying prefab structure and appearance without creating a full scene.

Common Flags

FlagDescription
--scene <path>Path to scene file
--schemas <path>Path to schemas directory (repeatable for multi-schema layering; default: schemas)
--format <fmt>Output format: json, toml, or text
--fixApply auto-fixes (with validate)
--dry-runPreview changes without applying

Usage

# Get help
flint --help
flint <command> --help

# Examples
flint init my-game
flint edit levels/tavern.scene.toml              # Interactive scene viewer
flint edit models/character.glb --watch           # Model previewer
flint play levels/tavern.scene.toml
flint render levels/tavern.scene.toml -o shot.png
flint gen specs/oak_tree.procgen.toml -o tree.glb
flint query "entities where archetype == 'door'" --scene levels/tavern.scene.toml

File Formats

All Flint data formats use TOML. This page provides a complete reference for every file type.

Scene Files (.scene.toml)

The primary data format. Each scene file contains metadata and a collection of named entities with their component data.

[scene]
name = "Scene Name"
version = "1.0"
input_config = "custom_input.toml"  # Optional input binding config

[entities.<name>]
archetype = "<archetype>"
parent = "<parent_name>"          # Optional parent entity

[entities.<name>.<component>]
field = value

Scenes may also include optional top-level blocks for post-processing and environment settings:

[post_process]
bloom_enabled = true
bloom_intensity = 0.04
bloom_threshold = 1.0
vignette_enabled = true
vignette_intensity = 0.3
exposure = 1.0

[environment]
ambient_color = [0.1, 0.1, 0.15]
ambient_intensity = 0.3
fog_enabled = false
fog_color = [0.5, 0.5, 0.6]
fog_density = 0.02

The [post_process] block configures the HDR post-processing pipeline (see Post-Processing). The [environment] block sets ambient lighting and fog parameters.

Scenes are loaded by flint-scene and can be edited with flint entity create, flint entity delete, or by hand. The serve --watch viewer reloads automatically when the file changes.

Component Schemas (schemas/components/*.toml)

Define the fields, types, and defaults for each component kind. Components are dynamic — they exist as schema TOML, not compiled Rust types.

[component.<name>]
description = "Human-readable description"

[component.<name>.fields]
field_name = { type = "<type>", default = <value>, description = "..." }

Supported field types: bool, i32, f32, string, vec3, enum, entity_ref, array.

Key component schemas: transform, material, door, bounds, rigidbody, collider, character_controller, audio_source, audio_listener, audio_trigger, animator, skeleton, script, interactable, sprite, asset_def.

Archetype Schemas (schemas/archetypes/*.toml)

Bundle components together with sensible defaults for common entity types.

[archetype.<name>]
description = "..."
components = ["comp1", "comp2"]

[archetype.<name>.defaults.<component>]
field = value

Constraint Files (schemas/constraints/*.toml)

Declarative validation rules checked by flint validate. Each file can contain multiple [[constraint]] entries.

[[constraint]]
name = "rule_name"
description = "What this constraint checks"
query = "entities where archetype == 'door'"
severity = "error"                 # "error" or "warning"
message = "Door '{name}' is missing a transform component"

[constraint.kind]
type = "required_component"        # Constraint type
archetype = "door"
component = "transform"

Constraint kinds: required_component, required_child, value_range, reference_valid, query_rule.

Animation Clips (animations/*.anim.toml)

TOML-defined keyframe animation clips for property tweens. Loaded by scanning the animations directory at startup.

name = "clip_name"
duration = 0.8

[[tracks]]
interpolation = "Linear"           # "Step", "Linear", or "CubicSpline"

[tracks.target]
type = "Rotation"                  # "Position", "Rotation", "Scale", or "CustomFloat"
# component = "material"           # Required for CustomFloat
# field = "emissive_strength"      # Required for CustomFloat

[[tracks.keyframes]]
time = 0.0
value = [0.0, 0.0, 0.0]           # [x, y, z] (euler degrees for rotation)

[[tracks.keyframes]]
time = 0.8
value = [0.0, 90.0, 0.0]
# in_tangent = [...]               # Optional, for CubicSpline
# out_tangent = [...]

[[events]]                         # Optional timed events
time = 0.0
event_name = "door_start"

Asset Sidecars (assets/**/*.asset.toml)

Metadata files stored alongside imported assets in the catalog.

[asset]
name = "asset_name"
type = "mesh"                      # mesh, texture, material, audio, script
hash = "sha256:a1b2c3..."
source_path = "models/chair.glb"
format = "glb"
tags = ["furniture", "medieval"]

[asset.properties]                 # Optional provider-specific metadata
prompt = "wooden tavern chair"
provider = "meshy"

Style Guides (styles/*.style.toml)

Define visual vocabulary for consistent AI asset generation. Searched in styles/ then .flint/styles/.

[style]
name = "medieval_tavern"
description = "Weathered medieval fantasy tavern"
prompt_prefix = "Medieval fantasy tavern style, low-fantasy realism"
prompt_suffix = "Photorealistic textures, warm candlelight tones"
negative_prompt = "modern, sci-fi, neon, plastic"
palette = ["#8B4513", "#A0522D", "#D4A574", "#4A4A4A"]

[style.materials]
roughness_range = [0.6, 0.95]
metallic_range = [0.0, 0.15]
preferred_materials = ["aged oak wood", "rough-hewn stone", "hammered wrought iron"]

[style.geometry]
max_triangles = 5000
require_uvs = true
require_normals = true

Semantic Asset Definitions (schemas/components/asset_def.toml)

The asset_def component schema describes what an entity needs in terms of assets, expressed as intent. Used by the batch resolver to auto-generate missing assets.

[entities.tavern_wall.asset_def]
name = "tavern_wall_texture"
description = "Rough stone wall with mortar lines"
type = "texture"
material_intent = "rough stone"
wear_level = 0.7
size_class = "large"
tags = ["wall", "interior"]

Prefab Templates (prefabs/*.prefab.toml)

Reusable entity group templates with variable substitution. Prefabs define a set of entities that can be instantiated multiple times in a scene with different prefixes and per-instance overrides.

[prefab]
name = "template_name"
description = "Optional description"

[entities.body]

[entities.body.transform]
position = [0, 0, 0]

[entities.body.model]
asset = "model_name"

[entities.child]
parent = "${PREFIX}_body"

[entities.child.transform]
position = [0.5, 0, 0]

All string values containing ${PREFIX} are replaced with the instance prefix. Entity names are prepended with the prefix (e.g., body becomes player_body with prefix "player").

Scenes instantiate prefabs in a [prefabs] section:

[prefabs.player]
template = "template_name"
prefix = "player"

[prefabs.player.overrides.body.transform]
position = [0, 0, 0]

[prefabs.ai1]
template = "template_name"
prefix = "ai1"

[prefabs.ai1.overrides.body.transform]
position = [5, 0, -3]

Overrides are deep-merged at the field level — specifying one field in a component preserves all other fields from the template.

See Scenes: Prefabs for usage details.

Spline Files (splines/*.spline.toml)

Define smooth 3D paths using Catmull-Rom control points. Used for track layouts, camera paths, and procedural geometry generation.

[spline]
name = "Track Name"
closed = true             # true for closed loops, false for open paths

[sampling]
spacing = 2.0             # Distance between sampled points (meters)

[[control_points]]
position = [0, 0, 0]
twist = 0.0               # Banking angle in degrees

[[control_points]]
position = [0, 0, -50]
twist = 0.0

[[control_points]]
position = [50, 0, -100]
twist = 5.0               # Banked turn
FieldTypeDescription
spline.namestringHuman-readable name
spline.closedboolWhether the spline forms a closed loop
sampling.spacingf32Distance between sampled points along the curve
control_points[].position[f32; 3]3D position [x, y, z]
control_points[].twistf32Banking angle in degrees (interpolated with C1 continuity via Catmull-Rom)

The engine samples the control points into a dense array using Catmull-Rom interpolation, stored as the spline_data ECS component. Scripts can query this data via spline_closest_point() and spline_sample_at().

UI Layouts (ui/*.ui.toml)

Data-driven UI element trees. Loaded by scripts via load_ui(). Each layout file references a companion style file.

[ui]
name = "Race HUD"
style = "ui/race_hud.style.toml"

[elements.<id>]
type = "panel"                     # panel, text, rect, circle, image
anchor = "bottom-center"           # Screen anchor (root elements only)
class = "hud-panel"                # Style class from .style.toml
parent = "parent_id"               # Optional parent element
text = "Default text"              # For text elements
src = "logo.png"                   # For image elements
visible = true

Element types: panel (container with background), text (styled text), rect (filled or outlined rectangle), circle (filled circle), image (sprite).

Anchor points: top-left, top-center, top-right, center-left, center, center-right, bottom-left, bottom-center, bottom-right.

See Scripting: Data-Driven UI for the full layout/style/API reference.

UI Styles (ui/*.style.toml)

Named style classes for UI elements. Referenced by .ui.toml layout files.

[styles.<class-name>]
width = 200
height = 60
color = [1.0, 1.0, 1.0, 1.0]     # Primary color (RGBA)
bg_color = [0.0, 0.0, 0.0, 0.6]  # Background color
font_size = 24
text_align = "center"              # left, center, right
rounding = 8
opacity = 1.0
padding = [12, 8, 12, 8]          # [left, top, right, bottom]
layout = "stack"                   # stack (vertical) or horizontal
layer = 0                          # Render depth ordering
width_pct = 100                    # Percentage of parent width
margin_bottom = 4                  # Spacing in flow layout

Style properties support float, color ([r,g,b,a]), string, and boolean values. See Scripting: Style Properties for the complete property table.

Rhai Scripts (scripts/*.rhai)

Game logic scripts written in Rhai. Attached to entities via the script component. See Scripting for the full API reference.

#![allow(unused)]
fn main() {
fn on_init() {
    log("Entity initialized");
}

fn on_update() {
    let dt = delta_time();
    // Called every frame — use delta_time() for frame delta
}

fn on_interact() {
    // Called when the player interacts with this entity
    play_sound("door_open");
}
}

Input Configuration (config/input.toml, ~/.flint/input_{game_id}.toml)

Define action-to-binding mappings for keyboard, mouse, and gamepad input. Loaded with layered precedence: engine defaults → game config → user overrides → CLI override.

version = 1
game_id = "doom_fps"

[actions.move_forward]
kind = "button"
[[actions.move_forward.bindings]]
type = "key"
code = "KeyW"
[[actions.move_forward.bindings]]
type = "gamepad_axis"
axis = "LeftStickY"
direction = "negative"
threshold = 0.35
gamepad = "any"

[actions.fire]
kind = "button"
[[actions.fire.bindings]]
type = "mouse_button"
button = "Left"
[[actions.fire.bindings]]
type = "gamepad_button"
button = "RightTrigger"
gamepad = "any"

[actions.look_x]
kind = "axis1d"
[[actions.look_x.bindings]]
type = "mouse_delta"
axis = "x"
scale = 2.0
[[actions.look_x.bindings]]
type = "gamepad_axis"
axis = "RightStickX"
deadzone = 0.15
scale = 1.0
invert = false
gamepad = "any"

Binding types: key, mouse_button, mouse_delta, mouse_wheel, gamepad_button, gamepad_axis. Action kinds: button (discrete), axis1d (analog). Gamepad selector: "any" or a numeric index. User overrides are written automatically when bindings are remapped at runtime.

Configuration (~/.flint/config.toml, .flint/config.toml)

Layered configuration for API keys and generation settings. Global config is merged with project-level config; environment variables override both.

[providers.flux]
api_key = "your-api-key"
enabled = true

[providers.meshy]
api_key = "your-api-key"
enabled = true

[providers.elevenlabs]
api_key = "your-api-key"
enabled = true

[generation]
default_style = "medieval_tavern"

Environment variable overrides: FLINT_FLUX_API_KEY, FLINT_MESHY_API_KEY, FLINT_ELEVENLABS_API_KEY.

Architecture Overview

Flint is structured as a twenty-three-crate Cargo workspace with clear dependency layering. Each crate has a focused responsibility, and dependencies flow in one direction — from the binaries down to core types.

Workspace Structure

flint/
├── crates/
│   ├── flint-cli/          # CLI binary (clap). Entry point for all commands.
│   ├── flint-asset-gen/    # AI asset generation: providers, style guides, batch resolution
│   ├── flint-procgen/      # Procedural generation: Generator trait, registry, tree/texture/creature generators
│   ├── flint-procgen-ai/   # AI-assisted procgen: ProcGenAgent trait, spec creation/refinement
│   ├── flint-player/       # Standalone player binary with game loop, physics, audio, animation, scripting
│   ├── flint-android/      # Android entry point (NativeActivity, APK asset extraction)
│   ├── flint-script/       # Rhai scripting: ScriptEngine, ScriptSync, hot-reload
│   ├── flint-viewer/       # egui-based GUI inspector with hot-reload
│   ├── flint-particles/    # GPU-instanced particle system with pooling and emission shapes
│   ├── flint-animation/    # Two-tier animation: property tweens + skeletal/glTF
│   ├── flint-audio/        # Kira spatial audio: 3D sounds, ambient loops, triggers
│   ├── flint-terrain/      # Heightmap terrain with splat-map blending
│   ├── flint-runtime/      # Game loop infrastructure (GameClock, InputState, EventBus, GameStateMachine)
│   ├── flint-physics/      # Rapier 3D integration (PhysicsWorld, CharacterController)
│   ├── flint-render/       # wgpu PBR renderer with Cook-Torrance shading + skinned mesh pipeline
│   ├── flint-import/       # File importers (glTF/GLB with skeleton/skin extraction)
│   ├── flint-asset/        # Content-addressed asset storage and catalog
│   ├── flint-constraint/   # Constraint definitions and validation engine
│   ├── flint-query/        # PEG query language (pest parser)
│   ├── flint-scene/        # TOML scene serialization/deserialization
│   ├── flint-ecs/          # hecs wrapper with stable IDs, names, hierarchy
│   ├── flint-schema/       # Component/archetype schema loading and validation
│   └── flint-core/         # Fundamental types: EntityId, Transform, Vec3, etc.
├── schemas/                # Default component, archetype, and constraint definitions
├── demo/                   # Showcase scenes and build scripts
└── docs/                   # This documentation (mdBook)

Design Decisions

Dynamic Components

The most significant architectural choice: components are stored as toml::Value rather than Rust types. This means:

  • Archetypes are runtime data, not compiled types
  • New components can be defined in TOML without recompiling
  • The schema system validates component data against definitions
  • Trade-off: less compile-time safety, more flexibility

Stable Entity IDs

Entity IDs are monotonically increasing 64-bit integers that never recycle. A BiMap maintains the mapping between EntityId and hecs Entity handles. On scene load, the ID counter adjusts to be above the maximum existing ID.

Scene as Source of Truth

The TOML file on disk is canonical. In-memory state is derived from it. The serve --watch viewer re-parses the entire file on change rather than attempting incremental updates. This is simpler and avoids synchronization bugs.

Fixed-Timestep Physics

The game loop uses a fixed-timestep accumulator pattern (1/60s default). Physics simulation steps at a constant rate regardless of frame rate, ensuring deterministic behavior. Rendering interpolates between physics states for smooth visuals.

Error Handling

All crates use thiserror for error types. Each crate defines its own error enum and a Result<T> type alias. Errors propagate upward through the crate hierarchy.

Technology Choices

ComponentTechnologyRationale
LanguageRustPerformance, safety, game ecosystem
ECShecsLightweight, standalone, well-tested
Renderingwgpu 23Cross-platform, modern GPU API
Windowingwinit 0.30ApplicationHandler trait pattern
PhysicsRapier 3D 0.22Mature Rust physics, character controller
AudioKira 0.11Rust-native, game-focused, spatial audio
GUIegui 0.30Immediate-mode, easy integration with wgpu
Scene formatTOMLHuman-readable, diffable, good Rust support
Query parserpestPEG grammar, good error messages
ScriptingRhai 1.24Sandboxed, embeddable, Rust-native
AI generationureqLightweight HTTP client for provider APIs
CLI frameworkclap (derive)Ergonomic, well-documented
Error handlingthiserror + anyhowTyped errors in libraries, flexible in binary

Data Flow

Flint has two entry points: the CLI for scene authoring and validation, and the player for interactive gameplay. Both flow through the same crate hierarchy:

User / AI Agent
      │
      ├──────────────────────────────────┐
      ▼                                  ▼
  flint-cli                        flint-player
  (scene authoring)                (interactive gameplay)
      │                                  │
      ├──► flint-viewer    (GUI)         ├──► flint-runtime   (game loop, input)
      ├──► flint-query     (queries)     ├──► flint-physics   (Rapier 3D)
      ├──► flint-scene     (load/save)   ├──► flint-audio     (Kira spatial audio)
      ├──► flint-render    (renderer)    ├──► flint-animation (tweens + skeletal)
      ├──► flint-constraint(validation)  ├──► flint-particles (GPU particles)
      ├──► flint-asset     (catalog)     ├──► flint-script    (Rhai scripting)
      ├──► flint-asset-gen (AI gen)      ├──► flint-terrain   (heightmap terrain)
      ├──► flint-procgen   (proc gen)    └──► flint-render    (PBR + skinned mesh)
      ├──► flint-procgen-ai(AI procgen)          │
      └──► flint-import    (glTF import)         │
              │                                  ▼
              ▼                              flint-import  (glTF meshes + skins)
          flint-ecs                              │
          flint-schema                           ▼
          flint-core                         flint-ecs
                                             flint-schema
                                             flint-core

Crate Details

flint-core

Fundamental types shared by all crates. Minimal external dependencies (thiserror, serde, sha2).

  • EntityId — stable 64-bit entity identifier
  • ContentHash — SHA-256 based content addressing
  • Transform, Vec3, Color — geometric primitives
  • FlintError — base error type

flint-schema

Loads component and archetype definitions from TOML files. Provides a registry for introspection. Supports field types (bool, i32, f32, string, vec3, enum, entity_ref) with validation constraints.

flint-ecs

Wraps hecs with:

  • BiMap<EntityId, hecs::Entity> for stable ID mapping
  • Named entity lookup
  • Parent-child relationship tracking
  • Atomic ID counter for deterministic allocation

flint-scene

TOML serialization and deserialization for scenes. Handles the mapping between on-disk format and in-memory ECS world.

flint-query

PEG parser (pest) for the query language. Parses queries like entities where archetype == 'door' and executes them against the ECS world.

Supported operators: ==, !=, >, <, >=, <=, contains

flint-constraint

Constraint engine that validates scenes against declarative TOML rules. Supports required components, value ranges, reference validity, and custom query rules. Includes an auto-fix system with cascade detection.

flint-asset

Content-addressed asset storage with SHA-256 hashing. Manages an asset catalog with name/hash/type/tag indexing. Supports resolution strategies (strict, placeholder).

flint-import

File importers for bringing external assets into the content-addressed store. Supports glTF/GLB with mesh, material, and texture extraction.

flint-render

wgpu 23 PBR renderer with:

  • Cook-Torrance shading — physically-based BRDF with roughness/metallic workflow
  • Cascaded shadow mapping — directional light shadows across multiple distance ranges
  • glTF mesh rendering — imported models rendered with full material support
  • Billboard sprite pipeline — camera-facing quads with sprite sheet animation and binary alpha
  • Camera modes — orbit (scene viewer) and first-person (player), sharing view/projection math
  • Headless mode — render to PNG for CI and automated screenshots

flint-viewer

egui-based GUI inspector built on top of flint-render:

  • Entity tree with selection
  • Component property editor
  • Constraint violation overlay
  • Hot-reload via file watching (serve --watch)

flint-runtime

Game loop infrastructure for interactive scenes:

  • GameClock — fixed-timestep accumulator (1/60s default)
  • InputState and InputConfig — keyboard/mouse/gamepad tracking with TOML-configured action bindings
  • EventBus — decoupled event dispatch between systems
  • RuntimeSystem trait — standard interface for update/render systems
  • GameStateMachine — pushdown automaton for game states (play, pause, menu) with per-system SystemPolicy
  • PersistentStore — key-value data that survives scene transitions

flint-physics

Rapier 3D integration:

  • PhysicsWorld — manages Rapier rigid body and collider sets, raycasting via EntityRaycastHit
  • PhysicsSync — bridges TOML component data to Rapier bodies, maintains collider-to-entity mapping
  • CharacterController — kinematic first-person movement with gravity, jumping, and ground detection
  • Uses kinematic bodies for player control, static bodies for world geometry

flint-audio

Kira 0.11 integration for game audio:

  • AudioEngine — wraps Kira AudioManager, handles sound loading and listener positioning
  • AudioSync — bridges TOML audio_source components to Kira spatial tracks
  • AudioTrigger — maps game events (collision, interaction) to sound playback
  • Spatial 3D audio with distance attenuation, non-spatial ambient loops
  • Graceful degradation when no audio device is available (headless/CI)

flint-animation

Two-tier animation system:

  • Tier 1: Property tweensAnimationClip with keyframe tracks targeting transform properties (position, rotation, scale) or custom fields. Step, Linear, and CubicSpline interpolation. Clips defined in .anim.toml files.
  • Tier 2: Skeletal animationSkeleton and SkeletalClip types for glTF skin/joint hierarchies. GPU vertex skinning via bone matrix storage buffer. Crossfade blending between clips.
  • AnimationSync bridges ECS animator components to property playback
  • SkeletalSync bridges ECS to skeletal playback with bone matrix computation

flint-particles

GPU-instanced particle system for visual effects:

  • ParticlePool — swap-remove array for O(1) particle death, contiguous alive iteration
  • ParticleSync — bridges ECS particle_emitter components to the simulation, auto-discovers new emitters each frame
  • ParticleSystem — top-level RuntimeSystem that ticks simulation in update() (variable-rate, not fixed-step)
  • ParticlePipeline — wgpu render pipeline with alpha and additive variants, storage buffer for instances
  • Emission shapes: point, sphere, cone, box. Value-over-lifetime interpolation for size and color.

flint-script

Rhai scripting engine for runtime game logic:

  • ScriptEngine — compiles .rhai files, manages per-entity Scope and AST, dispatches callbacks
  • ScriptSync — discovers entities with script components, monitors file timestamps for hot-reload
  • ScriptSystemRuntimeSystem implementation running in update() (variable-rate)
  • Full API: entity CRUD, input, time, audio, animation, physics (raycast, camera), math, events, logging, UI draw
  • ScriptCommand pattern — deferred audio/event effects processed by PlayerApp after script batch
  • DrawCommand pattern — immediate-mode 2D draw primitives (text, rect, circle, line, sprite) rendered via egui
  • ScriptCallContext with raw *mut FlintWorld pointer for world access during call batches
  • Depends on flint-physics for raycast and camera direction access

flint-asset-gen

AI asset generation pipeline:

  • GenerationProvider trait with pluggable implementations (Flux, Meshy, ElevenLabs, Mock)
  • StyleGuide — TOML-defined visual vocabulary (palette, materials, geometry constraints) for prompt enrichment
  • SemanticAssetDef — maps intent (description, material, wear level) to generation requests
  • Batch scene resolution with strategies: AiGenerate, HumanTask, AiThenHuman
  • validate_model() — checks GLB geometry and materials against style constraints
  • BuildManifest — provenance tracking (provider, prompt, content hash) for all generated assets
  • FlintConfig — layered configuration for API keys and provider settings
  • JobStore — persistent tracking of async generation jobs (for long-running 3D model generation)

flint-procgen

Procedural generation framework:

  • Generator trait — pluggable generator interface with generate(), param_schema(), estimate_cost()
  • GeneratorRegistry — register and look up generators by type name
  • ProcGenSpec — TOML spec format with metadata, seed config, and generator parameters
  • Built-in generators: tree_v1 (L-system/space colonization trees), texture_v1 (PBR texture maps), creature_v1
  • ProcGenCache — LRU cache keyed by (spec_hash, seed) with memory budget
  • Algorithmic building blocks: noise (Perlin, simplex, Worley, FBM), L-system engine, mesh builder, space colonization

flint-procgen-ai

AI-assisted procedural generation (tool-time only):

  • ProcGenAgent trait — interpret_spec(), create_spec_from_prompt(), refine_spec()
  • MockAgent implementation for testing

flint-terrain

Heightmap terrain system:

  • Chunked mesh generation from grayscale PNG heightmaps
  • RGBA splat-map blending for up to 4 texture layers
  • Bilinear height interpolation for smooth surfaces
  • terrain_height(x, z) callback for script queries
  • Does NOT depend on flint-render; uses standard Vertex format

flint-android

Android entry point (excluded from default workspace members):

  • NativeActivity integration
  • APK asset extraction
  • Build via cargo ndk -p flint-android; min API 26

flint-player

Standalone player binary that wires together runtime, physics, audio, animation, particles, scripting, and rendering:

  • Full game loop: clock tick, fixed-step physics, audio sync, animation advance, script update, first-person rendering
  • Scene loading with physics body creation from TOML collider/rigidbody components
  • Audio source loading and spatial listener tracking
  • Skeletal animation with bone matrix upload to GPU each frame
  • Rhai script system with event dispatch (collisions, triggers, actions, interactions)
  • Script-driven 2D HUD overlay via DrawCommand pipeline (replaces hardcoded HUD)
  • Billboard sprite rendering for Doom-style entities
  • First-person controls (WASD, mouse look, jump, sprint, interact, fire)
  • Optional asset catalog integration for runtime name-based asset resolution

flint-cli

Binary crate with clap-derived command definitions. Routes commands to the appropriate subsystem crate. Commands: init, entity, scene, query, schema, edit, play, validate, asset, render, gen, prefab.

Further Reading

Crate Dependency Graph

This page shows how Flint’s twenty-three crates depend on each other. Dependencies flow downward — higher crates depend on lower ones, never the reverse.

Dependency Diagram

          ┌─────────────┐                ┌──────────────┐
          │  flint-cli  │                │ flint-player │
          │  (binary)   │                │   (binary)   │
          └──────┬──────┘                └──────┬───────┘
                 │                              │
    ┌────┬───┬──┴──┬────┬────┬─────┐   ┌───┬───┴───┬───┬───┬───┬───┐
    │    │   │     │    │    │     │   │   │       │   │   │   │   │
    ▼    │   ▼     ▼    ▼    ▼     ▼   ▼   ▼       ▼   │   │   │   │
 ┌──────┐│┌─────┐┌────┐│ ┌─────┐┌────────┐┌────────┐┌────────┐│   │
 │viewer│││scene││qry ││ │const││asset-gen││runtime ││physics ││   │
 └──┬───┘│└──┬──┘└─┬──┘│ └──┬──┘└───┬────┘└───┬────┘└───┬────┘│   │
    │    │   │     │   │    │       │          │         │     │   │
    │    ▼   │     │   │    │       │          │         │     ▼   │
    │ ┌──────────┐ │   │    │       │          │         │  ┌─────────┐
    ├►│  render  │◄┘   │    │       │          │         ├─►│ script  │
    │ └────┬─────┘     │    │       │          │         │  └────┬────┘
    │      │           │    │       │          │         │       │
    │      │           │    │       │          │         ▼       ▼
    │      │           │    │       │          │      ┌─────────────┐
    │      │           │    │       │          │      │audio  anim  │
    │      ▼           │    │       │          │      └──────┬──────┘
    │ ┌──────────┐     │   ┌┘      │          │             │
    │ │  import  │     │   │       │          │             │
    │ └────┬─────┘     │   │       │          │             │
    │      │           ▼   ▼       ▼          ▼             ▼
    │      │    ┌──────────────────────────────────────────────────┐
    │      │    │              flint-ecs                            │
    │      │    │  (hecs wrapper, stable IDs, hierarchy)           │
    │      │    └────────────────┬─────────────────────────────────┘
    │      │                    │
    │      │                    ▼
    │      │             ┌──────────────┐
    │      │             │ flint-schema │
    │      │             └──────┬───────┘
    │      │                    │
    │      ▼                    ▼
    │ ┌──────────┐  ┌─────────────────────────────────────┐
    │ │  asset   │  │            flint-core                │
    │ └────┬─────┘  │ (EntityId, Vec3, Transform, Hash)   │
    │      │        └─────────────────────────────────────┘
    │      │                    ▲
    └──────┴────────────────────┘

Dependency Details

CrateDepends OnDepended On By
flint-core(none)all other crates
flint-schemacoreecs, constraint
flint-ecscore, schemascene, query, render, constraint, runtime, physics, audio, animation, viewer, player, cli
flint-assetcoreimport, cli
flint-importcore, assetrender, animation, viewer, cli, player
flint-querycore, ecsconstraint, cli
flint-scenecore, ecs, schemaviewer, player, cli
flint-constraintcore, ecs, schema, queryviewer, cli
flint-rendercore, ecs, importviewer, player, cli
flint-runtimecore, ecsphysics, audio, animation, player
flint-physicscore, ecs, runtimescript, player
flint-audiocore, ecs, runtimeplayer
flint-animationcore, ecs, import, runtimeplayer
flint-particlescore, ecs, runtimeplayer
flint-scriptcore, ecs, runtime, physicsplayer
flint-terraincoreplayer
flint-asset-gencore, asset, importcli
flint-procgencorecli, player, procgen-ai
flint-procgen-aicore, procgencli
flint-viewercore, ecs, scene, schema, render, import, constraintcli
flint-playercore, schema, ecs, scene, render, runtime, physics, audio, animation, particles, script, import, asset, terrain, procgen(binary entry point)
flint-androidplayer(binary entry point, excluded from default members)
flint-cliall crates(binary entry point)

Key Properties

Acyclic. The dependency graph has no cycles. This is enforced by Cargo and ensures clean compilation ordering.

Layered. Crates form clear layers:

  1. Core — fundamental types (flint-core)
  2. Schema — data definitions (flint-schema)
  3. Storage — entity and asset management (flint-ecs, flint-asset)
  4. Logic — query, scene, constraint, import, asset-gen, procgen, procgen-ai
  5. Systems — render, runtime, physics, audio, animation, particles, script, terrain
  6. Applications — viewer, player, android
  7. Interface — CLI binary (flint-cli), player binary (flint-player)

Two entry points. The CLI binary (flint-cli) serves scene authoring and validation workflows. The player binary (flint-player) serves interactive gameplay. Both share the same underlying crate hierarchy.

Mostly independent subsystems. The constraint, asset, physics, audio, animation, particles, asset generation, and render systems don’t depend on each other. The one exception is flint-script, which depends on flint-physics for raycasting and camera direction access. This means most subsystems can be built and tested in isolation.

External Dependencies

Key third-party crates used across the workspace:

CrateUsed ByPurpose
hecsflint-ecsUnderlying ECS implementation
tomlmost cratesTOML parsing and serialization
serdeall cratesSerialization framework
pestflint-queryPEG parser generator
wgpuflint-render, flint-viewer, flint-playerGPU abstraction layer
winitflint-render, flint-viewer, flint-runtime, flint-playerWindow and input management
rapier3dflint-physics3D physics simulation
kiraflint-audioSpatial audio engine
glamflint-audioVec3/Quat types for Kira spatial positioning (via mint interop)
eguiflint-viewerImmediate-mode GUI framework
clapflint-cli, flint-playerCommand-line argument parsing
thiserrorall library cratesError derive macros
sha2flint-core, flint-assetSHA-256 hashing
gltfflint-importglTF file parsing (meshes, materials, skins, animations)
crossbeamflint-physicsChannel-based event collection (Rapier)
rhaiflint-scriptEmbedded scripting language
gilrsflint-playerGamepad input (buttons, axes, multi-controller)
ureqflint-asset-genHTTP client for AI provider APIs
uuidflint-asset-genUnique job identifiers

Roadmap

Flint has a solid foundation — PBR rendering, physics, audio, animation, scripting, particles, post-processing, AI asset generation, and a shipped Doom-style FPS demo. The roadmap now focuses on the features needed to ship production games.

Visual Scene Tweaking

Priority: High

Flint’s core thesis is that scenes are authored by AI agents and code — not by dragging objects around a viewport. But AI-generated layouts often need human nudges: a light that’s slightly too far left, a prop that clips through a wall, a rotation that’s five degrees off. The goal isn’t a full scene editor — it’s a lightweight adjustment layer on top of the CLI-first workflow.

  • Translate / rotate / scale gizmos for fine-tuning positions
  • Property inspector for tweaking component values in-place
  • Changes write back to the scene TOML (preserving AI-authored structure)
  • Undo / redo for safe experimentation

Frustum Culling & Level of Detail

Priority: High

Without visibility culling, every object renders every frame regardless of whether it’s on screen. This is the performance ceiling that blocks larger scenes.

  • BVH spatial acceleration structure
  • Frustum culling (skip off-screen objects entirely)
  • Mesh LOD switching by camera distance
  • Optional texture streaming for large worlds

Priority: High

Every game with NPCs needs this. Currently enemies can only do simple raycast-based movement — entire genres are blocked without proper pathfinding.

  • Nav mesh generation from scene geometry
  • A* pathfinding with dynamic obstacle avoidance
  • Script API: find_path(from, to), move_along_path()
  • Optional crowd simulation (RVO) for dense NPC scenes

Coroutines & Async Scripting

Priority: High

Rhai scripts today are strictly synchronous per-frame. There’s no clean way to express “wait 2 seconds, then open the door, then play a sound” without manually tracking elapsed time in component state.

  • yield / wait(seconds) mechanism for time-based sequences
  • Coroutine scheduling integrated with the game loop
  • Cleaner cutscene, tutorial, and event-chain authoring

Transparent Material Rendering

Priority: High

The renderer currently uses binary alpha only — pixels are either fully opaque or discarded. There’s no way to render glass, water surfaces, energy shields, smoke, or any translucent material. This is a core rendering capability that gates visual variety across every genre.

  • Sorted alpha blending pass (back-to-front) for translucent materials
  • opacity field on material component (0.0–1.0)
  • Blend modes: alpha, additive, multiply
  • Refraction for glass and water (screen-space distortion)
  • Depth peeling or weighted-blended OIT for overlapping transparencies

Script Modules & Shared Code

Priority: High

As games grow beyond a handful of scripts, there’s no way to share utility functions. Every .rhai file is isolated — common code (damage formulas, inventory helpers, math utilities) gets copy-pasted across scripts. This is the biggest developer-productivity bottleneck for larger projects.

  • import "utils" mechanism to load shared .rhai modules
  • Module search path: scripts/lib/ for shared code, game-level overrides
  • Pre-compiled module caching (avoid re-parsing shared code per entity)
  • Hot-reload awareness (recompile dependents when a module changes)

UI Layout System Done

Data-driven UI with layout/style/logic separation. Structure defined in .ui.toml, visuals in .style.toml, logic in Rhai scripts. The procedural draw_* API continues to work alongside the layout system.

  • Anchor-based positioning (9 anchor points: top-left through bottom-right)
  • Flow layouts: vertical stacking (default) and horizontal
  • Percentage-based sizing, auto-height containers, padding and margin
  • Named style classes with runtime overrides from scripts
  • Rhai API: load_ui, unload_ui, ui_set_text, ui_show/ui_hide, ui_set_style, ui_set_class, ui_get_rect
  • Element types: Panel, Text, Rect, Circle, Image
  • Multi-document support with handle-based load/unload
  • Layout caching with automatic invalidation on screen resize

Terrain System

Priority: Medium-High

The engine excels at interior scenes — taverns, dungeons, arenas — but has no solution for outdoor environments. Height-field terrain is the single biggest genre-unlocking feature missing: open-world, exploration, RTS, and large-scale games all depend on it.

  • Height-field terrain with chunk-based rendering
  • Material splatting (blend grass, dirt, rock, snow by painted weight maps)
  • Chunk LOD for draw-distance scaling
  • Collision mesh generation for physics and character controller
  • Script API: get_terrain_height(x, z) for grounding NPCs and objects

Audio Environment Zones

Priority: Medium-High

Walking from a stone cathedral into an open field should sound different. The spatial audio system handles positioning well, but there’s no environmental modeling. This is the audio equivalent of reflection probes — a massive immersion jump for minimal complexity.

  • Reverb zones defined as trigger volumes in scenes
  • Preset environments (cathedral, cave, forest, small room, underwater)
  • Smooth crossfade when transitioning between zones
  • Occlusion: sounds behind walls are muffled (raycast-based)
  • Script API: set_reverb_zone(entity_id, preset), set_reverb_mix(wet, dry)

Decal System

Priority: Medium

Bullet holes, blood splatters, scorch marks, footprints — decals are the detail layer that makes game worlds feel responsive. Currently there’s no way to project textures onto existing geometry at runtime.

  • Projected-texture decal rendering
  • Configurable lifetime, fade, and layering
  • Script API: spawn_decal(position, normal, texture)

Reflection Probes & Environment Mapping

Priority: Medium

The PBR pipeline handles diffuse and specular lighting well, but specular reflections are essentially absent. This is the single biggest visual quality jump available.

  • Pre-baked cubemap reflection probes at authored positions
  • Probe blending between adjacent volumes
  • Correct specular reflections on metals, water, glass, and polished surfaces

Material Instance System

Priority: Medium

Each entity currently specifies its own texture paths and PBR parameters. There’s no way to define “worn stone” once and apply it to fifty objects.

  • Named material definitions (textures + PBR parameters)
  • Material instances that reference and override a base material
  • Material library for cross-scene reuse

Save & Load Game State

Priority: Medium

PersistentStore survives scene transitions, but there’s no way to snapshot and restore full ECS state mid-scene. Any game longer than a single session needs this.

  • Full ECS snapshot (all entities, components, script state) to disk
  • Restore from snapshot with entity ID remapping
  • Checkpoint and quicksave support
  • Script API: save_game(slot), load_game(slot)

3D Debug Drawing

Priority: Medium

The 2D overlay draws in screen-space, but there’s no way to visualize 3D information — physics colliders, AI sight cones, pathfinding routes, trigger volumes, raycast results. This is the single most impactful developer tool for iterating on gameplay.

  • Script API: debug_line(from, to, color), debug_box(center, size, color), debug_sphere(center, radius, color), debug_ray(origin, dir, length, color)
  • Wireframe overlay rendered after scene, before HUD
  • Auto-clear each frame (immediate-mode, like the 2D draw API)
  • Toggle with a debug key (e.g. F10) — zero overhead when disabled
  • Optional built-in modes: visualize physics colliders, trigger volumes, nav meshes

Performance Profiler Overlay

Priority: Medium

Targeted optimization requires knowing where time is spent. Currently there’s no visibility into the frame budget breakdown.

  • In-engine overlay: frame time, draw calls, triangle count, memory
  • Per-system breakdown (render vs physics vs scripts vs audio)
  • Frame time graph with spike detection
  • Toggle with a debug key (e.g. F9)

Further Horizon

These are ideas under consideration, not committed plans:

  • Networking — multiplayer support with entity replication
  • Plugin system — third-party engine extensions
  • Package manager — share schemas, constraints, and assets between projects
  • WebAssembly — browser-based viewer and potentially runtime
  • Shader graph — visual shader editing for non-programmers

Contributing

All five phases of Flint are complete. Contributions are welcome in these areas:

  • Bug reports — file issues on GitHub
  • Schema definitions — new component and archetype schemas
  • Documentation — improvements to this guide
  • Test coverage — additional unit and integration tests (217 tests across 18 crates)
  • Constraint kinds — new validation rule types
  • Physics — additional collider shapes, improved character controller behavior
  • Rendering — post-processing effects, LOD, additional debug views
  • Audio — additional audio formats, reverb zones, music system
  • Animation — blend trees, additive blending, animation state machines
  • Scripting — new Rhai API functions, script debugging tools, performance profiling
  • AI generation — new provider integrations, improved style validation, prompt engineering

Development Setup

git clone https://github.com/chrischaps/flint.git
cd flint
cargo build
cargo test
cargo clippy
cargo fmt --check

Running the Demo

# Scene viewer with hot-reload
cargo run --bin flint -- serve demo/phase4_runtime.scene.toml --watch

# First-person walkable scene
cargo run --bin flint -- play demo/phase4_runtime.scene.toml

Code Style

  • Run cargo fmt before committing
  • Run cargo clippy and address warnings
  • Each crate has its own error type using thiserror
  • Tests live alongside the code they test (#[cfg(test)] modules)
  • Prefer explicit over clever; readability over brevity

Architecture

The project is an 18-crate Cargo workspace. See the Architecture Overview and Crate Dependency Graph for how the crates relate to each other. Key principles:

  • Dependencies flow in one direction (binary crates at the top, flint-core at the bottom)
  • Components are dynamic toml::Value, not Rust types — schemas are runtime data
  • Two entry points: flint-cli (scene authoring) and flint-player (interactive gameplay)