Reimplemented Pathfinder to use A* instead of spawning an entity
#closes 788
This commit is contained in:
1 parent
db406cacbf
commit
da9eb2b3dd
6 files changed
+318
-10
No files matched your search
@@ -37,6 +37,8 @@ setmetatable(__eidEntityDescriptions,
|
|||||||
end })
|
end })
|
||||||
---------------------------------------------------------------------------
|
---------------------------------------------------------------------------
|
||||||
-------------------------Handle API Functions -----------------------------
|
-------------------------Handle API Functions -----------------------------
|
||||||
|
EID.Pathfinder = require("features.pathfinder.luafinding")
|
||||||
|
|
||||||
local nullVector = Vector(0,0)
|
local nullVector = Vector(0,0)
|
||||||
local game = Game()
|
local game = Game()
|
||||||
local maxCardID = Card.NUM_CARDS - 1
|
local maxCardID = Card.NUM_CARDS - 1
|
||||||
@@ -3116,3 +3118,36 @@ function EID:IsItemHidden(entity)
|
|||||||
end
|
end
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---Returns true if the position at the given grid location is valid for pathfinding.
|
||||||
|
---gridPosition is a table with an x and y entry. Its derived from the FindVector class of "LuaFindings" module (pathfinder)
|
||||||
|
---@param gridPosition table
|
||||||
|
---@return boolean
|
||||||
|
function EID:EvaluateLocation(gridPosition)
|
||||||
|
local room = game:GetRoom()
|
||||||
|
local width = room:GetGridWidth()
|
||||||
|
local height = room:GetGridHeight()
|
||||||
|
if gridPosition.x < 1 or gridPosition.y < 1 or gridPosition.x >= height-1 or gridPosition.y >= width - 1 then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
local gridIndex = gridPosition.x * room:GetGridWidth() + gridPosition.y
|
||||||
|
local collision = room:GetGridCollision(gridIndex)
|
||||||
|
return collision == GridCollisionClass.COLLISION_NONE or collision == GridCollisionClass.COLLISION_WALL_EXCEPT_PLAYER
|
||||||
|
end
|
||||||
|
|
||||||
|
---Returns true if an unobstructed path between the start and end position exists.
|
||||||
|
---Obstructions can be something like Rocks, Pits, Walls, etc.
|
||||||
|
---@param startPos Vector
|
||||||
|
---@param endPos Vector
|
||||||
|
---@return boolean
|
||||||
|
function EID:HasPathToPosition(startPos, endPos)
|
||||||
|
-- divide by 40 to convert from world to grid coords
|
||||||
|
local room = game:GetRoom()
|
||||||
|
local width = room:GetGridWidth()
|
||||||
|
local startGI = room:GetGridIndex(startPos)
|
||||||
|
local endGI = room:GetGridIndex(endPos)
|
||||||
|
-- convert GridIndex into Grid X,Y coordinates and calculate path
|
||||||
|
local pathfinderObj = EID.Pathfinder(Vector(startGI%width, math.floor(startGI/width)), Vector(endGI%width, math.floor(endGI/width)), EID.EvaluateLocation)
|
||||||
|
-- return true if it has a path
|
||||||
|
return pathfinderObj:GetPath() ~= nil
|
||||||
|
end
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
This A*-pathfinder module is based on the following git repo:
|
||||||
|
https://github.com/GlorifiedPig/Luafinding
|
||||||
|
|
||||||
|
Licence: MIT
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
|
||||||
|
local FindVector = {}
|
||||||
|
FindVector.__index = FindVector
|
||||||
|
|
||||||
|
local function newFindVector( x, y )
|
||||||
|
return setmetatable( { x = x or 0, y = y or 0 }, FindVector )
|
||||||
|
end
|
||||||
|
|
||||||
|
function FindVector.__unm( vTbl )
|
||||||
|
return newFindVector( -vTbl.x, -vTbl.y )
|
||||||
|
end
|
||||||
|
|
||||||
|
function FindVector.__add( a, b )
|
||||||
|
return newFindVector( a.x + b.x, a.y + b.y )
|
||||||
|
end
|
||||||
|
|
||||||
|
function FindVector.__sub( a, b )
|
||||||
|
return newFindVector( a.x - b.x, a.y - b.y )
|
||||||
|
end
|
||||||
|
|
||||||
|
function FindVector.__mul( a, b )
|
||||||
|
if type( a ) == "number" then
|
||||||
|
return newFindVector( a * b.x, a * b.y )
|
||||||
|
elseif type( b ) == "number" then
|
||||||
|
return newFindVector( a.x * b, a.y * b )
|
||||||
|
else
|
||||||
|
return newFindVector( a.x * b.x, a.y * b.y )
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function FindVector.__div( a, b )
|
||||||
|
return newFindVector( a.x / b, a.y / b )
|
||||||
|
end
|
||||||
|
|
||||||
|
function FindVector.__eq( a, b )
|
||||||
|
return a.x == b.x and a.y == b.y
|
||||||
|
end
|
||||||
|
|
||||||
|
function FindVector:__tostring()
|
||||||
|
return "(" .. self.x .. ", " .. self.y .. ")"
|
||||||
|
end
|
||||||
|
|
||||||
|
function FindVector:ID()
|
||||||
|
if self._ID == nil then
|
||||||
|
local x, y = self.x, self.y
|
||||||
|
self._ID = 0.5 * ( ( x + y ) * ( x + y + 1 ) + y )
|
||||||
|
end
|
||||||
|
|
||||||
|
return self._ID
|
||||||
|
end
|
||||||
|
|
||||||
|
return setmetatable( FindVector, { __call = function( _, ... ) return newFindVector( ... ) end } )
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
|
||||||
|
local Heap = {}
|
||||||
|
Heap.__index = Heap
|
||||||
|
|
||||||
|
local function findLowest( a, b )
|
||||||
|
return a < b
|
||||||
|
end
|
||||||
|
|
||||||
|
local function newHeap( template, compare )
|
||||||
|
return setmetatable( {
|
||||||
|
Data = {},
|
||||||
|
Compare = compare or findLowest,
|
||||||
|
Size = 0
|
||||||
|
}, template )
|
||||||
|
end
|
||||||
|
|
||||||
|
local function sortUp( heap, index )
|
||||||
|
if index <= 1 then return end
|
||||||
|
local pIndex = index % 2 == 0 and index / 2 or ( index - 1 ) / 2
|
||||||
|
|
||||||
|
if not heap.Compare( heap.Data[pIndex], heap.Data[index] ) then
|
||||||
|
heap.Data[pIndex], heap.Data[index] = heap.Data[index], heap.Data[pIndex]
|
||||||
|
sortUp( heap, pIndex )
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function sortDown( heap, index )
|
||||||
|
local leftIndex, rightIndex, minIndex
|
||||||
|
leftIndex = index * 2
|
||||||
|
rightIndex = leftIndex + 1
|
||||||
|
if rightIndex > heap.Size then
|
||||||
|
if leftIndex > heap.Size then return
|
||||||
|
else minIndex = leftIndex end
|
||||||
|
else
|
||||||
|
if heap.Compare( heap.Data[leftIndex], heap.Data[rightIndex] ) then minIndex = leftIndex
|
||||||
|
else minIndex = rightIndex end
|
||||||
|
end
|
||||||
|
|
||||||
|
if not heap.Compare( heap.Data[index], heap.Data[minIndex] ) then
|
||||||
|
heap.Data[index], heap.Data[minIndex] = heap.Data[minIndex], heap.Data[index]
|
||||||
|
sortDown( heap, minIndex )
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Heap:Empty()
|
||||||
|
return self.Size == 0
|
||||||
|
end
|
||||||
|
|
||||||
|
function Heap:Clear()
|
||||||
|
self.Data, self.Size, self.Compare = {}, 0, self.Compare or findLowest
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function Heap:Push( item )
|
||||||
|
if item then
|
||||||
|
self.Size = self.Size + 1
|
||||||
|
self.Data[self.Size] = item
|
||||||
|
sortUp( self, self.Size )
|
||||||
|
end
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function Heap:Pop()
|
||||||
|
local root
|
||||||
|
if self.Size > 0 then
|
||||||
|
root = self.Data[1]
|
||||||
|
self.Data[1] = self.Data[self.Size]
|
||||||
|
self.Data[self.Size] = nil
|
||||||
|
self.Size = self.Size - 1
|
||||||
|
if self.Size > 1 then
|
||||||
|
sortDown( self, 1 )
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return root
|
||||||
|
end
|
||||||
|
|
||||||
|
return setmetatable( Heap, { __call = function( self, ... ) return newHeap( self, ... ) end } )
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
|
||||||
|
-- Positions must be a table (or metatable) where table.x and table.y are accessible.
|
||||||
|
|
||||||
|
local FindVector = require( "features.pathfinder.find_vector" )
|
||||||
|
local Heap = require( "features.pathfinder.heap" )
|
||||||
|
|
||||||
|
local Luafinding = {}
|
||||||
|
Luafinding.__index = Luafinding
|
||||||
|
|
||||||
|
-- This instantiates a new Luafinding class for usage later.
|
||||||
|
-- "start" and "finish" should both be 2 dimensional FindVectors, or just a table with "x" and "y" keys. See the note at the top of this file.
|
||||||
|
-- positionOpenCheck can be a function or a table.
|
||||||
|
-- If it's a function it must have a return value of true or false depending on whether or not the position is open.
|
||||||
|
-- If it's a table it should simply be a table of values such as "pos[x][y] = true".
|
||||||
|
function Luafinding:Initialize( start, finish, positionOpenCheck )
|
||||||
|
local newPath = setmetatable( { Start = FindVector(start.Y,start.X), Finish = FindVector(finish.Y,finish.X), PositionOpenCheck = positionOpenCheck }, Luafinding )
|
||||||
|
newPath:CalculatePath()
|
||||||
|
return newPath
|
||||||
|
end
|
||||||
|
|
||||||
|
local function distance( start, finish )
|
||||||
|
local dx = start.x - finish.x
|
||||||
|
local dy = start.y - finish.y
|
||||||
|
return dx * dx + dy * dy
|
||||||
|
end
|
||||||
|
|
||||||
|
local positionIsOpen
|
||||||
|
local function positionIsOpenTable( pos, check ) return check[pos.x] and check[pos.x][pos.y] end
|
||||||
|
local function positionIsOpenCustom( pos, check ) return check( _, pos ) end
|
||||||
|
|
||||||
|
local adjacentPositions = {
|
||||||
|
FindVector( 0, -1 ),
|
||||||
|
FindVector( -1, 0 ),
|
||||||
|
FindVector( 0, 1 ),
|
||||||
|
FindVector( 1, 0 ),
|
||||||
|
}
|
||||||
|
|
||||||
|
local function fetchOpenAdjacentNodes( pos, positionOpenCheck )
|
||||||
|
local result = {}
|
||||||
|
|
||||||
|
for i = 1, #adjacentPositions do
|
||||||
|
local adjacentPos = pos + adjacentPositions[i]
|
||||||
|
if positionIsOpen( adjacentPos, positionOpenCheck ) then
|
||||||
|
table.insert( result, adjacentPos )
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return result
|
||||||
|
end
|
||||||
|
|
||||||
|
-- This is the function used to actually calculate the path.
|
||||||
|
-- It returns the calcated path table, or nil if it cannot find a path.
|
||||||
|
function Luafinding:CalculatePath()
|
||||||
|
local start, finish, positionOpenCheck = self.Start, self.Finish, self.PositionOpenCheck
|
||||||
|
if not positionOpenCheck then return end
|
||||||
|
positionIsOpen = type( positionOpenCheck ) == "table" and positionIsOpenTable or positionIsOpenCustom
|
||||||
|
if not positionIsOpen( finish, positionOpenCheck ) then return end
|
||||||
|
-- EID edit: check if end position is surrounded by stuff and therefore unaccessable. This is done for a quick early out
|
||||||
|
local directlyAdjacents = fetchOpenAdjacentNodes( self.Finish, positionOpenCheck )
|
||||||
|
if #directlyAdjacents == 0 then return end
|
||||||
|
-- edit end
|
||||||
|
local open, closed = Heap(), {}
|
||||||
|
|
||||||
|
start.gScore = 0
|
||||||
|
start.hScore = distance( start, finish )
|
||||||
|
start.fScore = start.hScore
|
||||||
|
|
||||||
|
open.Compare = function( a, b )
|
||||||
|
return a.fScore < b.fScore
|
||||||
|
end
|
||||||
|
|
||||||
|
open:Push( start )
|
||||||
|
|
||||||
|
while not open:Empty() do
|
||||||
|
local current = open:Pop()
|
||||||
|
local currentId = current:ID()
|
||||||
|
if not closed[currentId] then
|
||||||
|
if current == finish then
|
||||||
|
local path = {}
|
||||||
|
while true do
|
||||||
|
if current.previous then
|
||||||
|
table.insert( path, 1, current )
|
||||||
|
current = current.previous
|
||||||
|
else
|
||||||
|
table.insert( path, 1, start )
|
||||||
|
self.Path = path
|
||||||
|
return path
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
closed[currentId] = true
|
||||||
|
|
||||||
|
local adjacents = fetchOpenAdjacentNodes( current, positionOpenCheck )
|
||||||
|
for i = 1, #adjacents do
|
||||||
|
local adjacent = adjacents[i]
|
||||||
|
if not closed[adjacent:ID()] then
|
||||||
|
local added_gScore = current.gScore + distance( current, adjacent )
|
||||||
|
|
||||||
|
if not adjacent.gScore or added_gScore < adjacent.gScore then
|
||||||
|
adjacent.gScore = added_gScore
|
||||||
|
if not adjacent.hScore then
|
||||||
|
adjacent.hScore = distance( adjacent, finish )
|
||||||
|
end
|
||||||
|
adjacent.fScore = added_gScore + adjacent.hScore
|
||||||
|
|
||||||
|
open:Push( adjacent )
|
||||||
|
adjacent.previous = current
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Luafinding:GetPath()
|
||||||
|
return self.Path
|
||||||
|
end
|
||||||
|
|
||||||
|
function Luafinding:GetDistance()
|
||||||
|
local path = self.Path
|
||||||
|
if not path then return end
|
||||||
|
return distance( path[1], path[#path] )
|
||||||
|
end
|
||||||
|
|
||||||
|
function Luafinding:GetTiles()
|
||||||
|
local path = self.Path
|
||||||
|
if not path then return end
|
||||||
|
return #path
|
||||||
|
end
|
||||||
|
|
||||||
|
function Luafinding:__tostring()
|
||||||
|
local path = self.Path
|
||||||
|
local string = ""
|
||||||
|
|
||||||
|
if path then
|
||||||
|
for k, v in ipairs( path ) do
|
||||||
|
local formatted = ( k .. ": " .. v )
|
||||||
|
string = k == 1 and formatted or string .. "\n" .. formatted
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return string
|
||||||
|
end
|
||||||
|
|
||||||
|
return setmetatable( Luafinding, { __call = function( self, ... ) return self:Initialize( ... ) end } )
|
||||||
@@ -1138,7 +1138,6 @@ end
|
|||||||
EID:AddCallback(ModCallbacks.MC_POST_PICKUP_INIT, EID.CollectibleSpawnedThisFrame, PickupVariant.PICKUP_COLLECTIBLE)
|
EID:AddCallback(ModCallbacks.MC_POST_PICKUP_INIT, EID.CollectibleSpawnedThisFrame, PickupVariant.PICKUP_COLLECTIBLE)
|
||||||
|
|
||||||
-- Pathchecking
|
-- Pathchecking
|
||||||
local pathCheckerEntity = nil
|
|
||||||
local lastPathfindFrame = -1
|
local lastPathfindFrame = -1
|
||||||
|
|
||||||
local function attemptPathfind(entity)
|
local function attemptPathfind(entity)
|
||||||
@@ -1146,18 +1145,13 @@ local function attemptPathfind(entity)
|
|||||||
pathsChecked[entity.InitSeed] = true
|
pathsChecked[entity.InitSeed] = true
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
-- Don't reattempt pathfinding more than 3 times a second, unless this is a new entity
|
-- Don't reattempt pathfinding more than 2 times a second, unless this is a new entity
|
||||||
if pathsChecked[entity.InitSeed] == false and EID.GameUpdateCount - lastPathfindFrame < 10 then return false end
|
if pathsChecked[entity.InitSeed] == false and EID.GameUpdateCount - lastPathfindFrame < 15 then return false end
|
||||||
|
|
||||||
-- Spawn a Fireplace entity to attempt a pathfind to the target pickup, then remove it afterwards
|
local successful = EID:HasPathToPosition(EID.player.Position, entity.Position)
|
||||||
pathCheckerEntity = game:Spawn(33, 0, EID.player.Position, nullVector, EID.player, 6969, 4354)
|
|
||||||
EID:setEntityData(pathCheckerEntity, "EID_Pathfinder", true)
|
|
||||||
pathCheckerEntity.Visible = false
|
|
||||||
local successful = pathCheckerEntity:ToNPC().Pathfinder:HasPathToPos(entity.Position, false)
|
|
||||||
pathsChecked[entity.InitSeed] = successful
|
pathsChecked[entity.InitSeed] = successful
|
||||||
pathCheckerEntity:Remove()
|
|
||||||
pathCheckerEntity = nil
|
|
||||||
lastPathfindFrame = EID.GameUpdateCount
|
lastPathfindFrame = EID.GameUpdateCount
|
||||||
|
|
||||||
return successful
|
return successful
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user