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.
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()
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.
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
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.