Game lags ALOT from water flowing script

You can write your topic however you want, but you need to answer these questions:

  1. What do you want to achieve? Keep it simple and clear!

Hello, i am working on grid based water-physics. But I’ve found out that the game begins lagging BADLY when water begins to flow downwards.

  1. What is the issue? Include screenshots / videos if possible!

Water-Physics lags game badly when flowing downwards. I’m basically forced to use task manager to close studio.

  1. What solutions have you tried so far? Did you look for solutions on the Creator Hub?

I’ve looked through the forums for any issues but i cant find my solution.

I think the issue is the raycasts hitting nothing? I’m really confused, but it doesn’t lag out when CanQuery is enabled or the liquid is in a raycast exclusion table. (so it doesn’t hit the water)

After that, you should include more details if you have any. Try to make your topic as descriptive as possible, so that it’s easier for people to help you!

Here’s my script:

local RunService = game:GetService("RunService")
local Block = script.Parent
local Folder = workspace.Liquids -- Holds all the liquid blocks

--CONFIG--
local TickRate = 1	-- time between each raycast

local function fireRay(part)
	local dist = 3
	local origin = part.Position
	local direction = part.CFrame.UpVector * part.Size.Y
	local direction2 = part.CFrame.RightVector * part.Size.X
	local direction3 = part.CFrame.LookVector * part.Size.Z
	local rayParams = RaycastParams.new()
	rayParams.FilterDescendantsInstances = {Folder} -- rays ignore liquid blocks
	rayParams.FilterType = Enum.RaycastFilterType.Exclude
	local result = workspace:Raycast(origin, -direction, rayParams, dist)
	local result2 = workspace:Raycast(origin, direction2, rayParams, dist)
	local result3 = workspace:Raycast(origin, direction3, rayParams, dist)
	local result4 = workspace:Raycast(origin, -direction2, rayParams, dist)
	local result5 = workspace:Raycast(origin, -direction3, rayParams, dist)
	
	if result == nil then -- down
		local newWater = Block:Clone()
		newWater.Position = part.Position - direction
		newWater.Parent = workspace.Liquids
	else
		if result2 == nil then -- right
			local newWater = Block:Clone()
			newWater.Position = part.Position + direction2
			newWater.Parent = workspace.Liquids
		end
		if result3 == nil then -- front
			local newWater = Block:Clone()
			newWater.Position = part.Position + direction3
			newWater.Parent = workspace.Liquids
		end
		if result4 == nil then -- left
			local newWater = Block:Clone()
			newWater.Position = part.Position - direction2
			newWater.Parent = workspace.Liquids
		end
		if result5 == nil then -- back
			local newWater = Block:Clone()
			newWater.Position = part.Position - direction3
			newWater.Parent = workspace.Liquids
		end
	end
end

RunService.Heartbeat:Connect(function()
	task.wait(TickRate)
	fireRay(Block)
end)

Please do not ask people to write entire scripts or design entire systems for you. If you can’t answer the three questions above, you should probably pick a different category.

thanks for reading, hope you know how to help with my problem! :smiley:

you dont have task.wait()

if thats not the solution i dont really know scripting

So first thing to do would be to add timestamps, from what i think its going to be the raycasts, you can get the rough time something takes to run using bacaic math with tick().

eg local TickBefore = tick()
section of code
print(tick()-TickBefore).

you can asume some things are laggier than others but this is a great way of learning to optimise code.

raycasting seems to be the only expensive thing here tho.

also you are cloning the block which means its not gonna be 1 instance of this code running, give itl multiply quickly and lag out. task.wait is redundant as heartbeat will run regardless, also the water block should not be filtered as its going to indefintly make clones in the same position as its seeing it as a open spot but theres actuallt already water there, each time spawning another block clone and hence anothee instance of this code running, meaning its gonna replicate much more than neccisary

you should check out OOP before continuing this

I think you meant to do

while true do
    task.wait(TickRate)
    fireRay(Block)
end

The way you’re currently doing it is every frame it will wait 1 second then call fire ray. Except Heartbeat is called every frame, it won’t wait for the last heartbeat function to finish. So every frame you’re calling fireRay rather than every second

1 Like

So…first tick creates +6 blocks
second tick creates +42 blocks
third tick creates +dunno, i am not great at math
on each next tick each new part produces +6 new parts
what did you expect specifically?

1 Like

Hi!
Totally agree with @af_2048. Try to revise your code logic and rewrite the whole script as the one you made looks pretty uneffecient :cry:

heart beat is 60x per second aprox, so essentially within about 1 second you have 3600 copys of this code running

1 Like

this worked pretty well, the water now flows to the bottom.

Though, once the water fills the hole completely, studio freezes again…

oh my goodness :sob:

well, atleast i know that will happen if i use heartbeat for a script like this X_X

basically include the water in the results and add a if statement for if its water, if all sides are taken delete the script so its stops running, stop using heartbeat and use
while task.wait(TickRate) do

So, there are a few potential problems here, the main one being that you are likely doing exponentially more raycasts every frame, and the other being that you’re just doing a lot of raycasts in general.

Spawning a block of water doesn’t appear to prevent more blocks of water from being spawned at the same position, meaning that water can overlap with itself when you exclude the liquid blocks. Imagine a block on the edge casting against the surface it was spawned on and repeatedly spawning a new block at its location. That means that you will create more and more water blocks forever, causing exponentially more rays and exponentially more blocks of water, all overlapping at the same position.

This is something you’ll probably want to solve. The easiest way would probably just be to not exclude the water blocks.

Also tmk you don’t get a performance benefit by excluding things in a raycast because the engine actually applies this filtering close to last, after it already does all of its queries for your ray. That’s pretty typical for a lot of things, it’s just good to know that adding filtering to your rays to limit what it has to consider typically won’t help out with performance much if at all. Afaik collision groups can help though!

Another problem is that if you’re not careful about where you place these blocks, they can spread infinitely into open space and cripple your game.

One thing that you can do to limit the amount of water that gets created is to assign a ‘volume’ or ‘distance’ attribute to your initial ‘source’ block. When you spawn a new block, you give it a new ‘volume’ that’s one less than the current block’s volume to keep track of how far the water has spread. And within your block’s script, you only start your heartbeat loop that spawns water if it has a volume/distance above 0.

This means that if you set your initial source block’s volume to 10, or 100, or 1000, you at least won’t get infinite blocks of water being created, although it’s good to keep in mind that your water is still going to spread exponentially meaning that the difference in computational complexity between 10 and 100 isn’t 10x, it’s much much bigger.

Edit: I missed the task.wait(TickRate) in the original code! My bad. This meant that the code was running every frame when it was intended to run at a much lower frequency.

(Old response)

This is (pretty much) functionally identical to running your code in a heartbeat connection. When the engine is running luau code it’s not doing anything else, it’s blocked until your luau code yields to the engine (e.g. via task.wait() which lets the engine continue running) or the thread it ran ends. This is why while true do end freezes the engine. That’s called synchronous execution, the main engine thread only runs one thing at a time (synchronously), even if that thing is your luau code.

The heartbeat event fires after heartbeat, and task.wait also resumes after heartbeat. The only time that you will see a difference is if code in your callback is yielding to the engine, in that case your while loop will run more infrequently because it’ll also be waiting for any yielding happening inside, but there isn’t any yielding happening in the fire ray code that would slow the loop down. This would also be equivalent to just using a task.wait with a large timeout or in other words, reducing the frequency that the callback code runs at, which does not make it faster, it just means that you are running it less often.

1 Like

Heartbeat tossed in the trash as it should be.
Estimate: saves 80–95% cycles.

Event driven
--ServerScript in ServerScriptService
local Block = script.Parent
local Folder = workspace:WaitForChild("Liquids")
local waterGrid = {}

-- Smaller = faster spread, more blocks per second
-- Larger = slower spread, less visually aggressive
local TickRate = 0.1

-- prioritize downward flow first, then horizontal directions
local directions = {
	Vector3.new(0, -Block.Size.Y, 0), -- down
	Vector3.new(Block.Size.X, 0, 0),  -- right
	Vector3.new(-Block.Size.X, 0, 0), -- left
	Vector3.new(0, 0, Block.Size.Z),  -- forward
	Vector3.new(0, 0, -Block.Size.Z)  -- back
}

local function getKey(pos)
	return string.format("%d_%d_%d", pos.X, pos.Y, pos.Z)
end

local function addWater(pos)
	local key = getKey(pos)
	if waterGrid[key] then return end
	waterGrid[key] = true
	local newWater = Block:Clone()
	newWater.Position = pos
	newWater.Parent = Folder
	task.spawn(function()
		for _, dir in ipairs(directions) do
			local newPos = pos + dir
			local newKey = getKey(newPos)
			if not waterGrid[newKey] then
				task.wait(TickRate)
				addWater(newPos)
			end
		end
	end)
end

addWater(Block.Position)
With Propagation filters
local waterGrid = {}

-- Smaller = faster spread, more blocks per second
-- Larger = slower spread, less visually aggressive
local TickRate = 0.1

local MaxBlocks = 200 -- cap to prevent too many blocks
local Lifetime = 5 -- seconds before a block disappears
local totalBlocks = 0

local directions = {
	Vector3.new(0, -Block.Size.Y, 0),  -- down
	Vector3.new(Block.Size.X, 0, 0),   -- right
	Vector3.new(-Block.Size.X, 0, 0),  -- left
	Vector3.new(0, 0, Block.Size.Z),   -- forward
	Vector3.new(0, 0, -Block.Size.Z)   -- back
}

local function getKey(pos)
	return string.format("%d_%d_%d", pos.X, pos.Y, pos.Z)
end

local function addWater(pos)
	if totalBlocks >= MaxBlocks then return end
	local key = getKey(pos)
	if waterGrid[key] then return end
	waterGrid[key] = true
	totalBlocks += 1

	local newWater = Block:Clone()
	newWater.Position = pos
	newWater.Parent = Folder

	task.delay(Lifetime, function()
		newWater:Destroy()
		waterGrid[key] = nil
		totalBlocks -= 1
	end)

	task.spawn(function()
		for _, dir in ipairs(directions) do
			local newPos = pos + dir
			local newKey = getKey(newPos)
			if not waterGrid[newKey] then
				task.wait(TickRate)
				addWater(newPos)
			end
		end
	end)
end

addWater(Block.Position)

Filter version should be outstanding.

They’re different if the wait duration isn’t 0 / nil. E.g. try running these scripts to see what I mean:

-- Runs every frame
game:GetService("RunService").Heartbeat:Connect(function()
	task.wait(1)
	print("Heartbeat")
end)
-- Runs once a second
while true do
	task.wait(1)
	print("While loop")
end

Not to imply adding this one change would make the code runnable

i’ll set this as the solution, since it gives me more ideas to work on, thank you all for helping out though!

@windyslow1 it is “a lot”

“alot” is not a word

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.