Roblox UtilitiesDevlHub Roblox Documentation

Character Controller Library

The Character Controller Library (CCL) is a modular framework for building character movement and behaviors through attributes and Luau scripts.

The Character Controller Library (CCL) is a modular framework for building character movement and behaviors through attributes and Luau scripts. This architecture replaces rigid Humanoid state machines with a flexible, extensible system for character mechanics.

Enable CCL#

The CCL is opt-in through Studio's Avatar Settings window. To enable it:

  1. Enable the CCL beta through File ⟩ Beta Features ⟩ AvatarAbilities Character Controller Library.

  2. From the Avatar tab, open Avatar Settings.

    Avatar Settings indicated in Studio's toolbar
  3. Select the Movement tab on the left side of the window and, in the Abilities section, select Character Controller Library.

    Character Controller Library toggle in the Avatar Settings window
  4. All of the standard abilities like Running, Jumping, and Climbing are enabled by default. To disable any of them at runtime, uncheck the associated box.

It's not recommended to disable Running, as doing so will prevent characters from moving along the ground. Additionally, you should always keep Getting Up enabled if Falling Down is enabled, as a mismatch will allow characters to fall down (trip) but never get back up.

Abilities#

Abilities evaluate what a character can do, such as the ability to run, jump, climb, and swim. Instead of relying on a fixed set of engine‑defined character states like those in HumanoidStateType, CCL abilities dynamically determine what a character can do and how it should respond to player input.

Structurally, an ability is a self-contained Luau table that primarily specifies the following:

Table Fields Purpose
Name Name that is allocated a label bit, so conditions and conflicts can reference the ability. Multiple ability definitions can use the same name. The ModuleScript name determines the unique configuration key.
Labels, TimedLabels Named bits in a shared 64-bit mask which acts as the coordination bus between abilities; see labels.
StartsWhen, RunsWhile Conditions which define when to start the ability and when to keep it running, respectively; see conditions.
Blocks, Stops, Suspends, ExclusiveGroup How to handle conflicts between abilities that can't be active at once.
Input The input which triggers the ability. The CCL injects it into StartsWhen and, when you omit RunsWhile, uses it as the default continuation condition; see inputs.
Config, State Default configuration values and replicated state for each ability registration. Callbacks read configuration from abilityCtx.Config and read or write replicated state through abilityCtx.State.
OnSetup, OnStart, OnStop, OnUpdate, OnTeardown Lifecycle callbacks where the ability's actual behavior is scripted; see callbacks.

Labels#

A label is a named bit in a shared 64-bit mask which acts as the coordination bus between abilities. Essentially:

  • An active ability broadcasts its Labels and TimedLabels to the world mask.
  • Ability conditions (StartsWhen, RunsWhile) test the world mask and react.
  • Ability conflicts define which other abilities are blocked, stopped, or suspended upon activation.

In the following setup, the "CanFallDown" label is broadcast to the world mask when Running is active. The FallingDown ability with its condition of StartsWhen = All( "CanFallDown", "Stunned" ) automatically becomes a candidate, but "Stunned" must also be broadcast to the world mask before FallingDown occurs.

Luau
local AvatarAbilities = require("@rbx/AvatarAbilities")

local Identifiers = AvatarAbilities.Identifiers
local Ability = Identifiers.Ability
local Rule = AvatarAbilities.Rule
local Sensor = Identifiers.Sensor
local All, Any, Not = Rule.All, Rule.Any, Rule.Not

local Running: AvatarAbilities.AbilityDefinition = {
	Name = Ability.Running,
	Labels = { "CanFallDown" }, -- Labels broadcast when ability is active
	StartsWhen = Sensor.Ground,
	RunsWhile = Sensor.Ground,
}
Luau
local AvatarAbilities = require("@rbx/AvatarAbilities")

local Identifiers = AvatarAbilities.Identifiers
local Ability = Identifiers.Ability
local Rule = AvatarAbilities.Rule
local Sensor = Identifiers.Sensor
local All, Any, Not = Rule.All, Rule.Any, Rule.Not

local FallingDown: AvatarAbilities.AbilityDefinition = {
	Name = Ability.FallingDown,
	StartsWhen = All( "CanFallDown", "Stunned" ), -- Labels necessary for ability to start
	Blocks = { Ability.Running }
}

Labels can also be broadcast or consumed in a timed manner using the TimedLabels dictionary.

Key Description
TimedLabels.OnStart Dictionary containing labels (keys) and associated durations. Label(s) are broadcast when the ability starts and automatically expire when their duration ends. For example, OnStart = { Dashing = 1 } broadcasts the Dashing label for 1 second when the ability starts.
TimedLabels.OnStop Dictionary containing labels (keys) and associated durations. Label(s) are broadcast when the ability stops and automatically expire when their duration ends. For example, OnStop = { DashCooldown = 2 } broadcasts the DashCooldown label for 2 seconds when the ability stops.
TimedLabels.Consumes List of labels to remove (consume) when the ability activates. For example, if a fighting game allows players to counter‑attack after blocking an opponent's attack, the CounterAttack ability may contain both StartsWhen = "AfterBlock" and TimedLabels = { Consumes = { "AfterBlock" } } to prevent double‑triggering of the CounterAttack ability.
Luau
local AvatarAbilities = require("@rbx/AvatarAbilities")

local Identifiers = AvatarAbilities.Identifiers
local Rule = AvatarAbilities.Rule
local Sensor = Identifiers.Sensor
local All, Any, Not = Rule.All, Rule.Any, Rule.Not

local Dash: AvatarAbilities.AbilityDefinition = {
	Name = "Dash",
	StartsWhen = All( Sensor.Ground, Not("DashCooldown") ),
	RunsWhile = "Dashing",
	TimedLabels = {
	 Dashing = 1 },
	 DashCooldown = 2 },
	},
}

Conditions#

A condition is one or more labels, sensors, or an input reference, used by StartsWhen and RunsWhile. Conditions compile to bitmask operations at runtime and evaluation is integer math — no table walks and no string comparisons.

Goal Syntax Example
One required condition. StartsWhen = Sensor.Ground
AND logic for when all of the labels exist in the world mask and all of the sensors are active. All() StartsWhen = All( "CanFallDown", "Stunned" )
OR logic for when any of the labels exist in the world mask or any of the sensors are active. Any() StartsWhen = Any( "WallClimbing", "Climbing" )
Negation such that the labels can not exist in the world mask and the sensors can not be active. Not() RunsWhile = Not("Stunned")

Not() takes exactly one label/sensor, not a group. To negate multiple, use All( Not(), Not() ).

Conditional evaluation can be combined for more complex logic, such as All() chaining plus Not() to indicate that a sensor must be active while a label must be nonexistent:

Luau
local AvatarAbilities = require("@rbx/AvatarAbilities")

local Identifiers = AvatarAbilities.Identifiers
local Rule = AvatarAbilities.Rule
local Sensor = Identifiers.Sensor
local All, Any, Not = Rule.All, Rule.Any, Rule.Not

local Dive: AvatarAbilities.AbilityDefinition = {
	Name = "Dive",
	StartsWhen = All( Sensor.WaterSurface, Not("Recovering") ),
}

Conflicts#

Some abilities cannot be active when another ability is; for example, characters can't jump while swimming, and they can't run while falling. The engine resolves these conflicts declaratively inside an ability's definition:

Conflict Key Purpose
Blocks While the owning ability is active, the listed other abilities cannot start. For example, a ScopeAim ability might contain Blocks = { Ability.Running, Ability.Jumping } to prevent characters from running or jumping while carefully aiming through their weapon's scope.
Stops When the owning ability starts, the listed other abilities force‑stop and must re‑trigger. For instance, a Hover ability might contain Stops = { Ability.Running } to immediately stop a character's running motion when they start hovering.
Suspends When the owning ability starts, the listed other abilities pause and then auto-resume when the owning ability stops. For example, a custom sprint ability might contain Suspends = { Ability.Running } so that running 🄐 pauses on sprint start, 🄑 is blocked mid‑sprint, and 🄒 resumes on sprint stop.

Another unique conflict key is ExclusiveGroup which places multiple abilities into a group, each with a Priority value. Only one ability per group can be active and higher priority wins. However, if a challenger declares Stops targeting the holder's name/label, it wins regardless of priority.

In the following setup, three abilities (Sprinting, Crouching, Stagger) are added to a Locomotion exclusive group. Sprinting has the highest priority (200) so it wins over Crouching (100) and the two never run at the same time. However, Stagger forcibly stops sprinting (Stops = { Ability.Sprinting }), so it can interrupt and supersede Sprinting even though its priority (150) is lower.

Luau
local AvatarAbilities = require("@rbx/AvatarAbilities")

local Identifiers = AvatarAbilities.Identifiers
local Ability = Identifiers.Ability
local Rule = AvatarAbilities.Rule
local Sensor = Identifiers.Sensor
local All, Any, Not = Rule.All, Rule.Any, Rule.Not

local Sprinting: AvatarAbilities.AbilityDefinition = {
	Name = Ability.Sprinting,
	ExclusiveGroup = { Name = "Locomotion", Priority = 200 },
}
local Crouching: AvatarAbilities.AbilityDefinition = {
	Name = Ability.Crouching,
	ExclusiveGroup = { Name = "Locomotion", Priority = 100 },
}
-- A lower-priority ability can override a higher-priority ability by stopping it
local Stagger: AvatarAbilities.AbilityDefinition = {
	Name = "Stagger",
	ExclusiveGroup = { Name = "Locomotion", Priority = 150 },
	Stops = { Ability.Sprinting },
}

Inputs#

An ability's Input definition specifies the input used to attempt to activate the ability. It takes key-value pairs that configure the input behavior, action slot, and optional touch button icons.

Luau
local AvatarAbilities = require("@rbx/AvatarAbilities")

local Identifiers = AvatarAbilities.Identifiers
local Rule = AvatarAbilities.Rule
local Sensor = Identifiers.Sensor
local All, Any, Not = Rule.All, Rule.Any, Rule.Not

local Dash: AvatarAbilities.AbilityDefinition = {
	Name = "Dash",
	Input = { InputName = "Dash", Mode = "Press", ActionSlot = 5 }
}
  • InputName is a logical name, not a key. The CCL generates an input sensor and always adds it to StartsWhen. Don't add Rule.Input to StartsWhen yourself.

  • Mode defines how this input will be interpreted:

    Mode Behavior Use Cases
    Press Ability activation is attempted when the input is pressed. Automatically injected into the StartsWhen condition. Discrete actions like dash, attack, and throw.
    Hold Ability runs while the input is held; releasing stops it when the generated input sensor is the RunsWhile condition. Sustained actions like sprint, aim, and block.
    Toggle Each press flips the ability on or off when the generated input sensor is the RunsWhile condition. Toggled stances or motions like crouch or levitate.
    Repeat Like Hold, but re-triggers each cycle. Actions that self‑stop and can re‑fire while held.
  • ActionSlot defines an action slot which is associated with a list of InputActions and InputBindings within the Input Action System.

Several action slot input bindings are predefined by Roblox and, in the future, the Input Action Manager will allow you to reconfigure default input bindings for action slots as desired. Setting ActionSlot to 0 will choose the next available empty slot. On mobile devices, slots 1-7 populate to buttons on the screen (see diagram below).

Slot Keyboard & Mouse Gamepad Touch Default Assignment
1 Space ButtonA ① Jump
2 LeftShift ButtonL1 ② Sprint
3 LeftControl ButtonB ③ Crouch
4 R ButtonX ④
5 MouseLeftButton ButtonR2 ⑤
6 Q ButtonY ⑥
7 X ButtonR1 ⑦
8 C ButtonL2
9 F DPadLeft
10 G DPadRight
11 V DPadDown
  • CustomIcon, CustomIconActive, and CustomIconInvalid specify Roblox asset IDs for the touch button when the ability is idle, active, or unavailable, respectively.

When you omit RunsWhile, the CCL uses the generated input sensor as the continuation condition. When you define RunsWhile, it replaces that default. For a Hold or Toggle ability that must stop when its input becomes inactive, include the Rule.Input sentinel directly in the custom condition. Rule.Input is a value, not a function:

Luau
local AvatarAbilities = require("@rbx/AvatarAbilities")

local Rule = AvatarAbilities.Rule
local Sensor = AvatarAbilities.Identifiers.Sensor
local All, Any, Not = Rule.All, Rule.Any, Rule.Not
local Input = Rule.Input

local Glide: AvatarAbilities.AbilityDefinition = {
	Name = "Glide",
	Input = { InputName = "Glide", Mode = "Hold", ActionSlot = 6 },
	StartsWhen = Not(Sensor.Ground),
	RunsWhile = All(Not(Sensor.Ground), Input),
}

Sensors#

A sensor is a named value about the world that the engine reads for you. You'll typically read sensors rather than write them. For convenience, several sensors are pre-registered:

Sensor Description
Sensor.Ground Standing on a surface
Sensor.IsMoving Movement input is being applied
Sensor.MoveInput The movement vector itself
Sensor.Ceiling Something is directly overhead
Sensor.Climb A climbable surface is in range
Sensor.Water / Sensor.WaterSurface In water / at the surface
Sensor.Sit Seated
Sensor.Tipped Fallen over
Sensor.Tool Holding a Tool
Sensor.LookDirectionInput The commanded look direction
Sensor.RotateToLookDirectionInput Whether the character should rotate to the commanded look direction

Callbacks#

Ability callback functions let you script specific behavior:

Although you register custom abilities on the server, their callbacks run in both the predicted client simulation and the authoritative server simulation. Keep callback behavior deterministic so both simulations produce the same result.

Callback Runs Use Cases
OnSetup(managerCtx, abilityCtx) Once, when the ability is registered. Cache references, initialize state, etc.
OnStart(managerCtx, abilityCtx, hadLabel) Each time the ability is activated. Apply an effect such as an impulse. The hadLabel() function reports whether a specified label was present when activation began, before conflict resolution.
OnUpdate(managerCtx, abilityCtx) Each active frame. Continuous work such as timers or per‑frame forces.
OnStop(managerCtx, abilityCtx) Each deactivation, voluntary or forced. Undo what OnStart() did.
OnTeardown(managerCtx, abilityCtx) On ability removal. Disconnect connections, destroy instances, etc.

Each callback function's first parameter, managerCtx, is a ManagerContext object with shared character and manager properties, including:

The second parameter, abilityCtx, is an AbilityContext object with engine-managed tables for the current ability registration:

Store custom callback data in abilityCtx.State or abilityCtx.Local. Writing custom fields directly to abilityCtx is an error.

Callbacks must never yield as they run inside the engine's synchronous step and yielding breaks the frame. Do not include any yielding functions such as task.wait() or WaitForChild(). Simulation callbacks also restrict some DataModel operations, including Destroy(). You can set attributes and update physics in a callback. For instance lifecycle or other restricted work, set an attribute in the callback and respond to it from a regular script.