sparze
glassesneo/sparzeExpert guidance for Sparze ECS apps including systems, queries, components, resources, events, and performance trade-offs.
SKILL.md
name: sparze description: Expert guidance for Sparze ECS apps including systems, queries, components, resources, events, and performance trade-offs.
Sparze ECS Skill
Expert guidance for building high-performance ECS applications with Sparze.
Core Concepts
World: ECS coordinator with compile-time component, resource, event, and group types.
const World = sparze.World(
.{ Position, Velocity, Health }, // Components
.{ DeltaTime, Score }, // Resources
.{ CollisionEvent }, // Events
.{ struct { Position, Velocity } }, // Groups
);
Entity: 32-bit handle [version:16 | index:16]. index selects dense slot; version invalidates stale handles. Use .index, .version, or getIndex() / getVersion(). Create via Entity.init(), serialize with toInt() / fromInt(u32).
Components: Data attached to entities. Tag components (empty structs) use 1 bit per entity.
Resources: Global singletons injected via Resource(T) / ResourceMut(T). Can implement init() / deinit(), or opt out with pub const auto_init = false (manual initialization required). POD resources are zero-initialized for compatibility.
Events: 1-frame delayed, double-buffered communication. Events written in frame N are read in frame N+1.
Docs: @docs/ARCHITECTURE.md, @docs/ENTITY_LIFECYCLE.md
System Functions
System functions use injected parameters and must use commands: anytype for world mutation. Return void or !void.
fn mySystem(
allocator: std.mem.Allocator,
movement: Group(struct { Position, Velocity }),
health: SingleQuery(Health),
delta: Resource(DeltaTime),
score: ResourceMut(Score),
reader: EventReader(CollisionEvent),
writer: EventWriter(DamageEvent),
commands: anytype,
) !void {
// Implementation
}
Commands API
Immediate (returns now):
commands.createEntity()try commands.createEntityWith(.{ Position{...}, Velocity{...} })commands.setResource(T, value)/commands.getResource(T)/commands.getResourcePtrMut(T)try commands.initResources(.{ ... })try commands.serializeToFile(path)/try commands.deserializeFromFile(path)
Deferred (applied at world.endFrame()):
try commands.addComponent(e, T, value)/try commands.removeComponent(e, T)try commands.addTag(e, T)/try commands.removeTag(e, T)try commands.destroyEntity(e)
Why deferred: avoids invalidating iterators or shifting dense arrays mid-iteration.
Docs: @docs/SYSTEM_PATTERNS.md
Queries
Decision guide:
- SingleQuery: single component, any frequency
- Query: multi-component, occasional use
- Group: multi-component, hot path
SingleQuery(Component)
fn healthSystem(query: SingleQuery(Health)) !void {
for (query.entities, query.components) |entity, *health| {
health.hp = @max(0, health.hp);
}
}
Query(struct { ... })
fn combatSystem(query: Query(struct { Position, Health, ?Shield })) !void {
for (query.entities) |entity| {
if (query.filter(entity)) {
const pos = query.getComponent(entity, Position);
const health = query.getComponentMut(entity, Health);
const shield = query.getOptional(entity, Shield);
_ = pos; _ = health; _ = shield;
}
}
}
Group(struct { ... })
const MovementGroup = struct { Position, Velocity };
const World = sparze.World(.{ Position, Velocity }, .{}, .{}, .{ MovementGroup });
fn movementSystem(group: Group(MovementGroup)) !void {
const positions = group.getMutArrayOf(Position);
const velocities = group.getArrayOf(Velocity);
for (positions, velocities) |*pos, vel| {
pos.x += vel.x;
pos.y += vel.y;
}
}
Free(Component): use in groups that do not own a component. Access via group.getComponent(entity, T) instead of array access.
Tag Queries
fn enemySystem(query: TagQuery(struct { Enemy, ?Boss, Exclude(Dead) })) !void {
for (query.entities) |entity| {
if (query.filter(entity)) {
_ = entity;
}
}
}
Docs: @docs/QUERY_PATTERNS.md
Query Modifiers
- Optional (?T): entity may or may not have component
- Exclude(T): filter out entities with component
- Free(T): group does not own component; access by lookup
System Organization
Principles:
- One system, one responsibility
- Writers run before readers
- Use events for loose coupling (1-frame delay)
- Group systems by domain (physics, combat, rendering)
Frame lifecycle:
world.beginFrame();
try world.runSystem(inputSystem);
try world.runSystem(physicsSystem);
try world.runSystem(renderSystem);
try world.endFrame();
Critical: Always call endFrame() to flush deferred commands.
Resources
Initialization options:
init()/deinit()for auto-lifecyclepub const auto_init = falsefor manual setup- POD resources are zero-initialized
Safety helpers:
isResourceInitialized(T)tryGetResource(T)initResources(.{ ... })for bulk init
Performance
Fastest to slowest:
- Group owned components (
getMutArrayOf) - SingleQuery
- Group free components (
getComponent) - Query
Best practices:
- Use Group for hot-path systems
- Use Query for ad-hoc/dynamic access
- Validate group ownership with
World.validateGroups()
Storage notes:
- Tag components use 1 bit per entity
- Reserve with
getSparseSetPtrMut(T).reserve(capacity) - Pagination: 4096 entities per page
Docs: @docs/PERFORMANCE.md, @docs/STORAGE_INTERNALS.md
Quick Reference
| Task | API |
|---|---|
| Create entity | commands.createEntity() |
| Add component | try commands.addComponent(e, T, value) |
| Query single | SingleQuery(Health) |
| Query multi | Query(struct { Position, Velocity }) |
| Fastest iteration | Group(struct { Position, Velocity }) |
| Optional component | Query(struct { Position, ?Sprite }) |
| Exclude entities | Query(struct { Enemy, Exclude(Dead) }) |
| Read resource | Resource(DeltaTime) |
| Write resource | ResourceMut(Score) |
| Read events | EventReader(CollisionEvent) |
| Write events | EventWriter(DamageEvent) |
| Tag iteration | SingleTag(Enemy) |
Documentation Index
- @docs/ARCHITECTURE.md
- @docs/QUERY_PATTERNS.md
- @docs/ENTITY_LIFECYCLE.md
- @docs/STORAGE_INTERNALS.md
- @docs/SYSTEM_PATTERNS.md
- @docs/PERFORMANCE.md