How to Create Camera Cutscenes

Hello! This tutorial was created because of this in the #help-and-feedback:scripting-support category. I’ll paste the video the OP of that topic gave because it will be relevant to this tutorial.

Here’s a breakdown of what you need to know going into this tutorial and links to tutorials for that topic. This tutorial also assumes you know basic scripting (variables, loops, tables, functions, ect.) and editing of properties in studio.

Pre-knowledge

To actually understand what I’m doing, you’ll have to understand how the following work:

If you do not, then you can check out these links for tutorials and information:

CFrame

TweenService & TweenInfo

Object & World Space

These are all the tutorials I used when teaching myself to program in Roblox. However, one them I haven’t fully watched (the 2021 one). I just saw the introduction and thought it looked really good. If you need further help, you can check out the Roblox Onboarding experience, this beginner scripting series by AlvinBlox, or by searching the #help-and-feedback and #resources:community-tutorials categories of the forum.

  • Note: Just a disclaimer, this tutorial uses the Command bar and output in studio. If you do not have these enabled, it is recommended you do so. You can do this in the View tab in studio or by right-clicking in one of the tab bars and selecting them from the list.

Alright, so first things first, the provided video showed several camera angles instead of just one. Assuming that we want multiple, this will make it a teeny bit trickier to make, but not much. All it really entails will be creating an array with an initial offset cframe, end offset cframe, and a tween info instance. This way you’ll be able to customize each camera movement animation specifically. An example of the type structure of this array is:

type cameraScene = {InitialOffset: CFrame, EndOffset: CFrame, TweenInfo: TweenInfo, Delay: number}
type cameraScenes = {cameraScene}


Step One: Creating a character dummy
Before you do anything else, you’ll want to create a character dummy. This can be done in the plugins tab in studio using the built-in Rig Builder plugin. For this tutorial I created a simple R15 block rig, but you can use any rig. I’ve named my rig “CameraDummy” for easy access in the command bar.

Step Two: Creating an offset value table
Now what you want to do is create a script. Because this is a demonstration, I’m going to be creating a simple Script inside the CameraDummy Model. However, you will want use a LocalScript because when in-game, this action can only be done on the client.

Inside the created BaseScript object you will want to create an array containing your offset values. While this example only contains a single array, you can create as many as you need. I will be using more later in this tutorial. Also, I’ve added some other variables for the purpose of animating the camera later.

local TweenService = game:GetService("TweenService") -- the tween service

local rootPart = script.Parent.PrimaryPart -- the path to the character part you are basing your offset off of
local camera = workspace.CurrentCamera -- the camera

local cameraScenes = {
	{
		InitialOffset = CFrame.new(),
		EndOffset = CFrame.new(),
		TweenInfo = TweenInfo.new(),
		Delay = 0
	}
}
  • Note: If you understand type-checking then you’ll notice that the cameraScenes variable is the cameraScenes type I provided at the top of this tutorial and the arrays it contains are the cameraScene type. Because type-checking is not necessary however, I will not be including it in this tutorial. The only purpose for the example of the types was for a better understanding of the array structure.

Step Three: Getting the offset values
Now that we have our rig we want to find the position to view to character from. This should be relatively easy since studio uses a Camera while you are looking through and editing the 3D World, just like it uses in-game.


This means that we can position the camera how we want it and then use the Camera.CFrame to find the relative offset. In the video, the first scene showed the camera as starting around the navel. So, we will position our camera near here.

Great! Now that we’ve positioned our camera we can get the relative offset of the cframe using this line in the command bar:

print(workspace.CameraDummy.PrimaryPart.CFrame:ToObjectSpace(workspace.CurrentCamera.CFrame))
  • Note: You should replace “CameraDummy” with the name of your rig if it’s named differently.

Now you should notice something in the output like this:

We will want to copy this part…


and then paste it in the appropriate section of the table we created in the previous step. If it’s a camera angle we want to start from, we will paste it in the InitialOffset CFrame variable. If it’s one we want to end at, we want to end at we can paste it in the EndOffset CFrame variable. For example, because this is my starting point my array will now look like this:

local cameraScenes = {
	{
		InitialOffset = CFrame.new(0.0948219299, -0.0850167274, -1.67939758, -0.999864638, 8.58287458e-05, 0.0164527874, 7.27595675e-12, 0.99998647, -0.00521659805, -0.0164530091, -0.0052158921, -0.999851108),
		EndOffset = CFrame.new(),
		TweenInfo = TweenInfo.new(),
		Delay = 4
	}
}

These are the values we need to construct the cframe. You should repeat this step for all camera positions you want.

Also, as a side note, you can “correct” the cframe offset if it’s not exactly centered to the character if you want. However, I caution against this, as a slight but somewhat noticeable imperfection can actually make the camera animation more natural.

Step Four: Editing the tween animation
With step three finished, you should now have an array containing all the start and endpoints you want. However, you may notice that TweenInfo property is still empty. This is because you’ll want to customize the tween values for each camera scene individually.

In the first animation scene, the animation takes approximately 7 seconds, give or take.

So we will define the time parameter as 7. For animation speed smoothness, I will be setting EasingDirection to Enum.EasingDirection.InOut. I will be using several styles for the EasingStyle.

My TweenInfo for the first animation should now look like this:

TweenInfo = TweenInfo.new(7, Enum.EasingStyle.Cubic, Enum.EasingDirection.InOut)

If you want to copy the exact animation my tutorial will be using then your array should look like this:

Animation Array
local cameraScenes = {
	{
		InitialOffset = CFrame.new(0.0948219299, -0.0850167274, -1.67939758, -0.999864638, 8.58287458e-05, 0.0164527874, 7.27595675e-12, 0.99998647, -0.00521659805, -0.0164530091, -0.0052158921, -0.999851108),
		EndOffset = CFrame.new(0.0343780518, 1.79274559, -1.51996231, -0.999989986, 0.00100219855, -0.0043754559, -0, 0.974757075, 0.223268166, 0.00448876619, 0.223265931, -0.974747181),
		TweenInfo = TweenInfo.new(7, Enum.EasingStyle.Cubic, Enum.EasingDirection.InOut),
		Delay = 3
	},
	{
		InitialOffset = CFrame.new(1.89093018, 2.03204489, -0.995923996, -0.491765082, -0.238556981, 0.837411284, -7.4505806e-09, 0.961737037, 0.27397421, -0.870727897, 0.13473095, -0.4729487),
		EndOffset = CFrame.new(3.52386475, 2.56629276, -1.91816902, -0.491765082, -0.238556057, 0.837411582, 7.4505806e-09, 0.961737335, 0.273973137, -0.870727897, 0.134730428, -0.472948849),
		TweenInfo = TweenInfo.new(6, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut),
		Delay = 2
	},
	{
		InitialOffset = CFrame.new(-2.3183403, -1.81286108, -2.97310638, -0.814565957, -0.241467848, -0.52742362, -0, 0.909239888, -0.416272908, 0.580071032, -0.339081734, -0.740635753),
		EndOffset = CFrame.new(-2.86165237, 0.232928276, -3.73602676, -0.814565897, -0.241467506, -0.527423918, -0, 0.909240127, -0.416272223, 0.580071151, -0.339081168, -0.740635931),
		TweenInfo = TweenInfo.new(5, Enum.EasingStyle.Quart, Enum.EasingDirection.InOut),
		Delay = 2
	}
}

Step Five: Making the animation play
Now that we have all the animations we want, we can create a function to iterate through the list and play them! All we need to do is use what we already have. First we want to store the current camera offset and type in a variable and then set the camera’s Camera.CameraType to scriptable.

local function animateCamera(rootPart, camera, cameraScenes)
	local initialCameraFrame = rootPart.CFrame:ToObjectSpace(camera.CFrame)
	local initialCameraType = camera.CameraType
	camera.CameraType = Enum.CameraType.Scriptable
	-- other code

Now that we’ve done that, we will want to iterate through the cameraScenes array, create the tween, and play it.

	for _, cameraScene in pairs(cameraScenes) do
		camera.CFrame = rootPart.CFrame:ToWorldSpace(cameraScene.InitialOffset)
		
		local cameraTween = TweenService:Create(camera, cameraScene.TweenInfo, {CFrame = rootPart.CFrame:ToWorldSpace(cameraScene.EndOffset)}) -- create the tween
		cameraTween:Play() -- play the tween
		cameraTween.Completed:Wait() -- wait for tween to finish playing
		cameraTween:Destroy() -- destroy the tween
		
		task.wait(cameraScene.Delay) -- delay until the next scene
	end

Great! Now that that works, we must set the camera type back to its initial value.

	-- other code
	camera.CFrame = rootPart.CFrame:ToWorldSpace(initialCameraFrame) -- set the camera cframe back
	camera.CameraType = initialCameraType -- set the camera type back

And that finishes our function! Here’s the completed one:

local function animateCamera(rootPart, camera, cameraScenes)
	local initialCameraFrame = rootPart.CFrame:ToObjectSpace(camera.CFrame)
	local initialCameraType = camera.CameraType
	camera.CameraType = Enum.CameraType.Scriptable
	
	for _, cameraScene in ipairs(cameraScenes) do
		camera.CFrame = rootPart.CFrame:ToWorldSpace(cameraScene.InitialOffset)
		
		local cameraTween = TweenService:Create(camera, cameraScene.TweenInfo, {CFrame = rootPart.CFrame:ToWorldSpace(cameraScene.EndOffset)}) -- create the tween
		cameraTween:Play() -- play the tween
		cameraTween.Completed:Wait() -- wait for tween to finish playing
		cameraTween:Destroy() -- destroy the tween
		
		task.wait(cameraScene.Delay) -- delay until the next scene
	end
	
	camera.CFrame = rootPart.CFrame:ToWorldSpace(initialCameraFrame) -- set the camera cframe back
	camera.CameraType = initialCameraType -- set the camera type back
end

Conclusion: Full script, optional typed script, and model

And there you have it! Here’s the full script:

local TweenService = game:GetService("TweenService") -- the tween service

local rootPart = script.Parent.PrimaryPart -- the path to the character part you are basing your offset off of
local camera = workspace.CurrentCamera -- the camera

local cameraScenes = {
	{
		InitialOffset = CFrame.new(0.0948219299, -0.0850167274, -1.67939758, -0.999864638, 8.58287458e-05, 0.0164527874, 7.27595675e-12, 0.99998647, -0.00521659805, -0.0164530091, -0.0052158921, -0.999851108),
		EndOffset = CFrame.new(0.0343780518, 1.79274559, -1.51996231, -0.999989986, 0.00100219855, -0.0043754559, -0, 0.974757075, 0.223268166, 0.00448876619, 0.223265931, -0.974747181),
		TweenInfo = TweenInfo.new(7, Enum.EasingStyle.Cubic, Enum.EasingDirection.InOut),
		Delay = 3
	},
	{
		InitialOffset = CFrame.new(1.89093018, 2.03204489, -0.995923996, -0.491765082, -0.238556981, 0.837411284, -7.4505806e-09, 0.961737037, 0.27397421, -0.870727897, 0.13473095, -0.4729487),
		EndOffset = CFrame.new(3.52386475, 2.56629276, -1.91816902, -0.491765082, -0.238556057, 0.837411582, 7.4505806e-09, 0.961737335, 0.273973137, -0.870727897, 0.134730428, -0.472948849),
		TweenInfo = TweenInfo.new(6, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut),
		Delay = 2
	},
	{
		InitialOffset = CFrame.new(-2.3183403, -1.81286108, -2.97310638, -0.814565957, -0.241467848, -0.52742362, -0, 0.909239888, -0.416272908, 0.580071032, -0.339081734, -0.740635753),
		EndOffset = CFrame.new(-2.86165237, 0.232928276, -3.73602676, -0.814565897, -0.241467506, -0.527423918, -0, 0.909240127, -0.416272223, 0.580071151, -0.339081168, -0.740635931),
		TweenInfo = TweenInfo.new(5, Enum.EasingStyle.Quart, Enum.EasingDirection.InOut),
		Delay = 2
	}
}


local function animateCamera(rootPart, camera, cameraScenes)
	local initialCameraFrame = rootPart.CFrame:ToObjectSpace(camera.CFrame)
	local initialCameraType = camera.CameraType
	camera.CameraType = Enum.CameraType.Scriptable
	
	for _, cameraScene in pairs(cameraScenes) do
		camera.CFrame = rootPart.CFrame:ToWorldSpace(cameraScene.InitialOffset)
		
		local cameraTween = TweenService:Create(camera, cameraScene.TweenInfo, {CFrame = rootPart.CFrame:ToWorldSpace(cameraScene.EndOffset)}) -- create the tween
		cameraTween:Play() -- play the tween
		cameraTween.Completed:Wait() -- wait for tween to finish playing
		cameraTween:Destroy() -- destroy the tween
		
		task.wait(cameraScene.Delay) -- delay until the next scene
	end
	
	camera.CFrame = rootPart.CFrame:ToWorldSpace(initialCameraFrame) -- set the camera cframe back
	camera.CameraType = initialCameraType -- set the camera type back
end

animateCamera(rootPart, camera, cameraScenes)

Here’s an optional typed script:

Typed Script
type CameraScene = {InitialOffset: CFrame, EndOffset: CFrame, TweenInfo: TweenInfo, Delay: number}
type CameraScenes = {CameraScene}


local TweenService: TweenService = game:GetService("TweenService") -- the tween service

local rootPart: BasePart = script.Parent.PrimaryPart -- the path to the character part you are basing your offset off of
local camera: Camera = workspace.CurrentCamera -- the camera

local cameraScenes: CameraScenes = {
	{
		InitialOffset = CFrame.new(0.0948219299, -0.0850167274, -1.67939758, -0.999864638, 8.58287458e-05, 0.0164527874, 7.27595675e-12, 0.99998647, -0.00521659805, -0.0164530091, -0.0052158921, -0.999851108),
		EndOffset = CFrame.new(0.0343780518, 1.79274559, -1.51996231, -0.999989986, 0.00100219855, -0.0043754559, -0, 0.974757075, 0.223268166, 0.00448876619, 0.223265931, -0.974747181),
		TweenInfo = TweenInfo.new(7, Enum.EasingStyle.Cubic, Enum.EasingDirection.InOut),
		Delay = 3
	},
	{
		InitialOffset = CFrame.new(1.89093018, 2.03204489, -0.995923996, -0.491765082, -0.238556981, 0.837411284, -7.4505806e-09, 0.961737037, 0.27397421, -0.870727897, 0.13473095, -0.4729487),
		EndOffset = CFrame.new(3.52386475, 2.56629276, -1.91816902, -0.491765082, -0.238556057, 0.837411582, 7.4505806e-09, 0.961737335, 0.273973137, -0.870727897, 0.134730428, -0.472948849),
		TweenInfo = TweenInfo.new(6, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut),
		Delay = 2
	},
	{
		InitialOffset = CFrame.new(-2.3183403, -1.81286108, -2.97310638, -0.814565957, -0.241467848, -0.52742362, -0, 0.909239888, -0.416272908, 0.580071032, -0.339081734, -0.740635753),
		EndOffset = CFrame.new(-2.86165237, 0.232928276, -3.73602676, -0.814565897, -0.241467506, -0.527423918, -0, 0.909240127, -0.416272223, 0.580071151, -0.339081168, -0.740635931),
		TweenInfo = TweenInfo.new(5, Enum.EasingStyle.Quart, Enum.EasingDirection.InOut),
		Delay = 2
	}
}


local function animateCamera(rootPart: BasePart, camera: Camera, cameraScenes: CameraScenes)
	local initialCameraFrame: CFrame = rootPart.CFrame:ToObjectSpace(camera.CFrame) -- the initial cframe offset
	local initialCameraType: Enum.CameraType = camera.CameraType -- store the initial camera type
	camera.CameraType = Enum.CameraType.Scriptable -- set the camera type
	
	for _, cameraScene: CameraScene in ipairs(cameraScenes) do
		camera.CFrame = rootPart.CFrame:ToWorldSpace(cameraScene.InitialOffset)
		
		local cameraTween: Tween = TweenService:Create(camera, cameraScene.TweenInfo, {CFrame = rootPart.CFrame:ToWorldSpace(cameraScene.EndOffset)}) -- create the tween
		cameraTween:Play() -- play the tween
		cameraTween.Completed:Wait() -- wait for tween to finish playing
		cameraTween:Destroy() -- destroy the tween
		
		task.wait(cameraScene.Delay) -- delay until the next scene
	end
	
	camera.CFrame = rootPart.CFrame:ToWorldSpace(initialCameraFrame) -- set the camera cframe back
	camera.CameraType = initialCameraType -- set the camera type back
end

animateCamera(rootPart, camera, cameraScenes)

And here’s a model for the demonstration. When you go in studio and insert it, make sure to hit the Run button instead of Play or Play Here. Do remember, however, that this should be done on a local script and this was only created on a server script for demonstration purposes.

EDIT: Changed pairs to ipairs in the animateCamera function so that the animation tracks play in the correct order.

49 Likes

Is there a way to use Camera.CFrame.lookAt and start the camera tween?

You cannot use Camera.CFrame.lookAt because that is not a valid method of the CFrame datatype. It is instead a constructor for creating a CFrame. If you wanted to set the camera to look at something, you would do it like this:

Camera.CFrame = CFrame.lookAt(origin, target)

With origin and target both being Vector3 values.

Apart from that, i don’t really know what you want to happen. You didn’t provide enough details. If you want to make the camera look at a specific point when starting the animation, you can do that by editing the InitialOffset property in the first dictionary in the cameraScenes array.

For example, if you already have an animation array and wanted to set the initial position of the camera, you could do so by setting the InitialOffset property to the offset of the cframe from the rootPart. Here’s a function to accomplish this:

local function getOffset(rootPart, cframe)
	return rootPart.CFrame:ToObjectSpace(cframe)
end

Then, using this function, we can edit the InitialOffset property for the first dictionary in the array.

local cframe = CFrame.lookAt(origin, target)

cameraScenes[1].InitialOffset = getOffset(rootPart, cframe)

An example we can do using of this is to tween the camera from its initial position to the lookat cframe.

local targetCframe = CFrame.lookAt(origin, target)

local animationTrack = cameraScenes[1] do -- get the first dictionary and set its start and ending offsets
	animationTrack.InitialOffset = getOffset(rootPart, camera.CFrame)
	animationTrack.EndOffset = getOffset(rootPart, targetCframe)
end

animateCamera(rootPart, camera, cameraScenes)
1 Like

Amazing cutscene but is there a way I could possibly use this to point at the users own avatar instead?

1 Like

Yes. The dummy is just a stand-in for the actual player character in the first place. You can execute this in a LocalScript on the client, set the rootPart to the clients own character root part, and then execute the script.

is there possible to make like, first scene look at your character and the second scene look at the enemy?

It is possible, but not with how the current setup works. You could change the cameraScenes array and add a RootPart argument to use for each scene though. Then, for looking at other characters you just use the other characters root part.

However, I caution against this. It would be a lot of work to change all the rootpart’s in specific tracks for another player. It would be easier to just play the animation with the player’s root part and then play the same animation again with the other player’s root part (if you are wanting to play the same animation that is.

1 Like

Hello, is this still working? i tried it and it will not start working.

Thanks for you’re tutorial. I just needed to put cinematics in my project.

1 Like

Sorry for the late reply.

I tested it to see if the script broke, but it seems to be in working order. Remember that this is just an example model to demonstrate the tutorial. Also, if you want to test it in studio, insert it into the workspace. Then press the Run button and wait until the animation starts.

Screen Shot 2022-07-11 at 11.25.41 PM

1 Like

I know this has been a while, but how would I implement camera shakes into the cutscene?

1 Like