Game Programming with Reflow

Reflow's game architecture follows the Entity-Component-System pattern. The AssetDB is the world. Components are queryable data. DAG actors are systems.

AssetDB (World)          Reflow DAG (Systems)         External Tools
┌──────────────┐        ┌──────────────────┐        ┌──────────────┐
│ player:      │◀──────▶│ PhysicsSystem    │        │ Zeal Editor  │
│   transform  │        │ CameraSystem     │        │ Debug Tools  │
│   rigidbody  │        │ LightCollector   │        │ Scripts      │
│   collider   │        │ MaterialSystem   │        │ Unit Tests   │
│   mesh       │        │ RenderSystem     │        │              │
│   material   │        └──────────────────┘        └──────────────┘
│              │                                           │
│ sun:light    │◀──────────────────────────────────────────┘
│ main:camera  │         any tool reads/writes the same DB
└──────────────┘

Quick Start

1. Set up the world

Create entities by putting components into the AssetDB. An entity is just a name prefix. A component is a type suffix.

#![allow(unused)]
fn main() {
use reflow_assets::get_or_create_db;
use serde_json::json;

let db = get_or_create_db("./game.db")?;

// Player entity
db.set_component_json("player", "transform", json!({
    "position": [0.0, 1.0, 0.0],
    "rotation": [0.0, 0.0, 0.0, 1.0],
    "scale": [1.0, 1.0, 1.0],
}), json!({}))?;

db.set_component_json("player", "rigidbody", json!({
    "bodyType": "dynamic",
    "mass": 80.0,
    "linearDamping": 0.1,
    "gravityScale": 1.0,
}), json!({}))?;

db.set_component_json("player", "collider", json!({
    "shape": "capsule",
    "radius": 0.3,
    "height": 1.8,
    "friction": 0.5,
    "restitution": 0.1,
}), json!({}))?;

// Ground
db.set_component_json("ground", "transform", json!({
    "position": [0.0, 0.0, 0.0],
}), json!({}))?;

db.set_component_json("ground", "rigidbody", json!({
    "bodyType": "static",
}), json!({}))?;

db.set_component_json("ground", "collider", json!({
    "shape": "box",
    "halfExtents": [50.0, 0.1, 50.0],
}), json!({}))?;

// Camera
db.set_component_json("main", "camera", json!({
    "mode": "thirdPerson",
    "target": "player",
    "fov": 60.0,
    "distance": 5.0,
    "height": 2.0,
    "orbitPitch": 0.3,
    "active": true,
}), json!({}))?;

// Sun light
db.set_component_json("sun", "light", json!({
    "type": "directional",
    "direction": [0.0, -1.0, 0.5],
    "color": [1.0, 1.0, 0.9],
    "intensity": 2.0,
    "castShadow": true,
}), json!({}))?;

// Torch (point light)
db.set_component_json("torch", "transform", json!({
    "position": [3.0, 2.0, 1.0],
}), json!({}))?;

db.set_component_json("torch", "light", json!({
    "type": "point",
    "color": [1.0, 0.6, 0.2],
    "range": 10.0,
    "intensity": 3.0,
}), json!({}))?;

// Material
db.set_component_json("player", "material", json!({
    "albedo": [0.8, 0.2, 0.1],
    "metallic": 0.0,
    "roughness": 0.5,
}), json!({}))?;
}

2. Wire the game loop DAG

The DAG connects systems. Each system reads components, processes, writes results back.

#![allow(unused)]
fn main() {
use reflow_network::{network::{Network, NetworkConfig}, message::Message};

let mut net = Network::new(NetworkConfig::default());

// Register system actors
for tpl in [
    "tpl_interval_trigger",
    "tpl_scene_physics",
    "tpl_scene_camera",
    "tpl_scene_light_collector",
    "tpl_scene_material",
] {
    net.register_actor_arc(tpl, reflow_components::get_actor_for_template(tpl).unwrap())?;
}

// Game tick at 60fps
net.add_node("tick", "tpl_interval_trigger", config(json!({
    "interval": 16, "startImmediately": true,
})))?;

// Systems — all read/write the same AssetDB
net.add_node("physics", "tpl_scene_physics", config(json!({
    "$db": "./game.db",
    "gravity": [0.0, -9.81, 0.0],
    "dt": 0.016,
})))?;

net.add_node("camera", "tpl_scene_camera", config(json!({
    "$db": "./game.db",
    "aspect": 1.777,
    "cameraTag": "main",
})))?;

net.add_node("lights", "tpl_scene_light_collector", config(json!({
    "$db": "./game.db",
})))?;

net.add_node("materials", "tpl_scene_material", config(json!({
    "$db": "./game.db",
})))?;

// Wire: tick drives all systems
net.add_connection(wire("tick", "trigger", "physics", "tick"));
net.add_connection(wire("tick", "trigger", "camera", "tick"));
net.add_connection(wire("tick", "trigger", "lights", "tick"));
net.add_connection(wire("tick", "trigger", "materials", "tick"));

// Start
net.add_initial(iip("tick", "_trigger", Message::Flow));
net.start()?;
}

3. Query the world from anywhere

The AssetDB is the single source of truth. Any tool can read and write it — not just the DAG.

#![allow(unused)]
fn main() {
// Inspect an entity
let snapshot = db.entity_snapshot("player")?;
println!("{}", serde_json::to_string_pretty(&snapshot)?);
// {
//   "transform": { "position": [0.0, 0.83, 0.0], ... },
//   "rigidbody": { "bodyType": "dynamic", "mass": 80.0, ... },
//   "collider": { "shape": "capsule", ... },
//   "velocity": { "linear": [0.0, -0.12, 0.0], ... }
// }

// Find all dynamic bodies
let dynamic_entities = db.query_dsl(&json!({
    "type": "rigidbody",
    "metadata.bodyType": "dynamic",
}))?;

// Find all entities with both mesh and material
let renderable = db.entities_with(&["mesh", "material", "transform"])?;

// Teleport the player
db.set_component_json("player", "transform", json!({
    "position": [10.0, 5.0, 0.0],
    "rotation": [0.0, 0.0, 0.0, 1.0],
    "scale": [1.0, 1.0, 1.0],
}), json!({}))?;
}

Component Reference

transform

Position, rotation, and scale of an entity in the world.

{
    "position": [0.0, 0.0, 0.0],
    "rotation": [0.0, 0.0, 0.0, 1.0],
    "scale": [1.0, 1.0, 1.0]
}

rigidbody

Physics body properties. The physics system picks up any entity with both rigidbody and transform.

{
    "bodyType": "dynamic",
    "mass": 1.0,
    "linearDamping": 0.1,
    "angularDamping": 0.1,
    "gravityScale": 1.0,
    "ccd": false
}

Body types: "dynamic" (simulated), "static" (immovable), "kinematic" (user-driven).

collider

Collision shape attached to a rigidbody.

{
    "shape": "capsule",
    "radius": 0.3,
    "height": 1.8,
    "friction": 0.5,
    "restitution": 0.3,
    "isSensor": false
}

Shapes: "box" (halfExtents), "sphere" (radius), "capsule" (radius + height), "cylinder" (radius + height).

camera

View configuration. Multiple cameras per scene are supported. Use "active": true or pass a tag to select which renders.

{
    "mode": "thirdPerson",
    "target": "player",
    "fov": 60.0,
    "near": 0.1,
    "far": 1000.0,
    "distance": 5.0,
    "height": 2.0,
    "orbitYaw": 0.0,
    "orbitPitch": 0.3,
    "active": true
}

Modes: "fixed" (position + target), "firstPerson" (attached to entity), "thirdPerson" (follow with offset), "orbit" (rotate around center).

light

Light source. Position comes from the entity's transform component for point/spot lights.

{
    "type": "directional",
    "direction": [0.0, -1.0, 0.5],
    "color": [1.0, 1.0, 0.9],
    "intensity": 2.0,
    "castShadow": true,
    "range": 20.0,
    "innerAngle": 30.0,
    "outerAngle": 45.0
}

Types: "directional" (sun), "point" (bulb), "spot" (cone), "ambient" (fill).

material

PBR material properties. Texture fields reference AssetDB entity IDs.

{
    "albedo": [0.8, 0.2, 0.1],
    "metallic": 0.0,
    "roughness": 0.5,
    "emissive": [0.0, 0.0, 0.0],
    "emissiveStrength": 0.0,
    "ao": 1.0,
    "alphaMode": "opaque",
    "doubleSided": false,
    "albedoTexture": "wood:texture",
    "normalTexture": "wood_normal:texture"
}

System Actors

ActorTemplate IDReadsWritesOutputs
ScenePhysicsSystemtpl_scene_physicsrigidbody, collider, transformtransform, velocitycollision pairs
SceneCameraSystemtpl_scene_cameracamera, transform (of target)camera_matricesactive camera data
SceneLightCollectortpl_scene_light_collectorlight, transformpacked light buffer
SceneMaterialSystemtpl_scene_materialmaterialpacked material buffer

Spawning Entities

Use spawn_from to instantiate prefabs:

#![allow(unused)]
fn main() {
// Define a template once
db.set_component_json("crate_template", "transform", json!({
    "position": [0.0, 0.0, 0.0],
}), json!({}))?;
db.set_component_json("crate_template", "rigidbody", json!({
    "bodyType": "dynamic", "mass": 10.0,
}), json!({}))?;
db.set_component_json("crate_template", "collider", json!({
    "shape": "box", "halfExtents": [0.5, 0.5, 0.5],
}), json!({}))?;
db.set_component_json("crate_template", "material", json!({
    "albedo": [0.6, 0.4, 0.2], "roughness": 0.8,
}), json!({}))?;

// Spawn 10 crates at different positions
for i in 0..10 {
    let name = format!("crate_{}", i);
    db.spawn_from("crate_template", &name)?;
    db.set_component_json(&name, "transform", json!({
        "position": [i as f64 * 2.0, 5.0, 0.0],
    }), json!({}))?;
}
}

The physics system picks them up automatically on the next tick.

Importing Assets

Load a Mixamo character into the world:

#![allow(unused)]
fn main() {
// Load file
let glb_data = std::fs::read("character.glb")?;

// Import extracts mesh, skeleton, animation, skin
// Wire in DAG: FileLoad → GltfImport → AssetStore
// Or programmatically:
db.put("character:mesh", &mesh_bytes, json!({"stride": 24}))?;
db.put_json("character:skeleton", skeleton_json, json!({}))?;
db.put_json("character:animation", clip_json, json!({}))?;

// Create entity using imported assets
db.set_component_json("npc", "transform", json!({
    "position": [5.0, 0.0, 3.0],
}), json!({}))?;
db.set_component_json("npc", "rigidbody", json!({
    "bodyType": "dynamic", "mass": 60.0,
}), json!({}))?;
db.set_component_json("npc", "collider", json!({
    "shape": "capsule", "radius": 0.3, "height": 1.6,
}), json!({}))?;
}

Multiple Cameras

Switch cameras by tag:

#![allow(unused)]
fn main() {
// Define cameras
db.set_component_json("gameplay_cam", "camera", json!({
    "mode": "thirdPerson", "target": "player", "fov": 60, "active": true,
}), json!({}))?;
db.tag("gameplay_cam:camera", &["gameplay"])?;

db.set_component_json("cutscene_cam", "camera", json!({
    "mode": "fixed", "position": [10, 5, 0], "target": [0, 0, 0], "active": false,
}), json!({}))?;
db.tag("cutscene_cam:camera", &["cutscene"])?;

// In the DAG, the camera system accepts a tag input:
// wire("camera_selector", "tag", "camera", "camera_tag")
// Send "cutscene" to switch to the cutscene camera
}

Collision Handling

The physics system outputs collision pairs. Wire a handler in the DAG:

tick → ScenePhysicsSystem
           │
           └→ collisions → YourCollisionHandler (user actor)
                               │
                               └→ reads collision pairs:
                                  [{ "a": "player", "b": "coin_3" }]
                                  → removes coin, adds score

Architecture Summary

ConcernWhere it livesWhy
Entity dataAssetDB componentsQueryable by any tool, not coupled to DAG
Game logicDAG actors (systems)Visual wiring, reorderable, hot-swappable
Execution orderDAG connectionsExplicit, debuggable dataflow
PersistenceAssetDB storage backendFile, IndexedDB, or S3 — same API
Editor integrationDirect AssetDB reads/writesNo DAG needed for inspection/editing