aboutsummaryrefslogtreecommitdiff
path: root/main.go
diff options
context:
space:
mode:
authorMartin Kagamino Lehoux <martin@lehoux.net>2026-01-15 22:05:38 +0100
committerMartin Kagamino Lehoux <martin@lehoux.net>2026-01-15 22:05:46 +0100
commit13626410c4d6db7fdc15ded5ee7865fcdbf0b88a (patch)
treea549d918804acb7682c857e742f1af189692cf62 /main.go
parentcf85fc095bc9b6ab4b49e4f88a5f04d7ac2fe74b (diff)
refactor: transition to struct-of-arrays ECS architecture
- Replace individual Entity structs with centralized component slices in Game struct (SoA layout). - Implement bitmask-based component lookup (CM_Position, CM_Move, etc.). - Convert logic from object-oriented Update() methods to standalone Systems (UpdateMoveSystem, UpdateGatherSystem, UpdateSpawnSystem). - Update rendering, selection, and input handling to iterate over component arrays.
Diffstat (limited to 'main.go')
-rw-r--r--main.go153
1 files changed, 88 insertions, 65 deletions
diff --git a/main.go b/main.go
index 6392488..e67e50b 100644
--- a/main.go
+++ b/main.go
@@ -31,37 +31,79 @@ const (
var soilColor = color.RGBA{0x60, 0x40, 0x20, 0xff}
type Game struct {
- Entities []*Entity
- CurrentAction Action
- Selection selection.GlobalSelection
- ResourceAmount int
- FaceSource *text.GoTextFaceSource
- UnitBuilder EntityBuilder
+ CurrentAction Action
+ GlobalSelection selection.GlobalSelection
+ ResourceAmount int
+ FaceSource *text.GoTextFaceSource
+ UnitBuilder EntityBuilder
+ // Components
+ Mask []ecs.Mask
+ // Entities []*Entity
+ Position []physics.Point
+ RelBounds []physics.Rectangle
+ Image []*ebiten.Image
+ Selection []selection.Selection
+ Move []physics.Move
+ Order []OrderKind
+ PatrolOrder []PatrolOrder
+ ResourceGatherer []ResourceGatherer
+ ResourceSource []ResourceSource
+ ResourceStorage []ResourceStorage
+ Spawn []Spawn
}
func (g Game) Now() time.Time {
return time.Now()
}
+func (g *Game) Append(components Components, mask ecs.Mask) ecs.Entity {
+ g.Mask = append(g.Mask, mask)
+ g.Position = append(g.Position, components.Position)
+ g.RelBounds = append(g.RelBounds, components.RelBounds)
+ g.Image = append(g.Image, components.Image)
+ g.Selection = append(g.Selection, components.Selection)
+ g.Move = append(g.Move, components.Move)
+ g.Order = append(g.Order, components.Order)
+ g.ResourceGatherer = append(g.ResourceGatherer, components.ResourceGatherer)
+ g.ResourceSource = append(g.ResourceSource, components.ResourceSource)
+ g.ResourceStorage = append(g.ResourceStorage, components.ResourceStorage)
+ g.Spawn = append(g.Spawn, components.Spawn)
+ return ecs.Entity(len(g.Mask) - 1)
+}
+
// TODO: This could be a maintained index
func (g *Game) getMoveMap() physics.MoveMap {
+ required := ecs.CM_Position
blocked := map[physics.Point]bool{}
- for _, e := range g.Entities {
- if e.Position.IsEnabled {
- blocked[e.Position.Value] = true
+ for i, mask := range g.Mask {
+ if mask&required == required {
+ blocked[g.Position[i]] = true
}
}
return physics.MoveMap{Width: 3200, Height: 2400, Blocked: blocked}
}
// TODO: This could be a maintained index
-func (g *Game) entityAt(position physics.Point) *Entity {
- for _, e := range g.Entities {
- if e.Position.IsEnabled && e.Position.Value == position {
- return e
+func (g *Game) entityAt(position physics.Point) ecs.Entity {
+ required := ecs.CM_Position
+ for i, mask := range g.Mask {
+ if mask&required == required && g.Position[i] == position {
+ return ecs.Entity(i)
}
}
- return nil
+ return -1
+}
+
+func getAllStorageDockings(g *Game) []physics.Point {
+ storageDockings := []physics.Point{}
+ required := ecs.CM_Position | ecs.CM_ResourceStorage
+ for i, mask := range g.Mask {
+ if mask&required == required {
+ position := g.Position[i]
+ storageDockings = append(storageDockings, physics.AdjacentPoints(position)...)
+ }
+ }
+ return storageDockings
}
const (
@@ -71,58 +113,38 @@ const (
func (g *Game) updateSelecting(cursor physics.Point, moveMap physics.MoveMap) {
if inpututil.IsKeyJustReleased(ebiten.KeyEscape) {
- g.Selection.Unselect()
+ g.GlobalSelection.Unselect(g.Selection)
}
if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) {
- g.Selection.Start(cursor)
+ g.GlobalSelection.Start(cursor)
}
if inpututil.IsMouseButtonJustReleased(ebiten.MouseButtonRight) {
destination := cursor.Div(100).Mul(100)
- entityAtDestination := g.entityAt(destination)
slog.Info("destination", slog.String("destination", destination.String()))
- for _, e := range g.Entities {
- if e.Selection.IsEnabled && e.Selection.Value.IsSelected {
- e.MainAction(g, destination, entityAtDestination, moveMap)
- }
+ for _, i := range g.GlobalSelection.Selected {
+ MainAction(g, i, destination, moveMap)
}
}
if inpututil.IsMouseButtonJustReleased(ebiten.MouseButtonLeft) {
- if g.Selection.IsActive(cursor) {
- // TODO: simplify with component columns
- targets := make([]selection.SelectTarget, len(g.Entities))
- for i, e := range g.Entities {
- targets[i] = selection.SelectTarget{
- Selection: &e.Selection,
- Position: e.Position,
- Bounds: e.RelBounds,
- }
- }
- if g.Selection.IsArea(cursor) {
- g.Selection.SelectMultiple(cursor, targets)
+ if g.GlobalSelection.IsActive(cursor) {
+ if g.GlobalSelection.IsArea(cursor) {
+ g.GlobalSelection.SelectMultiple(cursor, g.Mask, g.Selection, g.Position, g.RelBounds)
} else {
- g.Selection.SelectSingle(cursor, targets)
+ g.GlobalSelection.SelectSingle(cursor, g.Mask, g.Selection, g.Position, g.RelBounds)
}
}
}
if inpututil.IsKeyJustReleased(KeySpawnRequest) {
- for _, e := range g.Entities {
- if !e.Selection.IsEnabled || !e.Selection.Value.IsSelected || !e.Spawn.IsEnabled {
- continue
- }
- e.Spawn.Value.AddRequest(g)
- }
- }
-}
-
-func (g *Game) updatePatrolling(cursor physics.Point) {
- if inpututil.IsMouseButtonJustReleased(ebiten.MouseButtonRight) {
- destination := cursor.Div(100).Mul(100)
- for _, e := range g.Entities {
- if e.Selection.IsEnabled && e.Selection.Value.IsSelected {
- Patrol(e.Position, e.Move, &e.Order, destination)
+ required := ecs.CM_Spawn
+ for _, i := range g.GlobalSelection.Selected {
+ if g.Mask[i]&required == required {
+ spawn := &g.Spawn[i]
+ if g.ResourceAmount >= spawn.UnitResourceCost {
+ g.ResourceAmount -= spawn.UnitResourceCost
+ spawn.Requests = append(spawn.Requests, SpawnRequest{Start: g.Now()})
+ }
}
}
- g.CurrentAction = Selecting
}
}
@@ -141,13 +163,16 @@ func (g *Game) Update() error {
case Selecting:
g.updateSelecting(cursor, g.getMoveMap())
case Patrolling:
- g.updatePatrolling(cursor)
- }
- for _, e := range g.Entities {
- physics.UpdateMove(&e.Move, &e.Position, g.getMoveMap())
- e.UpdateOrder(g)
- UpdateSpawn(g, &e.Spawn, e.Position)
+ if inpututil.IsMouseButtonJustReleased(ebiten.MouseButtonRight) {
+ destination := cursor.Div(100).Mul(100)
+ StartPatrolSystem(destination, g.Mask, g.Position, g.Move, g.Order, g.PatrolOrder)
+ g.CurrentAction = Selecting
+ }
}
+ physics.UpdateMoveSystem(g.getMoveMap(), g.Mask, g.Move, g.Position)
+ UpdateSpawnSystem(g, g.Now(), g.Mask, g.Spawn, g.Position)
+ UpdateGatherSystem(g, g.Now(), g.getMoveMap(), g.Mask, g.Order, g.Position, g.Move, g.ResourceGatherer)
+ UpdatePatrolSystem(g.getMoveMap(), g.Mask, g.Order, g.PatrolOrder, g.Position, g.Move)
return nil
}
@@ -167,7 +192,7 @@ func main() {
}
logHandler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})
slog.SetDefault(slog.New(logHandler))
- ebiten.SetWindowSize(640, 480)
+ ebiten.SetWindowSize(960, 640)
ebiten.SetWindowTitle("Age of Empire")
icon, err := os.Open("icon-crop.jpg")
kcore.Expect(err, "failed to open icon")
@@ -181,16 +206,14 @@ func main() {
s, err := text.NewGoTextFaceSource(bytes.NewReader(goregular.TTF))
kcore.Expect(err, "failed to create font source")
game.FaceSource = s
- ironMine := NewEntityBuilder().WithPosition(physics.Point{X: 1000, Y: 1000}).WithSolid(NewFilledRectangleImage(physics.Point{X: 100, Y: 100}, color.RGBA{0x80, 0x80, 0x80, 0xff})).WithResourceSource(1000).WithSelection("square", selection.Building).Build()
- game.Entities = append(game.Entities, &ironMine)
- townCenter := NewEntityBuilder().WithPosition(physics.Point{X: 1000, Y: 2000}).WithSolid(NewFilledRectangleImage(physics.Point{X: 100, Y: 100}, color.RGBA{0x0, 0x0, 0xff, 0xff})).WithResourceStorage().WithSelection("square", selection.Building).WithSpawn(NewSpawn(50, 1*time.Second)).Build()
- game.Entities = append(game.Entities, &townCenter)
+ ironMineBuilder := NewEntityBuilder().WithSolid(NewFilledRectangleImage(physics.Point{X: 100, Y: 100}, color.RGBA{0x80, 0x80, 0x80, 0xff})).WithResourceSource(1000).WithSelection("square", selection.Building)
+ game.Append(ironMineBuilder.WithPosition(physics.Point{X: 1000, Y: 1000}).Build())
+ townCenterBuilder := NewEntityBuilder().WithSolid(NewFilledRectangleImage(physics.Point{X: 100, Y: 100}, color.RGBA{0x0, 0x0, 0xff, 0xff})).WithResourceStorage().WithSelection("square", selection.Building).WithSpawn(NewSpawn(50, 1*time.Second))
+ townCenter := game.Append(townCenterBuilder.WithPosition(physics.Point{X: 1000, Y: 2000}).Build())
game.UnitBuilder = NewEntityBuilder().WithSolid(NewFilledCircleImage(100, color.White)).WithSelection("round", selection.Unit).WithMove().WithOrder().WithResourceGatherer(15)
for i := 0; i < 0; i++ {
- spawnPosition, _ := physics.Closest(townCenter.Position.Value, physics.AdjacentPoints(townCenter.Position.Value), game.getMoveMap())
- person := game.UnitBuilder.Build()
- person.Position = ecs.C(spawnPosition)
- game.Entities = append(game.Entities, &person)
+ spawnPosition, _ := physics.Closest(game.Position[townCenter], physics.AdjacentPoints(game.Position[townCenter]), game.getMoveMap())
+ game.Append(game.UnitBuilder.WithPosition(spawnPosition).Build())
}
game.CurrentAction = Selecting
ebiten.SetRunnableOnUnfocused(true)