How would I optimize this sand simulation script?

So I’m trying to optimize this script that iterates by a lot

A video on what happends
Sand Simulation.wmv (231.9 KB)

The Code

local RunService = game:GetService("RunService")
local Parts = workspace:WaitForChild("Parts"):GetChildren()
local TouchPart = workspace:WaitForChild("TouchPart")

local TouchFunction = TouchPart.Touched:Connect(function() return end)
function GetNeighbors(Pos,Size)
	TouchPart.Size = Vector3.new(Size,0.05,Size)
	TouchPart.Position = Pos
	local TouchingParts = TouchPart:GetTouchingParts() -- very costly but I don't know a better alrernative
	return TouchingParts
end

function GetBlocked(v,Neighbors)
	local Blocked = {} --Up,Down,Right,Left,DownRight,DownLeft,UpRight,UpLeft	
	for _,v2 in pairs(Neighbors) do
		if v2.Name == "Part" then
			if v2.Position.Z == v.Position.Z-1 and v2.Position.X == v.Position.X then
				table.insert(Blocked,2,true)
			end
			if v2.Position.Z == v.Position.Z+1 and v2.Position.X == v.Position.X then
				table.insert(Blocked,1,true)
			end
			if v2.Position.X == v.Position.X-1 and v2.Position.Z == v.Position.Z then
				table.insert(Blocked,3,true)
			end
			if v2.Position.X == v.Position.X+1 and v2.Position.Z == v.Position.Z then
				table.insert(Blocked,4,true)
			end

			if v2.Position == v.Position+Vector3.new(-1,0,-1) then
				table.insert(Blocked,5,true)
			end
			if v2.Position == v.Position+Vector3.new(1,0,-1) then
				table.insert(Blocked,6,true)
			end
			if v2.Position == v.Position+Vector3.new(-1,0,1) then
				table.insert(Blocked,7,true)
			end
			if v2.Position == v.Position+Vector3.new(1,0,1) then
				table.insert(Blocked,8,true)
			end
		end
	end
	return Blocked
end

------------------------------------------------------

workspace:WaitForChild("Parts").ChildAdded:Connect(function(c)
	table.insert(Parts,c)
end)

while true do
	for _ = 1,1 do RunService.Stepped:Wait() end -- so I can set the speed
	for _,v in pairs(Parts) do
		local Neighbors = GetNeighbors(v.Position,2)
		local Blocked = GetBlocked(v,Neighbors)
		
		if Blocked[2] == nil then
			if v.Position.Z > -100 then
				v.Velocity = Vector3.new(0,0,-1)
			else
				v.Velocity = Vector3.new(0,0,0)
			end
		elseif Blocked[6] == nil and Blocked[4] == nil then
			v.Velocity = Vector3.new(1,0,-1)
		elseif Blocked[5] == nil and Blocked[3] == nil then
			v.Velocity = Vector3.new(-1,0,-1)
		else
			v.Velocity = Vector3.new(0,0,0)
		end
	end
	
	for _,v in pairs(Parts) do
		if math.abs(v.Velocity.X)+math.abs(v.Velocity.Z) ~= 0 then
			local OldPos = v.Position
			v.Position += v.Velocity
			local Neighbors = GetNeighbors(v.Position,2)
			for _,v2 in pairs(Neighbors) do
				if v2.Position == OldPos + v.Velocity and v2.Name == "Part" and v2 ~= v then
					v.Position = OldPos
					break
				end
			end
			if v.Position.Z <= -100 then
				v.Position += Vector3.new(0,0,1)
			end
		end
	end
end

1 Like

You can create a swich statement after the “if v2.Name == “Part” then” if statement.
You can create an if statement with this.

1 Like

Why not just use else-if, if anything that’d be slightly less optimised. You can however localise the name which would optimise further.

1 Like
Okay so first off you can make your GetBlocked function *soooo* much more readable like this:

Having magic numbers like you do all over the place is usually a bad idea. Give the numbers a descriptive name like this:

(also changed table.insert(Blocked, table_index, value) to Blocked[table_index]=value. Should be slightly faster and a bunch shorter)

local DIR_ID_UP = 1
local DIR_ID_DOWN = 2
local DIR_ID_RIGHT = 3
local DIR_ID_LEFT = 4
local DIR_ID_DOWNRIGHT = 5
local DIR_ID_DOWNLEFT = 6
local DIR_ID_UPRIGHT = 7
local DIR_ID_UPLEFT = 8

function GetBlocked(v,Neighbors)
	local Blocked = {} --Up,Down,Right,Left,DownRight,DownLeft,UpRight,UpLeft	
	for _,v2 in pairs(Neighbors) do
		if v2.Name == "Part" then
			if v2.Position.Z == v.Position.Z-1 and v2.Position.X == v.Position.X then
				Blocked[DIR_ID_DOWN] = true
			end

			if v2.Position.Z == v.Position.Z+1 and v2.Position.X == v.Position.X then
				Blocked[DIR_ID_UP] = true
			end

			if v2.Position.X == v.Position.X-1 and v2.Position.Z == v.Position.Z then
				Blocked[DIR_ID_RIGHT] = true
			end

			if v2.Position.X == v.Position.X+1 and v2.Position.Z == v.Position.Z then
				Blocked[DIR_ID_LEFT] = true
			end

			if v2.Position == v.Position+Vector3.new(-1,0,-1) then
				Blocked[DIR_ID_DOWNRIGHT] = true
			end

			if v2.Position == v.Position+Vector3.new(1,0,-1) then
				Blocked[DIR_ID_DOWNLEFT] = true
			end

			if v2.Position == v.Position+Vector3.new(-1,0,1) then
				Blocked[DIR_ID_UPRIGHT] = true
			end

			if v2.Position == v.Position+Vector3.new(1,0,1) then
				Blocked[DIR_ID_UPLEFT] = true
			end
		end
	end
	return Blocked
end

Next, you can replace your current "neighbor finding approach" with a simple lookup. That should be a lot faster than doing actual 3D collision detection with GetTouchingParts. You can use a 2D map for it:
local particles = {}

function mapGet(map, c1, c2)
    if map[c1] then --Could be omitted if you initialize map to be all empty tables, e.g. particles = table.create({}, mapWidth)
        return map[c1][c2]
    end
end

function mapSet(map, c1, c2, value)
    map[c1] = map[c1] or {} --Could be omitted if you initialize map to be all empty tables, e.g. particles = table.create({}, mapWidth)
    map[c1][c2] = value
end

Your GetNeighbors function can then look like

function getNeighbors(point)
    local neighbors = {}

    local x0, y0 = point.X, point.Y
    local n1, n2, n3, n4 = 
        mapGet(particles, x0 + 1, y0),
        mapGet(particles, x0, y0 + 1),
        mapGet(particles, x0 - 1, y0),
        mapGet(particles, x0, y0 - 1)
    
    if n1 then neighbors[#neighbors+1] = n1 end
    if n2 then neighbors[#neighbors+1] = n2 end
    if n2 then neighbors[#neighbors+1] = n3 end
    if n2 then neighbors[#neighbors+1] = n4 end

    return neighbors
end

Now that I'm starting to understand your code a bit, it seems like your `GetBlocked` function could be improved. A particle can presumably only "block" a single coordinate, so if you've determined which coordinate a particle blocks, you don't need to check if it blocks any other coordinates:
function GetBlocked(v,Neighbors)
	local Blocked = {} --Up,Down,Right,Left,DownRight,DownLeft,UpRight,UpLeft	
	for _,v2 in pairs(Neighbors) do
		if v2.Name == "Part" then
			if v2.Position.Z == v.Position.Z-1 and v2.Position.X == v.Position.X then
				Blocked[DIR_ID_DOWN] = true
                continue --Dont' check the other coordinates 
			end
			...
			if v2.Position == v.Position+Vector3.new(1,0,1) then
				Blocked[DIR_ID_UPLEFT] = true
                continue
			end
		end
	end

	return Blocked

The biggest issue is probably that for each particle you’re checking every other particle to see if the destination for a particle is blocked, which gives it a running time of like O(n^2), i.e. slow as heck. Using the 2d map approach can make it O(n), i.e. decently fast.

I tried making a version based on what you posted, here it is:

local particles = game.Workspace:WaitForChild"Parts"):GetChildren() --array
local particleMap = {} --2d map

function mapGet(map, c1, c2)
    if map[c1] then --Could be omitted if you initialize map to be all empty tables, e.g. particleMap = table.create({}, mapWidth)
        return map[c1][c2]
    end
end

function mapSet(map, c1, c2, value)
    map[c1] = map[c1] or {} --Could be omitted if you initialize map to be all empty tables, e.g. particleMap = table.create({}, mapWidth)
    map[c1][c2] = value
end

--Build particleMap based on where the particles start
for _, particle in pairs(particles) do
    local pos = particle.Position
    assert( not mapGet(particleMap, pos.X, pos.Z), ("Two particles have the same position (%s)"):format(tostring(pos)) )
    assert( pos.X%1 == 0 and pos.Z%1 == 0, ("A particle has non-integer coordinates (%s)"):format(tostring(pos)))
    mapSet(particleMap, pos.X, pos.Z, particle)
end

local neighbor_dirs = {
    Vector3.new(1, 0, 0),
    Vector3.new(1, 0, 1),
    Vector3.new(0, 0, 1),
    Vector3.new(-1, 0, 1),
    Vector3.new(-1, 0, 0),
    Vector3.new(-1, 0, -1),
    Vector3.new(0, 0, -1),
    Vector3.new(1, 0, -1),
}

local DIR_ID_RIGHT = 1
local DIR_ID_UPRIGHT = 2
local DIR_ID_UP = 3
local DIR_ID_UPLEFT = 4
local DIR_ID_LEFT = 5
local DIR_ID_DOWNLEFT = 6
local DIR_ID_DOWN = 7
local DIR_ID_DOWNRIGHT = 8

function getNeighbor(point, dir_id)
    return mapGet(
        particleMap, 
        point.X + neighbor_dirs[dir_id].X, 
        point.Z + neighbor_dirs[dir_id].Z
    )
end

function stepSimulation()
    --Update velocities based on which neighbor positions are empty
    local ndirs = neighbor_dirs
    for _, particle in pairs(particles) do
        local pos = particle.Position
        if not getNeighbor(pos, DIR_ID_DOWN) then --Try moving down
            if particle.Position.Z > -100 then
                particle.Velocity = neighbor_dirs[DIR_ID_DOWN]
            else
                --Don't move down if far below the ground (???)
                particle.Velocity = neighbor_dirs[DIR_ID_UP] --actually, move it up???
            end
        elseif (not getNeighbor(pos, DIR_ID_LEFT)) and (not getNeighbor(pos, DIR_ID_DOWNLEFT)) then --Try moving down to the left
            particle.Velocity = neighbor_dirs[DIR_ID_DOWNLEFT]
        elseif (not getNeighbor(pos, DIR_ID_RIGHT)) and (not getNeighbor(pos, DIR_ID_DOWNRIGHT)) then --Try moving down to the right
            particle.Velocity = neighbor_dirs[DIR_ID_DOWNRIGHT]
        else --Everything is blocked, stand still
            particle.Velocity = Vector3.new(0, 0, 0)
        end
    end

    --Update position
    for _, particle in pairs(particles) do
        if particle.Velocity.X ~= 0 or particle.Velocity.Z ~= 0 then --Don't move if standing still
            local new_pos = particle.Position + particle.Velocity
            if not mapGet( particleMap, new_pos.X, new_pos.Z ) then --Don't move if destination is blocked
                mapSet(particleMap, particle.Position.X, particle.Position.Z, nil)
                particle.Position = new_pos
                mapSet(particleMap, particle.Position.X, particle.Position.Z, particle)
            end
        end
    end
end

sGRvBGs4ot

It doesn’t seem to work correctly, but it’s fairly easy to tweak the rules so you can get it to work like you want. Hope this helps!

2 Likes

i was thinking of a 2D map but i wasn’t sure how to operate it. also that doesn’t look right because I messed up the if statement things for down right-left