Skip to content

Instantly share code, notes, and snippets.

@corporatepiyush
Last active September 10, 2025 15:51
Show Gist options
  • Save corporatepiyush/a6cd96c659f655b7c5ce1b68d31aadb1 to your computer and use it in GitHub Desktop.
Save corporatepiyush/a6cd96c659f655b7c5ce1b68d31aadb1 to your computer and use it in GitHub Desktop.
Entity Component System
package main
import "fmt"
// --- CORE TYPES ---
// Entity is a unique identifier.
type Entity uint32
// ComponentType is the identifier for a component type, used for bitmasking.
type ComponentType uint64
// Using iota for compile-time generation of unique component IDs.
// This is the key to avoiding reflection.
const (
PositionComponentType ComponentType = 1 << iota
VelocityComponentType
RenderableComponentType
HealthComponentType
)
// --- COMPONENTS ---
// Components remain pure data structs.
type PositionComponent struct {
X, Y float64
}
type VelocityComponent struct {
VX, VY float64
}
type RenderableComponent struct {
Sprite rune
}
type HealthComponent struct {
Current, Max int
}
// --- WORLD ---
// The World manages all data, organized for cache-friendly access.
type World struct {
nextEntityID Entity
// Stores the component signature for each entity. Using a slice for dense entity IDs.
signatures map[Entity]ComponentType
systems []System
// --- Component Storage (Struct of Arrays) ---
// Each component type has its own slice. This is the core of the cache-friendly design.
positions []PositionComponent
velocities []VelocityComponent
renderables []RenderableComponent
healths []HealthComponent
// Maps to find the index of an entity's component in the slices above.
entityToPositionIndex map[Entity]int
entityToVelocityIndex map[Entity]int
entityToRenderableIndex map[Entity]int
entityToHealthIndex map[Entity]int
}
// NewWorld creates an initialized World.
func NewWorld() *World {
return &World{
signatures: make(map[Entity]ComponentType),
// Initialize component slices and index maps
positions: make([]PositionComponent, 0, 128),
velocities: make([]VelocityComponent, 0, 128),
renderables: make([]RenderableComponent, 0, 128),
healths: make([]HealthComponent, 0, 128),
entityToPositionIndex: make(map[Entity]int),
entityToVelocityIndex: make(map[Entity]int),
entityToRenderableIndex: make(map[Entity]int),
entityToHealthIndex: make(map[Entity]int),
}
}
// NewEntity creates a new entity with a unique ID.
func (w *World) NewEntity() Entity {
w.nextEntityID++
return w.nextEntityID
}
// --- Add Component Methods ---
// We now have specific, type-safe methods for adding each component.
func (w *World) AddPosition(e Entity, comp PositionComponent) {
w.positions = append(w.positions, comp)
w.entityToPositionIndex[e] = len(w.positions) - 1
w.signatures[e] |= PositionComponentType
}
func (w *World) AddVelocity(e Entity, comp VelocityComponent) {
w.velocities = append(w.velocities, comp)
w.entityToVelocityIndex[e] = len(w.velocities) - 1
w.signatures[e] |= VelocityComponentType
}
func (w *World) AddRenderable(e Entity, comp RenderableComponent) {
w.renderables = append(w.renderables, comp)
w.entityToRenderableIndex[e] = len(w.renderables) - 1
w.signatures[e] |= RenderableComponentType
}
func (w *World) AddHealth(e Entity, comp HealthComponent) {
w.healths = append(w.healths, comp)
w.entityToHealthIndex[e] = len(w.healths) - 1
w.signatures[e] |= HealthComponentType
}
func (w *World) AddSystem(system System) {
w.systems = append(w.systems, system)
}
func (w *World) Update() {
for _, sys := range w.systems {
sys.Update(w)
}
}
// --- SYSTEMS ---
type System interface {
Update(world *World)
}
// MovementSystem updates entity positions based on their velocity.
type MovementSystem struct {
signature ComponentType
}
func NewMovementSystem() *MovementSystem {
return &MovementSystem{
signature: PositionComponentType | VelocityComponentType,
}
}
func (s *MovementSystem) Update(world *World) {
fmt.Println("-> Running Movement System...")
// We iterate over entities that have a velocity, as that's a smaller set
// than entities that have a position.
for entity, signature := range world.signatures {
if (signature & s.signature) == s.signature {
// Get components by direct index lookup. This is very fast.
pos := &world.positions[world.entityToPositionIndex[entity]]
vel := world.velocities[world.entityToVelocityIndex[entity]]
pos.X += vel.VX
pos.Y += vel.VY
fmt.Printf(" - Moving Entity %d to (%.1f, %.1f)\n", entity, pos.X, pos.Y)
}
}
}
// RenderSystem "draws" entities to the console.
type RenderSystem struct {
signature ComponentType
}
func NewRenderSystem() *RenderSystem {
return &RenderSystem{
signature: PositionComponentType | RenderableComponentType,
}
}
func (s *RenderSystem) Update(world *World) {
fmt.Println("-> Running Render System...")
for entity, signature := range world.signatures {
if (signature & s.signature) == s.signature {
pos := world.positions[world.entityToPositionIndex[entity]]
render := world.renderables[world.entityToRenderableIndex[entity]]
// Check for health to display it
var healthInfo string
if (signature & HealthComponentType) == HealthComponentType {
health := world.healths[world.entityToHealthIndex[entity]]
healthInfo = fmt.Sprintf(" [HP:%d/%d]", health.Current, health.Max)
}
fmt.Printf(" - Drawing Entity %d ('%c') at (%.1f, %.1f)%s\n", entity, render.Sprite, pos.X, pos.Y, healthInfo)
}
}
}
// DamageSystem reduces health of entities every frame.
type DamageSystem struct {
signature ComponentType
}
func NewDamageSystem() *DamageSystem {
return &DamageSystem{
signature: HealthComponentType,
}
}
func (s *DamageSystem) Update(world *World) {
fmt.Println("-> Running Damage System...")
for entity, signature := range world.signatures {
if (signature & s.signature) == s.signature {
health := &world.healths[world.entityToHealthIndex[entity]]
health.Current -= 10
fmt.Printf(" - Damaging Entity %d, new HP: %d\n", entity, health.Current)
if health.Current < 0 {
health.Current = 0
}
}
}
}
// --- MAIN ---
func main() {
world := NewWorld()
// Create a Player with Position, Velocity, Renderable, and Health.
player := world.NewEntity()
world.AddPosition(player, PositionComponent{X: 10, Y: 10})
world.AddVelocity(player, VelocityComponent{VX: 1, VY: 0.5})
world.AddRenderable(player, RenderableComponent{Sprite: 'P'})
world.AddHealth(player, HealthComponent{Current: 100, Max: 100})
// Create an Enemy that also has all components.
enemy := world.NewEntity()
world.AddPosition(enemy, PositionComponent{X: 50, Y: 20})
world.AddVelocity(enemy, VelocityComponent{VX: -0.5, VY: -0.2})
world.AddRenderable(enemy, RenderableComponent{Sprite: 'E'})
world.AddHealth(enemy, HealthComponent{Current: 50, Max: 50})
// Create a Tree that is static and cannot be damaged.
tree := world.NewEntity()
world.AddPosition(tree, PositionComponent{X: 80, Y: 15})
world.AddRenderable(tree, RenderableComponent{Sprite: 'T'})
// Add systems in the desired order of execution.
world.AddSystem(NewDamageSystem())
world.AddSystem(NewMovementSystem())
world.AddSystem(NewRenderSystem()) // Render last to show final state
// Run the game loop.
for i := 0; i < 3; i++ {
fmt.Printf("\n--- Frame %d ---\n", i+1)
world.Update()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment