Hi, I need help, and I am not asking for help without trying hard to solve this myself.
I am making a mod that adds bugs. I need the bugs to follow a set path. That means that each ant will have a list of positions which they should walk to in order, then return to the first position. I added self variables to the mob definition to hold the instruction table, so self.ant_instructions is a table of positions. self.ant_goal_number holds the index of the position on that list which is the current goal of the ant
I need a do_custom function that will check if the bug is at its goal, and if it isn't, use pathfinding to move there. (then, of course, it updates self.goal).
I saw where on the bottom of pg 80 of this topic you posted the code for a pathfinding do_custom function. I tried to use that method, but I kept getting errors because self:smart_mobs() uses self.attack to know where its going, and has multiple other references to attack. Since each ant is *not* attacking, these lines cause errors from within the api when called from do_custom.
So then I spent several hours pouring through the smart_mobs function, trying to understand it, and I copied it and several helper functions to my mod as a local function, and tried to edit it. I finally got it so it didn't throw any errors when it ran, but my ants were going every which way, not reaching their goals, not returning home, and I don't know what is wrong.
There should be a simple solution: I just have no idea what it is, and my code is getting more and more complicated, and I am spending more and more time trying to understand it and the changes I am making. HELP!
here is where I am atm:
Code: Select all
--SETTINGS---
local max_obj = 50 -- what is the maxium number of objects after which no more ants will spawn
local stuck_timeout = 1 -- how long before stuck mod starts searching
local stuck_path_timeout = 10 -- how long will mob follow path before giving up
local enable_pathfinding = true
-- functions --
local get_horizantal_dist_sq = function(pos1,pos2)
local x1 = pos1.x
local z1 = pos1.z
local x2 = pos2.x
local z2 = pos2.z
return ((x2-x1)^2) + ((z2 - z1)^2)
end
---------------------------------------------------------Here lie copied codes--------------------------------------------------------
local get_distance = function(a, b)
local x, y, z = a.x - b.x, a.y - b.y, a.z - b.z
return (x * x + y * y + z * z)*(x * x + y * y + z * z)
end
--##############################################################
-- turn mob to face position
local yaw_to_pos = function(self, target, rot)
rot = rot or 0
local pos = self.object:get_pos()
local vec = {x = target.x - pos.x, z = target.z - pos.z}
local yaw = (atan(vec.z / vec.x) + rot + pi / 2) - self.rotate
if target.x > pos.x then
yaw = yaw + pi
end
yaw = self:set_yaw(yaw, 6)
return yaw
end
--############################################################
-- path finding and smart mob routine by rnd,
-- line_of_sight and other edits by Elkien3
-- edited to be a generic move_towards command by MisterE
function mob_pathfind_towards(self,s, p, dist, dtime) -- self is the mob's self object. s is the pos of the mob, p is the pos of the target. dist is the distance between the two, and pass dtime from the custom function
local s1 = self.path.lastpos -- this gets the last postion on the path for sticky situation detect
local target_pos = p
-- is it becoming stuck?
if math.abs(s1.x - s.x) + math.abs(s1.z - s.z) < .5 then
self.path.stuck_timer = self.path.stuck_timer + dtime
else
self.path.stuck_timer = 0
end
-- update last pos for next iter
self.path.lastpos = {x = s.x, y = s.y, z = s.z}
local use_pathfind = true --in case we dont?
local has_lineofsight = minetest.line_of_sight(
{x = s.x, y = (s.y) + .5, z = s.z},
{x = target_pos.x, y = (target_pos.y) + 1.5, z = target_pos.z}, .2)
-- im stuck, search for path
--if we dont have line of sight, and we used to, then turn on pathfinding
if not has_lineofsight then -- if we don't have line of sight to target, then
if self.los_switcher == true then
use_pathfind = true
self.los_switcher = false
end -- cannot see target!
else -- if has line_of_sight
if self.los_switcher == false then
self.los_switcher = true
use_pathfind = false
minetest.after(1, function(self) -- I think this can be translated to: after 1 sec, if the mob is still there,
if self.object:get_luaentity() then
if has_lineofsight then
self.path.following = false
end
end
end, self)
end -- can see target!
end
if (self.path.stuck_timer > stuck_timeout and not self.path.following) then --if we are stuck for too long and we are not already following a path
use_pathfind = true
self.path.stuck_timer = 0
minetest.after(1, function(self)
if self.object:get_luaentity() then
if has_lineofsight then
self.path.following = false
end
end
end, self)
end
if (self.path.stuck_timer > stuck_path_timeout and self.path.following) then
use_pathfind = true -- to use pathfind means to try to get a path
self.path.stuck_timer = 0
minetest.after(1, function(self)
if not self.object:get_luaentity() then
return
end
if self.object:get_luaentity() then
if has_lineofsight then
self.path.following = false
end
end
end, self)
end
if math.abs(vector.subtract(s,target_pos).y) > self.stepheight then
if self.height_switcher then
use_pathfind = true
self.height_switcher = false
end
else
if not self.height_switcher then
use_pathfind = false
height_switcher = true
end
end
-- lets try find a path, first take care of positions
-- since pathfinder is very sensitive
if use_pathfind then
-- round position to center of node to avoid stuck in walls
-- also adjust height for player models!
s.x = math.floor(s.x + 0.5)
s.z = math.floor(s.z + 0.5)
local ssight, sground = minetest.line_of_sight(s, {
x = s.x, y = s.y - 4, z = s.z}, 1)
-- determine node above ground
if not ssight then
s.y = sground.y + 1
end
local p1 = p
p1.x = math.floor(p1.x + 0.5)
p1.y = math.floor(p1.y + 0.5)
p1.z = math.floor(p1.z + 0.5)
local dropheight = 6
if self.fear_height ~= 0 then dropheight = self.fear_height end
local jumpheight = 0
if self.jump and self.jump_height >= 4 then
jumpheight = min(ceil(self.jump_height / 4), 4)
elseif self.stepheight > 0.5 then
jumpheight = 1
end
self.path.way = minetest.find_path(s, p1, 16, jumpheight,
dropheight, "Dijkstra")
--[[
-- show path using particles
if self.path.way and #self.path.way > 0 then
print("-- path length:" .. tonumber(#self.path.way))
for _,pos in pairs(self.path.way) do
minetest.add_particle({
pos = pos,
velocity = {x=0, y=0, z=0},
acceleration = {x=0, y=0, z=0},
expirationtime = 1,
size = 4,
collisiondetection = false,
vertical = false,
texture = "heart.png",
})
end
end
--]]
-- no path found, try something else
if not self.path.way then
self.path.following = false
--try something else here
-- will try again in 2 second
self.path.stuck_timer = stuck_timeout - 2
elseif s.y < p1.y and (not self.fly) then
self:do_jump() --add jump to pathfinding
self.path.following = true
else
-- yay i found path
self:set_velocity(self.walk_velocity)
-- follow path now that it has it
self.path.following = true
end
end
end
--------------------------------------------------------here end copied codes --------------------------------------------------------
-- helper function for initializing worker ant destinations in connection with an ant nest
local find_destinations = function(pos) -- pos is that of the nest
local instructions = {pos,} -- will need to return this, if it exists... the first pos in instructions is always the home node
local facing = math.random(1,4) --choose one of 4 directions to start writing the path in
local steps = math.random(5,7) --how many insturctions are we going to have past the first
for i = 1, steps do --we will have 5 to 7 more locations after the first one
--- the order here is: move dist in facing direction, log an instruction point, turn left or right and update facing
local start_pos = instructions[i]
local dist = math.random(3,10) -- choose random distance to travel to the next point
local new_pos = {}
--find the new pos based on dist and facing direction; 1= +x, 2 = +z, 3 = -x, 4= -z
if facing == 1 then
new_pos = {x=start_pos.x + dist, y=start_pos.y, z=start_pos.z}
end
if facing == 2 then
new_pos = {x=start_pos.x, y=start_pos.y, z=start_pos.z + dist}
end
if facing == 3 then
new_pos = {x=start_pos.x - dist, y=start_pos.y, z=start_pos.z}
end
if facing == 4 then
new_pos = {x=start_pos.x, y=start_pos.y, z=start_pos.z - dist}
end
instructions[i+1] = new_pos -- add a new position to the instructions list
-- choose a new direction, by turing left or right
facing = facing + math.random(-1,1)
if facing == 5 then
facing = 1
end
if facing == 0 then
facing = 4
end
end -- this finishes the for loop, and will make a list of positions that form a path.
return instructions -- this returns the list of position instructions
end
mobs:register_mob("bugs_ant:ant_worker", {
type = "animal",
visual = "mesh",
jump = false,
visual_size = {x = 7, y = 7},
mesh = "bugs_ant.b3d",
collisionbox = {-0.07, -0.01, -0.07, 0.07, 0.07, 0.07},
animation = {
speed_normal = 1,
speed_run = 1,
stand_start = 1,
stand_end = 10,
walk_start = 20,
walk_end = 24,
run_start = 20,
run_end = 24,
jump_start = 28,
jump_end = 32,
},
textures = {
{"bugs_ant.png"},
},
fear_height = 3,
runaway = false,
fly = false,
walk_chance = 99,
stand_chance = 1,
walk_velocity = 1,
run_velocity = 1,
view_range = 5,
passive = true,
hp_min = 1,
hp_max = 2,
armor = 200,
lava_damage = 5,
fall_damage = 0,
water_damage = 2,
makes_footstep_sound = false,
drops = {},
sounds = {},
ant_instructions = {},
ant_goal_number = nil,
time_on_goal = 0,
los_switcher = false,
height_switcher = false,
reach = .2,
ant_debug = false,
on_rightclick = function(self, clicker)
self.ant_debug = true
end,
do_custom = function(self, dtime) -- this allows for the custom movement
if self.ant_debug then
if get_horizantal_dist_sq(self.object:get_pos(), self.ant_instructions[self.ant_goal_number]) < 1 then
minetest.chat_send_all("your ant reached goal #"..dump(self.ant_goal_number))
end
end
local time_on_goal = self.time_on_goal
--minetest.chat_send_all(dump(dtime).. time_on_goal)
local goal_timeout = 10 -- # of seconds the ant should take to try to get to the next goal. If it fails to reach it before that time, It will move on to the next goal or go home.
local ant_instructions = nil -- if there aren't any instructions then it will use nil for the value
if self.ant_instructions then -- what if there are
ant_instructions = self.ant_instructions
self.pathfinding = 1
end
if not ant_instructions then -- if the ant doesnt have any instructions, then use regular api for movement
return true
end
if not self.ant_goal_number then -- if for some reason we dont have a goal, then set it to the home position
self.ant_goal_number = 1
end
local goal_pos = ant_instructions[self.ant_goal_number] --where are we going?
self.time_on_goal = time_on_goal + dtime -- keep track of how long we have been on the current goal
--so now move towards our goal, if we can
--###############################################################
-- set positions
local pos1 = self.object:get_pos() -- ant's current position
local pos2 = goal_pos
-- if no path set then setup mob
if not self.path or not self.path.way then
self.pathfinding = 1 -- just incase it's not set in mobdef
end
-- call pathfinding function to control player movement
if self.pathfinding then
local horiz_dist = get_horizantal_dist_sq(pos1, pos2) -- if we have not reached our goal yet then we will call the smart mobs func to move us towards the goal
if horiz_dist > 1 then
local dist = get_distance(pos1, pos2)
mob_pathfind_towards(self,pos1, pos2, dist, dtime)
else --if we have reached our goal, then we need to get our next goal, and reset the goal timer
self.ant_goal_number = self.ant_goal_number + 1
self.time_on_goal = 0 -- reset the time
if self.ant_goal_number > #self.ant_instructions then -- if we advance the goal number beyond the nuber of goals, go home
self.ant_goal_number = 1
end
end
end
--#################################################################
--ok we have moved, so now we need to double-check our goals. We need to recognize if we are stuck, because there are no paths to the goal. that will be determined by how long we have had the same goal.
if self.time_on_goal > 10 then --if we have been trying to get to the same goal for more than 10 sec, move on to the next one and reset the goal timer. Hopefully it wont take more than 10 sec to reach the next goal
self.ant_goal_number = self.ant_goal_number + 1
self.time_on_goal = 0 -- reset the time
if self.ant_goal_number > #self.ant_instructions then -- if we advance the goal number beyond the nuber of goals, go home
self.ant_goal_number = 1
end
end
end
})
minetest.register_node("bugs_ant:ant_nest",{
description = "Ant Nest",
tiles = {"default_dirt.png"},
drawtype = "normal",
groups = {crumbly=1},
after_place_node = function(pos, placer, itemstack, pointed_thing)
local destination_table = find_destinations(pos) -- gets the instruction list for the ants to follow.
--debug
minetest.chat_send_all(dump(destination_table))
local timer = minetest.get_node_timer(pos) --start a timer to make the node keep producing ants
timer:start(1)
if destination_table then -- just to make sure
--place the destination table in the node's meta here. After this, the destination table is stored in metadata as a string. Use something like data = minetest.deserialize(minetest:get_string("foo")) to de-strigify it.
local meta = minetest.get_meta(pos)
meta:set_string("instruction", minetest.serialize(destination_table))
-- show goals using add_particlespawner
for posi in ipairs(destination_table) do
minetest.add_particle({
pos = posi,
velocity = {x=0, y=0, z=0},
acceleration = {x=0, y=0, z=0},
expirationtime = 4000,
size = 4,
collisiondetection = false,
vertical = false,
texture = "heart.png",
})
end
end
end,
on_timer = function(pos)
local timer = minetest.get_node_timer(pos)
local meta = minetest.get_meta(pos)
local next_timer = 1 -- how long until this function runs again, init at 1
local objs = minetest.get_objects_inside_radius(pos, 30) --checking for ants and other objs within 30 nodes
if #objs < max_obj then -- only spawn more ants if the max objects in the area is low enough
local worker_ant_obj = minetest.add_entity(pos, "bugs_ant:ant_worker", nil) --spawn a worker ant, and, below, try to give it instructions. This returns the object
local worker_ant_ent = worker_ant_obj:get_luaentity() -- this gets the entity from the object
local destination_table = minetest.deserialize(meta:get_string("instruction")) --retrieve the destination table from the metadata
if destination_table then -- just to make sure it exists and not cause crashes
worker_ant_ent.ant_instructions = destination_table --give this ant instructions
worker_ant_ent.ant_goal_number = 2 -- set the goal for the second position in the instruction list (the fisrt is the home position)
worker_ant_ent.time_on_goal = 0 --init goal timer
end
end
timer:start(next_timer) -- restart the timer
end,
})
I am probably doing this all wrong... maybe I can use smart_mobs after all.
My goals are simple, the coding not so much:
1) the ant nest makes a list of instructions when it is placed. The list contains positions that are near each other and can be reached by walking in straight lines. Either: Only the x and z values matter, or the position has to be on the surface.
2) when a worker ant is spawned from its nest, the node timer function that spawns it sets the variables such as self.ant_instructions, self.ant_goal_number, etc so that all ants from the same nest have the same instructions
3) the do custom function behaves as follows:
a) if there are no instructions, release control of the mob back to the api (return true)
b) if the ant has reached its goal, increment to goal index value, or if the current goal index value is the same as the length of the instruction list, then set the goal to #1 (which is the location of the nest)
c) move the ant towards its goal. Use pathfinding. Also, keep track of how long the ant has been trying to get to its goal, and if it exceeds a certain time , change the goal to the next one on the list or home, so that if the goal is unreachable, it will be skipped.
----
hopefully this will make ants march mostly in a line, taking a path that zigzags, mostly staying together, and then returning home, where they then start it up again. Another possibility is to kill them when they get home, and let the nest spawn more, possibly with different instructions. (that would be better)
anyhow, I have tried to make this work, and have gotten pretty stuck. I would really appreciate some help, ppl who help me will be mentioned on the readme. I am intending to release this under the MIT license, because much of the code is copied from mobs.
extra info: An instruction list is made by choosing a cardinal direction, and traveling a short distance in that direction from the nest. make a waypoint. Then turn left or right, and travel another short distance. Place another waypoint in the list. Repeat several times.
I think I need a function like:
mob.goto(pos,range)
that moves the mob towards the pos and returns true when it is within "range" of "pos", and false if it isn't yet.