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

Reflection: components and resources as JSON

Reflection lets code that doesn’t know your types read and change them by name: the agent bridge (MCP), the inspector, and save/load all use it. It’s opt-in per type and built on serde.

#![allow(unused)]
fn main() {
use ponko::*;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
struct Health { current: f32, max: f32 }

Game::new("Arena", 1280, 720).run(|ctx| {
    ctx.world.reflect::<Health>();
    let e = ctx.world.spawn((Transform2D::default(), Health { current: 10.0, max: 10.0 }));

    // Read: {"current": 10.0, "max": 10.0}
    let hp = ctx.world.get_json(e, "Health").unwrap();

    // Write only some fields; the rest keep their values.
    ctx.world.set_json(e, "Health", json!({ "current": 3.0 })).unwrap();
});
}

Add serde = { version = "1", features = ["derive"] } to your Cargo.toml.

C and C++ components

C++ has no serde, so a C++ component lists its fields with world.reflect<T>. From then on it is a reflected component like any other: agents read and write it through ponko-mcp, and saves include it, under the type’s name (Unit, or game::Unit in a namespace) or the one you pass first: world.reflect<Unit>("Soldier", {...}).

struct Unit {
    float current;
    float max;
    crabby::vec2 home;
    crabby::entity target;   // entity ids are uint64_t
    bool angry;
};

static void reflect_units(crabby::world_view world) {
    // Tools and agents (ponko-mcp) now see {"current", "max", "home", "target", "angry"}.
    world.reflect<Unit>({
        crabby::field("current", &Unit::current),
        crabby::field("max", &Unit::max),
        crabby::field("home", &Unit::home),
        crabby::field("target", &Unit::target),
        crabby::field("angry", &Unit::angry),
    });
}

Fields can be float, double, the fixed-width integers, bool, crabby::vec2 ([x, y] in JSON) and crabby::color ({"r", "g", "b", "a"}). Entities are uint64_t, which is how Rust saves an Entity too. Bytes no field covers (padding, fields you leave out) are zero in values made from JSON, and editing a field leaves them alone.

In C, crabby_component_reflect(world, id, fields, count) takes the same list as CrabbyField { name, kind, offset }, with offsetof for the offset. The Rust side of this is World::reflect_fields, for components that are registered at runtime and have no Rust type.

Resources

Game-wide state (a score, the current round, rules an agent might want to tweak) lives in resources. Reflect them the same way:

#![allow(unused)]
fn main() {
#[derive(Serialize, Deserialize)]
struct Score { red: u32, blue: u32 }

world.reflect_resource::<Score>();
world.insert_resource(Score { red: 0, blue: 0 });

world.resource_json("Score")?;                          // {"red": 0, "blue": 0}
world.set_resource_json("Score", json!({ "blue": 3 }))?; // only blue changes
}
struct Teams { std::uint32_t red, blue; };

static void reflect_teams(crabby::world_view world) {
    world.reflect_resource<Teams>({crabby::field("red", &Teams::red), crabby::field("blue", &Teams::blue)});
    world.set_resource(Teams{0, 0});   // agents: get_resource "Teams" -> {"blue": 0, "red": 0}
}

Reflected resources show up in ponko-mcp (list_resources, get_resource, set_resource) and are part of world saves. Resources hold whatever Rust type you like, so the ones that can’t be JSON (a nav grid, a GPU handle) just stay unreflected. A C++ resource is saved under its type name with namespaces (game::Score); C names it in crabby_resource_reflect.

Names

A component’s name is its type name without the module path: my_game::units::Health is "Health". Reflecting two different types with the same short name panics and names both, so a name always means one type.

Transform2D, Sprite and Attached are reflected in every game already. A sprite’s texture is written as the name of its image in assets/ ("texture": "crab.png"), and setting one loads the image. Sprites of textures made in code (create_texture) have no file to name, so they’re left out of saves.

The methods

All of them come from WorldReflect, which use ponko::*; brings in.

MethodDoes
reflect::<T>()makes T visible to reflection
reflected_components()names of all reflected components
get_json(e, name)one component as JSON
set_json(e, name, value)writes it; adds it if missing; objects merge field by field
remove_json(e, name)removes it; Ok(true) if it was there
entity_json(e)all reflected components of e, keyed by name
spawn_json(value)spawns from the entity_json format; on error spawns nothing
reflect_resource::<R>()makes resource R visible to reflection
reflected_resources()names of all reflected resources
resource_json(name)one resource as JSON
set_resource_json(name, value)writes it; inserts it if missing; objects merge field by field

entity_json and spawn_json round-trip, so a snapshot of an entity can be copied, saved, or sent over the wire:

#![allow(unused)]
fn main() {
let snapshot = world.entity_json(e)?;   // {"Transform2D": {...}, "Health": {...}}
let copy = world.spawn_json(snapshot)?;
}

Components that aren’t reflected are left out of entity_json.

Errors

Every call returns a ReflectError that says what went wrong: UnknownComponent, NoSuchEntity, MissingComponent, UnknownResource, MissingResource, or BadValue (with serde’s message, such as invalid type: string "seven", expected struct Health).

Tips

  • #[serde(default)] on a struct lets set_json add the component from a partial value. Transform2D has it.
  • Reflection is for tools, not hot loops: it goes through JSON. Use queries inside systems.