Hello.
I made a simple client script to highlight adjacent squares on a grid for a mini-game that I’m creating. The problem is, it ends up repeating over 2500 times, causing the client script to lag very badly due to the exponential amount of operations for each highlight or “infection” to each square.
SimpleBoardClicker.rbxm (7.1 KB)
(put in StarterGui)
Here is the code if anyone takes a look:
local TweenService = game:GetService("TweenService")
local Infecting = false
local Infected = {}
local Operations = 0
function GetAdjacent(Position)
local X = Position.X
local Y = Position.Y
local Adjacent = {}
local function ShareX(Tile)
if X == Tile.AbsolutePosition.X then
return true
end
return false
end
local function ShareY(Tile)
if Y == Tile.AbsolutePosition.Y then
return true
end
return false
end
for _, Tile in next, script.Parent:GetChildren() do
if Tile:IsA("TextButton") then
if Tile.AbsolutePosition.X == X - 26 and ShareY(Tile) and not Infected[tonumber(Tile.Name)] then
Adjacent.Left = Tile
end
if Tile.AbsolutePosition.X == X + 26 and ShareY(Tile) and not Infected[tonumber(Tile.Name)] then
Adjacent.Right = Tile
end
if Tile.AbsolutePosition.Y == Y - 26 and ShareX(Tile) and not Infected[tonumber(Tile.Name)] then
Adjacent.Above = Tile
end
if Tile.AbsolutePosition.Y == Y + 26 and ShareX(Tile) and not Infected[tonumber(Tile.Name)] then
Adjacent.Below = Tile
end
end
end
return Adjacent
end
function Infect(Number)
spawn(function()
if not Infected[Number] then
Operations = Operations + 1
Number = tonumber(Number)
Infected[Number] = true
local Tile = script.Parent:FindFirstChild(tostring(Number))
local Adjacent = {}
TweenService:Create(Tile, TweenInfo.new(1), {BackgroundColor3 = Color3.new(0.5, 1, 0.5)}):Play()
for _, Tile in next, GetAdjacent({X = Tile.AbsolutePosition.X, Y = Tile.AbsolutePosition.Y}) do
Infect(Tile.Name)
end
end
end)
end
for _, Tile in next, script.Parent:GetChildren() do
if Tile:IsA("TextButton") then
Tile.MouseButton1Down:Connect(function()
Infect(Tile.Name)
end)
end
end
spawn(function()
while wait() do
print("Successful operations:", Operations)
end
end)
Any help is appreciated! Thank you
1 Like
The main issue is that you perform all your calculations directly on instances. Any kind of a floodfill algorithm will be a hog for your CPU, causing heavy performance drops, but adding indexing to that takes it to a different level.
What I’d do is ditch the OOP design altogether and initialize all properties into separate row-major order arrays, just like you already do with the Infected array. This will avoid the costly lookups on instances.
Another thing I’d add are occasional pauses when the frame time is above 1/45 of a second.
local st = tick()
--code in a loop or something
if (tick()-st)>1/45 then
heartbeat:Wait()
st = tick()
end
Don’t forget to use table.create(arrsize) whenever possible as it avoids resizing the array every time you add a new element.
4 Likes
I think what noonisback means is to store all the tiles in a way that you can look up tiles at any coordinate instead of having to search for tiles with a given coordinate every time.
I usually use a 2D table, so that’s how I’ll do it in these examples.
So your GetAdjecent function could be simplified to
function GetAdjacent(Position)
--Make sure Position has already been divided by tileSize, or change this function to divide the x and y coords
local x, y = Position.X, Position.Y
local Adjacent = {
Tiles[x - 1][y],
Tiles[x + 1][y],
Tiles[x][y - 1],
Tiles[x][y + 1],
}
return Adjacent
end
Just dictionary lookups, no searching through every tile in the game.
Of course you'll need to set up the Tiles table, which you can do at the start of the game like this:
local tileSize = 26
local Tiles = {}
for _, Tile in next, script.Parent:GetChildren() do
if Tile:IsA("TextButton") then
local tileX = Tile.AbsolutePosition.X / tileSize
local tileY = Tile.AbsolutePosition.Y / tileSize
--Setup row, if it hasn't already been set up
Tiles[tileX] = Tiles[tileX] or {}
--Set col, overwriting if it has already been set (i.e. if some other Tile has the same coordinate, maybe throw an error instead?)
Tiles[tileX][tileY] = Tile
end
end
A few other issues with your script:
Calling GetAdjecent like this:
GetAdjacent({X = Tile.AbsolutePosition.X, Y = Tile.AbsolutePosition.Y})
It seems like there’s no good reason to do it like that. Just pass the X coord as the first parameter and the Y coord as the second. It’s never difficult to remember which order X and Y go in, it’s literally the alphabet.
GetAdjacent(Tile.AbsolutePosition.X, Tile.AbsolutePosition.Y)
for _, Tile in next, script.Parent:GetChildren() do
if Tile:IsA("TextButton") then
Tile.MouseButton1Down:Connect(function()
Infect(Tile.Name)
end)
end
end
Setting up listeners for every tile is probably a bad idea. At least, now that you can just look up tiles at any coordinate you don't *need* to. Instead you can do
function getTileScreenSpace(screenX, screenY)
local tileX, tileY = math.floor(screenX/tileSize + 0.5), math.floor(screenY/tileSize + 0.5)
local Tile
if Tiles[tileX] then
return Tiles[tileX][tileY]
end
end
game:GetService("UserInputService").InputBegan:Connect(function(input)
if input.UserInputType == Enum.UserInputType.MouseButton1 then
local mouseX, mouseY = input.Position.X, input.Position.Y
local Tile = getTileScreenSpace(mouseX, mouseY)
if Tile then
Infect(Tile.Name)
end
end
end)
1 Like
I made a short floodfill function that does practically what you want.
@ThanksRoBama Yes, but also no. What I meant is that accessing data from instances is relatively slow, unlike with arrays. In other words, replace everything with arrays and it will be a lot faster. Same goes for multi dimensional arrays as accessing an element inside an array inside an array is practically 2x slower than a row major order.
In a 100x100 array, accessing multidimensional arrays would be [x][y], while for row majors it would be [y*100+x+1], given your position starts with 0 and ends with 99.
local confSizeX = 100
local confSizeY = 100
local confMaxSpread = 10 --max steps to make
local confNodeCount = confSizeX*confSizeY
local arrNode = table.create(confNodeCount)
local arrMarked = table.create(confNodeCount,false)
local function mark(px,py,id)
print("Node "..px.." "..py.." marked.")
--do whatever you want here
end
local function MarkNodeStart(px0,py0,maxspread)
if arrMarked[py0*confSizeX+px0+1] then
print("ERROR: Node already marked.")
return
end
arrMarked[py0*confSizeX+px0+1] = true
local maxspread = maxspread or confMaxSpread
local countLast = 1
local listLastX = {px0}
local listLastY = {py0}
local countSpread = 0
local st = tick()
while listLastX[1] do
local newLastX = table.create(countLast*2)
local newLastY = table.create(countLast*2)
local newCountLast = 0
for i=1,countLast do
local px0 = listLastX[i]
local py0 = listLastY[i]
local id0 = py0*confSizeX+px0+1
if px0~=0 then --left (negative)
local id1 = id0-1
if not arrMarked[id1] then
arrMarked[id1] = true
newCountLast = newCountLast+1
newLastX[newCountLast] = px0-1
newLastY[newCountLast] = py0
mark(px0-1,py0,id1)
end
end
if px0~=confSizeX-1 then --right (positive)
local id1 = id0+1
if not arrMarked[id1] then
arrMarked[id1] = true
newCountLast = newCountLast+1
newLastX[newCountLast] = px0+1
newLastY[newCountLast] = py0
mark(px0+1,py0,id1)
end
end
if py0~=0 then --up (negative)
local id1 = id0-confSizeX
if not arrMarked[id1] then
arrMarked[id1] = true
newCountLast = newCountLast+1
newLastX[newCountLast] = px0
newLastY[newCountLast] = py0-1
mark(px0,py0-1,id1)
end
end
if py0~=confSizeY-1 then --down (positive)
local id1 = id0+confSizeX
if not arrMarked[id1] then
arrMarked[id1] = true
newCountLast = newCountLast+1
newLastX[newCountLast] = px0
newLastY[newCountLast] = py0+1
mark(px0,py0+1,id1)
end
end
end
countSpread = countSpread+1
if countSpread == maxspread then
print("Reached max spread")
break
end
listLastX = newLastX
listLastY = newLastY
countLast = newCountLast
if (st-tick())>1/45 then
--heartbeat:Wait()
st = tick()
end
end
print("Done")
--console visualizer because I don't have Roblox rn
local str = ""
for y=0,confSizeY-1 do
for x=0,confSizeX-1 do
local id = y*confSizeX+x+1
str = str..(arrMarked[id] and "#" or "_")
end
str = str.."\n"
end
print(str)
end
--a small test
MarkNodeStart(10,10,50)
Instances are slower to access because a function has to be fired which returns the value in question. IDK how Luau handles this, but it’s still slower than a pure array.
1 Like