diff --git a/boids.lua b/boids.lua index 122a23e..2bbefc9 100644 --- a/boids.lua +++ b/boids.lua @@ -3,7 +3,18 @@ ----------- local abs = math.abs -local random = math.random +local atan2 = math.atan2 +local sin = math.sin +local cos = math.cos + +local function average_angle(tbl) + local sum_sin, sum_cos = 0, 0 + for _, v in pairs(tbl) do + sum_sin = sum_sin + sin(v) + sum_cos = sum_cos + cos(v) + end + return atan2(sum_sin, sum_cos) +end local function average(tbl) local sum = 0 @@ -13,20 +24,17 @@ local function average(tbl) return sum / #tbl end -local function average_angle(tbl) - local sum_sin, sum_cos = 0, 0 - for _, v in pairs(tbl) do - sum_sin = sum_sin + math.sin(v) - sum_cos = sum_cos + math.cos(v) - end - return math.atan2(sum_sin, sum_cos) +local function interp_rad(a, b, w) + local cs = (1 - w) * cos(a) + w * cos(b) + local sn = (1 - w) * sin(a) + w * sin(b) + return atan2(sn, cs) end -local vec_dist = vector.distance -local vec_dir = vector.direction local vec_add = vector.add -local vec_normal = vector.normalize +local vec_dir = vector.direction +local vec_dist = vector.distance local vec_divide = vector.divide +local vec_normal = vector.normalize local function get_average_pos(vectors) local sum = {x = 0, y = 0, z = 0} @@ -53,13 +61,91 @@ local dir2yaw = minetest.dir_to_yaw -- Get Boid Members -- --- This function scans within --- a set radius for potential --- boid members, and assigns --- a leader. A new leader --- is only assigned every 12 --- seconds or if a new mob --- is in the boid. +function creatura.get_boid_cached(self) + local pos = self.object:get_pos() + if not pos then return end + local radius = self.tracking_range * 0.5 or 4 + local members = self._movement_data.boids or {} + local max_boids = self.max_boids or 7 + if #members > 0 then + for i = #members, 1, -1 do + local object = members[i] + if not object or not object:get_yaw() then members[i] = nil end + end + if #members >= max_boids then return members end + end + local objects = minetest.get_objects_inside_radius(pos, radius) + if #objects < 2 then return {} end + for _, object in ipairs(objects) do + local ent = object and object ~= self.object and object:get_luaentity() + if ent + and ent.name == self.name then + local move_data = ent._movement_data + if move_data + and (not move_data.boids + or #move_data.boids < max_boids) then + table.insert(members, object) + end + end + if #members >= max_boids then break end + end + self._movement_data.boids = members + + return members +end + +-- Calculate Boid Movement Direction + +function creatura.get_boid_dir(self) + local pos = self.object:get_pos() + if not pos then return end + local boids = creatura.get_boid_cached(self) + if #boids < 2 then return end + local pos_no, pos_sum = 0, {x = 0, y = 0, z = 0} + local sum_sin, sum_cos = 0, 0 + local lift_no, lift_sum = 0, 0 + + local vel + local boid_pos + local closest_pos + for _, object in ipairs(boids) do + if object then + boid_pos, vel = object:get_pos(), object:get_velocity() + local obj_yaw = object:get_yaw() + pos_no, pos_sum = pos_no + 1, vec_add(pos_sum, boid_pos) + sum_sin, sum_cos = sum_sin + sin(obj_yaw), sum_cos + cos(obj_yaw) + lift_no, lift_sum = lift_no + 1, lift_sum + vel.y + if not closest_pos + or vec_dist(pos, boid_pos) < vec_dist(pos, closest_pos) then + closest_pos = boid_pos + end + end + end + if not closest_pos then return end + local center = vec_divide(pos_sum, pos_no) + local lift = lift_sum / lift_no + + local angle_sin, angle_cos + local radius = self.tracking_range * 0.5 or 4 + local dist_factor = (radius - vec_dist(pos, closest_pos)) / radius + + local alignment = atan2(sum_sin, sum_cos) + local seperation = dir2yaw(vec_dir(closest_pos, pos)) + local cohesion = dir2yaw(vec_dir(pos, center)) + if dist_factor > 0.9 then + seperation = interp_rad(alignment, seperation, 0.5) + angle_sin, angle_cos = sin(seperation), cos(seperation) + else + angle_sin, angle_cos = sin(cohesion), cos(cohesion) + end + local angle = atan2(angle_sin + sin(alignment), angle_cos + cos(alignment)) + + local dir = yaw2dir(angle) + dir.y = lift + return vector.normalize(dir), boids +end + +-- Deprecated function creatura.get_boid_members(pos, radius, name) local objects = minetest.get_objects_inside_radius(pos, radius) @@ -71,7 +157,6 @@ function creatura.get_boid_members(pos, radius, name) local object = objects[i] if object:get_luaentity() and object:get_luaentity().name == name then - object:get_luaentity().boid_heading = math.rad(random(360)) table.insert(members, object) end end @@ -86,8 +171,6 @@ function creatura.get_boid_members(pos, radius, name) return members end --- Calculate Boid angles and offsets. - function creatura.get_boid_angle(self, _boids, range) local pos = self.object:get_pos() local boids = _boids or creatura.get_boid_members(pos, range or 4, self.name) @@ -127,9 +210,6 @@ function creatura.get_boid_angle(self, _boids, range) local seperation = yaw + -(dir2yaw(dir2closest) - yaw) local cohesion = dir2yaw(dir2center) local params = {alignment} - if self.boid_heading then - table.insert(params, yaw + self.boid_heading) - end if dist_2d(pos, closest_pos) < (self.boid_seperation or self.width * 3) then table.insert(params, seperation) elseif dist_2d(pos, center) > (#boids * 0.33) * (self.boid_seperation or self.width * 3) then @@ -145,6 +225,5 @@ function creatura.get_boid_angle(self, _boids, range) elseif math.abs(pos.y - closest_pos.y) > 1.5 * (self.boid_seperation or self.width * 3) then table.insert(vert_params, vert_cohesion + (lift - vert_cohesion) * 0.1) end - self.boid_heading = nil return average_angle(params), average(vert_params) end \ No newline at end of file diff --git a/methods.lua b/methods.lua index 6d37e05..bf785ad 100644 --- a/methods.lua +++ b/methods.lua @@ -40,7 +40,7 @@ local dir2yaw = minetest.dir_to_yaw texture = tex or "creatura_particle_red.png", expirationtime = time or 0.1, glow = 16, - size = 16 + size = 24 }) end]] @@ -104,7 +104,7 @@ end]] end]] local get_node_def = creatura.get_node_def -local get_node_height = creatura.get_node_height_from_def +--local get_node_height = creatura.get_node_height_from_def function creatura.get_collision_ranged(self, dir, range) local pos, yaw = self.object:get_pos(), self.object:get_yaw() @@ -114,13 +114,17 @@ function creatura.get_collision_ranged(self, dir, range) pos.y = pos.y + 0.01 dir = vec_normal(dir or yaw2dir(yaw)) yaw = dir2yaw(dir) - local ahead = vec_add(pos, vec_multi(dir, width)) + local outset = math.floor(width) + if outset < 0.5 then outset = 0.5 end + local ahead = vec_add(pos, vec_multi(dir, outset)) + local height_half = self.height * 0.5 + local center_y = pos.y + height_half -- Loop local cos_yaw = cos(yaw) local sin_yaw = sin(yaw) local pos_x, pos_y, pos_z = ahead.x, ahead.y, ahead.z local dir_x, dir_y, dir_z = dir.x, dir.y, dir.z - local dist + local danger = 0 local collision for _ = 0, range or 4 do pos_x = pos_x + dir_x @@ -135,13 +139,14 @@ function creatura.get_collision_ranged(self, dir, range) } for y = 0, height, height / ceil(height) do pos2.y = pos_y + y - local dist2 = vec_dist(pos, pos2) - if not dist - or dist2 < dist then - if pos2.y - pos_y > (self.stepheight or 1.1) - and get_node_def(pos2).walkable then + if pos2.y - pos_y > (self.stepheight or 1.1) + and get_node_def(pos2).walkable then + local dist_dngr = (height_half - abs(center_y - pos2.y)) / height_half + local field_dngr = vec_dot(dir, vec_normal(vec_dir(pos, pos2))) + local ttl_dngr = dist_dngr + field_dngr + if ttl_dngr > danger then + danger = ttl_dngr collision = pos2 - dist = dist2 end end end @@ -192,7 +197,7 @@ local function get_avoidance_dir(self) vel.y = 0 local vel_len = vec_len(vel) * (1 + (self.step_delay or 0)) local ahead = vec_add(pos, vec_normal(vel)) - local avoidance_force = vector.subtract(ahead, col_pos) + local avoidance_force = vec_sub(ahead, col_pos) avoidance_force.y = 0 avoidance_force = vec_multi(vec_normal(avoidance_force), (vel_len > 1 and vel_len) or 1) return vec_dir(pos, vec_add(ahead, avoidance_force)) @@ -210,7 +215,7 @@ local steer_directions = { vec_normal({x = -1, y = 0, z = 1}) } -function creatura.get_context_steering(self, goal, range) +--[[function creatura.get_context_steering(self, goal, range) local pos, vel = self.object:get_pos(), self.object:get_velocity() if not pos then return end local heading = vec_normal(vel) @@ -246,7 +251,7 @@ function creatura.get_context_steering(self, goal, range) local ahead = vec_add(pos, vec_multi(heading, self.width + dist2col)) local avd_force = vec_normal(vec_sub(ahead, collision)) dir.y = avd_force.y / 4 - local dot_weight = vec_dot(vec_normal(dir2col), dir) + local dot_weight = vec_dot(vec_normal(dir2col), dir2goal) local dist_weight = (range - dist2col) / range interest = interest - dot_weight danger = dist_weight @@ -256,6 +261,62 @@ function creatura.get_context_steering(self, goal, range) output_dir = vector.add(output_dir, vector.multiply(dir, score)) end return output_dir +end]] + +local function get_collision_single(pos) + local pos2 = {x = pos.x, y = pos.y, z = pos.z} + if get_node_def(pos2).walkable then + pos2.y = pos.y + 1 + local col_max = get_node_def(pos2).walkable + pos2.y = pos.y - 1 + local col_min = col_max and get_node_def(pos2).walkable + if col_min then + return pos + else + pos2.y = pos.y + 1 + return pos2 + end + end +end + +function creatura.get_context_steering(self, goal, range) + local pos, yaw = self.object:get_pos(), self.object:get_yaw() + if not pos or not yaw then return end + range = range or 8; if range < 2 then range = 2 end + local width, height = self.width, self.height + local dir2goal = vec_normal(vec_dir(pos, goal)) + local output_dir = {x = 0, y = 0, z = 0} + pos.y = (pos.y + height * 0.5) + + local collision + local dir2col + local dir + for _, _dir in ipairs(steer_directions) do + dir = {x = _dir.x, y = 0, z = _dir.z} + local score = vec_dot(dir2goal, dir) + local interest = clamp(score, 0, 1) + local danger = 0 + if interest > 0 then + if width <= 0.5 + and height <= 1 then + collision = get_collision_single(vec_add(pos, dir)) + else + _, collision = creatura.get_collision_ranged(self, dir, range) + end + if collision then + dir2col = vec_normal(vec_dir(pos, collision)) + local dist2col = vec_dist(pos, collision) - width + dir.y = dir2col.y * -1 + local dot_weight = vec_dot(dir2col, dir2goal) + local dist_weight = (range - dist2col) / range + interest = interest - dot_weight + danger = dist_weight + end + end + score = interest - danger + output_dir = vector.add(output_dir, vector.multiply(dir, score)) + end + return output_dir end ------------- @@ -508,7 +569,7 @@ creatura.register_movement_method("creatura:context_based_steering", function(se end -- Calculate Movement steer_timer = (steer_timer > 0 and steer_timer - self.dtime) or 0.25 - steer_to = (steer_timer <= 0 and creatura.get_context_steering(self, goal, 2)) or steer_to + steer_to = (steer_timer <= 0 and creatura.get_context_steering(self, goal, 3)) or steer_to local speed = abs(_self.speed or 2) * speed_factor or 0.5 local turn_rate = abs(_self.turn_rate or 5) -- Apply Movement diff --git a/settingtypes.txt b/settingtypes.txt index 6d1a005..81ff588 100644 --- a/settingtypes.txt +++ b/settingtypes.txt @@ -19,6 +19,9 @@ creatura_mapgen_spawn_interval (Mapgen Spawning Interval) int 5 # How many Mobs can be a in a Mapblock before ABM spawning is blocked creatura_mapblock_limit (Max Mobs per Mapblock) int 99 +# Minimum distance to a player for ABM Spawning +creatura_min_abm_dist (Minimum ABM Spawning Distance) int 32 + # Allotted time (in μs) per step for A* pathfinding (lower means less lag but slower pathfinding) creatura_a_star_alloted_time (A* Pathfinding Alloted time per step) int 500