SpeechBubbleController extends DialogueController to create positioned speech bubbles with directional arrows and avatar support. Perfect for in-game character dialogue, NPC conversations, or tutorial hints.
UIViewComponent → DialogueController → SpeechBubbleController
using UnityEngine;
using CupkekGames.Luna;
public class NPCDialogue : MonoBehaviour
{
[SerializeField] private SpeechBubbleController _speechBubble;
[SerializeField] private Sprite _npcAvatar;
private void Start()
{
_speechBubble.OnContinue += OnPlayerContinue;
_speechBubble.OnTextComplete += OnTextComplete;
}
public void StartDialogue()
{
// Show speech with avatar and arrow pointing to character
_speechBubble.Continue(
"Hello adventurer! Welcome to our village.",
avatarLeft: _npcAvatar,
avatarRight: null,
arrowPosition: SpeechBubbleArrowPosition.BottomLeft,
skipCurrent: false
);
}
private void OnTextComplete()
{
_speechBubble.ShowNext();
}
private void OnPlayerContinue()
{
_speechBubble.HideNext();
// Load next dialogue line...
}
}| Position | Description |
|---|---|
| None | No arrow displayed |
| BottomLeft | Arrow points down-left |
| BottomRight | Arrow points down-right |
| TopLeft | Arrow points up-left |
| TopRight | Arrow points up-right |
// Point arrow toward speaker location
_speechBubble.SetArrowPosition(SpeechBubbleArrowPosition.BottomLeft);Gets or sets whether the next icon automatically shows when text completes.
_speechBubble.AutoShowNext = true; // DefaultExtended version with avatar and arrow support.
public bool Continue(
string text,
Sprite avatarLeft,
Sprite avatarRight,
SpeechBubbleArrowPosition arrowPosition,
bool skipCurrent
)| Parameter | Description |
|---|---|
| text | The dialogue text (supports text effects) |
| avatarLeft | Sprite for left avatar (null to hide) |
| avatarRight | Sprite for right avatar (null to hide) |
| arrowPosition | Direction of the speech bubble arrow |
| skipCurrent | Whether to skip currently playing text |
Sets the speech bubble position on screen.
public void SetPostion(Vector2 pos)// Position bubble at screen coordinates
_speechBubble.SetPostion(new Vector2(400, 300));Sets the maximum width of the speech bubble container.
public void SetSpeechBubbleMaxWidth(StyleLength maxWidth)// Limit bubble width
_speechBubble.SetSpeechBubbleMaxWidth(new StyleLength(400));Changes the arrow direction.
public void SetArrowPosition(SpeechBubbleArrowPosition arrowPosition)// Show avatars with breathing animation
public void ShowAvatarLeft(Sprite sprite)
public void ShowAvatarRight(Sprite sprite)
// Hide avatars
public void HideAvatarLeft()
public void HideAvatarRight()// Show/hide the "continue" indicator
public void ShowNext()
public void HideNext()| Event | Inherited | Description |
|---|---|---|
| OnTextStart | Yes | Fired when text begins playing |
| OnTextComplete | Yes | Fired when text finishes |
| OnContinue | No | Fired when player presses continue |
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:luna="CupkekGames.Luna">
<ui:VisualElement name="SpeechBubble">
<ui:VisualElement name="AvatarLeftContainer">
<ui:VisualElement name="AvatarLeft" />
</ui:VisualElement>
<ui:Label name="Speech" />
<ui:VisualElement name="AvatarRightContainer">
<ui:VisualElement name="AvatarRight" />
</ui:VisualElement>
<ui:Button name="ContinueButton" />
<luna:InputPrompt name="NextIcon" />
<ui:VisualElement name="SpeechArrowContainer">
<ui:VisualElement class="bottom_left" />
<ui:VisualElement class="bottom_right" />
<ui:VisualElement class="top_left" />
<ui:VisualElement class="top_right" />
</ui:VisualElement>
</ui:VisualElement>
</ui:UXML>using System.Collections.Generic;
using UnityEngine;
using CupkekGames.Luna;
public class CharacterConversation : MonoBehaviour
{
[SerializeField] private SpeechBubbleController _speechBubble;
[SerializeField] private Sprite _heroAvatar;
[SerializeField] private Sprite _villagerAvatar;
private Queue<DialogueLine> _dialogueQueue = new();
private void Start()
{
_speechBubble.OnContinue += NextLine;
_speechBubble.OnTextComplete += () => _speechBubble.ShowNext();
// Queue up conversation
_dialogueQueue.Enqueue(new DialogueLine(
"Excuse me, have you seen any monsters around here?",
_heroAvatar, null, SpeechBubbleArrowPosition.BottomRight
));
_dialogueQueue.Enqueue(new DialogueLine(
"Monsters? Oh my! They went toward the <shake>dark forest</shake>!",
null, _villagerAvatar, SpeechBubbleArrowPosition.BottomLeft
));
_dialogueQueue.Enqueue(new DialogueLine(
"Thank you! I'll deal with them.",
_heroAvatar, null, SpeechBubbleArrowPosition.BottomRight
));
NextLine();
}
private void NextLine()
{
_speechBubble.HideNext();
if (_dialogueQueue.Count > 0)
{
var line = _dialogueQueue.Dequeue();
_speechBubble.Continue(
line.Text,
line.LeftAvatar,
line.RightAvatar,
line.ArrowPosition,
false
);
}
else
{
// Conversation complete
_speechBubble.UIView.FadeOutThenDestroy();
}
}
private struct DialogueLine
{
public string Text;
public Sprite LeftAvatar;
public Sprite RightAvatar;
public SpeechBubbleArrowPosition ArrowPosition;
public DialogueLine(string text, Sprite left, Sprite right,
SpeechBubbleArrowPosition arrow)
{
Text = text;
LeftAvatar = left;
RightAvatar = right;
ArrowPosition = arrow;
}
}
}The controller uses these USS classes for animations:
/* Avatar breathing animation */
.vn_avatar_anim {
scale: 1.02;
transition: scale 0.5s ease-in-out;
}
/* Next icon bounce animation */
.vn_next_icon_anim {
translate: 0 -5px;
transition: translate 0.3s ease-in-out;
}SetPosition to place bubbles near speaking charactersSettings
Theme
Light
Contrast
Material
Dark
Dim
Material Dark
System
Sidebar(Light & Contrast only)
Font Family
DM Sans
Wix
Inclusive Sans
AR One Sans
Direction