A high-performance, archetype-based Entity Component System (ECS) for Odin
Key Features:
- Archetype-based storage with bitmask queries
- Generational entity handles (prevents ABA problem)
- Contiguous component storage for cache-friendly iteration
- O(1) bit indexing via
count_trailing_zerosintrinsic - Query caching for repeated iteration patterns
column_uncheckedfor zero-overhead inner loops- Batch spawning with pre-allocated capacity
- Simple, data-oriented API
This is an Odin port of freecs, a Rust ECS library.
Copy freecs.odin into your project or import it as a package:
package main
import ecs "freecs-odin"
import "core:fmt"
Position :: struct {
x, y: f32,
}
Velocity :: struct {
x, y: f32,
}
main :: proc() {
world := ecs.create_world()
defer ecs.destroy_world(&world)
// Register component types (returns bitmask for queries)
POSITION := ecs.register(&world, Position)
VELOCITY := ecs.register(&world, Velocity)
// Spawn entities with components
entity := ecs.spawn(&world, Position{1, 2}, Velocity{3, 4})
// Get components
if pos := ecs.get(&world, entity, Position); pos != nil {
fmt.println("Position:", pos^)
}
// Set components
ecs.set(&world, entity, Position{10, 20})
// Check if entity has a component
if ecs.has(&world, entity, Position) {
fmt.println("Entity has position")
}
// Despawn entities
ecs.despawn(&world, entity)
}Systems iterate over archetypes and process entities with matching components:
update_positions :: proc(world: ^ecs.World, dt: f32) {
for &arch in world.archetypes {
// Skip archetypes that don't have required components
if arch.mask & (POSITION | VELOCITY) != (POSITION | VELOCITY) {
continue
}
// Get typed slices - pass bit for O(1) lookup (fast path)
positions := ecs.column(&arch, Position, POSITION)
velocities := ecs.column(&arch, Velocity, VELOCITY)
if positions == nil || velocities == nil { continue }
// Process all entities in this archetype
for i in 0..<len(arch.entities) {
positions[i].x += velocities[i].x * dt
positions[i].y += velocities[i].y * dt
}
}
}Two overloads are available:
// Fast path - O(1) via bit index array lookup
positions := ecs.column(&arch, Position, POSITION)
// Convenience path - O(n) linear scan by typeid
positions := ecs.column(&arch, Position)Use the bit-based version in performance-critical code.
Spawn many entities efficiently with pre-allocated capacity:
// Spawns 1000 entities with same components
entities := ecs.spawn_batch(&world, 1000, Position{0, 0}, Velocity{1, 1})For maximum performance, use cached queries and unchecked column access:
update_positions :: proc(world: ^ecs.World, dt: f32) {
move_mask := POSITION | VELOCITY
// Cached query - archetypes matching this mask are remembered
matching := ecs.get_matching_archetypes(world, move_mask)
for arch_idx in matching {
arch := &world.archetypes[arch_idx]
// Zero-overhead column access (no nil checks, no bounds checks)
positions := ecs.column_unchecked(arch, Position, POSITION)
velocities := ecs.column_unchecked(arch, Velocity, VELOCITY)
count := len(arch.entities)
// Unchecked inner loop for SIMD-friendly iteration
#no_bounds_check for i in 0..<count {
positions[i].x += velocities[i].x * dt
positions[i].y += velocities[i].y * dt
}
}
}world := ecs.create_world() // Create a new world
ecs.destroy_world(&world) // Clean up world resources
count := ecs.entity_count(&world) // Get total entity count// Register returns a bitmask for the component type
POSITION := ecs.register(&world, Position)
VELOCITY := ecs.register(&world, Velocity)
// Use masks for queries
MOVABLE := POSITION | VELOCITY// Spawn with any number of components
entity := ecs.spawn(&world, Position{0, 0}, Velocity{1, 1})
// Check if entity is alive
if ecs.is_alive(&world, entity) { ... }
// Despawn entity (slot reused with new generation)
ecs.despawn(&world, entity)// Get component (returns nil if not present)
pos := ecs.get(&world, entity, Position)
// Set component value
ecs.set(&world, entity, Position{10, 20})
// Check if entity has component
if ecs.has(&world, entity, Position) { ... }for &arch in world.archetypes {
// Check component mask
if arch.mask & REQUIRED_MASK != REQUIRED_MASK {
continue
}
// Get typed component slices
positions := ecs.column(&arch, Position)
velocities := ecs.column(&arch, Velocity)
// Iterate entities
for i in 0..<len(arch.entities) {
pos := positions[i]
vel := velocities[i]
// ...
}
}See examples/boids.odin for a complete boids flocking simulation using raylib:
just run
Controls:
- Space: Pause/unpause
- +/-: Add/remove 1000 boids
- Arrow keys: Adjust alignment/cohesion weights
- Left mouse: Attract boids
- Right mouse: Repel boids
just test
All 13 tests verify:
- Entity spawn/despawn
- Component get/set/has
- Generational indices
- Archetype management
- Query iteration
This project is licensed under the MIT License - see the LICENSE file for details.