From 80a6a6efb01a72a33d5430d9d45044f75b12786d Mon Sep 17 00:00:00 2001 From: kno10 Date: Wed, 31 Jul 2024 02:30:29 +0200 Subject: [PATCH] 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 Co-authored-by: kno10 Co-committed-by: kno10 --- mods/ENTITIES/mcl_mobs/spawning.lua | 97 +++++++++-------------------- 1 file changed, 31 insertions(+), 66 deletions(-) diff --git a/mods/ENTITIES/mcl_mobs/spawning.lua b/mods/ENTITIES/mcl_mobs/spawning.lua index c0e68e55b..85b0ca3e5 100644 --- a/mods/ENTITIES/mcl_mobs/spawning.lua +++ b/mods/ENTITIES/mcl_mobs/spawning.lua @@ -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_connected_players = minetest.get_connected_players +local math_min = math.min +local math_max = math.max local math_random = math.random local math_floor = math.floor local math_ceil = math.ceil local math_cos = math.cos 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 vector_distance = vector.distance @@ -54,8 +55,10 @@ local FIND_SPAWN_POS_RETRIES = 16 local FIND_SPAWN_POS_RETRIES_SUCCESS_RESPIN = 8 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_OUTER = 128 +local MOB_SPAWN_ZONE_OUTER_SQ = MOB_SPAWN_ZONE_OUTER^2 -- squared -- range for mob count local MOB_CAP_INNER_RADIUS = 32 @@ -601,71 +604,40 @@ function mcl_mobs:spawn_specific(name, dimension, type_of_spawning, biomes, min_ spawn_dictionary[key]["check_position"] = check_position 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) -- Select a distance such that distances closer to the player are selected much more often than - -- those further away from the player. - local fx = (math_random(1,10000)-1) / 10000 - local x = inverse_pwl(fx, SPAWN_DISTANCE_CDF_PWL) - local distance = x * (MOB_SPAWN_ZONE_OUTER - MOB_SPAWN_ZONE_INNER) + MOB_SPAWN_ZONE_INNER + -- those further away from the player. This does produce a concentration at INNER (24 blocks) + local distance = math_random()^2 * (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)) - -- TODO Floor xoff and zoff and add 0.5 so it tries to spawn in the middle of the square. Less failed attempts. - -- Use spherical coordinates https://en.wikipedia.org/wiki/Spherical_coordinate_system#Cartesian_coordinates - local theta = math_random() * two_pi - local phi = math_random() * two_pi - local xoff = math_round(distance * math_sin(theta) * math_cos(phi)) - local yoff = math_round(distance * math_cos(theta)) - local zoff = math_round(distance * math_sin(theta) * math_sin(phi)) + -- Choose a random direction. Rejection sampling is simple and fast (1-2 tries usually) + local xoff, yoff, zoff, dd + repeat + xoff, yoff, zoff = math_random() * 2 - 1, math_random() * 2 - 1, math_random() * 2 - 1 + dd = xoff*xoff + yoff*yoff + zoff*zoff + until (dd <= 1 and dd >= 1e-6) -- outside of uniform ball, retry + 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) - 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)) return nil end -- Calculate upper/lower y limits - local R1 = MOB_SPAWN_ZONE_OUTER - 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( R1*R1 - d*d ) -- absolue value of distance to outer sphere + local d2 = xoff*xoff + zoff*zoff -- squared distance in x,z plane only + local y1 = math_sqrt( MOB_SPAWN_ZONE_OUTER_SQ - d2 ) -- absolue value of distance to outer sphere - local y_min - local y_max - if d >= MOB_SPAWN_ZONE_INNER then + local y_min, y_max + if d2 >= MOB_SPAWN_ZONE_INNER_SQ then -- Outer region, y range has both ends on the outer sphere y_min = pos.y - y1 y_max = pos.y + y1 else -- Inner region, y range spans between inner and outer spheres - local R2 = MOB_SPAWN_ZONE_INNER - local y2 = math_sqrt( R2*R2 - d*d ) - if goal_pos.y > pos. y then + local y2 = math_sqrt( MOB_SPAWN_ZONE_INNER_SQ - d2 ) + if goal_pos.y > pos.y then -- Upper hemisphere y_min = pos.y + y2 y_max = pos.y + y1 @@ -675,16 +647,9 @@ local function get_next_mob_spawn_pos(pos) y_max = pos.y - y2 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) - if y_max > goal_pos.y + 16 then - y_max = goal_pos.y + 16 - end - if y_min < goal_pos.y - 16 then - y_min = goal_pos.y - 16 - end + y_min = math_max(math_floor(y_min), goal_pos.y - 16) + y_max = math_min(math_ceil(y_max), goal_pos.y + 16) -- Ask engine for valid spawn locations local spawning_position_list = find_nodes_in_area_under_air( @@ -997,7 +962,7 @@ if mobs_spawn then mob_total_wide = 0 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("cap_space_wide", cap_space_wide) @@ -1005,10 +970,10 @@ if mobs_spawn then local cap_space_available = 0 if mob_type == "hostile" then 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 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 local mob_total_close = mob_counts_close[mob_type] @@ -1017,8 +982,8 @@ if mobs_spawn then mob_total_close = 0 end - local cap_space_close = math.max(close_zone_cap - mob_total_close, 0) - cap_space_available = math.min(cap_space_available, cap_space_close) + local cap_space_close = math_max(close_zone_cap - mob_total_close, 0) + cap_space_available = math_min(cap_space_available, cap_space_close) mcl_log("cap_space_close", cap_space_close) 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) 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) if logging then @@ -1196,8 +1161,8 @@ if mobs_spawn then local players = get_connected_players() 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_non_hostile = math.max(mob_cap.global_non_hostile - total_non_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) mcl_log("global cap_space_hostile", cap_space_hostile) mcl_log("global cap_space_non_hostile", cap_space_non_hostile)