Documentation
¶
Index ¶
- func ReadComponent[T any](reader ComponentReader, entityId EntityId) *T
- func RegisterComponent[T any](r *ComponentRegistry)
- type Archetype
- func (a *Archetype) Compact()
- func (a *Archetype) Delete(entityIndex uint32)
- func (a *Archetype) GetComponent(entityIndex uint32, compType reflect.Type) any
- func (a *Archetype) HasComponent(compType reflect.Type) bool
- func (a *Archetype) ID() uint32
- func (a *Archetype) Iter() func(yield func(EntityId) bool)
- func (a *Archetype) Spawn(components []any) uint32
- func (a *Archetype) Types() []reflect.Type
- type ArchetypeStats
- type Commands
- func (c *Commands) AddComponent(entity EntityId, component any)
- func (c *Commands) Defer(fn func())
- func (c *Commands) Delete(entity EntityId)
- func (c *Commands) Flush(storage *Storage)
- func (c *Commands) RemoveComponent(entity EntityId, compType reflect.Type)
- func (c *Commands) Spawn(components ...any)
- type ComponentReader
- type ComponentRegistry
- type EntityId
- type EntityRef
- type Query
- type Scheduler
- type SchedulerStats
- type Singleton
- type Storage
- func (s *Storage) AddComponent(id EntityId, component any) EntityId
- func (s *Storage) AddSingleton(component any) unsafe.Pointer
- func (s *Storage) CollectStats() *StorageStats
- func (s *Storage) CreateEntityRef(id EntityId) *EntityRef
- func (s *Storage) Delete(id EntityId)
- func (s *Storage) GetArchetype(components ...any) *Archetype
- func (s *Storage) GetArchetypeById(id uint32) *Archetype
- func (s *Storage) GetArchetypeByTypes(types []reflect.Type) *Archetype
- func (s *Storage) GetArchetypes() map[uint32]*Archetype
- func (s *Storage) GetComponent(id EntityId, compType reflect.Type) any
- func (s *Storage) GetSingleton(componentType reflect.Type) any
- func (s *Storage) HasComponent(id EntityId, compType reflect.Type) bool
- func (s *Storage) InvalidateEntityRef(ref *EntityRef) bool
- func (s *Storage) ReadSingleton(ptr any) bool
- func (s *Storage) RemoveComponent(id EntityId, compType reflect.Type) EntityId
- func (s *Storage) ResolveEntityRef(ref *EntityRef) (EntityId, bool)
- func (s *Storage) Spawn(components ...any) EntityId
- type StorageStats
- type System
- type SystemStats
- type UpdateFrame
- type View
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func ReadComponent ¶
func ReadComponent[T any](reader ComponentReader, entityId EntityId) *T
func RegisterComponent ¶
func RegisterComponent[T any](r *ComponentRegistry)
RegisterComponent registers a new component type with the given registry. This must be called for each component type before it can be used.
Types ¶
type Archetype ¶
type Archetype struct {
// contains filtered or unexported fields
}
Archetype represents a unique combination of component types
func NewArchetype ¶
func NewArchetype(id uint32, types []reflect.Type, registry *ComponentRegistry) *Archetype
NewArchetype creates a new archetype with the given ID and sorted component types
func (*Archetype) Compact ¶
func (a *Archetype) Compact()
Compact reorganizes all component storage to eliminate empty slots and reduce fragmentation EntityRefs remain valid and are automatically updated to point to the new indices
Example ¶
ExampleArchetype_Compact demonstrates archetype compaction to reclaim memory. When entities are deleted, they can leave gaps in the archetype's component arrays. Compaction moves entities to fill these gaps, improving memory locality and iteration performance. Entity IDs are updated during compaction, but EntityRefs remain valid and automatically track the new locations.
registry := ecs.NewComponentRegistry()
ecs.RegisterComponent[Position](registry)
ecs.RegisterComponent[Health](registry)
storage := ecs.NewStorage(registry)
entities := make([]ecs.EntityId, 5)
for i := range 5 {
entities[i] = storage.Spawn(
Position{X: float32(i * 10), Y: 0},
Health{Current: 100, Max: 100},
)
}
storage.Delete(entities[1])
storage.Delete(entities[3])
view := ecs.NewView[struct {
*Position
*Health
}](storage)
fmt.Println("Before compaction:")
count := 0
for range view.Iter() {
count++
}
fmt.Printf("Entities: %d\n", count)
archetype := storage.GetArchetype(entities[0].ArchetypeId())
if archetype != nil {
archetype.Compact()
}
fmt.Println("\nAfter compaction:")
count = 0
for item := range view.Iter() {
fmt.Printf("Position: (%.0f, %.0f)\n", item.Position.X, item.Position.Y)
count++
}
fmt.Printf("Entities: %d\n", count)
Output: Before compaction: Entities: 3 After compaction: Position: (0, 0) Position: (20, 0) Position: (40, 0) Entities: 3
func (*Archetype) Delete ¶
Delete marks an entity's components as deleted Indices remain stable - the slot is simply marked as empty
func (*Archetype) GetComponent ¶
GetComponent returns the component of the given type for the entity at entityIndex The entityIndex is the storage position directly
func (*Archetype) HasComponent ¶
HasComponent checks if this archetype has the given component type
type ArchetypeStats ¶ added in v0.0.6
ArchetypeStats provides statistics for a single archetype.
type Commands ¶
type Commands struct {
// contains filtered or unexported fields
}
Commands provides a buffer for deferred ECS operations that are executed at the end of a frame. This prevents structural changes to the ECS storage during system execution.
Example ¶
ExampleCommands demonstrates using command buffers to defer entity mutations. Commands are essential when modifying entities during iteration, as directly spawning or deleting entities while iterating can invalidate iterators and cause crashes. The Scheduler automatically flushes commands at the end of each frame, applying all deferred operations safely.
registry := ecs.NewComponentRegistry()
ecs.RegisterComponent[Position](registry)
ecs.RegisterComponent[Velocity](registry)
ecs.RegisterComponent[Health](registry)
storage := ecs.NewStorage(registry)
storage.Spawn(Position{X: 0, Y: 0}, Health{Current: 0, Max: 100})
storage.Spawn(Position{X: 10, Y: 10}, Health{Current: 50, Max: 100})
storage.Spawn(Position{X: 20, Y: 20}, Health{Current: 100, Max: 100})
scheduler := ecs.NewScheduler(storage)
scheduler.Register(&CleanupSystem{})
scheduler.Once(1.0)
view := ecs.NewView[struct{ *Position }](storage)
remaining := 0
for range view.Iter() {
remaining++
}
fmt.Printf("Remaining entities: %d\n", remaining)
Output: Queued 1 dead entities for deletion Remaining entities: 2
Example (Spawning) ¶
ExampleCommands_spawning shows using commands to spawn entities during iteration. This is common for systems that need to create projectiles, particles, or other entities based on existing entity state. Commands ensure spawning happens after iteration completes, preventing iterator invalidation.
registry := ecs.NewComponentRegistry()
ecs.RegisterComponent[Position](registry)
ecs.RegisterComponent[Velocity](registry)
ecs.RegisterComponent[ShootTimer](registry)
storage := ecs.NewStorage(registry)
storage.Spawn(
Position{X: 10, Y: 10},
Velocity{DX: 1, DY: 0},
ShootTimer{TimeUntilShot: 0},
)
storage.Spawn(
Position{X: 20, Y: 20},
Velocity{DX: 0, DY: 1},
ShootTimer{TimeUntilShot: 5},
)
scheduler := ecs.NewScheduler(storage)
scheduler.Register(&ShootingSystem{})
scheduler.Once(1.0)
view := ecs.NewView[struct {
*Position
*Velocity
}](storage)
count := 0
for range view.Iter() {
count++
}
fmt.Printf("Total entities with velocity: %d\n", count)
Output: Spawned projectile at (10, 10) Total entities with velocity: 3
func (*Commands) AddComponent ¶
AddComponent queues a component addition operation.
func (*Commands) Defer ¶ added in v0.0.6
func (c *Commands) Defer(fn func())
Defer queues a function execution operation.
func (*Commands) Flush ¶ added in v0.0.2
Flush flushes all commands to the provided storage, reseting the buffer state
func (*Commands) RemoveComponent ¶
RemoveComponent queues a component removal operation.
type ComponentRegistry ¶
type ComponentRegistry struct {
// contains filtered or unexported fields
}
ComponentRegistry manages component type registration for an ECS instance. Each Storage instance has its own ComponentRegistry, allowing multiple independent ECS systems to coexist without interference.
func NewComponentRegistry ¶
func NewComponentRegistry() *ComponentRegistry
NewComponentRegistry creates a new component registry.
type EntityId ¶
type EntityId uint64
EntityId encodes both the archetype ID (upper 32 bits) and the entity index (lower 32 bits)
func NewEntityId ¶
NewEntityId creates an EntityId from an archetype ID and entity index
func (EntityId) ArchetypeId ¶
ArchetypeId extracts the archetype ID from the entity ID
type EntityRef ¶
EntityRef is a stable reference to an entity
Example ¶
ExampleEntityRef demonstrates using EntityRefs to maintain stable references to entities. Unlike EntityIds which can become invalid when entities move between archetypes or are deleted, EntityRefs remain valid and automatically track entities as they move. This makes them ideal for storing relationships between entities in components.
registry := ecs.NewComponentRegistry()
ecs.RegisterComponent[Position](registry)
ecs.RegisterComponent[Velocity](registry)
storage := ecs.NewStorage(registry)
target := storage.Spawn(Position{X: 100, Y: 100})
targetRef := storage.CreateEntityRef(target)
targetId, ok := storage.ResolveEntityRef(targetRef)
if ok {
targetPos := ecs.ReadComponent[Position](storage, targetId)
fmt.Printf("Target at (%.0f, %.0f)\n", targetPos.X, targetPos.Y)
}
target = storage.AddComponent(target, Velocity{DX: 0, DY: 0})
targetId, ok = storage.ResolveEntityRef(targetRef)
if ok {
targetPos := ecs.ReadComponent[Position](storage, targetId)
fmt.Printf("Target moved archetypes, still at (%.0f, %.0f)\n", targetPos.X, targetPos.Y)
}
storage.Delete(target)
_, ok = storage.ResolveEntityRef(targetRef)
fmt.Printf("Target deleted, ref valid: %v\n", ok)
Output: Target at (100, 100) Target moved archetypes, still at (100, 100) Target deleted, ref valid: false
Example (RelationshipComponent) ¶
ExampleEntityRef_relationshipComponent shows using EntityRefs within components to create relationships between entities. This pattern is common for AI targets, parent-child hierarchies, or any other entity-to-entity relationships that need to survive archetype changes and entity deletions.
registry := ecs.NewComponentRegistry()
ecs.RegisterComponent[Position](registry)
ecs.RegisterComponent[FollowerAI](registry)
storage := ecs.NewStorage(registry)
leader := storage.Spawn(Position{X: 50, Y: 50})
leaderRef := storage.CreateEntityRef(leader)
storage.Spawn(
Position{X: 40, Y: 40},
FollowerAI{Target: leaderRef},
)
storage.Spawn(
Position{X: 60, Y: 40},
FollowerAI{Target: leaderRef},
)
fmt.Println("Followers tracking leader:")
view := ecs.NewView[struct {
*Position
*FollowerAI
}](storage)
for item := range view.Iter() {
if targetId, ok := storage.ResolveEntityRef(item.FollowerAI.Target); ok {
targetPos := ecs.ReadComponent[Position](storage, targetId)
fmt.Printf("Follower at (%.0f, %.0f) following target at (%.0f, %.0f)\n",
item.Position.X, item.Position.Y, targetPos.X, targetPos.Y)
}
}
storage.Delete(leader)
fmt.Println("\nAfter leader deleted:")
for item := range view.Iter() {
if _, ok := storage.ResolveEntityRef(item.FollowerAI.Target); !ok {
fmt.Printf("Follower at (%.0f, %.0f) lost its target\n",
item.Position.X, item.Position.Y)
}
}
Output: Followers tracking leader: Follower at (40, 40) following target at (50, 50) Follower at (60, 40) following target at (50, 50) After leader deleted: Follower at (40, 40) lost its target Follower at (60, 40) lost its target
type Query ¶
type Query[T any] struct { // contains filtered or unexported fields }
Query wraps a View with caching optimizations for repeated iteration. Queries cache matching archetypes to avoid re-calculating this on every run.
Example ¶
ExampleQuery demonstrates using queries for high-performance iteration. Unlike Views, Queries cache the list of matching archetypes, which provides a significant performance boost for repeated iteration, as the set of matching archetypes doesn't need to be re-calculated each time.
registry := ecs.NewComponentRegistry()
ecs.RegisterComponent[Position](registry)
ecs.RegisterComponent[Velocity](registry)
ecs.RegisterComponent[Health](registry)
storage := ecs.NewStorage(registry)
storage.Spawn(Position{X: 0, Y: 0}, Velocity{DX: 1, DY: 0})
storage.Spawn(Position{X: 10, Y: 10}, Velocity{DX: 0, DY: 1}, Health{Current: 100, Max: 100})
storage.Spawn(Position{X: 20, Y: 20}, Velocity{DX: -1, DY: -1})
query := ecs.NewQuery[struct {
*Position
*Velocity
}](storage)
type result struct {
x, y, newX, newY float32
}
results := make([]result, 0)
for item := range query.Iter() {
newX := item.Position.X + item.Velocity.DX
newY := item.Position.Y + item.Velocity.DY
results = append(results, result{item.Position.X, item.Position.Y, newX, newY})
}
for i := 0; i < len(results); i++ {
for j := i + 1; j < len(results); j++ {
if results[i].x > results[j].x {
results[i], results[j] = results[j], results[i]
}
}
}
fmt.Println("Moving entities:")
for _, r := range results {
fmt.Printf("Position (%.0f, %.0f) -> (%.0f, %.0f)\n", r.x, r.y, r.newX, r.newY)
}
Output: Moving entities: Position (0, 0) -> (1, 0) Position (10, 10) -> (10, 11) Position (20, 20) -> (19, 19)
type Scheduler ¶
type Scheduler struct {
// contains filtered or unexported fields
}
Scheduler manages and executes systems in order.
Example ¶
ExampleScheduler demonstrates building a game loop with multiple systems. The Scheduler manages system execution order, automatically initializes Query fields, executes queries each frame, and flushes command buffers. Systems are executed in registration order, and all queries are synchronized before any system runs.
package main
import (
"fmt"
"github.com/plus3/ooftn/ecs"
)
type Transform struct {
X, Y float32
}
type Speed struct {
DX, DY float32
}
type Hitpoints struct {
Current, Max int
}
type PhysicsSystem struct {
Entities ecs.Query[struct {
*Transform
*Speed
}]
}
func (s *PhysicsSystem) Execute(frame *ecs.UpdateFrame) {
for entity := range s.Entities.Iter() {
entity.Transform.X += entity.Speed.DX * float32(frame.DeltaTime)
entity.Transform.Y += entity.Speed.DY * float32(frame.DeltaTime)
}
}
type HealingSystem struct {
Entities ecs.Query[struct{ *Hitpoints }]
RegenRate float32
}
func (s *HealingSystem) Execute(frame *ecs.UpdateFrame) {
for entity := range s.Entities.Iter() {
if entity.Hitpoints.Current < entity.Hitpoints.Max {
entity.Hitpoints.Current += int(s.RegenRate * float32(frame.DeltaTime))
if entity.Hitpoints.Current > entity.Hitpoints.Max {
entity.Hitpoints.Current = entity.Hitpoints.Max
}
}
}
}
func main() {
registry := ecs.NewComponentRegistry()
ecs.RegisterComponent[Transform](registry)
ecs.RegisterComponent[Speed](registry)
ecs.RegisterComponent[Hitpoints](registry)
storage := ecs.NewStorage(registry)
storage.Spawn(
Transform{X: 0, Y: 0},
Speed{DX: 10, DY: 5},
Hitpoints{Current: 80, Max: 100},
)
storage.Spawn(
Transform{X: 100, Y: 100},
Speed{DX: -5, DY: -5},
Hitpoints{Current: 50, Max: 100},
)
scheduler := ecs.NewScheduler(storage)
scheduler.Register(&PhysicsSystem{})
scheduler.Register(&HealingSystem{RegenRate: 10})
scheduler.Once(1.0)
view := ecs.NewView[struct {
*Transform
*Hitpoints
}](storage)
fmt.Println("After one frame:")
for item := range view.Iter() {
fmt.Printf("Position: (%.0f, %.0f), Health: %d/%d\n",
item.Transform.X, item.Transform.Y,
item.Hitpoints.Current, item.Hitpoints.Max)
}
}
Output: After one frame: Position: (10, 5), Health: 90/100 Position: (95, 95), Health: 60/100
Example (WithSingletons) ¶
ExampleScheduler_withSingletons demonstrates using singleton components in systems. Singleton fields are automatically initialized by the Scheduler, just like Query fields. This provides efficient access to global state without the iteration overhead of queries.
package main
import (
"fmt"
"github.com/plus3/ooftn/ecs"
)
type Transform struct {
X, Y float32
}
type GameTime struct {
TotalFrames int
TotalTime float64
}
type TimeTracker struct {
Entities ecs.Query[struct{ *Transform }]
GameTime ecs.Singleton[GameTime]
}
func (s *TimeTracker) Execute(frame *ecs.UpdateFrame) {
gameTime := s.GameTime.Get()
gameTime.TotalFrames++
gameTime.TotalTime += frame.DeltaTime
}
type ScoreTracker struct {
Points int
}
type ScoreSystem struct {
Entities ecs.Query[struct{ *Transform }]
Score ecs.Singleton[ScoreTracker]
}
func (s *ScoreSystem) Execute(frame *ecs.UpdateFrame) {
count := 0
for range s.Entities.Iter() {
count++
}
s.Score.Get().Points += count * 10
}
func main() {
registry := ecs.NewComponentRegistry()
ecs.RegisterComponent[Transform](registry)
storage := ecs.NewStorage(registry)
// Initialize singletons
ecs.NewSingleton[GameTime](storage, GameTime{TotalFrames: 0, TotalTime: 0})
ecs.NewSingleton[ScoreTracker](storage, ScoreTracker{Points: 0})
// Spawn entities
storage.Spawn(Transform{X: 0, Y: 0})
storage.Spawn(Transform{X: 10, Y: 10})
storage.Spawn(Transform{X: 20, Y: 20})
// Create scheduler with systems that use singletons
scheduler := ecs.NewScheduler(storage)
scheduler.Register(&TimeTracker{})
scheduler.Register(&ScoreSystem{})
// Run for 3 frames
scheduler.Once(0.016)
scheduler.Once(0.016)
scheduler.Once(0.016)
// Check singleton values
var gameTime *GameTime
storage.ReadSingleton(&gameTime)
fmt.Printf("Frames: %d, Time: %.3f\n", gameTime.TotalFrames, gameTime.TotalTime)
var score *ScoreTracker
storage.ReadSingleton(&score)
fmt.Printf("Score: %d points\n", score.Points)
}
Output: Frames: 3, Time: 0.048 Score: 90 points
func NewScheduler ¶
NewScheduler creates a new scheduler for the given storage.
func (*Scheduler) GetStats ¶ added in v0.0.6
func (s *Scheduler) GetStats() *SchedulerStats
GetStats returns statistics about system execution.
func (*Scheduler) Register ¶
Register adds a system to the scheduler and initializes its Query fields.
func (*Scheduler) Run ¶
Run executes all systems repeatedly at the given interval until the context is cancelled.
Example ¶
ExampleScheduler_Run demonstrates running a continuous game loop. The Run method blocks and executes all systems at a fixed interval until the context is cancelled. This is the typical pattern for a real-time game or simulation.
package main
import (
"context"
"fmt"
"time"
"github.com/plus3/ooftn/ecs"
)
type Transform struct {
X, Y float32
}
type Speed struct {
DX, DY float32
}
type PhysicsSystem struct {
Entities ecs.Query[struct {
*Transform
*Speed
}]
}
func (s *PhysicsSystem) Execute(frame *ecs.UpdateFrame) {
for entity := range s.Entities.Iter() {
entity.Transform.X += entity.Speed.DX * float32(frame.DeltaTime)
entity.Transform.Y += entity.Speed.DY * float32(frame.DeltaTime)
}
}
func main() {
registry := ecs.NewComponentRegistry()
ecs.RegisterComponent[Transform](registry)
ecs.RegisterComponent[Speed](registry)
storage := ecs.NewStorage(registry)
storage.Spawn(Transform{X: 0, Y: 0}, Speed{DX: 1, DY: 1})
scheduler := ecs.NewScheduler(storage)
scheduler.Register(&PhysicsSystem{})
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
scheduler.Run(ctx, 16*time.Millisecond)
fmt.Println("Scheduler stopped")
}
Output: Scheduler stopped
type SchedulerStats ¶ added in v0.0.6
type SchedulerStats struct {
SystemCount int
TotalExecutions int64
Systems []SystemStats
}
SchedulerStats provides statistics about scheduler execution.
type Singleton ¶ added in v0.0.4
type Singleton[T any] struct { // contains filtered or unexported fields }
Singleton provides efficient access to a single component instance that is not associated with any entity. Use this for global game state, configuration, or other singleton data.
Example (MultipleReferences) ¶
ExampleSingleton_multipleReferences shows that multiple Singleton instances reference the same underlying data.
package main
import (
"fmt"
"github.com/plus3/ooftn/ecs"
)
type GameScore struct {
Points int
Level int
}
func main() {
registry := ecs.NewComponentRegistry()
storage := ecs.NewStorage(registry)
// Create first singleton reference
score1 := ecs.NewSingleton[GameScore](storage, GameScore{Points: 0, Level: 1})
fmt.Printf("Score1: %d points, Level %d\n", score1.Get().Points, score1.Get().Level)
// Modify via first reference
score1.Get().Points = 100
score1.Get().Level = 2
// Create second reference to same singleton
score2 := ecs.NewSingleton[GameScore](storage)
fmt.Printf("Score2: %d points, Level %d\n", score2.Get().Points, score2.Get().Level)
// Both references point to the same data
score2.Get().Points = 250
fmt.Printf("Score1 after Score2 update: %d points\n", score1.Get().Points)
}
Output: Score1: 0 points, Level 1 Score2: 100 points, Level 2 Score1 after Score2 update: 250 points
func NewSingleton ¶ added in v0.0.4
NewSingleton creates a new Singleton accessor for the given storage. If initializer is provided and the singleton doesn't exist in storage, it will be created with the initializer value. Otherwise, a zero value is used. This guarantees the singleton exists in storage after the call.
Example ¶
ExampleNewSingleton demonstrates creating and accessing singleton components. Singletons are global components not associated with any entity, useful for game state, configuration, or other application-wide data.
package main
import (
"fmt"
"github.com/plus3/ooftn/ecs"
)
type GameConfig struct {
MaxPlayers int
Difficulty string
}
func main() {
registry := ecs.NewComponentRegistry()
storage := ecs.NewStorage(registry)
// Create singleton with initializer
config := ecs.NewSingleton[GameConfig](storage, GameConfig{
MaxPlayers: 4,
Difficulty: "Normal",
})
fmt.Printf("Config: %d players, %s difficulty\n", config.Get().MaxPlayers, config.Get().Difficulty)
// Modify the singleton
config.Get().Difficulty = "Hard"
fmt.Printf("Updated difficulty: %s\n", config.Get().Difficulty)
// Create another reference to the same singleton
sameConfig := ecs.NewSingleton[GameConfig](storage)
fmt.Printf("Same config: %s difficulty\n", sameConfig.Get().Difficulty)
}
Output: Config: 4 players, Normal difficulty Updated difficulty: Hard Same config: Hard difficulty
func (*Singleton[T]) Get ¶ added in v0.0.4
func (s *Singleton[T]) Get() *T
Get returns a pointer to the singleton component. The singleton is guaranteed to exist (it's created automatically if needed).
type Storage ¶
type Storage struct {
// contains filtered or unexported fields
}
Storage is the main ECS storage interface
Example ¶
ExampleStorage demonstrates the basic API for managing entities and components. Storage is the core container for all entities and their component data. Components are organized by archetype - entities with the same component types share the same archetype for efficient memory layout and iteration.
registry := ecs.NewComponentRegistry()
ecs.RegisterComponent[Position](registry)
ecs.RegisterComponent[Velocity](registry)
ecs.RegisterComponent[Health](registry)
storage := ecs.NewStorage(registry)
player := storage.Spawn(
Position{X: 10, Y: 20},
Velocity{DX: 1, DY: 0},
Health{Current: 100, Max: 100},
)
pos := ecs.ReadComponent[Position](storage, player)
fmt.Printf("Player spawned at (%.0f, %.0f)\n", pos.X, pos.Y)
pos.X = 15
pos.Y = 25
fmt.Printf("Player moved to (%.0f, %.0f)\n", pos.X, pos.Y)
storage.Delete(player)
fmt.Println("Player deleted")
Output: Player spawned at (10, 20) Player moved to (15, 25) Player deleted
Example (AddRemoveComponents) ¶
ExampleStorage_addRemoveComponents shows how entity archetypes change when components are added or removed. When an entity's components change, it moves to a different archetype that matches its new component set.
registry := ecs.NewComponentRegistry()
ecs.RegisterComponent[Position](registry)
ecs.RegisterComponent[Velocity](registry)
ecs.RegisterComponent[Health](registry)
storage := ecs.NewStorage(registry)
entity := storage.Spawn(Position{X: 0, Y: 0})
hasVel := storage.HasComponent(entity, reflect.TypeOf(Velocity{}))
fmt.Printf("Has velocity: %v\n", hasVel)
entity = storage.AddComponent(entity, Velocity{DX: 5, DY: 3})
vel := ecs.ReadComponent[Velocity](storage, entity)
fmt.Printf("Has velocity: %v (%.0f, %.0f)\n", vel != nil, vel.DX, vel.DY)
entity = storage.AddComponent(entity, Health{Current: 50, Max: 50})
health := ecs.ReadComponent[Health](storage, entity)
fmt.Printf("Has health: %v (%d/%d)\n", health != nil, health.Current, health.Max)
entity = storage.RemoveComponent(entity, reflect.TypeOf(Velocity{}))
hasVel = storage.HasComponent(entity, reflect.TypeOf(Velocity{}))
fmt.Printf("Has velocity: %v\n", hasVel)
Output: Has velocity: false Has velocity: true (5, 3) Has health: true (50/50) Has velocity: false
func NewStorage ¶
func NewStorage(registry *ComponentRegistry) *Storage
NewStorage creates a new ECS storage system with the given component registry
func (*Storage) AddSingleton ¶ added in v0.0.4
AddSingleton adds or updates a singleton component in storage. Singleton components are not associated with any entity and provide efficient global state access. Returns a pointer to the stored component.
func (*Storage) CollectStats ¶ added in v0.0.6
func (s *Storage) CollectStats() *StorageStats
CollectStats gathers statistics about the current storage state.
func (*Storage) CreateEntityRef ¶
func (*Storage) GetArchetype ¶
GetArchetype returns an archetype storage (if one exists)
func (*Storage) GetArchetypeById ¶ added in v0.0.6
GetArchetypeById returns an archetype by its ID
func (*Storage) GetArchetypeByTypes ¶
GetArchetypeByTypes returns an archetype storage (if one exists) based on reflect.Type
func (*Storage) GetArchetypes ¶ added in v0.0.6
GetArchetypes returns all archetypes in storage
func (*Storage) GetComponent ¶
GetComponent returns the component for the given entity ID and component type
func (*Storage) GetSingleton ¶ added in v0.0.4
GetSingleton returns a pointer to a singleton component, or nil if it doesn't exist.
func (*Storage) HasComponent ¶
HasComponent checks if an entity has a specific component type
func (*Storage) InvalidateEntityRef ¶
func (*Storage) ReadSingleton ¶ added in v0.0.4
ReadSingleton reads a singleton component into the provided pointer. The ptr parameter must be a pointer to a pointer (e.g., &gameState where gameState is *GameState). Returns true if the singleton exists and was successfully read, false otherwise.
Example usage:
var gameState *GameState
if storage.ReadSingleton(&gameState) {
// use gameState here
}
Example ¶
ExampleStorage_ReadSingleton demonstrates the ReadSingleton API for convenient singleton access outside of systems.
package main
import (
"fmt"
"github.com/plus3/ooftn/ecs"
)
type GameConfig struct {
MaxPlayers int
Difficulty string
}
type GameScore struct {
Points int
Level int
}
func main() {
registry := ecs.NewComponentRegistry()
storage := ecs.NewStorage(registry)
// Create singleton
ecs.NewSingleton[GameConfig](storage, GameConfig{
MaxPlayers: 8,
Difficulty: "Expert",
})
// Read singleton using pointer pattern
var config *GameConfig
if storage.ReadSingleton(&config) {
fmt.Printf("Game: %d players, %s mode\n", config.MaxPlayers, config.Difficulty)
}
// Try reading non-existent singleton
var score *GameScore
if storage.ReadSingleton(&score) {
fmt.Println("Score exists")
} else {
fmt.Println("Score not found")
}
}
Output: Game: 8 players, Expert mode Score not found
func (*Storage) RemoveComponent ¶
func (*Storage) ResolveEntityRef ¶
type StorageStats ¶ added in v0.0.6
type StorageStats struct {
ArchetypeCount int
TotalEntityCount int
ArchetypeBreakdown []ArchetypeStats
SingletonCount int
SingletonTypes []string
TotalStorageSlots int
EmptyStorageSlots int
StorageUtilization float32
}
StorageStats provides statistics about ECS storage state.
type System ¶
type System interface {
Execute(frame *UpdateFrame)
}
System represents a behavior that operates on entities with specific components. User-defined systems should implement this interface and can include Query fields for accessing entities, as well as custom state fields that persist between frames.
type SystemStats ¶ added in v0.0.6
type SystemStats struct {
Name string
ExecutionCount int64
MinDuration time.Duration
MaxDuration time.Duration
AvgDuration time.Duration
LastDuration time.Duration
TotalDuration time.Duration
}
SystemStats provides execution statistics for a single system.
type UpdateFrame ¶
type View ¶
type View[T any] struct { // contains filtered or unexported fields }
View represents a query for entities with a specific combination of components The type T should be a struct with embedded pointer fields for each component type Named fields can be marked as optional using the `ecs:"optional"` struct tag
Example ¶
ExampleView demonstrates using Views for flexible entity queries and spawning. Views provide a way to query entities with specific component combinations and optionally spawn new entities. Unlike Queries, Views don't require a Scheduler and perform iteration on-demand, making them ideal for one-off queries, tools, or situations where you need to query entities outside of a system.
registry := ecs.NewComponentRegistry()
ecs.RegisterComponent[Position](registry)
ecs.RegisterComponent[Velocity](registry)
ecs.RegisterComponent[Health](registry)
storage := ecs.NewStorage(registry)
player := storage.Spawn(
Position{X: 10, Y: 20},
Velocity{DX: 1, DY: 0},
Health{Current: 100, Max: 100},
)
view := ecs.NewView[struct {
*Position
*Velocity
}](storage)
if item := view.Get(player); item != nil {
fmt.Printf("Player at (%.0f, %.0f) moving (%.0f, %.0f)\n",
item.Position.X, item.Position.Y, item.Velocity.DX, item.Velocity.DY)
}
Output: Player at (10, 20) moving (1, 0)
Example (Optional) ¶
ExampleView_optional demonstrates using optional components in views. Optional components allow a single view to match entities that may or may not have certain components. This is useful for systems that need to handle both cases, like rendering entities with optional visual effects or processing entities with optional AI components.
registry := ecs.NewComponentRegistry()
ecs.RegisterComponent[Position](registry)
ecs.RegisterComponent[Velocity](registry)
ecs.RegisterComponent[Health](registry)
storage := ecs.NewStorage(registry)
storage.Spawn(Position{X: 10, Y: 10}, Velocity{DX: 1, DY: 0}, Health{Current: 50, Max: 100})
storage.Spawn(Position{X: 20, Y: 20}, Velocity{DX: 0, DY: 1}, Health{Current: 75, Max: 100})
storage.Spawn(Position{X: 30, Y: 30}, Velocity{DX: -1, DY: 0})
view := ecs.NewView[struct {
Position *Position
Velocity *Velocity
Health *Health `ecs:"optional"`
}](storage)
fmt.Println("All moving entities:")
for item := range view.Iter() {
if item.Health != nil {
fmt.Printf("Entity at (%.0f, %.0f) with health %d/%d\n",
item.Position.X, item.Position.Y, item.Health.Current, item.Health.Max)
} else {
fmt.Printf("Invulnerable entity at (%.0f, %.0f)\n", item.Position.X, item.Position.Y)
}
}
Output: All moving entities: Entity at (10, 10) with health 50/100 Entity at (20, 20) with health 75/100 Invulnerable entity at (30, 30)
func NewView ¶
NewView creates a new view for the given struct type The struct T should have embedded or named fields that are pointers to component types Embedded fields are always required Named fields can be marked as optional using the `ecs:"optional"` struct tag
func (*View[T]) Fill ¶
Fill populates the provided struct pointer with component data for the given entity Returns false if the entity is missing any required components Optional components are set to nil if not present
func (*View[T]) Get ¶
Get returns a populated view struct for the given entity, or nil if the entity doesn't have all the required components
func (*View[T]) GetRef ¶
GetRef returns a populated view struct for the given entity ref, or nil if invalid
func (*View[T]) Iter ¶
Iter returns an iterator over all entities that have all the required components for this view The iterator yields T where T is the populated view struct Optional components are set to nil if not present
Example ¶
ExampleView_Iter shows iterating over all entities matching a view. Views automatically match entities across all archetypes that contain the required components, making it easy to process entities without worrying about their specific archetype layout.
By including an EntityId field in the view struct, you can access each entity's ID during iteration. This is useful for storing references, deleting entities, or performing operations that require the entity ID.
registry := ecs.NewComponentRegistry()
ecs.RegisterComponent[Position](registry)
ecs.RegisterComponent[Velocity](registry)
ecs.RegisterComponent[Health](registry)
storage := ecs.NewStorage(registry)
storage.Spawn(Position{X: 0, Y: 0}, Velocity{DX: 1, DY: 0})
storage.Spawn(Position{X: 10, Y: 10}, Velocity{DX: 0, DY: 1}, Health{Current: 50, Max: 100})
storage.Spawn(Position{X: 20, Y: 20}, Velocity{DX: -1, DY: -1})
storage.Spawn(Position{X: 100, Y: 100})
view := ecs.NewView[struct {
Id ecs.EntityId
*Position
*Velocity
}](storage)
type result struct {
x, y float32
}
results := make([]result, 0)
entityIds := make([]ecs.EntityId, 0)
for item := range view.Iter() {
item.Position.X += item.Velocity.DX
item.Position.Y += item.Velocity.DY
results = append(results, result{item.Position.X, item.Position.Y})
entityIds = append(entityIds, item.Id)
}
for i := 0; i < len(results); i++ {
for j := i + 1; j < len(results); j++ {
if results[i].x > results[j].x {
results[i], results[j] = results[j], results[i]
}
}
}
fmt.Println("Entities with position and velocity:")
for _, r := range results {
fmt.Printf("New position: (%.0f, %.0f)\n", r.x, r.y)
}
fmt.Printf("Total entities with IDs: %d\n", len(entityIds))
Output: Entities with position and velocity: New position: (1, 0) New position: (10, 11) New position: (19, 19) Total entities with IDs: 3
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package debugui provides immediate-mode GUI integration for ECS applications using Dear ImGui.
|
Package debugui provides immediate-mode GUI integration for ECS applications using Dear ImGui. |
|
ebiten
Package ebiten provides Dear ImGui backend integration for the Ebiten game engine.
|
Package ebiten provides Dear ImGui backend integration for the Ebiten game engine. |