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 FileBeta FeaturesAvatarAbilities 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.

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 Enum.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 FieldsPurpose
NameName 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, TimedLabelsNamed bits in a shared 64-bit mask which acts as the coordination bus between abilities; see labels.
StartsWhen, RunsWhileConditions which define when to start the ability and when to keep it running, respectively; see conditions.
Blocks, Stops, Suspends, ExclusiveGroupHow to handle conflicts between abilities that can't be active at once.
InputThe 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, StateDefault 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, OnTeardownLifecycle 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.

Running Ability
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,
}
FallingDown Ability
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.

KeyDescription
TimedLabels.OnStartDictionary 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.OnStopDictionary 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.ConsumesList 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.
Timed Labels
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.

GoalSyntaxExample
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")

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 KeyPurpose
BlocksWhile 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.
StopsWhen 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.
SuspendsWhen 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 }
}
  • 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:

    ModeBehaviorUse Cases
    PressAbility activation is attempted when the input is pressed. Automatically injected into the StartsWhen condition.Discrete actions like dash, attack, and throw.
    HoldAbility 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.
    ToggleEach press flips the ability on or off when the generated input sensor is the RunsWhile condition.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.
  • 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).

    SlotKeyboard & MouseGamepadTouchDefault Assignment
    1SpaceButtonAJump
    2LeftShiftButtonL1Sprint
    3LeftControlButtonBCrouch
    4RButtonX
    5MouseLeftButtonButtonR2
    6QButtonY
    7XButtonR1
    8CButtonL2
    9FDPadLeft
    10GDPadRight
    11VDPadDown
  • 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:

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:

SensorDescription
Sensor.GroundStanding on a surface
Sensor.IsMovingMovement input is being applied
Sensor.MoveInputThe movement vector itself
Sensor.CeilingSomething is directly overhead
Sensor.ClimbA climbable surface is in range
Sensor.Water / Sensor.WaterSurfaceIn water / at the surface
Sensor.SitSeated
Sensor.TippedFallen over
Sensor.ToolHolding a Tool
Sensor.LookDirectionInputThe commanded look direction
Sensor.RotateToLookDirectionInputWhether 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.

CallbackRunsUse 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:

  • managerCtx.AbilityOwner — The character Model such that managerCtx.AbilityOwner.PrimaryPart is the root part.
  • managerCtx.AbilityManager — A cut-down view of the manager so that an ability can add, remove and query abilities from inside its own callbacks.
  • managerCtx.BodyParts — The character's body parts, with helpers for turning collision on and off per limb.
  • managerCtx.ControllerManager — The character's ControllerManager for physics control. It can be nil when no registered ability requires physics.
  • managerCtx.RootCFrame — The root part's CFrame, snapshotted once at the start of the frame so callbacks don't each go and fetch it themselves.
  • managerCtx.RootLookVector — Direction the character's root part is facing.
  • managerCtx.RootUpVectorY — The Y component of the root part's up vector.
  • managerCtx.TaskSynchronize() — Synchronizes a callback before DataModel access when parallel callback support is enabled. Currently, OnUpdate doesn't run in a parallel context, so this function has no effect. Full Parallel Luau support is planned for a future update.

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

  • abilityCtx.Config — Read-only configuration values for this ability registration.
  • abilityCtx.State — Mutable state that replicates through the DataModel. Server Authority restores these values during rollback and resimulation.
  • abilityCtx.Local — Mutable scratch state that doesn't replicate or participate in rollback.

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

©2026 Roblox Corporation. Roblox, the Roblox logo and Powering Imagination are among our registered and unregistered trademarks in the U.S. and other countries.