Add pathfinding system

This commit is contained in:
Trevor Slocum 2022-11-15 00:07:21 -08:00
parent 60fc3a544c
commit 868cfe9aa3
9 changed files with 246 additions and 48 deletions

View File

@ -33,7 +33,7 @@ func NewGame() (*Game, error) {
once := gohan.NewEntity()
once.AddComponent(&component.Once{})
world.Map = world.NewGameMap(time.Now().UnixNano(), world.MapSize, world.MapSize)
world.Map = world.NewGameMap(time.Now().UnixNano())
return g, nil
}

1
go.mod
View File

@ -4,6 +4,7 @@ go 1.19
require (
code.rocketnine.space/tslocum/gohan v1.0.0
github.com/beefsack/go-astar v0.0.0-20200827232313-4ecf9e304482
github.com/hajimehoshi/ebiten/v2 v2.4.12
)

2
go.sum
View File

@ -1,6 +1,8 @@
code.rocketnine.space/tslocum/gohan v1.0.0 h1:WBcJq7nVfmr1EB8bew6xWlB5Q1714yWJ3a9/q6aBBrY=
code.rocketnine.space/tslocum/gohan v1.0.0/go.mod h1:12yOt5Ygl/RVwnnZSVZRuS1W6gCaHJgezcvg8+THk10=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/beefsack/go-astar v0.0.0-20200827232313-4ecf9e304482 h1:p4g4uok3+r6Tg6fxXEQUAcMAX/WdK6WhkQW9s0jaT7k=
github.com/beefsack/go-astar v0.0.0-20200827232313-4ecf9e304482/go.mod h1:Cu3t5VeqE8kXjUBeNXWQprfuaP5UCIc5ggGjgMx9KFc=
github.com/ebitengine/purego v0.0.0-20220905075623-aeed57cda744/go.mod h1:Eh8I3yvknDYZeCuXH9kRNaPuHEwvXDCk378o9xszmHg=
github.com/ebitengine/purego v0.1.0 h1:vAEo1FvmbjA050QKsbDbcHj03hhMMvh0fmr9LSehpnU=
github.com/ebitengine/purego v0.1.0/go.mod h1:Eh8I3yvknDYZeCuXH9kRNaPuHEwvXDCk378o9xszmHg=

View File

@ -4,6 +4,7 @@ import (
"code.rocketnine.space/tslocum/commandeuropa/component"
"code.rocketnine.space/tslocum/commandeuropa/world"
"code.rocketnine.space/tslocum/gohan"
"github.com/beefsack/go-astar"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/inpututil"
)
@ -86,13 +87,25 @@ func (r *HandleInput) Update(e gohan.Entity) error {
}
if inpututil.IsKeyJustPressed(ebiten.KeyV) && ebiten.IsKeyPressed(ebiten.KeyShift) {
if world.Debug == 0 {
world.Debug = 1
} else {
world.Debug++
if world.Debug > 2 {
world.Debug = 0
}
}
if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) {
tx, ty := world.ScreenCoordinatesToPath(world.CursorX, world.CursorY)
from := world.Map[world.PathTileIndex(1, 1)]
to := world.Map[world.PathTileIndex(tx, ty)]
path, _, _ := astar.Path(from, to)
world.MapPath = nil
for _, p := range path {
t := p.(world.Tile)
world.MapPath = append(world.MapPath, [2]int{t.X, t.Y})
}
}
return nil
}

View File

@ -2,6 +2,7 @@ package system
import (
"fmt"
"image"
"image/color"
"code.rocketnine.space/tslocum/commandeuropa/component"
@ -39,6 +40,46 @@ func (s *RenderDebug) Draw(e gohan.Entity, screen *ebiten.Image) error {
s.Initialize()
}
if world.Debug > 1 {
for x := 0; x < world.MapSize; x++ {
for y := 0; y < world.MapSize; y++ {
for ox := 0; ox < 4; ox++ {
for oy := 0; oy < 4; oy++ {
px, py := x*4+ox, y*4+oy
fillColor := color.RGBA{255, 0, 0, 255}
if world.Map[world.PathTileIndex(px, py)].Walkable {
fillColor = color.RGBA{0, 255, 0, 255}
}
drawX, drawY := world.PathCoordinatesToScreen(px, py)
// Skip drawing off-screen tiles.
if world.PixelCoordinatesOffScreen(drawX, drawY) {
continue
}
screen.SubImage(image.Rect(drawX, drawY, drawX+4*world.CamScale, drawY+4*world.CamScale)).(*ebiten.Image).Fill(fillColor)
}
}
}
}
if len(world.MapPath) > 0 {
for _, xy := range world.MapPath {
x, y := xy[0], xy[1]
drawX, drawY := world.PathCoordinatesToScreen(x, y)
// Skip drawing off-screen tiles.
if world.PixelCoordinatesOffScreen(drawX, drawY) {
continue
}
fillColor := color.RGBA{255, 255, 255, 255}
screen.SubImage(image.Rect(drawX, drawY, drawX+4*world.CamScale, drawY+4*world.CamScale)).(*ebiten.Image).Fill(fillColor)
}
}
}
s.debugImg.Fill(color.RGBA{0, 0, 0, 80})
ebitenutil.DebugPrintAt(s.debugImg, fmt.Sprintf("ENT %d\nUPD %d\nDRA %d\nTPS %0.0f\nFPS %0.0f", gohan.CurrentEntities(), gohan.CurrentUpdates(), gohan.CurrentDraws(), ebiten.CurrentTPS(), ebiten.CurrentFPS()), 2, 0)
screen.DrawImage(s.debugImg, s.op)

View File

@ -30,30 +30,24 @@ func (r *RenderEnvironment) Draw(e gohan.Entity, screen *ebiten.Image) error {
r.Initialize()
}
x, y := -1, 0
for _, t := range world.Map {
x++
if x == world.MapSize {
y++
x = 0
for x := 0; x < world.MapSize; x++ {
for y := 0; y < world.MapSize; y++ {
i := world.TileIndex(x, y)
t := world.Map[i]
if t.Sprite == nil {
continue
}
drawX, drawY := world.LevelCoordinatesToScreen(x, y)
// Skip drawing off-screen tiles.
if world.PixelCoordinatesOffScreen(drawX, drawY) {
continue
}
r.renderTile(t, x, y, screen)
}
drawX, drawY := world.LevelCoordinatesToScreen(x, y)
// Skip drawing off-screen tiles.
padding := world.TileSize * world.CamScale
width, height := world.TileSize*world.CamScale, world.TileSize*world.CamScale
left := drawX
right := drawX + width
top := drawY
bottom := drawY + height
if (left < -padding || left > world.ScreenWidth+padding) || (top < -padding || top > world.ScreenHeight+padding) ||
(right < -padding || right > world.ScreenWidth+padding) || (bottom < -padding || bottom > world.ScreenHeight+padding) {
continue
}
r.renderTile(t, x, y, screen)
}
highlightX, highlightY := -1, -1
@ -81,7 +75,7 @@ func (r *RenderEnvironment) Draw(e gohan.Entity, screen *ebiten.Image) error {
return nil
}
func (r *RenderEnvironment) renderTile(t world.MapTile, x int, y int, target *ebiten.Image) int {
func (r *RenderEnvironment) renderTile(t world.Tile, x int, y int, target *ebiten.Image) int {
r.op.GeoM.Reset()
// Move to current position.

View File

@ -7,6 +7,10 @@ import (
"github.com/hajimehoshi/ebiten/v2"
)
const MapSize = 128
const tileDivisions = 4
const numTiles = MapSize * MapSize * tileDivisions * tileDivisions
func colorTile(r, g, b uint8) *ebiten.Image {
img := ebiten.NewImage(TileSize, TileSize)
img.Fill(color.RGBA{r, g, b, 255})
@ -35,16 +39,36 @@ var (
redTiles = []*ebiten.Image{redTile1, redTile1, redTile2, redTile3}
)
type MapTile struct {
Sprite *ebiten.Image
}
func NewGameMap(seed int64, w, h int) []MapTile {
func NewGameMap(seed int64) []Tile {
r := rand.New(rand.NewSource(seed))
tiles := make([]MapTile, w*h)
for i := range tiles {
tiles[i].Sprite = lightBlueTiles[r.Intn(len(lightBlueTiles))]
tiles := make([]Tile, numTiles)
{
tx, ty := 0, 0
for i := range tiles {
tiles[i].X, tiles[i].Y = tx, ty
tiles[i].Walkable = true
tx++
if tx == MapSize*4 {
tx = 0
ty++
}
}
}
for x := 0; x < MapSize; x++ {
for y := 0; y < MapSize; y++ {
index := TileIndex(x, y)
tiles[index].Sprite = lightBlueTiles[r.Intn(len(lightBlueTiles))]
}
}
setWalkable := func(tx, ty int, walkable bool) {
tileIndexes := PathTilesAtMapTile(tx, ty)
for _, index := range tileIndexes {
tiles[index].Walkable = walkable
}
}
numMediumBlue := r.Intn(10) + 7
@ -54,10 +78,12 @@ func NewGameMap(seed int64, w, h int) []MapTile {
bSizeY := r.Intn(TileSize) + 7
for offsetX := 0; offsetX < bSizeX; offsetX++ {
for offsetY := 0; offsetY < bSizeY; offsetY++ {
index := TileIndex(bx+offsetX, by+offsetY)
tx, ty := bx+offsetX, by+offsetY
index := TileIndex(tx, ty)
if index == -1 {
continue
}
setWalkable(tx, ty, false)
tiles[index].Sprite = mediumBlueTiles[r.Intn(len(mediumBlueTiles))]
}
}
@ -70,10 +96,12 @@ func NewGameMap(seed int64, w, h int) []MapTile {
bSizeY := r.Intn(12) + 7
for offsetX := 0; offsetX < bSizeX; offsetX++ {
for offsetY := 0; offsetY < bSizeY; offsetY++ {
index := TileIndex(bx+offsetX, by+offsetY)
tx, ty := bx+offsetX, by+offsetY
index := TileIndex(tx, ty)
if index == -1 {
continue
}
setWalkable(tx, ty, false)
tiles[index].Sprite = darkBlueTiles[r.Intn(len(darkBlueTiles))]
}
}
@ -86,15 +114,17 @@ func NewGameMap(seed int64, w, h int) []MapTile {
bSize := rand.Intn(14) + 32
for offset := 0; offset < bSize; offset++ {
var index int
var tx, ty int
if vertical {
index = TileIndex(bx, by+offset)
tx, ty = bx, by+offset
} else {
index = TileIndex(bx+offset, by)
tx, ty = bx+offset, by
}
if index == -1 {
index := TileIndex(tx, ty)
if index == -1 || !tiles[index].Walkable {
continue
}
setWalkable(tx, ty, false)
tiles[index].Sprite = redTiles[r.Intn(len(redTiles))]
}
}
@ -102,9 +132,34 @@ func NewGameMap(seed int64, w, h int) []MapTile {
return tiles
}
func PathTileIndex(x, y int) int {
if x < 0 || y < 0 || x >= MapSize*tileDivisions || y >= MapSize*tileDivisions {
return -1
}
return y*MapSize*tileDivisions + x
}
func TileIndex(x, y int) int {
if x < 0 || y < 0 || x >= MapSize || y >= MapSize {
return -1
}
return y*MapSize + x
return y*tileDivisions*MapSize*tileDivisions + x*tileDivisions
}
func ValidXY(x, y int) bool {
return x >= 0 && y >= 0 && x < MapSize && y < MapSize
}
func PathTilesAtMapTile(tx, ty int) []int {
pi := TileIndex(tx, ty)
tiles := make([]int, 16)
i := 0
for offsetY := 0; offsetY < 4; offsetY++ {
for offsetX := 0; offsetX < 4; offsetX++ {
tiles[i] = pi + (offsetY * MapSize * 4) + offsetX
i++
}
}
return tiles
}

78
world/tile.go Normal file
View File

@ -0,0 +1,78 @@
package world
import (
"github.com/beefsack/go-astar"
"github.com/hajimehoshi/ebiten/v2"
)
const TileSize = 16
type Tile struct {
X int
Y int
Walkable bool
Sprite *ebiten.Image
}
func (t Tile) Up() int {
tx, ty := t.X, t.Y-1
return PathTileIndex(tx, ty)
}
func (t Tile) Down() int {
tx, ty := t.X, t.Y+1
return PathTileIndex(tx, ty)
}
func (t Tile) Left() int {
tx, ty := t.X-1, t.Y
return PathTileIndex(tx, ty)
}
func (t Tile) Right() int {
tx, ty := t.X+1, t.Y
return PathTileIndex(tx, ty)
}
func (t Tile) PathNeighbors() []astar.Pather {
var neighbors []astar.Pather
i := t.Up()
if i != -1 && Map[i].Walkable {
neighbors = append(neighbors, Map[i])
}
i = t.Down()
if i != -1 && Map[i].Walkable {
neighbors = append(neighbors, Map[i])
}
i = t.Left()
if i != -1 && Map[i].Walkable {
neighbors = append(neighbors, Map[i])
}
i = t.Right()
if i != -1 && Map[i].Walkable {
neighbors = append(neighbors, Map[i])
}
return neighbors
}
func (t Tile) PathNeighborCost(to astar.Pather) float64 {
toT := to.(Tile)
if !toT.Walkable {
return 0
}
return 1
}
func (t Tile) PathEstimatedCost(to astar.Pather) float64 {
toT := to.(Tile)
x := toT.X - t.X
if x < 0 {
x = -x
}
y := toT.Y - t.Y
if y < 0 {
y = -y
}
return float64(x + y)
}

View File

@ -8,10 +8,6 @@ import (
const TPS = 144
const TileSize = 16
const MapSize = 128
var (
ScreenWidth, ScreenHeight = 800, 600
@ -22,7 +18,9 @@ var (
CursorX, CursorY = 0, 0
Map []MapTile
Map []Tile
MapPath [][2]int
Debug int
@ -43,6 +41,22 @@ func LevelCoordinatesToScreen(x, y int) (int, int) {
return (x*TileSize-CamX)*CamScale + ScreenWidth/2, (y*TileSize-CamY)*CamScale + ScreenHeight/2
}
func PathCoordinatesToScreen(x, y int) (int, int) {
return (x*tileDivisions-CamX)*CamScale + ScreenWidth/2, (y*tileDivisions-CamY)*CamScale + ScreenHeight/2
}
func PixelCoordinatesOffScreen(x, y int) bool {
padding := TileSize * CamScale
left, right := x, x+TileSize*CamScale
top, bottom := y, y+TileSize*CamScale
return (left < -padding || left > ScreenWidth+padding) || (top < -padding || top > ScreenHeight+padding) ||
(right < -padding || right > ScreenWidth+padding) || (bottom < -padding || bottom > ScreenHeight+padding)
}
func ScreenCoordinatesToLevel(x, y int) (int, int) {
return (((x - ScreenWidth/2) / CamScale) + CamX) / TileSize, (((y - ScreenHeight/2) / CamScale) + CamY) / TileSize
}
func ScreenCoordinatesToPath(x, y int) (int, int) {
return (((x - ScreenWidth/2) / CamScale) + CamX) / tileDivisions, (((y - ScreenHeight/2) / CamScale) + CamY) / tileDivisions
}