( same goal with the above, but a box instead of circle )
RECALL:
I am NOT trying to get 2D positions of these parts. The studs and shapes are simplified into 2D for easier understanding.
I am trying to get all integer Vector3s inside a Sphere or a 3D Box.
What is the issue? : I can’t find a way to optimize this. Looping through all the studs will crash the entire game, only going through a portion can still significantly impact the game’s performance.
Imagine if you already had each of these integer vector3’s in an ordered list for example the cube. It would be a list of 1000 Vector3s if the cube had sidelength 10.
You can imagine a function h(n, Sidelength) that would index that hypothetical array at n for this cube with the sidelength SideLength. However instead of indexing this nonexistent array, it would calculate what the value at that index would be using math. The same concept can apply to the sphere, although the math will be a lot trickier.
Of course I have to ask how you intend on using this information (the Vector3s), as you will run into the same lag issues even using this approach if you try to brute force calculate all values.
So instead holding all possible values, just let this mathematical function act as your array and calculate the ones you need on the spot. You could even implement a cache if you’re going to frequently reuse certain values.
If you are searching for best practices, these methods are already pretty performant.
Depending on the size of the request, it can lag. But if it gets to that point, you should have an alternative.
You could cache all the positions of the parts and update them regularly.
Create a function to iterate through and compare distance via a magnitude check. This can act just as well as the GetPartBounds methods.
I would just use a workspace:GetPartBoundsInBox operation, then loop through the table and log each parts coordinates. For a flat circle I think I would use a workspace:GetPartBoundsInRadius then loop through the table that the operation gives me and filter by a specified Y coordinate, maybe to optimize just alter the way the grid works, this may not be an option for what you’re doing but I would just increase the gap between for each part so less items can be found.
Here is my approach, this assumes that the block is not rotated. Not sure if it works, you should test it yourself:
-- Returns every possible integer position in an unrotated block/ball.
local function getIntPositionsInBounds(boundPart: Part)
local intPositionArray: {Vector3} = {}
if boundPart.Shape == Enum.PartType.Block then
-- This assumes that the block is not rotated.
local lowerBounds = (boundPart.Position - boundPart.Size / 2):Ceil()
local higherBounds = (boundPart.Position + boundPart.Size / 2):Floor()
for x = lowerBounds.X, higherBounds.X do
for y = lowerBounds.Y, higherBounds.Y do
for z = lowerBounds.Z, higherBounds.Z do
local intPosition = Vector3.new(x, y, z)
table.insert(intPositionArray, intPosition)
end
end
end
elseif boundPart.Shape == Enum.PartType.Ball then
-- It wouldn't matter if you rotated the ball.
-- Act like the ball is a block, if the distance between the point and
-- the position of the ball is bigger than the radius of the ball then
-- ignore that position and continue to the next one.
local lowerBounds = (boundPart.Position - boundPart.Size / 2):Ceil()
local higherBounds = (boundPart.Position + boundPart.Size / 2):Floor()
for x = lowerBounds.X, higherBounds.X do
for y = lowerBounds.Y, higherBounds.Y do
for z = lowerBounds.Z, higherBounds.Z do
local intPosition = Vector3.new(x, y, z)
if (intPosition - boundPart.Position).Magnitude > boundPart.Size.X / 2 then continue end
table.insert(intPositionArray, intPosition)
end
end
end
else
error("'boundPart' is not a block or a ball.")
end
return intPositionArray
end
print(getIntPositionsInBounds(-- Block/ball here.))
You could also add a check to see if the block is rotated (and perhaps error if it is), hope this helps!
I went ahead and wrote a short module to illustrate my idea. As I mentioned, instead of storing the exponential amount of Vector3s it simply calculates the one it needs. I wrote the vast majority of this on my phone so I apologize if it errors.
--!strict
local Query = {}
Query.Sphere = {}
Query.Cube = {}
function newQuery(ScalarName: string, ScalarValue: number, CF: CFrame?, SampleFunction: (index: number, Scalar: number) -> Vector3?)
local Query = {
_Cache = {};
[ScalarName] = ScalarValue;
CFrame = CF or CFrame.new();
}
--[[
Samples the query at index to retrieve a point.
]]
function Query:Sample(index: number): Vector3?
if not self._Cache[index] then
local Position = SampleFunction(index, self[ScalarName])
if Position then
self._Cache[index] = self.CFrame * Position
end
end
return self._Cache[index]
end
return Query
end
--[[
Determines the position of the point corresponding to the nth (index) element in a hypothetical ordering of integer triples in a sphere of radius Scalar.
The ordering assumes an XZY coordinate system.
]]
function Query.Sphere.GetSphereCell(index: number, Scalar: number): Vector3?
if index >= 1 then
local Position = Vector3.zero
local FoundZ = false
for Z = -math.floor(Scalar), math.floor(Scalar) do
local PlanarRadius = math.floor(math.sqrt(Scalar ^ 2 - Z ^ 2))
local PlaneCount = 0
for Y = -PlanarRadius, PlanarRadius do
local LinearRadius = math.floor(math.sqrt(Scalar ^ 2 - Z ^ 2 - Y ^ 2))
PlaneCount += 2 * LinearRadius + 1
end
if index > PlaneCount then
index -= PlaneCount
else
Position += Z * Vector3.zAxis
FoundZ = true
break
end
end
if FoundZ then
local ZPlanarRadius = math.floor(math.sqrt(Scalar ^ 2 - Position.Z ^ 2))
for Y = -ZPlanarRadius, ZPlanarRadius do
local LinearRadius = math.floor(math.sqrt(Scalar ^ 2 - Position.Z ^ 2 - Y ^ 2))
local RowCount = 2 * LinearRadius + 1
if index > RowCount then
index -= RowCount
else
Position += Y * Vector3.yAxis
break
end
end
local YLinearRadius = math.floor(math.sqrt(Scalar ^ 2 - Position.Z ^ 2 - Position.Y ^ 2))
Position += (index - YLinearRadius - 1) * Vector3.xAxis
return Position
end
end
return nil
end
--[[
Determines the position of the point corresponding to the nth (index) element in a hypothetical ordering of integer triples in a cube of sidelength Scalar.
The ordering assumes an XZY coordinate system.
]]
function Query.Cube.GetCubeCell(index: number, Scalar: number): Vector3?
if 1 <= index and index <= Scalar ^ 3 then
return Vector3.new(
index % Scalar,
math.ceil(index % Scalar ^ 2 / Scalar),
math.ceil(index / Scalar ^ 2)
)
end
return nil
end
--[[
Creates and returns a sphere which can be sampled from.
]]
function Query.Sphere.new(Radius: number, CFrame: CFrame?)
return newQuery("Radius", Radius, CFrame, Query.Sphere.GetSphereCell)
end
--[[
Creates and returns a cube which can be sampled from.
]]
function Query.Cube.new(Sidelength: number, CFrame: CFrame?)
return newQuery("Sidelength", Sidelength, CFrame, Query.Cube.GetCubeCell)
end
return Query
An example use case would be like so:
local ShapeQuery = require(path_to_module)
local Sphere = ShapeQuery.Sphere.new(50, CFrame.new(300, 50, 300))
local Position = Sphere:Sample(325000) -- ONLY calculates and caches the 325000th position, as opposed to all 523000 in a radius 50 sphere