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

Итак, на повестке дня у нас, один из вариантов реализации системы диалогов в игре. Сразу оговоримся, что представленный проект из разряда, дешево и сердито. То есть, реализованы только базовые возможности. Идея заключается в том, что диалоги в игре разбиты на отдельные файлы. Каждый диалог, это конкретный файл, XML типа. Например, когда игрок подходит к NPC, делается запрос на диалог, связанный с этим персонажем. Происходит его загрузка и на основе полученных данных, создается интерфейс пользователя. При этом предусмотрена возможность не только вручную создавать файл, но и с помощью простенького XML генератора. Еще один важный момент в том, что такой подход позволяет создать сколько угодно языковых вариантов диалогов.

Настройка сцены. Для отображения диалога воспользуемся системой UI Юнити. Добавляем Scroll View и настраиваем его для вертикальной прокрутки. Затем добавляем еще Button, ширину кнопки подстраиваем под ширину окна прокрутки, высота будет регулироваться через скрипт. Еще нужно настроить присет трансформа кнопки, устанавливаем как показано на скриншоте:

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

Дополнительно на кнопку вешаем скрипт:

using UnityEngine;
using UnityEngine.UI;
using System.Collections;

public class ButtonComponent : MonoBehaviour {

	public Button button;
	public Text text;
	public RectTransform rect;

}

Здесь мы указываем компоненты кнопки, для упрощенного доступа к ним в дальнейшем.

Кстати говоря, в проекта должна быть папка Resources в ней будут хранится все файлы диалогов. Внутри нее создаем подпапку, например Russian, то есть, это будет у нас язык по умолчанию. В дальнейшем можно просто сделать дубликат этой папки, обозвать ее другим языком и сделать перевод текста в файлах. Таким образом, можно сделать несколько языков в игре.

Как должен выглядеть XML:

<dialogue name="Example">
  <node id="0" npcText="Привет!">
    <answer text="Привет." toNode="1" />
    <answer text="Мне пора." exit="True" />
  </node>
  <node id="1" npcText="Как дела?">
    <answer text="Нормально." exit="True" />
  </node>
</dialogue>

npcText - текст который говорит NPC. Затем идут ответы игрока.
Атрибут toNode - перенаправление в другой узел диалога.
Атрибут exit закрывает диалоговое окно.

Всё это дело можно прописывать вручную, однако мы напишем небольшой скрипт:

using UnityEngine;
using System.Collections;
using System.IO;
using System.Xml;

public class DialogueGenerator : MonoBehaviour {
	
	public string fileName = "Example"; // имя генерируемого файла (без разрешения)
	public string folder = "Russian"; // подпапка в Resources, для записи
	public DialogueNode[] node;

	public void Generate()
	{
		string path = Application.dataPath + "/Resources/" + folder + "/" + fileName + ".xml";

		XmlNode userNode;
		XmlElement element;

		XmlDocument xmlDoc = new XmlDocument();
		XmlNode rootNode = xmlDoc.CreateElement("dialogue");
		XmlAttribute attribute = xmlDoc.CreateAttribute("name");
		attribute.Value = fileName;
		rootNode.Attributes.Append(attribute);
		xmlDoc.AppendChild(rootNode);

		for(int j = 0; j < node.Length; j++)
		{
			userNode = xmlDoc.CreateElement("node");
			attribute = xmlDoc.CreateAttribute("id");
			attribute.Value = j.ToString();
			userNode.Attributes.Append(attribute);
			attribute = xmlDoc.CreateAttribute("npcText");
			attribute.Value = node[j].npcText;
			userNode.Attributes.Append(attribute);

			for(int i = 0; i < node[j].playerAnswer.Length; i++)
			{
				element = xmlDoc.CreateElement("answer");
				element.SetAttribute("text", node[j].playerAnswer[i].text);
				if(node[j].playerAnswer[i].toNode > 0) element.SetAttribute("toNode", node[j].playerAnswer[i].toNode.ToString());
				if(node[j].playerAnswer[i].exit) element.SetAttribute("exit", node[j].playerAnswer[i].exit.ToString());
				userNode.AppendChild(element);
			}

			rootNode.AppendChild(userNode);
		}

		xmlDoc.Save(path);
		Debug.Log(this + " Создан XML файл диалога [ " + fileName + " ] по адресу: " + path);
	}
}

[System.Serializable]
public class DialogueNode
{
	public string npcText;
	public PlayerAnswer[] playerAnswer;
}


[System.Serializable]
public class PlayerAnswer
{
	public string text;
	public int toNode;
	public bool exit;
}

А к нему еще один вспомогательный:

#if UNITY_EDITOR
using UnityEngine;
using System.Collections;
using UnityEditor;

[CustomEditor(typeof(DialogueGenerator))]

public class DialogueGeneratorEditor : Editor {

	public override void OnInspectorGUI()
	{
		DrawDefaultInspector();
		GUILayout.Space(15);
		DialogueGenerator e = (DialogueGenerator)target;
		if(GUILayout.Button("Generate Dialogue XML"))
		{
			e.Generate();
		}
	}
}
#endif

С помощью этой парочки, можно генерировать XML. Предназначены они для работы только в редакторе. Просто заполняем массив диалога в инспекторе Юнити, настраиваем опции и жмем кнопочку Generate и готово. Файл будет создан по указанному адресу.


Далее. Что нам еще нужно? Чтение созданного файла и создание окна диалога.

За это отвечает данный скрипт:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using UnityEngine.UI;
using System.Xml;
using System.IO;

public class DialogueManager : MonoBehaviour {

	public ScrollRect scrollRect;
	public ButtonComponent button;
	public string folder = "Russian"; // подпапка в Resources, для чтения
	public int offset = 20;

	private string fileName, lastName;
	private List<Dialogue> node;
	private Dialogue dialogue;
	private Answer answer;
	private List<RectTransform> buttons = new List<RectTransform>();
	private float curY, height;
	private static DialogueManager _internal;

	public void DialogueStart(string name)
	{
		if(name == string.Empty) return;
		fileName = name;
		Load();
	}

	public static DialogueManager Internal
	{
		get{ return _internal; }
	}

	void Awake()
	{
		_internal = this;
		button.gameObject.SetActive(false);
		scrollRect.gameObject.SetActive(false);
	}

	void Load()
	{
		scrollRect.gameObject.SetActive(true);

		if(lastName == fileName) // проверка, чтобы не загружать уже загруженный файл
		{
			BuildDialogue(0);
			return;
		}

		node = new List<Dialogue>();

		try // чтение элементов XML и загрузка значений атрибутов в массивы
		{
			TextAsset binary = Resources.Load<TextAsset>(folder + "/" + fileName);
			XmlTextReader reader = new XmlTextReader(new StringReader(binary.text));

			int index = 0;
			while(reader.Read())
			{
				if(reader.IsStartElement("node"))
				{
					dialogue = new Dialogue();
					dialogue.answer = new List<Answer>();
					dialogue.npcText = reader.GetAttribute("npcText");
					node.Add(dialogue);

					XmlReader inner = reader.ReadSubtree();
					while(inner.ReadToFollowing("answer"))
					{
						answer = new Answer();
						answer.text = reader.GetAttribute("text");

						int number;
						if(int.TryParse(reader.GetAttribute("toNode"), out number)) answer.toNode = number; else answer.toNode = 0;

						bool result;
						if(bool.TryParse(reader.GetAttribute("exit"), out result)) answer.exit = result; else answer.exit = false;

						node[index].answer.Add(answer);
					}
					inner.Close();

					index++;
				}
			}

			lastName = fileName;
			reader.Close();
		}
		catch(System.Exception error)
		{
			Debug.Log(this + " Ошибка чтения файла диалога: " + fileName + ".xml >> Error: " + error.Message);
			scrollRect.gameObject.SetActive(false);
			lastName = string.Empty;
		}

		BuildDialogue(0);
	}

	void AddToList(bool exit, int toNode, string text, bool isActive)
	{
		BuildElement(exit, toNode, text, isActive);
		curY += height + offset;
		RectContent();
	}

	void BuildElement(bool exit, int toNode, string text, bool isActiveButton)
	{
		ButtonComponent clone = Instantiate(button) as ButtonComponent;
		clone.gameObject.SetActive(true);
		clone.rect.SetParent(scrollRect.content);
		clone.rect.localScale = Vector3.one;
		clone.text.text = text;
		clone.rect.sizeDelta = new Vector2(clone.rect.sizeDelta.x, clone.text.preferredHeight + offset);
		clone.button.interactable = isActiveButton;
		height = clone.rect.sizeDelta.y;
		clone.rect.anchoredPosition = new Vector2(0, -height/2 - curY);

		if(toNode > 0) SetNextDialogue(clone.button, toNode);
		if(exit) SetExitDialogue(clone.button);

		buttons.Add(clone.rect);
	}

	void RectContent()
	{
		scrollRect.content.sizeDelta = new Vector2(scrollRect.content.sizeDelta.x, curY);
		scrollRect.content.anchoredPosition = Vector2.zero;
	}

	void ClearDialogue()
	{
		curY = offset;
		foreach(RectTransform b in buttons)
		{
			Destroy(b.gameObject);
		}
		buttons = new List<RectTransform>();
		RectContent();
	}

	void SetNextDialogue(Button button, int id) // добавляем событие кнопке, для перенаправления на другой узел диалога
	{
		button.onClick.AddListener(() => BuildDialogue(id));
	}

	void SetExitDialogue(Button button) // добавляем событие кнопке, для выхода из диалога
	{
		button.onClick.AddListener(() => CloseDialogue());
	}

	void CloseDialogue()
	{
		scrollRect.gameObject.SetActive(false);
		ClearDialogue();
	}

	void BuildDialogue(int current)
	{
		ClearDialogue();
		AddToList(false, 0, node[current].npcText, false);
		for(int i = 0; i < node[current].answer.Count; i++)
		{
			AddToList(node[current].answer[i].exit, node[current].answer[i].toNode, node[current].answer[i].text, true);
		}
	}
}
	
class Dialogue
{
	public string npcText;
	public List<Answer> answer;
}


class Answer
{
	public string text;
	public int toNode;
	public bool exit;
}

Здесь показано как загрузить XML из ресурсов Юнити, а затем извлечь из него значения атрибутов. После загрузки, массивы будут иметь точно такой же вид, как при создании в генераторе, соответственно и в окне, узлы будут отображаться в таком же виде.

Ниже представлен рабочий проект, где можно изучить работу диалогов:

Внимание! Посетители, находящиеся в группе Гости, не могут скачивать файлы.

Комментариев 11

Офлайн
beril 21 мая 2016
Вы можете добавлять скринов или гифки того, что в результате должно получиться.... А то не понятно, как оно будет выглядеть sob
Офлайн
Light 22 мая 2016
beril, в данном случаи, проще скачать пакет и самому потестить.
Офлайн
Достаточно не дурно и очень понятно, но для 3д игры врят ли подойдет
Офлайн
Light 24 января 2017
Артемий Шабайло, подойдет для любой.
Офлайн
Light,
у меня демо не работает запускаю игру там появляется 8 кнопок с названием text кликаю ничего не работает.А также и зеленые квадраты исчезают
Офлайн
RedRanger 17 октября 2018
Light,
Приветствую. Хотел получить совет - как сделать диалоги над игроком? То бишь, я подхожу к NPC, нажимаю, допустим "Е" и диалог персонажа высвечивается над ним. Создавать второй канвас или можно обойтись без него?
Офлайн
Light 18 октября 2018
RedRanger, реплика НПС? Это надо выводить в отдельный UI Text.
Офлайн
RedRanger 18 октября 2018
Light,
Это я понял, меня больше интересует куда этот UI Text пихать. В новый, отдельный Canvas с Render Mode = World Space? Или Все можно провернуть в Canvas'e игрока?
Офлайн
Light 18 октября 2018
RedRanger, можно в одном Canvas делать, один UI Text для всех НПС, просто менять его позицию на экране.
Офлайн
RedRanger 18 октября 2018
Light,
Хорошо, а если стоит вопрос о многоразовом использовании? Наверняка играл в Ведьмака 3 и видел там реплики у многих NPC. Создавать копии в таком случае?
Офлайн
Light 18 октября 2018
RedRanger, тогда надо делать пул из UI Text в расчете на максимальное число реплик на экране одновременно. А потом просто прогонять массив пула.
Информация
Посетители, находящиеся в группе Гости, не могут оставлять комментарии к данной публикации.
  • Дешевый хостинг
  • Яндекс.Метрика