Roblox GUI Maker logoRoblox GUI Maker
Unofficial third-party tool · Not affiliated with Roblox Corporation

Dialogue System Roblox GUI Template with NPC Chat & Choices

An NPC dialogue UI with typewriter text, speaker portrait placeholder, and branching choice buttons. Designed for story-driven Roblox experiences with a charming cartoon aesthetic. Features smooth typewriter text animation, animated NPC portraits, player choice branching, and full dialogue tree support for complex narrative flows.

DialogueMarketplace· Open in editor for free during beta
Dialogue System Roblox GUI template — NPC Chat & Choices, cartoon dialogue layout, screenshot 1
Dialogue System Roblox GUI template — screenshot 1 of 1, showing npc chat & choices on a cartoon dialogue layout for desktop.
Open in Web EditorChecking access…

Live preview

Dialogue System

1920×1080
Zoom100%
Desktop (FHD)
ScreenGui
Frame
Frame
NPC Bob
Welcome to the village! What would you like to do?
Hello!
Do you have quests?
Goodbye.
An NPC dialogue UI with typewriter text, speaker portrait placeholder, and branching choice buttons. Designed for story-driven Roblox experiences with a charming cartoon aesthetic. Features smooth typewriter text animation, animated NPC portraits, player choice branching, and full dialogue tree support for complex narrative flows.

Includes real, production-ready Luau logic — not a static mockup.

What's inside

How to use this template

The Dialogue System template shows NPC dialogue with typewriter text, speaker portrait, and branching choices. Paste the client Luau into StarterGui and a dialogue box appears at the bottom of the screen. The text appears one character at a time using a RunService loop, and players can click to skip the typewriter and reveal the full line. When the line finishes, branching choice buttons appear below the dialogue box. The layout uses Scale-based sizing, so the dialogue box fits any screen. To customize, replace the placeholder dialogue tree with your own lines and choices. The typewriter speed is configurable. Wire the choice clicks to your quest or story logic. Test in Studio by advancing through the dialogue and confirming the choices appear at the right time.

Add it to your game in 3 steps

  1. 1

    Copy the Luau

    Open the template in the web editor and click Export. Choose LocalScript to copy the full client Luau to your clipboard.

  2. 2

    Paste into StarterGui

    In Roblox Studio, right-click StarterGui in the Explorer, insert a LocalScript, and paste the copied Luau. Rename it to match the template.

  3. 3

    Press Play to verify

    Click Play in Studio. The GUI should appear exactly as designed. Tweak any Size or Position values in the script if you want to adjust the layout.

dialogue-system.lua
lua
1--[[
2 Dialogue System - Client LocalScript
3 An NPC dialogue UI with typewriter text, speaker portrait, and branching choices.
4 Place in: StarterPlayer > StarterPlayerScripts
5--]]
6
7local Players = game:GetService("Players")
8local TweenService = game:GetService("TweenService")
9local UserInputService = game:GetService("UserInputService")
10local ReplicatedStorage = game:GetService("ReplicatedStorage")
11
12local player = Players.LocalPlayer
13local playerGui = player:WaitForChild("PlayerGui")
14
15-- ============================================================
16-- REMOTE EVENTS
17-- ============================================================
18local remotes = ReplicatedStorage:WaitForChild("Dialogue_System_Remotes")
19local startDialogueEvent = remotes:WaitForChild("StartDialogue")
20local advanceDialogueEvent = remotes:WaitForChild("AdvanceDialogue")
21local selectChoiceEvent = remotes:WaitForChild("SelectChoice")
22local endDialogueEvent = remotes:WaitForChild("EndDialogue")
23local requestDialogueEvent = remotes:WaitForChild("RequestDialogue")
24local portraitUpdateEvent = remotes:WaitForChild("PortraitUpdate")
25local emotionUpdateEvent = remotes:WaitForChild("EmotionUpdate")
26
27-- ============================================================
28-- CONFIGURATION
29-- ============================================================
30local CONFIG = {
31 -- Colors - warm cartoon RPG palette
32 BG_PRIMARY = Color3.fromRGB(45, 40, 55),
33 BG_SECONDARY = Color3.fromRGB(60, 55, 75),
34 BG_ACCENT = Color3.fromRGB(80, 75, 100),
35 TEXT_WHITE = Color3.fromRGB(255, 250, 240),
36 TEXT_GRAY = Color3.fromRGB(180, 175, 170),
37 TEXT_YELLOW = Color3.fromRGB(255, 220, 100),
38 NAME_TAG_BG = Color3.fromRGB(120, 80, 180),
39 CHOICE_BG = Color3.fromRGB(70, 65, 90),
40 CHOICE_HOVER = Color3.fromRGB(100, 95, 130),
41 CHOICE_SELECTED = Color3.fromRGB(140, 130, 180),
42 PORTRAIT_BORDER = Color3.fromRGB(160, 140, 100),
43 PORTRAIT_BG = Color3.fromRGB(35, 30, 45),
44 CONTINUE_GLOW = Color3.fromRGB(200, 180, 100),
45 OVERLAY_COLOR = Color3.fromRGB(0, 0, 0),
46
47 -- Animation
48 TWEEN_FAST = TweenInfo.new(0.15, Enum.EasingStyle.Quad, Enum.EasingDirection.Out),
49 TWEEN_NORMAL = TweenInfo.new(0.3, Enum.EasingStyle.Quad, Enum.EasingDirection.Out),
50 TWEEN_SLOW = TweenInfo.new(0.5, Enum.EasingStyle.Quad, Enum.EasingDirection.Out),
51 TWEEN_BOUNCE = TweenInfo.new(0.5, Enum.EasingStyle.Back, Enum.EasingDirection.Out),
52 TYPEWRITER_SPEED = 0.035, -- seconds per character
53 CHOICE_STAGGER = 0.08,
54 CONTINUE_BLINK_SPEED = 0.6,
55
56 -- Layout
57 DIALOGUE_WIDTH = 700,
58 DIALOGUE_HEIGHT = 200,
59 PORTRAIT_SIZE = 140,
60 CHOICE_HEIGHT = 40,
61 NAME_TAG_HEIGHT = 32,
62}
63
64-- ============================================================
65-- STATE
66-- ============================================================
67local state = {
68 isActive = false,
69 currentText = "",
70 targetText = "",
71 typewriterTimer = nil,
72 canContinue = false,
73 currentSpeaker = "",
74 currentEmotion = "neutral",
75 choices = {},
76 dialogueHistory = {},
77 historyOpen = false,
78 currentPortraitId = "",
79}
80
81-- ============================================================
82-- HELPER FUNCTIONS
83-- ============================================================
84
85local function createInstance(className, props)
86 local instance = Instance.new(className)
87 for key, value in pairs(props) do
88 instance[key] = value
89 end
90 return instance
91end
92
93local function createCorner(parent, radius)
94 local corner = Instance.new("UICorner")
95 corner.CornerRadius = UDim.new(0, radius or 8)
96 corner.Parent = parent
97 return corner
98end
99
100local function createStroke(parent, color, thickness)
101 local stroke = Instance.new("UIStroke")
102 stroke.Color = color or CONFIG.PORTRAIT_BORDER
103 stroke.Thickness = thickness or 2
104 stroke.Parent = parent
105 return stroke
106end
107
108local function createGradient(parent, color1, color2, rotation)
109 local gradient = Instance.new("UIGradient")
110 gradient.Color = ColorSequence.new({
111 ColorSequenceKeypoint.new(0, color1 or CONFIG.BG_PRIMARY),
112 ColorSequenceKeypoint.new(1, color2 or CONFIG.BG_SECONDARY),
113 })
114 gradient.Rotation = rotation or 0
115 gradient.Parent = parent
116 return gradient
117end
118
119-- ============================================================
120-- UI CONSTRUCTION
121-- ============================================================
122
123local screenGui = createInstance("ScreenGui", {
124 Name = "Dialogue_System",
125 ResetOnSpawn = false,
126 ZIndexBehavior = Enum.ZIndexBehavior.Sibling,
127 Parent = playerGui,
128})
129
130-- --------------------------------------------------------
131-- DARK OVERLAY
132-- --------------------------------------------------------
133local darkOverlay = createInstance("Frame", {
134 Name = "DarkOverlay",
135 Size = UDim2.fromScale(1, 1),
136 BackgroundColor3 = CONFIG.OVERLAY_COLOR,
137 BackgroundTransparency = 1,
138 BorderSizePixel = 0,
139 ZIndex = 9,
140 Parent = screenGui,
141 Visible = false,
142})
143
144-- --------------------------------------------------------
145-- DIALOGUE CONTAINER
146-- --------------------------------------------------------
147local dialogueContainer = createInstance("Frame", {
148 Name = "DialogueContainer",
149 Size = UDim2.fromOffset(CONFIG.DIALOGUE_WIDTH, CONFIG.DIALOGUE_HEIGHT),
150 Position = UDim2.new(0.5, 0, 1, -60),
151 AnchorPoint = Vector2.new(0.5, 1),
152 BackgroundTransparency = 1,
153 ZIndex = 10,
154 Parent = screenGui,
155 Visible = false,
156})
157
158-- --------------------------------------------------------
159-- PORTRAIT CONTAINER
160-- --------------------------------------------------------
161local portraitContainer = createInstance("Frame", {
162 Name = "PortraitContainer",
163 Size = UDim2.fromOffset(CONFIG.PORTRAIT_SIZE, CONFIG.PORTRAIT_SIZE),
164 Position = UDim2.fromOffset(-20, -30),
165 BackgroundTransparency = 1,
166 ZIndex = 11,
167 Parent = dialogueContainer,
168})
169
170local portraitBg = createInstance("Frame", {
171 Name = "PortraitBg",
172 Size = UDim2.fromScale(1, 1),
173 BackgroundColor3 = CONFIG.PORTRAIT_BG,
174 BorderSizePixel = 0,
175 ZIndex = 11,
176 Parent = portraitContainer,
177})
178createCorner(portraitBg, 16)
179createStroke(portraitBg, CONFIG.PORTRAIT_BORDER, 3)
180
181-- Inner glow
182local portraitGlow = createInstance("Frame", {
183 Name = "PortraitGlow",
184 Size = UDim2.new(1, -6, 1, -6),
185 Position = UDim2.fromScale(0.5, 0.5),
186 AnchorPoint = Vector2.new(0.5, 0.5),
187 BackgroundColor3 = CONFIG.BG_SECONDARY,
188 BorderSizePixel = 0,
189 ZIndex = 12,
190 Parent = portraitBg,
191})
192createCorner(portraitGlow, 13)
193
194local portraitImage = createInstance("ImageLabel", {
195 Name = "PortraitImage",
196 Size = UDim2.new(1, -12, 1, -12),
197 Position = UDim2.fromScale(0.5, 0.5),
198 AnchorPoint = Vector2.new(0.5, 0.5),
199 BackgroundTransparency = 1,
200 Image = "", -- Set dynamically via portrait update
201 ZIndex = 13,
202 Parent = portraitBg,
203})
204createCorner(portraitImage, 10)
205
206-- Placeholder when no image
207local portraitPlaceholder = createInstance("TextLabel", {
208 Name = "PortraitPlaceholder",
209 Size = UDim2.fromScale(1, 1),
210 BackgroundTransparency = 1,
211 Text = "NPC",
212 TextColor3 = CONFIG.TEXT_GRAY,
213 TextSize = 36,
214 Font = Enum.Font.GothamBlack,
215 ZIndex = 13,
216 Parent = portraitImage,
217})
218
219-- Speaker name below portrait
220local speakerName = createInstance("TextLabel", {
221 Name = "SpeakerName",
222 Size = UDim2.new(1, 0, 0, 20),
223 Position = UDim2.fromOffset(0, CONFIG.PORTRAIT_SIZE + 4),
224 BackgroundTransparency = 1,
225 Text = "",
226 TextColor3 = CONFIG.TEXT_WHITE,
227 TextSize = 12,
228 Font = Enum.Font.GothamBold,
229 TextXAlignment = Enum.TextXAlignment.Center,
230 ZIndex = 11,
231 Parent = portraitContainer,
232})
233
234-- Emotion indicator
235local emotionIndicator = createInstance("Frame", {
236 Name = "EmotionIndicator",
237 Size = UDim2.fromOffset(24, 24),
238 Position = UDim2.new(1, -10, 0, -5),
239 AnchorPoint = Vector2.new(1, 0),
240 BackgroundColor3 = CONFIG.CONTINUE_GLOW,
241 BorderSizePixel = 0,
242 ZIndex = 14,
243 Parent = portraitContainer,
244 Visible = false,
245})
246createCorner(emotionIndicator, 12)
247
248local emotionText = createInstance("TextLabel", {
249 Name = "EmotionText",
250 Size = UDim2.fromScale(1, 1),
251 BackgroundTransparency = 1,
252 Text = "N",
253 TextColor3 = CONFIG.BG_PRIMARY,
254 TextSize = 14,
255 Font = Enum.Font.GothamBlack,
256 ZIndex = 15,
257 Parent = emotionIndicator,
258})
259
260-- Portrait entry animation offset
261portraitContainer.Position = UDim2.fromOffset(-200, -30)
262
263-- --------------------------------------------------------
264-- DIALOGUE BOX (Text area)
265-- --------------------------------------------------------
266local dialogueBox = createInstance("Frame", {
267 Name = "DialogueBox",
268 Size = UDim2.new(1, -CONFIG.PORTRAIT_SIZE + 10, 1, 0),
269 Position = UDim2.fromOffset(CONFIG.PORTRAIT_SIZE - 10, 0),
270 BackgroundTransparency = 1,
271 ZIndex = 10,
272 Parent = dialogueContainer,
273})
274
275local dialogueBg = createInstance("Frame", {
276 Name = "DialogueBg",
277 Size = UDim2.fromScale(1, 1),
278 BackgroundColor3 = CONFIG.BG_PRIMARY,
279 BackgroundTransparency = 0.15,
280 BorderSizePixel = 0,
281 ZIndex = 10,
282 Parent = dialogueBox,
283})
284createCorner(dialogueBg, 14)
285createStroke(dialogueBg, CONFIG.BG_ACCENT, 2)
286createGradient(dialogueBg, CONFIG.BG_PRIMARY, CONFIG.BG_SECONDARY, 180)
287
288local dialogueText = createInstance("TextLabel", {
289 Name = "DialogueText",
290 Size = UDim2.new(1, -32, 1, -50),
291 Position = UDim2.fromOffset(16, 16),
292 BackgroundTransparency = 1,
293 Text = "",
294 TextColor3 = CONFIG.TEXT_WHITE,
295 TextSize = 18,
296 Font = Enum.Font.GothamBold,
297 TextWrapped = true,
298 TextXAlignment = Enum.TextXAlignment.Left,
299 TextYAlignment = Enum.TextYAlignment.Top,
300 LineHeight = 1.4,
301 ZIndex = 11,
302 Parent = dialogueBg,
303})
304
305-- Continue hint (blinking)
306local continueHint = createInstance("TextLabel", {
307 Name = "ContinueHint",
308 Size = UDim2.fromOffset(160, 20),
309 Position = UDim2.new(1, -170, 1, -28),
310 BackgroundTransparency = 1,
311 Text = "Click to continue >>",
312 TextColor3 = CONFIG.TEXT_GRAY,
313 TextSize = 11,
314 Font = Enum.Font.GothamBold,
315 TextXAlignment = Enum.TextXAlignment.Right,
316 ZIndex = 11,
317 Parent = dialogueBg,
318 Visible = false,
319})
320
321-- Typing cursor
322local typingCursor = createInstance("Frame", {
323 Name = "TypingCursor",
324 Size = UDim2.fromOffset(2, 18),
325 BackgroundColor3 = CONFIG.TEXT_WHITE,
326 BorderSizePixel = 0,
327 ZIndex = 12,
328 Parent = dialogueBg,
329 Visible = false,
330})
331
332-- --------------------------------------------------------
333-- NAME TAG
334-- --------------------------------------------------------
335local nameTag = createInstance("Frame", {
336 Name = "NameTag",
337 Size = UDim2.fromOffset(140, CONFIG.NAME_TAG_HEIGHT),
338 Position = UDim2.fromOffset(CONFIG.PORTRAIT_SIZE - 30, -CONFIG.NAME_TAG_HEIGHT + 10),
339 BackgroundTransparency = 1,
340 ZIndex = 12,
341 Parent = dialogueContainer,
342})
343
344local nameTagBg = createInstance("Frame", {
345 Name = "NameTagBg",
346 Size = UDim2.fromScale(1, 1),
347 BackgroundColor3 = CONFIG.NAME_TAG_BG,
348 BorderSizePixel = 0,
349 ZIndex = 12,
350 Parent = nameTag,
351})
352createCorner(nameTagBg, 8)
353createGradient(nameTagBg, CONFIG.NAME_TAG_BG, Color3.fromRGB(100, 60, 160), 0)
354
355local nameTagText = createInstance("TextLabel", {
356 Name = "NameTagText",
357 Size = UDim2.fromScale(1, 1),
358 BackgroundTransparency = 1,
359 Text = "",
360 TextColor3 = CONFIG.TEXT_WHITE,
361 TextSize = 14,
362 Font = Enum.Font.GothamBlack,
363 ZIndex = 13,
364 Parent = nameTagBg,
365})
366
367-- --------------------------------------------------------
368-- CHOICES CONTAINER
369-- --------------------------------------------------------
370local choicesContainer = createInstance("Frame", {
371 Name = "ChoicesContainer",
372 Size = UDim2.new(1, -CONFIG.PORTRAIT_SIZE + 10, 0, 0),
373 Position = UDim2.fromOffset(CONFIG.PORTRAIT_SIZE - 10, CONFIG.DIALOGUE_HEIGHT + 8),
374 BackgroundTransparency = 1,
375 ZIndex = 11,
376 Parent = dialogueContainer,
377 Visible = false,
378 AutomaticSize = Enum.AutomaticSize.Y,
379})
380
381local choicesLayout = createInstance("UIListLayout", {
382 SortOrder = Enum.SortOrder.LayoutOrder,
383 VerticalAlignment = Enum.VerticalAlignment.Top,
384 HorizontalAlignment = Enum.HorizontalAlignment.Right,
385 Padding = UDim.new(0, 6),
386 Parent = choicesContainer,
387})
388
389-- Choice button template (created dynamically)
390local choiceButtonTemplate = createInstance("TextButton", {
391 Name = "ChoiceButton",
392 Size = UDim2.new(1, 0, 0, CONFIG.CHOICE_HEIGHT),
393 BackgroundColor3 = CONFIG.CHOICE_BG,
394 Text = "",
395 TextColor3 = CONFIG.TEXT_WHITE,
396 TextSize = 15,
397 Font = Enum.Font.GothamBold,
398 BorderSizePixel = 0,
399 ZIndex = 11,
400 Visible = false,
401})
402createCorner(choiceButtonTemplate, 8)
403createStroke(choiceButtonTemplate, CONFIG.BG_ACCENT, 1)
404
405-- Store template
406choiceButtonTemplate.Parent = screenGui
407
408-- --------------------------------------------------------
409-- SKIP BUTTON
410-- --------------------------------------------------------
411local skipButton = createInstance("ImageButton", {
412 Name = "SkipButton",
413 Size = UDim2.fromOffset(80, 28),
414 Position = UDim2.new(1, -10, 0, -36),
415 AnchorPoint = Vector2.new(1, 0),
416 BackgroundColor3 = CONFIG.BG_SECONDARY,
417 BackgroundTransparency = 0.3,
418 BorderSizePixel = 0,
419 ZIndex = 12,
420 Parent = dialogueContainer,
421})
422createCorner(skipButton, 6)
423createStroke(skipButton, CONFIG.BG_ACCENT, 1)
424
425local skipText = createInstance("TextLabel", {
426 Name = "SkipText",
427 Size = UDim2.fromScale(1, 1),
428 BackgroundTransparency = 1,
429 Text = "Skip >>",
430 TextColor3 = CONFIG.TEXT_GRAY,
431 TextSize = 11,
432 Font = Enum.Font.GothamBold,
433 ZIndex = 13,
434 Parent = skipButton,
435})
436
437-- Hover effects
438skipButton.MouseEnter:Connect(function()
439 TweenService:Create(skipButton, CONFIG.TWEEN_FAST, { BackgroundTransparency = 0.1 }):Play()
440 TweenService:Create(skipText, CONFIG.TWEEN_FAST, { TextColor3 = CONFIG.TEXT_WHITE }):Play()
441end)
442
443skipButton.MouseLeave:Connect(function()
444 TweenService:Create(skipButton, CONFIG.TWEEN_FAST, { BackgroundTransparency = 0.3 }):Play()
445 TweenService:Create(skipText, CONFIG.TWEEN_FAST, { TextColor3 = CONFIG.TEXT_GRAY }):Play()
446end)
447
448-- --------------------------------------------------------
449-- DIALOGUE HISTORY
450-- --------------------------------------------------------
451local historyPanel = createInstance("Frame", {
452 Name = "DialogueHistory",
453 Size = UDim2.fromOffset(350, 300),
454 Position = UDim2.new(1, -370, 0, 20),
455 BackgroundColor3 = CONFIG.BG_PRIMARY,
456 BackgroundTransparency = 0.1,
457 BorderSizePixel = 0,
458 ZIndex = 15,
459 Parent = dialogueContainer,
460 Visible = false,
461})
462createCorner(historyPanel, 12)
463createStroke(historyPanel, CONFIG.BG_ACCENT, 2)
464
465local historyTitle = createInstance("TextLabel", {
466 Name = "HistoryTitle",
467 Size = UDim2.new(1, 0, 0, 30),
468 BackgroundTransparency = 1,
469 Text = "Conversation History",
470 TextColor3 = CONFIG.TEXT_YELLOW,
471 TextSize = 14,
472 Font = Enum.Font.GothamBlack,
473 ZIndex = 16,
474 Parent = historyPanel,
475})
476
477local historyScrolling = createInstance("ScrollingFrame", {
478 Name = "HistoryScrolling",
479 Size = UDim2.new(1, -16, 1, -60),
480 Position = UDim2.fromOffset(8, 35),
481 BackgroundTransparency = 1,
482 ScrollBarThickness = 4,
483 ScrollBarImageColor3 = CONFIG.BG_ACCENT,
484 ZIndex = 16,
485 Parent = historyPanel,
486})
487
488local historyLayout = createInstance("UIListLayout", {
489 SortOrder = Enum.SortOrder.LayoutOrder,
490 Padding = UDim.new(0, 4),
491 Parent = historyScrolling,
492})
493
494local closeHistoryButton = createInstance("TextButton", {
495 Name = "CloseHistory",
496 Size = UDim2.fromOffset(60, 24),
497 Position = UDim2.new(1, -10, 0, 4),
498 AnchorPoint = Vector2.new(1, 0),
499 BackgroundColor3 = CONFIG.CHOICE_BG,
500 Text = "Close",
501 TextColor3 = CONFIG.TEXT_WHITE,
502 TextSize = 11,
503 Font = Enum.Font.GothamBold,
504 BorderSizePixel = 0,
505 ZIndex = 17,
506 Parent = historyPanel,
507})
508createCorner(closeHistoryButton, 4)
509
510-- ============================================================
511-- TYPEWRITER SYSTEM
512-- ============================================================
513
514local typewriterConnection = nil
515local continueBlinkConnection = nil
516
517local function startTypewriter(text)
518 state.currentText = ""
519 state.targetText = text
520 state.canContinue = false
521 continueHint.Visible = false
522 typingCursor.Visible = true
523 dialogueText.Text = ""
524
525 -- Cancel existing typewriter
526 if typewriterConnection then
527 typewriterConnection:Disconnect()
528 typewriterConnection = nil
529 end
530
531 local charIndex = 0
532 local totalChars = #text
533
534 typewriterConnection = game:GetService("RunService").Heartbeat:Connect(function()
535 charIndex += 1
536 if charIndex > totalChars then
537 -- Typewriter complete
538 if typewriterConnection then
539 typewriterConnection:Disconnect()
540 typewriterConnection = nil
541 end
542 state.currentText = text
543 state.canContinue = true
544 dialogueText.Text = text
545 continueHint.Visible = true
546 typingCursor.Visible = false
547
548 -- Pulse the continue hint
549 if continueBlinkConnection then
550 continueBlinkConnection:Disconnect()
551 end
552 local visible = true
553 continueBlinkConnection = game:GetService("RunService").Heartbeat:Connect(function()
554 -- Blink every CONTINUE_BLINK_SPEED seconds
555 end)
556 -- Use a simpler task-based blink
557 task.spawn(function()
558 while state.canContinue do
559 task.wait(CONFIG.CONTINUE_BLINK_SPEED)
560 if continueHint and continueHint.Parent then
561 continueHint.TextTransparency = continueHint.TextTransparency == 0 and 0.5 or 0
562 end
563 end
564 end)
565 return
566 end
567
568 -- Add next character
569 state.currentText = string.sub(text, 1, charIndex)
570 dialogueText.Text = state.currentText
571
572 -- Play typewriter sound every few characters
573 if charIndex % 2 == 0 then
574 -- Sound effect trigger would go here
575 end
576 end)
577end
578
579local function skipTypewriter()
580 if typewriterConnection then
581 typewriterConnection:Disconnect()
582 typewriterConnection = nil
583 end
584 state.currentText = state.targetText
585 state.canContinue = true
586 dialogueText.Text = state.targetText
587 continueHint.Visible = true
588 typingCursor.Visible = false
589end
590
591-- ============================================================
592-- CHOICE MANAGEMENT
593-- ============================================================
594
595local choiceButtons = {}
596
597local function clearChoices()
598 for _, btn in ipairs(choiceButtons) do
599 btn:Destroy()
600 end
601 choiceButtons = {}
602 choicesContainer.Visible = false
603end
604
605local function showChoices(choices)
606 clearChoices()
607 state.choices = choices
608 choicesContainer.Visible = true
609
610 for i, choice in ipairs(choices) do
611 local btn = createInstance("TextButton", {
612 Name = "ChoiceButton_" .. i,
613 Size = UDim2.new(1, 0, 0, CONFIG.CHOICE_HEIGHT),
614 BackgroundColor3 = CONFIG.CHOICE_BG,
615 Text = choice.text,
616 TextColor3 = CONFIG.TEXT_WHITE,
617 TextSize = 15,
618 Font = Enum.Font.GothamBold,
619 BorderSizePixel = 0,
620 LayoutOrder = i,
621 ZIndex = 11,
622 Parent = choicesContainer,
623 ClipsDescendants = true,
624 })
625 createCorner(btn, 8)
626 createStroke(btn, CONFIG.BG_ACCENT, 1)
627
628 -- Hover effects
629 btn.MouseEnter:Connect(function()
630 TweenService:Create(btn, CONFIG.TWEEN_FAST, {
631 BackgroundColor3 = CONFIG.CHOICE_HOVER,
632 Position = UDim2.new(0, 8, 0, btn.Position.Y.Offset),
633 }):Play()
634 end)
635
636 btn.MouseLeave:Connect(function()
637 TweenService:Create(btn, CONFIG.TWEEN_FAST, {
638 BackgroundColor3 = CONFIG.CHOICE_BG,
639 Position = UDim2.new(0, 0, 0, btn.Position.Y.Offset),
640 }):Play()
641 end)
642
643 -- Click handler
644 btn.Activated:Connect(function()
645 -- Animate selection
646 TweenService:Create(btn, CONFIG.TWEEN_FAST, {
647 BackgroundColor3 = CONFIG.CHOICE_SELECTED,
648 Size = UDim2.new(0.95, 0, 0, CONFIG.CHOICE_HEIGHT + 4),
649 }):Play()
650
651 task.delay(0.2, function()
652 selectChoiceEvent:FireServer(choice.id)
653 clearChoices()
654 end)
655 end)
656
657 -- Entry animation
658 btn.BackgroundTransparency = 1
659 btn.TextTransparency = 1
660 TweenService:Create(btn, TweenInfo.new(0.2, Enum.EasingStyle.Back, Enum.EasingDirection.Out, 0, false, (i - 1) * CONFIG.CHOICE_STAGGER), {
661 BackgroundTransparency = 0,
662 TextTransparency = 0,
663 }):Play()
664
665 table.insert(choiceButtons, btn)
666 end
667end
668
669-- ============================================================
670-- PORTRAIT MANAGEMENT
671-- ============================================================
672
673local function updatePortrait(portraitId, speakerName_text)
674 state.currentPortraitId = portraitId
675 state.currentSpeaker = speakerName_text
676
677 -- Animate portrait change
678 TweenService:Create(portraitImage, CONFIG.TWEEN_FAST, {
679 ImageTransparency = 1,
680 }):Play()
681
682 task.delay(0.15, function()
683 portraitImage.Image = portraitId ~= "" and portraitId or ""
684 portraitPlaceholder.Visible = portraitId == ""
685
686 TweenService:Create(portraitImage, CONFIG.TWEEN_FAST, {
687 ImageTransparency = 0,
688 }):Play()
689 end)
690
691 -- Update name
692 nameTagText.Text = speakerName_text
693 speakerName.Text = speakerName_text
694
695 -- Animate name tag
696 nameTagBg.Size = UDim2.fromOffset(20, CONFIG.NAME_TAG_HEIGHT)
697 TweenService:Create(nameTagBg, CONFIG.TWEEN_BOUNCE, {
698 Size = UDim2.fromOffset(math.max(140, #speakerName_text * 10), CONFIG.NAME_TAG_HEIGHT),
699 }):Play()
700end
701
702local function updateEmotion(emotion)
703 state.currentEmotion = emotion
704 emotionIndicator.Visible = emotion ~= "neutral"
705
706 local emojiMap = {
707 happy = "H",
708 sad = "S",
709 angry = "A",
710 surprised = "!",
711 thinking = "?",
712 neutral = "N",
713 }
714
715 emotionText.Text = emojiMap[emotion] or "N"
716
717 local colorMap = {
718 happy = Color3.fromRGB(255, 220, 80),
719 sad = Color3.fromRGB(100, 150, 255),
720 angry = Color3.fromRGB(255, 80, 80),
721 surprised = Color3.fromRGB(255, 180, 80),
722 thinking = Color3.fromRGB(180, 120, 255),
723 neutral = CONFIG.CONTINUE_GLOW,
724 }
725
726 emotionIndicator.BackgroundColor3 = colorMap[emotion] or CONFIG.CONTINUE_GLOW
727
728 -- Bounce animation
729 TweenService:Create(emotionIndicator, CONFIG.TWEEN_BOUNCE, {
730 Size = UDim2.fromOffset(28, 28),
731 }):Play()
732 task.delay(0.2, function()
733 TweenService:Create(emotionIndicator, CONFIG.TWEEN_FAST, {
734 Size = UDim2.fromOffset(24, 24),
735 }):Play()
736 end)
737end
738
739-- ============================================================
740-- DIALOGUE HISTORY
741-- ============================================================
742
743local function addToHistory(speaker, text)
744 local item = createInstance("TextLabel", {
745 Name = "HistoryItem",
746 Size = UDim2.new(1, 0, 0, 36),
747 BackgroundTransparency = 1,
748 Text = string.format("<b>%s:</b> %s", speaker, text),
749 TextColor3 = CONFIG.TEXT_WHITE,
750 TextSize = 12,
751 Font = Enum.Font.GothamBold,
752 TextWrapped = true,
753 TextXAlignment = Enum.TextXAlignment.Left,
754 LineHeight = 1.3,
755 ZIndex = 16,
756 Parent = historyScrolling,
757 RichText = true,
758 })
759
760 -- Auto-scroll
761 task.delay(0.05, function()
762 historyScrolling.CanvasPosition = Vector2.new(0, historyScrolling.AbsoluteCanvasSize.Y)
763 end)
764
765 table.insert(state.dialogueHistory, { speaker = speaker, text = text })
766end
767
768local function toggleHistory()
769 state.historyOpen = not state.historyOpen
770 historyPanel.Visible = state.historyOpen
771
772 if state.historyOpen then
773 TweenService:Create(historyPanel, CONFIG.TWEEN_NORMAL, {
774 BackgroundTransparency = 0.1,
775 Size = UDim2.fromOffset(350, 300),
776 }):Play()
777 end
778end
779
780closeHistoryButton.Activated:Connect(toggleHistory)
781
782-- ============================================================
783-- DIALOGUE OPEN / CLOSE
784-- ============================================================
785
786local function openDialogue(speaker, text, portrait, emotion, choices)
787 state.isActive = true
788
789 -- Show overlay
790 darkOverlay.Visible = true
791 TweenService:Create(darkOverlay, CONFIG.TWEEN_SLOW, {
792 BackgroundTransparency = 0.5,
793 }):Play()
794
795 -- Show container
796 dialogueContainer.Visible = true
797
798 -- Slide in from bottom
799 dialogueContainer.Position = UDim2.new(0.5, 0, 1, 100)
800 TweenService:Create(dialogueContainer, CONFIG.TWEEN_BOUNCE, {
801 Position = UDim2.new(0.5, 0, 1, -60),
802 }):Play()
803
804 -- Slide in portrait
805 task.delay(0.1, function()
806 TweenService:Create(portraitContainer, CONFIG.TWEEN_BOUNCE, {
807 Position = UDim2.fromOffset(-20, -30),
808 }):Play()
809 end)
810
811 -- Update portrait and speaker
812 if portrait then
813 updatePortrait(portrait, speaker)
814 end
815 if emotion then
816 updateEmotion(emotion)
817 end
818
819 -- Start typewriter
820 task.delay(0.3, function()
821 startTypewriter(text)
822 end)
823
824 -- Show choices if provided
825 if choices and #choices > 0 then
826 task.delay(0.5 + #text * CONFIG.TYPEWRITER_SPEED, function()
827 showChoices(choices)
828 end)
829 end
830end
831
832local function closeDialogue()
833 state.isActive = false
834 state.canContinue = false
835
836 -- Stop typewriter
837 if typewriterConnection then
838 typewriterConnection:Disconnect()
839 typewriterConnection = nil
840 end
841
842 -- Hide choices
843 clearChoices()
844
845 -- Slide out
846 TweenService:Create(dialogueContainer, CONFIG.TWEEN_NORMAL, {
847 Position = UDim2.new(0.5, 0, 1, 100),
848 }):Play()
849
850 TweenService:Create(portraitContainer, CONFIG.TWEEN_NORMAL, {
851 Position = UDim2.fromOffset(-200, -30),
852 }):Play()
853
854 TweenService:Create(darkOverlay, CONFIG.TWEEN_SLOW, {
855 BackgroundTransparency = 1,
856 }):Play()
857
858 task.delay(0.5, function()
859 dialogueContainer.Visible = false
860 darkOverlay.Visible = false
861 historyPanel.Visible = false
862 end)
863end
864
865local function continueDialogue()
866 if not state.canContinue then
867 -- Skip typewriter if still typing
868 skipTypewriter()
869 return
870 end
871
872 -- Send continue to server
873 advanceDialogueEvent:FireServer()
874end
875
876-- ============================================================
877-- INPUT HANDLING
878-- ============================================================
879
880-- Click on dialogue box to continue
881local dialogueClickConnection = nil
882
883local function setupDialogueClick()
884 if dialogueClickConnection then return end
885
886 dialogueClickConnection = dialogueBg.InputBegan:Connect(function(input)
887 if input.UserInputType == Enum.UserInputType.MouseButton1
888 or input.UserInputType == Enum.UserInputType.Touch then
889 if state.isActive then
890 continueDialogue()
891 end
892 end
893 end)
894end
895
896setupDialogueClick()
897
898-- Skip button
899skipButton.Activated:Connect(function()
900 if state.isActive then
901 endDialogueEvent:FireServer()
902 closeDialogue()
903 end
904end)
905
906-- Keyboard shortcuts
907UserInputService.InputBegan:Connect(function(input, gameProcessed)
908 if gameProcessed then return end
909
910 if not state.isActive then return end
911
912 if input.KeyCode == Enum.KeyCode.Space
913 or input.KeyCode == Enum.KeyCode.Return
914 or input.KeyCode == Enum.KeyCode.E then
915 continueDialogue()
916 elseif input.KeyCode == Enum.KeyCode.H then
917 toggleHistory()
918 elseif input.KeyCode == Enum.KeyCode.Escape then
919 endDialogueEvent:FireServer()
920 closeDialogue()
921 end
922end)
923
924-- ============================================================
925-- REMOTE EVENT CONNECTIONS
926-- ============================================================
927
928startDialogueEvent.OnClientEvent:Connect(function(data)
929 -- data: { speaker, text, portrait, emotion, choices }
930 openDialogue(
931 data.speaker or "NPC",
932 data.text or "...",
933 data.portrait,
934 data.emotion or "neutral",
935 data.choices
936 )
937end)
938
939advanceDialogueEvent.OnClientEvent:Connect(function(data)
940 -- Update with next dialogue segment
941 if data.speaker then
942 updatePortrait(data.portrait or "", data.speaker)
943 end
944 if data.emotion then
945 updateEmotion(data.emotion)
946 end
947 if data.text then
948 addToHistory(data.speaker or state.currentSpeaker, state.currentText)
949 startTypewriter(data.text)
950 end
951 if data.choices and #data.choices > 0 then
952 task.delay(0.3, function()
953 showChoices(data.choices)
954 end)
955 else
956 clearChoices()
957 end
958end)
959
960endDialogueEvent.OnClientEvent:Connect(function()
961 addToHistory(state.currentSpeaker, state.currentText)
962 closeDialogue()
963end)
964
965portraitUpdateEvent.OnClientEvent:Connect(function(portraitId, speakerName_text)
966 updatePortrait(portraitId, speakerName_text)
967end)
968
969emotionUpdateEvent.OnClientEvent:Connect(function(emotion)
970 updateEmotion(emotion)
971end)
972
973-- ============================================================
974-- INITIALIZATION
975-- ============================================================
976
977print("[Dialogue System] Initialized successfully")
978
979-- Continue hint blink (global)
980task.spawn(function()
981 while true do
982 task.wait(CONFIG.CONTINUE_BLINK_SPEED)
983 if state.canContinue and continueHint and continueHint.Parent then
984 continueHint.TextTransparency = continueHint.TextTransparency == 0 and 0.5 or 0
985 end
986 end
987end)
988
989-- Demo sequence after 2 seconds
990task.delay(2, function()
991 if not state.isActive then
992 openDialogue(
993 "Elder Wizard",
994 "Welcome, traveler! I have been expecting you. The kingdom is in grave danger and only you can help us...",
995 "",
996 "happy",
997 {
998 { id = "accept", text = "I will help! Tell me more." },
999 { id = "decline", text = "I'm busy right now." },
1000 { id = "ask", text = "What kind of danger?" },
1001 }
1002 )
1003 end
1004end)

Code preview

Dialogue System

Luau
Interactive visual preview is shown in the Live preview section above.Switch tabs to inspect the generated Luau client/server code.
Visual preview of the templateRoblox GUI Maker

Frequently asked questions

Keep going