I made something like this a while ago as a proof of concept.
This takes a string from a StringValue and types it out letter by letter in a TextLabel, making a click sound from my ui effects library each time. It pauses for a second at each line break. (If you were to use this, remove the lines that start with effects)
local wait = task.wait
local ui = script.Parent
local textLabel = ui.TextLabel
local textData = script.textData
for o in textData:gmatch(".") do
if string.byte(o) == 10 then wait(1) end -- if there is a line break, wait
textLabel.Text = textLabel.Text..o
effects.chatClickSound()
wait(0.05)
end
textLabel.TextStrokeTransparency = 0
effects.tween(textLabel, {TextStrokeTransparency = 1}, "InOut", "Linear", 1)
For the record: If you even plan on using emojis with skin tone modifiers or translating your game into another language like Vietnamese, which has multiple characters in a single grapheme, do not use the second method with string.sub. It does not handle the accents or skin tone of emojis correctly, as shown in this demo file (40.7 KB).
local typewriter = {};
typewriter.__index = typewriter;
local heartbeat = game:GetService("RunService").Heartbeat;
local template = script.Template;
function typewriter.new(container)
local self = setmetatable({
container = container,
typewriting = nil,
interval = 0.2,
_labels = {},
}, typewriter);
return self;
end
function typewriter:typewrite(text)
if (self.typewriting) then
self.typewriting:Disconnect();
end
for _,l in ipairs(self._labels) do
l:Destroy();
end
table.clear(self._labels);
local dummy = template:Clone();
dummy.Text = "";
dummy.Parent = self.container;
local function getScale(text)
dummy.Text = text;
local bounds = dummy.TextBounds;
dummy.Text = "";
return bounds.X / self.container.AbsoluteSize.X, dummy.Size.Y.Scale;
end
local words = text:split(" ");
local currentWord = 1;
local currentLetter = 1;
local currentLine = 1;
local elapsedXScale = 0;
local elapsed = self.interval;
self.typewriting = heartbeat:Connect(function(dt)
elapsed += dt;
if (elapsed <= self.interval) then return; end
if (currentWord == #words and currentLetter == #words[#words] + 1) then
self.typewriting:Disconnect();
self.typewriting = nil;
return;
end
if (currentLetter == #words[currentWord] + 1) then
currentWord += 1;
currentLetter = 0;
local wordSizeX = getScale(words[currentWord]);
if (elapsedXScale + wordSizeX >= 1) then
elapsedXScale = 0;
currentLine += 1;
else
elapsedXScale += getScale(" ");
end
end
local l = template:Clone();
l.Text = words[currentWord]:sub(currentLetter, currentLetter);
local scaleX, scaleY = getScale(l.Text);
l.Size = UDim2.fromScale(scaleX, scaleY);
table.insert(self._labels, l);
l.Parent = self.container;
local goalPosition = UDim2.fromScale(elapsedXScale + scaleX / 2, currentLine * scaleY - scaleY / 2);
l.Position = goalPosition + UDim2.fromScale(0, 0.1);
l:TweenPosition(goalPosition, Enum.EasingDirection.Out, Enum.EasingStyle.Back, 0.25, true);
currentLetter += 1;
elapsedXScale += scaleX;
elapsed = 0;
end);
end
return typewriter;
In no way is this optimized or anything, this is just more of a proof-of-concept that I put together in about 40 minutes.
Module also includes checks for when to go to next line.
Didn’t implement a check to see if the word scale exceeds the container bounds though, but it’s straightforward to implement.
Future me:
Cache the scales of each letter so that I don’t have to update Text of a TextLabel everytime to get the size
When scale function is called for a word add scales of each letter and if special character then calculate it and return
More optimization
EDIT: Place file if anyone wants to take a look Typewriter.rbxl (44.3 KB)
The second method is a way better as it allow us to made lot of different effects without doing texts duplication tricks which decrease game performances.
Thank’s for the tutorial, it is very useful.