Random spawning script needs code review (RNG?)

Spawns a bunch of parts on a basepart in an orderly manner based on factors like minimum distance, how many of them and which map to spawn on. has a little bit type and sanity checking i guess

local GemSpawning = {}
local self = GemSpawning

local function isFarEnough(newPos : Vector3, spawnedPositions, MIN_DISTANCE : number)
	for _, pos in ipairs(spawnedPositions) do
		if (pos - newPos).Magnitude < MIN_DISTANCE then
			return false
		end
	end
	return true
end

function self.spawnGem(MIN_DISTANCE : number, gemModel : BasePart, mapArea : BasePart, AMOUNT_TO_SPAWN : number)
	local spawnedPositions: { Vector3 }  = {}

	local pos = mapArea.Position
	local px, py, pz = pos.X, pos.Y, pos.Z

	local size = mapArea.Size
	local sx, sy, sz = size.X, size.Y, size.Z

	while #spawnedPositions < AMOUNT_TO_SPAWN do
		local spawnX = math.random(px - sx/2, px + sx/2)
		local spawnZ = math.random(pz - sz/2, pz + sz/2)
		local spawnPos = Vector3.new(spawnX, py + gemModel.Size.Y, spawnZ)

		if isFarEnough(spawnPos, spawnedPositions, MIN_DISTANCE ) then
			local gemCl = gemModel:Clone()
			gemCl.Position = spawnPos
			gemCl.Parent = workspace
			table.insert(spawnedPositions, spawnPos)
		end
	end

	return #spawnedPositions > 0
end

return GemSpawning

1 Like
Edit

Backtracked, was taking longer than expected. The point is that there are multiple other approaches that produce similar results and are faster in worst-case scenarios. The approach you presented may perform many random calculations before it completes, many times more than the amount input. You can minimize the worst-case scenarios by adding limitations if you want to keep the same approach.

The grid approach splits the area into a 2-dimensional grid, distancing each section by the minimum distance and ensuring that a random point is picked within the section, so randomization is only needed once and no verification is necessary. The math can be quite tricky to get right since the amount could be non-square (4, 9, 16, …), which tripped me up.

You are appending to the table, however each time you append you are also searching through the entire table to ensure you can append. The approach you are using could perform quite bad in cases where the area to spawn the gems and the distance to separate them conflicts with the amount. I suggest adding a way to check to ensure that even spawning the amount is possible, if not then reduce the gem amount until it is. Maybe sx * sz / MIN_DISTANCE > AMOUNT_TO_SPAWN.

A different approach could be taken

The isFarEnough method could be removed in favor of a grid based system that ensures all gems are separated at least some distance and doesn’t require a table. The grid system would split the mapArea into multiple segments. The grid system will require some testing to flesh out and studio isn’t currently letting me play test, so I’ll get back to you in a few days, but it will most likely involve using a loop to position each created part within segment of the mapArea allocated, randomly.

function spawnGem(
	MIN_DISTANCE: number, 
	gemModel: BasePart, 
	mapArea: BasePart, 
	AMOUNT_TO_SPAWN: number
)
	local pos = mapArea.Position
	local px, py, pz = pos.X, pos.Y, pos.Z

	local size = mapArea.Size
	local sx, sy, sz = size.X, size.Y, size.Z
	local gemYSize = gemModel.Size.Y
	-- uncertain
	if sx * sz // MIN_DISTANCE > AMOUNT_TO_SPAWN then
		error(`AMOUNT_TO_SPAWN is too large, cannot be greater than {sx * sz // MIN_DISTANCE}`)
	end
	-- or i=1?
	for i=0, AMOUNT_TO_SPAWN do
		local gemCl = gemModel:Clone()
		 -- some calculations, will get back to you later
		local minX, maxX = 0, 1
		local minZ, maxZ = 0, 1
		
		gemCl.Position = Vector3.new(
			math.random(minX, maxX), 
			gemYSize, 
			math.random(minZ, maxZ)
		)
		gemCl.Parent = workspace
	end
end