PcoWSkbVqDnWTu_dm2ix

10 min

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 of RunService/RenderStepped|RenderStepped.
  • Minimize the amount of waiting scripts
    Avoid using while wait() do end or while true do wait() end constructs, since these aren’t guaranteed to run exactly every frame or gameplay step. Instead, use events like RunService/Stepped|Stepped or RunService/Heartbeat|Heartbeat. Similarly, avoid using spawn() or delay() as they use the same internal mechanics as wait(). Uses of spawn() are generally better served with coroutine.wrap() and coroutine.resume() of the coroutine library.
  • Manage physical states carefully
    RunService/Stepped|Stepped happens before physics while RunService/Heartbeat|Heartbeat happens after physics. Therefore, gameplay logic that affects the physics state should be done in RunService/Stepped|Stepped, such as setting the BasePart/Velocity|Velocity of parts. In contrast, gameplay logic that relies on or reacts to the physics state should be handled in RunService/Heartbeat|Heartbeat, such as reading the BasePart/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 an Animator, RunService/Stepped|Stepped is the last Lua event fired before Motor6D/Transform is applied to part positions.
Tags:
  • task
  • frame
  • scheduler
  • run