aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMartin Kagamino Lehoux <martin@lehoux.net>2023-09-15 08:10:04 +0200
committerMartin Kagamino Lehoux <martin@lehoux.net>2023-09-15 08:10:04 +0200
commit2d2a9fe8520f469897f49dc261289d7cc1c5d26c (patch)
tree79cf40889d6bf3cdaa26c158ec271d40f9692f6a
parentf7c05f1b9417eff446753b8fafc7829859521aa3 (diff)
refactor: Into ECS
-rw-r--r--entity.go39
-rw-r--r--image.go21
-rw-r--r--main.go110
-rw-r--r--move.go63
-rw-r--r--person.go49
-rw-r--r--selection.go34
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
+}
diff --git a/main.go b/main.go
index 3042b06..10bc3c9 100644
--- a/main.go
+++ b/main.go
@@ -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)
diff --git a/move.go b/move.go
index d6c0707..2135aaf 100644
--- a/move.go
+++ b/move.go
@@ -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
+}