Перейти к основному содержимому

Система диалогов

Система диалогов — это центральный узел движка для всего модального UI — от простых текстовых окон до полноэкранных меню, таких как Сумка, Покедекс или Состав монстров. Она построена вокруг синглтона DialogManager и использует архитектуру на основе очереди с анимацией пишущей машинки, локализацией и ветвящимися вариантами выбора.

DialogManager​

Расположение: Assets/OpenMon/Core/Runtime/UI/Dialogs/DialogManager.cs Пространство имён: OpenMon.MCE.UI.Dialogs

DialogManager расширяет Singleton<DialogManager> и реализует IInputReceiver. Он предоставляет полностью статический API — все публичные методы вызываются как DialogManager.ShowDialog(...), DialogManager.ShowChoiceMenu(...) и т.д.

Создание через фабрику Zenject​

DialogManager НЕ размещается в сцене вручную. Он создаётся во время выполнения через 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);
}

DialogManager.Factory — это GameObjectFactory<DialogManager>, который создаёт префаб и позволяет Zenject внедрить все зависимости.

Внедрённые зависимости​

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

Базовые текстовые диалоги​

Показ простого диалога​

// Забыл-и-запустил: показывает текст и ждёт ввода игрока
DialogManager.ShowDialog("Dialogs/Welcome");

// С говорящим персонажем
DialogManager.ShowDialog("Dialogs/ProfGreeting", character: professorData);

// С текстовыми модификаторами (интерполяция строк через локализацию)
DialogManager.ShowDialog("Dialogs/XPGain/Single",
localizableModifiers: false,
modifiers: new[] { monsterName, "500" });

Показ и ожидание (Корутина)​

Для последовательных потоков диалогов используйте вариант с корутиной:

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

Это блокирует корутину, пока игрок не закроет диалог.

Полная сигнатура ShowDialog​

public static void ShowDialog(
string localizationKey,
CharacterData character = null,
MonsterInstance monster = null,
bool acceptInput = true,
float typewriterSpeed = .01f,
float switchToNextAfterSeconds = -1, // -1 = ждать ввода
bool localizableModifiers = true,
BasicDialogBackground background = BasicDialogBackground.Normal,
HorizontalAlignmentOptions horizontalAlignment = HorizontalAlignmentOptions.Left,
params string[] modifiers
);

Описание параметров​

ПараметрНазначение
localizationKeyКлюч в системе локализации (например, "Dialogs/Welcome")
characterЕсли установлен, показывает имя персонажа в панели говорящего
monsterЕсли установлен, показывает имя/никнейм монстра в панели говорящего
acceptInputМожет ли игрок продвигать/закрывать диалог
typewriterSpeedСекунд на символ для анимации пишущей машинки
switchToNextAfterSecondsАвтопереход через N секунд (-1 = вручную)
localizableModifiersЕсли true, строки модификаторов обрабатываются как ключи локализации; если false, используются как буквальный текст
backgroundNormal или Sign (разные визуальные фоны)
modifiersЗначения, подставляемые в {0}, {1} и т.д. в локализованной строке

Структура DialogText​

Каждый диалог внутренне представлен как readonly структура DialogText:

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;
}

Метод ToString(ILocalizer) разрешает ключ локализации и подставляет модификаторы:

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;
}

Очередь диалогов и поток​

DialogManager использует внутреннюю очередь для обработки нескольких последовательных диалогов:

ShowDialog("key1") ──► Поставить DialogText в очередь
ShowDialog("key2") ──► Поставить DialogText в очередь
ShowDialog("key3") ──► Поставить DialogText в очередь

Игрок нажимает Выбрать:
├─ Если пишущая машинка всё ещё анимирует → SkipTypewriter()
└─ Если текст полностью показан → NextDialogInternal()
├─ Если есть ещё страницы в текущем тексте → перейти на следующую
├─ Если есть ещё диалоги в очереди → извлечь и показать следующий
└─ Если очередь пуста → освободить ввод, скрыть диалог, установить active = false

Ожидание диалогов​

// WaitUntil очередь диалогов пуста
yield return DialogManager.WaitForDialog;

// WaitWhile пишущая машинка всё ещё анимирует
yield return DialogManager.WaitForTypewriter;

Автопереход​

Когда switchToNextAfterSeconds > 0, диалог автоматически переходит дальше после завершения пишущей машинки и истечения указанной задержки:

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

Анимация пишущей машинки​

Текст отображается с использованием LocalizedTypeWriterTMP, который обёртывает MCETextAnimator — пишущую машинку на основе твинов. Тип DOTween ниже — это собственный фасад MCE OpenMon.Foundation.Tweening.DOTween (поставляется с движком), а не ассет Demigiant:

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

Управление скоростью​

LocalizedTypeWriterTMP.SetTypewriterSpeed(float) устанавливает три временных уровня:

УровеньПеременнаяНазначение
ОбычныйwaitForNormalCharsБазовая задержка на символ
СреднийwaitMiddleЗадержка на запятых (по умолчанию 5x)
ДлинныйwaitLongЗадержка на точках (по умолчанию 20x)

Текстовые эффекты​

MCETextAnimator поддерживает опциональные эффекты вершин (отключены по умолчанию):

  • <wave> — синусоидальное вертикальное колебание на символ.
  • <shake> — случайное смещение на символ каждый кадр.

Включите с помощью enableVertexEffects = true на компоненте.

Пропуск​

Нажатие кнопки выбора во время анимации пишущей машинки вызывает SkipTypewriter(), который мгновенно показывает все символы.

Панель говорящего​

Когда передаются CharacterData или MonsterInstance, диалог показывает панель с именем говорящего:

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);

Меню выбора​

Диалоги выбора используют ChoiceMenuController — подкласс MenuSelector, поддерживающий 1–11 вариантов с авторазмером:

DialogManager.ShowChoiceMenu(
options: new List<string> { "Common/True", "Common/False" },
callback: choice => { /* 0 = Да, 1 = Нет */ },
onBackCallback: () => { /* Обработать как отмену */ },
showDialog: true,
localizationKey: "Menu/Save/Confirmation"
);

Полная сигнатура ShowChoiceMenu​

public static void ShowChoiceMenu(
List<string> options, // Ключи локализации для каждого варианта
Action<int> callback, // Вызывается с индексом выбора
Transform position = null, // Пользовательская позиция (null = по умолчанию)
Action onBackCallback = null, // Если задано, кнопка назад закрывает и вызывает это
CharacterData character = null, // Говорящий
MonsterInstance monster = null, // Говорящий
bool showDialog = false, // Показать текстовый диалог рядом с выбором
string localizationKey = "", // Ключ локализации текста диалога
bool localizableModifiers = true,
params string[] modifiers
);

Как это работает внутри​

  1. ChoiceMenuController.SetNumberOfOptions(n) включает первые N элементов ChoiceOption и изменяет размер панели.
  2. LocalizedTextMeshPro каждого варианта устанавливается на предоставленный ключ локализации.
  3. Меню выбора появляется ПОСЛЕ завершения пишущей машинки (когда showDialog равен true).
  4. При выборе срабатывает колбэк, и меню выбора автоматически скрывается.
  5. При возврате (если разрешено) срабатывает onBackCallback.

Общий шаблон: Да/Нет с корутиной​

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)
{
// Игрок выбрал Да
}

Интеграция локализации​

Весь текст диалогов проходит через систему ILocalizer:

  • LocalizedTextMeshPro.SetValue(key, localizableModifiers, modifiers) разрешает ключ и подставляет {0}, {1} и т.д.
  • Когда localizableModifiers равен true, строки модификаторов сами разрешаются как ключи локализации.
  • Когда false, модификаторы вставляются как буквальные строки (полезно для имён игроков, чисел и т.д.).
// Локализуемый модификатор: разрешаются и "Dialogs/Caught", и "Monsters/Pikachu"
DialogManager.ShowDialog("Dialogs/Caught", modifiers: "Monsters/Pikachu");

// Буквальный модификатор: "500" вставляется как есть
DialogManager.ShowDialog("Dialogs/XPGain/Single",
localizableModifiers: false,
modifiers: new[] { monsterName, "500" });

Сложные диалоги (Управляемые экраны)​

Помимо базового текста, DialogManager владеет ссылками на каждый основной экран и предоставляет статические методы запуска:

МетодОткрывает
ShowGameMenu(...)Внутриигровое меню паузы
ShowPlayerRosterMenu(...)Экран команды монстров (с режимами выбора/обмена)
ShowBag(...)Инвентарь с вкладками (режим битвы, продажи, выбора)
ShowDexScreen(...)Полный браузер покедекса
ShowSingleMonsterDexScreen(...)Запись одного монстра в покедексе
ShowShop(...)Магазин покупки/продажи
ShowOptionsMenu()Экран настроек
ShowProfileScreen(...)Карточка тренера
ShowQuestsScreen(...)Журнал заданий
RequestTextInput(...)Диалог клавиатурного ввода (имена и т.д.)
ShowMoveTutorDialog(...)Выбор учителя атак
ShowMoveLearnPanel(...)Поток изучения/замены атаки
ShowNewMonsterDialog(...)Поток имени + хранения нового монстра
ShowXPGainDialogLearnMoveAndEvolveForSingleMonsterInPlayerRoster(...)Полный конвейер: получение опыта → повышение уровня → изучение атаки → эволюция
CloseMenus()Закрывает ВСЕ открытые экраны последовательно

Уведомления​

DialogManager.Notifications предоставляет дочерний NotificationManager для уведомлений в стиле toast:

DialogManager.Notifications.StopAllNotifications();

Индикатор загрузки​

DialogManager.ShowLoadingIcon(true);
DialogManager.ShowLoadingText("Loading/MapData");
// ... асинхронная работа ...
DialogManager.ClearLoadingText();
DialogManager.ShowLoadingIcon(false);

Использование диалогов из CommandGraph​

Поведение NPC создаётся в CommandGraph (узловая система визуального скриптинга). Узлы диалогов внутренне вызывают тот же статический API DialogManager.ShowDialog(...) и DialogManager.ShowChoiceMenu(...). Узлы CommandGraph обрабатывают:

  • Сцепление нескольких текстов диалогов последовательно
  • Ветвление по результатам выбора
  • Установку говорящего персонажа на узел
  • Ожидание завершения диалога перед продвижением графа

Из кода вы взаимодействуете с теми же статическими методами — нет различий в базовом API.

Варианты фона​

BasicDialogBackground — это перечисление, вложенное внутри DialogManager (т.е. DialogManager.BasicDialogBackground):

// Определено внутри DialogManager
public enum BasicDialogBackground
{
Normal, // Стандартное диалоговое окно
Sign // Стиль вывески / доски объявлений
}

Передайте background: DialogManager.BasicDialogBackground.Sign в ShowDialog(), чтобы использовать вариант вывески. DialogManager переключается между сериализованными ссылками HidableUiElement фона, хранящимися в SerializableDictionary<BasicDialogBackground, HidableUiElement>.