Often times people want to create fancy effects, or a custom camera system, or perhaps some beautiful physic calculations, but in doing so, they end up getting sloppy results, unsynchronized effects, or overall lag intensive effects.
This tutorial will go over how to properly use RunService, along with how to synchronize frame rates, and how you can prevent memory leaks. This tutorial is meant for intermediates, however it can be useful for all levels of programmers.
In this tutorial, I will be creating a basic part that will be moving away from the player, synchronized on all clients. I will not be going over the communication, but you should be able to use RemoteEvents at this point. Everything in this tutorial is done on the client!
Before going in to too much depth, there is already a really good post on Memory Leaks, that goes over specific cases and collections- I recommend you read up on that! For our case I will be discussing the usage of disconnecting unnecessary functions, etc…
Let’s first discuss what memory leaks are, and how they can effect your game. By definition, a memory leak is, “a type of resource leak that occurs when a computer program incorrectly manages memory allocations in a way that memory which is no longer needed is not released.” (Wikipedia)
In our case memory leaks are similar to what sources such as Wikipedia and other sites will tell you, however as a Roblox Developer we cannot control the allocation of a users memory, we can simply view memory usage, which I will not be going over in this tutorial- read up on StatsService. So, a memory leak is something that is no longer needed, but still takes up calculation space, or allocation of memory. How do we stop this?
This tutorials purpose is to help people learn how to properly use RunService and prevent its abuse, as such the main thing I would like to talk about is properly disconnecting your connections.
Let’s move to theory. You have a special effect that you relay to every client, the effect moves a magical ball of epicness from point A to point B. Upon hitting another part, or moving 300 studs from the original position, you destroy the “magic ball of epicness”, job well done, right? Well no. Even though you destroyed the part, as long as your connection is still running, the calculations are still being ran- even if the part doesn’t exist.
--// Creates a memory leak! RunService.Heartbeat:Connect(function() part.CFrame = part.CFrame * CFrame.new(0, 0, -4) local distance = (part.Position - origin).Magnitude print(distance) -- This will continue to print even after your parts destroyed! if distance > 50 then part:Destroy() end end)
One thing to note is that when you destroy a part, it parents it to nil, which allows for calculations to still be made on part, while it does disconnect all functions correlating to the part (which is why you should always use destroy) the part technically exists! Read more on this here.
Wonderful, you now have level entry knowledge at what causes memory leaks when it comes to making your sweet visual effect! Now you need to know how to prevent this from happening. The case of disconnecting connections comes in to play!
You can disconnect any type of connection you want, doing so will prevent that connection from being fired/triggered at any point, until you connect a new function to the given connection! This is applicable to anything that you run a connect function to.
So in our case of using RunService to make our amazing event, you would not only want to destroy the part, but you would also want to disconnect any connections you make regarding the effect of that part. Now keep in mind, upon destroying a part the connections relating to the particular part are disconnected automatically, so you really only need to disconnect our RunService connection!
How do you disconnect a connection? Well it’s easy, you simply call the Disconnect function on a RBXScriptConnection. How do you get an RBXScriptConnection? When connecting a function to an event or otherwise, it returns it!
-- No more memory leaks! -- This touched connection does not need to be disconnected because -- when destroying a part, it automatically disconnects for you! local touched = part.Touched:Connect(function() print('I was touched by a part!') end) -- However, you could in theory, do touched:Disconnect() which will prevent -- the connection from going off when touched! -- We want to make sure our connection is accessible from within our -- function, so we define it before assigning it. local connection connection = RunService.Heartbeat:Connect(function() print('Moving Part') -- Will now only print up until the parts destroyed! part.CFrame = part.CFrame * CFrame.new(0, 0, -4) if (part.Position - origin).Magnitude > 100 then part:Destroy() -- We destroy our part, which in turn disconnects our touched event! connection:Disconnect() -- We disconnect our connection, which is the RunService loop! end end)
Heartbeat and RenderStepped are both powerful RunService functions that runs every frame. This is extremely useful, but also extremely tedious when making cool visual effects that need to be synchronized across everyone’s screen!
Imagine this, a player is on a lower end computer and is running your game at 30 frames per second, while you- being the ultimate gamer you are- have a computer thats running at 60 frames per second, (in some cases if you unlock your frames even higher) this means that your HeartBeat event will be triggered twice as many times in a second then the other players!
If you move a part one stud every time Heartbeat/RenderStepped is called, on everyones screen, that part will arrive at different times and move at different speeds on everyones screen based on their framerate!
Functions / Connections
Heartbeat a powerful connection that is triggered every frame, after the physic calculations, heartbeat should be used for visual effects, such as bullets, etc…
RenderStepped similar to heartbeat, it is triggered every frame, however, opposite to heartbeat, before the physic calculations, and before the frame itself is rendered. Recommended for local effects such as camera movements.
You should try your hardest to consistently use Heartbeat for things such as visual effects that are replicated across clients! Local effects, such as camera shakes, and other things, can be handled by RenderStepped! Remember that due to the nature of these functions running every frame, you will need to be careful with your calculations within the functions.
Now it’s impossible to synchronize framerates as a whole, and why would you want to? But what you should get in to the habbit of doing is synchronizing the speed in which your effect or object moves, in ratio with how many frames per second you have.
That way if you have 30 frames per second, your part/effect may move two studs per frame, rather then if someone has 60 frames per second, there part would move one stud per frame. In both cases, the part would move 120 studs in two seconds! Magic!
This fixes the issue that we had previously, where two effects move at different speeds, such as a gun shooting faster on one persons screen in comparison to on another persons. Now how do we do this? There is a wonderful tutorial on delta time, and I highly recommend you read up on it, however its usage case isn’t exactly what we’re looking for.
What we want is to have a constant point, or the start, we’ll use a gun for analogy. Every frame we want to move the bullet forward in ratio to what your framerate is, we will then take this number and move the bullet that distance.
Let’s say a user has 60 frames, the delta time should be around 0.01, as it’s only been one single frame, or a 1/60th of a second, since the last time the connection was fired. Now if they have 30 frames then it would be? 0.03! So what can we do? We can multiply the users current dt, by the speed in which we want the part to move
local connection connection = RunService.HeartBeat:Connect(function(dt) part.CFrame *= CFrame.new(0, 0, dt* -mult) -- Multiplier is your speed! end)
Alternatively you can move the part away from the origin, by a constant amount! This is the way that I would recommend handling it.
local connection local x = 0 connection = RunService.HeartBeat:Connect(function(dt) x += mult * dt part.CFrame = origin * CFrame.new(0, 0, -x) end)