7.1 RunService

Games contain many types of operations that need to be run at regular intervals, such as the rendering of objects to the screen. Or other operations that need the processing to be spread over some finite amount of time, such as an animation.

In previous sections, whenever we needed to run these types of operations, we did so by creating a while-loop in unison with task.wait as a means to halt each iteration until the next frame.

While there is not anything inherently wrong with this approach, there are tools that may be more suitable or can simplify the task.

Because the while loop has to be halted to prevent it from running as fast a possible and seizing the thread, the task.wait function is required.

If we take a look at the documentation for the task scheduler, we can see that wait states resume a specific time during the frame and there are no provisions to change this.

Starting or stopping a loop from an external source may also require additional facilities to implement. This becomes cumbersome as more loops are added and more types of functions or variables need to be kept track of.

The Roblox RunServicecontains methods and events for time-management‘ that ‘allow your code to adhere to Roblox’s frame-by-frame loop.’

In plain English, the Roblox engine maintains a master loop that runs tasks at pre-determined intervals throughout a game. RunService gives you the methods and events as a means to attach your own routines to be run in that loop.

Depending on which event you connect to, your function is run at a different point within that loop. Or how you configure the method, if you go that route instead.


Events

If we take a look at the events section of the RunService, names like ‘PostSimulation’ and ‘PreRender’ give us an idea of when each event fires. The diagram from the task scheduler page provides insight into the order of those events within a frame.

The RunService breaks each frame down into three phases.

First is the stage right before the world is rendered onto the player’s screen. Operations connected to the ‘RenderStepped’ event are executed during this stage. Put anything that needs to be reflected on the current frame, such as camera updates.

Note that this stage is available only on clients since the server does not render to a screen.

In the second stage, the screen has begun rendering. This takes some time to do so it runs in parallel in the background.

Right before physics simulation is run, the ‘Stepped’ event fires, making it the ideal time to run anything that should be reflected in the physics simulation. This might include updating forces acting on a part or modifying a part’s velocity.

The third phase takes place after physics simulation and extends until the next frame. During this stage the ‘Heartbeat’ event fires. For operations that are not timing critical or might take a long time to run, this is a safe event to connect to since it does not risk delaying rendering or physics.

Like any other event, you can connect your function using the syntax we saw before.

Lua
local RunService = game:GetService("RunService")

local function foo(dt)
	print(dt)
end

local connection = RunService.Heartbeat:Connect(foo)

-- 0.017002906650304794
-- 0.017002906650304794
-- 0.016002735123038292
-- 0.017009001225233078
-- 0.024005375802516937

When the Heartbeat event fires, it passes in the time that elapsed since the last frame—this is very similar to running a while loop.

By default, the RunService loop runs at 60 frames per second. However, due to the widely varying capability of devices and the option for some clients to change their maximum framerate, it is not recommended to assume a frame time of 1/60 second.

Most of the examples in this series will use the Heartbeat event. However, I encourage you to also familiarize yourself with the other RunService events and tailor them to what best suits your requirements.


BindToRenderStep

During the RenderStepped stage, the order in which code is executed can affect the output during the frame and rendering to the screen. Since it is not known when the RenderStepped event fires during that phase, a more fine-grained approached may be required.

The BindToRenderStep method runs during the same stage as ‘RenderStepped’ but further splits it into smaller sub-stages which you can then configure to run a function.

This requires a RenderPriority argument specifying when the provided function should run along with the function itself.

Since this method does not return anything, a string name is also required so that the function can be unbound from the loop at a later time. This can be done using the ‘UnbindFromRenderStep’ method.

Like with the RenderStepped event, BindToRenderStep and UnbindFromRenderStep only work on the client.

Lua
local RunService = game:GetService("RunService")

local function foo(dt)
	print(dt)
end

RunService:BindToRenderStep("bar", Enum.RenderPriority.Camera.Value - 1, foo)

The higher the value, the later the function will be run in the stage. So subtracting 1 from the camera priority runs this function just before the camera.

And to remove the function, call the UnbindFromRenderStep method with the named you gave it when you bound the function.

Lua
RunService:UnbindFromRenderStep("bar")