diff options
| author | Martin Kagamino Lehoux <martin@lehoux.net> | 2023-09-15 08:10:04 +0200 |
|---|---|---|
| committer | Martin Kagamino Lehoux <martin@lehoux.net> | 2023-09-15 08:10:04 +0200 |
| commit | 2d2a9fe8520f469897f49dc261289d7cc1c5d26c (patch) | |
| tree | 79cf40889d6bf3cdaa26c158ec271d40f9692f6a | |
| parent | f7c05f1b9417eff446753b8fafc7829859521aa3 (diff) | |
refactor: Into ECS
| -rw-r--r-- | entity.go | 39 | ||||
| -rw-r--r-- | image.go | 21 | ||||
| -rw-r--r-- | main.go | 110 | ||||
| -rw-r--r-- | move.go | 63 | ||||
| -rw-r--r-- | person.go | 49 | ||||
| -rw-r--r-- | selection.go | 34 |
6 files changed, 188 insertions, 128 deletions
diff --git a/entity.go b/entity.go new file mode 100644 index 0000000..6066772 --- /dev/null +++ b/entity.go @@ -0,0 +1,39 @@ +package main + +import "github.com/hajimehoshi/ebiten/v2" + +type Component[T any] struct { + IsEnabled bool + Value T +} + +func C[T any](t T) Component[T] { + return Component[T]{IsEnabled: true, Value: t} +} + +type Entity struct { + Position Component[Point] + Image Component[*ebiten.Image] + Selection Component[Selection] + Move Component[Move] +} + +func (e Entity) Bounds() Rectangle { + return Rectangle{ + e.Position.Value, + e.Position.Value.Add(e.Image.Value.Bounds().Size()), + } +} + +func Draw(screen *ebiten.Image, e *Entity) { + if e.Image.IsEnabled && e.Position.IsEnabled { + op := &ebiten.DrawImageOptions{} + op.GeoM.Translate(float64(e.Position.Value.X), float64(e.Position.Value.Y)) + if e.Selection.IsEnabled && e.Selection.Value.IsSelected { + op.GeoM.Translate(-10, -10) + screen.DrawImage(e.Selection.Value.SelectedImage, op) + } else { + screen.DrawImage(e.Image.Value, op) + } + } +} diff --git a/image.go b/image.go new file mode 100644 index 0000000..c95dd94 --- /dev/null +++ b/image.go @@ -0,0 +1,21 @@ +package main + +import ( + "image/color" + + "github.com/hajimehoshi/ebiten/v2" +) + +func NewColorImage(size Point, color color.Color) *ebiten.Image { + image := ebiten.NewImage(size.X, size.Y) + image.Fill(color) + return image +} + +func NewHaloImage(sourceImage *ebiten.Image, color color.Color, width int) *ebiten.Image { + image := NewColorImage(sourceImage.Bounds().Size().Add(Point{2 * width, 2 * width}), color) + op := &ebiten.DrawImageOptions{} + op.GeoM.Translate(float64(width), float64(width)) + image.DrawImage(sourceImage, op) + return image +} @@ -2,10 +2,12 @@ package main import ( "image/color" + "os" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/inpututil" "github.com/hajimehoshi/ebiten/v2/vector" + "golang.org/x/exp/slog" ) type Action string @@ -14,38 +16,22 @@ const ( Selecting Action = "selecting" ) -type Tile struct { - *ebiten.Image - Size Point - Position Point -} - -func NewTile(size Point, position Point, color color.Color) Tile { - image := ebiten.NewImage(int(size.X), int(size.Y)) - image.Fill(color) - return Tile{Size: size, Position: position, Image: image} -} -func (t Tile) CollisionBounds() Rectangle { - return Rectangle{t.Position, t.Position.Add(t.Size)} -} - var soilColor = color.RGBA{0x60, 0x40, 0x20, 0xff} -var soil = NewTile(Point{2800, 2000}, Point{200, 200}, soilColor) -type Selection struct { +type GlobalSelection struct { IsActive bool Start Point } type Game struct { - Persons []*Person + Entities []*Entity CurrentAction Action - Selection Selection + Selection GlobalSelection } -func (g *Game) getBlocked() map[Point]bool { +func (g *Game) getMoveMap() MoveMap { blocked := map[Point]bool{} - return blocked + return MoveMap{Width: 3200, Height: 2400, Blocked: blocked} } func (g *Game) updateSelecting(cursor Point) { @@ -54,34 +40,22 @@ func (g *Game) updateSelecting(cursor Point) { g.Selection.IsActive = true } if inpututil.IsMouseButtonJustReleased(ebiten.MouseButtonRight) { - destination := cursor.Sub(soil.Position).Div(100).Mul(100).Add(Point{50, 50}) - for _, p := range g.Persons { - if p.IsSelected { - futureBounds := Rectangle{ - destination.Sub(p.size.Div(2)), - destination.Add(p.size.Div(2)), - } - if futureBounds.In(soil.Bounds()) { - p.MoveTo(destination, g.getBlocked()) - } - } + destination := cursor.Div(100).Mul(100) + slog.Info("destination", slog.String("destination", destination.String())) + moveMap := g.getMoveMap() + for _, e := range g.Entities { + e.StartMove(destination, moveMap) } } if inpututil.IsMouseButtonJustReleased(ebiten.MouseButtonLeft) { if g.Selection.IsActive && Distance(g.Selection.Start, cursor) > 10 { - for _, p := range g.Persons { - p.IsSelected = false - selectionBounds := Rectangle{g.Selection.Start.Sub(soil.Position), cursor.Sub(soil.Position)}.Canon() - if selectionBounds.Overlaps(p.CollisionBounds()) { - p.IsSelected = true - } + for _, e := range g.Entities { + e.SelectMultiple(cursor, g.Selection) } } else { canBeSelected := true - for _, p := range g.Persons { - p.IsSelected = false - if cursor.Sub(soil.Position).In(p.CollisionBounds()) { - p.IsSelected = canBeSelected + for _, e := range g.Entities { + if e.SelectSingle(cursor, canBeSelected) { canBeSelected = false } } @@ -100,35 +74,22 @@ func (g *Game) Update() error { case Selecting: g.updateSelecting(cursor) } - for _, p := range g.Persons { - p.Update() + for _, e := range g.Entities { + e.UpdateMove() } return nil } func (g *Game) Draw(screen *ebiten.Image) { x, y := ebiten.CursorPosition() - screenClick := Point{x, y} - screen.Fill(color.RGBA{0x80, 0xa0, 0xc0, 0xff}) - soil.Fill(soilColor) - for _, p := range g.Persons { - bounds := p.Image().Bounds() - op := &ebiten.DrawImageOptions{} - op.GeoM.Translate(float64(p.Position.X-bounds.Dx()/2), float64(p.Position.Y-bounds.Dy()/2)) - soil.DrawImage(p.Image(), op) - if p.IsSelected && p.move.IsActive { - last := p.Position - for _, point := range p.move.Path { - vector.StrokeLine(soil.Image, float32(last.X), float32(last.Y), float32(point.X), float32(point.Y), 10.0, color.RGBA{256 * 3 / 16, 256 * 3 / 16, 256 * 3 / 16, 256 / 4}, true) - last = point - } - } + cursor := Point{x, y} + screen.Fill(soilColor) + for _, e := range g.Entities { + Draw(screen, e) + DrawMove(screen, e) } - op := &ebiten.DrawImageOptions{} - op.GeoM.Translate(float64(soil.Position.X), float64(soil.Position.Y)) - screen.DrawImage(soil.Image, op) if g.Selection.IsActive { - vector.StrokeRect(screen, float32(g.Selection.Start.X), float32(g.Selection.Start.Y), float32(screenClick.X-g.Selection.Start.X), float32(screenClick.Y-g.Selection.Start.Y), 10.0, color.RGBA{256 * 3 / 16, 256 * 3 / 16, 256 * 3 / 16, 256 / 4}, true) + vector.StrokeRect(screen, float32(g.Selection.Start.X), float32(g.Selection.Start.Y), float32(cursor.X-g.Selection.Start.X), float32(cursor.Y-g.Selection.Start.Y), 10.0, color.RGBA{256 * 3 / 16, 256 * 3 / 16, 256 * 3 / 16, 256 / 4}, true) } } @@ -137,13 +98,28 @@ func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeigh } func main() { + logHandler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}) + slog.SetDefault(slog.New(logHandler)) ebiten.SetWindowSize(640, 480) ebiten.SetWindowTitle("Age of Empire") game := &Game{} - mainPerson := NewPerson(Point{soil.Image.Bounds().Dx() / 2, soil.Image.Bounds().Dy() / 2}) - nonPlayerPerson := NewPerson(Point{450, 450}) - game.Persons = append(game.Persons, &mainPerson) - game.Persons = append(game.Persons, &nonPlayerPerson) + ironImage := NewColorImage(Point{100, 100}, color.RGBA{0x80, 0x80, 0x80, 0xff}) + ironSelectedImage := NewHaloImage(ironImage, color.RGBA{0xff, 0xff, 0xff, 0xff}, 10) + ironMine := Entity{ + Position: C(Point{1050, 1050}), + Image: C(ironImage), + Selection: C(Selection{IsSelected: false, SelectedImage: ironSelectedImage}), + } + game.Entities = append(game.Entities, &ironMine) + personImage := NewColorImage(Point{100, 100}, color.RGBA{0xff, 0xff, 0xff, 0xff}) + personSelectedImage := NewHaloImage(personImage, color.RGBA{0xff, 0x00, 0x00, 0xff}, 10) + alterPerson := Entity{ + Position: C(Point{2050, 2050}), + Image: C(personImage), + Selection: C(Selection{IsSelected: false, SelectedImage: personSelectedImage}), + Move: C(Move{IsActive: false}), + } + game.Entities = append(game.Entities, &alterPerson) game.CurrentAction = Selecting if err := ebiten.RunGame(game); err != nil { panic(err) @@ -1,10 +1,13 @@ package main import ( - "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" ) type Move struct { @@ -24,21 +27,27 @@ func (n Node) Cost() int { return n.CostFromOrigin + n.CostToDestination } +type MoveMap struct { + Width int + Height int + Blocked map[Point]bool +} + type PathSearch struct { origin Point destination Point openList map[Point]Node closedList map[Point]Node - blocked map[Point]bool + moveMap MoveMap } -func SearchPath(origin Point, destination Point, blocked map[Point]bool) ([]Point, bool) { +func SearchPath(origin Point, destination Point, moveMap MoveMap) ([]Point, bool) { search := PathSearch{ origin: origin, destination: destination, openList: map[Point]Node{}, closedList: map[Point]Node{}, - blocked: blocked, + moveMap: moveMap, } return search.search() } @@ -47,19 +56,19 @@ func (s *PathSearch) addNeighbours(point Point, node Node) { if point.X >= 100 { s.consider(Point{point.X - 100, point.Y}, point) } - if point.X <= soil.Size.X-100 { + if point.X <= s.moveMap.Width-100 { s.consider(Point{point.X + 100, point.Y}, point) } if point.Y >= 100 { s.consider(Point{point.X, point.Y - 100}, point) } - if point.Y <= soil.Size.Y-100 { + if point.Y <= s.moveMap.Height-100 { s.consider(Point{point.X, point.Y + 100}, point) } } func (s *PathSearch) consider(neighbor Point, point Point) { - if s.blocked[neighbor] { + if s.moveMap.Blocked[neighbor] { return } if _, ok := s.closedList[neighbor]; !ok { @@ -119,14 +128,14 @@ func (s *PathSearch) search() ([]Point, bool) { return []Point{}, false } -func NewMove(origin Point, destination Point, blocked map[Point]bool) Move { +func NewMove(origin Point, destination Point, moveMap MoveMap) Move { current := Point{ - origin.X/100*100 + 50, - origin.Y/100*100 + 50, + origin.X / 100 * 100, + origin.Y / 100 * 100, } - search, ok := SearchPath(current, destination, blocked) + search, ok := SearchPath(current, destination, moveMap) if !ok { - fmt.Println("No path found") + slog.Info("no path found", slog.String("destination", destination.String())) return Move{IsActive: false} } return Move{IsActive: true, Origin: origin, Destination: destination, Path: search} @@ -140,6 +149,7 @@ func (m *Move) Update(position Point, speed int) Point { if Distance(position, next) < float64(speed) { if next == m.Destination { m.IsActive = false + slog.Info("move finished", slog.String("destintation", m.Destination.String())) } else { m.Path = m.Path[1:] } @@ -149,3 +159,32 @@ func (m *Move) Update(position Point, speed int) Point { delta := remainingMove.Mul(speed).Div(int(Length(remainingMove))) return position.Add(delta) } + +func (e *Entity) StartMove(destination Point, moveMap MoveMap) { + if e.Move.IsEnabled && e.Position.IsEnabled && e.Selection.IsEnabled { + if e.Selection.Value.IsSelected { + e.Move.Value = NewMove(e.Position.Value, destination, moveMap) + slog.Info("entity starting move", slog.String("destination", destination.String())) + } + } +} + +func (e *Entity) UpdateMove() { + if e.Move.IsEnabled && e.Position.IsEnabled { + e.Position.Value = e.Move.Value.Update(e.Position.Value, 10) + } +} + +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 + } + } + } +} diff --git a/person.go b/person.go deleted file mode 100644 index 4ddeddc..0000000 --- a/person.go +++ /dev/null @@ -1,49 +0,0 @@ -package main - -import ( - "image/color" - - "github.com/hajimehoshi/ebiten/v2" -) - -type Person struct { - size Point - Position Point - IsSelected bool - move Move - image *ebiten.Image - selectedImage *ebiten.Image -} - -func NewPerson(position Point) Person { - image := ebiten.NewImage(100, 100) - image.Fill(color.RGBA{0xff, 0xff, 0xff, 0xff}) - selectedImage := ebiten.NewImage(120, 120) - op := &ebiten.DrawImageOptions{} - op.GeoM.Translate(10, 10) - selectedImage.Fill(color.RGBA{0xff, 0x00, 0x00, 0xff}) - selectedImage.DrawImage(image, op) - return Person{Position: position, size: Point{100, 100}, image: image, selectedImage: selectedImage} -} - -func (p Person) Image() *ebiten.Image { - if p.IsSelected { - return p.selectedImage - } - return p.image -} - -func (p Person) CollisionBounds() Rectangle { - return Rectangle{ - p.Position.Sub(p.size.Div(2)), - p.Position.Add(p.size.Div(2)), - } -} - -func (p *Person) MoveTo(destination Point, blocked map[Point]bool) { - p.move = NewMove(p.Position, destination, blocked) -} - -func (p *Person) Update() { - p.Position = p.move.Update(p.Position, 10) -} diff --git a/selection.go b/selection.go new file mode 100644 index 0000000..9b6dbb2 --- /dev/null +++ b/selection.go @@ -0,0 +1,34 @@ +package main + +import ( + "github.com/hajimehoshi/ebiten/v2" + "golang.org/x/exp/slog" +) + +type Selection struct { + IsSelected bool + SelectedImage *ebiten.Image +} + +func (e *Entity) SelectMultiple(cursor Point, selection GlobalSelection) { + if e.Selection.IsEnabled { + e.Selection.Value.IsSelected = false + selectionBounds := Rectangle{selection.Start, 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 Point, canBeSelected bool) bool { + if e.Selection.IsEnabled { + e.Selection.Value.IsSelected = false + if canBeSelected && cursor.In(e.Bounds()) { + e.Selection.Value.IsSelected = true + slog.Info("entity selected", slog.String("position", e.Position.Value.String())) + return true + } + } + return false +} |
