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.
| Method | Does |
|---|---|
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 letsset_jsonadd the component from a partial value.Transform2Dhas it.- Reflection is for tools, not hot loops: it goes through JSON. Use queries inside systems.