From 563b9ee704a14322ed4805896c4f8ca3fa5df03a Mon Sep 17 00:00:00 2001 From: Martin Kagamino Lehoux Date: Fri, 25 Jul 2025 11:41:19 +0200 Subject: feat: Selection with priority --- README.md | 2 +- draw.go | 3 +- entity.go | 12 ++++--- main.go | 24 ++++++------- selection.go | 40 ---------------------- selection/selection.go | 91 ++++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 111 insertions(+), 61 deletions(-) delete mode 100644 selection.go create mode 100644 selection/selection.go diff --git a/README.md b/README.md index 121bf0d..3fa232a 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,12 @@ ## TODO - time must come from engine, not now -- selection priority: selecting units + building -> select only units - see the current action (debug?) - keyboard config - bigger map - move the map view - ecs array: on delete, replace by last and update id->index map +- `ecs.Component[*ebiten.Image]` drop the pointer - perf: look for nil comparison -> more data oriented - perf: keep a short array of selected diff --git a/draw.go b/draw.go index ba4f651..528955e 100644 --- a/draw.go +++ b/draw.go @@ -2,6 +2,7 @@ package main import ( "age_of_empires/physics" + "age_of_empires/selection" "fmt" "image/color" @@ -19,7 +20,7 @@ func (e *Entity) Draw(g *Game, screen *ebiten.Image) { screen.DrawImage(e.Image.Value, op) if e.Selection.IsEnabled && e.Selection.Value.IsSelected { opt := &ebiten.DrawImageOptions{} - opt.GeoM.Translate(float64(e.Position.Value.X-SELECTION_HALO_WIDTH/2), float64(e.Position.Value.Y-SELECTION_HALO_WIDTH/2)) + opt.GeoM.Translate(float64(e.Position.Value.X-selection.HALO_WIDTH/2), float64(e.Position.Value.Y-selection.HALO_WIDTH/2)) screen.DrawImage(e.Selection.Value.Halo, opt) if e.ResourceSource.IsEnabled { resourceText := fmt.Sprintf("%d", e.ResourceSource.Value.Remaining) diff --git a/entity.go b/entity.go index cf3f4ac..2b120f5 100644 --- a/entity.go +++ b/entity.go @@ -3,6 +3,7 @@ package main import ( "age_of_empires/ecs" "age_of_empires/physics" + "age_of_empires/selection" "image/color" "github.com/hajimehoshi/ebiten/v2" @@ -11,7 +12,7 @@ import ( type Entity struct { Position ecs.Component[physics.Point] Image ecs.Component[*ebiten.Image] - Selection ecs.Component[Selection] + Selection ecs.Component[selection.Selection] Move ecs.Component[physics.Move] Order ecs.Component[Order] ResourceGatherer ecs.Component[ResourceGatherer] @@ -49,16 +50,17 @@ func (b EntityBuilder) WithImage(image *ebiten.Image) EntityBuilder { var red = color.RGBA{0xff, 0x00, 0x00, 0xff} -func (b EntityBuilder) WithSelection(haloKind string) EntityBuilder { - b.entity.Selection = ecs.C(Selection{ +func (b EntityBuilder) WithSelection(haloKind string, priority selection.Priority) EntityBuilder { + b.entity.Selection = ecs.C(selection.Selection{ IsSelected: false, Halo: nil, + Priority: priority, }) switch haloKind { case "round": - b.entity.Selection.Value.Halo = NewStrokeCircleImage(110, SELECTION_HALO_WIDTH, red) + b.entity.Selection.Value.Halo = NewStrokeCircleImage(110, selection.HALO_WIDTH, red) case "square": - b.entity.Selection.Value.Halo = NewStrokeRectangleImage(physics.Point{X: 110, Y: 110}, SELECTION_HALO_WIDTH, red) + b.entity.Selection.Value.Halo = NewStrokeRectangleImage(physics.Point{X: 110, Y: 110}, selection.HALO_WIDTH, red) } return b } diff --git a/main.go b/main.go index d119c43..3d014bf 100644 --- a/main.go +++ b/main.go @@ -3,6 +3,7 @@ package main import ( "age_of_empires/ecs" "age_of_empires/physics" + "age_of_empires/selection" "bytes" "image" "image/color" @@ -28,15 +29,10 @@ const ( var soilColor = color.RGBA{0x60, 0x40, 0x20, 0xff} -type GlobalSelection struct { - IsActive bool - Start physics.Point -} - type Game struct { Entities []*Entity CurrentAction Action - Selection GlobalSelection + Selection selection.GlobalSelection ResourceAmount int FaceSource *text.GoTextFaceSource UnitBuilder EntityBuilder @@ -80,15 +76,15 @@ func (g *Game) updateSelecting(cursor physics.Point, moveMap physics.MoveMap) { } if inpututil.IsMouseButtonJustReleased(ebiten.MouseButtonLeft) { if g.Selection.IsActive && physics.Distance(g.Selection.Start, cursor) > 10 { + selector := selection.NewMultiple(g.Selection.Start, cursor) for _, e := range g.Entities { - e.SelectMultiple(cursor, g.Selection) + selector.Preselect(e.Position, e.Bounds(), &e.Selection) } + selector.Select() } else { - canBeSelected := true + selector := selection.NewSingle(cursor) for _, e := range g.Entities { - if e.SelectSingle(cursor, canBeSelected) { - canBeSelected = false - } + selector.Select(e.Position, e.Bounds(), &e.Selection) } } g.Selection.IsActive = false @@ -168,11 +164,11 @@ 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}).WithImage(NewFilledRectangleImage(physics.Point{X: 100, Y: 100}, color.RGBA{0x80, 0x80, 0x80, 0xff})).WithResourceSource(1000).WithSelection("square").Build() + ironMine := NewEntityBuilder().WithPosition(physics.Point{X: 1000, Y: 1000}).WithImage(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}).WithImage(NewFilledRectangleImage(physics.Point{X: 100, Y: 100}, color.RGBA{0x0, 0x0, 0xff, 0xff})).WithResourceStorage().WithSelection("square").WithSpawn(NewSpawn(50, 1*time.Second)).Build() + townCenter := NewEntityBuilder().WithPosition(physics.Point{X: 1000, Y: 2000}).WithImage(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) - game.UnitBuilder = NewEntityBuilder().WithImage(NewFilledCircleImage(100, color.White)).WithSelection("round").WithMove().WithOrder().WithResourceGatherer(15) + game.UnitBuilder = NewEntityBuilder().WithImage(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() diff --git a/selection.go b/selection.go deleted file mode 100644 index 496d7f6..0000000 --- a/selection.go +++ /dev/null @@ -1,40 +0,0 @@ -package main - -import ( - "age_of_empires/physics" - - "github.com/hajimehoshi/ebiten/v2" - "golang.org/x/exp/slog" -) - -const SELECTION_HALO_WIDTH = 10 - -type Selection struct { - IsSelected bool - Halo *ebiten.Image -} - -func (e *Entity) SelectMultiple(cursor physics.Point, selection GlobalSelection) { - if !e.Selection.IsEnabled { - return - } - e.Selection.Value.IsSelected = false - 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())) - } -} - -func (e *Entity) SelectSingle(cursor physics.Point, canBeSelected bool) bool { - if !e.Selection.IsEnabled { - return false - } - e.Selection.Value.IsSelected = false - if !canBeSelected || !cursor.In(e.Bounds()) { - return false - } - e.Selection.Value.IsSelected = true - slog.Info("entity selected", slog.String("position", e.Position.Value.String())) - return true -} diff --git a/selection/selection.go b/selection/selection.go new file mode 100644 index 0000000..2755110 --- /dev/null +++ b/selection/selection.go @@ -0,0 +1,91 @@ +package selection + +import ( + "age_of_empires/ecs" + "age_of_empires/physics" + "math" + + "github.com/hajimehoshi/ebiten/v2" +) + +const HALO_WIDTH = 10 + +type Priority int + +const ( + Unit Priority = iota + Building +) + +type GlobalSelection struct { + IsActive bool + Start physics.Point +} + +type Selection struct { + IsSelected bool + Halo *ebiten.Image + Priority Priority +} + +type Multiple struct { + Bounds physics.Rectangle + Preselected []*ecs.Component[Selection] +} + +func NewMultiple(start physics.Point, end physics.Point) Multiple { + return Multiple{ + Bounds: physics.Rectangle{Min: start, Max: end}.Canon(), + Preselected: []*ecs.Component[Selection]{}, + } +} + +func (m *Multiple) Preselect(position ecs.Component[physics.Point], bounds physics.Rectangle, selection *ecs.Component[Selection]) { + if !selection.IsEnabled { + return + } + selection.Value.IsSelected = false + if m.Bounds.Overlaps(bounds) { + m.Preselected = append(m.Preselected, selection) + } +} + +func (m *Multiple) Select() { + if len(m.Preselected) == 0 { + return + } + highestPriority := Priority(math.MaxInt) + for _, selection := range m.Preselected { + if selection.Value.Priority < highestPriority { + highestPriority = selection.Value.Priority + } + } + for _, selection := range m.Preselected { + if selection.Value.Priority == highestPriority { + selection.Value.IsSelected = true + } + } +} + +type Single struct { + Cursor physics.Point + HasSelected bool +} + +func NewSingle(cursor physics.Point) Single { + return Single{ + Cursor: cursor, + HasSelected: false, + } +} + +func (s *Single) Select(position ecs.Component[physics.Point], bounds physics.Rectangle, selection *ecs.Component[Selection]) { + if !selection.IsEnabled { + return + } + selection.Value.IsSelected = false + if s.HasSelected || !s.Cursor.In(bounds) { + return + } + selection.Value.IsSelected = true +} -- cgit v1.3