How can I improve this typewriter system?

I have a typewriter module that, well, acts as a typewriter for dialog. I’m basically looking for any kind of improvement to the code to make it cleaner, easier to read, or just a better way of doing things.
By the way, this is my first time using metatables in a module, so please point out any mistakes I’ve made! :grinning:

Here it is in action!

Here is the code:

local TweenService = game:GetService("TweenService")


TypeWriter = {}
TypeWriter.__index = TypeWriter


function TypeWriter.new(name)
	local self = setmetatable({
		Mouse = game.Players.LocalPlayer:GetMouse(),
		Dialog = game.ReplicatedStorage.Stuff.UI.Dialog:Clone(),
	}, TypeWriter)
	self.Dialog.Parent = game.Players.LocalPlayer.PlayerGui
	self.Dialog.Enabled = true
	
	self.TopLine = self.Dialog.BG.Extras.Top
	self.TypeSound = self.Dialog.TypeSound
	self.ClickSound = self.Dialog.ClickSound
	self.TextBox = self.Dialog.BG.TextBox
	self.Continue = self.Dialog.BG.Continue
	self.Typing = false
	self.ContinueTween = nil
	self.Halt = false
	self.Connection = game.Players.LocalPlayer:GetMouse().Button1Down:Connect(function()
		if self.Typing then
			self.Halt = true
		end
	end)
	
	
	TweenService:Create(self.TopLine, TweenInfo.new(1, Enum.EasingStyle.Quad, Enum.EasingDirection.Out), {Size = UDim2.new(0.853, 0, 0, self.TopLine.Size.Y.Offset)}):Play()
	
	
	for i = 0, string.len(name), 1 do
		local ns = string.sub(name, 1, i)
		self.Dialog.BG.ThingName.Text = ns
		wait()
	end

	return self
end

function TypeWriter:Type(text)
	if type(text) == "string" then
		local connection
		local Dialog = self.Dialog
		local TypeSound = self.TypeSound
		local ClickSound = self.ClickSound
		local TextBox = self.TextBox
		local TopLine = self.TopLine
		
		self.Typing = true
		for i = 0, string.len(text), 1 do
			if self.ContinueTween then
				self.ContinueTween:Cancel()
				self.ContinueTween = nil
			end
			if self.Halt then TextBox.Text = text self.Halt = false self.Typing = false break end
			
			local ns = string.sub(text, 1, i)
			TypeSound:Play()
			
			if string.sub(text, i - 1, i - 1) == "," or string.sub(text, i - 1, i - 1) == "." or string.sub(text, i - 1, i - 1) == "!" or string.sub(text, i - 1, i - 1) == "?" or string.sub(text, i - 1, i - 1) == ";" or string.sub(text, i - 1, i - 1) == ":" then
				wait(.3)
			end
			TextBox.Text = ns
			wait(.02)
		end
		
		self.Typing = false
		
		
		delay(2, function()
			if self.Continue.Visible == false then
				if self.Typing == false then
					self.Continue.Visible = true
					self.ContinueTween = TweenService:Create(self.Continue, TweenInfo.new(1, Enum.EasingStyle.Linear, Enum.EasingDirection.InOut, -1, true), {ImageTransparency = 0})
					self.ContinueTween:Play()
				end
			end
		end)

		self.Mouse.Button1Down:Wait()
		
		self.Continue.ImageTransparency = 1
		self.Continue.Visible = false
		if self.ContinueTween then
			self.ContinueTween:Cancel()
		end
		ClickSound:Play()
		TextBox.Text = ""

		
			for i = 0, string.len(Dialog.BG.ThingName.Text), 1 do
				local ns = string.sub(Dialog.BG.ThingName.Text,  i, -1)
				Dialog.BG.ThingName.Text = ns
				wait(.02)
			end
		
		

		for i = 0, string.len(TextBox.Text), 1 do
			
			local ns = string.sub(TextBox.Text, i, -1)
			TextBox.Text = ns
			wait(.02)
		end

		wait(.2)
	elseif type(text) == "table" then
		local connection
		local Dialog = self.Dialog
		local TypeSound = self.TypeSound
		local ClickSound = self.ClickSound
		local TextBox = self.TextBox
		local TopLine = self.TopLine
		
		for ind, v in ipairs(text) do
			self.Typing = true
			for i = 0, string.len(v), 1 do
				if self.ContinueTween then
					self.ContinueTween:Cancel()
					self.ContinueTween = nil
				end
				if self.Halt then TextBox.Text = v self.Halt = false self.Typing = false break end
				local ns = string.sub(v, 1, i)
				TypeSound:Play()
				if string.sub(v, i - 1, i - 1) == "," or string.sub(v, i - 1, i - 1) == "." or string.sub(v, i - 1, i - 1) == "!" or string.sub(v, i - 1, i - 1) == "?" or string.sub(v, i - 1, i - 1) == ";" or string.sub(v, i - 1, i - 1) == ":" then
					wait(.3)
				end
				TextBox.Text = ns
				wait(.02)
			end
			self.Typing = false
			
		
			delay(2, function()
				if self.Continue.Visible ~= true then
					if not self.Typing then
						self.Continue.Visible = true
						self.ContinueTween = TweenService:Create(self.Continue, TweenInfo.new(1, Enum.EasingStyle.Linear, Enum.EasingDirection.InOut, -1, true), {ImageTransparency = 0})
						self.ContinueTween:Play()
					end
				end
			end)
			
			self.Mouse.Button1Down:Wait()
			
			self.Continue.ImageTransparency = 1
			self.Continue.Visible = false
			if self.ContinueTween then
				self.ContinueTween:Cancel()
			end
			
			ClickSound:Play()
			TextBox.Text = ""

			for i = 0, string.len(TextBox.Text), 1 do
				local ns = string.sub(TextBox.Text, i, -1)
				TextBox.Text = ns
				wait(.02)
			end

			wait(.2)
		end
		
	elseif type(text) == "function" then
		text()
	end
end

function TypeWriter:ChangeName(name)
	local Dialog = self.Dialog

	for i = 0, string.len(name), 1 do
		local ns = string.sub(name, 1, i)
		Dialog.BG.ThingName.Text	 = ns
		wait()
	end
end

function TypeWriter:CleanUp(Callback)
	local Dialog = self.Dialog
	local TopLine = self.TopLine
	local OGPos = Dialog.BG.Position
	local OGTrans = Dialog.BG.BackgroundTransparency
	local BGPos = TweenService:Create(Dialog.BG, TweenInfo.new(.8, Enum.EasingStyle.Cubic, Enum.EasingDirection.In), {Position = UDim2.new(0, 1000, 0, 1000)})
	local BGTrans = TweenService:Create(Dialog.BG, TweenInfo.new(.8, Enum.EasingStyle.Cubic, Enum.EasingDirection.In), {Transparency = 1})
	TweenService:Create(TopLine, TweenInfo.new(.5, Enum.EasingStyle.Linear, Enum.EasingDirection.Out), {Size = UDim2.new(0, 0, 0, TopLine.Size.Y.Offset)}):Play()
	BGPos:Play()
	BGTrans:Play()
	if Callback then Callback() end
	BGPos.Completed:Wait()
	BGTrans.Completed:Wait()
	Dialog.Enabled = false
	Dialog.BG.Position = OGPos
	Dialog.BG.BackgroundTransparency = OGTrans
	Dialog:Destroy()
	
	self.Connection:Disconnect()
end

return TypeWriter

Here’s a rbxl file to test it out for yourself.
TypeWriter Module.rbxl (58.4 KB)

1 Like

This looks really good! I only have a few suggestions:

  • Fix the positioning of the TextLabels, as at the current moment they look weirdly organized
  • If this is a dialogue system, maybe add a label that says “Click to continue” or something along that line to help direct the user

I would strongly recommend using TextLabel.MaxVisibleGraphemes for a typewriter effect as it was intended for that purpose.

1 Like

Thank you for your suggestions!
For your first suggestion, I have added an imagelabel that tells the player to click.

For the issue of the textlabels, I think I’ve fixed it? I’m not 100% sure, so if you encounter any more scaling issues please tell me.

I am aware that the option of MaxVisibleGraphemes is available; however, I personally prefer the sub method and I don’t want to rewrite everything.
Thank you for pointing that out though.

I would agree with @Gooncreeper, you aren’t using string.sub() to its advantage, this can easily be swapped out with MaxVisibleGraphemes

But then, string.sub would be a cooler effect depending on the way its designed

Could you further explain what you mean by “you aren’t using string.sub() to its advantage”?

I mean this as in you have the Text set to the Left the Gui, because of that, MaxVisibleGraphemes will look identical compared to it being set in the Middle, the differences are more appearent that way

1 Like

I recently did this with a recursive function and an attribute. There is a text label on the gui which is linked to the attribute changed event. This way it automatically updates.

Here’s some code snippets of the important parts for the typewriter effect:

--++ Local Attirbutes 
local typeingDelay = 0.05 -- delay between characters
local sentenceDelay = 2 -- delay between sentences
local index = 1 -- current index in the message
--++ Public Attributes
DialogModule.DialogText = ReplicatedStorage.Global.DisplayAttributes:WaitForChild("DialogText")
--++ Local Functions

function typeText(message)
	-- Get the current text
	local currentText = DialogModule.DialogText.Value

	-- Add the next character to the current text
	currentText = currentText .. message:sub(index, index)

	-- Update the text of the text label
	DialogModule.DialogText.Value = currentText

	-- Increment the index
	index = index + 1

	-- If we have reached the end of the message, stop the loop
	if index > #message then
		return
	end

	-- Call this function again after a delay
	task.wait(typeingDelay)
	typeText(message)
end
.
.
.
	-- display dialog
	for i, dialog in ipairs(Chat) do	
		-- sound effect 
		...
		-- clear text
		DialogModule.DialogText.Value = ""
		-- set start position for each sentence
		index = 1
		-- reveal text
		typeText(dialog.Text)		
		-- pause between sentences
		task.wait(sentenceDelay)	 
	end
.
.
.
1 Like

Add a quick skip option where the player could skip the typewriting process in case the typewriting process is slower than the reader’s speed (by clicking the box when it is still processing). Not saying some people out there want to skip the story :eyes: but there are times when the player might have lag issues that cause the typewriting process to stutter and releases the text at a slower speed than expected.

1 Like

Thanks for your suggestion. I’ve added a skip feature by clicking. :slightly_smiling_face:

1 Like