Improve pathfinding

master
Lean Rada 2015-02-02 02:03:46 +08:00
parent 23a0e6bd4e
commit dc4ef98f27
5 changed files with 195 additions and 84 deletions

View File

@ -1,7 +1,7 @@
defense.director = {}
local director = defense.director
director.call_interval = 1.0
director.intensity_decay = 0.92
director.intensity_decay = 0.93
director.max_entities = 50
director.spawn_list = {
{
@ -20,12 +20,12 @@ director.spawn_list = {
description = "Unggoy horde",
name = "defense:unggoy",
intensity_min = 0.0,
intensity_max = 0.0,
intensity_max = 0.1,
group_min = 21,
group_max = 24,
probability = 0.8,
day_start = 1,
spawn_time = 71.0,
spawn_time = 31.0,
spawn_location = "ground",
},
{
@ -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 and not defense.debug then
if defense:is_dark() and #minetest.luaentities < self.max_entities then
self:spawn_monsters()
end
@ -239,8 +239,8 @@ function director:update_intensity()
local mob_count = #minetest.luaentities
local delta =
-0.16 * math.min(0.06, average_health - last_average_health)
+ 2.0 * math.min(0, 1 / average_health)
-0.2 * (average_health - last_average_health)
+ 4.0 * math.max(0, 1 / average_health - 0.1)
+ 0.006 * (mob_count - last_mob_count)
last_average_health = average_health

View File

@ -24,7 +24,7 @@ end
function defense:is_dark()
local tod = minetest.get_timeofday()
return tod < 0.21 or tod > 0.8 or defense.debug
return tod < 0.2 or tod > 0.8 or defense.debug
end
dofile2("util.lua")

View File

@ -39,15 +39,11 @@ function mobs.default_prototype:on_activate(staticdata)
end
function mobs.default_prototype:on_step(dtime)
local destination_distance = 0
if self.destination then
destination_distance = vector.distance(self.object:getpos(), self.destination)
end
if self.pause_timer <= 0 then
if self.destination then
self:move(dtime, self.destination)
if destination_distance < 1 then
if vector.distance(self.object:getpos(), self.destination) < 0.5 then
self.destination = nil
end
else
@ -61,6 +57,7 @@ function mobs.default_prototype:on_step(dtime)
self:set_animation("fall", {"jump", "attack", "move_attack"})
end
-- Die when morning comes
if not defense:is_dark() then
local damage = self.object:get_hp() * math.random()
if damage >= 0.5 then
@ -68,14 +65,29 @@ function mobs.default_prototype:on_step(dtime)
end
end
-- Remove when far enough and may not reach the player at all
local nearest = self:find_nearest_player()
if self.life_timer <= 0 then
if destination_distance > 6 then
if nearest.distance > 6 then
self.object:remove()
end
else
self.life_timer = self.life_timer - dtime
end
-- Disable collision when far enough
if self.collide_with_objects then
if nearest.distance > 8 then
self.collide_with_objects = false
self.object:set_properties({collide_with_objects = self.collide_with_objects})
end
else
if nearest.distance < 3 then
self.collide_with_objects = true
self.object:set_properties({collide_with_objects = self.collide_with_objects})
end
end
self.timer = self.timer + dtime
end
@ -134,7 +146,14 @@ function mobs.default_prototype:hunt()
end
if direction then
self.destination = vector.add(pos, vector.multiply(direction, 1.25))
local sx = self.collisionbox[4] - self.collisionbox[1]
local sy = self.collisionbox[5] - self.collisionbox[2]
local sz = self.collisionbox[6] - self.collisionbox[3]
local r = math.sqrt(sx*sx + sy*sy + sz*sz)/2 + 1
local x = pos.x + direction.x * r
local y = pos.y + direction.y * r
local z = pos.z + direction.z * r
self.destination = {x=x, y=y, z=z}
else
local r = math.max(0, self.attack_range - 2)
local dir = vector.direction(nearest.position, self.object:getpos())
@ -261,7 +280,7 @@ function mobs.move_method:air(dtime, destination)
z=math.sin(r_angle)*r_radius
})
local speed = self.move_speed * math.max(0, math.min(1, 0.8 * dist))
local speed = self.move_speed * math.max(0, math.min(1, 1.2 * dist))
local t
local v = self.object:getvelocity()
if vector.length(v) < self.move_speed * 1.5 then
@ -302,7 +321,7 @@ function mobs.move_method:ground(dtime, destination)
z=math.sin(r_angle)*r_radius
})
local speed = self.move_speed * math.max(0, math.min(1, 0.8 * dist))
local speed = self.move_speed * math.max(0, math.min(1, 1.2 * dist))
local t
local v = self.object:getvelocity()
if self:is_standing() and vector.length(v) < self.move_speed * 4 then
@ -319,25 +338,32 @@ function mobs.move_method:ground(dtime, destination)
v2.y = v.y
self.object:setvelocity(v2)
-- Check for obstacle to jump
-- Check for jump
local jump = false
if dist > 1 then
if self.smart_path then
local p = self.object:getpos()
p.y = p.y + self.collisionbox[2] + 0.5
local sx = self.collisionbox[4] - self.collisionbox[1]
local sz = self.collisionbox[6] - self.collisionbox[3]
local r = math.sqrt(sx*sx + sz*sz)/2 + 0.5
local fronts = {
{x = dir.x * self.jump_height, y = 0, z = dir.z * self.jump_height},
{x = dir.x * r, y = 0, z = dir.z * r},
{x = dir.x + dir.z * r, y = 0, z = dir.z + dir.x * r},
{x = dir.x - dir.z * r, y = 0, z = dir.z - dir.x * r},
}
for _,f in ipairs(fronts) do
local node = minetest.get_node_or_nil(vector.add(p, f))
if not node or minetest.registered_nodes[node.name].walkable then
jump = true
break
if destination.y > p.y + 0.5 then
jump = true
end
else
if dist > 1 then
local p = self.object:getpos()
p.y = p.y + self.collisionbox[2] + 0.5
local sx = self.collisionbox[4] - self.collisionbox[1]
local sz = self.collisionbox[6] - self.collisionbox[3]
local r = math.sqrt(sx*sx + sz*sz)/2 + 0.5
local fronts = {
{x = dir.x * self.jump_height, y = 0, z = dir.z * self.jump_height},
{x = dir.x * r, y = 0, z = dir.z * r},
{x = dir.x + dir.z * r, y = 0, z = dir.z + dir.x * r},
{x = dir.x - dir.z * r, y = 0, z = dir.z - dir.x * r},
}
for _,f in ipairs(fronts) do
local node = minetest.get_node_or_nil(vector.add(p, f))
if not node or minetest.registered_nodes[node.name].walkable then
jump = true
break
end
end
end
end
@ -380,6 +406,7 @@ function mobs.register_mob(name, def)
z = math.ceil(prototype.collisionbox[6] - prototype.collisionbox[3])
},
collisionbox = prototype.collisionbox,
jump_height = math.floor(prototype.jump_height),
cost_method = def.pathfinder_cost or defense.pathfinder.cost_method[prototype.movement]
})
end

View File

@ -38,13 +38,13 @@ defense.mobs.register_mob("defense:unggoy", {
if math.random() < 0.1 then
self.destination = vector.add(
self.object:getpos(),
{x=math.random(-10,10),y=0,z=math.random(-10,10)}
{x=math.random(-4,4),y=0,z=math.random(-4,4)}
)
elseif math.random() < 0.1 then
self.wander = false
end
else
if math.random() < 0.05 then
if math.random() < 0.006 then
self.wander = true
else
self:hunt()
@ -60,6 +60,11 @@ defense.mobs.register_mob("defense:unggoy", {
if defense.mobs.default_prototype.is_standing(self) then
return true
else
local vel = self.object:getvelocity()
if math.abs(vel.y) > 0.05 then
return false
end
local pos = self.object:getpos()
pos.y = pos.y - 1
for _,o in ipairs(minetest.get_objects_inside_radius(pos, 1)) do

View File

@ -1,15 +1,28 @@
defense.pathfinder = {}
local pathfinder = defense.pathfinder
pathfinder.path_max_range = 16
pathfinder.fields = {}
pathfinder.path_max_range = 32
pathfinder.classes = {}
local visit_queues = {}
local chunk_size = 16
-- State
local fields = {}
local visit_queues = {}
local player_last_update = {}
local morning_reset = false
-- local cid_data = {}
-- minetest.after(0, function()
-- for name, def in pairs(minetest.registered_nodes) do
-- cid_data[minetest.get_content_id(name)] = {
-- name = name,
-- walkable = def.walkable,
-- }
-- end
-- end)
function pathfinder:register_class(class, properties)
self.fields[class] = self.fields[class] or {}
self.classes[class] = properties
fields[class] = fields[class] or {}
visit_queues[class] = Queue.new()
end
@ -24,20 +37,42 @@ end
-- Returns a vector
function pathfinder:get_direction(class, position)
local field = self:get_field(class, position)
if not field then
local total = vector.new(0, 0, 0)
local count = 0
local time = minetest.get_gametime()
local cells = {
position,
{x=position.x + 1, y=position.y, z=position.z},
{x=position.x - 1, y=position.y, z=position.z},
{x=position.x, y=position.y + 1, z=position.z},
{x=position.x, y=position.y - 1, z=position.z},
{x=position.x, y=position.y, z=position.z + 1},
{x=position.x, y=position.y, z=position.z - 1},
}
for _,p in ipairs(cells) do
local field = self:get_field(class, p)
if field then
local last_time = player_last_update[field.player] or field.time
if last_time + field.distance > time then
local direction = ({{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]
total = vector.add(total, direction)
count = count + 1
end
end
end
if count > 0 then
return vector.normalize(total)
else
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}
@ -50,7 +85,7 @@ function pathfinder:get_field(class, position)
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]
local chunk = fields[class][chunk_key]
if not chunk then
return nil
end
@ -62,7 +97,7 @@ function pathfinder:get_field(class, position)
return chunk[index]
end
function pathfinder:set_field(class, position, distance, direction, time)
function pathfinder:set_field(class, position, player, 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])
@ -71,24 +106,33 @@ function pathfinder:set_field(class, position, distance, direction, time)
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]
local chunk = fields[class][chunk_key]
if not chunk then
chunk = {}
self.fields[class][chunk_key] = chunk
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}
chunk[index] = {time=time, direction=direction, distance=distance, player=player}
end
function pathfinder:update(dtime)
if not defense:is_dark() then
-- reset flow fields
-- reset flow fields in the morning
if not morning_reset then
morning_reset = true
player_last_update = {}
for c,_ in pairs(self.classes) do
fields[c] = {}
visit_queues[c] = Queue.new()
end
end
return
end
morning_reset = false
local neighborhood = {
{x=1, y=0, z=0},
@ -102,15 +146,14 @@ function pathfinder:update(dtime)
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
for i=1,math.min(size,20) 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)
local cost = class.cost_method(class, npos, current.position)
if cost then
local next_distance = current.distance + cost
local neighbor_field = self:get_field(c, npos)
@ -119,10 +162,11 @@ function pathfinder:update(dtime)
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
self:set_field(c, npos, current.player, next_distance, di, current.time)
if next_distance < self.path_max_range and size < 100 then
Queue.push(vq, {
position = npos,
player = current.player,
distance = next_distance,
direction = di,
time = current.time,
@ -138,23 +182,27 @@ function pathfinder:update(dtime)
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})
for y=pos.y+0.1,pos.y+2 do
local tp = {x=pos.x, y=y, z=pos.z}
local field = self:get_field(c, tp)
if not field or field.distance > 0 then
local name = p:get_player_name()
self:set_field(c, pos, name, 0, 4, time)
Queue.push(visit_queues[c], {position=tp, player=name, distance=0, direction=0, time=time})
player_last_update[name] = time
end
end
end
end
end
pathfinder.cost_method = {}
function pathfinder.cost_method.air(pos, parent, size)
function pathfinder.cost_method.air(class, pos, parent)
-- 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
for y=pos.y,pos.y+class.size.y-1 do
for z=pos.z,pos.z+class.size.z-1 do
for x=pos.x,pos.x+class.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
@ -165,33 +213,64 @@ function pathfinder.cost_method.air(pos, parent, size)
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
function pathfinder.cost_method.ground(class, pos, parent)
-- Check if in solid
for z=pos.z,pos.z+class.size.z-1 do
for x=pos.x,pos.x+class.size.x-1 do
for y=pos.y,pos.y+class.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
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})
-- Check if this is a fall
if parent.y < pos.y then
return 2
end
-- Check if on top of solid
local ground_distance = 9999
for z=pos.z,pos.z+class.size.z-1 do
for x=pos.x,pos.x+class.size.x-1 do
for y=pos.y-1,pos.y-class.jump_height-1,-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
on_ground = true
ground_distance = math.min(ground_distance, pos.y - y)
if ground_distance == 1 then
return 1
end
break
end
end
end
end
if not on_ground then
if ground_distance > 1 then
if ground_distance <= class.jump_height + 1 then
local ledges = {
{x=pos.x + class.size.x, y=pos.y - 1, z=pos.z},
{x=pos.x - 1, y=pos.y - 1, z=pos.z},
{x=pos.x, y=pos.y - 1, z=pos.z + class.size.z},
{x=pos.x, y=pos.y - 1, z=pos.z - 1},
}
for _,l in ipairs(ledges) do
local node = minetest.get_node_or_nil(l)
if not node then return nil end
if minetest.registered_nodes[node.name].walkable then
return 1 + (ground_distance - 1)
end
end
end
return pathfinder.path_max_range + 1
end
return 1 + math.ceil(math.abs(pos.y - parent.y))
return 1
end
minetest.register_globalstep(function(dtime)