How to make Roblox Cutscenes
Learning how to make cutscenes in Roblox gives games that extra cinematic polish. Instead of a flat and static screen, we can introduce the game by sweeping across our maps or share our story as if it were a movie.
In this post, I’ll help demystify how the camera uses CFrame and demonstrate the right design patterns you need to create your scenes.
Get your character’s ready for their close up!
What you’ll learn
- Design pattern to setup scenes
- Playing sounds and music in a scene
- Controlling the camera
- How to animate the camera
How Roblox camera works
The Roblox camera is modelled after a real life physical camera that you would buy in the store. It’s able to set its focus to make close up objects sharp and clear while far away objects become blurry. The camera can also control its field of view to control how much it can see.
Like in a movie or TV show, the camera can also pan itself and follow its main subject. We can use different types of cameras through the CameraType property.
Different Roblox camera types
- Fixed – a stationary camera that does not move
- Follow – moves with the current subject and rotates to keep subject in view
- Track – moves with the current subject but does not rotate
- Watch – a stationary camera that follows its current subject (similar to a security camera)
- Scriptable – uses a custom script to define its behave
Roblox camera scripts should go under the StartPlayerScripts folder in the workspace.
If models or parts are blocking a camera’s view, we can use the GetPartsObscuringTarget function to get all of these parts.
We can use the FirstPersonTransition event to react to when the player moves from first person perspective to third person and vice versa.
For VR camera controls, we can use GetPanSpeed and GetTiltSpeed to know how fast a player’s headset is moving on the x, y, and z axises.
How to animate and control the camera
For cutscenes we’ll use a Scriptable camera type. To animate the camera’s position we’ll use the TweenService.
Tweening comes from the word between since it calculates values from a start value to a desired end value. We can use tweening to manipulate all kinds of numeric properties such as position, color, opacity and so on.
The TweenService also gives a script the ability to define how quickly it should transition from a start value. For example, we can transition a part’s color from bright red to dark blue while at the same time moving the part from point A to point B.
Using different easing styles also gives the animation a more natural feel. Think about how a moving vehicle slowly stops as you apply the brakes. If you hit the brakes hard the vehicle lurches to a stop but if you apply the brakes normally it’s a lot smoother.
Code Example
local Camera = workspace.CurrentCamera
local Player = game.Players.LocalPlayer
local Character = Player.Character
local Destination = workspace.DestinationPart
local TweenService = game:GetService("TweenService")
local TweenInfo = TweenInfo.new(5, Enum.EasingStyle.Linear, Enum.EasingDirection.Out, 0, false, 0)
local Tween = TweenService:Create(Camera, TweenInf, {CFrame = Pos})
Tween:Play()
In this example, we have a part named DestinationPart in our workspace that the camera will move towards. The tween info set the animation duration to 5 seconds and use a smooth linear easing style. We can use other EasingStyle enums to have different effects.
How to setup cutscenes
To first set up a cutscene in Roblox we should determine the right data structures our scripts will use. A cutscene is made up of its camera and a video script (not a lua script) to define what happens during the scene.
A film script contains a sequence and directions for the camera, the actors, and background action in the scenery. In regular cinema, they use beats as a guideline for the pacing of a scene.
Just like any movie or video, the pacing helps set the mood. Since we’re working with code and not actors, we’ll use a time duration for each beat in our scene.
Now to get into the technical bits, we’ll use a Lua table keyed by the starting point of each beat. The start point is how many seconds have passed since the start of the scene.
Cut scene data structure example
local cutscene = {}
local firstBeat = { start = 0, next = nil, actions = {} }
local action = {
Type = nil,
StartPosition = nil -- CFrame
EndPosition = nil -- CFrame
}
Since there’s a lot happening as each second passes, actions must be able to run at or near the same time. This includes moving the camera, animating characters, and playing music and sounds.
local RunService = game:GetService(“RunService)
-- CutScene Beat class
CutSceneBeat = {}
function CutSceneBeat:new()
local self = setmetatable({}, CutSceneBeat)
self.start = 0
self.next = nil
Self.Actions = {}
return self
end
function CutSceneBeat:AddAction(action)
table.insert(self.Actions, action)
end
function CutSceneBeat:play()
for i, action in ipairs(self.Actions) do
-- execute action
end
end
function CutSceneBeat:onRenderStep(deltaTime)
-- trigger actions in the beat
end
-- CutScene class
CutScene = {}
function CutScene:new()
local self = setmetatable({}, CutScene)
self.Beats = {}
self.CurrentBeat = nil
self.Position = 0
return self
end
function CutScene:AddBeat(beat)
self.Beats[beat.start] = beat
end
function CutScene:Play()
if not self.CurrentBeat then
self.CurrentBeat = self.Beats[0]
End
if self.CurrentBeat then
self.CurrentBeat:Play()
end
end
function CutScene.onRenderStep(deltaTime)
if self.CurrentBeat then
self.CurrentBeat:onRenderStep(deltaTime)
end
end
We’ll use an enum to distinguish between the different types of beat actions.
CutSceneActionType = {
Animation = 1,
Emote = 2,
Dialog = 3,
Music = 4,
Sound = 5
}
We’ll use a Scene player object to execute the sequence of each beat. The runner will have functions to start and stop the cut scene.
Cut Scene Player script
Here’s a fully working script for a Cutscene service. It provides a player and objects for the beats and actions.
We use RunService to tap into the game’s render loop through its RenderStep event. The RenderStep event gives us how many seconds have passed for each render frame. Typically, Roblox will lock the frame rate to 60 frames per second.
-- module script
-- add to ReplicatedStorage
local RunService = game:GetService("RunService")
local TweenService = game:GetService("TweenService")
local module = {}
local CutSceneActionType = {
Animation = 1,
Emote = 2,
Dialog = 3,
Music = 4,
Sound = 5
}
local CutSceneState = {
Waiting = 1,
Playing = 2,
Paused = 3,
Stopped = 4,
Done = 5
}
local ActionState = {
Waiting = 1,
Started = 2,
Done = 3
}
-- CutScene Beat class
CutSceneBeat = {}
function CutSceneBeat:new()
local o = {}
o.Name = "CutSceneBeat"
o.Start = 0
o.Next = nil
o.Actions = {}
self.__index = self
return setmetatable(o, self)
end
function CutSceneBeat:AddAction(action)
table.insert(self.Actions, action)
end
function CutSceneBeat:Play(seconds)
local waitingActions = {}
for i, action in ipairs(self.Actions) do
local waiting = action.State == ActionState.Waiting
if waiting then
table.insert(waitingActions, action)
end
end
for i, action in ipairs(waitingActions) do
-- execute action by type
action.State = ActionState.Started
if action.Type == CutSceneActionType.Animation then
self:TriggerAnimation(action)
elseif action.Type == CutSceneActionType.Emote then
self:TriggerEmote(action)
elseif action.Type == CutSceneActionType.Dialog then
self:TriggerDialog(action)
elseif action.Type == CutSceneActionType.Music then
self:TriggerMusic(action)
elseif action.Type == CutSceneActionType.Sound then
self:TriggerSound(action)
end
end
end
function CutSceneBeat:TriggerAnimation(action)
local camera = workspace.CurrentCamera
camera.CameraType = Enum.CameraType.Scriptable
camera.CFrame = action.StartPosition
local cameraMovement = {
CFrame = action.EndPosition
}
local tweenInfo = TweenInfo.new(action.Duration)
local tween = TweenService:Create(camera, tweenInfo, cameraMovement)
tween:Play()
end
function CutSceneBeat:TriggerEmote(action)
print('trigger emote')
end
function CutSceneBeat:TriggerDialog(action)
print('trigger dialog')
end
function CutSceneBeat:TriggerMusic(action)
print('trigger music')
end
function CutSceneBeat:TriggerSound(action)
print('trigger sound')
end
-- CutScene class
CutScene = {}
function CutScene:new(o)
local o = {}
o.Name = "CutScene"
o.Beats = {}
o.State = CutSceneState.Waiting
o.Position = 0
o._DeltaTotal = 0
self.__index = self
return setmetatable(o, self)
end
function CutScene:AddBeat(beat)
self.Beats[beat.Start] = beat
end
function CutScene:Play()
self.State = CutSceneState.Playing
end
function CutScene:onRenderStep(deltaTime)
if self.State == CutSceneState.Playing then
self._DeltaTotal = self._DeltaTotal + deltaTime
self.Position = math.floor(self._DeltaTotal)
local CurrentBeat = self.Beats[self.Position]
if CurrentBeat then
CurrentBeat:Play(self.Position)
end
end
end
-- CutScene class
CutScenePlayer = {}
function CutScenePlayer:new(o)
o = o or {}
setmetatable(o, self)
self.__index = self
self.Camera = nil
self.CutScene = nil
return o
end
function CutScenePlayer:Play()
if self.CutScene then
self.CutScene:Play()
end
end
function CutScenePlayer:Pause()
if self.CutScene then
self.CutScene:Pause()
end
end
function CutScenePlayer:Resume()
if self.CutScene then
self.CutScene:Resume()
end
end
function CutScenePlayer:onRenderStep(deltaTime)
if self.CutScene then
self.CutScene:onRenderStep(deltaTime)
end
end
function createAction(params)
local action = {
Type = params.Type,
StartTime = params.StartTime,
Duration = params.Duration,
StartPosition = params.StartPosition,
EndPosition = params.EndPosition,
State = ActionState.Waiting
}
return action
end
module.CutScene = CutScene
module.CutSceneBeat = CutSceneBeat
module.createAction = createAction
module.CutSceneActionType = CutSceneActionType
module.CutScenePlayer = CutScenePlayer
return module
Next we’ll set up our cutscene in a LocalScript that moves the camera through several positions using invisible parts. Using these parts, we can reference their CFrame to create the camera track that our cutscene follows.
-- LocalScript Example
-- Add to StarterPlayerScripts
local RunService = game:GetService("RunService")
local localPlayer = game.Players.LocalPlayer
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local CutSceneService = require(ReplicatedStorage:WaitForChild("CutsceneService"))
local CutScene = CutSceneService.CutScene
local CutSceneBeat = CutSceneService.CutSceneBeat
local createAction = CutSceneService.createAction
local CutSceneActionType = CutSceneService.CutSceneActionType
local CutScenePlayer = CutSceneService.CutScenePlayer
local function startCutscene()
-- example
local cutscene = CutScene:new()
-- setup test cutscene beats & actions
local beat = CutSceneBeat:new()
beat.Start = 0
local action = createAction({
Type = CutSceneActionType.Animation,
StartTime = beat.Start,
Duration = 3,
StartPosition = workspace.scene1.CFrame,
EndPosition = workspace.scene2.CFrame
})
beat:AddAction(action)
cutscene:AddBeat(beat)
local beat2 = CutSceneBeat:new()
beat2.Start = 5
local action2 = createAction({
Type = CutSceneActionType.Animation,
StartTime = beat2.Start,
Duration = 3,
StartPosition = workspace.scene2.CFrame,
EndPosition = workspace.scene3.CFrame
})
beat2:AddAction(action2)
cutscene:AddBeat(beat2)
local beat3 = CutSceneBeat:new()
beat3.Start = 10
local action3 = createAction({
Type = CutSceneActionType.Animation,
StartTime = beat3.Start,
Duration = 5,
StartPosition = workspace.scene3.CFrame,
EndPosition = workspace.scene4.CFrame
})
beat3:AddAction(action3)
cutscene:AddBeat(beat3)
-- setup cutscene runner
local runner = CutScenePlayer:new()
runner.Camera = workspace.CurrentCamera
runner.CutScene = cutscene
RunService.RenderStepped:Connect(function (delta)
runner:onRenderStep(delta)
end)
-- start cutscene
runner:Play()
end
local function onCharacterAdded(character)
startCutscene()
end
localPlayer.CharacterAdded:connect(onCharacterAdded)
What’s next
Now you have a working example of how to make Roblox cutscenes. Your game can trigger these cutscenes in all sorts of ways.
Try triggering them by:
- When the player joins the game
- Player touches a part
- Current round is over or just starting
Check out my guide on how Roblox games work if you’re not sure how to create these triggers.
Please join my email newsletter so you don’t miss out on any future guides!
Thank you for reading and stay curious!