Pull go-astar code into the repo, improve perf (#411)

Copied go-astar into d2common/d2astar, made a few optimizations.  Runs
roughly 30% faster according to my benchmarking.

Added a `maxCost` param to prevent searching the entire map for a path.
This probably needs tweaked a bit, but follows the original game more
closely.

Co-authored-by: Nicholas Eden <neden@zigzagame.com>
This commit is contained in:
nicholas-eden 2020-06-22 23:04:17 -07:00 committed by GitHub
parent fe0fa6f14d
commit dd21809288
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 867 additions and 16 deletions

153
d2common/d2astar/README.md Normal file
View File

@ -0,0 +1,153 @@
D2 A*
========
**A\* pathfinding implementation for OpenDiablo2**
***Forked from [go-astar](https://github.com/beefsack/go-astar)***
Changes
-------
* Used [sync.Pool](https://golang.org/pkg/sync/#Pool) to reuse objects created during pathfinding. This improves performance by roughly 30% by reducing allocations.
* Added a check on the target for neighbors to identify if the user clicked an inaccessible area.
* Added a max cost to prevent searching the entire region for a path.
TODO
------
* Evaluate bi-directional A*, specifically if it would more quickly identify if the user clicked an in inaccessible area (such as an island).
The [A\* pathfinding algorithm](http://en.wikipedia.org/wiki/A*_search_algorithm) is a pathfinding algorithm noted for its performance and accuracy and is commonly used in game development. It can be used to find short paths for any weighted graph.
A fantastic overview of A\* can be found at [Amit Patel's Stanford website](http://theory.stanford.edu/~amitp/GameProgramming/AStarComparison.html).
Examples
--------
The following crude examples were taken directly from the automated tests. Please see `path_test.go` for more examples.
### Key
* `.` - Plain (movement cost 1)
* `~` - River (movement cost 2)
* `M` - Mountain (movement cost 3)
* `X` - Blocker, unable to move through
* `F` - From / start position
* `T` - To / goal position
* `●` - Calculated path
### Straight line
```
.....~...... .....~......
.....MM..... .....MM.....
.F........T. -> .●●●●●●●●●●.
....MMM..... ....MMM.....
............ ............
```
### Around a mountain
```
.....~...... .....~......
.....MM..... .....MM.....
.F..MMMM..T. -> .●●●MMMM●●●.
....MMM..... ...●MMM●●...
............ ...●●●●●....
```
### Blocked path
```
............
.........XXX
.F.......XTX -> No path
.........XXX
............
```
### Maze
```
FX.X........ ●X.X●●●●●●..
.X...XXXX.X. ●X●●●XXXX●X.
.X.X.X....X. -> ●X●X.X●●●●X.
...X.X.XXXXX ●●●X.X●XXXXX
.XX..X.....T .XX..X●●●●●●
```
### Mountain climber
```
..F..M...... ..●●●●●●●●●.
.....MM..... .....MM...●.
....MMMM..T. -> ....MMMM..●.
....MMM..... ....MMM.....
............ ............
```
### River swimmer
```
.....~...... .....~......
.....~...... ....●●●.....
.F...X...T.. -> .●●●●X●●●●..
.....M...... .....M......
.....M...... .....M......
```
Usage
-----
### Import the package
```go
import "github.com/beefsack/go-astar"
```
### Implement Pather interface
An example implementation is done for the tests in `path_test.go` for the Tile type.
The `PathNeighbors` method should return a slice of the direct neighbors.
The `PathNeighborCost` method should calculate an exact movement cost for direct neighbors.
The `PathEstimatedCost` is a heuristic method for estimating the distance between arbitrary tiles. The examples in the test files use [Manhattan distance](http://en.wikipedia.org/wiki/Taxicab_geometry) to estimate orthogonal distance between tiles.
```go
type Tile struct{}
func (t *Tile) PathNeighbors() []astar.Pather {
return []astar.Pather{
t.Up(),
t.Right(),
t.Down(),
t.Left(),
}
}
func (t *Tile) PathNeighborCost(to astar.Pather) float64 {
return to.MovementCost
}
func (t *Tile) PathEstimatedCost(to astar.Pather) float64 {
return t.ManhattanDistance(to)
}
```
### Call Path function
```go
// t1 and t2 are *Tile objects from inside the world.
path, distance, found := astar.Path(t1, t2)
if !found {
log.Println("Could not find path")
}
// path is a slice of Pather objects which you can cast back to *Tile.
```
Authors
-------
Michael Alexander <beefsack@gmail.com>
Robin Ranjit Chauhan <robin@pathwayi.com>

154
d2common/d2astar/astar.go Normal file
View File

@ -0,0 +1,154 @@
package d2astar
import (
"container/heap"
"fmt"
"sync"
)
var nodePool *sync.Pool
var nodeMapPool *sync.Pool
var priorityQueuePool *sync.Pool
func init() {
nodePool = &sync.Pool {
New: func()interface{} {
return &node{}
},
}
nodeMapPool = &sync.Pool {
New: func()interface{} {
return make(nodeMap, 128)
},
}
priorityQueuePool = &sync.Pool {
New: func()interface{} {
return priorityQueue{}
},
}
}
// astar is an A* pathfinding implementation.
// Pather is an interface which allows A* searching on arbitrary objects which
// can represent a weighted graph.
type Pather interface {
// PathNeighbors returns the direct neighboring nodes of this node which
// can be pathed to.
PathNeighbors() []Pather
// PathNeighborCost calculates the exact movement cost to neighbor nodes.
PathNeighborCost(to Pather) float64
// PathEstimatedCost is a heuristic method for estimating movement costs
// between non-adjacent nodes.
PathEstimatedCost(to Pather) float64
}
// node is a wrapper to store A* data for a Pather node.
type node struct {
pather Pather
cost float64
rank float64
parent *node
open bool
closed bool
index int
}
func (n *node) reset () {
n.pather = nil
n.cost = 0
n.rank = 0
n.parent = nil
n.open = false
n.closed = false
n.index = 0
}
// nodeMap is a collection of nodes keyed by Pather nodes for quick reference.
type nodeMap map[Pather]*node
// get gets the Pather object wrapped in a node, instantiating if required.
func (nm nodeMap) get(p Pather) *node {
n, ok := nm[p]
if !ok {
n = nodePool.Get().(*node)
n.pather = p
nm[p] = n
}
return n
}
// Path calculates a short path and the distance between the two Pather nodes.
//
// If no path is found, found will be false.
func Path(from, to Pather, maxCost float64) (path []Pather, distance float64, found bool) {
// Quick escape for inaccessible areas.
toNeighbors := to.PathNeighbors()
if len(toNeighbors) == 0 {
return nil, 0, false
}
nm := nodeMapPool.Get().(nodeMap)
nq := priorityQueuePool.Get().(priorityQueue)
defer func() {
for k, v := range nm {
v.reset()
nodePool.Put(v)
delete(nm, k)
}
nq = nq[0:0]
nodeMapPool.Put(nm)
priorityQueuePool.Put(nq)
}()
heap.Init(&nq)
fromNode := nm.get(from)
fromNode.open = true
heap.Push(&nq, fromNode)
for {
if nq.Len() == 0 {
// There's no path, return found false.
return
}
current := heap.Pop(&nq).(*node)
current.open = false
current.closed = true
if current == nm.get(to) {
// Found a path to the goal.
p := make([]Pather, 0, 16)
curr := current
for curr != nil {
p = append(p, curr.pather)
curr = curr.parent
}
return p, current.cost, true
}
for _, neighbor := range current.pather.PathNeighbors() {
cost := current.cost + current.pather.PathNeighborCost(neighbor)
if cost > maxCost {
fmt.Println("Canceling path")
continue
}
neighborNode := nm.get(neighbor)
if cost < neighborNode.cost {
if neighborNode.open {
heap.Remove(&nq, neighborNode.index)
}
neighborNode.open = false
neighborNode.closed = false
}
if !neighborNode.open && !neighborNode.closed {
neighborNode.cost = cost
neighborNode.open = true
neighborNode.rank = cost + neighbor.PathEstimatedCost(to)
neighborNode.parent = current
heap.Push(&nq, neighborNode)
}
}
}
}

View File

@ -0,0 +1,96 @@
package d2astar
// goreland_example.go implements implements Pather for
// the sake of testing. This functionality forms the back end for
// goreland_test.go, and serves as an example for how to use A* for a graph.
// The Magical World of Goreland, is where Ted Stevens and Al Gore are from.
//
// It is composed of Big Trucks, and a Series of Tubes!
//
// Ok, it is basically just a Graph.
// Nodes are called "Trucks" and they have X, Y coordinates
// Edges are called "Tubes", they connect Trucks, and they have a cost
//
// The key differences between this example and the Tile world:
// 1) There is no grid. Trucks have arbitrary coordinates.
// 2) Edges are not implied by the grid positions. Instead edges are explicitly
// modelled as Tubes.
//
// The key similarities between this example and the Tile world:
// 1) They both use Manhattan distance as their heuristic
// 2) Both implement Pather
type Goreland struct {
// trucks map[int]*Truck // not needed really
}
type Tube struct {
from *Truck
to *Truck
Cost float64
}
// A Truck is a Truck in a grid which implements Grapher.
type Truck struct {
// X and Y are the coordinates of the truck.
X, Y int
// array of tubes going to other trucks
out_to []Tube
label string
}
// PathNeighbors returns the neighbors of the Truck
func (t *Truck) PathNeighbors() []Pather {
neighbors := []Pather{}
for _, tube_element := range t.out_to {
neighbors = append(neighbors, Pather(tube_element.to))
}
return neighbors
}
// PathNeighborCost returns the cost of the tube leading to Truck.
func (t *Truck) PathNeighborCost(to Pather) float64 {
for _, tube_element := range (t).out_to {
if Pather((tube_element.to)) == to {
return tube_element.Cost
}
}
return 10000000
}
// PathEstimatedCost uses Manhattan distance to estimate orthogonal distance
// between non-adjacent nodes.
func (t *Truck) PathEstimatedCost(to Pather) float64 {
toT := to.(*Truck)
absX := toT.X - t.X
if absX < 0 {
absX = -absX
}
absY := toT.Y - t.Y
if absY < 0 {
absY = -absY
}
r := float64(absX + absY)
return r
}
// RenderPath renders a path on top of a Goreland world.
func (w Goreland) RenderPath(path []Pather) string {
s := ""
for _, p := range path {
pT := p.(*Truck)
s = pT.label + " " + s
}
return s
}

View File

@ -0,0 +1,86 @@
package d2astar
import (
"math"
"testing"
)
func AddTruck(x int, y int, label string) *Truck {
t1 := new(Truck)
t1.X = x
t1.Y = y
t1.label = label
return t1
}
func AddTube(t1, t2 *Truck, cost float64) *Tube {
tube1 := new(Tube)
tube1.Cost = cost
tube1.from = t1
tube1.to = t2
t1.out_to = append(t1.out_to, *tube1)
t2.out_to = append(t2.out_to, *tube1)
return tube1
}
// Consider a world with Nodes (Trucks) and Edges (Tubes), Edges each having a cost
//
// E
// /|
// / |
// S--M
//
// S=Start at (0,0)
// E=End at (1,1)
// M=Middle at (0,1)
//
// S-M and M-E are clean clear tubes. cost: 1
//
// S-E is either:
//
// 1) TestGraphPath_ShortDiagonal : diagonal is a nice clean clear Tube , cost: 1.9
// Solver should traverse the bridge.
// Expect solution: Start, End Total cost: 1.9
//
// 1) TestGraphPath_LongDiagonal : diagonal is a Tube plugged full of
// "enormous amounts of material"!, cost: 10000.
// Solver should avoid the plugged tube.
// Expect solution Start,Middle,End Total cost: 2.0
func createGorelandGraphPath_Diagonal(t *testing.T, diagonal_cost float64, expectedDist float64) {
world := new(Goreland)
tr_start := AddTruck(0, 0, "Start")
tr_mid := AddTruck(0, 1, "Middle")
tr_end := AddTruck(1, 1, "End")
AddTube(tr_start, tr_end, diagonal_cost)
AddTube(tr_start, tr_mid, 1)
AddTube(tr_mid, tr_end, 1)
t.Logf("Goreland. Diagonal cost: %v\n\n", diagonal_cost)
p, dist, found := Path(tr_start, tr_end, math.MaxFloat64)
if !found {
t.Log("Could not find a path")
} else {
t.Logf("Resulting path\n%s", world.RenderPath(p))
}
if !found && expectedDist >= 0 {
t.Fatal("Could not find a path")
}
if found && dist != expectedDist {
t.Fatalf("Expected dist to be %v but got %v", expectedDist, dist)
}
}
func TestGraphPaths_ShortDiagonal(t *testing.T) {
createGorelandGraphPath_Diagonal(t, 1.9, 1.9)
}
func TestGraphPaths_LongDiagonal(t *testing.T) {
createGorelandGraphPath_Diagonal(t, 10000, 2.0)
}

View File

@ -0,0 +1,132 @@
package d2astar
// path_test.go contains the high level tests without the testing
// implementation. testPath is used to check the calculated path distance is
// what we're expecting.
import (
"math"
"testing"
)
// testPath takes a string encoded world, decodes it, calculates a path and
// checks the expected distance matches. An expectedDist of -1 expects that no
// path will be found.
func testPath(worldInput string, t *testing.T, expectedDist float64) {
world := ParseWorld(worldInput)
t.Logf("Input world\n%s", world.RenderPath([]Pather{}))
p, dist, found := Path(world.From(), world.To(), math.MaxFloat64)
if !found {
t.Log("Could not find a path")
} else {
t.Logf("Resulting path\n%s", world.RenderPath(p))
}
if !found && expectedDist >= 0 {
t.Fatal("Could not find a path")
}
if found && dist != expectedDist {
t.Fatalf("Expected dist to be %v but got %v", expectedDist, dist)
}
}
// TestStraightLine checks that having no obstacles results in a straight line
// path.
func TestStraightLine(t *testing.T) {
testPath(`
.....~......
.....MM.....
.F........T.
....MMM.....
............
`, t, 9)
}
// TestPathAroundMountain checks that having a round mountain in the path
// results in a path around the mountain.
func TestPathAroundMountain(t *testing.T) {
testPath(`
.....~......
.....MM.....
.F..MMMM..T.
....MMM.....
............
`, t, 13)
}
// TestBlocked checks that no path is returned when there is no possible path.
func TestBlocked(t *testing.T) {
testPath(`
............
.........XXX
.F.......XTX
.........XXX
............
`, t, -1)
}
// TestMaze checks that paths can double back on themselves to reach the goal.
func TestMaze(t *testing.T) {
testPath(`
FX.X........
.X...XXXX.X.
.X.X.X....X.
...X.X.XXXXX
.XX..X.....T
`, t, 27)
}
// TestMountainClimber checks that a path will choose to go over a mountain,
// which has a movement penalty of 3, if it's faster than going around the
// mountain.
func TestMountainClimber(t *testing.T) {
testPath(`
..F..M......
.....MM.....
....MMMM..T.
....MMM.....
............
`, t, 12)
}
// TestRiverSwimmer checks that the path will prefer to cross a river, which
// has a movement penalty of 2, over a mountain which has a movement penalty of
// 3.
func TestRiverSwimmer(t *testing.T) {
testPath(`
.....~......
.....~......
.F...X...T..
.....M......
.....M......
`, t, 11)
}
func BenchmarkLarge(b *testing.B) {
world := ParseWorld(`
F............................~.................................................
.............................~.................................................
........M...........X........~.................................................
.......MMM.........X.........~~................................................
........MM........X...........~................................................
.......MM........X............~................................................
................X.............~................................................
...............X..............~~...............................................
..............X................~...............................................
.............X.................~...X...............~...........................
............X.......................X..............~...........................
...........X.........................X.............~...........................
..........X..................~........X............~...........................
.........X...................~.........X...........~...........................
.............................~..........X..........~...............XXXXXXXXXXXX
............................~............X..........~..............X...X...X...
............................~.............X.........~......MMM.....X.X.X.X.X.X.
............................~..............X........~......MM......X.X.X.X.X.X.
............................~...............X.......~....MMMM......X.X.X.X.X.X.
...........................~.................X.....~......MMM......X.X.X.X.X.X.
..............................................X....~.......MM......X.X.X.X.X.X.
...............................................X...~.......M.........X...X...XT
`)
for i := 0; i < b.N; i++ {
Path(world.From(), world.To(), math.MaxFloat64)
}
}

View File

@ -0,0 +1,198 @@
package d2astar
// pather_test.go implements a basic world and tiles that implement Pather for
// the sake of testing. This functionality forms the back end for
// path_test.go, and serves as an example for how to use A* for a grid.
import (
"fmt"
"strings"
)
// Kind* constants refer to tile kinds for input and output.
const (
// KindPlain (.) is a plain tile with a movement cost of 1.
KindPlain = iota
// KindRiver (~) is a river tile with a movement cost of 2.
KindRiver
// KindMountain (M) is a mountain tile with a movement cost of 3.
KindMountain
// KindBlocker (X) is a tile which blocks movement.
KindBlocker
// KindFrom (F) is a tile which marks where the path should be calculated
// from.
KindFrom
// KindTo (T) is a tile which marks the goal of the path.
KindTo
// KindPath (●) is a tile to represent where the path is in the output.
KindPath
)
// KindRunes map tile kinds to output runes.
var KindRunes = map[int]rune{
KindPlain: '.',
KindRiver: '~',
KindMountain: 'M',
KindBlocker: 'X',
KindFrom: 'F',
KindTo: 'T',
KindPath: '●',
}
// RuneKinds map input runes to tile kinds.
var RuneKinds = map[rune]int{
'.': KindPlain,
'~': KindRiver,
'M': KindMountain,
'X': KindBlocker,
'F': KindFrom,
'T': KindTo,
}
// KindCosts map tile kinds to movement costs.
var KindCosts = map[int]float64{
KindPlain: 1.0,
KindFrom: 1.0,
KindTo: 1.0,
KindRiver: 2.0,
KindMountain: 3.0,
}
// A Tile is a tile in a grid which implements Pather.
type Tile struct {
// Kind is the kind of tile, potentially affecting movement.
Kind int
// X and Y are the coordinates of the tile.
X, Y int
// W is a reference to the World that the tile is a part of.
W World
}
// PathNeighbors returns the neighbors of the tile, excluding blockers and
// tiles off the edge of the board.
func (t *Tile) PathNeighbors() []Pather {
neighbors := []Pather{}
for _, offset := range [][]int{
{-1, 0},
{1, 0},
{0, -1},
{0, 1},
} {
if n := t.W.Tile(t.X+offset[0], t.Y+offset[1]); n != nil &&
n.Kind != KindBlocker {
neighbors = append(neighbors, n)
}
}
return neighbors
}
// PathNeighborCost returns the movement cost of the directly neighboring tile.
func (t *Tile) PathNeighborCost(to Pather) float64 {
toT := to.(*Tile)
return KindCosts[toT.Kind]
}
// PathEstimatedCost uses Manhattan distance to estimate orthogonal distance
// between non-adjacent nodes.
func (t *Tile) PathEstimatedCost(to Pather) float64 {
toT := to.(*Tile)
absX := toT.X - t.X
if absX < 0 {
absX = -absX
}
absY := toT.Y - t.Y
if absY < 0 {
absY = -absY
}
return float64(absX + absY)
}
// World is a two dimensional map of Tiles.
type World map[int]map[int]*Tile
// Tile gets the tile at the given coordinates in the world.
func (w World) Tile(x, y int) *Tile {
if w[x] == nil {
return nil
}
return w[x][y]
}
// SetTile sets a tile at the given coordinates in the world.
func (w World) SetTile(t *Tile, x, y int) {
if w[x] == nil {
w[x] = map[int]*Tile{}
}
w[x][y] = t
t.X = x
t.Y = y
t.W = w
}
// FirstOfKind gets the first tile on the board of a kind, used to get the from
// and to tiles as there should only be one of each.
func (w World) FirstOfKind(kind int) *Tile {
for _, row := range w {
for _, t := range row {
if t.Kind == kind {
return t
}
}
}
return nil
}
// From gets the from tile from the world.
func (w World) From() *Tile {
return w.FirstOfKind(KindFrom)
}
// To gets the to tile from the world.
func (w World) To() *Tile {
return w.FirstOfKind(KindTo)
}
// RenderPath renders a path on top of a world.
func (w World) RenderPath(path []Pather) string {
width := len(w)
if width == 0 {
return ""
}
height := len(w[0])
pathLocs := map[string]bool{}
for _, p := range path {
pT := p.(*Tile)
pathLocs[fmt.Sprintf("%d,%d", pT.X, pT.Y)] = true
}
rows := make([]string, height)
for x := 0; x < width; x++ {
for y := 0; y < height; y++ {
t := w.Tile(x, y)
r := ' '
if pathLocs[fmt.Sprintf("%d,%d", x, y)] {
r = KindRunes[KindPath]
} else if t != nil {
r = KindRunes[t.Kind]
}
rows[y] += string(r)
}
}
return strings.Join(rows, "\n")
}
// ParseWorld parses a textual representation of a world into a world map.
func ParseWorld(input string) World {
w := World{}
for y, row := range strings.Split(strings.TrimSpace(input), "\n") {
for x, raw := range row {
kind, ok := RuneKinds[raw]
if !ok {
kind = KindBlocker
}
w.SetTile(&Tile{
Kind: kind,
}, x, y)
}
}
return w
}

View File

@ -0,0 +1,35 @@
package d2astar
// A priorityQueue implements heap.Interface and holds Nodes. The
// priorityQueue is used to track open nodes by rank.
type priorityQueue []*node
func (pq priorityQueue) Len() int {
return len(pq)
}
func (pq priorityQueue) Less(i, j int) bool {
return pq[i].rank < pq[j].rank
}
func (pq priorityQueue) Swap(i, j int) {
pq[i], pq[j] = pq[j], pq[i]
pq[i].index = i
pq[j].index = j
}
func (pq *priorityQueue) Push(x interface{}) {
n := len(*pq)
no := x.(*node)
no.index = n
*pq = append(*pq, no)
}
func (pq *priorityQueue) Pop() interface{} {
old := *pq
n := len(old)
no := old[n-1]
no.index = -1
*pq = old[0 : n-1]
return no
}

View File

@ -1,6 +1,6 @@
package d2common
import "github.com/beefsack/go-astar"
import "github.com/OpenDiablo2/OpenDiablo2/d2common/d2astar"
type PathTile struct {
Walkable bool
@ -8,8 +8,8 @@ type PathTile struct {
X, Y float64
}
func (t *PathTile) PathNeighbors() []astar.Pather {
result := make([]astar.Pather, 0)
func (t *PathTile) PathNeighbors() []d2astar.Pather {
result := make([]d2astar.Pather, 0, 8)
if t.Up != nil {
result = append(result, t.Up)
}
@ -38,11 +38,11 @@ func (t *PathTile) PathNeighbors() []astar.Pather {
return result
}
func (t *PathTile) PathNeighborCost(to astar.Pather) float64 {
func (t *PathTile) PathNeighborCost(to d2astar.Pather) float64 {
return 1 // No cost specifics currently...
}
func (t *PathTile) PathEstimatedCost(to astar.Pather) float64 {
func (t *PathTile) PathEstimatedCost(to d2astar.Pather) float64 {
toT := to.(*PathTile)
absX := toT.X - t.X
if absX < 0 {

View File

@ -4,8 +4,8 @@ import (
"math"
"github.com/OpenDiablo2/OpenDiablo2/d2common"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2astar"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/beefsack/go-astar"
)
func (m *MapEngine) RegenerateWalkPaths() {
@ -69,7 +69,7 @@ func (m *MapEngine) RegenerateWalkPaths() {
}
// Finds a walkable path between two points
func (m *MapEngine) PathFind(startX, startY, endX, endY float64) (path []astar.Pather, distance float64, found bool) {
func (m *MapEngine) PathFind(startX, startY, endX, endY float64) (path []d2astar.Pather, distance float64, found bool) {
startTileX := int(math.Floor(startX))
startTileY := int(math.Floor(startY))
if !m.TileExists(startTileX, startTileY) {
@ -97,7 +97,7 @@ func (m *MapEngine) PathFind(startX, startY, endX, endY float64) (path []astar.P
}
endNode := &m.walkMesh[endNodeIndex]
path, distance, found = astar.Path(endNode, startNode)
path, distance, found = d2astar.Path(endNode, startNode, 80)
if path != nil {
path = path[1:]
}

View File

@ -4,8 +4,8 @@ import (
"math"
"github.com/OpenDiablo2/OpenDiablo2/d2common"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2astar"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2render"
"github.com/beefsack/go-astar"
)
type MapEntity interface {
@ -25,7 +25,7 @@ type mapEntity struct {
TargetX float64
TargetY float64
Speed float64
path []astar.Pather
path []d2astar.Pather
done func()
directioner func(angle float64)
@ -44,11 +44,11 @@ func createMapEntity(x, y int) mapEntity {
subcellX: 1 + math.Mod(locX, 5),
subcellY: 1 + math.Mod(locY, 5),
Speed: 6,
path: []astar.Pather{},
path: []d2astar.Pather{},
}
}
func (m *mapEntity) SetPath(path []astar.Pather, done func()) {
func (m *mapEntity) SetPath(path []d2astar.Pather, done func()) {
m.path = path
m.done = done
}
@ -112,7 +112,7 @@ func (m *mapEntity) Step(tickTime float64) {
if len(m.path) > 1 {
m.path = m.path[1:]
} else {
m.path = []astar.Pather{}
m.path = []d2astar.Pather{}
}
} else {
m.LocationX = m.TargetX

1
go.mod
View File

@ -6,7 +6,6 @@ require (
github.com/JoshVarga/blast v0.0.0-20180421040937-681c804fb9f0
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d // indirect
github.com/beefsack/go-astar v0.0.0-20171024231011-f324bbb0d6f7
github.com/go-restruct/restruct v0.0.0-20191227155143-5734170a48a1
github.com/hajimehoshi/ebiten v1.11.2
github.com/pkg/profile v1.5.0

2
go.sum
View File

@ -5,8 +5,6 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafo
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/beefsack/go-astar v0.0.0-20171024231011-f324bbb0d6f7 h1:dX/NcR4V4sY+xio5sjMUUaBfmXz/7UH4R7S//oVPqhY=
github.com/beefsack/go-astar v0.0.0-20171024231011-f324bbb0d6f7/go.mod h1:Cu3t5VeqE8kXjUBeNXWQprfuaP5UCIc5ggGjgMx9KFc=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4 h1:WtGNWLvXpe6ZudgnXrq0barxBImvnnJoMEhXAzcbM0I=