Your first effect
Build a "Hit" effect one track at a time: a camera kick, a shake, a red flash and a sound. Each step adds one track, so you can play it after every step and see what changed.
You're at the deepest level. This page builds tracks by hand from primitives and drivers. The same effect fits in four lines with ready-made tracks, and the Effect builder shows how each one maps to what you'll write here. Going this deep is what lets you customise everything.
Setup
In a LocalScript, require MotionFX and grab the driver library. Add --!strict at the top so the editor checks your tracks and autocompletes names as you type.
--!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local MotionFX = require(ReplicatedStorage.MotionFX)
local Types = require(ReplicatedStorage.MotionFX.Types)
local FX, Drive = MotionFX.Enum, MotionFX.Drive
1. A kick
An effect is a list of tracks, and a track is one primitive (what changes) driven by one driver (how it changes over time). Start with a single Rotation track: a spring that pitches the camera up 4 degrees and lets it settle back.
MotionFX:Create("Hit", {
Tracks = {
{ Primitive = "Rotation", Driver = Drive.Spring({ Kick = Vector3.new(4, 0, 0), Frequency = 7, Damping = 0.45 }) },
},
})
MotionFX:Play("Hit")
You don't need to stop it. A spring ends once it has settled, and the effect ends with its longest track.
2. A shake
Add a second Rotation track. Tracks on the same primitive add up, so the shake plays on top of the kick. Noise on its own would shake forever, so Modulate multiplies it by an envelope that starts at full strength and fades out over 0.6 s.
local hitEnvelope = Drive.Envelope({ Attack = 0.03, Release = 0.6 })
-- add to Tracks
{
Primitive = "Rotation",
Driver = Drive.Modulate(Drive.Noise({ Frequency = 16, Amplitude = Vector3.new(1, 1, 2) }), hitEnvelope),
},
An envelope produces a number, and Rotation needs a Vector3. Wrapping drivers this way is what the Which drivers fit table is for.
3. A red flash
A Vignette track takes a number for a black edge, or a VignetteLayer for colour. Map reshapes the same envelope into a red layer, so the flash fades exactly as the shake does. The : Types.VignetteLayer annotation tells strict mode what the function returns.
-- add to Tracks
{
Primitive = "Vignette",
Driver = Drive.Map(hitEnvelope, function(amount: number): Types.VignetteLayer
return { Intensity = amount * 0.6, Color = Color3.fromRGB(150, 0, 0) }
end),
},
4. A sound
Sound tracks take no driver. They play their SoundId once and follow the effect's intensity. MotionFX.Sounds has the preset sounds, and you can pass any asset id.
-- add to Tracks
{ Primitive = "Sound", SoundId = MotionFX.Sounds.Shake, Volume = 0.8 },
5. Play and control it
Play returns a handle for that one playthrough. Scale it with Intensity, react when it finishes, or stop it early.
local handle = MotionFX:Play("Hit", { Intensity = 0.7 })
handle.Finished:Once(function()
print("Hit finished")
end)
Some drivers never end on their own: Constant, Noise, Bind and Cycle, and looped sounds. An effect that uses one runs until you stop it:
local grade = MotionFX:Apply("Color", Drive.Constant({ Saturation = -0.6 }), { FadeIn = 0.3 })
-- later
grade:Stop(0.5)
The whole effect
local hitEnvelope = Drive.Envelope({ Attack = 0.03, Release = 0.6 })
MotionFX:Create("Hit", {
Tracks = {
{ Primitive = "Rotation", Driver = Drive.Spring({ Kick = Vector3.new(4, 0, 0), Frequency = 7, Damping = 0.45 }) },
{
Primitive = "Rotation",
Driver = Drive.Modulate(Drive.Noise({ Frequency = 16, Amplitude = Vector3.new(1, 1, 2) }), hitEnvelope),
},
{
Primitive = "Vignette",
Driver = Drive.Map(hitEnvelope, function(amount: number): Types.VignetteLayer
return { Intensity = amount * 0.6, Color = Color3.fromRGB(150, 0, 0) }
end),
},
{ Primitive = "Sound", SoundId = MotionFX.Sounds.Shake, Volume = 0.8 },
},
})
MotionFX:Play("Hit", { Intensity = 0.7 })
From here, see Retrigger policies for what happens when it's played again while running, and Examples for bigger effects.
Common mistakes
| Symptom | Cause and fix |
|---|---|
| A track is underlined in the editor | The driver doesn't produce the primitive's value type, for example an Envelope (a number) on Rotation (a Vector3). Wrap it as shown in Which drivers fit, such as Drive.Modulate(Drive.Constant(Vector3.new(4, 0, 0)), envelope). |
A Color3 is rejected on Color | Color takes a ColorValue table. Use Drive.Map(driver, function(c: Color3): Types.ColorValue return { Tint = c } end). |
A Map function is underlined | Strict mode can't tell what the returned table is. Annotate the return type: function(a: number): Types.VignetteLayer or : Types.ColorValue. |
| The effect never ends | It contains an open-ended driver (Constant, Noise, Bind, Cycle) or a looped sound. Keep the handle and call handle:Stop(), give the effect a Duration, or shape the driver with Modulate and an envelope. |
Driver must be built with MotionFX.Drive | A raw value was passed as a driver. Write Drive.Constant(6) instead of 6. |
an effect named "Hit" already exists | Create ran twice, often because the script runs again. Check MotionFX:Has("Hit") first, or pass { Overwrite = true }. |
MotionFX is client-only | It was required from server code. Require it from a LocalScript or a Script with its RunContext set to Client, and fire a RemoteEvent to trigger effects from the server. |
Enum.KeyCode stops working | A local named Enum hides Roblox's global. Name it FX: local FX = MotionFX.Enum. |
| An overlay shows as a black box | The effect was filmed on black, and Roblox images can't blend additively. Turn on Make black transparent in the Overlay maker. |
| An overlay doesn't show at all | Its Images are probably Decal ids. Overlays need the Image id behind each Decal; Output shows a MotionFX warning when an image can't load. See Decal ids vs Image ids. |
| An overlay is blank or shows the wrong frames | Check that Columns, Rows, FrameSize and Frames match how the sheet was made (the Overlay maker shows them). Sheets bigger than 1024×1024 are scaled down by Roblox, which puts FrameSize out of step. A newly uploaded image also stays blank until moderation approves it. |
| The game stutters while an overlay plays | Overlays are heavy: each sheet costs texture memory, and each playing copy draws a full-screen image. Use fewer sheets, give overlay effects Retrigger = "Refresh" so they don't stack, and see overlay performance. |
| An overlay blinks the first time it plays | Its sheets weren't ready to draw yet. Preload the sheet while the game loads: task.spawn(MotionFX.Overlay.Preload, sheet). It keeps the images ready until you call MotionFX.Overlay.Unload. |
No suggestions inside Primitive = "" | Editors usually don't suggest inside quotes automatically. Press Ctrl+Space, or in VS Code set "editor.quickSuggestions": { "strings": "on" } for Luau. |