Spider enemy that uses pathfinding

master
Auri 2021-12-10 23:38:06 -08:00
parent d947671e8a
commit 808fdd9a00
18 changed files with 323 additions and 128 deletions

View File

@ -1,2 +1,4 @@
time_speed = 0
max_forceloaded_blocks = 10000
active_object_send_range_blocks = 8
active_block_range = 16

3
mods/enemy/init.lua Normal file
View File

@ -0,0 +1,3 @@
local path = minetest.get_modpath('enemy') .. '/'
dofile(path .. 'script/enemy/spider.lua')

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,78 @@
minetest.register_entity('enemy:spider', {
visual = 'mesh',
visual_size = vector.new(14, 14, 14),
textures = { 'enemy_spider.png' },
mesh = 'enemy_spider.b3d',
physical = true,
collisionbox = { -0.25, -0.5, -0.25, 0.25, 0.25, 0.25 },
on_activate = function(self, static_data)
self.object:set_acceleration({x = 0, y = -10, z = 0})
self.object:set_animation({ x = 0, y = 15 }, 30, 0, true)
end,
on_punch = function(self)
self.object:remove()
end,
on_step = function(self, dtime, collision)
if not self.path then
if not navigation.graph then return end
self.path = navigation.find_path(
navigation.graph, vector.round(self.object:get_pos()), navigation.graph.player_spawn)
if not self.path then return end
self.path_index = #self.path - 1
for _, node in ipairs(self.path) do
minetest.add_particle({
pos = node,
size = 16,
expirationtime = 3,
texture = 'navigation_indicator.png'
})
end
end
local pos_2d = self.object:get_pos()
pos_2d.y = 0
local target = self.path[self.path_index]
if target == nil then return end
local target_2d = { x = target.x, y = 0, z = target.z }
local target_dist = vector.distance(pos_2d, target_2d)
if target_dist < 0.2 then
self.path_index = self.path_index - 1
if self.path_index < 1 then
self.path = nil
self.path_index = nil
return
end
else
local current_dir = self.object:get_rotation().y + math.pi / 2
local target_dir_vec = vector.direction(pos_2d, target_2d)
local target_dir = math.atan2(target_dir_vec.z, target_dir_vec.x)
local to_dir = current_dir + (target_dir - current_dir) * 0.1
local to_dir_vec = { x = math.cos(to_dir), y = 0, z = math.sin(to_dir) }
self.object:set_rotation({ x = 0, y = to_dir - math.pi / 2, z = 0 })
self.object:set_velocity({x = target_dir_vec.x * 3, y = self.object:get_velocity().y, z = target_dir_vec.z * 3})
if collision.touching_ground and target.y > self.object:get_pos().y then
self.object:set_velocity({x = self.object:get_velocity().x, y = 5, z = self.object:get_velocity().z })
end
end
end
})
minetest.register_chatcommand('spawnenemy', {
params = '<type>',
description = 'Spawns an enemy',
func = function(name, type)
local player = minetest.get_player_by_name(name)
minetest.add_entity(vector.round(player:get_pos()), 'enemy:spider', type)
end
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -6,10 +6,6 @@ minetest.register_entity('machine:mining_drill_entity', {
pointable = false,
on_activate = function(self, static_data)
self.node_pos = (minetest.deserialize(static_data) or {}).node_pos
if not self.node_pos then
self.object:remove()
return
end
minetest.after(math.random() * 10, function() self.object:set_animation({ x = 0, y = 375 }, 30, 0, true) end)
end,
get_staticdata = function(self)

View File

@ -1,24 +1,24 @@
_G['pathfinding'] = {}
_G['navigation'] = {}
dofile(minetest.get_modpath('pathfinding') .. '/script/load.lua')
dofile(minetest.get_modpath('pathfinding') .. '/script/astar.lua')
dofile(minetest.get_modpath('pathfinding') .. '/script/nodes.lua')
dofile(minetest.get_modpath('pathfinding') .. '/script/graph.lua')
dofile(minetest.get_modpath('navigation') .. '/script/load.lua')
dofile(minetest.get_modpath('navigation') .. '/script/astar.lua')
dofile(minetest.get_modpath('navigation') .. '/script/nodes.lua')
dofile(minetest.get_modpath('navigation') .. '/script/graph.lua')
local graph = nil
navigation.graph = nil
local spawn_pos = { x = 9, y = 2, z = -36 }
local map_min = { x = -85, y = -38, z = -156 }
local map_size = { x = 320, y = 160, z = 320 }
function init_graph()
local res, graph_duration = pathfinding.build_graph(spawn_pos)
graph = res
minetest.chat_send_all('Graphed ' .. graph.count .. ' nodes in ' .. graph_duration .. ' ms.')
local res, graph_duration = navigation.build_graph(spawn_pos)
navigation.graph = res
minetest.chat_send_all('Graphed ' .. navigation.graph.count .. ' nodes in ' .. graph_duration .. ' ms.')
end
minetest.register_chatcommand('refresh_graph', {
params = '',
description = 'Graphs pathfinding nodes',
description = 'Graphs navigation nodes',
func = function(name)
init_graph()
end
@ -28,9 +28,16 @@ minetest.register_chatcommand('test_paths', {
params = '',
description = 'Graphs paths from enemy spawns to player spawn',
func = function(name)
for i, spawn in ipairs(graph.enemy_spawns) do
if navigation.graph == nil then
minetest.chat_send_all('Graph not initialized')
return
end
minetest.chat_send_all('Graphing paths.')
for i, spawn in ipairs(navigation.graph.enemy_spawns) do
local start_time = minetest.get_us_time()
local path = pathfinding.find_path(graph, spawn, graph.player_spawn, i + 4)
local path = navigation.find_path(navigation.graph, spawn, navigation.graph.player_spawn, i + 4)
local duration = math.floor((minetest.get_us_time() - start_time) / 1000)
if path then
@ -39,7 +46,7 @@ minetest.register_chatcommand('test_paths', {
pos = node,
size = 16,
expirationtime = 3,
texture = 'pathfinding_indicator.png'
texture = 'navigation_indicator.png'
})
end
@ -52,7 +59,7 @@ minetest.register_chatcommand('test_paths', {
})
minetest.after(1, function()
pathfinding.load_area(map_min, vector.add(map_min, map_size), function(_, loaded, forceload_duration)
navigation.load_area(map_min, vector.add(map_min, map_size), function(_, loaded, forceload_duration)
minetest.chat_send_all('Force loaded ' .. loaded .. ' blocks in ' .. forceload_duration .. 'ms.')
init_graph()
end)

View File

@ -8,7 +8,7 @@
-- @returns the path from the start to the end, or nil if no path was found.
--
function pathfinding.find_path(graph, from, to)
function navigation.find_path(graph, from, to)
local to_pos_str = minetest.pos_to_string(to)
local closed = {}
local open = {}
@ -69,15 +69,15 @@ function pathfinding.find_path(graph, from, to)
break
end
for adj, cost in pairs(pathfinding.adjacent_list) do
for adj, cost in pairs(navigation.adjacent_list) do
local adj_pos = { x = lowest.pos.x + adj.x, y = lowest.pos.y + adj.y, z = lowest.pos.z + adj.z }
local adj_pos_str = minetest.pos_to_string(adj_pos)
local score = get_node_score(adj_pos)
if score ~= nil and closed[adj_pos_str] == nil then
local existing = open[adj_pos_str]
if existing == nil or lowest.g + cost <= existing.g then
add_node_to_open(adj_pos, lowest, cost, adj_pos_str)
if existing == nil or lowest.g + cost + score <= existing.g then
add_node_to_open(adj_pos, lowest, cost + score, adj_pos_str)
end
end
end

View File

@ -1,5 +1,5 @@
-- Positions to be considered 'adjacent' for graph generation and pathfinding.
pathfinding.adjacent_list = {
-- Positions to be considered 'adjacent' for graph generation and navigation.
navigation.adjacent_list = {
[{ x = 1, y = -1, z = 0 }] = 1,
[{ x = -1, y = -1, z = 0 }] = 1,
[{ x = 0, y = -1, z = 1 }] = 1,
@ -18,21 +18,8 @@ pathfinding.adjacent_list = {
[{ x = 0, y = 1, z = -1 }] = 1
}
-- Nodes to be considered valid map nodes.
local map_nodes = {
['pathfinding:navigation'] = true,
['pathfinding:navigation_hidden'] = true,
['pathfinding:player_spawn'] = true,
['pathfinding:player_spawn_hidden'] = true,
['pathfinding:enemy_spawn'] = true,
['pathfinding:enemy_spawn_hidden'] = true
}
-- Nodes to be considered enemy spawn points
local enemy_spawn_nodes = {
['pathfinding:enemy_spawn'] = true,
['pathfinding:enemy_spawn_hidden'] = true
}
local BASE_COST = 10
local MAGNET_STRENGTH = 10
--
-- Builds the map graph by recursively scanning the map for map nodes.
@ -42,11 +29,31 @@ local enemy_spawn_nodes = {
-- @returns the graph of the map, and the time to generate it.
--
function pathfinding.build_graph(start)
function navigation.build_graph(start)
local scan_nodes = {}
local magnet_nodes = {}
local enemy_spawn_nodes = {}
for name, def in pairs(minetest.registered_nodes) do
if def.groups['nav_traversable'] then
scan_nodes[name] = true
if def._navigation.magnet ~= 0 then
magnet_nodes[name] = def._navigation.magnet
end
if def._navigation.spawn == 'enemy' then
enemy_spawn_nodes[name] = true
end
end
end
local start_time = minetest.get_us_time()
local scanned = { minetest.pos_to_string(start) }
local to_scan = { start }
local magnet_positions = {}
local graph = {
-- The player's spawn point vector.
@ -70,22 +77,49 @@ function pathfinding.build_graph(start)
if not graph.nodes[pos.y] then graph.nodes[pos.y] = {} end
if not graph.nodes[pos.y][pos.x] then graph.nodes[pos.y][pos.x] = {} end
if not graph.nodes[pos.y][pos.x][pos.z] then graph.nodes[pos.y][pos.x][pos.z] = 1 end
if not graph.nodes[pos.y][pos.x][pos.z] then graph.nodes[pos.y][pos.x][pos.z] = BASE_COST end
for adj, _ in pairs(pathfinding.adjacent_list) do
for adj, _ in pairs(navigation.adjacent_list) do
local adj_pos = { x = pos.x + adj.x, y = pos.y + adj.y, z = pos.z + adj.z }
local adj_pos_str = minetest.pos_to_string(adj_pos)
if not scanned[adj_pos_str] then
local node = minetest.get_node(adj_pos)
if map_nodes[node.name] then
if scan_nodes[node.name] then
table.insert(to_scan, adj_pos)
scanned[adj_pos_str] = true
if enemy_spawn_nodes[node.name] then
table.insert(graph.enemy_spawns, adj_pos)
end
if magnet_nodes[node.name] then
table.insert(magnet_positions, adj_pos)
end
end
end
end
end
for _, pos in ipairs(magnet_positions) do
local val = magnet_nodes[minetest.get_node(pos).name]
local radius = math.abs(val)
for x = pos.x - radius, pos.x + radius do
for y = pos.y - radius, pos.y + radius do
for z = pos.z - radius, pos.z + radius do
local strength = math.max(1 - (vector.distance(pos, { x = x, y = y, z = z }) / radius), 0) *
(val < 0 and -1 or 1)
if strength ~= 0 then
local res_cost = BASE_COST - strength * MAGNET_STRENGTH
if graph.nodes[y] and graph.nodes[y][x] then
local cur_value = graph.nodes[y][x][z]
if cur_value and ((strength > 0 and cur_value > res_cost) or (strength < 0 and cur_value < res_cost)) then
graph.nodes[y][x][z] = res_cost
end
end
end
end
end
end

View File

@ -21,7 +21,7 @@ end
-- @param callback - The callback to call when the area is emerged.
--
function pathfinding.load_area(min_pos, max_pos, cb)
function navigation.load_area(min_pos, max_pos, cb)
local start_time = minetest.get_us_time()
local function free()

View File

@ -0,0 +1,159 @@
-- Whether or not the navigation nodes are visible.
local nav_visible = false
--
-- Registers an invisible navigation node that defines the navmesh of the map.
-- You can toggle the visibility and targeting of the node with /toggle_paths.
-- The properties below determine the functionality of the node.
--
-- @param def - The definition of the node, with the following keys:
-- - name: The name of the node.
-- - color: The color of the node when paths are toggled on.
-- - traversable: Whether or not the node is traversable by enemies.
-- - placeable: Whether or not items can be placed on this node.
-- - collidable: Whether or not the player collides with the node.
-- If true, functions as a barrier.
-- - magnet: If the node functions as an attractive or repulsive magnet to navigation.
-- A positive number defines an attractive magnet of the radius supplied.
-- A negative number defines a repulsive magnet of the absolute value of the radius supplied.
-- - spawn: The type of spawnpoint this node is
-- nil, 'player', 'enemy'
--
function register_nav_node(def)
local navigation = {
placeable = def.placeable,
traversable = def.traversable,
magnet = def.magnet,
spawn = def.spawn
}
minetest.register_node('navigation:' .. def.name, {
description = def.name,
drawtype = 'glasslike_framed',
tiles = {
'navigation_indicator_frame.png^[multiply:' .. def.color,
'navigation_indicator.png^[multiply:' .. def.color
},
walkable = def.collidable or false,
paramtype = 'light',
sunlight_propagates = true,
drop = '',
groups = {
nav_node = 1,
nav_traversable = def.traversable and 1 or 0,
nav_visible = 1,
creative_dig = 1
},
_navigation = navigation,
on_place = function(stack, player, target)
local pos = target.above
if nav_visible then minetest.set_node(pos, { name = 'navigation:' .. def.name })
else minetest.set_node(pos, { name = 'navigation:' .. def.name .. '_hidden' }) end
return stack
end
})
minetest.register_node('navigation:' .. def.name .. '_hidden', {
description = def.name,
drawtype = 'airlike',
walkable = def.collidable or false,
pointable = false,
paramtype = 'light',
sunlight_propagates = true,
drop = '',
groups = {
nav_node = 1,
nav_traversable = def.traversable and 1 or 0,
nav_hidden = 1,
creative_dig = 1,
not_in_creative_inventory = 1
},
_navigation = navigation
})
end
--
-- Toggles the visibility of the navigation nodes
-- using active block modifiers and commands.
--
minetest.register_abm({
label = 'Make navigation nodes visible',
nodenames = { 'group:nav_hidden' },
interval = 1,
chance = 1,
min_y = -150,
max_y = 150,
action = function(pos, node)
if not nav_visible then return end
local node_name = node.name:gsub('_hidden', '')
minetest.set_node(pos, { name = node_name })
end
})
minetest.register_abm({
label = 'Making navigation nodes hidden',
nodenames = { 'group:nav_visible' },
interval = 1,
chance = 1,
min_y = -150,
max_y = 150,
action = function(pos, node)
if nav_visible then return end
local node_name = node.name .. '_hidden'
minetest.set_node(pos, { name = node_name })
end
})
minetest.register_chatcommand('toggle_nav', {
description = 'Toggle nav node visibility',
func = function() nav_visible = not nav_visible end
})
--
-- Register the nav nodes.
--
register_nav_node({
name = 'nav',
color = '#5CD9FF',
traversable = true,
placeable = true
})
register_nav_node({
name = 'nav_positive_magnet',
color = '#3DFF7E',
traversable = true,
placeable = true,
magnet = 5
})
register_nav_node({
name = 'nav_negative_magnet',
color = '#F56642',
traversable = true,
placeable = true,
magnet = -5
})
register_nav_node({
name = 'barrier',
color = '#FF33A7',
collidable = true
})
register_nav_node({
name = 'player_spawn',
color = '#FFED47',
traversable = true,
spawn = 'player'
})
register_nav_node({
name = 'enemy_spawn',
color = '#C53DFF',
traversable = true,
spawn = 'enemy'
})

View File

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

View File

@ -1,84 +0,0 @@
local nodes_visible = false
function register_pathfinding_node(name, color, def)
minetest.register_node('pathfinding:' .. name, table.merge({
description = name,
drawtype = 'glasslike_framed',
tiles = { 'pathfinding_indicator_frame.png^[multiply:' .. color, 'pathfinding_indicator.png^[multiply:' .. color },
walkable = false,
paramtype = 'light',
sunlight_propagates = true,
drop = '',
groups = {
pathfinding = 1,
pathfinding_visible = 1,
['pathfinding_' .. name] = 1,
creative_dig = 1
},
on_place = function(stack, player, target)
local pos = target.above
if nodes_visible then minetest.set_node(pos, { name = 'pathfinding:' .. name })
else minetest.set_node(pos, { name = 'pathfinding:' .. name .. '_hidden' }) end
return stack
end
}, def or {}))
minetest.register_node('pathfinding:' .. name .. '_hidden', table.merge({
description = name,
drawtype = 'airlike',
walkable = false,
pointable = false,
paramtype = 'light',
sunlight_propagates = true,
drop = '',
groups = {
pathfinding = 1,
pathfinding_hidden = 1,
['pathfinding_' .. name] = 1,
creative_dig = 1,
not_in_creative_inventory = 1
}
}, def))
end
minetest.register_abm({
label = 'Making pathfinding nodes visible',
nodenames = { 'group:pathfinding_hidden' },
interval = 1,
chance = 1,
min_y = -300,
max_y = 300,
action = function(pos, node)
if not nodes_visible then return end
local node_name = node.name:gsub('_hidden', '')
minetest.set_node(pos, { name = node_name })
end
})
minetest.register_abm({
label = 'Making pathfinding nodes hidden',
nodenames = { 'group:pathfinding_visible' },
interval = 1,
chance = 1,
min_y = -300,
max_y = 300,
action = function(pos, node)
if nodes_visible then return end
local node_name = node.name .. '_hidden'
minetest.set_node(pos, { name = node_name })
end
})
minetest.register_chatcommand('toggle_paths', {
params = '',
description = 'Toggles pathfinding node visibility',
func = function()
nodes_visible = not nodes_visible
end
})
register_pathfinding_node('navigation', '#5CD9FF')
register_pathfinding_node('navigation_magnet', '#3DFF7E')
register_pathfinding_node('barrier', '#FF33A7', { walkable = true })
register_pathfinding_node('player_spawn', '#FFED47')
register_pathfinding_node('enemy_spawn', '#C53DFF')

0
mods/power/init.lua Normal file
View File

Binary file not shown.