13 min read

Source: Roblox Creator Hub · CC BY 4.0 · View source · Code samples: MIT Imported 2026-10-03. Formatting adapted for this site.

Character Controller Library

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.

Note

 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](#labels) bit, so [conditions](#conditions) and [conflicts](#conflicts) can reference the ability. Multiple ability definitions can use the same name. The [`ModuleScript`](/docs/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](#labels).
`StartsWhen`, `RunsWhile` Conditions which define when to start the ability and when to keep it running, respectively; see [conditions](#conditions).
`Blocks`, `Stops`, `Suspends`, `ExclusiveGroup` How to handle [conflicts](#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](#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](#callbacks).

Labels

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

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 = {
		OnStart = { Dashing = 1 },
		OnStop = { 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")`

Note

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 }
}

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`](/docs/enum-keycode#space) [`ButtonA`](/docs/enum-keycode#buttona) ① Jump
`2` [`LeftShift`](/docs/enum-keycode#leftshift) [`ButtonL1`](/docs/enum-keycode#buttonl1) ② Sprint
`3` [`LeftControl`](/docs/enum-keycode#leftcontrol) [`ButtonB`](/docs/enum-keycode#buttonb) ③ Crouch
`4` [`R`](/docs/enum-keycode#r) [`ButtonX`](/docs/enum-keycode#buttonx) ④
`5` [`MouseLeftButton`](/docs/enum-keycode#mouseleftbutton) [`ButtonR2`](/docs/enum-keycode#buttonr2) ⑤
`6` [`Q`](/docs/enum-keycode#q) [`ButtonY`](/docs/enum-keycode#buttony) ⑥
`7` [`X`](/docs/enum-keycode#x) [`ButtonR1`](/docs/enum-keycode#buttonr1) ⑦
`8` [`C`](/docs/enum-keycode#c) [`ButtonL2`](/docs/enum-keycode#buttonl2)
`9` [`F`](/docs/enum-keycode#f) [`DPadLeft`](/docs/enum-keycode#dpadleft)
`10` [`G`](/docs/enum-keycode#g) [`DPadRight`](/docs/enum-keycode#dpadright)
`11` [`V`](/docs/enum-keycode#v) [`DPadDown`](/docs/enum-keycode#dpaddown)

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`](/docs/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.

Note

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.