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:
Enable the CCL beta through File ⟩ Beta Features ⟩ AvatarAbilities Character Controller Library.
From the Avatar tab, open Avatar Settings.
Select the Movement tab on the left side of the window and, in the Abilities section, select Character Controller Library.
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
LabelsandTimedLabelsto 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.
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,
}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. |
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:
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.
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.
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 }
}InputNameis a logical name, not a key. The CCL generates an input sensor and always adds it toStartsWhen. Don't addRule.InputtoStartsWhenyourself.Modedefines how this input will be interpreted:Mode Behavior Use Cases PressAbility activation is attempted when the input is pressed. Automatically injected into the StartsWhencondition.Discrete actions like dash, attack, and throw. HoldAbility runs while the input is held; releasing stops it when the generated input sensor is the RunsWhilecondition.Sustained actions like sprint, aim, and block. ToggleEach press flips the ability on or off when the generated input sensor is the RunsWhilecondition.Toggled stances or motions like crouch or levitate. RepeatLike Hold, but re-triggers each cycle.Actions that self‑stop and can re‑fire while held. ActionSlotdefines an action slot which is associated with a list ofInputActionsandInputBindingswithin 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, andCustomIconInvalidspecify 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:
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 |