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

Writing a Component

A component is a plain struct that derives Reflect and implements Component. That is the whole contract — the Inspector, scene serialisation and the Add Component menu all follow from the derive.

The smallest one that works

#![allow(unused)]
fn main() {
use kooch::kooch_ecs::Reflect;
use kooch::kooch_ecs::component::Component;

/// How much damage this entity can still take.
#[derive(Default, Reflect)]
pub struct Health {
    pub current: f32,
    pub max: f32,
}

impl Component for Health {}
}

Create it from the editor, which drops this scaffold in src/ — or write the file yourself in any editor. Either way the registrations regenerate on their own: the editor polls src/ and rewrites them within a second of a save.

⚠️ Regenerating is not rebuilding. The Inspector lists what your project’s last build contained, so a component added a moment ago is in registrations.rs and in no binary yet. The toolbar’s Resync button pulses while the two disagree.

Public fields show up in the Inspector automatically. No attribute is required to opt in; attributes exist to opt out, or to say something the type alone cannot.

What the Inspector can draw

Each field’s Rust type maps to a FieldKind, and the kind decides the widget:

Rust typeWidget
f32, f64Drag value
u8…u64, i8…i64Drag value, clamped to the type
boolCheckbox
StringText field
Vec2, Vec3, Vec4Component-wise drag values
QuatEuler angles, in degrees
Mat4Decomposed to translation / rotation / lossy scale, read-only
Option<Guid> + #[reflect(asset = "…")]Typed asset picker
Option<EntityRef>Entity picker, and a drop target for a drag from the World panel
Entity, Option<Entity>Same widget, but see “Pointing at another entity” below
A struct that also derives ReflectNested, drawn inline

The maths types come from the prelude. Vec3, Quat and Mat4 are glam types, and kooch::prelude re-exports them so a project never declares its own glam dependency — which is the point, since a Quat from a different version is a different type and the compiler error would name two types spelled identically.

Hover a field name in the Inspector to see its doc comment. The derive harvests /// straight off the field, so documenting a component for the next reader also documents it for whoever is authoring the scene — there is no second place to write it and no second place for it to go stale.

Anything outside that list — Vec<T>, HashMap<K, V>, your own enums — is not supported yet. Recursive reflection for nested types and collections is #649. Until it lands, a field of an unsupported type needs #[reflect(skip)] or the derive will not compile.

Attributes

On the struct

#![allow(unused)]
fn main() {
#[derive(Default, Reflect)]
#[reflect(category = "Gameplay")]      // groups it in the Add Component menu
#[reflect(inspector = "read_only")]    // "hidden" | "read_only" | "editable" (default)
pub struct Health { … }
}

On a field

#![allow(unused)]
fn main() {
#[derive(Default, Reflect)]
pub struct Weapon {
    /// Not shown, not serialised. Use for runtime caches and for types
    /// the Inspector has no representation for.
    #[reflect(skip)]
    cached_target: Option<Entity>,

    /// A typed asset picker instead of a raw Guid text field.
    #[reflect(asset = "Mesh")]
    pub projectile: Option<Guid>,

    /// A dropdown of named values instead of a bare integer.
    #[reflect(choices = FIRE_MODE_CHOICES)]
    pub fire_mode: u32,

    /// A row of checkboxes instead of a bitmask you compute in your head.
    #[reflect(bits = DAMAGE_TYPE_BITS)]
    pub damage_types: u32,

    /// Only drawn when another field says it is relevant.
    #[reflect(shown_when = BURST_ONLY)]
    pub burst_count: u32,

    /// A reference the picker will only let you point at an entity
    /// carrying a `PhysicsBody`.
    #[reflect(requires = "PhysicsBody")]
    pub anchored_to: Option<EntityRef>,
}
}

choices, bits and shown_when take a path to a constant, not a string literal — so the same table is used by the Inspector and by your code, and they cannot drift apart.

shown_when is what keeps a component with many mutually-exclusive fields readable: the engine’s own Joint uses it so a hinge does not show you spring stiffness.

requires names a component, by its short name, that the target has to carry. The picker filters by it and refuses a drop that fails it, saying why — a reference accepted but inert is indistinguishable from a broken one.

Pointing at another entity

Use Option<EntityRef>.

#![allow(unused)]
fn main() {
use kooch_ecs::reflect::EntityRef;

#[derive(Default, Reflect)]
pub struct Turret {
    pub target: Option<EntityRef>,
}
}

Three things assign it, and all three write the same value: your code (turret.target = Some(EntityRef::live(entity))), the Inspector’s picker, and dragging an entity from the World panel onto the field.

EntityRef is two states, because a reference means two different things depending on where it lives:

  • Live — an index and a generation. What a running component holds, and what EntityRef::entity() gives you back for a query or a lookup.
  • Persistent — an identity that survives a reload. What a scene file holds.

You do not convert between them. Saving resolves live to persistent, loading resolves back, and a reference whose target’s scene is not open stays persistent until it is — which is why the field is Option<EntityRef> and not Option<Entity>. An Entity field has nowhere to put an unresolved reference, so it loses the link instead of keeping it.

Entity and Option<Entity> still reflect, for a handle the engine resolves itself (Parent is one). They refuse to store anything but a live reference.

Registration

You do not write it. The editor scans src/, finds impl Component for Health, and regenerates registrations.rs with both halves:

#![allow(unused)]
fn main() {
// Registers the type with the running ECS — scene save/load and the Inspector.
registry.register_cpu_reflected::<Health>();

// Describes the type to a standalone editor that loaded this dylib.
declare_component::<Health>(engine);
}

The component’s name comes from std::any::type_name::<T>(), so there is exactly one name for a type and no way for two sides to disagree about it.

What survives a save

A component is saved as its reflected fields. Two consequences worth knowing before you design a component:

  • #[reflect(skip)] fields are not saved. They are reconstructed by your code, or they are gone.
  • A reference to another entity is saved as an identity, not as a handle. The save path resolves it and assigns the target a persistent id if it has none, which is why saving a scene can modify the world. Nothing is asked of you beyond using Option<EntityRef>; a handle reaching a file is refused by name, and the save fails rather than writing a reference that would load pointing at some other entity.