2 Method's for a Typewriter Effect

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)
1 Like

I see

I did say that this wasn’t meant to be as advanced:

Its alright tho

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

Allow me to introduce myself :man_bowing:

Code
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.
RobloxStudioBeta_HufDF1bmZD

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 :sparkles: optimization :sparkles:

EDIT: Place file if anyone wants to take a look
Typewriter.rbxl (44.3 KB)

6 Likes

This is fantastic! I really like the way they come up when they appear, that’s seriously awesome1!!1

1 Like

Very late reply, but cool!

1 Like

I never understood the use of MaxVisibleGraphemes either. Always the random properties that are the most useful.

3 Likes

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.
20e2f72dc981c3bcfe97f8ddb897b756


3d36845d6d21430f322918e2f26fae08

1 Like

MaxVisibleGraphemes or string.sub()?

I’m talking about the string.sub() version ^^

1 Like

People are telling me "no, its incorrect to use string.sub()" so idk.

Nah, both are fine as long as you’re using and coding them well… at least i guess.

1 Like