diff options
| -rw-r--r-- | README.md | 2 | ||||
| -rw-r--r-- | ecs/component.go | 10 | ||||
| -rwxr-xr-x | entity | bin | 0 -> 10923288 bytes | |||
| -rw-r--r-- | entity.go | 38 | ||||
| -rw-r--r-- | gather_order.go | 19 | ||||
| -rw-r--r-- | image.go | 5 | ||||
| -rw-r--r-- | main.go | 84 | ||||
| -rw-r--r-- | patrol_order.go | 16 | ||||
| -rw-r--r-- | physics/geometry.go (renamed from geometry.go) | 4 | ||||
| -rw-r--r-- | physics/move.go (renamed from move.go) | 81 | ||||
| -rw-r--r-- | selection.go | 8 |
11 files changed, 141 insertions, 126 deletions
@@ -4,6 +4,8 @@ ## TODO +- atm, stops moving if someone settles at destination + ```go func (g \*Game) updatePlacingWall(cursor Point) { if inpututil.IsMouseButtonJustReleased(ebiten.MouseButtonLeft) { diff --git a/ecs/component.go b/ecs/component.go new file mode 100644 index 0000000..29ef967 --- /dev/null +++ b/ecs/component.go @@ -0,0 +1,10 @@ +package ecs + +type Component[T any] struct { + IsEnabled bool + Value T +} + +func C[T any](t T) Component[T] { + return Component[T]{IsEnabled: true, Value: t} +} Binary files differ@@ -1,31 +1,27 @@ package main -import "github.com/hajimehoshi/ebiten/v2" +import ( + "age_of_empires/ecs" + "age_of_empires/physics" -type Component[T any] struct { - IsEnabled bool - Value T -} - -func C[T any](t T) Component[T] { - return Component[T]{IsEnabled: true, Value: t} -} + "github.com/hajimehoshi/ebiten/v2" +) type Entity struct { - Position Component[Point] - Image Component[*ebiten.Image] - Selection Component[Selection] - Move Component[Move] - Order Component[Order] - ResourceGatherer Component[ResourceGatherer] - ResourceSource Component[ResourceSource] - ResourceStorage Component[ResourceStorage] + Position ecs.Component[physics.Point] + Image ecs.Component[*ebiten.Image] + Selection ecs.Component[Selection] + Move ecs.Component[physics.Move] + Order ecs.Component[Order] + ResourceGatherer ecs.Component[ResourceGatherer] + ResourceSource ecs.Component[ResourceSource] + ResourceStorage ecs.Component[ResourceStorage] } -func (e Entity) Bounds() Rectangle { - return Rectangle{ - e.Position.Value, - e.Position.Value.Add(e.Image.Value.Bounds().Size()), +func (e Entity) Bounds() physics.Rectangle { + return physics.Rectangle{ + Min: e.Position.Value, + Max: e.Position.Value.Add(e.Image.Value.Bounds().Size()), } } diff --git a/gather_order.go b/gather_order.go index b4f3c9e..2f5a060 100644 --- a/gather_order.go +++ b/gather_order.go @@ -1,6 +1,7 @@ package main import ( + "age_of_empires/physics" "math" "time" @@ -22,7 +23,7 @@ func (o *GatherOrder) Update(e *Entity, g *Game) { o.updateGathererFull(e, g, gatherer) return } - if Distance(e.Position.Value, gatherer.CurrentTarget.Position.Value) <= 100 { + if physics.Distance(e.Position.Value, gatherer.CurrentTarget.Position.Value) <= 100 { o.updateGathering(gatherer, source) return } @@ -56,7 +57,7 @@ func (o *GatherOrder) updateGathererFull(e *Entity, g *Game, gatherer *ResourceG slog.Info("storage target", slog.String("storage", destination.String()), slog.Int("distance", distance)) if distance > 1 { slog.Info("moving to storage", slog.String("storage", destination.String())) - e.StartMove(destination, g.getMoveMap()) + physics.StartMove(&e.Move,e.Position, destination, g.getMoveMap()) return } gatherer.CurrentVolume = 0 @@ -65,21 +66,21 @@ func (o *GatherOrder) updateGathererFull(e *Entity, g *Game, gatherer *ResourceG } func (o *GatherOrder) startMoveToSource(e *Entity, gatherer *ResourceGatherer, g *Game) { - sourceDockings := AdjacentPoints(gatherer.CurrentTarget.Position.Value) + sourceDockings := physics.AdjacentPoints(gatherer.CurrentTarget.Position.Value) destination, distance := g.Closest(e.Position.Value, sourceDockings) if distance == math.MaxInt { slog.Info("no accessible storage, canceling gather order") e.Order.Value = nil return } - e.StartMove(destination, g.getMoveMap()) + physics.StartMove(&e.Move,e.Position, destination, g.getMoveMap()) } -func getAllStorageDockings(g *Game) []Point { - storageDockings := []Point{} +func getAllStorageDockings(g *Game) []physics.Point { + storageDockings := []physics.Point{} for _, entity := range g.Entities { if entity.Position.IsEnabled && entity.ResourceStorage.IsEnabled { - storageDockings = append(storageDockings, AdjacentPoints(entity.Position.Value)...) + storageDockings = append(storageDockings, physics.AdjacentPoints(entity.Position.Value)...) } } return storageDockings @@ -91,14 +92,14 @@ func Gather(e *Entity, source *Entity, g *Game) Order { slog.Info("gathering from", slog.String("source", source.Position.Value.String())) e.Order.Value = &GatherOrder{source: source} e.ResourceGatherer.Value.CurrentTarget = source - sourceDockings := AdjacentPoints(source.Position.Value) + sourceDockings := physics.AdjacentPoints(source.Position.Value) destination, distance := g.Closest(e.Position.Value, sourceDockings) if distance == math.MaxInt { slog.Info("no accessible storage, canceling gather order") e.Order.Value = nil return nil } - e.StartMove(destination, g.getMoveMap()) + physics.StartMove(&e.Move,e.Position, destination, g.getMoveMap()) } } return &GatherOrder{source: source} @@ -1,19 +1,20 @@ package main import ( + "age_of_empires/physics" "image/color" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/vector" ) -func NewFilledRectangleImage(size Point, color color.Color) *ebiten.Image { +func NewFilledRectangleImage(size physics.Point, color color.Color) *ebiten.Image { image := ebiten.NewImage(size.X, size.Y) image.Fill(color) return image } -func NewStrokeRectangleImage(size Point, strokeWidth int, color color.Color) *ebiten.Image { +func NewStrokeRectangleImage(size physics.Point, strokeWidth float32, color color.Color) *ebiten.Image { image := ebiten.NewImage(size.X, size.Y) vector.StrokeRect(image, 0, 0, float32(size.X), float32(size.Y), float32(strokeWidth), color, true) return image @@ -1,6 +1,8 @@ package main import ( + "age_of_empires/ecs" + "age_of_empires/physics" "image" "image/color" _ "image/jpeg" @@ -24,7 +26,7 @@ var soilColor = color.RGBA{0x60, 0x40, 0x20, 0xff} type GlobalSelection struct { IsActive bool - Start Point + Start physics.Point } type Game struct { @@ -33,22 +35,36 @@ type Game struct { Selection GlobalSelection } -func (g *Game) getMoveMap() MoveMap { - blocked := map[Point]bool{} +func DrawMove(screen *ebiten.Image, e *Entity) { + if e.Move.IsEnabled && e.Position.IsEnabled && e.Selection.IsEnabled { + if e.Selection.Value.IsSelected && e.Move.Value.IsActive { + last := e.Position.Value + dx := +e.Image.Value.Bounds().Dx() / 2 + dy := +e.Image.Value.Bounds().Dy() / 2 + for _, point := range e.Move.Value.Path { + vector.StrokeLine(screen, float32(last.X+dx), float32(last.Y+dy), float32(point.X+dx), float32(point.Y+dy), 10.0, color.RGBA{256 * 3 / 16, 256 * 3 / 16, 256 * 3 / 16, 256 / 4}, true) + last = point + } + } + } +} + +func (g *Game) getMoveMap() physics.MoveMap { + blocked := map[physics.Point]bool{} for _, e := range g.Entities { if e.Position.IsEnabled { blocked[e.Position.Value] = true } } - return MoveMap{Width: 3200, Height: 2400, Blocked: blocked} + return physics.MoveMap{Width: 3200, Height: 2400, Blocked: blocked} } -func (g *Game) Closest(from Point, candidates []Point) (Point, int) { - var closest Point +func (g *Game) Closest(from physics.Point, candidates []physics.Point) (physics.Point, int) { + var closest physics.Point closestDistance := math.MaxInt moveMap := g.getMoveMap() for _, dest := range candidates { - path, ok := SearchPath(from, dest, moveMap) + path, ok := physics.SearchPath(from, dest, moveMap) if !ok { continue } @@ -60,7 +76,7 @@ func (g *Game) Closest(from Point, candidates []Point) (Point, int) { return closest, closestDistance } -func (g *Game) updateSelecting(cursor Point, moveMap MoveMap) { +func (g *Game) updateSelecting(cursor physics.Point, moveMap physics.MoveMap) { if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) { g.Selection.Start = cursor g.Selection.IsActive = true @@ -80,13 +96,13 @@ func (g *Game) updateSelecting(cursor Point, moveMap MoveMap) { if entityAtDestination != nil && entityAtDestination.ResourceSource.IsEnabled { Gather(e, entityAtDestination, g) } else { - e.StartMove(destination, moveMap) + physics.StartMove(&e.Move,e.Position, destination, moveMap) } } } } if inpututil.IsMouseButtonJustReleased(ebiten.MouseButtonLeft) { - if g.Selection.IsActive && Distance(g.Selection.Start, cursor) > 10 { + if g.Selection.IsActive && physics.Distance(g.Selection.Start, cursor) > 10 { for _, e := range g.Entities { e.SelectMultiple(cursor, g.Selection) } @@ -102,7 +118,7 @@ func (g *Game) updateSelecting(cursor Point, moveMap MoveMap) { } } -func (g *Game) updatePatrolling(cursor Point) { +func (g *Game) updatePatrolling(cursor physics.Point) { if inpututil.IsMouseButtonJustReleased(ebiten.MouseButtonRight) { destination := cursor.Div(100).Mul(100) for _, e := range g.Entities { @@ -114,7 +130,7 @@ func (g *Game) updatePatrolling(cursor Point) { func (g *Game) Update() error { x, y := ebiten.CursorPosition() - cursor := Point{x, y} + cursor := physics.Point{X: x, Y: y} if inpututil.IsKeyJustReleased(ebiten.KeyEscape) { g.CurrentAction = Selecting slog.Info("selecting action") @@ -131,7 +147,7 @@ func (g *Game) Update() error { g.updatePatrolling(cursor) } for _, e := range g.Entities { - e.UpdateMove(moveMap) + physics.UpdateMove(&e.Move,&e.Position, moveMap) e.UpdateOrder(g) } return nil @@ -139,7 +155,7 @@ func (g *Game) Update() error { func (g *Game) Draw(screen *ebiten.Image) { x, y := ebiten.CursorPosition() - cursor := Point{x, y} + cursor := physics.Point{X: x, Y: y} screen.Fill(soilColor) for _, e := range g.Entities { Draw(screen, e) @@ -175,39 +191,39 @@ func main() { } ebiten.SetWindowIcon([]image.Image{iconImg}) game := &Game{} - ironImage := NewFilledRectangleImage(Point{100, 100}, color.RGBA{0x80, 0x80, 0x80, 0xff}) + ironImage := NewFilledRectangleImage(physics.Point{X: 100, Y: 100}, color.RGBA{0x80, 0x80, 0x80, 0xff}) ironMine := Entity{ - Position: C(Point{1000, 1000}), - Image: C(ironImage), - ResourceSource: C(ResourceSource{Remaining: 1000}), + Position: ecs.C(physics.Point{X: 1000, Y: 1000}), + Image: ecs.C(ironImage), + ResourceSource: ecs.C(ResourceSource{Remaining: 1000}), } game.Entities = append(game.Entities, &ironMine) - storageImage := NewFilledRectangleImage(Point{100, 100}, color.RGBA{0x00, 0x00, 0xff, 0xff}) + storageImage := NewFilledRectangleImage(physics.Point{X: 100, Y: 100}, color.RGBA{0x00, 0x00, 0xff, 0xff}) storage := Entity{ - Position: C(Point{1000, 2000}), - Image: C(storageImage), - ResourceStorage: C(ResourceStorage{}), + Position: ecs.C(physics.Point{X: 1000, Y: 2000}), + Image: ecs.C(storageImage), + ResourceStorage: ecs.C(ResourceStorage{}), } game.Entities = append(game.Entities, &storage) var order Order personImage := NewFilledCircleImage(100, color.RGBA{0xff, 0xff, 0xff, 0xff}) personSelectionHalo := NewStrokeCircleImage(110, SELECTION_HALO_WIDTH, color.RGBA{0xff, 0x00, 0x00, 0xff}) person1 := Entity{ - Position: C(Point{2000, 2000}), - Image: C(personImage), - Selection: C(Selection{IsSelected: false, Halo: personSelectionHalo}), - Move: C(Move{IsActive: false}), - Order: C(order), - ResourceGatherer: C(ResourceGatherer{MaxCapacity: 15}), + Position: ecs.C(physics.Point{X: 2000, Y: 2000}), + Image: ecs.C(personImage), + Selection: ecs.C(Selection{IsSelected: false, Halo: personSelectionHalo}), + Move: ecs.C(physics.Move{IsActive: false}), + Order: ecs.C(order), + ResourceGatherer: ecs.C(ResourceGatherer{MaxCapacity: 15}), } game.Entities = append(game.Entities, &person1) person2 := Entity{ - Position: C(Point{2200, 2200}), - Image: C(personImage), - Selection: C(Selection{IsSelected: false, Halo: personSelectionHalo}), - Move: C(Move{IsActive: false}), - Order: C(order), - ResourceGatherer: C(ResourceGatherer{MaxCapacity: 15}), + Position: ecs.C(physics.Point{X: 2200, Y: 2200}), + Image: ecs.C(personImage), + Selection: ecs.C(Selection{IsSelected: false, Halo: personSelectionHalo}), + Move: ecs.C(physics.Move{IsActive: false}), + Order: ecs.C(order), + ResourceGatherer: ecs.C(ResourceGatherer{MaxCapacity: 15}), } game.Entities = append(game.Entities, &person2) game.CurrentAction = Selecting diff --git a/patrol_order.go b/patrol_order.go index b7201ed..eb9dfb0 100644 --- a/patrol_order.go +++ b/patrol_order.go @@ -1,10 +1,14 @@ package main -import "golang.org/x/exp/slog" +import ( + "age_of_empires/physics" + + "golang.org/x/exp/slog" +) type PatrolOrder struct { - origin Point - destination Point + origin physics.Point + destination physics.Point } func (o *PatrolOrder) Update(e *Entity, g *Game) { @@ -12,15 +16,15 @@ func (o *PatrolOrder) Update(e *Entity, g *Game) { if !e.Move.Value.IsActive { moveMap := g.getMoveMap() if e.Position.Value == o.destination { - e.StartMove(o.origin, moveMap) + physics.StartMove(&e.Move, e.Position, o.origin, moveMap) } else { - e.StartMove(o.destination, moveMap) + physics.StartMove(&e.Move, e.Position, o.destination, moveMap) } } } } -func Patrol(e *Entity, destination Point) Order { +func Patrol(e *Entity, destination physics.Point) Order { if e.Selection.IsEnabled && e.Position.IsEnabled && e.Move.IsEnabled && e.Order.IsEnabled { if e.Selection.Value.IsSelected { origin := e.Position.Value diff --git a/geometry.go b/physics/geometry.go index ba3f643..1902fdf 100644 --- a/geometry.go +++ b/physics/geometry.go @@ -1,4 +1,4 @@ -package main +package physics import ( "image" @@ -13,7 +13,7 @@ func Distance(p1, p2 Point) float64 { } func Length(p Point) float64 { - return Distance(p, Point{0, 0}) + return Distance(p, Point{X:0,Y:0}) } func Normalize(p Point) Point { diff --git a/move.go b/physics/move.go index 1322a51..b6f13d7 100644 --- a/move.go +++ b/physics/move.go @@ -1,12 +1,10 @@ -package main +package physics import ( + "age_of_empires/ecs" "fmt" - "image/color" "math" - "github.com/hajimehoshi/ebiten/v2" - "github.com/hajimehoshi/ebiten/v2/vector" "golang.org/x/exp/slices" "golang.org/x/exp/slog" ) @@ -63,16 +61,16 @@ func SearchPath(origin Point, destination Point, moveMap MoveMap) (Path, bool) { func (s *PathSearch) addNeighbors(point Point, node Node) { if point.X >= 100 { - s.consider(Point{point.X - 100, point.Y}, point) + s.consider(Point{X: point.X - 100, Y: point.Y}, point) } if point.X <= s.moveMap.Width-100 { - s.consider(Point{point.X + 100, point.Y}, point) + s.consider(Point{X: point.X + 100, Y: point.Y}, point) } if point.Y >= 100 { - s.consider(Point{point.X, point.Y - 100}, point) + s.consider(Point{X: point.X, Y: point.Y - 100}, point) } if point.Y <= s.moveMap.Height-100 { - s.consider(Point{point.X, point.Y + 100}, point) + s.consider(Point{X: point.X, Y: point.Y + 100}, point) } } @@ -95,7 +93,7 @@ func (s *PathSearch) consider(neighbor Point, point Point) { // openList must not be empty func (s *PathSearch) bestOpenNode() (Point, Node) { - bestPoint := Point{} + bestPoint := Point{X: 0, Y: 0} bestNode := Node{} bestCost := math.MaxInt32 for point, node := range s.openList { @@ -139,8 +137,8 @@ func (s *PathSearch) search() (Path, bool) { func NewMove(origin Point, destination Point, moveMap MoveMap) Move { current := Point{ - origin.X / 100 * 100, - origin.Y / 100 * 100, + X: origin.X / 100 * 100, + Y: origin.Y / 100 * 100, } path, ok := SearchPath(current, destination, moveMap) if !ok { @@ -159,62 +157,47 @@ func (p *Path) isValid(moveMap MoveMap) bool { return true } -func (e *Entity) StartMove(destination Point, moveMap MoveMap) { - if e.Move.IsEnabled && e.Position.IsEnabled && e.Selection.IsEnabled { - e.Move.Value = NewMove(e.Position.Value, destination, moveMap) +func StartMove(move *ecs.Component[Move], position ecs.Component[Point], destination Point, moveMap MoveMap) { + if move.IsEnabled && position.IsEnabled { + move.Value = NewMove(position.Value, destination, moveMap) slog.Info("entity starting move", slog.String("destination", destination.String())) } } -func (e *Entity) moveToward(destination Point) { - remainingMove := destination.Sub(e.Position.Value) +func moveToward(position *ecs.Component[Point], destination Point) { + remainingMove := destination.Sub(position.Value) delta := remainingMove.Mul(MOVE_SPEED).Div(int(Length(remainingMove))) - e.Position.Value = e.Position.Value.Add(delta) + position.Value = position.Value.Add(delta) } -func (e *Entity) UpdateMove(moveMap MoveMap) { - if !e.Move.IsEnabled || !e.Position.IsEnabled || !e.Move.Value.IsActive { +func UpdateMove(move *ecs.Component[Move], position *ecs.Component[Point], moveMap MoveMap) { + if !move.IsEnabled || !position.IsEnabled || !move.Value.IsActive { return } - position := e.Position.Value - if len(e.Move.Value.Path) == 0 { - e.Move.Value.IsActive = false + if len(move.Value.Path) == 0 { + move.Value.IsActive = false return } - next := e.Move.Value.Path[0] - if Distance(position, next) < MOVE_SPEED { - e.Position.Value = next - remainingPath := e.Move.Value.Path[1:] - if next == e.Move.Value.Destination { - e.Move.Value.IsActive = false - slog.Info("move finished", slog.String("destination", e.Move.Value.Destination.String())) + next := move.Value.Path[0] + if Distance(position.Value, next) < MOVE_SPEED { + position.Value = next + remainingPath := move.Value.Path[1:] + if next == move.Value.Destination { + move.Value.IsActive = false + slog.Info("move finished", slog.String("destination", move.Value.Destination.String())) } else if remainingPath.isValid(moveMap) { - e.Move.Value.Path = remainingPath + move.Value.Path = remainingPath } else { // Here maybe the destination has to change - path, ok := SearchPath(next, e.Move.Value.Destination, moveMap) + path, ok := SearchPath(next, move.Value.Destination, moveMap) if !ok { - slog.Info("no path found", slog.String("destination", e.Move.Value.Destination.String())) - e.Move.Value.IsActive = false + slog.Info("no path found", slog.String("destination", move.Value.Destination.String())) + move.Value.IsActive = false } else { - e.Move.Value.Path = path[1:] + move.Value.Path = path[1:] } } } else { - e.moveToward(next) - } -} - -func DrawMove(screen *ebiten.Image, e *Entity) { - if e.Move.IsEnabled && e.Position.IsEnabled && e.Selection.IsEnabled { - if e.Selection.Value.IsSelected && e.Move.Value.IsActive { - last := e.Position.Value - dx := +e.Image.Value.Bounds().Dx() / 2 - dy := +e.Image.Value.Bounds().Dy() / 2 - for _, point := range e.Move.Value.Path { - vector.StrokeLine(screen, float32(last.X+dx), float32(last.Y+dy), float32(point.X+dx), float32(point.Y+dy), 10.0, color.RGBA{256 * 3 / 16, 256 * 3 / 16, 256 * 3 / 16, 256 / 4}, true) - last = point - } - } + moveToward(position, next) } } diff --git a/selection.go b/selection.go index e41fce3..91f35fe 100644 --- a/selection.go +++ b/selection.go @@ -1,6 +1,8 @@ package main import ( + "age_of_empires/physics" + "github.com/hajimehoshi/ebiten/v2" "golang.org/x/exp/slog" ) @@ -12,10 +14,10 @@ type Selection struct { Halo *ebiten.Image } -func (e *Entity) SelectMultiple(cursor Point, selection GlobalSelection) { +func (e *Entity) SelectMultiple(cursor physics.Point, selection GlobalSelection) { if e.Selection.IsEnabled { e.Selection.Value.IsSelected = false - selectionBounds := Rectangle{selection.Start, cursor}.Canon() + selectionBounds := physics.Rectangle{Min:selection.Start, Max:cursor}.Canon() if selectionBounds.Overlaps(e.Bounds()) { e.Selection.Value.IsSelected = true slog.Info("entity selected", slog.String("position", e.Position.Value.String())) @@ -23,7 +25,7 @@ func (e *Entity) SelectMultiple(cursor Point, selection GlobalSelection) { } } -func (e *Entity) SelectSingle(cursor Point, canBeSelected bool) bool { +func (e *Entity) SelectSingle(cursor physics.Point, canBeSelected bool) bool { if e.Selection.IsEnabled { e.Selection.Value.IsSelected = false if canBeSelected && cursor.In(e.Bounds()) { |
