Drive
Drivers produce a value over time. Build them with MotionFX.Drive, then combine them to shape anything from a single kick to a live binding to game state.
You're at the deepest level. Every primitive and every driver is open here, and nothing is hidden underneath: the Effect builder and the ready-made tracks are made from these same pieces. You can still drop ready-made tracks into any effect where they fit.
local Drive = MotionFX.Drive
A driver is a table { Start: (PlayContext) -> Sampler<T>, Duration: number? }. The sampler is called as sampler(t, dt). t is the number of seconds since the track began and is never negative. dt is the frame step multiplied by the effect's Speed. A Duration of nil means the driver is open-ended.
| Driver | Value | Duration |
|---|---|---|
Constant | any T | open-ended |
Tween | Lerpable T | duration |
Envelope | number | Attack + Hold + Release |
Noise | number | Vector3 | open-ended |
Spring | scalable T | until settled |
Bind | any T | open-ended |
Cycle | Lerpable T | open-ended |
Modulate | carrier's T | shorter finite one |
Sum | T | longest, if all are finite |
Sequence | T | total, if the last one is finite |
Map | U | same as the source |
A driver fits a primitive when it produces that primitive's value type. See the table in Which drivers fit, and the recipes under each primitive.
Basic drivers
Drive.Constant
functionDrive.Constant<T>(value: T): Driver<T>
Always returns value. It is open-ended, so the effect runs until stopped unless it has a Duration. Pair it with Modulate to shape a fixed value with an envelope.
{ Primitive = "Vignette", Driver = Drive.Constant(0.65) }
Drive.Tween
functionDrive.Tween<T>(from: T, to: T, duration: number, easing: Easing?): Driver<T>
Animates from from to to over duration seconds, which must be greater than 0, then ends. Works with number, Vector3, Color3, ColorValue and VignetteLayer. The default easing is Quad / Out.
To hold the end value, follow the tween with a Constant in a Sequence, or give the effect a longer Duration.
Drive.Tween(0, 1, 0.5, { Style = Enum.EasingStyle.Back, Direction = Enum.EasingDirection.Out })
Drive.Envelope
functionDrive.Envelope(config: EnvelopeConfig, peak: number?): Driver<number>
Rises from 0 to peak (default 1), optionally holds, then falls back to 0. Use it on its own for scalar primitives (Blur, FOV, Vignette), or as the second argument to Modulate to give any carrier an attack and a decay.
| Field | Type | Description |
|---|---|---|
Attack | number? | Seconds to reach the peak. Default 0. |
Hold | number? | Seconds at the peak. Default 0. |
Release | number? | Seconds to return to 0. Default 0. |
Easing | Easing? | Applied to both the rise and the fall. |
-- Blur flashes to 14 and decays over 0.9 s
{ Primitive = "Blur", Driver = Drive.Envelope({ Attack = 0, Release = 0.9 }, 14) }
Drive.Noise
functionDrive.Noise<T>(config: NoiseConfig<T>): Driver<T>
Smooth Perlin noise in [-Amplitude, Amplitude], with each axis independent for Vector3. It is open-ended. Shape it with Modulate(noise, envelope) to get a shake that has an attack and a decay.
| Field | Type | Description |
|---|---|---|
Frequency | number | Oscillations per second, roughly. |
Amplitude | number | Vector3 | Peak value, per axis for a Vector3. |
Octaves | number? | Layers of finer detail. Each one has half the amplitude and double the frequency. Default 1. |
Seed | number? | Defaults to the play's seed (PlayOptions.Seed, or a random one). |
Drive.Noise({ Frequency = 18, Amplitude = Vector3.new(1.2, 1.2, 2.5) })
Drive.Spring
functionDrive.Spring<T>(config: SpringConfig<T>): Driver<T>
A damped spring released from Kick, an initial displacement, that settles back to 0. The spring is solved in closed form, so it gives the same result at any frame rate. It ends once it has settled, which takes about six decay time constants.
| Field | Type | Description |
|---|---|---|
Kick | T | Starting displacement, for example 7 FOV degrees or Vector3.new(3.5, 0, 0) degrees of pitch. |
Frequency | number | Natural frequency in Hz. Must be greater than 0. |
Damping | number | Below 1 overshoots and wobbles. 1 is critically damped. |
-- Recoil: pitch up and settle with a little overshoot
Drive.Spring({ Kick = Vector3.new(3.5, 0, 0), Frequency = 9, Damping = 0.4 })
Drive.Bind
functionDrive.Bind<T>(read: (dt: number) -> T): Driver<T>
Calls read(dt) every frame, which permanently ties the primitive to live data such as health, speed, or an event's last value. It is open-ended. The value is still scaled by the effect's intensity and fades.
Drive.Bind(function()
local speed = root.AssemblyLinearVelocity.Magnitude
return math.clamp((speed - 30) / 4, 0, 12) -- FOV widens past 30 studs/s
end)
Drive.Cycle
functionDrive.Cycle<T>(values: { T }, period: number, easing: Easing?): Driver<T>
Loops through values forever, easing from each one to the next over period seconds and wrapping from the last back to the first. It needs at least two values. Two values make an oscillation, and more make a palette walk. The default easing is Sine / InOut.
Drive.Cycle({ Color3.fromRGB(205, 140, 255), Color3.fromRGB(255, 180, 110), Color3.fromRGB(130, 170, 255) }, 1.6)
Composition
Drive.Modulate
functionDrive.Modulate<T>(carrier: Driver<T>, envelope: Driver<number>): Driver<T>
Multiplies the carrier by the envelope every frame. It ends when the shorter finite driver ends. This is the standard way to build a shake: Noise × Envelope.
local envelope = Drive.Envelope({ Attack = 0.02, Release = 0.5 })
Drive.Modulate(Drive.Noise({ Frequency = 18, Amplitude = Vector3.new(1.2, 1.2, 2.5) }), envelope)
Drive.Sum
functionDrive.Sum<T>(...: Driver<T>): Driver<T>
Adds the drivers' values together. It is finite only if every driver is finite, in which case it lasts as long as the longest one.
-- A base blur of 8 that wobbles by ±4
Drive.Sum(Drive.Constant(8), Drive.Noise({ Frequency = 0.6, Amplitude = 4 }))
Drive.Sequence
functionDrive.Sequence<T>(...: Driver<T>): Driver<T>
Plays the drivers one after another. Only the last one may be open-ended.
Drive.Sequence(
Drive.Spring({ Kick = -2.5, Frequency = 5, Damping = 0.5 }),
Drive.Spring({ Kick = -1.5, Frequency = 5, Damping = 0.5 })
)
Drive.Map
functionDrive.Map<T, U>(source: Driver<T>, transform: (T) -> U): Driver<U>
Reshapes another driver's values. It has the same duration as the source. Typical uses are turning a Cycle of Color3 into ColorValue tints, or an envelope into a coloured VignetteLayer.
Drive.Map(Drive.Envelope({ Attack = 0.05, Hold = 0.9, Release = 1.4 }), function(amount: number): Types.VignetteLayer
return { Intensity = amount * 0.7, Color = Color3.fromRGB(120, 0, 0), Inset = 0.3, Size = 0.9 }
end)
Value math
Every driver and channel works through three operations, Scale, Add and Lerp, which support these value types:
| Type | Scale toward neutral | Add | Lerp |
|---|---|---|---|
number, Vector3 | multiply | add | linear |
Color3 | — | — | Color3:Lerp |
ColorValue | tint toward white, the rest toward 0 | tints multiply, the rest add | per field |
VignetteLayer | Intensity only | Intensities add, the rest comes from the first layer | per field, and the Image switches at 0.5 |
You can Tween or Cycle a Color3, but primitives don't accept one directly. Map it into a ColorValue or a VignetteLayer first.
Custom drivers
Any table with the right shape works as a driver. Start runs once per play, so keep per-play state in locals inside it:
local Types = require(ReplicatedStorage.MotionFX.Types)
local function Pulse(rate: number, amount: number): Types.Driver<number>
return {
Duration = nil, -- open-ended
Start = function(context)
local phase = (context.Seed % 100) / 100 * math.pi * 2
return function(t, _dt)
return amount * (0.5 + 0.5 * math.sin(t * rate * math.pi * 2 + phase))
end
end,
}
end