Implement pathfinding

master
Lean Rada 2015-02-01 05:05:47 +08:00
parent 5a2c575e14
commit bfa290bb4f
13 changed files with 302 additions and 35 deletions

23
mods/defense/Queue.lua Normal file
View File

@ -0,0 +1,23 @@
Queue = {}
function Queue.new ()
return {first = 0, last = -1}
end
function Queue.push (queue, value)
local last = queue.last + 1
queue.last = last
queue[last] = value
end
function Queue.pop (queue)
local first = queue.first
if first > queue.last then error("queue is empty") end
local value = queue[first]
queue[first] = nil
queue.first = first + 1
return value
end
function Queue.size (queue)
return queue.last - queue.first + 1
end

View File

@ -85,7 +85,7 @@ function director:on_interval()
end
if self.cooldown_timer <= 0 then
if defense:is_dark() and #minetest.luaentities < self.max_entities then
if defense:is_dark() and #minetest.luaentities < self.max_entities and not defense.debug then
self:spawn_monsters()
end

View File

@ -17,16 +17,6 @@ minetest.register_chatcommand("debug", {
end,
})
function minetest.wallmounted_to_dir(wallmounted)
return ({[0]={x=0, y=1, z=0},
{x=0, y=-1, z=0},
{x=1, y=0, z=0},
{x=-1, y=0, z=0},
{x=0, y=0, z=1},
{x=0, y=0, z=-1}})
[wallmounted]
end
local modpath = minetest.get_modpath("defense")
local function dofile2(file)
dofile(modpath .. "/" .. file)
@ -37,12 +27,16 @@ function defense:is_dark()
return tod < 0.21 or tod > 0.8 or defense.debug
end
dofile2("util.lua")
dofile2("Queue.lua")
dofile2("initial_stuff.lua")
dofile2("pathfinder.lua")
dofile2("director.lua")
dofile2("music.lua")
dofile2("mob.lua")
dofile2("mobs/unggoy.lua")
dofile2("mobs/sarangay.lua")
dofile2("mobs/paniki.lua")
dofile2("mobs/botete.lua")
dofile2("director.lua")
dofile2("music.lua")
dofile2("initial_stuff.lua")
dofile2("mobs/botete.lua")

View File

@ -11,6 +11,8 @@ mobs.default_prototype = {
stepheight = 0.6,
-- custom properties
id = 0,
smart_path = true,
mass = 1,
movement = "ground", -- "ground"/"air"
move_speed = 1,
jump_height = 1,
@ -121,15 +123,26 @@ end
function mobs.default_prototype:hunt()
local nearest = self:find_nearest_player()
if nearest.player then
local dir = vector.direction(nearest.position, self.object:getpos())
if nearest.distance <= self.attack_range then
self:do_attack(nearest.player)
end
if nearest.distance > self.attack_range or nearest.distance < self.attack_range/2-1 then
local r = math.max(0, self.attack_range - 2)
self.destination = vector.add(nearest.position, vector.multiply(dir, r))
local pos = self.object:getpos()
local direction = nil
if self.smart_path and nearest.distance < defense.pathfinder.path_max_range then
direction = defense.pathfinder:get_direction(self.name, pos)
end
if direction then
self.destination = vector.add(pos, vector.multiply(direction, 1.25))
else
local r = math.max(0, self.attack_range - 2)
local dir = vector.direction(nearest.position, self.object:getpos())
self.destination = vector.add(nearest.position, vector.multiply(dir, r))
end
end
end
end
function mobs.default_prototype:do_attack(obj)
@ -149,7 +162,11 @@ end
function mobs.default_prototype:jump(direction)
if self:is_standing() then
direction = vector.normalize(direction)
if direction then
direction = vector.normalize(direction)
else
direction = {x=0,y=0,z=0}
end
local v = self.object:getvelocity()
v.y = math.sqrt(2 * -mobs.gravity * (self.jump_height + 0.2))
v.x = direction.x * self.jump_height
@ -212,21 +229,19 @@ function mobs.default_prototype:find_nearest_player()
local nearest_pos = p
local nearest_dist = 9999
for _,obj in ipairs(minetest.get_connected_players()) do
if obj:is_player() then
if not nearest_player then
if not nearest_player then
nearest_player = obj
nearest_pos = obj:getpos()
nearest_pos.y = nearest_pos.y + 1
nearest_dist = vector.distance(nearest_pos, p)
else
local pos = obj:getpos()
pos.y = pos.y + 1
local d = vector.distance(pos, p)
if d < nearest_dist then
nearest_player = obj
nearest_pos = obj:getpos()
nearest_pos.y = nearest_pos.y + 1
nearest_dist = vector.distance(nearest_pos, p)
else
local pos = obj:getpos()
pos.y = pos.y + 1
local d = vector.distance(pos, p)
if d < nearest_dist then
nearest_player = obj
nearest_pos = pos
nearest_dist = d
end
nearest_pos = pos
nearest_dist = d
end
end
end
@ -357,5 +372,17 @@ function mobs.register_mob(name, def)
prototype.move = def.move or mobs.move_method[prototype.movement]
if defense.pathfinder and prototype.smart_path then
defense.pathfinder:register_class(name, {
size = {
x = math.ceil(prototype.collisionbox[4] - prototype.collisionbox[1]),
y = math.ceil(prototype.collisionbox[5] - prototype.collisionbox[2]),
z = math.ceil(prototype.collisionbox[6] - prototype.collisionbox[3])
},
collisionbox = prototype.collisionbox,
cost_method = def.pathfinder_cost or defense.pathfinder.cost_method[prototype.movement]
})
end
minetest.register_entity(name, prototype)
end

View File

@ -135,6 +135,7 @@ defense.mobs.register_mob("defense:botete", {
move_attack = {a=80, b=99, rate=25},
},
smart_path = false,
mass = 1,
movement = "air",
move_speed = 4,

View File

@ -1,6 +1,6 @@
defense.mobs.register_mob("defense:paniki", {
hp_max = 7,
collisionbox = {-0.3,-0.3,-0.3, 0.3,0.3,0.3},
collisionbox = {-0.4,-0.4,-0.4, 0.4,0.4,0.4},
mesh = "defense_paniki.b3d",
textures = {"defense_paniki.png"},
makes_footstep_sound = false,

View File

@ -27,6 +27,7 @@ defense.mobs.register_mob("defense:sarangay", {
start = {a=110, b=119, rate=15},
},
smart_path = false,
mass = 12,
move_speed = 6,
jump_height = 1,

View File

@ -50,6 +50,9 @@ defense.mobs.register_mob("defense:unggoy", {
self:hunt()
end
end
if math.random() < 0.05 then
self:jump()
end
end,
is_standing = function(self)

199
mods/defense/pathfinder.lua Normal file
View File

@ -0,0 +1,199 @@
defense.pathfinder = {}
local pathfinder = defense.pathfinder
pathfinder.path_max_range = 16
pathfinder.fields = {}
pathfinder.classes = {}
local visit_queues = {}
local chunk_size = 16
function pathfinder:register_class(class, properties)
self.fields[class] = self.fields[class] or {}
self.classes[class] = properties
visit_queues[class] = Queue.new()
end
-- Returns a number
-- function pathfinder:get_distance(class, position)
-- local field = self:get_field(class, position)
-- if not field then
-- return nil
-- end
-- return field.distance
-- end
-- Returns a vector
function pathfinder:get_direction(class, position)
local field = self:get_field(class, position)
if not field then
return nil
end
if field.distance == 0 then
return {x=0, y=0, z=0}
end
return ({{x=-1, y=0, z=0},
{x=1, y=0, z=0},
{x=0, y=-1, z=0},
{x=0, y=1, z=0},
{x=0, y=0, z=-1},
{x=0, y=0, z=1}})
[field.direction]
end
-- Returns a table {time, distance}
function pathfinder:get_field(class, position)
local collisionbox = self.classes[class].collisionbox
local x = math.floor(position.x + collisionbox[1])
local y = math.floor(position.y + collisionbox[2])
local z = math.floor(position.z + collisionbox[3])
local chunk_key = math.floor(x/chunk_size) ..
":" .. math.floor(y/chunk_size) ..
":" .. math.floor(z/chunk_size)
local chunk = self.fields[class][chunk_key]
if not chunk then
return nil
end
local cx = x % chunk_size
local cy = y % chunk_size
local cz = z % chunk_size
local index = (cy * chunk_size + cz) * chunk_size + cx
return chunk[index]
end
function pathfinder:set_field(class, position, distance, direction, time)
local collisionbox = self.classes[class].collisionbox
local x = math.floor(position.x + collisionbox[1])
local y = math.floor(position.y + collisionbox[2])
local z = math.floor(position.z + collisionbox[3])
local chunk_key = math.floor(x/chunk_size) ..
":" .. math.floor(y/chunk_size) ..
":" .. math.floor(z/chunk_size)
local chunk = self.fields[class][chunk_key]
if not chunk then
chunk = {}
self.fields[class][chunk_key] = chunk
end
local cx = x % chunk_size
local cy = y % chunk_size
local cz = z % chunk_size
local index = (cy * chunk_size + cz) * chunk_size + cx
chunk[index] = {time=time, direction=direction, distance=distance}
end
function pathfinder:update(dtime)
if not defense:is_dark() then
-- reset flow fields
return
end
local neighborhood = {
{x=1, y=0, z=0},
{x=-1, y=0, z=0},
{x=0, y=1, z=0},
{x=0, y=-1, z=0},
{x=0, y=0, z=1},
{x=0, y=0, z=-1},
}
-- Update the field
for c,class in pairs(self.classes) do
local vq = visit_queues[c]
local size = Queue.size(vq)
minetest.debug(size)
for i=1,math.min(size,1000 * dtime) do
local current = Queue.pop(vq)
for di,n in ipairs(neighborhood) do
local npos = vector.add(current.position, n)
npos.x = math.floor(npos.x)
npos.y = math.floor(npos.y)
npos.z = math.floor(npos.z)
local cost = class.cost_method(npos, current.position, class.size)
if cost then
local next_distance = current.distance + cost
local neighbor_field = self:get_field(c, npos)
if not neighbor_field
or neighbor_field.time < current.time
and neighbor_field.direction ~= di
or neighbor_field.time == current.time
and neighbor_field.distance > next_distance then
self:set_field(c, npos, next_distance, di, current.time)
if next_distance < self.path_max_range then
Queue.push(vq, {
position = npos,
distance = next_distance,
direction = di,
time = current.time,
})
end
end
end
end
end
end
-- Update player positions
local time = minetest.get_gametime()
for _,p in ipairs(minetest.get_connected_players()) do
local pos = p:getpos()
pos.y = pos.y + 1
for c,_ in pairs(self.classes) do
local field = self:get_field(c, pos)
if not field or field.distance > 0 then
self:set_field(c, pos, 0, 0, time)
Queue.push(visit_queues[c], {position=pos, distance=0, direction=0, time=time})
end
end
end
end
pathfinder.cost_method = {}
function pathfinder.cost_method.air(pos, parent, size)
-- Check if solid
for y=pos.y,pos.y+size.y-1 do
for z=pos.z,pos.z+size.z-1 do
for x=pos.x,pos.x+size.x-1 do
local node = minetest.get_node_or_nil({x=x, y=y, z=z})
if not node then return nil end
if minetest.registered_nodes[node.name].walkable then
return pathfinder.path_max_range + 1
end
end
end
end
return 1
end
function pathfinder.cost_method.ground(pos, parent, size)
local on_ground = false
for z=pos.z,pos.z+size.z-1 do
for x=pos.x,pos.x+size.x-1 do
-- Check if solid
for y=pos.y,pos.y+size.y-1 do
local node = minetest.get_node_or_nil({x=x, y=y, z=z})
if not node then return nil end
if minetest.registered_nodes[node.name].walkable then
return pathfinder.path_max_range + 1
end
end
if not on_ground then
-- Check if on top of solid
local node = minetest.get_node_or_nil({x=x, y=pos.y-1, z=z})
if not node then return nil end
if minetest.registered_nodes[node.name].walkable then
on_ground = true
end
end
end
end
if not on_ground then
return pathfinder.path_max_range + 1
end
return 1 + math.ceil(math.abs(pos.y - parent.y))
end
minetest.register_globalstep(function(dtime)
pathfinder:update(dtime)
end)

Binary file not shown.

Binary file not shown.

19
mods/defense/util.lua Normal file
View File

@ -0,0 +1,19 @@
minetest.wallmounted_to_dir = minetest.wallmounted_to_dir or function(wallmounted)
return ({[0]={x=0, y=1, z=0},
{x=0, y=-1, z=0},
{x=1, y=0, z=0},
{x=-1, y=0, z=0},
{x=0, y=0, z=1},
{x=0, y=0, z=-1}})
[wallmounted]
end
math.sign = math.sign or function(x)
if x < 0 then
return -1
elseif x > 0 then
return 1
else
return 0
end
end