Cancellable Typewriter Effect

Learn how to script a simple typewriter effect that's cancellable!

by YasuYoshida

Author Avatar

Scripting yourself a simple typewriter effect is easier than you would think! Through the useage of attributes, the task library, and the utf8 library, you can have a simple typewriter working in no time!

This is the following heiarchy you'll need for the tutorial:

ScreenGui ‎ ‎ ‎ ‎TextLabel ‎‎ ‎ ‎ ‎ ‎ ‎ LocalScript

It doesn't matter how the TextLabel is styled. As long as you can see the text, then you're fine. All of the following code will go into the LocalScript parented underneath the TextLabel.


The Setup

So the first thing we're going to do is setup the code that will run the updateText function whenever the TypewriterText attribute on the TextLabel changes. We'll also call the onCurrentTextChanged function when the script first runs just in case the attribute has been set before the script started running.

local textLabel = script.Parent

local function updateText(newText: string)
	--typewriter effect will go here!
end

local function onCurrentTextChanged()
	updateText(textLabel:GetAttribute("TypewriterText"))
end

textLabel:GetAttributeChangedSignal("TypewriterText"):Connect(onCurrentTextChanged)
onCurrentTextChanged()

The Typewriter Effect

With that out of the way, we can begin putting together the typewriter effect. We'll be focusing on adding code to the updateText function now. To achieve the effect, we're going to be using a property called MaxVisibleGraphemes that all TextLabels have. This property determines how many units of text (graphemes) are visible. If it's set to -1, then all the units of text will be visible.

To determine how many graphemes a string has, we can use utf8.len from the utf8 library. Then with the number of graphemes, we can use a for loop to gradually change the MaxVisibleGraphemes property with a small pause inbetween by using task.wait:

local function updateText(newText: string)	
	textLabel.Text = newText

	local totalGraphemes = utf8.len(newText)
	for i = 1,totalGraphemes do
		textLabel.MaxVisibleGraphemes = i
		task.wait(1/100)
	end
end

To support rich text as well, then instead of using the length of newText, you can use the length of the ContentText property on TextLabels. This property is the current text, but without all the rich text nonsense, allowing us to get the real number of graphemes:

local totalGraphemes = utf8.len(textLabel.ContentText)

And that's almost everything we need for the typewriter effect! It's always good practice to set MaxVisibleGraphemes to -1 after you're done, just in case the TextLabel has AutoLocalize set to true. Doing this will ensure that no matter what, all the graphemes will be visible even after the typewriter effect is done.

Not only that, but before we change the TextLabel's text to the newText, we should set MaxVisibleGraphemes to 0 just to ensure the newText is not visible for one frame.

local function updateText(newText: string)
	textLabel.MaxVisibleGraphemes = 0	
	textLabel.Text = newText

	local totalGraphemes = utf8.len(textLabel.ContentText)
	for i = 1,totalGraphemes do
		textLabel.MaxVisibleGraphemes = i
		task.wait(1/100)
	end

	textLabel.MaxVisibleGraphemes = -1
end

And there's our simple typewriter effect! The only part that's left now is to make it cancellable.

Making It Cancellable

Why would we want it to be cancellable? Here's something to think about. What would happen if the TypewriterText attribute changes while the typewriter loop is already running? You would have two loops running at the same time! When that happens, they will both be trying to set the MaxVisibleGraphemes property, causing a visual bug until one of the loops end.

To fix this, we just need to cancel the previous loop before we run a new one!

How would we "cancel" a loop in this situation though? We'd use the task library that just came out last year! We can put the typewriter loop inside a thread created with task.spawn and cancel it with task.cancel. But if we want to cancel the last created thread, we'll need to keep track of it with a variable called lastUpdateTextThread that's defined outside of the updateText function:

local lastUpdateTextThread = nil
local function updateText(newText: string)
	if lastUpdateTextThread then
		task.cancel(lastUpdateTextThread)
		lastUpdateTextThread = nil
	end
	
	lastUpdateTextThread = task.spawn(function()
		textLabel.MaxVisibleGraphemes = 0
		textLabel.Text = newText
		
		local totalGraphemes = utf8.len(textLabel.ContentText)
		for i = 1,totalGraphemes do
			textLabel.MaxVisibleGraphemes = i
			task.wait(1/100)
		end

		textLabel.MaxVisibleGraphemes = -1
	end)
end

The Final Product

Once we've done everything above, we're left with this code within the LocalScript:

local textLabel = script.Parent

local lastUpdateTextThread = nil
local function updateText(newText: string)
	if lastUpdateTextThread then
		task.cancel(lastUpdateTextThread)
		lastUpdateTextThread = nil
	end

	lastUpdateTextThread = task.spawn(function()
		textLabel.MaxVisibleGraphemes = 0
		textLabel.Text = newText

		local totalGraphemes = utf8.len(textLabel.ContentText)
		for i = 1,totalGraphemes do
			textLabel.MaxVisibleGraphemes = i
			task.wait(1/100)
		end

		textLabel.MaxVisibleGraphemes = -1
	end)
end

local function onCurrentTextChanged()
	updateText(textLabel:GetAttribute("TypewriterText"))
end

textLabel:GetAttributeChangedSignal("TypewriterText"):Connect(onCurrentTextChanged)
onCurrentTextChanged()

As you can see, putting together a simple typewritter effect that's cancellable really isn't that hard! With the help of attributes, the task library, and the utf8 library, we were able to put together a common effect that many games use!

You don't need to use an attribute on the TextLabel though, you can use one on anything! I personally think that it'd be better to have the TypewriterText attribute on the LocalPlayer, but I didn't do it in this tutorial for the sake of simplicity. If you want a more versatile setup, then I'd recommend using a Modulescript that handles the effect.

View in-game to comment, award, and more!