Как сделать диалоги в unity3d
Система диалогов
Итак, на повестке дня у нас, один из вариантов реализации системы диалогов в игре. Сразу оговоримся, что представленный проект из разряда, дешево и сердито. То есть, реализованы только базовые возможности. Идея заключается в том, что диалоги в игре разбиты на отдельные файлы. Каждый диалог, это конкретный файл, XML типа. Например, когда игрок подходит к NPC, делается запрос на диалог, связанный с этим персонажем. Происходит его загрузка и на основе полученных данных, создается интерфейс пользователя. При этом предусмотрена возможность не только вручную создавать файл, но и с помощью простенького XML генератора. Еще один важный момент в том, что такой подход позволяет создать сколько угодно языковых вариантов диалогов.
Настройка сцены. Для отображения диалога воспользуемся системой UI Юнити. Добавляем Scroll View и настраиваем его для вертикальной прокрутки. Затем добавляем еще Button, ширину кнопки подстраиваем под ширину окна прокрутки, высота будет регулироваться через скрипт. Еще нужно настроить присет трансформа кнопки, устанавливаем как показано на скриншоте:
Дополнительно на кнопку вешаем скрипт:
Здесь мы указываем компоненты кнопки, для упрощенного доступа к ним в дальнейшем.
Кстати говоря, в проекта должна быть папка Resources в ней будут хранится все файлы диалогов. Внутри нее создаем подпапку, например Russian, то есть, это будет у нас язык по умолчанию. В дальнейшем можно просто сделать дубликат этой папки, обозвать ее другим языком и сделать перевод текста в файлах. Таким образом, можно сделать несколько языков в игре.
Как должен выглядеть XML:
Всё это дело можно прописывать вручную, однако мы напишем небольшой скрипт:
А к нему еще один вспомогательный:
С помощью этой парочки, можно генерировать XML. Предназначены они для работы только в редакторе. Просто заполняем массив диалога в инспекторе Юнити, настраиваем опции и жмем кнопочку Generate и готово. Файл будет создан по указанному адресу.
Далее. Что нам еще нужно? Чтение созданного файла и создание окна диалога.
За это отвечает данный скрипт:
Здесь показано как загрузить XML из ресурсов Юнити, а затем извлечь из него значения атрибутов. После загрузки, массивы будут иметь точно такой же вид, как при создании в генераторе, соответственно и в окне, узлы будут отображаться в таком же виде.
Ниже представлен рабочий проект, где можно изучить работу диалогов:
Как сделать диалоги в unity3d
Привет, это небольшой урок о реализации интерфейса системы диалогов в Unity3D. Отмечу, что в статье не будет описано построение базы данных диалогов. В статье речь идет только о интерфейсе.
Для удобства я создал проект с основными ассетами(2 спрайта + шрифт), для его открытия нужна версия Unity3D 4.3+. На основе этого проекта мы будем писать систему диалогов.
Отключите Answers и включите Text. Сохраните сцену и проект(на всякий случай).
II. Теперь по коду. Скачаем для начала iTween, с ним даже плохие ассеты могут показывать хорошую картинку, благодаря красивым анимациям.
Зайдите в Asset Stote, в поиск введите «iTween”
200?’200px’:»+(this.scrollHeight+5)+’px’);»> public float letterDelay = 0.06f; //Задержка в написании одной буквы для красивого эффекта
public float transitionTime = 0.5f; //Время всех твинов
private string currentText; //Текущий текст
private string testText = «Lorem ipsum dolor sit amet, consectetur adipiscing elit. » +
«Sed sit amet interdum dui. Morbi et turpis ac tellus ullamcorper placerat. » +
«Vivamus rutrum neque non luctus dapibus. » +
«Nam elementum, nibh in lacinia ullamcorper, magna tortor pretium lorem, nec feugiat velit magna eget nisl. » +
«Interdum et malesuada fames ac ante ipsum primis in faucibus. » +
«Sed fringilla magna augue, vel fermentum turpis mattis eu.»; //Любой текст, который будет форматироваться и изменяться в процессе.
private List testAnswers = new List (); //Лист с тремя вариантами ответов
private string formattedText; //Отформатированный текст для нашего окна
private string nextText; //Отрезок оставшегося текста
private bool tweenCompleted; //Любое действие можно будет совершить только если все анимации и твины закончены
private bool answersMode; //Отвечам или читаем
private int selectedAnswer = 0; //Выбранный на данный момент ответ
private int maxLetters = 50; //Макс. кол-во букв
private int maxLines = 3; //Макс. кол-во строчек
200?’200px’:»+(this.scrollHeight+5)+’px’);»> public string FormatString ( string newText ) <
string[] words = newText.Split(» «[0]);
string result = «»;
int numberOfLines = 1;
int letterCounter = 0;
for( int index = 0; index 0 ) <
if (letterCounter + word.Length + 1 IEnumerator TypeText () <
formattedText = FormatString (currentText);
foreach (var letter in formattedText.ToCharArray()) <
text.guiText.text += letter;
yield return new WaitForSeconds(letterDelay);
if (text.guiText.text == formattedText)
<
break;
>
>
>
private void StartPointerTween() <
iTween.MoveAdd(pointer,iTween.Hash(«name»,»pointer»,»x»,0.01f,»time»,0.4f, «looptype», iTween.LoopType.pingPong, «easetype», iTween.EaseType.easeInOutCirc));
>
void SelectAnswer () <
tweenCompleted = false;
pointer.SetActive(false);
Reset();
void ShowAnswers()
<
iTween.FadeFrom (textBackground, 0, transitionTime/2);
iTween.MoveFrom (textBackground,iTween.Hash(«y»,-1f,»time»,transitionTime/2,»delay»,0,»oncomplete»,»OnAnswersTweenComplete»,»onCompleteTarget», gameObject));
tweenCompleted = false;
answersMode = true;
text.SetActive (false);
>
200?’200px’:»+(this.scrollHeight+5)+’px’);»> private void OnPointerAppearingTweenComplete () <
tweenCompleted = true;
StartPointerTween ();
>
private void OnAppearingTweenComplete () <
tweenCompleted = true;
StartCoroutine(TypeText());
>
Система сообщений или “мягкая связь” между компонентами для Unity3D
Введение
В данной статье будут затронуты темы, связанные с реализацией возможности “мягкой связи” компонентов игровой логики на основе системы сообщений при разработке игр на Unity3D.
Ни для кого не секрет, что в подавляющем большинстве случаев средств, которые предоставляет движок в базовом виде, недостаточно для полноценной реализации систем обмена данными между компонентами игры. В самом примитивном варианте, с которого все начинают, мы получаем информацию через экземпляр объекта. Получить этот экземпляр можно разными способами от ссылки на объект сцены, до функций Find. Это не удобно, делает код не гибким и заставляет программиста предусматривать множество нестандартных поведений логики: от “объект исчез из сцены”, до “объект не активен”. Помимо прочего может страдать и скорость работы написанного кода.
Прежде, чем приступить к рассмотрению способов решения проблемы, остановимся подробнее на ее предпосылках и базовых терминах, которые будут упомянуты ниже. Начнем с того, что подразумевается под “мягкой связью”. В данной случае — это обмен данными между компонентами игровой логики таким образом, чтобы эти компоненты абсолютно ничего не знали друг о друге. Исходя из этого, любые ссылки на объекты сцены или же поиск объекта в сцене по имени или типу дают нам “жесткую связь”. Если эти связи начнут выстраиваться в цепочки, то в случае необходимости изменения поведения логики программисту придется все перенастраивать заново. Как не сложно догадаться, гибкостью здесь и не пахнет. Конечно, можно написать расширение редактора, которое будет автоматически заполнять ссылки, но это не решит другую проблему – покомпонентное тестирование игровой логики.
Остановимся более подробно на базовом принципе реализации “мягкой связи”. Как было сказано выше, чтобы “мягко” связать два компонента, мы должны передать данные от одного к другому, так, чтобы они не знали ничего друг о друге. Для того, чтобы это обеспечить, нам нужно получить данные не по запросу (имея на руках экземпляр объекта), а использовать механизм уведомлений. Фактически это означает, что при наступлении каких-либо событий в объекте/компоненте, мы не спрашиваем этот объект о его состоянии, объект сам уведомляет о том, что в нем произошли изменения. Набор таких уведомлений формирует интерфейс (не путать с interface в C#), с помощью которого игровая логика получает данные о нашем объекте. Наглядно это можно представить следующим образом:
Таким образом любой компонент системы через интерфейс уведомлений может получить необходимые данные об объекте игровой логике, при этом наличие самого объекта для тестирования связанных с ним компонентов необязательно, нам достаточно реализовать интерфейс, а затем подменить его на рабочий экземпляр.
Рассмотрим более подробно способы реализации описанного выше механизма, а также их плюсы и минусы.
Система сообщений на основе UnityEvents/UnityAction
Данная система появилось сравнительно недавно (в 5 версии движка Unity3D). Пример того, как реализовать простую систему сообщений можно посмотреть по этой ссылке.
Плюсы использования данного способа:
Классическая система C# на Event/Delegate
Самый простой и достаточно эффективный способ реализации связи компонентов на основе уведомлений — это использование пары event/delegate, которая является частью языка C# (подробнее можно почитать в статьях на habr’е или msdn).
Есть много разных вариантов реализации системы сообщений на основе event/delegate, некоторые из них можно найти на просторах интернета. Я приведу пример, на мой взгляд, наиболее удобной системы, однако для начала хочу упомянуть об одной важной детали, связанной с использованием event’ов. Если у события (event) нет подписчиков, то при вызове этого события произойдет ошибка, поэтому перед использованием обязательна проверка на null. Это не совсем удобно. Конечно можно написать обертку для каждого event, где будет проводиться проверка на null, но это еще более не удобно. Перейдем к реализации.
Как видно из примера, подписка происходит в методе OnEnable, а отписка в OnDisable и в данном случае она обязательна, иначе гарантирована утечка памяти и исключения по нулевой ссылке (null reference exception), если объект будет удален из игры. Саму подписку можно осуществлять в любой необходимый момент времени, это не обязательно делать только в OnEnable.
Легко заметить, что при таком подходе, мы можем без каких-либо проблем тестировать работу класса GUILogic, даже в отсутствии реальной логики GameLogicObject. Достаточно написать имитатор, использующий интерфейс уведомлений и использовать вызовы вида GameLogicObject.StartEvent ().
Какие плюсы дает нам данная реализация:
Reflection система сообщений с идентификаций на string
Прежде чем приступить к описанию системы и ее реализации, хотелось бы остановиться на предпосылках, которые толкнули на ее создание. До прихода к этим мыслям в своих приложениях я использовал описанную выше систему на основе event/delegate. Те проекты, которые мне пришлось разрабатывать на тот момент, были относительно простые, в них требовалась скорость реализации, минимум багов на тестах, исключение по максимуму человеческого фактора в фазе разработки игровой логики. Исходя из этого, родился ряд некоторых требований относительно обмена данными между компонентами:
GlobalMessanger.MessageHandler – атрибут, который указывает нам, что метод является обработчиком события. Для того, чтобы определить тип события, которое данный метод обрабатывает, существует два способа (хотя на самом деле может быть и больше):
Как видно, данная обертка содержит в себе два метода, которые позволяют подписываться и отписываться от события (тип события забирается из имени метода). Они необходимы в случае, если нам нужно осуществить подписку на событие по ходу работы логики. Автоматическая подписка осуществляется в методе Awake. Отписка от событий осуществляется автоматически, но об этом чуть позже.
Класс GlobalMessanger является обычным компонентом Unity, доступ к которому осуществляется на основе Unity-синглетона. При этом, для этого компонента создается отдельный объект сцены, который существует только внутри нее и будет удален, когда сцена будет выгружена. Поскольку у нас события идентифицируются на основе строк, то информацию о событиях и подписчиках я решил хранить в хеш-таблице.
Как видно, при вызове события происходит проверка на существование объекта и на активность объекта. В первом случае, удаленный объект убирается из подписчиков, во втором же игнорируется при вызове методов обработки события. Таким образом, следить за отпиской событий у удаленного объекта не требуется, все осуществляется автоматически. При этом, если объект был временно деактивирован, то не осуществляется отписка от событий и повторная подписка, а также при вызове наличие подписчиков на событие не обязательно.
Легко заметить, что описанная выше система не представляет из себя ничего сверхординарного и не несет в себе откровений, однако она проста и удобна и хорошо подходит для относительно небольших проектов.
Думаю, сразу заметно насколько сократился код по сравнению с event/delegate, что меня лично радует.
Какие плюсы дает нам данная реализация:
Reflection система сообщений с идентификаций на типах данных
В предыдущем разделе была описана система более удобная (на мой взгляд) по сравнению с event/delegate, однако она все также имеет ряд недостатков, которые сильно влияют на гибкость нашего кода, поэтому следующим шагом было ее развитие с учетом этих факторов.
Итак, нам нужно сохранить все плюсы предыдущей системы, но сделать ее гибче и более устойчивой к возможным изменениям в игровой логике. Поскольку основная проблема — это изменения имени события и передаваемых параметров, то возникла идея идентифицировать события именно по ним. Фактически это означает, что любое событие, которое возникает в компоненте характеризуется ничем иным, как данными, которые оно передает. Поскольку мы не можем просто привязаться к стандартным типам (int, float и т.п.), потому что в логике такие данные могут передавать многие компоненты, то логичным шагом было сделать обертку над ними, которая была бы удобной, легко читаемой и однозначно интерпретирующей событие.
Как видно, у событий появились атрибуты. Это дает нам возможность получить отладочную информацию, в случае, если событие требует подписчика, а в коде его по каким-то причинам нет.
Метод вызова событий Call (и его перегрузки), который ранее у нас был частью класса GlobalMessanger, теперь является статическим и находится в GEvents.BaseEvent и принимает теперь в качестве параметра экземпляр класса, описывающего тип события.
Подписка и отписка на события, осуществляется тем же самым способом, что и ранее, через атрибуты методов, однако теперь идентификация типа события происходит не по строковому значению (имя метода или параметр атрибута), а по типу параметра метода (в примере это классы StartEvent, ChangeHealthEvent и DeathEvent).
Таким образом, используя описанную выше реализацию, мы получили максимально возможную гибкость в коде, поскольку теперь мы можем как угодно менять передаваемые данные в событиях без значительных затрат, нам достаточно изменить тело обработчика под новые параметры. В случае же, если мы захотим изменить имя события (имя класса), компилятор скажет нам в каких местах кода используется старый вариант. При этом необходимость менять имя метода обработчика полностью отпадает.
Я постарался описать в этой статье все возможные на мой субъективный взгляд способы построения систем обмена данными между компонентами на основе уведомлений. Все эти способы были использованы мной в разных проектах и разной сложности: от простых мобильных проектов, до сложных PC. Какую систему использовать в вашем проекте – решать только вам.
PS: я намеренно не стал описывать в статье построение системы сообщения на основе SendMessage-функций, поскольку по сравнению с остальными она не выдерживает критики не только по удобству, но и по скорости работы.
Квестовая система диалогов
Продолжаем тему организации диалогов в игре. По сути это эволюция предыдущего нашего проекта, который заточен для создания простых диалогов. Однако на этот раз, внесены значительные изменения, в результате появилась возможность создавать полноценные квесты. Система диалога позволяет следующее: взять квест, отказаться от него или сдать. Всё это регулируется с помощью атрибутов диалога и напрямую зависит от состояния, запрашиваемого квеста. С помощью атрибутов мы можем фильтровать строки диалога. Например, если квест не взят, то показывать один текст, если же квест активен, то тогда будет показан другой текст.
Итак, окно диалога мы делаем на основе Scroll View, а в качестве текстовых полей, будем использовать UI кнопки.
Для кнопки нам понадобится скрипт:
Затем нужно определиться, сколько вариантов ответа может быть одновременно доступно игроку в диалоге. Если в течении всей игры максимальное число, например, будет равно пяти, то нам нужно создать шесть полей. То есть +1 к общему числу, так как одно текстовое поле, всегда будет использоваться для вывода текста NPC.
Заполняем окно диалога нужным количеством кнопок, незабываем что у каждой кнопки должен быть выше указанный скрипт.
Такая конструкция окна была выбрана для оптимизации работы системы диалога, то бишь, вместо того чтобы каждый раз использовать создание/удаление объектов, мы будем просто обновлять те, которые уже есть.
Далее, на Canvas окна диалога, вешаем управляющий скрипт:
Здесь и происходит магия. Логика его работы в принципе простая, загрузка и расшифровка файла, в котором прописаны теги и текст диалога. Данный менеджер работает исключительно в связке с менеджером скриптов.
Каждый скрипт уникален, может отслеживать различные игровые события или объекты. Поэтому связь с ним нужно организовывать через менеджера, которому мы передаем имя квеста и какой-нибудь параметр, а уже в менеджере, по имени фильтруем и находим нужный квест и обращаемся к нему напрямую.
Менеджер квестов выглядит следующим образом:
Тег answer используется для вывода меню диалога, ответы игрока. Атрибуты тега:
Если мы хотим чтобы строка меню отображалась, пока квест еще не активен, то:
Если квест активен то тогда questValue не будет равно нулю, соответственно эта строка не появится в диалоге.
При этом состояние questValue мы запрашиваем у менеджера, который в свою очередь делает запрос у конкретного квеста. Это значит, что нам нужно прописывать состояние переменной в каждом квесте отдельно, так как каждый из них уникален.
Предлагаем скачать рабочий пример проекта:
В этом проекте реализован квест, который можно выполнить. И наглядно посмотреть как организованно взаимодействие скриптов. Кроме того, в проекте присутствует генератор диалога, с помощью которого был создан файл диалога. В общем, изучаем и пробуем.