mirror of
https://github.com/ElCeejo/creatura.git
synced 2025-03-15 04:11:24 +00:00
Improved Boids and Context Steering
This commit is contained in:
parent
66f02bc77a
commit
6c411bc6ac
3 changed files with 182 additions and 39 deletions
129
boids.lua
129
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
|
89
methods.lua
89
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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue