How to Make Cutscenes & Text Sequences!

Hello there!

After maintaining my two main tutorials in the background for the past couple of years, along with my open-source, I decided to write some more, and with that, here we are:

What Are We Making?

Text sequences.
You’ve seen 'em everywhere. From NPC dialogues in your favorite RPGs, to cutscenes in scenic exhibitions and gameshows, they’re a greatway to both relay crucial information to your players, while keeping them engaged with the game and action currently ongoing:

Some Examples



Some examples of what I’m referencing to.

This tutorial’s finished product:

So, let’s get started!

Setting Up the UI

The first thing we need to do to make a text sequence is to, well, make the text sequence. To do that, we’ll need to create a ScreenGui, and incorporate the elements needed, like a Frame to display the user interface, and a TextLabel to display any dialogue. For my specific example, I’ve also decided to use a UICorner and UIAspectRatioConstraint to create curved edges and a consistent shape on all devices.

See How I've Structured It

image

To get this aesthetic for the user interface, I had…

  • Used AnchorPoint property to center the frame from left to right, with a value of 0.5,0
  • Also used an AnchorPoint property for the text label to center it entirely within the frame with a value of 0.5, 0.5
  • Set the text label’s TextScaled property to be true, with a Gotham font-face

The information of the properties are below. Each UI is different, so it’s important you experiment and see what works for you!

Properties

image
image


Programming the Sequence

First, we need a scenario.
Text sequences can be prompted for a variety of reasons. For this tutorial, what we can do is place a placeholder NPC on the field, and add a ProximityPrompt. We’ll have it so that when a player interacts with the prompt, a text sequence will show up!

image

Let’s set up the event for that in a LocalScript (since we only want to affect one player, not everyone). We’ll place this LocalScript into StarterPlayerScripts (a container for client-sided and UI-related code like this), which is inside StarterPlayer. We should also set up our variables for objects that we’ll be interacting with throughout our program, such as that prompt, the user interface of the text sequence, and ourselves!

See the Code For That!
--> Variables. The object references can be different depending on how YOU named these things!
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local npc = workspace.Noob
local prompt = npc.ProximityPrompt
local player = Players.LocalPlayer --> Us!
local playerGui = player:WaitForChild("PlayerGui")
local textSequenceUI = playerGui:WaitForChild("TextSequence")
local mainFrame = textSequenceUI.Frame
local textLabel = mainFrame.TextLabel 
--> We'll use these positions to help move the UI back and forth!
local POSITION_ON_SCREEN = UDim2.new(0.5,0,0.85,0)
local POSITION_OFF_SCREEN = UDim2.new(0.5,0,1,0)

prompt.Triggered:Connect(function(playerWhoTriggered)
    if playerWhoTriggered == player then --> Check if we were the ones who triggered the prompt
        --> TODO: Our text sequence code
    end
end)

The primary portion of our programming is that of the text sequence itself!
When programming, I always find it best to segment and split my general idea into a bunch of fragments that can be tied together. With a text sequence, what I think of is:

  • Code to open the text sequence, and show the first bit of text
  • Code that cycles to the next piece of text in the sequence
  • Code that closes the text sequence in general
  • And perhaps something to tie it all into one piece of code to call!
local function enter(text)
end

local function changeText(text)
end

local function exit()
end

local function playSequence()
end

Obviously, just typing in the names of the functions will make them functional, so we’ll need to program the logic. Specifically, we need to be able to:

  • Move the main frame up in positioning when entering, so it can be seen by players (perhaps use TweenPosition, because that’d make movement smoother)
  • Change the text displayed by the TextLabel as needed (we’ll probably need an input like a list of text, so we know how to display in what order)
  • Maybe add a cool effect to it, like fading? (We could use a for loop to change the transparency of the text
  • When it’s done, we should “exit” by moving the main frame back down, out of view
Code of the Functions for Text Sequences
--> Functions
local function enter(text)
	--> Move the text sequence UI into view!
	mainFrame:TweenPosition(POSITION_ON_SCREEN, "Out", "Back", 0.5)
	textLabel.TextTransparency = 1
	task.wait(0.5)
	--> Let's set the first piece of text onto the screen
	textLabel.Text = text
	for i = 1, 0, -.05 do
		textLabel.TextTransparency = i
		RunService.RenderStepped:Wait() --> This lets us run the loop each frame!
	end
	task.wait(5) --> To let the player read the text!
end

local function changeText(text)
	--> We want to fade the text out, change it, and fade it back in!
	--> Like enter(text), we'll use a for loop to do it
	for i = 0, 1, .05 do
		textLabel.TextTransparency = i
		RunService.RenderStepped:Wait()
	end
	textLabel.Text = text
	for i = 1, 0, -.05 do
		textLabel.TextTransparency = i
		RunService.RenderStepped:Wait()
	end
	task.wait(5) --> Allow some time for the player to read!!
end

local function exit()
	--> We need to fade the text out, and then close the UI
	for i = 0, 1, .05 do
		textLabel.TextTransparency = i
		RunService.RenderStepped:Wait()
	end
	mainFrame:TweenPosition(POSITION_OFF_SCREEN , "In", "Back", .5)
end

local function playSequence(listOfTexts)
	for i, string in pairs(listOfTexts) do --> We'll do a loop. If it's the first time, we enter. If not, we'll just change text. Once it's done, we'll exit.
		if i == 1 then
			enter(string)
		else
			changeText(string)
		end
	end
	exit()
end

And with that, what’s left is to incorporate these functions into the rest of the LocalScript, as such!

--> Variables. The object references can be different depending on how YOU named these things!
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local npc = workspace.Noob
local prompt = npc.ProximityPrompt
local player = Players.LocalPlayer --> Us!
local playerGui = player:WaitForChild("PlayerGui")
local textSequenceUI = playerGui:WaitForChild("TextSequence")
local mainFrame = textSequenceUI.Frame
local textLabel = mainFrame.TextLabel 
--> We'll use these positions to help move the UI back and forth!
local POSITION_ON_SCREEN = UDim2.new(0.5,0,0.85,0)
local POSITION_OFF_SCREEN = UDim2.new(0.5,0,1,0)


--> Functions
local function enter(text)
	--> Move the text sequence UI into view!
	mainFrame:TweenPosition(POSITION_ON_SCREEN, "Out", "Back", 0.5)
	textLabel.TextTransparency = 1
	task.wait(0.5)
	--> Let's set the first piece of text onto the screen
	textLabel.Text = text
	for i = 1, 0, -.05 do
		textLabel.TextTransparency = i
		RunService.RenderStepped:Wait() --> This lets us run the loop each frame!
	end
	task.wait(5) --> To let the player read the text!
end

local function changeText(text)
	--> We want to fade the text out, change it, and fade it back in!
	--> Like enter(text), we'll use a for loop to do it
	for i = 0, 1, .05 do
		textLabel.TextTransparency = i
		RunService.RenderStepped:Wait()
	end
	textLabel.Text = text
	for i = 1, 0, -.05 do
		textLabel.TextTransparency = i
		RunService.RenderStepped:Wait()
	end
	task.wait(5) --> Allow some time for the player to read!!
end

local function exit()
	--> We need to fade the text out, and then close the UI
	for i = 0, 1, .05 do
		textLabel.TextTransparency = i
		RunService.RenderStepped:Wait()
	end
	mainFrame:TweenPosition(POSITION_OFF_SCREEN , "In", "Back", .5)
end

local function playSequence(listOfTexts)
	for i, string in pairs(listOfTexts) do
		if i == 1 then
			enter(string)
		else
			changeText(string)
		end
	end
	exit()
end

prompt.Triggered:Connect(function(playerWhoTriggered)
	if playerWhoTriggered == player then --> Check if we were the ones who triggered the prompt
		--> playSequence will do the job for us! However, we might need to create some dialogue...
		local dialogue = {
			"Welcome to PolyCity, the best city for the best Robloxians, and home to the best that Roblox has to offer! Let's take a tour of it all, shall we?",
			"As you enter the city, you'll be greeted to Jiffy, our signature gas station! It's got the cheapest prices for your gas needs, and a nice store for anything else!",
			"Right next to it, is our beautiful coffee shop, with a great selection of menu items, and space for comfort! You'll feel right at home!",
			"And just outside this corner of town is our newly renovated playground, perfect for kids of all ages, looking to have some fun under the sun!",
			"And to prolong your stay, PolyCity has MANY luxury lodging and suites that'll be sure to give you the experience of a lifetime. Have fun in PolyCity!"
		}
		playSequence(dialogue)
	end
end)

And this is how it looks right now! Our very own text sequence!

See How It Looks Right Now!

Now, for you ambitious folks, we can expand FURTHER on this by adding nice, cinematic cutscenes throughout it all, in the next section!

Setting Up Cutscenes!

For those of you interested in going the extra mile, cutscenes are the perfect way to accomplish that! There are two key components to keep in mind while setting them up:

Capturing the Scene & Coordinates

When trying to capture the right scenery for your cutscene, you’ll want to simply just move around your map in Studio, and find an area that you want to show off, and highlight. While different for everyone, in my scenario, I talk a lot about certain places, so I’ll want to move myself in Studio to get a good angle of these places, Jiffy Gas Station!

When I go to program this camera angle as a cutscene, I’m going to need to be able to refer back to what this camera angle and positioning was when I program it. That’s why I’ll use the Command Bar, and type in the following commands:

print(workspace.CurrentCamera.CFrame.Position)
(For the camera’s position)

print(workspace.CurrentCamera.Focus.Position)
(For where it’s looking at)

In your Output, you’ll find a long string of numbers. This is what we’ll use later to program:

image

And since cutscenes are moving scenes, I’ll look at repeating the same steps and commands for a zoomed in angle of this gas station!

See The Process Visualized (Again)

print(workspace.CurrentCamera.CFrame.Position)

print(workspace.CurrentCamera.Focus.Position)

image
Your numbers WILL be different, as you’ll be creating your own scenes!

Now with that, we have the data for one cutscene! Since I have 5 pieces of dialogue in my text sequence, I’ll be creating 5 cutscenes in total, and repeat the process for that.

To avoid getting lost and mixing up numbers, I immediately try to store these numbers into a list in the same LocalScript. Thus, I’ll create a new variable:

local cutsceneInfo = {
    {startingCoord = CFrame.lookAt(Vector3.new(184, 33, -99), Vector3.new(186, 32, -101)), endCoord = CFrame.lookAt(Vector3.new(198, 29, -108), Vector3.new(200, 29, -109))}, --> I like to round my numbers to make them more readable :3
     --> Make sure to fill in the rest of the cutscenes, as you need!
    {startingCoord = CFrame.lookAt(), endCoord = CFrame.lookAt()},
    {startingCoord = CFrame.lookAt(), endCoord = CFrame.lookAt()},
    {startingCoord = CFrame.lookAt(), endCoord = CFrame.lookAt()},
    {startingCoord = CFrame.lookAt(), endCoord = CFrame.lookAt()},
}

What we’re doing here is making a list of dictionaries, which are storing information about how each cutscene starts and ends!

A key thing to note is that CFrame.lookAt(positionVector, lookAtVector). In essence, this is a coordinate that is stored by two inputs: the position of the camera, and where it’s focused in on! (order DOES matter). We’ll use this later in programming to change the our camera’s CFrame!

Programming the Cutscenes

Putting it together, we now have our original program, with a list of data.

See How Our Code Looks Now
--> Variables. The object references can be different depending on how YOU named these things!
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local npc = workspace.Noob
local prompt = npc.ProximityPrompt
local player = Players.LocalPlayer --> Us!
local playerGui = player:WaitForChild("PlayerGui")
local textSequenceUI = playerGui:WaitForChild("TextSequence")
local mainFrame = textSequenceUI.Frame
local textLabel = mainFrame.TextLabel 
--> We'll use these positions to help move the UI back and forth!
local POSITION_ON_SCREEN = UDim2.new(0.5,0,0.85,0)
local POSITION_OFF_SCREEN = UDim2.new(0.5,0,1,0)

--> For cutscenes
local cutsceneInfo = {
	{startingCoord = CFrame.lookAt(Vector3.new(152, 60, -96), Vector3.new(151, 61, -95)), endCoord = CFrame.lookAt(Vector3.new(193, 69, -127), Vector3.new(192, 70, -126))},
	{startingCoord = CFrame.lookAt(Vector3.new(184, 33, -99), Vector3.new(186, 32, -101)), endCoord = CFrame.lookAt(Vector3.new(198, 29, -108), Vector3.new(200, 29, -109))},
	{startingCoord = CFrame.lookAt(Vector3.new(172, 20, -231), Vector3.new(172, 20, -229)), endCoord = CFrame.lookAt(Vector3.new(172, 9, -261), Vector3.new( 172, 9, -259))},
	{startingCoord = CFrame.lookAt(Vector3.new(51, 19, -79 ), Vector3.new(53, 19, -80)), endCoord = CFrame.lookAt(Vector3.new(7, 20, -67 ), Vector3.new(8, 20, -67))},
	{startingCoord = CFrame.lookAt(Vector3.new(-202, 66, -85), Vector3.new(-200, 64, -85 )), endCoord = CFrame.lookAt(Vector3.new(-180, 162, -86 ), Vector3.new(-180, 160, -86 ))},
}


--> Functions
local function enter(text)
	--> Move the text sequence UI into view!
	mainFrame:TweenPosition(POSITION_ON_SCREEN, "Out", "Back", 0.5)
	textLabel.TextTransparency = 1
	task.wait(0.5)
	--> Let's set the first piece of text onto the screen
	textLabel.Text = text
	for i = 1, 0, -.05 do
		textLabel.TextTransparency = i
		RunService.RenderStepped:Wait() --> This lets us run the loop each frame!
	end
	task.wait(5) --> To let the player read the text!
end

local function changeText(text)
	--> We want to fade the text out, change it, and fade it back in!
	--> Like enter(text), we'll use a for loop to do it
	for i = 0, 1, .05 do
		textLabel.TextTransparency = i
		RunService.RenderStepped:Wait()
	end
	textLabel.Text = text
	for i = 1, 0, -.05 do
		textLabel.TextTransparency = i
		RunService.RenderStepped:Wait()
	end
	task.wait(5) --> Allow some time for the player to read!!
end

local function exit()
	--> We need to fade the text out, and then close the UI
	for i = 0, 1, .05 do
		textLabel.TextTransparency = i
		RunService.RenderStepped:Wait()
	end
	mainFrame:TweenPosition(POSITION_OFF_SCREEN , "In", "Back", .5)
end

local function playSequence(listOfTexts)
	for i, string in pairs(listOfTexts) do
		if i == 1 then
			enter(string)
		else
			changeText(string)
		end
	end
	exit()
end

prompt.Triggered:Connect(function(playerWhoTriggered)
	if playerWhoTriggered == player then --> Check if we were the ones who triggered the prompt
		--> playSequence will do the job for us! However, we might need to create some dialogue...
		local dialogue = {
			"Welcome to PolyCity, the best city for the best Robloxians, and home to the best that Roblox has to offer! Let's take a tour of it all, shall we?",
			"As you enter the city, you'll be greeted to Jiffy, our signature gas station! It's got the cheapest prices for your gas needs, and a nice store for anything else!",
			"Right next to it, is our beautiful coffee shop, with a great selection of menu items, and space for comfort! You'll feel right at home!",
			"And just outside this corner of town is our newly renovated playground, perfect for kids of all ages, looking to have some fun under the sun!",
			"And to prolong your stay, PolyCity has MANY luxury lodging and suites that'll be sure to give you the experience of a lifetime. Have fun in PolyCity!"
		}
		playSequence(dialogue)
	end
end)

Similar to programming sequence, we’ll need to think about how we want to approach this and segment it out:

  • When we’re entering the text sequence, we’ll need to make sure we can actually move the camera. Since we’re scripting it, we should probably set the CameraType to Scriptable.
  • We should create a function that handles moving the camera in the right place for each cutscene, and gradually moving it (we could use TweenService!)
  • Since these cutscenes will happen for each piece of dialogue, we can call it in the same loop that plays the text sequence!
Code for Setting Up a Cutscene!
local TweenService = game:GetService("TweenService")
local camera = workspace:WaitForChild("Camera")
local TWEEN_INFO = TweenInfo.new(5, Enum.EasingStyle.Cubic, Enum.EasingDirection.InOut) --> This is just information for how TweenService will move the camera, such as how long it'll take (5 seconds), and how fast it should move!

local function doCutscene(info) --> We'll use a dictionary of CFrames as an input
    camera.CFrame = info.startingCoord --> Sets the camera in the right place to start
    local tween = TweenService:Create(camera, TWEEN_INFO, {CFrame = info.endCoord})
    tween:Play() --> Have the cutscene play!
end

And when adding in the changing of camera types, and into the for loop, we have our completed text sequence program, similar to below!

--> Variables. The object references can be different depending on how YOU named these things!
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local npc = workspace.Noob
local prompt = npc.ProximityPrompt
local player = Players.LocalPlayer --> Us!
local playerGui = player:WaitForChild("PlayerGui")
local textSequenceUI = playerGui:WaitForChild("TextSequence")
local mainFrame = textSequenceUI.Frame
local textLabel = mainFrame.TextLabel 
--> We'll use these positions to help move the UI back and forth!
local POSITION_ON_SCREEN = UDim2.new(0.5,0,0.85,0)
local POSITION_OFF_SCREEN = UDim2.new(0.5,0,1,0)

--> For cutscenes
local cutsceneInfo = {
	{startingCoord = CFrame.lookAt(Vector3.new(152, 60, -96), Vector3.new(151, 61, -95)), endCoord = CFrame.lookAt(Vector3.new(193, 69, -127), Vector3.new(192, 70, -126))},
	{startingCoord = CFrame.lookAt(Vector3.new(184, 33, -99), Vector3.new(186, 32, -101)), endCoord = CFrame.lookAt(Vector3.new(198, 29, -108), Vector3.new(200, 29, -109))},
	{startingCoord = CFrame.lookAt(Vector3.new(172, 20, -231), Vector3.new(172, 20, -229)), endCoord = CFrame.lookAt(Vector3.new(172, 9, -261), Vector3.new( 172, 9, -259))},
	{startingCoord = CFrame.lookAt(Vector3.new(51, 19, -79 ), Vector3.new(53, 19, -80)), endCoord = CFrame.lookAt(Vector3.new(7, 20, -67 ), Vector3.new(8, 20, -67))},
	{startingCoord = CFrame.lookAt(Vector3.new(-202, 66, -85), Vector3.new(-200, 64, -85 )), endCoord = CFrame.lookAt(Vector3.new(-180, 162, -86 ), Vector3.new(-180, 160, -86 ))},
}

local TweenService = game:GetService("TweenService")
local camera = workspace:WaitForChild("Camera")
local TWEEN_INFO = TweenInfo.new(5, Enum.EasingStyle.Cubic, Enum.EasingDirection.InOut) --> This is just information for how TweenService will move the camera, such as how long it'll take (5 seconds), and how fast it should move!

local function doCutscene(info) --> We'll use a dictionary of CFrames as an input
    camera.CFrame = info.startingCoord --> Sets the camera in the right place to start
    local tween = TweenService:Create(camera, TWEEN_INFO, {CFrame = info.endCoord})
    tween:Play() --> Have the cutscene play!
end

--> Functions
local function enter(text)
	--> Move the text sequence UI into view!
	mainFrame:TweenPosition(POSITION_ON_SCREEN, "Out", "Back", 0.5)
	textLabel.TextTransparency = 1
	task.wait(0.5)
	--> Let's set the first piece of text onto the screen
	textLabel.Text = text
	for i = 1, 0, -.05 do
		textLabel.TextTransparency = i
		RunService.RenderStepped:Wait() --> This lets us run the loop each frame!
	end
	task.wait(5) --> To let the player read the text!
end

local function changeText(text)
	--> We want to fade the text out, change it, and fade it back in!
	--> Like enter(text), we'll use a for loop to do it
	for i = 0, 1, .05 do
		textLabel.TextTransparency = i
		RunService.RenderStepped:Wait()
	end
	textLabel.Text = text
	for i = 1, 0, -.05 do
		textLabel.TextTransparency = i
		RunService.RenderStepped:Wait()
	end
	task.wait(5) --> Allow some time for the player to read!!
end

local function exit()
	--> We need to fade the text out, and then close the UI
	for i = 0, 1, .05 do
		textLabel.TextTransparency = i
		RunService.RenderStepped:Wait()
	end
	mainFrame:TweenPosition(POSITION_OFF_SCREEN , "In", "Back", .5)
end

local function playSequence(listOfTexts)
	for i, string in pairs(listOfTexts) do
		doCutscene(cutsceneInfo[i]) --> Calls the cutscene!
		if i == 1 then
			enter(string)
		else
			changeText(string)
		end
	end
	exit()
end

prompt.Triggered:Connect(function(playerWhoTriggered)
	if playerWhoTriggered == player then --> Check if we were the ones who triggered the prompt
		--> playSequence will do the job for us! However, we might need to create some dialogue...
		local dialogue = {
			"Welcome to PolyCity, the best city for the best Robloxians, and home to the best that Roblox has to offer! Let's take a tour of it all, shall we?",
			"As you enter the city, you'll be greeted to Jiffy, our signature gas station! It's got the cheapest prices for your gas needs, and a nice store for anything else!",
			"Right next to it, is our beautiful coffee shop, with a great selection of menu items, and space for comfort! You'll feel right at home!",
			"And just outside this corner of town is our newly renovated playground, perfect for kids of all ages, looking to have some fun under the sun!",
			"And to prolong your stay, PolyCity has MANY luxury lodging and suites that'll be sure to give you the experience of a lifetime. Have fun in PolyCity!"
		}
		--> Gotta change the camera type to Scriptable at the start, and back to normal at the end!
		camera.CameraType = Enum.CameraType.Scriptable
		playSequence(dialogue)
		camera.CameraType = Enum.CameraType.Custom
	end
end)

Let’s see how it does :3

The End Result!

We did it! Here’s how it turned out!

Thanks For Reading!

If you have any questions about how the code works, or some other aspect, I’ll try to reply as soon as I can! If you’re interested in other tutorials of mine, I’ve written one about spectates and UI-intros! If you’re particularly interested in manipulating the camera, like shown in this tutorial, I also have an open-source that you could dive into a bit deeper. Until next time! :grinning:

42 Likes

Separate your services from variables and put related variables together so it’s easier to read them. Use TweenService for tweening UI’s position and fading in/out of text, you only used it for camera movement. User ipairs instead of pairs, all your indices are numbers so no need to use pairs. Store the dialogue table outside of the .Triggered event (close to cutsceneInfo would make sense) so it doesn’t get created every time the player triggers it. Other than that, good job, will help a lot of people.

3 Likes