TypeWriter Effect for UI text using Coroutine + new dialog

Hi!! Hello!!
I made a video tutorial on how to create the typewriter effect, where text is displayed letter by letter, like it’s being typed in real time. This effect is perfect for dialogue systems, narration, or adding some cinematic flair to your UI.

The video is split into two parts:

  • Basic Example – a simple demonstration showing how to create the effect step by step.
  • Practical Use – applying the effect in my dialogue system from my earlier forum post.

:wrench: How the effect works

Here’s a quick summary of the approach:
image

  • Inside a StartTypeWriter function, I create a loop that displays the text one letter at a time.
  • A coroutine handles the delay between each character, creating that smooth “typing” animation.

:scroll: Example script

local fieldDefs = {
    {
        name = "UIField",
        label = "UI Field",
        hint = "UI Field",
        description = "UI Field",
        type = "UIPackageFile"
    },
    {
        name = "NarratorText",
        type = "string",
    },
    {
        name = "TypeSpeed",
        type = "float",
    },
    {
        name = "Duration",
        type = "float",
    }
}
script.DefineFields(fieldDefs)
`-- Get the imported UI package field
local UIField = script.fields.UIField
local narratorText = script.fields.NarratorText
local typeSpeed = script.fields.TypeSpeed
local duration = script.fields.Duration

-- Access the YaResourceManager API
local YaResourceManager = YahahaMiddleLayerSlim.Resource.YaResourceManager 

local mainPanel
local packageName
local audioPlayer

local co = require("Utils.CoroutineHelper")

local NarratorTextUI


script.OnStart(function ()
    LoadResource()
    audioPlayer = script.GetLuaComponent("com.yahaha.sdk.audio.components.AudioPlayer")
end)
function LoadResource()
    YaResourceManager.LoadResourceByUIPackageField(UIField, function(state, name)
        if state == AssetStatus.AllAssetCompleted then
            packageName = name

            -- Create the main panel
            CreateMainPanel()
        end
    end)
end

function CreateMainPanel()
    mainPanel = UIPackage.CreateObject(packageName, "Narrator")
    GRoot.inst:SetContentScaleFactor(2436, 1125)
    GRoot.inst:AddChild(mainPanel)
    mainPanel.size = GRoot.inst.size
    mainPanel:AddRelation(GRoot.inst, RelationType.Size)
    NarratorTextUI = mainPanel:GetChild("Text")
    StartTypeWritter()
end

function StartTypeWriter()
    NarratorTextUI.text = ""
    co.async(function()
        for i = 1, #narratorText do
            NarratorTextUI.text = string.sub(narratorText, 1, i)
            if audioPlayer then
                audioPlayer:Play()
            end
            co.wait(typeSpeed)
        end
        co.wait(duration)
        mainPanel:Dispose() -- Dispose the panel after the duration
    end)
end
script.OnDispose(function ()
    -- Destroy the panel
    mainPanel:Dispose()  
    -- Unload the UIField resource
    YaResourceManager.RemoveResourceByUIPackageField(UIField)
end)`

:scroll: Dialog Script

local fieldDefs = {
    {
        name = "UIField",
        label = "UI Field",
        hint = "UI Field",
        description = "UI Field",
        type = "UIPackageFile"
    },
    {
        name = "EndTrigger",
        type = "GameObject"
    },
    {
        name = "CharacterName",
        type = "string"
    },
    {
        name = "typeSpeed",
        type = "float",
        default = 0.03,
    },
    {
        name = "DialogList",
        type = {
            type = "list",
            items = {
                name = "Dialog",
                type = "string",
            }
        }
        
    }
}
script.DefineFields(fieldDefs)
local UIField = script.fields.UIField
local EndTrigger = script.fields.EndTrigger
local CharacterName = script.fields.CharacterName
local DialogList = script.fields.DialogList

local YaResourceManager = YahahaMiddleLayerSlim.Resource.YaResourceManager 

local mainPanel
local packageName
local Self = script.gameObject

local dialogText
local characterNameText
local currentDialog = 1
local audioPlayer

local typing = false
local typeSpeed = script.fields.typeSpeed -- seconds between characters
local fullText = ""
local co = require("Utils.CoroutineHelper")

script.OnStart(function ()
    LoadResource()
    audioPlayer = script.GetLuaComponent("com.yahaha.sdk.audio.components.AudioPlayer")
end)

-- Function to load resources
function LoadResource()
    YaResourceManager.LoadResourceByUIPackageField(UIField, function(state, name)
        if state == AssetStatus.AllAssetCompleted then
            packageName = name
            CreateMainPanel()
        end
    end)
end

function CreateMainPanel()
    mainPanel = UIPackage.CreateObject(packageName, "DialogBox")
    GRoot.inst:SetContentScaleFactor(2436, 1125)
    GRoot.inst:AddChild(mainPanel)
    mainPanel.size = GRoot.inst.size
    mainPanel:AddRelation(GRoot.inst, RelationType.Size)
    dialogText = mainPanel:GetChild("DialogText")
    characterNameText = mainPanel:GetChild("CharacterNameText")
    characterNameText.text = CharacterName

    StartTypewriter(DialogList[currentDialog])
end

function StartTypewriter(text)
    typing = true
    fullText = text
    dialogText.text = ""

    co.async(function()
        for i = 1, #text do
            if not typing then
                return -- Cancel typing
            end
            if audioPlayer then
                audioPlayer:Play()
            end
            dialogText.text = string.sub(text, 1, i)
            co.wait(typeSpeed)
        end
        typing = false
    end)
end

local Input = UnityEngine.Input
local KeyCode = UnityEngine.KeyCode

script.OnUpdate(function ()
    if mainPanel == nil then
        return -- Prevent interaction until UI is fully loaded
    end
    
    if Input.GetKeyDown(KeyCode.E) then
        if typing then
            typing = false -- Stop the async loop
            dialogText.text = fullText -- Show full line
        else
            currentDialog = currentDialog + 1
            if currentDialog > #DialogList then
                HandleEndDialog()
            else
                StartTypewriter(DialogList[currentDialog])
            end
            
        end
    end
end)

function HandleEndDialog()
    mainPanel:Dispose()
    Self:SetActive(false)
    YaResourceManager.RemoveResourceByUIPackageField(UIField)
    EndTrigger:SetActive(true)
end