Choose direction vectors uniformly for spawning (#4467)

The previous code was biased towards placing mobs on top or below the
player, because it chose the theta inclination angle uniformly,
but the sphere is more narrow at the top and bottom.

This code is also simpler.

Reviewed-on: https://git.minetest.land/VoxeLibre/VoxeLibre/pulls/4467
Reviewed-by: teknomunk <teknomunk@protonmail.com>
Co-authored-by: kno10 <erich.schubert@gmail.com>
Co-committed-by: kno10 <erich.schubert@gmail.com>
This commit is contained in:
kno10 2024-07-31 02:30:29 +02:00 committed by the-real-herowl
parent 32148262e1
commit 80a6a6efb0

@ -17,12 +17,13 @@ local mt_get_biome_name = minetest.get_biome_name
local get_objects_inside_radius = minetest.get_objects_inside_radius local get_objects_inside_radius = minetest.get_objects_inside_radius
local get_connected_players = minetest.get_connected_players local get_connected_players = minetest.get_connected_players
local math_min = math.min
local math_max = math.max
local math_random = math.random local math_random = math.random
local math_floor = math.floor local math_floor = math.floor
local math_ceil = math.ceil local math_ceil = math.ceil
local math_cos = math.cos local math_cos = math.cos
local math_sin = math.sin local math_sin = math.sin
local math_round = function(x) return (x > 0) and math_floor(x + 0.5) or math_ceil(x - 0.5) end
local math_sqrt = math.sqrt local math_sqrt = math.sqrt
local vector_distance = vector.distance local vector_distance = vector.distance
@ -54,8 +55,10 @@ local FIND_SPAWN_POS_RETRIES = 16
local FIND_SPAWN_POS_RETRIES_SUCCESS_RESPIN = 8 local FIND_SPAWN_POS_RETRIES_SUCCESS_RESPIN = 8
local MOB_SPAWN_ZONE_INNER = 24 local MOB_SPAWN_ZONE_INNER = 24
local MOB_SPAWN_ZONE_INNER_SQ = MOB_SPAWN_ZONE_INNER^2 -- squared
local MOB_SPAWN_ZONE_MIDDLE = 32 local MOB_SPAWN_ZONE_MIDDLE = 32
local MOB_SPAWN_ZONE_OUTER = 128 local MOB_SPAWN_ZONE_OUTER = 128
local MOB_SPAWN_ZONE_OUTER_SQ = MOB_SPAWN_ZONE_OUTER^2 -- squared
-- range for mob count -- range for mob count
local MOB_CAP_INNER_RADIUS = 32 local MOB_CAP_INNER_RADIUS = 32
@ -601,70 +604,39 @@ function mcl_mobs:spawn_specific(name, dimension, type_of_spawning, biomes, min_
spawn_dictionary[key]["check_position"] = check_position spawn_dictionary[key]["check_position"] = check_position
end end
-- Calculate the inverse of a piecewise linear function f(x). Line segments are represented as two
-- adjacent points specified as { x, f(x) }. At least 2 points are required. If there are most solutions,
-- the one with a lower x value will be chosen.
local function inverse_pwl(fx, f)
if fx < f[1][2] then
return f[1][1]
end
for i=2,#f do
local x0,fx0 = unpack(f[i-1])
local x1,fx1 = unpack(f[i ])
if fx < fx1 then
return (fx - fx0) * (x1 - x0) / (fx1 - fx0) + x0
end
end
return f[#f][1]
end
local SPAWN_DISTANCE_CDF_PWL = {
{0.000,0.00},
{0.083,0.40},
{0.416,0.75},
{1.000,1.00},
}
local two_pi = 2 * math.pi
local function get_next_mob_spawn_pos(pos) local function get_next_mob_spawn_pos(pos)
-- Select a distance such that distances closer to the player are selected much more often than -- Select a distance such that distances closer to the player are selected much more often than
-- those further away from the player. -- those further away from the player. This does produce a concentration at INNER (24 blocks)
local fx = (math_random(1,10000)-1) / 10000 local distance = math_random()^2 * (MOB_SPAWN_ZONE_OUTER - MOB_SPAWN_ZONE_INNER) + MOB_SPAWN_ZONE_INNER
local x = inverse_pwl(fx, SPAWN_DISTANCE_CDF_PWL)
local distance = x * (MOB_SPAWN_ZONE_OUTER - MOB_SPAWN_ZONE_INNER) + MOB_SPAWN_ZONE_INNER
--print("Using spawn distance of "..tostring(distance).." fx="..tostring(fx)..",x="..tostring(x)) --print("Using spawn distance of "..tostring(distance).." fx="..tostring(fx)..",x="..tostring(x))
-- TODO Floor xoff and zoff and add 0.5 so it tries to spawn in the middle of the square. Less failed attempts. -- Choose a random direction. Rejection sampling is simple and fast (1-2 tries usually)
-- Use spherical coordinates https://en.wikipedia.org/wiki/Spherical_coordinate_system#Cartesian_coordinates local xoff, yoff, zoff, dd
local theta = math_random() * two_pi repeat
local phi = math_random() * two_pi xoff, yoff, zoff = math_random() * 2 - 1, math_random() * 2 - 1, math_random() * 2 - 1
local xoff = math_round(distance * math_sin(theta) * math_cos(phi)) dd = xoff*xoff + yoff*yoff + zoff*zoff
local yoff = math_round(distance * math_cos(theta)) until (dd <= 1 and dd >= 1e-6) -- outside of uniform ball, retry
local zoff = math_round(distance * math_sin(theta) * math_sin(phi)) dd = distance / math_sqrt(dd) -- distance scaling factor
xoff, yoff, zoff = xoff * dd, yoff * dd, zoff * dd
local goal_pos = vector.offset(pos, xoff, yoff, zoff) local goal_pos = vector.offset(pos, xoff, yoff, zoff)
if not ( math.abs(goal_pos.x) <= SPAWN_MAPGEN_LIMIT and math.abs(pos.y) <= SPAWN_MAPGEN_LIMIT and math.abs(goal_pos.z) <= SPAWN_MAPGEN_LIMIT ) then if not ( math.abs(goal_pos.x) <= SPAWN_MAPGEN_LIMIT and math.abs(goal_pos.y) <= SPAWN_MAPGEN_LIMIT and math.abs(goal_pos.z) <= SPAWN_MAPGEN_LIMIT ) then
mcl_log("Pos outside mapgen limits: " .. minetest.pos_to_string(goal_pos)) mcl_log("Pos outside mapgen limits: " .. minetest.pos_to_string(goal_pos))
return nil return nil
end end
-- Calculate upper/lower y limits -- Calculate upper/lower y limits
local R1 = MOB_SPAWN_ZONE_OUTER local d2 = xoff*xoff + zoff*zoff -- squared distance in x,z plane only
local d = vector_distance( pos, vector.new( goal_pos.x, pos.y, goal_pos.z ) ) -- distance from player to projected point on horizontal plane local y1 = math_sqrt( MOB_SPAWN_ZONE_OUTER_SQ - d2 ) -- absolue value of distance to outer sphere
local y1 = math_sqrt( R1*R1 - d*d ) -- absolue value of distance to outer sphere
local y_min local y_min, y_max
local y_max if d2 >= MOB_SPAWN_ZONE_INNER_SQ then
if d >= MOB_SPAWN_ZONE_INNER then
-- Outer region, y range has both ends on the outer sphere -- Outer region, y range has both ends on the outer sphere
y_min = pos.y - y1 y_min = pos.y - y1
y_max = pos.y + y1 y_max = pos.y + y1
else else
-- Inner region, y range spans between inner and outer spheres -- Inner region, y range spans between inner and outer spheres
local R2 = MOB_SPAWN_ZONE_INNER local y2 = math_sqrt( MOB_SPAWN_ZONE_INNER_SQ - d2 )
local y2 = math_sqrt( R2*R2 - d*d )
if goal_pos.y > pos.y then if goal_pos.y > pos.y then
-- Upper hemisphere -- Upper hemisphere
y_min = pos.y + y2 y_min = pos.y + y2
@ -675,16 +647,9 @@ local function get_next_mob_spawn_pos(pos)
y_max = pos.y - y2 y_max = pos.y - y2
end end
end end
y_min = math_round(y_min)
y_max = math_round(y_max)
-- Limit total range of check to 32 nodes (maximum of 3 map blocks) -- Limit total range of check to 32 nodes (maximum of 3 map blocks)
if y_max > goal_pos.y + 16 then y_min = math_max(math_floor(y_min), goal_pos.y - 16)
y_max = goal_pos.y + 16 y_max = math_min(math_ceil(y_max), goal_pos.y + 16)
end
if y_min < goal_pos.y - 16 then
y_min = goal_pos.y - 16
end
-- Ask engine for valid spawn locations -- Ask engine for valid spawn locations
local spawning_position_list = find_nodes_in_area_under_air( local spawning_position_list = find_nodes_in_area_under_air(
@ -997,7 +962,7 @@ if mobs_spawn then
mob_total_wide = 0 mob_total_wide = 0
end end
local cap_space_wide = math.max(type_cap - mob_total_wide, 0) local cap_space_wide = math_max(type_cap - mob_total_wide, 0)
mcl_log("mob_type", mob_type) mcl_log("mob_type", mob_type)
mcl_log("cap_space_wide", cap_space_wide) mcl_log("cap_space_wide", cap_space_wide)
@ -1005,10 +970,10 @@ if mobs_spawn then
local cap_space_available = 0 local cap_space_available = 0
if mob_type == "hostile" then if mob_type == "hostile" then
mcl_log("cap_space_global", cap_space_hostile) mcl_log("cap_space_global", cap_space_hostile)
cap_space_available = math.min(cap_space_hostile, cap_space_wide) cap_space_available = math_min(cap_space_hostile, cap_space_wide)
else else
mcl_log("cap_space_global", cap_space_non_hostile) mcl_log("cap_space_global", cap_space_non_hostile)
cap_space_available = math.min(cap_space_non_hostile, cap_space_wide) cap_space_available = math_min(cap_space_non_hostile, cap_space_wide)
end end
local mob_total_close = mob_counts_close[mob_type] local mob_total_close = mob_counts_close[mob_type]
@ -1017,8 +982,8 @@ if mobs_spawn then
mob_total_close = 0 mob_total_close = 0
end end
local cap_space_close = math.max(close_zone_cap - mob_total_close, 0) local cap_space_close = math_max(close_zone_cap - mob_total_close, 0)
cap_space_available = math.min(cap_space_available, cap_space_close) cap_space_available = math_min(cap_space_available, cap_space_close)
mcl_log("cap_space_close", cap_space_close) mcl_log("cap_space_close", cap_space_close)
mcl_log("cap_space_available", cap_space_available) mcl_log("cap_space_available", cap_space_available)
@ -1145,7 +1110,7 @@ if mobs_spawn then
local amount_to_spawn = math.random(group_min, spawn_in_group) local amount_to_spawn = math.random(group_min, spawn_in_group)
mcl_log("Spawning quantity: " .. amount_to_spawn) mcl_log("Spawning quantity: " .. amount_to_spawn)
amount_to_spawn = math.min(amount_to_spawn, cap_space_available) amount_to_spawn = math_min(amount_to_spawn, cap_space_available)
mcl_log("throttled spawning quantity: " .. amount_to_spawn) mcl_log("throttled spawning quantity: " .. amount_to_spawn)
if logging then if logging then
@ -1196,8 +1161,8 @@ if mobs_spawn then
local players = get_connected_players() local players = get_connected_players()
local total_mobs, total_non_hostile, total_hostile = count_mobs_total_cap() local total_mobs, total_non_hostile, total_hostile = count_mobs_total_cap()
local cap_space_hostile = math.max(mob_cap.global_hostile - total_hostile, 0) local cap_space_hostile = math_max(mob_cap.global_hostile - total_hostile, 0)
local cap_space_non_hostile = math.max(mob_cap.global_non_hostile - total_non_hostile, 0) local cap_space_non_hostile = math_max(mob_cap.global_non_hostile - total_non_hostile, 0)
mcl_log("global cap_space_hostile", cap_space_hostile) mcl_log("global cap_space_hostile", cap_space_hostile)
mcl_log("global cap_space_non_hostile", cap_space_non_hostile) mcl_log("global cap_space_non_hostile", cap_space_non_hostile)