Start implementing lookup table calculation

This commit is contained in:
teknomunk 2024-03-27 07:21:10 +00:00
parent 9f5c948deb
commit 9fd09dce98
3 changed files with 173 additions and 29 deletions

View File

@ -105,6 +105,10 @@ local NODE_NEIGHBOR_DROP_TWO = {
vector.new( 0,-1, 1),
vector.new( 0,-1,-1),
}
local ACTION_WALK = nil
local ACTION_JUMP = {}
local ACTION_OPEN = {}
local ACTION_CLIMB = {}
local function get_navigation_rules_for_node(state, node_pos)
local area = state.area
local idx = area:index(node_pos.x,node_pos.y,node_pos.z)
@ -164,38 +168,58 @@ local function get_navigation_rules_for_node(state, node_pos)
-- We can navigate thru this node
local rule = {
height = height,
walk = {},
drop = {},
jump = {},
size = 1, -- TODO: calculate a clearance value for mobs larger than 1 block wide
pos = node_pos,
neighbors = {},
actions = {},
}
navigation_rules[idx] = rule
-- Update compressed rules map. This is used to store lookup tables in a smaller amount of memory
-- for better performance
local compression_mapping = state.navigation_lut.compression_mapping
compression_mapping.count = compression_mapping.count + 1
compression_mapping[minetest.hash_node_position(node_pos)] = {
idx = compression_mapping.count,
pos = node_pos
}
-- Now check neighbors to see if we can move there
for i,dir in ipairs(NODE_NEIGHBORS) do
local neighbors = rule.neighbors
local actions = rule.actions
local function check_neighbors(state, node_pos, dir, neighbors, actions)
-- Check direct neighbor
local neighbor_pos = vector.add(node_pos, dir)
local neighbor_rule = get_navigation_rules_for_node(state, neighbor_pos)
if not neighbor_rule.no_nav then
rule.walk[#rule.walk + 1] = dir
else
-- Check jumps
neighbor_rule = get_navigation_rules_for_node(state, vector.offset(neighbor_pos,0,1,0))
if not neighbor_rule.no_nav then
rule.jump[#rule.jump + 1] = NODE_NEIGHBOR_JUMPS[i]
else
-- Check drop one block
neighbor_rule = get_navigation_rules_for_node(state, vector.offset(neighbor_pos,0,-1,0))
if not neighbor_rule.no_nav then
rule.drop[#rule.drop + 1] = NODE_NEIGHBOR_DROP_ONE[i]
else
-- Check drop two blocks
neighbor_rule = get_navigation_rules_for_node(state, vector.offset(neighbor_pos,0,-1,0))
if not neighbor_rule.no_nav then
rule.drop[#rule.drop + 1] = NODE_NEIGHBOR_DROP_TWO[i]
end
end
end
neighbors[#neighbors + 1] = dir
return
end
-- Check jumps
neighbor_rule = get_navigation_rules_for_node(state, vector.offset(neighbor_pos,0,1,0))
if not neighbor_rule.no_nav then
local idx = #neighbors + 1
neighbors[idx] = NODE_NEIGHBOR_JUMPS[i]
actions[idx] = ACTION_JUMP
return
end
-- Check drop one block
neighbor_rule = get_navigation_rules_for_node(state, vector.offset(neighbor_pos,0,-1,0))
if not neighbor_rule.no_nav then
neighbors[#neighbors + 1] = NODE_NEIGHBOR_DROP_ONE[i]
return
end
-- Check drop two blocks
neighbor_rule = get_navigation_rules_for_node(state, vector.offset(neighbor_pos,0,-1,0))
if not neighbor_rule.no_nav then
neighbors[#neighbors + 1] = NODE_NEIGHBOR_DROP_TWO[i]
end
end
for i,dir in ipairs(NODE_NEIGHBORS) do
check_neighbors(state, node_pos, dir, neighbors, actions)
end
print("node_pos="..tostring(node_pos)..",name="..tostring(name)..",rule="..dump(rule))
@ -204,14 +228,12 @@ local function get_navigation_rules_for_node(state, node_pos)
end
local function gen_navigation_rules(state)
local node_base = state.node_base
local area = state.area
local navigation_rules = state.navigation_rules
local miny = state.bound.min.y
local maxy = state.bound.max.y
local minx = state.bound.min.x
local maxx = state.bound.max.x
-- Generate raw navigation rules for every node in the mapblock
for z = (state.bound.min.z),(state.bound.max.z) do
for y = miny,maxy do
for x = minx,maxx do
@ -220,19 +242,124 @@ local function gen_navigation_rules(state)
end
end
-- Invert navigation rules. Currently the data is all connections that you can go
-- to from a given node. We need all the connections that will get you to a given
-- node so that we can generate a gradient lookup table to get to any node from
-- any other node in this mapblock
local area = state.area
local inverted_navigation_rules = state.inverted_navigation_rules
for vm_idx,rule in pairs(state.navigation_rules) do
if rule.neighbors then
for _,neighbor_dir in pairs(rule.neighbors) do
local neighbor_pos = vector.add(neighbor_dir, rule.pos)
local neighbor_idx = area:index(neighbor_pos.x, neighbor_pos.y, neighbor_pos.z)
if neighbor_idx then
local neighbor_inverted_rules = inverted_navigation_rules[neighbor_idx]
if not neighbor_inverted_rules then
neighbor_inverted_rules = { count=0 }
inverted_navigation_rules[neighbor_idx] = neighbor_inverted_rules
end
local new_count = neighbor_inverted_rules.count + 1
neighbor_inverted_rules.count = new_count
neighbor_inverted_rules[new_count] = vector.multiply(neighbor_dir, -1)
end
end
end
end
print("Compression mapping = "..dump(state.navigation_lut.compression_mapping))
end
local function gen_navigation_lut(state, target_node_pos)
-- Based on Dijkstra's Algorithm for shortest path calculation
-- We want the minimum distance from every navigable node to
-- `node_pos'
local comp_map = state.navigation_lut.compression_mapping
local hash_pos = minetest.hash_node_position
local area = state.area
local distances = {}
local inv_nav_rules = state.inverted_navigation_rules
-- Start with `node_pops' as the only open node with a distance of 0
local open = { target_node_pos }
distances[comp_map[hash_pos(target_node_pos)].idx] = 0
-- While we have nodes left to compute a distance for
while #open > 0 do
-- Select next node to process. This will be the node
-- with the shortest dinstance of all open nodes
local next_node_idx = 1
local min_distance = nil
for i,node in ipairs(open) do
local distance = distances[comp_map[hash_pos(node)].idx]
if not min_distance or distance < min_distance then
next_node_idx = i
min_distance = distance
end
end
-- Remove the next node from the open list
local node_pos = open[next_node_idx]
open[next_node_idx] = open[#open]
open[#open] = nil
-- Process the node
local node_hash = hash_pos(node_pos)
local node_idx = area:index(node_pos.x, node_pos.y, node_pos.z)
local inv_nav_rule = inv_nav_rules[node_idx]
local distance = min_distance
-- Update neighbor distances
for i=1,inv_nav_rule.count do
local neighbor_pos = vector.add( inv_nav_rule[i], node_pos )
local neighbor_idx = comp_map[hash_pos(neighbor_pos)].idx
local neighbor_dist = distances[neighbor_idx]
if not neighbor_dist then
-- Neighbor hasn't been seen before, add to open list
open[#open + 1] = neighbor_pos
end
if not neighbor_dist or distance + 1 < neighbor_dist then
distances[neighbor_idx] = distance + 1
end
end
end
-- Now that we have the shortest distance from every navigable node to the target
-- node, we can calculate a lookup table that gives us the next neighbor connection
-- to take that is along the shortest path to the target node
-- TODO: implement
-- for each node, pick the forward neighbor rule that has the lowest distance from
-- the target node, then encoded this into a string with 4-bit indexes, with an index
-- of zero meaning no path exists inside this mapblock to the target node
print("distances="..dump(distances))
end
-- Precomputes a lookup table for each node to get the next direction to take to get to any other node
-- in the map block. This will take some time, but shouldn't have to be done very often (only when a change
-- in the map data makes it impossible to get to the target node)
local function gen_navigation_lut(state)
-- TODO: implement
local function gen_all_navigation_luts(state)
local navigation_lut = state.navigation_lut
local count = navigation_lut.compression_mapping.count
navigation_lut.compression_mapping.count = nil
-- Compute the navigation data for each node that is navigable
for k,v in pairs(navigation_lut.compression_mapping) do
local pos = v.pos
gen_navigation_lut(state, pos)
end
end
mcl2_pathfinding.recalculate_pathfinding_data = function(pos, async)
local state = {
content_id_cache = {},
navigation_rules = {},
inverted_navigation_rules = {},
navigation_lut = {
compression_mapping = {
count = 0,
},
},
node_base = vector.new(
math.floor(pos.x / 16) * 16,
math.floor(pos.y / 16) * 16,
@ -254,7 +381,7 @@ mcl2_pathfinding.recalculate_pathfinding_data = function(pos, async)
local function process(state)
gen_navigation_rules(state)
gen_navigation_lut(state)
gen_all_navigation_luts(state)
return state.node_base,state.navigation_lut
end

View File

@ -16,3 +16,19 @@ minetest.register_chatcommand("test_pathfinding",{
end
})
if minetest.get_modpath("worldedit_commands") then
minetest.register_chatcommand("test_path",{
description = S("Test path planning"),
params = "",
privs = { worldedit=true },
func = function(name)
local start = worldedit.pos1[name]
local stop = worldedit.pos2[name]
mcl2_pathfinding.clear_path_debug()
local path = mcl2_pathfinding.find_path(start, stop)
mcl2_pathfinding.debug_path(path)
end
})
end

View File

@ -1,3 +1,4 @@
name = mcl2_pathfinding
author = teknomunk
description = Adds a pathfinding API
optional_depends = worldedit_commands