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
- Understanding CFrames
- How to Think About CFrames
- What Is CFrame? | Roblox CFrame Tutorial | LookVector, Angles & More! - YouTube
TweenService & TweenInfo
- Roblox TweenService Tutorial - How To Tween Parts In Your Game - Part Tweening - YouTube
- Roblox TweenService Tutorial 2021 | Roblox Studio - YouTube
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 thecameraScenes
type I provided at the top of this tutorial and the arrays it contains are thecameraScene
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.