Unity предлагает отличные инструменты для создания небольших игр с малым количеством переменных. Когда проект разрастается, становится крайне неудобно изменять в стандартном Inspector данные, особенно если все данные хранятся в одном месте (что крайне удобно при редактировании).
Как сделать удобную для редактирования и понятную для геймдизайнеров базу данных?
Об этом ниже.
Для начала создадим класс DataBase который будет отвечать за общение с базой данных или хранение данных для небольших проектов.
Стоит поговорить с гемдизайнором и определится какой список типов данных потребуется. Для примера это будет 4 типа данных.
Записываем их в enum (потом будет понятно зачем)
public enum DataType
{
Item=0,
Ship=1,
Spell=2,
Recipe=3,
}
Тут-же создаем интерфейс для общения с нашими типами данных. В нем обязательными полями должны быть Id, Name и одна функция DrawGui() которая в дальнейшем и будет отвечать за отрисовку нашего интерфейса.
public interface IData
{
int Id { get; set; }
string Name { get; set; }
#if UNITY_EDITOR
void DrawGui();
#endif
}
Функция DrawGui() нам не нужна в скомпилированном проекте так что ее настоятельно рекомендую обнести директивой компиляции.
В самом классе DataBase добавляем функции для доступа к данным:
List GetDatas(DataType type) — для получения всего списка данных указанного типа
IData GetData(DataType type, int id) — для получения данных по id
void AddData(DataType type) — добавления нового значения
void UpdateData(DataType type, IData data) — обновление указанного значения
void Remove(DataType type, int id) — удаления указанного значения
В этих функциях будет описано взаимодействие с базой данных например с PostgreSQL.
Получилось вот так
Теперь надо создать класс для отрисовки окна в редакторе Unity. Создаем папку Editor и в ней скрипт DataBaseEditor. Наследуем его от EditorWindow.
Нужно реализовать переключение между созданными ранее типами данных.
Объявим статичную переменную DataType CurrentDataType и сделаем выбор типа данных в стандартной функции OnGUI()
Тут есть 2 варианта:
- воспользоваться стандартным EditorGUILayout.EnumPopup()
- для каждого элементы enum создать свою кнопку
Лично я предпочитаю второй вариант так получается удобнее. Вот такого кода вполне достаточно
var types = Enum.GetValues(typeof (DataType));
GUILayout.BeginHorizontal();
foreach (var type in types)
{
if (GUILayout.Button(type.ToString()))
{
CurrentDataType = (DataType) type;
}
}
GUILayout.EndHorizontal();
Можно немного поиграть и сделать выделение выбранного типа цветом.
Теперь добавим одну кнопку для добавления данных в нашу базу данных
if (GUILayout.Button("Add"))
{
DataBase.AddData(CurrentDataType);
}
И осталось вызвать наше окно из меню Unity для этого:
- создаем новый элемент меню
[MenuItem("DataBase/EditDataBase")]
- по клику на элемент меню создаем наше окно
static void Init()
{
var window = (DataBaseEditor)GetWindow(typeof(DataBaseEditor));
window.Show();
}
В результате должно получится что-то такое
Теперь нам нужно окошко в котором будет отображаться сама форма для изменения данных.
Создаем новый класс DataWindowEditor и как и ранее наследуем его от EditorWindow.
В этом классе нам нужно отследить 2 события (открытие и закрытие окна).
При открытии будем передавать этому окну данные для отображения и сохранять ранее открытые данные чтоб геймдизайнер случайно не потерял свою работу.
При закрытии сохранять изменения в нашу базу данных.
public void OnOpen(IData data,DataType type)
{
if (_data != null)
{
DataBaseEditor.DataBase.UpdateData(_dataType, _data);
}
_data = data;
_dataType = type;
}
void OnDestroy()
{
DataBaseEditor.DataBase.UpdateData(_dataType, _data);
}
И осталось нарисовать саму форму в OnGUI
public void OnGUI()
{
if (_data==null)
{
Close();
return;
}
_data.DrawGui();
}
Теперь нам надо получить список наших данных. Воспользуемся ранее созданной функцией GetDatas.
И отрисовываем наши данные средствами GUILayout, при клике на элемент открыть его представление.
var datas = DataBase.GetDatas(CurrentDataType);
foreach (var data in datas)
{
GUILayout.BeginHorizontal();
if (GUILayout.Button("Id: " + data.Id + " Name: " + data.Name))
{
var window = (DataWindowEditor)GetWindow(typeof(DataWindowEditor));
window.OnOpen(data,CurrentDataType);
window.Show();
}
if (GUILayout.Button("-"))
{
DataBase.Remove(CurrentDataType,data.Id);
}
GUILayout.EndHorizontal();
}
Вторая кнопка отвечает за удаление указанного элемента.
Теперь перейдем непосредственно к нашим данным.
Создадим новый класс Item.
Добавляем поля, нужные геймдизайнеру.
В моем случае пока это будет цена предмета.
Наследуем класс от ранее созданного интерфейса IData.
Функцию DrawGui() как и в интерфейсе стоит пометить директивой условной компиляции и в теле функции описать само отображение элемента:
#if UNITY_EDITOR
public void DrawGui()
{
GUILayout.Label("Id: "+Id);
Name = EditorGUILayout.TextField("Name: " , Name);
Price = EditorGUILayout.IntField("Price: " , Price);
}
#endif
На этом все. Переходим в редактор и получаем вот такую картинку:
Для остальных типов данных так-же создаем отдельные классы и делаем все тоже само что и для класса Item.
Комментарии (9)
LeonThundeR
14.11.2017 13:13Зачем хранить все в 4х Dictionary, а не в 1ом?
Blackelf2 Автор
14.11.2017 13:15Потому что в дальнейшем это будет хранится в отдельных таблицах базы данных и у каждой будут свои индексы. В один словарь их не засунуть(ключи будут повторятся)
LeonThundeR
14.11.2017 13:41Если цель хранить их в разных не связанных таблицах БД, согласен.
Но так будет очень неудобно работать со всеми элементами в целом.
Например, мы не сможем получить экземпляр IData только по id, без указания его типа, использовать всю мощь полиморфизма и т.д.
Не лучше ли сделать один Dictionary<int, IData>, а базе данных разнести информацию по таблицам следующим образом:
В «основной» таблице например BaseItems хранить автоинкрементный id и поля общие для всех типов IData.
Для каждого типа IData создать таблицу, которая будет хранить только поля специфичные для данного типа и id. И по id связать все эти таблицы с BaseItems.Blackelf2 Автор
14.11.2017 13:54Конечно так тоже можно.
Только тогда в IData надо еще добавить поле с типом данных чтоб вытаскивать весь список.
Возможно даже будет удобнее надо будет попробовать.LeonThundeR
14.11.2017 13:58Поле с типом данных не нужно. Нужно просто наследовать классы описывающие разные типы от абстрактного класса, который наследовать от IData.
Blackelf2 Автор
14.11.2017 15:59Без этого поля будет проблемно получить список данных указанного типа (например все предметы). И нет понимания из какой таблицы потом дергать описания элемента
LeonThundeR
14.11.2017 16:14Проблемы не будет т.к. у них будут разные классы. Описание любого элемента можно будет легко получить только по его id. А при запросе к бд, если нужно получить данные для нескольких (или всех) элементов разных типов нужно будет использовать LEFT JOIN, а для какого-то конкретного типа INNER JOIN.
Tutanhomon
Удобное — это Excel таблица и ее импорт в юнити. А то что вы описали — это НЕ удобно.
Для локальных каких-то «баз» — например, баз настроек процедурных анимаций, или не знаю чего еще — да. Но геймдизайнеру такое не показывайте. Здесь он не сможет параллельно конфигурации еще и баланс расписать.
Blackelf2 Автор
Таблица не всегда удобно. Например при если есть базовый предмет(Цена, имя, Id) и от него унаследованы оружие, броня, хилки и т.д.
Опять же удобнее перекрещивать таблицы(например если рецепт ссылается на несколько ранее созданных предметов, или лут из монстра)
Для выше описанного редактора можно прикрутить еще и нодовскую систему (для ветвления диалогов например) и это будет визуально понятно.
Баланс да придется отдельно просчитывать хотя некто не мешает чучуть доработать систему.
Кстати эта система создавалась под руководством геймдиза.