Skip to content
hikari
RenderingEditorAIToolchainDocumentation
GitHub
hikari

© 2026 Flying Rat Studio.
All rights reserved.

Explore the engineDocumentationContributorsLicenseBack to top
Documentation / Guides
Browse docs
Overview
Tutorials16
OverviewFirst game projectFirst entityAuthored user_data and the inspectorFirst mesh and materialFirst physics body and triggerFirst character controllerFirst session servicesFirst UIFirst input actionFirst messagesPlay, Edit, and scenesFirst runtime spawnFirst motionFirst skeletal animationAssets in Play (soft refs and hot reload)Dynamic editor recompile
Guides16
OverviewDevelopment guideGameplay APIChoosing component storageBuild and packagingProject filePluginsHikari Plugin APIData-driven content, JSON, and pathsScripting with KawaShader authoringTime of dayUser interfaceUI layoutUI widgetsMigration from Unity / UnrealActor and component lifecycle
Systems28
OverviewArchitectureApplication lifecycleFrontends and driversPlatforms and supportSession services and cross-scene stateScenes and gameplayActor communication (hi.actors)Game-facing refsSave / replication wire versionCoordinate space and camera conventionsRenderingRenderer architecture mapFrame governorGPU particlesVisual ZonesVolumetric mediaInputAudioPhysicsMotion KitTemporal KitAssets and ShinraPrefabsAsset residencyAsset formats (Shinra pipeline)UI and editorEditor asset hot reloadEditor Project Selector
Language reference2
OverviewAkari language referenceKawa language reference
Engine overview
Start exploring
  • No matching sections. Try fewer words or another topic.
NavigateEnter Openesc Close
Overview
Tutorials16
OverviewFirst game projectFirst entityAuthored user_data and the inspectorFirst mesh and materialFirst physics body and triggerFirst character controllerFirst session servicesFirst UIFirst input actionFirst messagesPlay, Edit, and scenesFirst runtime spawnFirst motionFirst skeletal animationAssets in Play (soft refs and hot reload)Dynamic editor recompile
Guides16
OverviewDevelopment guideGameplay APIChoosing component storageBuild and packagingProject filePluginsHikari Plugin APIData-driven content, JSON, and pathsScripting with KawaShader authoringTime of dayUser interfaceUI layoutUI widgetsMigration from Unity / UnrealActor and component lifecycle
Systems28
OverviewArchitectureApplication lifecycleFrontends and driversPlatforms and supportSession services and cross-scene stateScenes and gameplayActor communication (hi.actors)Game-facing refsSave / replication wire versionCoordinate space and camera conventionsRenderingRenderer architecture mapFrame governorGPU particlesVisual ZonesVolumetric mediaInputAudioPhysicsMotion KitTemporal KitAssets and ShinraPrefabsAsset residencyAsset formats (Shinra pipeline)UI and editorEditor asset hot reloadEditor Project Selector
Language reference2
OverviewAkari language referenceKawa language reference
Engine overview
Guides8 min read

Choosing component storage

On this page
On this pageCompare the three choicesAttachable: the general-purpose defaultEmbedded: permanent actor stateSwarm: bulk population simulationDecide by the required behaviorInspecting runtime changes Back to top

Use attachable components for ordinary gameplay, embedded components for permanent actor state, and swarms for large populations that can operate without full actor services.

Storage is chosen per component, not per actor. One defineActor can combine attachable and embedded components. defineSwarm is a separate population API: its rows cannot go in an actor's .components tuple. The choice belongs to the component declaration: the same component type does not switch between attachable and embedded storage on different actors.

Compare the three choices

AttachableEmbeddedSwarm
DeclarationdefineComponent (default)defineComponent with .storage = .embeddeddefineSwarm
Data layoutDense array of whole component values; actor stores a row indexComponent value inside the actor payloadSeparate array for each data field (structure of arrays / SoA)
IdentityBelongs to an actor with ActorRefBelongs to an actor with ActorRefPopulation row with SwarmRef
MembershipAdd/remove individual components at runtimeFixed by the actor definitionFixed row schema; spawn/retire rows
CapacityGrows as needed; growth can allocateFollows actor population and payload sizeMaximum capacity reserved when the store is created
Ordinary actor hooksYesYesNo; bulk integrate or per-row update
Scene and inspector fieldsYes, for supported authored fieldsYes, for supported authored fieldsNo individual scene actor or actor inspector
Component replication optionSupported for eligible fieldsUnsupportedUnsupported
Strongest fitFlexible, reusable gameplay componentsSmall, permanent actor-specific logic/stateUniform simulation over many simple rows

Both actor storage kinds allow mutable data, including transient Data.state. Fixed membership does not make embedded values immutable. A swarm's fixed capacity does not make its population size fixed.

Attachable: the general-purpose default

Choose attachable components when different actors share the same behavior, or when an individual actor may gain or lose that behavior. Health, inventory, status effects, interaction, and optional abilities are typical examples.

zig
const hi = @import("hikari_game");

pub const Health = hi.defineComponent(.{
    .name = "health",
    .data = struct {
        hp: u32 = 100,
    },
});

pub const Player = hi.defineActor(.{
    .archetype = "player",
    .components = .{Health},
});

// The target must currently lack Health.
pub fn giveHealth(target: hi.ActorRef) !void {
    try target.component(Health).add();
}

.storage = .attachable is optional because it is the default. Exported content discovery registers the actor and component. Components used only for runtime attachment must also be registered with the game/module.

Strengths: component values are packed together by type, and membership can change without resizing the actor's fixed payload. Empty Data is also valid: a component that only supplies hooks still participates in dispatch and add/remove operations.

Limitations: resolving an actor's data needs a row lookup. Structural changes can move rows, so pointers are borrowed only for the current callback. Declared components have indexed payload slots; components attached to an instance outside its declared tuple use a side-list lookup. Declare commonly expected components in the actor tuple even when they must remain removable.

Add/remove requests inside callbacks commit at a later structural boundary. Adding and then immediately reading or setting the new component in the same callback does not see the queued instance. Use defaults or awake for its initial data. See runtime membership for dependencies, errors, and activation order.

Embedded: permanent actor state

Choose embedded components for compact logic or state that every instance of an actor type always needs: a door controller, puzzle state, or a permanent motion controller.

zig
const hi = @import("hikari_game");

pub const Spin = hi.defineComponent(.{
    .name = "spin",
    .storage = .embedded,
    .data = struct {
        degrees_per_second: f32 = 45,
        state: struct { yaw: f32 = 0 } = .{},

        pub fn update(self: *@This(), actor: hi.ActorContext, tick: *const hi.TickContext) void {
            self.state.yaw = @mod(self.state.yaw + self.degrees_per_second * tick.dt, 360);
            actor.setRotationEuler(.{ 0, self.state.yaw, 0 });
        }
    },
});

pub const Spinner = hi.defineActor(.{
    .archetype = "spinner",
    .components = .{Spin},
});

This rotates the actor's transform. Add and configure a render component if the actor should be visible. degrees_per_second is authored; state.yaw is runtime state, excluded from scene output and the inspector.

Strengths: data lives directly in the actor payload, so access avoids the separate component-row lookup. Embedded components use the same ordinary hook signatures and supported authored field schemas as attachable components.

Limitations: the component cannot be added or removed from an individual actor, and the component replication option is unavailable. Large embedded data enlarges every instance's payload. Reading one embedded field across many actors does not provide the contiguous field array a swarm does. Use measurement before concluding that embedding makes an entire workload faster.

Membership is fixed because the actor's generated payload has compile-time sizes and offsets, with no nullable row slot for embedded data. Removing or inserting embedded values would change that layout and invalidate compiled sibling access. Use an attachable type when presence must vary; use fields on an embedded type to represent states such as idle/running. Typed add/remove is a compile error; name-based add/remove returns catchable error.NotAttachableStorage.

An actor can mix the two kinds: .components = .{Spin, Health} permanently embeds Spin while keeping Health removable. Both can access sibling data through callback-scoped actor.get(C) / actor.require(C).

Swarm: bulk population simulation

Choose a swarm when many instances share one data shape and do not need actor hierarchy, component attachments, individual actor scripts, or actor events. Examples include background crowds, boids, and simple projectiles whose movement and collision policy you implement in population logic.

zig
const hi = @import("hikari_game");

pub const Motes = hi.defineSwarm(.{
    .name = "motes",
    .capacity = 10_000,
    .data = struct {
        x: f32 = 0,
        speed: f32 = 2,
        remaining: f32 = 3,

        pub fn integrate(store: *hi.SwarmStore(@This()), dt: f32) void {
            const x = store.column("x");
            const speed = store.columnConst("speed");
            const remaining = store.column("remaining");
            for (store.liveIndices()) |index| {
                x[index] += speed[index] * dt;
                remaining[index] -= dt;
                if (remaining[index] <= 0) store.retireAt(index);
            }
        }
    },
});

// Call after the population has been registered in the active World.
pub fn emitMote() !void {
    _ = hi.swarm(Motes).spawn(.{ .x = 1, .speed = 4 }) orelse
        return error.PopulationFull;
}

The engine runs integrate and then drains retirements. Retiring a row during integration keeps the current iteration valid; that slot becomes reusable after the drain. Duplicate retirement requests are coalesced, and retirement uses capacity reserved at construction. Keep generation-checked SwarmRef handles when referring to rows across ticks.

Strengths: field arrays let bulk logic touch only the fields it needs. liveIndices() visits the live population even when most capacity is unused. Rows avoid the full actor object and component dispatch machinery. This example allocates no memory during integration or retirement.

Limitations: capacity is required, nonzero, and bounded by the u32 index range. Unused capacity still consumes memory for field arrays and bookkeeping. spawn returns null when full. Rows must be non-owning values; owning resources and deinit belong in actor components. Borrowed strings and references must outlive their use. A swarm is not a drop-in replacement for actors with physics, animation, hierarchy, or messages. Rendering needs an explicit visual binding; the simulation above does not draw anything.

The schema is fixed because each field has a population-wide column, and bulk kernels compile against those columns. A per-row optional component would require another layout and dispatch model, losing the uniform field traversal this API is designed for. Mutate existing fields, use a flag for an optional state, or spawn into a separately defined population with the other schema. This is an explicit application operation, not an automatic migration preserving a SwarmRef. Use actors when individual objects need dynamic capabilities.

SwarmRef cannot be passed where ActorRef is required. Typed actor component access rejects swarm definitions at compile time, not as a catchable error. Passing a registered swarm name to name-based actor membership is rejected as NotAttachableStorage; an unregistered name returns UnknownComponent.

Use bulk integrate(store, dt) for field-oriented work. The alternative is update(self, hi.swarm_store.SwarmEntry(hi.SwarmStore(@This())), dt), which gathers a row into a temporary value and writes it back. Choose one hook, not both. Column slices cover capacity, including dead and uninitialized slots: iterate liveIndices() unless a full-capacity traversal explicitly checks liveness. Let the engine drain retirements after callbacks; do not drain while iterating borrowed live indices.

Decide by the required behavior

RequirementChoice
A reusable gameplay component, with no special constraintsAttachable
Add/remove a capability on individual actorsAttachable
Use the SDK's component replication optionAttachable
Permanent, small actor state with frequent direct accessEmbedded
A large uniform population that can avoid actor servicesSwarm
Thousands of objects that still need full actor servicesActor components; population size alone does not justify a swarm

Storage and scheduling are separate choices. .parallel = true selects parallel tick hooks for ordinary components; it is not a fourth storage kind and changes the available callback API. Neither attachable nor embedded pointers may escape a callback. See Gameplay API and Component lifecycle for those contracts.

The Runtime Components tutorial keeps an embedded controller while adding/removing user and system attachments. It also demonstrates a catchable name-based rejection without stopping the actor.

Inspecting runtime changes

Play's inspector stays read-only but reads live values for both embedded and attachable components. Attaching a user component exposes its generated public field schema immediately; runtime assignments in its callbacks update the shown values. Built-in additions behave the same way. Added components carry a Runtime pill, and removal drops the section. Hidden/transient fields stay hidden. Embedded membership remains fixed even though its public values are live; swarms have no actor inspector. See live actor inspection for notification costs and the future Play-only edit boundary.

PreviousGameplay APINext Build and packaging

Documentation follows the current engine checkout.

Snapshot cc148c75Source docs/guides/component-storage.md
On this pageCompare the three choicesAttachable: the general-purpose defaultEmbedded: permanent actor stateSwarm: bulk population simulationDecide by the required behaviorInspecting runtime changes Back to top