How would I optimize this cellular automata script?

How Would I optimize this?

Place File:
Cellular Automata 3D Testing-2.rbxl (25.0 KB)

local RunService = game:GetService("RunService")
local Cells = game.Workspace.Cells:GetChildren()
local Sqrt = math.sqrt

local Friction = 1.05
local Distancing = 1.5

for i,v in pairs(Cells) do
	v.Position += Vector3.new(0,math.random(-10000,10000)/1000000,0)
end

function GetMagnitude(X,Z)
	return Sqrt(
		X^2 + 
			Z^2
	)
end

for i,v in pairs(Cells) do
	local Hates = game.Lighting.Hates:FindFirstChild(v:FindFirstChildOfClass("StringValue").Name):GetChildren()
	for i2,v2 in pairs(Hates) do
		v2:Clone().Parent = v:FindFirstChildOfClass("StringValue")
	end
	wait()
end

script.Parent:WaitForChild("Humanoid").Touched:Connect(function(hit)
	if hit and hit.Name == "Cell" then
		hit.Velocity += script.Parent:WaitForChild("Humanoid").RootPart.Velocity/100
	end
end)


RunService.Stepped:Connect(function(t,dt)
	for i,v in pairs(Cells) do
		local Position = (v.Position+(v.Velocity/Friction))

		if math.abs(Position.X) > 50 then
			v.Position = Vector3.new(math.clamp(-v.Position.X,-99,99),v.Position.Y,v.Position.Z)
		end

		if math.abs(Position.Z) > 50 then
			v.Position = Vector3.new(v.Position.X,v.Position.Y,math.clamp(-v.Position.Z,-99,99))
		end


		for i2,v2 in pairs(Cells) do
			if v2 ~= v then
				local Delta = (v2.Position-Position)
				local Distance = GetMagnitude(Delta.X,Delta.Z)^2

				if Distance > 100 then
					Distance = 0
				else
					if v:FindFirstChildOfClass("StringValue"):FindFirstChild(v2:FindFirstChildOfClass("StringValue").Name) then
						Distance = -Distance/2
						Distancing =-1
					else
						Distancing = 1
					end

					if Distance < Distancing then
						Distance = -math.abs(Distance)
					end

					v.Velocity += (Delta/Distance)/100
				end
			end
		end

		v.Velocity = v.Velocity/Friction
		v.Position += Vector3.new(v.Velocity.X,0,v.Velocity.Z)
	end
end)

If you’re looking to get feedback on working code, you should move it into #help-and-feedback:code-review

2 Likes

Too lazy to write a whole 5-page essay but here:

  1. use ipairs instead of pairs to iterate through :GetChildren(),
    ipairs meant for arrays
    pairs or next meant for dictionaries

  2. do not make a custom magnitude function. Just use the default .Magnitude key that is automatically given for each Vector object constructed.

  3. use runserviceHeartbeat:Wait() to yield for a frame instead of wait()

  4. yield for the humanoid before assigning the listener or else you are completely ruining the purpose of the :WaitForChild() method

local humanoid = script.Parent:WaitForChild("Humanoid")
if humanoid then
    humanoid.Touched:Connect(function(hit) -- I wasn't aware it was possible to assign a touched connection to a humanoid 0.o 
	     if hit and hit.Name == "Cell" then
		    hit.Velocity += script.Parent:WaitForChild("Humanoid").RootPart.Velocity/100
	     end
    end)
end
  1. when you are using the generic for loop it is good practice to always mark variables you don’t need as “_” since that variable will actually be garbage collected saving memory.
-- Not good
	for i2,v2 in pairs(Hates) do
		v2:Clone().Parent = v:FindFirstChildOfClass("StringValue")
	end
-- Good
	for _,v2 in pairs(Hates) do
		v2:Clone().Parent = v:FindFirstChildOfClass("StringValue")
	end
  1. your runservice connection seems pretty expensive, considering its running a for loop doing a bunch of calculations per frame. I’m not sure if there is a more optimal solution but if there isn’t then your connection is fine.

I like the logic of your code tho :>
Happy Coding

3 Likes

when you are using the generic for loop it is good practice to always mark variables you don’t need as “_” since that variable will actually be garbage collected saving memory.

I don’t think you know how garbage collection in Luau works. Marking a variable as _ will not be garbage collected immediately, people mark those variables as _ because it tells the reader that that variable has no use. Those variables will be garbage collected after the do scope ends assuming they have no strong references.

-- Good
	for i2,v2 in pairs(Hates) do
		v2:Clone().Parent = v:FindFirstChildOfClass("StringValue")
	end

-- Also Good but readability slightly better
	for _ ,v2 in pairs(Hates) do
		v2:Clone().Parent = v:FindFirstChildOfClass("StringValue")
	end
2 Likes

Why not? You can have an optimization where you call the function multiple times:

There was also another post about this that I can’t find.

Edit:

Turns out this was the fastest method back then when I used to be active on the forums, now using Magnitude is the fastest methodology thanks to changes. I apologize I wasn’t aware of the change.

https://devforum.roblox.com/t/psa-magnitude-now-obliterates-squared-distance-checks-in-luau-with-benchmarks/959737

2 Likes

each vector object you create with Vector(Dimension).new already has a .Magnitude key. No need for the extraneous function calling.

Can you elaborate more. I’m not understanding your point.

1 Like

Another note, using ipairs instead of pairs on an array makes a very small difference. Numerical for loops are faster than the two, but it optimizes very little.

local childTable = Instance:GetChildren()
for n = 1, #childTable do
    local child = childTable[n]
end

is an example of how you would use a numerical loop with GetChildren()

FindFirstChild() is actually 30% slower than the dot operator

Do this:

for i,v in pairs(Cells) do
     local childStringValue = v:FindFirstChildOfClass("StringValue")
     local Hates = game.Lighting.Hates[childStringValue.Name]:GetChildren()
     for i2,v2 in pairs(Hates) do
        v2:Clone().Parent = childStringValue
     end
     runService.Heartbeat:Wait()
end

Instead of

for i,v in pairs(Cells) do
	local Hates = game.Lighting.Hates:FindFirstChild(v:FindFirstChildOfClass("StringValue").Name):GetChildren()
	for i2,v2 in pairs(Hates) do
		v2:Clone().Parent = v:FindFirstChildOfClass("StringValue")
	end
	wait()
end

numerical loops are not recommended because of the fact ipairs caches the value. Indexing a key in a table is not instantaneous.

-- Not Cached Automatically
for i = 1, #array do
    local value = array[i]
     print(value)
end
-- Cached
for index, value in ipairs(array) do
      print(value)
end
local array = {}
for i = 1, 100 do
	array[#array + 1] = math.random(1, 10)
end

for i = 1, 100 do
	local numericTime = nil
	local ipairsTime = nil
	
	local startTime = os.clock()
	for i = 1, #array do
		local value = array[i]
	end
	numericTime = os.clock() - startTime
	startTime = os.clock()
	for index, value in ipairs(array) do
	end
	ipairsTime = os.clock() - startTime
	
	print(numericTime < ipairsTime and ('numeric is faster: %s'):format(numericTime) or ('ipairs is faster: %s'):format(ipairsTime))
end

i’ve tested this benchmark numerous times, and the generic is faster than numeric over 95% of the time. Only time I would use numeric is when iterating through an array backwards or by certain multiples, etc… If you are concerned about speed use ipairs.

Your “simple trick” is slower than the native .Magnitude property of all Vector3s. Look at these benchmarks:

local ITERATIONS = 1e7;
local TEST_VECTOR = Vector3.new(1,2,3);

local function GetMagnitude(vec: Vector3): number
	local x,y,z = vec.X,vec.Y,vec.Z;
	return math.sqrt(x*x+y*y+z*z);
end

local startTick = tick();
for _=1,ITERATIONS do
	local magnitude = GetMagnitude(TEST_VECTOR);
end
local timeGetMagnitude = tick()-startTick;

startTick = tick();
for _=1,ITERATIONS do
	local magnitude = TEST_VECTOR.Magnitude;
end
local timeNativeMagnitude = tick()-startTick;

startTick = tick();
for _=1,ITERATIONS do
	local magnitude = GetMagnitude(Vector3.new(1,2,3));
end
local timeInstantiateGetMagnitude = tick()-startTick;

startTick = tick();
for _=1,ITERATIONS do
	local magnitude = Vector3.new(1,2,3).Magnitude;
end
local timeInstantiateNativeMagnitude = tick()-startTick;

print(string.format("Timing Comparisons:\n\nGetMagnitude(): %fs\nNative .Magnitude: %fs\nGetMagnitude with new Vector3s: %fs\nNative .Magnitude with new Vector3s: %fs\n\nMagnitude = %f",timeGetMagnitude,timeNativeMagnitude,timeInstantiateGetMagnitude,timeInstantiateNativeMagnitude,TEST_VECTOR.Magnitude));

Timing Comparisons:

GetMagnitude(): 1.105992s
Native .Magnitude: 0.385828s
GetMagnitude with new Vector3s: 1.905560s
Native .Magnitude with new Vector3s: 1.138401s

Magnitude = 3.741657

2 Likes

must be a newer update then :man_shrugging: , ipairs used to be slower.

1 Like

Seems like it has been optimized, pretty sure it used to be expensive.

1 Like

.Magnitude is a key not a function. how can indexing a key be more expensive than calling a function?

1 Like

It’s internally calculated whenever you index it and that internal calculation is expensive.

1 Like

A calculation on top of that internal calculation is redundant

1 Like

It is because every time you use a vector3.new operator, the magnitude is internally calculated. Using your function would just use more performance, since you’re recalculating the magnitude again.

1 Like

Using your function would just use more performance, since you’re recalculating the magnitude again.

No, .Magnitude is calculated when you try to index it.

1 Like

No we’re not on the same page here, I know your function uses less performance. However, just by doing Vector3.new() the magnitude was already calculated. What’s the use of calculating it again with your function.

1 Like

the magnitude was already calculated. What’s the use of calculating it again with your function.

It isn’t like I said, it is only when you try to index it. Why would it calculate the magnitude unnecessarily, it will do if it is needed and that’s by indexing it. >.<

1 Like

Do you have the source on where I can find this?

1 Like