From 329e2f0fabc9cc2c062008f8425e2255f8c8e6b5 Mon Sep 17 00:00:00 2001 From: nicholas-eden Date: Wed, 24 Jun 2020 05:09:00 -0700 Subject: [PATCH] Use closest path if path not found (#431) When clicking on the other side of a wall or in an an inaccessible area, route the player the closest possible node. This allows running along walls and matches the original closely. Co-authored-by: Nicholas Eden --- d2common/d2astar/README.md | 4 +- d2common/d2astar/astar.go | 55 ++++++++++++++++++--------- d2core/d2map/d2mapengine/walk_mesh.go | 8 +++- d2networking/d2client/game_client.go | 4 +- 4 files changed, 47 insertions(+), 24 deletions(-) diff --git a/d2common/d2astar/README.md b/d2common/d2astar/README.md index 7919d490..4294d281 100644 --- a/d2common/d2astar/README.md +++ b/d2common/d2astar/README.md @@ -7,9 +7,9 @@ D2 A* 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. +* Used [sync.Pool](https://golang.org/pkg/sync/#Pool) to reuse objects created during path-finding. This improves performance by roughly 30% by reducing allocations. * Added a max cost to prevent searching the entire region for a path. +* If there is no path the target within the max cost, the path found that gets closest to target will be returned. This allows the player to click in inaccessible areas causing the character to run along the edge. TODO ------ diff --git a/d2common/d2astar/astar.go b/d2common/d2astar/astar.go index ea828086..779dbbc2 100644 --- a/d2common/d2astar/astar.go +++ b/d2common/d2astar/astar.go @@ -2,28 +2,28 @@ 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{} { + +func init() { + nodePool = &sync.Pool{ + New: func() interface{} { return &node{} }, } - nodeMapPool = &sync.Pool { - New: func()interface{} { + nodeMapPool = &sync.Pool{ + New: func() interface{} { return make(nodeMap, 128) }, } - priorityQueuePool = &sync.Pool { - New: func()interface{} { + priorityQueuePool = &sync.Pool{ + New: func() interface{} { return priorityQueue{} }, } @@ -55,7 +55,7 @@ type node struct { index int } -func (n *node) reset () { +func (n *node) reset() { n.pather = nil n.cost = 0 n.rank = 0 @@ -81,14 +81,8 @@ func (nm nodeMap) get(p Pather) *node { // Path calculates a short path and the distance between the two Pather nodes. // -// If no path is found, found will be false. +// If no path is found, found will be false and path will be the closest node to the target with a valid path. 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() { @@ -109,8 +103,8 @@ func Path(from, to Pather, maxCost float64) (path []Pather, distance float64, fo heap.Push(&nq, fromNode) for { if nq.Len() == 0 { - // There's no path, return found false. - return + // There's no path, fallback to closest. + break } current := heap.Pop(&nq).(*node) current.open = false @@ -130,7 +124,7 @@ func Path(from, to Pather, maxCost float64) (path []Pather, distance float64, fo for _, neighbor := range current.pather.PathNeighbors() { cost := current.cost + current.pather.PathNeighborCost(neighbor) if cost > maxCost { - fmt.Println("Canceling path") + // Out of range, tweak maxCost if this is cutting off too soon. continue } @@ -151,4 +145,27 @@ func Path(from, to Pather, maxCost float64) (path []Pather, distance float64, fo } } } + + // No path found, use closest node available, with found false to indicate the path doesn't reach the target. + closestNode := nm.get(from) + closestNode.rank = closestNode.pather.PathEstimatedCost(to) + for _, current := range nm { + if current.parent == nil { + // This node wasn't evaluated while path finding, probably isn't a good option. + continue + } + + current.rank = current.pather.PathEstimatedCost(to) + if current.rank < closestNode.rank { + closestNode = current + } + } + + p := make([]Pather, 0, 16) + curr := closestNode + for curr != nil { + p = append(p, curr.pather) + curr = curr.parent + } + return p, closestNode.cost, false } diff --git a/d2core/d2map/d2mapengine/walk_mesh.go b/d2core/d2map/d2mapengine/walk_mesh.go index d0666fc0..3bc9ad91 100644 --- a/d2core/d2map/d2mapengine/walk_mesh.go +++ b/d2core/d2map/d2mapengine/walk_mesh.go @@ -97,8 +97,14 @@ func (m *MapEngine) PathFind(startX, startY, endX, endY float64) (path []d2astar } endNode := &m.walkMesh[endNodeIndex] - path, distance, found = d2astar.Path(endNode, startNode, 80) + path, distance, found = d2astar.Path(startNode, endNode, 80) if path != nil { + // Reverse the path to fit what the game expects. + for i := len(path)/2-1; i >= 0; i-- { + opp := len(path)-1-i + path[i], path[opp] = path[opp], path[i] + } + path = path[1:] } return diff --git a/d2networking/d2client/game_client.go b/d2networking/d2client/game_client.go index 0de319a2..6a8a1acd 100644 --- a/d2networking/d2client/game_client.go +++ b/d2networking/d2client/game_client.go @@ -86,8 +86,8 @@ func (g *GameClient) OnPacketReceived(packet d2netpacket.NetPacket) error { case d2netpackettype.MovePlayer: movePlayer := packet.PacketData.(d2netpacket.MovePlayerPacket) player := g.Players[movePlayer.PlayerId] - path, _, found := g.MapEngine.PathFind(movePlayer.StartX, movePlayer.StartY, movePlayer.DestX, movePlayer.DestY) - if found { + path, _, _ := g.MapEngine.PathFind(movePlayer.StartX, movePlayer.StartY, movePlayer.DestX, movePlayer.DestY) + if len(path) > 0 { player.AnimatedComposite.SetPath(path, func() { tile := g.MapEngine.TileAt(player.TileX, player.TileY) if tile == nil {