Skip to main content

Dialog System

The dialog system is the engine's central hub for all modal UI -- from simple text boxes to full-screen menus like the Bag, Dex, or Monster Roster. It is built around the DialogManager singleton and uses a queue-based architecture with typewriter animation, localization, and branching choices.

DialogManager

Location: Assets/OpenMon/Core/Runtime/UI/Dialogs/DialogManager.cs Namespace: OpenMon.MCE.UI.Dialogs

DialogManager extends Singleton<DialogManager> and implements IInputReceiver. It provides an entirely static API -- all public methods are called as DialogManager.ShowDialog(...), DialogManager.ShowChoiceMenu(...), etc.

Instantiation via Zenject Factory

The DialogManager is NOT placed in a scene manually. It is instantiated at runtime through Zenject:

// DialogManagerInstaller.cs
[CreateAssetMenu(menuName = "MCE/Dependency Injection/Dialog Manager Installer",
fileName = "DialogManagerInstaller")]
public class DialogManagerInstaller : ScriptableObjectInstaller
{
public DialogManager Prefab;

public override void InstallBindings() =>
Container.BindFactory<DialogManager, DialogManager.Factory>()
.FromComponentInNewPrefab(Prefab);
}

The DialogManager.Factory is a GameObjectFactory<DialogManager> that creates the prefab and lets Zenject inject all dependencies.

Injected Dependencies

[Inject] private IInputManager inputManager;
[Inject] private ILocalizer localizer;
[Inject] private Roster playerRoster;
[Inject] private EvolutionManager evolutionManager;
[Inject] private MapSceneLauncher mapSceneLauncher;

Basic Text Dialogs

Showing a Simple Dialog

// Fire-and-forget: shows text and waits for player input
DialogManager.ShowDialog("Dialogs/Welcome");

// With a character speaker
DialogManager.ShowDialog("Dialogs/ProfGreeting", character: professorData);

// With text modifiers (string interpolation via localization)
DialogManager.ShowDialog("Dialogs/XPGain/Single",
localizableModifiers: false,
modifiers: new[] { monsterName, "500" });

Show and Wait (Coroutine)

For sequential dialog flows, use the coroutine variant:

yield return DialogManager.ShowDialogAndWait("Dialogs/Moves/Learnt",
localizableModifiers: false,
modifiers: new[] { monsterName, moveName });

This blocks the coroutine until the player dismisses the dialog.

Full ShowDialog Signature

public static void ShowDialog(
string localizationKey,
CharacterData character = null,
MonsterInstance monster = null,
bool acceptInput = true,
float typewriterSpeed = .01f,
float switchToNextAfterSeconds = -1, // -1 = wait for input
bool localizableModifiers = true,
BasicDialogBackground background = BasicDialogBackground.Normal,
HorizontalAlignmentOptions horizontalAlignment = HorizontalAlignmentOptions.Left,
params string[] modifiers
);

Parameters Explained

ParameterPurpose
localizationKeyKey into the localization system (e.g. "Dialogs/Welcome")
characterIf set, shows the character's name in a speaker panel
monsterIf set, shows the monster's name/nickname in the speaker panel
acceptInputWhether the player can advance/dismiss the dialog
typewriterSpeedSeconds per character for the typewriter animation
switchToNextAfterSecondsAuto-advance after N seconds (-1 = manual)
localizableModifiersIf true, modifier strings are treated as localization keys; if false, used as literal text
backgroundNormal or Sign (different visual backgrounds)
modifiersValues substituted into {0}, {1}, etc. in the localized string

DialogText Struct

Each dialog is internally represented as a DialogText readonly struct:

public readonly struct DialogText
{
public readonly string LocalizationKey;
public readonly string[] Modifiers;
public readonly bool LocalizableModifiers;
public readonly CharacterData Character;
public readonly MonsterInstance Monster;
public readonly float TypewriterSpeed;
public readonly float SwitchToNextAfterSeconds;
public readonly BasicDialogBackground Background;
public readonly HorizontalAlignmentOptions HorizontalAlignment;
}

The ToString(ILocalizer) method resolves the localization key and substitutes modifiers:

public string ToString(ILocalizer localizer)
{
string text = localizer[LocalizationKey];
for (int i = 0; i < Modifiers.Length; i++)
{
string modifier = Modifiers[i];
text = text.Replace("{" + i + "}", LocalizableModifiers ? localizer[modifier] : modifier);
}
return text;
}

Dialog Queue and Flow

DialogManager uses an internal queue to handle multiple sequential dialogs:

ShowDialog("key1") ──► Enqueue DialogText
ShowDialog("key2") ──► Enqueue DialogText
ShowDialog("key3") ──► Enqueue DialogText

Player presses Select:
├─ If typewriter is still animating → SkipTypewriter()
└─ If text is fully shown → NextDialogInternal()
├─ If more pages in current text → advance page
├─ If more dialogs in queue → dequeue and display next
└─ If queue empty → release input, hide dialog, set active = false

Waiting for Dialogs

// WaitUntil the dialog queue is empty
yield return DialogManager.WaitForDialog;

// WaitWhile the typewriter is still animating
yield return DialogManager.WaitForTypewriter;

Auto-Advance

When switchToNextAfterSeconds > 0, the dialog auto-advances after the typewriter finishes and the specified delay elapses:

private IEnumerator NextDialogAfterSeconds(float seconds)
{
yield return new WaitWhile(() => Text.IsTypewriting);
yield return new WaitForSeconds(seconds);
NextDialog();
}

Typewriter Animation

Text is displayed using LocalizedTypeWriterTMP, which wraps MCETextAnimator -- a DOTween-based typewriter:

// MCETextAnimator.ShowText()
textComponent.maxVisibleCharacters = 0;
typewriterTween = DOTween.To(
() => textComponent.maxVisibleCharacters,
x => textComponent.maxVisibleCharacters = x,
totalCharacters,
totalCharacters * waitForNormalChars
).SetEase(Ease.Linear)
.OnComplete(() => onTextShowed.Invoke());

Speed Control

LocalizedTypeWriterTMP.SetTypewriterSpeed(float) sets three timing tiers:

TierVariablePurpose
NormalwaitForNormalCharsBase delay per character
MiddlewaitMiddleDelay at commas (5x normal by default)
LongwaitLongDelay at periods (20x normal by default)

Text Effects

MCETextAnimator supports optional vertex effects (disabled by default):

  • <wave> -- sinusoidal vertical oscillation per character.
  • <shake> -- random offset per character each frame.

Enable with enableVertexEffects = true on the component.

Skipping

Pressing the select button while the typewriter is animating calls SkipTypewriter(), which instantly reveals all characters.

Speaker Panel

When a CharacterData or MonsterInstance is passed, the dialog shows a speaker name panel:

if (nextText.Character != null)
{
CharacterName.SetValue(nextText.Character.LocalizableName);
CharacterPanel.Show();
}
else if (nextText.Monster != null)
{
CharacterName.Text.SetText(nextText.Monster.GetNameOrNickName(localizer));
CharacterPanel.Show();
}
else
CharacterPanel.Show(false);

Choice Menus

Choice dialogs use the ChoiceMenuController -- a MenuSelector subclass that supports 1-11 options with auto-sizing:

DialogManager.ShowChoiceMenu(
options: new List<string> { "Common/True", "Common/False" },
callback: choice => { /* 0 = Yes, 1 = No */ },
onBackCallback: () => { /* Treat as cancel */ },
showDialog: true,
localizationKey: "Menu/Save/Confirmation"
);

Full ShowChoiceMenu Signature

public static void ShowChoiceMenu(
List<string> options, // Localization keys for each option
Action<int> callback, // Called with the selected index
Transform position = null, // Custom position (null = default)
Action onBackCallback = null, // If set, back button closes and calls this
CharacterData character = null, // Speaker
MonsterInstance monster = null, // Speaker
bool showDialog = false, // Show a text dialog alongside the choice
string localizationKey = "", // Dialog text localization key
bool localizableModifiers = true,
params string[] modifiers
);

How It Works Internally

  1. ChoiceMenuController.SetNumberOfOptions(n) enables the first N ChoiceOption items and resizes the panel.
  2. Each option's LocalizedTextMeshPro is set to the provided localization key.
  3. The choice menu appears AFTER the typewriter finishes (when showDialog is true).
  4. On selection, the callback fires and the choice menu auto-hides.
  5. On back (if allowed), onBackCallback fires.

Common Pattern: Yes/No with Coroutine

int choice = -1;

DialogManager.ShowChoiceMenu(
new List<string> { "Common/True", "Common/False" },
playerChoice => choice = playerChoice,
onBackCallback: () => choice = 1,
showDialog: true,
localizationKey: "Menu/Save/Confirmation"
);

yield return new WaitUntil(() => choice != -1);

if (choice == 0)
{
// Player chose Yes
}

Localization Integration

All dialog text goes through the ILocalizer system:

  • LocalizedTextMeshPro.SetValue(key, localizableModifiers, modifiers) resolves the key and substitutes {0}, {1}, etc.
  • When localizableModifiers is true, modifier strings are themselves resolved as localization keys.
  • When false, modifiers are inserted as literal strings (useful for player names, numbers, etc.).
// Localized modifier: both "Dialogs/Caught" and "Monsters/Pikachu" are resolved
DialogManager.ShowDialog("Dialogs/Caught", modifiers: "Monsters/Pikachu");

// Literal modifier: "500" is inserted as-is
DialogManager.ShowDialog("Dialogs/XPGain/Single",
localizableModifiers: false,
modifiers: new[] { monsterName, "500" });

Complex Dialogs (Managed Screens)

Beyond basic text, DialogManager owns references to every major screen and provides static launcher methods:

MethodOpens
ShowGameMenu(...)In-game pause menu
ShowPlayerRosterMenu(...)Monster party screen (with choosing/swap modes)
ShowBag(...)Inventory with tabs (battle mode, selling mode, selection mode)
ShowDexScreen(...)Full dex browser
ShowSingleMonsterDexScreen(...)Single monster dex entry
ShowShop(...)Buy/sell shop
ShowOptionsMenu()Settings screen
ShowProfileScreen(...)Trainer card
ShowQuestsScreen(...)Quest log
RequestTextInput(...)Keyboard input dialog (naming, etc.)
ShowMoveTutorDialog(...)Move tutor selection
ShowMoveLearnPanel(...)Move learn/replace flow
ShowNewMonsterDialog(...)New monster nickname + storage flow
ShowXPGainDialogLearnMoveAndEvolveForSingleMonsterInPlayerRoster(...)Full XP gain -> level up -> move learn -> evolution pipeline
CloseMenus()Closes ALL open screens in sequence

Notifications

DialogManager.Notifications exposes the child NotificationManager for toast-style notifications:

DialogManager.Notifications.StopAllNotifications();

Loading Indicator

DialogManager.ShowLoadingIcon(true);
DialogManager.ShowLoadingText("Loading/MapData");
// ... async work ...
DialogManager.ClearLoadingText();
DialogManager.ShowLoadingIcon(false);

Using Dialogs from CommandGraph

NPC behavior is authored in CommandGraph (a node-based visual scripting system). Dialog nodes internally call the same DialogManager.ShowDialog(...) and DialogManager.ShowChoiceMenu(...) static API. The CommandGraph nodes handle:

  • Chaining multiple dialog texts in sequence
  • Branching on choice results
  • Setting the speaking character per node
  • Waiting for dialog completion before advancing the graph

From code, you interact with the same static methods -- there is no difference in the underlying API.

Background Variants

BasicDialogBackground is an enum nested inside DialogManager (i.e. DialogManager.BasicDialogBackground):

// Defined inside DialogManager
public enum BasicDialogBackground
{
Normal, // Standard dialog box
Sign // Sign post / notice board style
}

Pass background: DialogManager.BasicDialogBackground.Sign to ShowDialog() to use the sign variant. The DialogManager switches between serialized background HidableUiElement references stored in a SerializableDictionary<BasicDialogBackground, HidableUiElement>.