The names RenderStepped, Stepped, and Heartbeat are technically deprecated because they should have a replacement soon. For now, I’ll keep them there as they’re the only options and there’s no actual documentation for the new ones. They should however just be one new event and the other ones being renamed.
If you find any information here that you believe is wrong, PLEASE send me a DM asking me to change it instead of replying here. I will hide that portion of the post until I can prove the information true of false. I doubt there will be anything wrong here at time of posting, as I’m only mentioning things I can confirm.
Hi!
This topic will show off things to be aware for when using RunService events, or anything that derives from them.
That means we’ll also talk about
- Wait functions, yes,
task.wait
still counts here. - A bit about tweening, I guess.
- Maybe other stuff I can’t think of right now
Using wait functions
Wait functions derive from RunService events, usually Heartbeat, in some way or another.
And remind yourself, it’s not just wait
, but also task.wait
. I’m talking about bad practices, and while wait
is worse, using task.wait
won’t fix those issues.
The problem with using wait functions is not accuracy, unlike what most people think.
Wait functions aren’t the worse thing ever, however how you use them can be.
Do I need to use a wait function?
Probably not.
I’m going to show you some examples.
"My code only works if I put a wait in there!"
This actually seems to be pretty common, it isn’t something I experienced too much or if at all, however I see a lot of people “fixing” their code using this.
As far as I know, this usually seems to be an issue with the order that events are fired, on which many people do not understand.
Events are fired from the last connected thing, to the first connected one.
For instance,
Event:Connect(function()
print("This fires last!")
end)
Event:Connect(function()
print("This fires first!")
end)
What this can cause, is that you would expect something to have already been processed once the code you asked for runs, but it didn’t, now your code doesn’t work and for some reason you learned using wait()
can help something work, and sure enough it did!
In the past, yeah sure, waiting a frame or so would be the only way to fix this, however now with the task library, this isn’t hard to do at all!
The task library introduces a method, defer
, defer schedules a thread to be ran later, still usually in the same frame, but later.
You can learn what this later means here, however I believe you shouldn’t write code to be specific to behaviour like this.
Anyhow, delaying a function without using wait
is simple as this!
task.defer(coroutine.running())
coroutine.yield()
This will schedule the current code to be ran later, usually right after all the connections were fired.
If you have some code that used wait
for this, you should try out replacing it with this instead, and seeing if it fixes the issue for you!
"I have a timer which uses wait!" or "I have a loop using wait!"
If you’re using wait for doing timers, something like this
local timePassed = 0
while true do
wait(1)
timePassed += 1
end
Then you should know that you’re doing a LOT of things wrong.
First of all, the time that wait actually takes, is not being calculated. So it will always be off. In this case, it would be a case of just += wait(1)
instead, however, this still isn’t good.
If you have a loop that never ends in your code, that’s a horrible thing, doesn’t matter if it’s in a coroutine or not, you shouldn’t have one. It messes with garbage collection and even in a routine, it can still be incredibly problematic.
The best way to do this, is by handling it using RunService events themselves, which isn’t hard to do at all!
local TimePassed = 0
RunService.Heartbeat:Connect(function(deltaTime) --> delta time is the time passed since the heartbeat call!
TimePassed += deltaTime
if TimePassed < 5 then
return
end
TimePassed = 0
-- Code that will run every 5 seconds
end)
The another good thing about this is that you can :Disconnect
the connection easily, stopping the loop with no issue.
I don’t think I need to explain how to handle things like calculating timers with the information I just passed you, basically same thing.
"I just use wait for something that takes the same time!"
For tweening for instance, you don’t need to use another wait to wait for the same amount of time.
You can use events. Wait’s biggest fear.
In the case of tweens, probably where people use wait wrongly the most, you use the Tween.Completed
event.
There are multiple places where the same applies, for example, Sound objects have .Ended
.
You should ALWAYS look to see if there’s anything like that for the thing you’re waiting for.
RBXScriptSignal
s (Events) have more than just :Connect
, they also have :Wait
.
:Wait
works by yielding the code until that event is fired. Note that it also returns the arguments passed through when that event was fired.
local Tween = TweenService:Create(Instance, TweenInfo, {
Position = UDim2.fromScale(0.5, 0.5)
})
Tween:Play()
Tween.Completed:Wait()
Not only is using events faster to react to, meaning it will happen pretty much the instant that actually ends, it also is better for performance, as there’s no polling going on. We’ll talk about what polling is later.
In the case that what you’re waiting for is something that you made, like custom tweening or something, then you should look into creating your own custom events. You can do that by using a Signal library. You should look into mine.
They’re easy to use, and usually really fast too, I highly recommend looking into them.
If it’s just one thread waiting around, then you can code the yielding and everything yourself, using coroutine.yield
and some others.
local thread = coroutine.running()
--\\ Later on:
coroutine.yield() --\\ Stops the current thread
print("Resumed!")
--\\ On some other part of the code, let's say it's custom tweening, i don't know.
-- ...
task.spawn(thread) --\\ now the code runs again!
--\\ "Resumed" is printed!
Using these is better if you’re messing with only ever one thread that you need to resume, otherwise, just use a signal and you’re good to go.
"I don't fit into any of these... Am I fine using wait?"
Probably. Like I said, a wait function isn’t a monster, as long as you’re using it properly, you’re fine.
The thing is that most of the time you don’t need to use it.
Anyhow, wait
has some inconsistent behaviour, use task.wait
instead.
If you’re unsure, hit me up with a DM, I will happily answer you. Your code can turn into an extra part of this post, if it’s a good example.
Why is wait bad?
Well, because first of all, usually when you’re using wait, you’re usually already doing something wrong.
That’s basically it.
When you need to use a wait function, it usually means you could do better.
Usually one wait call for a use is enough, but if you’re waiting for the same thing in other places in your script, then I highly suggest you to look into ScriptSignals, yet again, and fire one when that wait is done, and then listening to it and using :Wait
on other parts of your code.
Using RunService events
We are out of wait town, going into non-yielding town!
This is more so talking about things you can do wrong while using RunService.
Polling
Polling is the computer-equivalent of asking “Are we there yet?” every 2 seconds. In this case, it’s way more frequent than 2 seconds.
Polling in excess can cause your game to slow down. So stay away from it if possible.
Most polling looks something like this:
while not (condition) do
RunService.Heartbeat:Wait()
end
This is horrible! Not only you’re running a thread multiple times a second for no reason, just like what I talked about before, usually you’re doing something like indexing a table, checking if there’s something inside one, or literally anything that demands some searching, etc.
Like I said before, use custom events for these things! Use a Signal class, you’re already doing it right! Your issue is resolved.
Custom events are extremely useful for things like :WaitFor
functions in other libraries.
Memory Allocation every frame
This is a big rule in game development. At least AFAIK.
You should not allocate memory with anything every frame.
The thing here is if you’re allocating every frame, or frequently in that basis at all.
If you’re allocating here and there inside a handler, that’s probably not an issue.
But if you’re doing it frequently at all, then yes, it is an issue.
Setting anything to a local doesn’t count as memory allocation. Locals allocate memory for themselves automatically, that doesn’t count as memory allocation.
Things that would count as memory allocation are things like, creating a new table, setting things to a table, creating a new object, instance, things like those.
A big causer that people seem to not notice is using :GetPlayers
, :GetChildren
, or similar functions to that, in such applications.
:GetChildren
for example like it’s many counterparts, can allocate memory when called, as they are creating a new table each time. Doing that each frame is horrible.
For these things, as long as you’re not mutating it, you can cache these values, add some .ChildAdded
/ .ChildRemoved
listeners, and you have just one table for those things that does not contribute to this bad practice.
local RunService = game:GetService("RunService")
local Players = game:GetService("Players")
local PlayersList = Players:GetPlayers()
Players.PlayerAdded:Connect(function(player)
table.insert(PlayerList, player)
end)
Players.PlayerRemoving:Connect(function(player)
local index = table.find(PlayerList, player)
if index then
local tableLength = #PlayerList
PlayerList[index] = PlayerList[tableLength]
PlayerList[tableLength] = nil
--\\ Quick remove;
end
end)
RunService.Heartbeat:Connect(function(deltaTime)
for _, player in ipairs(PlayerList) do
--\\ Do something with every player! :) No extra memory allocation here!
end
end)
If you have some code lying around using something like this, you should deeply consider adding this solution to it!
Always running things every frame
This isn’t per say a bad thing, it only is if you don’t need to.
You can bundle updates to certain things every 30 frames, or 30 seconds, or 5 seconds, 0.5 seconds, etc.
It isn’t hard to do, and I highly recommend you do this for things you don’t need to do every frame.
If it doesn’t break from bundling / if it doesn’t depend on updating every single frame, then you should look into bundling that update.
You can just do what I talked about above on the “I have an loop using wait!” section.
Something like this should work for you.
local RunService = game:GetService("RunService")
local InvertalTime = 30
local TimePassedSinceLastUpdate = 0
RunService.Heartbeat:Connect(function(frameDeltaTime)
TimePassedSinceLastUpdate += frameDeltaTime
if TimePassedSinceLastUpdate >= InvertalTime then
return
end
local deltaTime = TimePassedSinceLastUpdate
TimePassedSinceLastUpdate = 0
-- Do something with the DeltaTime.
end)
If you need to, for something like cameras, yes, just update it right away, but for things like these, you’re losing a good chunk of performance by not doing so.
With all that said, I’m tired, I’ve been writing for 4 hours straight, writing code in a normal page, keeping track of 4 spaces, thinking of new things to discuss about, tomorrow I’ll be adding anything I remember, right now I’m exausted. Thanks for the read.
Any questions reply to me here and I’ll be happy to answer you. Just remind yourself for any discussion about information you believe should be changed, DM me instead, this includes correcting typos as well.