Pathfinding Fixes

mob_meta.get_node_height:
Moved to pathfinding, and made public (creatura.get_node_height).
Now uses collision box rather than node box, if available, unless flag specifies not to.
Fix for getting node height for nodes with connected node boxes

pathfinding.get_ground_level:
We now have one copy between pathfinding and mob_meta.
Can check a larger range of nodes at different heights.
Takes into account node height of both target node, and the height of the node we're coming from.

pathfinding.get_neighbors:
Get maximum distance mob can travel up/down from mob stepheight.
Improved checking diagonals are clear.
Improved checking vertical clearance, takes into account height of current and target node.
Vertical clearance can cope with nodes that have a different collision and node boxes, like snow.
Vertical clearance has a tiny height adjustment so a 2 node heigh entity can fit through a 2 node gap.
Fixed bug where it was assumed a node was reachable if it's the end goal.

methods.movement_theta_pathfind and movement_pathfind:
Fixed bug that raised goal pos by 0.5 nodes.
This commit is contained in:
Jordan Leppert 2022-02-20 21:27:00 +00:00 committed by Jordan Leppert
parent 23c61e0751
commit 35069011d6
3 changed files with 185 additions and 231 deletions

View file

@ -166,7 +166,6 @@ end
local function movement_theta_pathfind(self, pos2, speed) local function movement_theta_pathfind(self, pos2, speed)
local pos = self.object:get_pos() local pos = self.object:get_pos()
local goal = pos2
self._path = self._path or {} self._path = self._path or {}
local temp_goal = self._movement_data.temp_goal local temp_goal = self._movement_data.temp_goal
if not temp_goal if not temp_goal
@ -182,7 +181,6 @@ local function movement_theta_pathfind(self, pos2, speed)
table.remove(self._path, 1) table.remove(self._path, 1)
end end
end end
goal.y = pos.y + 0.5
local dir = vector.direction(self.object:get_pos(), pos2) local dir = vector.direction(self.object:get_pos(), pos2)
local tyaw = minetest.dir_to_yaw(dir) local tyaw = minetest.dir_to_yaw(dir)
local turn_rate = self.turn_rate or 10 local turn_rate = self.turn_rate or 10
@ -201,7 +199,7 @@ local function movement_theta_pathfind(self, pos2, speed)
self:animate("walk") self:animate("walk")
self:set_gravity(-9.8) self:set_gravity(-9.8)
self:set_forward_velocity(speed or 2) self:set_forward_velocity(speed or 2)
if self:pos_in_box(goal) then if self:pos_in_box(pos2) then
self:halt() self:halt()
end end
end end
@ -210,7 +208,6 @@ creatura.register_movement_method("creatura:theta_pathfind", movement_theta_path
local function movement_pathfind(self, pos2, speed) local function movement_pathfind(self, pos2, speed)
local pos = self.object:get_pos() local pos = self.object:get_pos()
local goal = pos2
local temp_goal = self._movement_data.temp_goal local temp_goal = self._movement_data.temp_goal
self._path = self._path or {} self._path = self._path or {}
if (not temp_goal if (not temp_goal
@ -227,7 +224,6 @@ local function movement_pathfind(self, pos2, speed)
table.remove(self._path, 1) table.remove(self._path, 1)
end end
end end
goal.y = pos.y + 0.5
local dir = vector.direction(self.object:get_pos(), pos2) local dir = vector.direction(self.object:get_pos(), pos2)
local tyaw = minetest.dir_to_yaw(dir) local tyaw = minetest.dir_to_yaw(dir)
local turn_rate = self.turn_rate or 10 local turn_rate = self.turn_rate or 10

View file

@ -62,79 +62,15 @@ end
local default_node_def = {walkable = true} -- both ignore and unknown nodes are walkable local default_node_def = {walkable = true} -- both ignore and unknown nodes are walkable
local function get_node_height(name)
local def = minetest.registered_nodes[name]
if not def then return 0.5 end
if def.walkable then
if def.drawtype == "nodebox" then
if def.node_box
and def.node_box.type == "fixed" then
if type(def.node_box.fixed[1]) == "number" then
return 0.5 + def.node_box.fixed[5]
elseif type(def.node_box.fixed[1]) == "table" then
return 0.5 + def.node_box.fixed[1][5]
else
return 1
end
else
return 1
end
else
return 1
end
else
return 1
end
end
local function get_node_def(name) local function get_node_def(name)
local def = minetest.registered_nodes[name] or default_node_def local def = minetest.registered_nodes[name] or default_node_def
if def.walkable if def.walkable
and get_node_height(name) < 0.26 then and creatura.get_node_height(name) < 0.26 then
def.walkable = false -- workaround for nodes like snow def.walkable = false -- workaround for nodes like snow
end end
return def return def
end end
local function get_ground_level(pos2, max_diff)
local node = minetest.get_node(pos2)
local node_under = minetest.get_node({
x = pos2.x,
y = pos2.y - 1,
z = pos2.z
})
local walkable = get_node_def(node_under.name) and not get_node_def(node.name)
if walkable then
return pos2
end
local diff = 0
if not get_node_def(node_under.name) then
for i = 1, max_diff do
pos2.y = pos2.y - 1
node = minetest.get_node(pos2)
node_under = minetest.get_node({
x = pos2.x,
y = pos2.y - 1,
z = pos2.z
})
walkable = get_node_def(node_under.name) and not get_node_def(node.name)
if walkable then break end
end
else
for i = 1, max_diff do
pos2.y = pos2.y + 1
node = minetest.get_node(pos2)
node_under = minetest.get_node({
x = pos2.x,
y = pos2.y - 1,
z = pos2.z
})
walkable = get_node_def(node_under.name) and not get_node_def(node.name)
if walkable then break end
end
end
return pos2
end
------------------------- -------------------------
-- Physics/Vitals Tick -- -- Physics/Vitals Tick --
@ -371,7 +307,7 @@ function mob:get_wander_pos(min_range, max_range, dir)
local offset = vector.add(pos, vec_multi(vec_dir(pos, self.object:get_pos()), 1.5)) local offset = vector.add(pos, vec_multi(vec_dir(pos, self.object:get_pos()), 1.5))
pos.x = floor(offset.x + 0.5) pos.x = floor(offset.x + 0.5)
pos.z = floor(offset.z + 0.5) pos.z = floor(offset.z + 0.5)
pos = get_ground_level(pos, 1) pos = creatura.get_ground_level(pos, 1, 1, 0)
end end
local width = self.width local width = self.width
local outset = random(min_range, max_range) local outset = random(min_range, max_range)

View file

@ -44,39 +44,55 @@ local function moveable(pos, width, height)
return true return true
end end
local function get_ground_level(pos2, max_height) creatura.get_node_height = function(name, force_node_box)
local node = minetest.get_node(pos2) local def = minetest.registered_nodes[name]
local node_under = minetest.get_node({ if not def then return 0.5 end
x = pos2.x, if def.walkable then
y = pos2.y - 1, if def.drawtype == "nodebox" then
z = pos2.z if def.collision_box and not force_node_box
}) and (def.collision_box.type == "fixed" or def.collision_box.type == "connected") then
local height = 0 if type(def.collision_box.fixed[1]) == "number" then
local walkable = is_node_walkable(node_under.name) and not is_node_walkable(node.name) return 0.5 + def.collision_box.fixed[5]
if walkable then elseif type(def.collision_box.fixed[1]) == "table" then
return pos2 return 0.5 + def.collision_box.fixed[1][5]
elseif not walkable then else
if not is_node_walkable(node_under.name) then return 1
while not is_node_walkable(node_under.name) end
and height < max_height do elseif def.node_box
pos2.y = pos2.y - 1 and (def.node_box.type == "fixed" or def.node_box.type == "connected") then
node_under = minetest.get_node({ if type(def.node_box.fixed[1]) == "number" then
x = pos2.x, return 0.5 + def.node_box.fixed[5]
y = pos2.y - 1, elseif type(def.node_box.fixed[1]) == "table" then
z = pos2.z return 0.5 + def.node_box.fixed[1][5]
}) else
height = height + 1 return 1
end end
else else
while is_node_walkable(node.name) return 1
and height < max_height do
pos2.y = pos2.y + 1
node = minetest.get_node(pos2)
height = height + 1
end end
else
return 1
end end
else
return 1
end
end
creatura.get_ground_level = function(pos, max_up, max_down, current_node_height)
for y = math.ceil(max_up) + 1, -(math.ceil(max_down)) - 1, -1 do
local pos2 = vector.new(pos.x, pos.y + y, pos.z)
local node = minetest.get_node(pos2)
local node_under = minetest.get_node(pos2 + vector.new(0, -1, 0))
if not is_node_walkable(node.name) and is_node_walkable(node_under.name) then
local node_height = creatura.get_node_height(node_under.name)
local y_diff = y - 1 + node_height - current_node_height
if y_diff <= max_up and y_diff >= (-max_down) then
return pos2 return pos2
end end
end
end
return nil
end end
local function get_distance(start_pos, end_pos) local function get_distance(start_pos, end_pos)
@ -120,6 +136,99 @@ end
-- Find a path from start to goal -- Find a path from start to goal
local function get_neighbors(self, pos, goal, swim, fly, climb, tbl, open, closed)
local width = self.width
local height = self.height
local result = {}
local max_up = self.stepheight or 1
local max_down = self.stepheight or 1
local node_name = minetest.get_node(pos).name
-- Get the height of the node collision box (and of its node box, if different)
local node_height = 0
local node_height_node_box = 0
if is_node_walkable(node_name) then
node_height = creatura.get_node_height(node_name)
node_height_node_box = creatura.get_node_height(node_name, true)
else
node_height = creatura.get_node_height(minetest.get_node(pos + vector.new(0, -1, 0)).name) - 1
node_height_node_box = creatura.get_node_height(minetest.get_node(pos + vector.new(0, -1, 0)).name, true) - 1
end
-- Calculate the height difference between the collision and node boxes
-- (This is because the mob will be standing on the collision box, but the
-- raycast checks will collide with the node box, so we must avoid it)
local node_height_diff = node_height_node_box - node_height
for i = 1, #tbl do
local neighbor = vector.add(pos, tbl[i])
if not open[minetest.hash_node_position(neighbor)]
and not closed[minetest.hash_node_position(neighbor)] then
local neighbor_x
local neighbor_z
if tbl[i].y == 0
and not fly
and not swim then
neighbor = creatura.get_ground_level(neighbor, max_up, max_down, node_height)
if neighbor and tbl[i].x ~= 0 and tbl[i].z ~= 0 then
-- This is a diagonal, check both corners are clear and same Y
neighbor_x = creatura.get_ground_level(vector.new(neighbor.x, neighbor.y, pos.z), max_up, max_down, node_height)
neighbor_z = creatura.get_ground_level(vector.new(pos.x, neighbor.y, neighbor.z), max_up, max_down, node_height)
if not neighbor_x or not neighbor_z
or neighbor_x.y ~= neighbor.y
or neighbor_z.y ~= neighbor.y then
neighbor = nil
end
end
end
if neighbor then
local can_move = true
if swim then
local neighbor_node = minetest.get_node(neighbor)
can_move = is_node_liquid(neighbor_node.name)
end
-- Adjust entity Y in clearance check by this much
local y_adjustment = -0.49
-- Adjust entity height in clearance check by this much
local h_adjustment = -0.02
-- Get the height of the node collision box, and the difference to the node box
local neighbor_height = creatura.get_node_height(minetest.get_node(neighbor + vector.new(0, -1, 0)).name) - 1
local neighbor_height_node_box = creatura.get_node_height(minetest.get_node(neighbor + vector.new(0, -1, 0)).name, true) - 1
local neighbor_height_diff = neighbor_height_node_box - neighbor_height
-- Check there is enough vertical clearance to move to this node
local height_clearance = math.max(pos.y + node_height - neighbor.y - neighbor_height, 0)
if not moveable(vec_raise(neighbor, y_adjustment + neighbor_height + neighbor_height_diff), width, height + h_adjustment + height_clearance - neighbor_height_diff) then
can_move = false
end
if tbl[i].x ~= 0 and tbl[i].z ~= 0 then
-- If target node is diagonal, check the orthogonal nodes too
if not moveable(vec_raise(neighbor_x, y_adjustment + neighbor_height + neighbor_height_diff), width, height + h_adjustment + height_clearance + neighbor_height_diff)
or not moveable(vec_raise(neighbor_z, y_adjustment + neighbor_height + neighbor_height_diff), width, height + h_adjustment + height_clearance + neighbor_height_diff) then
can_move = false
end
end
-- If we're going upwards, check there's enough clearance above our head
height_clearance = math.max(neighbor.y + neighbor_height - pos.y - node_height, 0)
if height_clearance > 0 and not moveable(vec_raise(pos, y_adjustment + node_height + node_height_diff), width, height + h_adjustment + height_clearance - node_height_diff) then
can_move = false
end
if (can_move
or (climb
and neighbor.x == pos.x
and neighbor.z == pos.z))
and (not swim
or is_node_liquid(minetest.get_node(neighbor).name)) then
table.insert(result, neighbor)
end
end
end
end
return result
end
function creatura.find_path(self, start, goal, obj_width, obj_height, max_open, climb, fly, swim) function creatura.find_path(self, start, goal, obj_width, obj_height, max_open, climb, fly, swim)
climb = climb or false climb = climb or false
fly = fly or false fly = fly or false
@ -158,49 +267,7 @@ function creatura.find_path(self, start, goal, obj_width, obj_height, max_open,
} }
end end
local function get_neighbors(pos, width, height, tbl, open, closed) local function find_path(self, start, goal)
local result = {}
for i = 1, #tbl do
local neighbor = vector.add(pos, tbl[i])
if neighbor.y == pos.y
and not fly
and not swim then
neighbor = get_ground_level(neighbor, 1)
end
local can_move = get_line_of_sight({x = pos.x, y = neighbor.y, z = pos.z}, neighbor)
if swim then
can_move = true
end
if not moveable(vec_raise(neighbor, -0.49), width, height) then
can_move = false
if neighbor.y == pos.y
and moveable(vec_raise(neighbor, 0.51), width, height) then
neighbor = vec_raise(neighbor, 1)
can_move = true
end
end
if vector.equals(neighbor, goal) then
can_move = true
end
if open[minetest.hash_node_position(neighbor)]
or closed[minetest.hash_node_position(neighbor)] then
can_move = false
end
if can_move
and ((is_on_ground(neighbor)
or (fly or swim))
or (neighbor.x == pos.x
and neighbor.z == pos.z
and climb))
and (not swim
or is_node_liquid(minetest.get_node(neighbor).name)) then
table.insert(result, neighbor)
end
end
return result
end
local function find_path(start, goal)
local us_time = minetest.get_us_time() local us_time = minetest.get_us_time()
start = { start = {
@ -299,7 +366,7 @@ function creatura.find_path(self, start, goal, obj_width, obj_height, max_open,
count = count - 1 count = count - 1
local adjacent = get_neighbors(current.pos, obj_width, obj_height, path_neighbors, openSet, closedSet) local adjacent = get_neighbors(self, current.pos, goal, swim, fly, climb, path_neighbors, openSet, closedSet)
-- Go through neighboring nodes -- Go through neighboring nodes
for i = 1, #adjacent do for i = 1, #adjacent do
@ -309,21 +376,18 @@ function creatura.find_path(self, start, goal, obj_width, obj_height, max_open,
gScore = 0, gScore = 0,
fScore = 0 fScore = 0
} }
local temp_gScore = current.gScore + get_distance_to_neighbor(current.pos, neighbor.pos) local neighbor_id = minetest.hash_node_position(neighbor.pos)
local new_gScore = 0 local neighbour_gScore = current.gScore + get_distance_to_neighbor(current.pos, neighbor.pos)
if openSet[minetest.hash_node_position(neighbor.pos)] then if (not openSet[neighbor_id]
new_gScore = openSet[minetest.hash_node_position(neighbor.pos)].gScore or neighbour_gScore < openSet[neighbor_id].gScore)
end and not closedSet[neighbor_id] then
if (temp_gScore < new_gScore if not openSet[neighbor_id] then
or not openSet[minetest.hash_node_position(neighbor.pos)])
and not closedSet[minetest.hash_node_position(neighbor.pos)] then
if not openSet[minetest.hash_node_position(neighbor.pos)] then
count = count + 1 count = count + 1
end end
local hCost = get_distance_to_neighbor(neighbor.pos, goal) local hCost = get_distance_to_neighbor(neighbor.pos, goal)
neighbor.gScore = temp_gScore neighbor.gScore = neighbour_gScore
neighbor.fScore = temp_gScore + hCost neighbor.fScore = neighbour_gScore + hCost
openSet[minetest.hash_node_position(neighbor.pos)] = neighbor openSet[neighbor_id] = neighbor
end end
end end
if count > (max_open or 100) then if count > (max_open or 100) then
@ -334,7 +398,7 @@ function creatura.find_path(self, start, goal, obj_width, obj_height, max_open,
self._path_data = {} self._path_data = {}
return nil return nil
end end
return find_path(start, goal) return find_path(self, start, goal)
end end
@ -408,49 +472,7 @@ function creatura.find_theta_path(self, start, goal, obj_width, obj_height, max_
} }
end end
local function get_neighbors(pos, width, height, tbl, open, closed) local function find_path(self, start, goal)
local result = {}
for i = 1, #tbl do
local neighbor = vector.add(pos, tbl[i])
if neighbor.y == pos.y
and not fly
and not swim then
neighbor = get_ground_level(neighbor, 1)
end
local can_move = get_line_of_sight({x = pos.x, y = neighbor.y, z = pos.z}, neighbor)
if swim then
can_move = true
end
if not moveable(vec_raise(neighbor, -0.49), width, height) then
can_move = false
if neighbor.y == pos.y
and moveable(vec_raise(neighbor, 0.51), width, height) then
neighbor = vec_raise(neighbor, 1)
can_move = true
end
end
if vector.equals(neighbor, goal) then
can_move = true
end
if open[minetest.hash_node_position(neighbor)]
or closed[minetest.hash_node_position(neighbor)] then
can_move = false
end
if can_move
and ((is_on_ground(neighbor)
or (fly or swim))
or (neighbor.x == pos.x
and neighbor.z == pos.z
and climb))
and (not swim
or is_node_liquid(minetest.get_node(neighbor).name)) then
table.insert(result, neighbor)
end
end
return result
end
local function find_path(start, goal)
local us_time = minetest.get_us_time() local us_time = minetest.get_us_time()
start = { start = {
@ -545,7 +567,7 @@ function creatura.find_theta_path(self, start, goal, obj_width, obj_height, max_
count = count - 1 count = count - 1
local adjacent = get_neighbors(current.pos, obj_width, obj_height, path_neighbors, openSet, closedSet) local adjacent = get_neighbors(self, current.pos, goal, swim, fly, climb, path_neighbors, openSet, closedSet)
-- Go through neighboring nodes -- Go through neighboring nodes
for i = 1, #adjacent do for i = 1, #adjacent do
@ -606,5 +628,5 @@ function creatura.find_theta_path(self, start, goal, obj_width, obj_height, max_
self._path_data = {} self._path_data = {}
return nil return nil
end end
return find_path(start, goal) return find_path(self, start, goal)
end end