Task Scheduler
Task Scheduler
The task scheduler coordinates tasks done each frame as the game runs, even when the game is paused. These tasks include detecting player input, animating characters, updating the physics simulation, and resuming scripts in a wait()
state.
While there may be multiple tasks running, the task scheduler can potentially be overloaded, especially in the following situations:
- Using a custom character rig or input scheme
- Animating parts yourself (instead of using an
Animator
) - Depending heavily on precise physics
- Replicating objects regularly
Frames
A frame is a unit of game logic where work is done. Each frame should perform tasks efficiently, leading to a more frames per second and a smoother player experience.
RunService
The most direct way to add frame-by-frame game tasks is through the following members of RunService
:
RunService/BindToRenderStep|RunService:BindToRenderStep()
RunService/RenderStepped
RunService/Stepped
RunService/Heartbeat
Scheduler Priority
The task scheduler categorizes and completes tasks in the following order. Some tasks may not perform work in a frame, while others may run multiple times.
User Input
|
Input events (
UserInputService ) and bound functions (ContextActionService/BindAction|ContextActionService:BindAction() ) are handled first, such as key presses, mouse movements, touches, gamepad vibration, etc. |
Rendering
|
BindToRenderStep() — All functions bound with
RunService/BindToRenderStep|RunService:BindToRenderStep() are called in priority order. In the built-in player module (Player /PlayerScripts /PlayerModule), the camera and character control scripts bind callbacks at certain priorities determined by enum/RenderPriority . |
RenderStepped — The
RunService/RenderStepped event is fired and, as with all events, connected functions are called with the most recently connected callback run first. |
Screen Drawing — The game state is rendered in parallel. Any further changes won't render until the next frame.
|
Replication Receive Jobs
|
Incoming property changes and event firings are applied.
|
Resume Wait States
|
Scripts in a
wait() state resume if enough time has passed. There is a minimum wait resume time of 0.03 seconds, or about 1/30th of a second. Functions passed to spawn() and delay() also run in this stage. Additionally, if there are any threads that need to resume after 0.1 seconds, they will be resumed in a later frame. |
Lua Garbage Collection
|
|
Simulation Job (if the game is paused, this step is skipped)
|
Internal Legacy Step
|
Explosion|Explosions occur, leading to terrain damage, knockback, and breaking joints. |
BodyMover|BodyMovers such as BodyPosition , RocketPropulsion , and BodyGyro resolve. |
Humanoid state updates occur, such as movement or death. |
Animator|Animators update Motor6D/Transform . This does not move parts or update the applied joint offset. |
Stepped — The
RunService/Stepped event is fired. |
Internal Physics Step
|
Relative positions of parts joined by
Motor6D are updated. |
Part contacts and constraints are updated/solved. This can happen multiple times per frame, since physics will update at 240 Hz.
BasePart/Touched and related physics events fire after all world steps are complete. |
Positions of non-owned bodies are interpolated, such as
BasePart/SetNetworkOwner|BasePart:SetNetworkOwner() . |
Heartbeat — The
RunService/Heartbeat event is fired. |
|
Replication Send Jobs
|
Outgoing property updates and event firings are sent. Does not happen every frame.
|
Helpful Rules
To build performant games with efficiency in mind, note the following:
- Don't connect/bind functions to the render step unless absolutely necessary
Only tasks that must be done after input but before rendering should be done in such a way, like camera movement. For strict control over order, use
RunService/BindToRenderStep|BindToRenderStep()
instead ofRunService/RenderStepped|RenderStepped
. - Minimize the amount of waiting scripts
Avoid using
while wait() do end
orwhile true do wait() end
constructs, since these aren’t guaranteed to run exactly every frame or gameplay step. Instead, use events likeRunService/Stepped|Stepped
orRunService/Heartbeat|Heartbeat
. Similarly, avoid usingspawn()
ordelay()
as they use the same internal mechanics aswait()
. Uses ofspawn()
are generally better served withcoroutine.wrap()
andcoroutine.resume()
of the coroutine library. - Manage physical states carefully
RunService/Stepped|Stepped
happens before physics whileRunService/Heartbeat|Heartbeat
happens after physics. Therefore, gameplay logic that affects the physics state should be done inRunService/Stepped|Stepped
, such as setting theBasePart/Velocity|Velocity
of parts. In contrast, gameplay logic that relies on or reacts to the physics state should be handled inRunService/Heartbeat|Heartbeat
, such as reading theBasePart/Position|Position
of parts to detect when they enter defined zones. - Motor6D transform changes should be done on the Stepped event
If you don't,
Animator|Animators
will overwrite changes on the next frame. Even without anAnimator
,RunService/Stepped|Stepped
is the last Lua event fired beforeMotor6D/Transform
is applied to part positions.