entity management

🕹️ ebitengineer

Entity Management & Architecture Reference (Object Pools & Light ECS) #

This module covers entity population architecture, memory slice pooling, deferred destruction buffers, and lightweight Entity Component Systems (ECS) in Go.


1. Slice Object Pooling vs. ECS Trade-Off #

Approach Best For Memory Overhead Complexity
Object Pool Slices ([]*Entity) Small to Medium Games (< 1,000 active entities) Minimal, contiguous heap Low
Light Component Bitmask (ECS) Bullet Hell / Swarms (1,000 - 50,000+ active entities) CPU L1/L2 Cache Friendly Medium

2. Slice Object Pool Pattern with Deferred Deletion #

To avoid GC allocation spikes during high-frequency entity creation and destruction (projectiles, enemies, particles), use a fixed-capacity slice pool and deferred deletion buffer:

go
type Entity struct {
	ID     int
	Active bool
	X, Y   float64
	VX, VY float64
	Type   string
}

type EntityManager struct {
	pool      []Entity
	nextID    int
	toDestroy []int
}

func NewEntityManager(capacity int) *EntityManager {
	return &EntityManager{
		pool:      make([]Entity, capacity),
		toDestroy: make([]int, 0, 64),
	}
}

func (em *EntityManager) Spawn(x, y float64, entityType string) *Entity {
	// Re-use inactive slot in pool
	for i := range em.pool {
		if !em.pool[i].Active {
			e := &em.pool[i]
			e.ID = em.nextID
			em.nextID++
			e.Active = true
			e.X = x
			e.Y = y
			e.VX = 0
			e.VY = 0
			e.Type = entityType
			return e
		}
	}
	return nil // Pool full
}

func (em *EntityManager) MarkForDestruction(id int) {
	em.toDestroy = append(em.toDestroy, id)
}

func (em *EntityManager) FlushDestructions() {
	if len(em.toDestroy) == 0 { return }

	for _, id := range em.toDestroy {
		for i := range em.pool {
			if em.pool[i].Active && em.pool[i].ID == id {
				em.pool[i].Active = false
				break
			}
		}
	}
	em.toDestroy = em.toDestroy[:0] // Reset buffer without allocation
}

3. Light Bitmask Component ECS Pattern #

When managing thousands of bullet hell projectiles or swarm units, store component data in parallel flat arrays indexed by Entity ID:

go
type ComponentMask uint32

const (
	CompTransform ComponentMask = 1 << iota
	CompVelocity
	CompRender
	CompCollider
)

type LightWorld struct {
	Masks      []ComponentMask
	X, Y       []float64 // Transform
	VX, VY     []float64 // Velocity
	Width, H   []float64 // Collider
	Active     []bool
	Capacity   int
}

func NewLightWorld(capacity int) *LightWorld {
	return &LightWorld{
		Masks:    make([]ComponentMask, capacity),
		X:        make([]float64, capacity),
		Y:        make([]float64, capacity),
		VX:       make([]float64, capacity),
		VY:       make([]float64, capacity),
		Width:    make([]float64, capacity),
		H:        make([]float64, capacity),
		Active:   make([]bool, capacity),
		Capacity: capacity,
	}
}

// System Update: CPU L1/L2 cache-friendly iteration
func (w *LightWorld) UpdateMovement(dt float64) {
	req := CompTransform | CompVelocity
	for i := 0; i < w.Capacity; i++ {
		if w.Active[i] && (w.Masks[i]&req) == req {
			w.X[i] += w.VX[i] * dt
			w.Y[i] += w.VY[i] * dt
		}
	}
}