История первая
Некоторое время назад я работал в одной игровой компании, которой руководил немец. Создание игр не было основным бизнесом этого немца. Основные доходы он получал от продажи косметики и от сдачи коммерческой недвижимости в аренду. Наличие игровой компании было способом выделиться среди своих знакомых бизнесменов.
Игровая компания немца разрабатывала 3 вида игр:
- Флэш-игры для мобильных телефонов с поддержкой технологии J2ME.
- Обучающие игры для портативной игровой приставки Nintendo DS. Заказчиками этих игр были европейские издатели, а покупателями — родители, чьи чада имели проблемы с обучением по математике, английскому или немецкому языкам. Подразделение игр для Nintendo DS выпустило много игр. Хотя они и не стали AAA-тайтлами, но окупили свою разработку и принесли небольшую прибыль.
- Игры для платформы Nintendo Wii.
В последней команде был я. Команда должна была разработать игру для маленьких девочек по детскому бренду. Бренд был достаточно известен в Германии (это был основной рынок) и в ряде других европейских стран: во Франции и в Великобритании.
С самого начала была понятна только общая канва игры. Маленькая фея ходит по саду, встречает своих подруг (других фей), разговаривает с ними и приглашает на вечеринку. Подготовка к этой вечеринке занимает значительную часть игры: фея украшает сад, собирает яблоки, готовит из них праздничный пирог. Вечеринка проходит весело: фея и ее подруги играют на музыкальных инструментах, а затем — танцуют.
Предполагалось, что игра будет представлять собой последовательность мини-игр. Каждая мини-игра посвящена определенной теме: украшению сада, сбору яблок, приготовлению праздничного пирога, исполнению музыки и танцам. Не смотря на то, что были понятны темы мини-игр, не были понятны их детали. Геймдизайнера в команде не было.
Поначалу над игрой работало два программиста и один 3D-моделлер. Когда я присоединился к команде, не было ни проработанного игрового дизайна (или человека за него отвечающего), ни платформы, на которой можно было бы сделать игру.
На второй день после моего устройства на работу ко мне подошел владелец компании и спросил: “Когда будет готова игра?”. К тому времени у меня был опыт работы в игровой индустрии и, согласно этому опыту, разработка такой несложной игры командой из 3-4 человек занимала около года. Так я и сказал, что потребуется где-то около года.
На это немец мне ответил: “Такого срока нет. Игра должна быть сделана через 3 месяца”. Немного офигевая, я спросил: “А почему через три месяца?” На что немец мне ответил: “У меня будет день рождения, и нужно, чтобы к моему дню рождения игра была сделана”.
Для такого жесткого требования по срокам были и объективные предпосылки. У немца был заключен контракт с издательством, согласно которому он получал фиксированную сумму денег на разработку плюс процент от продаж. Но процент от продаж он получал только в том случае, если игра будет сдана в срок. А срок сдачи игры совпадал с его днем рождения.
После добавления в команду 4-го программиста, на повестке дня встал вопрос о выборе начальства. Как обычно формируют проектную команду? Согласно правильному подходу, сначала ищется руководитель, а затем уже он нанимает под себя остальных сотрудников. Здесь же было сделано все наоборот. Сначала были найдены сотрудники, а затем начальство стало смотреть, кто из них может быть руководителем. Ладно бы, если бы при выборе руководителя во главу угла поставили бы профессиональные навыки.
Но в качестве руководителя выбрали человека, который пообещал сделать игру за 3 месяца.
После назначения руководителя команды началась работа над игрой. Проблемы с игровым дизайном и отсутствием технологии так и не были решены. Поэтому работа не двигалась. Это изрядно злило вновь назначенного руководителя. Нередкий диалог между ним и программистом:
Руководитель: “Где игра про яблоки?”
Программист: “Еще не сделана”.
Руководитель: “Когда будет сделана?”
Программист: “Не знаю. Мне непонятно, что программировать”.
Руководитель: “Ну ты же — программист! Придумай!”
Систему разработки игр в игровой студии можно представить в виде иерархии из 4-х системных уровней.
Первый системный уровень — это уровень ресурсов. К ним относятся: финансы, сотрудники (которые доступны либо на рынке, либо внутри компании), бренд (если игра создается по бренду), первоначальная концепция игры. Этот уровень задает основу для всего проекта.
Второй системный уровень — это игровой дизайн. Он не должен представлять “дизайн игры в вакууме”. Наоборот, он должен опираться на имеющиеся ресурсы.
Например, бюджет упомянутой игры для девочек был незначительным. К тому же, на разработку могла быть потрачена лишь половина суммы, т.к. вторая половина предназначалась для покупки оборудования (компьютеров, телевизоров, девкитов, тесткитов) и лицензирование программ для художников и программистов. Небольшой бюджет разработки и ограничения платформы исключали возможность использования в игре реалистичной графики и реалистичной физики. Денег не было на приобретение лицензии технологически продвинутого движка, а игровая консоль не позволяла использовать шейдеры. Приходилось использовать то, что уже есть — бесплатный 3D-движок NintendoWare компании Nintendo.
Применение физического движка дает игроку свободу передвижения. Игрок может двигаться по игровому уровню в любом направлении до тех пор, пока не столкнется с препятствием. Но поддержка физики требует определенных затрат как со стороны дизайнеров, которые должны расставлять на сцене collision boxes, так и со стороны программистов, которым приходится обрабатывать коллизии. Все это сказывается на длительности разработки. Поэтому мы решили ограничиться использованием физики только в одной, главной сцене и реализовать мини-игры исключительно с использованием анимаций.
Третий системный уровень — это технологическая платформа. Платформа позволяет создавать игры по имеющемуся игровому дизайну, вернее, определенные виды игр. Для достижения этой цели она использует имеющиеся ресурсы первого системного уровня.
Платформа включает в себя определенные технические и управленческие инструменты. В качестве технических инструментов выступают:
- система управления игровыми ресурсами;
- машина игровых состояний, или система управления игровым потоком (game flow);
- система сценариев (scripting system);
- система рассылки и обработки сообщений;
- система обработки ввода (например, в случае игровой приставки Nintendo Wii она может распознавать движения контроллера Wii Remote);
- пользовательский интерфейс;
- система загрузки ассетов (поверхности, статических и динамических объектов, сценариев);
- система загрузки и сохранения пользовательских данных;
- и т.д.
Управленческие инструменты включают:
- систему управления задачами;
- систему управления качеством;
- систему управления ресурсами;
- систему управления версиями;
- оценку проекта;
- план проекта;
- и т.д.
Четвертый системный уровень — это сама игра или серия подобных игр.
Разбиение на системные уровни позволяет бороться со сложностью. Есть разница, делает ли программист мини-игру по определенному дизайну и с использованием конкретной платформы, по которой он может задать вопросы другим опытным программистам, работающим в той же компании, или же “льет код” на низкоуровневом API. Сложность реализации одной и той же мини-игры в обоих случаях будет неравноценной.
Например, после того, как был придуман и описан гейм-дизайн каждой мини-игры и была реализована платформа, один программист мог реализовать одну мини-игру в черновом качестве за 3 рабочих дня. Еще 2 дня требовались для того, чтобы добавить в мини-игру мультиплеер и навести некоторые красивости. Таким образом, создание одной мини-игры в финальном качестве обходилось в 5 человеко-дней. Между тем, как до этого момента (при отсутствующих дизайне и технологической платформе) проходили недели, а работа — не двигалась.
История вторая
Однажды меня пригласили на собеседование в один стартап. Проект был не очень интересный для меня, но подкупило то, что компания находилась буквально в пяти минутах ходьбы от моего дома. Я подумал, что впервые в своей карьере смогу не тратить время на дорогу на работу.
Собеседование в стартап проводилось по скайпу, и вопросы были достаточно адекватные. В основном, они касались моего предыдущего опыта. Что я делал? Чем занимался? С какими задачами сталкивался?
Процедура отбора кандидатов в стартап была двухступенчатая. Собеседование я прошел успешно, поэтому через некоторое время руководитель стартапа связался со мной и предложил выполнить тестовое задание.
Тестовое задание звучало так:
Разработать текстовый онлайн-редактор наподобие того, что используется в Google Docs.
Необходимо было поддержать следующие возможности:
- разбиение текста на абзацы;
- автоматическое распределение текста по страницам;
- форматирование текста;
- возможность одновременного редактирования несколькими пользователями;
- отображение и маркировка всех изменений, сделанных разными пользователями;
- хранение документа в “облаке”.
На выполнение тестового задания выделялась неделя.
Не стоит говорить, что я не стал выполнять такое тестовое задание, а, вежливо поблагодарив руководителя, более с ним не связывался.
Промежуточные итоги
Подводя итоги, можно утверждать, что в обеих историях была допущена одинаковая ошибка — пропуск системного уровня [6].
В первой истории при работе над игрой для девочек были пропущены “уровень игрового дизайна” и “уровень технологической платформы”, что привело к застопориванию работы над проектом и увеличению времени разработки. Работа над проектом была разблокирована только после того, как были добавлены недостающие уровни: проработан игровой дизайн и реализована (пусть и легковесная) платформа.
Во второй истории тоже пропущены системные уровни. Предложенная задача выглядит явно сложнее обычного тестового задания. Для борьбы со сложностью решение надо бы представить в виде иерархической системы, где нижние слои обеспечивают работу слоев, расположенных выше. Но такая работа требует значительных трудозатрат и предъявляет к квалификации инженера завышенные требования. Он должен разбираться буквально во всех областях: и в сетевых протоколах, в текстовых редакторах. Поэтому она более доступна для компании, чем для одного человека.
Иерархия системных уровней
Подход с выявлением системных уровней можно использовать при проектировании приложений. Конечно, если речь идет о написании какой-то простой программы, то в разбиении на уровни нет необходимости. Многоуровневая (или многослойная) архитектура имеет смысл, если разрабатывается сложное приложение или приложение средней сложности.
Выявление системных уровней — это первый шаг к разработке архитектуры. Если вы читали книгу по объектно-ориентированному анализу и проектированию, то наверняка не раз видели, как автор задается вопросом: “Как находить кандидаты в классы?”. Возможно, программируя, вы и сами задавали себе такой же вопрос.
Процесс структурирования приложения не следует начинать с выявления классов. Класс — это слишком маленький элемент абстракции. Если проводить аналогию со строительством, то класс — как отдельный кирпичик. А строительство небоскреба вряд ли следует начинать с вопроса о том, как сложить его из кирпичей.
Трехслойная архитектура
Фаулер [7] и руководство компании Microsoft по проектированию архитектуры приложений [5] выделяют три системных уровня (другими словами — три слоя абстракции), на которые можно разделить разрабатываемое приложение.
Примечание: Оригинальную версию диаграммы можно посмотреть на сайте Microsoft
Первый слой абстракции — это слой доступа к данным. В задачи данного слоя входит абстрагирование от базы данных. SQL-запросы к базе данных, в которой и хранятся данные приложения, скрываются за фасадом, который использует бизнес-слой.
Второй слой абстракции — это слой бизнес-логики. Он содержит объекты предметной области, а также функции для работы с ними. Эти функции и реализуют данную бизнес-логику.
Третий слой абстракции — это слой взаимодействия с пользователем. Данный слой включает компоненты пользовательского интерфейса.
Трехслойная архитектура, описанная в руководстве компании Microsoft, является рабочей. Единственное, у меня вызывает непонимание некоторая вещь, связанная с взаимодействием слоя бизнес-логики со слоем доступа к данным.
Согласно моему пониманию, классы предметной области должны относиться к слою бизнес-логики. С другой стороны, основная задача слоя доступа к данным заключается в том, чтобы получать данные из базы данных и копировать эти полученные данные в бизнес-объекты.
Получается противоречие:
- С одной стороны, бизнес-объекты должны быть объявлены на уровне бизнес-логики, т.к. логически относятся к нему.
- А с другой стороны, бизнес-объекты должны быть доступны на уровне доступа к данным, чтобы в них можно было скопировать данные, прочитанные из базы данных.
Видя это противоречие, мне хочется предложить несколько иной подход к разбиению приложения на системные уровни.
Пятиуровневая архитектура
Предлагаемый мною подход подразумевает разбиение клиентского приложения на 5 системных уровней. Ключевыми из этих уровней являются лишь два. Это означает, что клиентское приложение, как минимум, может состоять из двух системных уровней. Использование же всех 5-ти слоев — это опциональный и, возможно, более желательный вариант.
Подход опробован мной и коллегами на 5-ти проектах. Все эти проекты представляют собой клиентские приложения. Четыре приложения предназначены для создания и редактирования различных игровых ресурсов. Пятое приложение — это сервисная программа, предназначенная для оказания услуг.
Первый уровень можно назвать инфраструктурным. Называется он так, потому что содержит инфраструктуру, т.е. библиотеки, модули, классы и методы, которые используются во всем приложении.
Если разработка приложения ведется на языке программирования C++, то, как правило, в инфраструктурный уровень включают:
- библиотеки работы с растровыми изображениями;
- XML-парсеры;
- библиотеки сжатия и шифрования данных;
- библиотеки контейнеров и различных вспомогательных классов (STL и Boost);
- вспомогательные утилиты, написанные разработчиками приложения.
Второй уровень можно назвать моделью данных. Этот уровень является ключевым, потому что без него вряд ли может обойтись хоть одно клиентское приложение.
Название “модель данных” перекликается с названием “слой доступа к данным” из руководства Microsoft по проектированию приложений. Не смотря на то, что задачи этого уровня приблизительно совпадают с задачами слоя доступа к данным, имеется и существенное отличие:
Модель данных содержит бизнес-объекты, которые, согласно упомянутому руководству, должны включаться в слой бизнес-логики.
Данные объекты либо не содержат никакой логики, либо обладают некоторой минимальной логикой, связанной, например, с верификацией данных. Количество методов, которыми обладают эти объекты, тоже невелико. Как правило, они либо предоставляют упрощенный доступ к другим объектам, связанным с первыми, либо отвечают за сериализацию и десериализацию самого объекта.
В программе для создания визуальных эффектов и в программе для создания анимационных блюпринтов объекты модели данных сами сериализуют себя в XML. Это само сохраняющиеся объекты. Модель данных включает в себя как объекты в памяти в виде экземпляров классов, так и сериализованные XML-файлы на жестком диске.
В GPS-навигаторе модель данных немного сложнее. Она состоит из нескольких специализированных баз данных и прослойки кода, осуществляющей доступ к данным.
Третий уровень — это уровень сервисов или служб. С моей точки зрения именно этот слой соответствует слою бизнес-логики из руководства компании Microsoft.
Как правило, данный слой представляет собой набор функциональных модулей, использующих модель данных. Каждый модуль отвечает за выполнение какой-то одной бизнес-функции.
В некоторых клиентских приложениях таких бизнес-функций может быть много. В некоторых — мало. А в некоторых — они совсем отсутствуют. Поэтому для ряда приложений уровень сервисов может быть пропущен.
Например, в редакторе визуальных эффектов каждый эффект может быть сохранен в формате XML. Однако для того, чтобы данный визуальный эффект мог использоваться в игре, его необходимо преобразовать в бинарный формат. Таким преобразованием занимается компилятор. Этот компилятор и является отдельным сервисом.
В GPS-навигаторе сервисов больше. Потому что это приложение предназначено для оказания услуг пользователям. К числу таких сервисов относятся:
- рисование карты местности, где находится пользователь;
- прокладывание маршрута в заданную точку;
- навигация по маршруту;
- поиск координат по адресу.
Модули, ответственные за оказание услуг, располагаются на уровне сервисов.
Четвертый уровень отвечает за редактирование. Данный уровень характерен для программ-редакторов. Он содержит типовые компоненты или типовые архитектуры редакторов. Прежде всего, я имею в виду:
- Архитектуру Документ/Вид (еще известную из библиотеки классов MFC) [1]
- Undo/Redo Management [3]
Архитектура Документ/Вид осуществляет привязку конкретного расширения файла к определенному типу документа, а также привязывает определенный тип документа к определенному виду, ответственному за его визуализацию.
Undo/Redo Management обеспечивает поддержку отмены операции. Благодаря такой возможности пользователь при редактировании сложного документа в любой момент может отменить ошибочно выполненную операцию. В результате, редактирование становится устойчивым к ошибкам и не страшным для человека. Такая функциональность стала де-факто стандартом различных программ редактирования.
Пятый уровень тоже является ключевым. Без него не обойдется ни одно клиентское приложение. Этот уровень содержит компоненты, отвечающие за визуализацию данных и взаимодействие с пользователем.
Не смотря на то, что этот уровень называется точно так же, как и слой из руководства Microsoft, тем не менее, он имеет чуть меньше обязанностей. Потому что, в моем понимании, слой взаимодействия с пользователем из руководства Microsoft, составлен из двух наших уровней: уровня редактирования и уровня взаимодействия с пользователем.
Уровень представления содержит компоненты пользовательского интерфейса, которые очень тесно связаны с компонентами редактирования.
Пример 1. Редактор All Sparks
Обзор
Редактор All Sparks представляет собой приложение для создания визуальных эффектов. По внешнему виду данное приложение напоминает аудиоредактор. Этому есть обоснование: как и звук, визуальный эффект имеет определенную длительность. Поэтому окно редактирования визуального эффекта содержит ось времени (timeline), расположенную горизонтально. Под осью времени расположены треки, которые тоже ориентированы горизонтально. Пользователь может добавлять и удалять треки.
Главное окно редактора All Sparks
На треках размещаются компоненты. Пользователь может выбирать эти компоненты из списка Component List и перетаскивать их на треки.
В качестве компонентов выступают:
- частицы;
- биллборды;
- трехмерные модели;
- источники света;
- музыка и звуки;
- различные силы, например:
- сила тяготения;
- сила сопротивления воздуха;
- сила турбулентности;
- и т.д.
Компоненты-силы воздействуют на компоненты-частицы. Если расположить компонент “частицы” на одном треке, а под ним расположить компонент “сила тяготения”, то во время проигрывания эффекта на разлетающиеся частицы будет действовать сила тяжести. Постепенно частицы будут падать вниз.
Компоненты располагаются на треках
Каждый компонент представлен в виде прямоугольника. Поскольку компонент может занимать место только на одном треке, то высота компонента совпадает с высотой трека. Длина компонента задает длительность его существования, а начало — момент времени его появления при воспроизведении эффекта.
Для того, чтобы пользователь мог быстро идентифицировать тип компонента, каждый тип компонента имеет свой цвет.
После размещения компонента на треке, пользователь может задавать различные его свойства. Свойства компонента отображаются в редакторе свойств, который располагается в нижней части окна редактирования.
Редактор свойств
Редактор свойств представляет собой таблицу из двух столбцов и множества строк. В левой колонке указываются названия свойства, а в правой — значения.
Пользователь может посмотреть на то, как выглядит визуальный эффект, в окне просмотра. Это окно располагается в левой части главного окна программы. Для того, чтобы воспроизвести эффект, пользователь должен нажать кнопку Play. После этого эффект компилируется в бинарный формат и передается игровому движку для воспроизведения.
Визуальный эффект в окне просмотра
Архитектура
Архитектуру редактора визуальных эффектов можно представить в виде иерархии из 5-ти системных уровней:
Первый системный уровень — это инфраструктура. Он представляет собой набор вспомогательных классов. Как правило, эти классы содержат какие-то математические методы, которые отсутствуют в стандартном классе Math, или же какие-то дополнительные операции со строками и с именами файлов. Инфраструктурный уровень очень небольшой.
Второй системный уровень — это модель данных. Модель данных описывает структуру визуального эффекта: треки, компоненты на них, свойства компонентов.
Каждый класс модели данных имеет методы Load и Save, которые предназначены для загрузки и сохранения объекта из/в XML. Для чтения и записи XML используется LINQ to XML.
public class Component
{
public Dictionary<Guid, Property> Properties { get; } = new Dictionary<Guid, Property>();
public Component Load(XElement item)
{
if (item == null)
{
throw new ArgumentNullException(nameof(item));
}
// Loading logic
return this;
}
public XElement Save()
{
return new XElement("component",
Properties.Values.Select(prop => prop.Save()).ToArray()
);
}
}
Третий системный уровень — это уровень сервисов и служб. В настоящий момент данный слой содержит только один сервис. Это — компилятор. Компилятор осуществляет преобразование эффекта в бинарный формат, который затем может быть использован движком.
Какое-то время назад редактор визуальных эффектов не был интегрирован с игровым движком. Это были два разных приложения. И для того, чтобы визуальный эффект можно было посмотреть, как он выглядит, существовал еще один сервис, который позволял обмениваться данными между клиентской программой (редактором визуальных эффектов) и сервером, в роли которого выступала программа-просмотр. Этот сервис назывался коммуникатором.
Таким образом, ранее было два сервиса. А сейчас, поскольку произошла интеграция редактора визуальных эффектов с игровым движком, необходимость в коммуникаторе отпала, и остался один сервис — компилятор.
Четвертым системным уровнем является уровень редактирования. Он реализует две архитектуры:
- Архитектуру Документ/Вид, которая позволяет открывать файлы визуальных эффектов в исходном формате и создавать для их редактирования и отображения различные виды.
- Undo/Redo Management, которая представляет собой стек команд, которые могут быть выполнены или отменены при редактировании эффекта.
В качестве пятого системного уровня выступает уровень взаимодействия с пользователем, который содержит различные контролы. В нашем случае такими контролами являются линия времени (timeline), треки, редактор свойств и т.д.
Пример 2. Редактор Genome
Обзор
Вторым приложением, архитектуру которого мне хочется рассмотреть, является редактор анимационных блюпринтов. Блюпринт — это программный модуль, реализующий определенную функциональность, но не написанный на языке программирования, а представленный в виде графа. Получается, что редактор блюпринтов — это средство визуального программирования.
Впервые блюпринты были реализованы в движке Unreal Engine 4 [2]. Они используются для разных целей, в том числе, для “программирования” логики игровой сцены или поведения персонажа.
В современных видеоиграх для создания качественной картинки 3D-аниматорам приходится делать очень много различных анимаций. Если речь идет о создании человекоподобного персонажа, то нужно анимировать множество движений: походку, бег, прыжок, приседание, поднимание рук (вместе и каждой руки по отдельности) и т.д.
Часто поведение персонажа нельзя свести к проигрыванию одной из анимаций или к проигрыванию какой-то заранее определенной последовательности этих анимаций. Порою анимации нужно совмещать. Например, персонаж может идти и одновременно нести чашку с горячим кофе в правой руке. Не останавливаясь, он может подносить эту чашку ко рту, чтобы сделать глоток.
Современные 3D-движки позволяют смешивать различные персонажные анимации, т.е. проигрывать их одновременно. Такое требуется для плавного перехода от одной анимации к другой или же когда одна анимация проигрывается на одной части скелета персонажа, а другая — на другой. Для того, чтобы задать логику подобного смешения и используется редактор анимационных блюпринтов. Впервые он появился в движке Unreal Engine 4. И поскольку он показался удобным нашим 3D-аниматорам, то потребовалось создать подобный редактор и у нас.
Главное окно редактора Genome
Анимационный блюпринт представляет собой граф. Он состоит из узлов и соединительных линий. Линии можно провести не в произвольное место узла, а к определенному сокету. Каждый узел графа может содержать один или несколько сокетов.
Узлы представляют собой различные операции, например, сравнение, сложение, вычитание, цикл, ветвление и т.д.
Примеры узлов-операций
Есть и более сложные узлы такие, как машина состояний. Сложные узлы содержат в себе вложенные графы. Например, машина состояний содержит в себе граф состояний, каждый узел которого обозначает определенное состояние, а соединительные линии — условные переходы. Состояния тоже содержат вложенные графы.
Машина состояний
Для попадания во вложенный граф пользователь должен дважды кликнуть по узлу мышью. Вложенный граф можно редактировать точно также, как и родительский. Он тоже состоит из узлов и соединительных линий.
Каждый узел может иметь один или несколько сокетов. Сокеты располагаются с левой и правой сторон узла. Сокеты, которые расположены слева, называются входными, а расположенные справа — выходными. Во входные сокеты поступают входные данные или управляющие сигналы, а из выходных сокетов могут быть получены результаты выполнения операции, за которую отвечает узел.
Узлы с различными входными и выходными сокетами
Сокеты являются типизированными, как и соединительные линии между ними. Существуют сокеты для целых и вещественных чисел, для булевских значений и строк. Типизация сокетов позволяет избежать ошибок, когда выходной сокет одного типа соединяется с входным сокетом другого типа, например, строка с целым числом. Напрямую такое соединение невозможно, только через вспомогательный узел, который выполняет операцию преобразования типа.
Соединительные линии между сокетами задают потоки данных и потоки управления. Для организации потоков управления существуют специализированные сокеты, которые в анимационном графе называются animation pose, а в графе событий — exec.
Подробнее о блюпринтах, узлах и сокетах можно прочитать в Blueprints Visual Scripting из руководства Unreal Engine 4 Documentation [2]. В редакторе Genome был использован аналогичный подход.
Архитектура
Архитектура редактора анимационных блюпринтов может быть представлена иерархией из 5-ти системных уровней. Эта иерархия похожа на ту, что используется и в редакторе визуальных эффектов. Названия уровней, их назначения, отдельные компоненты практически совпадают. Но есть и различия, на которые мне хочется обратить ваше внимание.
Различия обусловлены тем, что в основе редактора анимационных блюпринтов лежит редактор графов. Была поставлена задача переиспользовать этот редактор при переписывании других приложений: редактора материалов и редактора диалогов.
Это означает, что нельзя ограничиться одной моделью данных, которая будет представлять графы для анимационных блюпринтов. Нужна некоторая базовая модель данных для всех возможных графов. И нужен некоторый фреймворк для создания и редактирования графов. Этот фреймворк должны использовать конкретные редакторы: редактор анимационных блюпринтов, редактор диалогов, редактор материалов и т.п.
Инфраструктурный уровень редактора анимационных блюпринтов практически ничем не отличается от аналогичного уровня редактора визуальных эффектов за одним исключением. Поскольку контрол для визуализации графов реализован с помощью технологии WPF, то инфраструктурный слой содержит всякие вспомогательные классы, облегчающие работу с WPF, а также — стили для используемых контролов. Стили применяются не только в редакторе Genome, но и в других приложениях. Поэтому они расположены на уровне инфраструктуры.
Модель данных разделена на две части. Первая часть — это обобщенная модель данных для любого графа и любого блюпринта. Она представляет собой иерархический граф. Граф содержит узлы и соединительные линии. Некоторые узлы могут содержать в себе другие графы.
Вторая часть — это модель данных для анимационных блюпринтов. Она содержит в себе классы, которые описывают узлы анимационного блюпринта. К таким узлам относятся математические операции, операции сравнения, операции преобразования типов, операции смешивания анимаций и т.д. Отдельные классы представляют сокеты и соединительные линии.
Поверх модели данных располагается уровень сервисов. Как и в случае с редактором визуальных эффектов, данный уровень содержит только один сервис — компилятор. Компилятор преобразует анимационный блюпринт в исходном формате в бинарный формат, который воспроизводится движком.
Уровень редактирования разделен на три части. Первая часть — это библиотека, которая содержит базовые классы для архитектуры Документ/Вид и для управления Undo/Redo. Эта библиотека используется как в редакторе анимационных блюпринтов, так и в редакторе визуальных эффектов.
Вторая библиотека отвечает за редактирование графов. Она позволяет пользователю создавать узлы, расставлять их в окне редактирования, соединять узлы коннекторами различных типов, а также — удалять узлы и переключаться между вложенными графами. Однако эта библиотека имеет дело с обобщенной моделью графов и ничего не знает о конкретных узлах для редактора анимационных блюпринтов.
Третья библиотека предназначена для редактирования анимационных блюпринтов. Она позволяет создавать, удалять, редактировать конкретные узлы анимационного блюпринта.
Уровень взаимодействия с пользователем тоже разбит на три части. Первая часть содержит базовые компоненты пользовательского интерфейса. Второй часть содержит компоненты интерфейса, используемые для редактирования графов. А третья часть содержит компоненты пользовательского интерфейса, используемые для редактирования анимационных блюпринтов.
В целом, архитектуры редактора визуальных эффектов и редактора анимационных блюпринтов очень похожи и состоят из одних и тех же системных уровней. Однако имеются и различия, которые обуславливаются необходимостью создания обобщенной библиотеки для редактирования графов. Эти различия выливаются в то, что, прежде всего, модель данных разделяется на две части:
- обобщенную модель данных, которая описывает некий обобщенный граф;
- и конкретную модель данных, которая описывает граф конкретного вида — анимационный блюпринт.
Вместе с моделью данных такое же разделение происходит и на уровне редактирования, и на уровне взаимодействия с пользователем.
Пример 3. GPS-навигатор
Обзор
В третьем примере мне хотелось бы рассмотреть создание GPS-навигационного приложения для смартфона. Эта программа отличается от рассмотренных в предыдущих двух примерах приложений: редактора визуальных эффектов и редактора анимационных блюпринтов.
Основное отличие заключается в том, что редакторы нацелены на создание и редактирование документов в то время, как GPS-навигатор предназначен для оказания услуг.
К таким услугам относятся:
- Помощь пользователю в ориентации на местности. Эта услуга осуществляется путем отрисовки карты местности и указания места, где находится пользователь. При движении пользователя показывается и направление этого движения.
- Построение маршрута в заданную точку. Место назначение может быть задано как путем указания точки на карте, так и при помощи поиска.
- Поиск конкретного адреса или места пересечения двух дорог.
- Поиск ближайших точек интереса: кафе, автозаправочных станций, банкоматов и т.д.
Не смотря на нацеленность навигационной системы на оказание услуг, тем не менее, она также позволяет создавать и редактировать некоторые несложные документы. К ним относятся:
- профиль пользователя;
- история поиска точек интереса и адресов;
- редактирование списка фаворитов (домашний адрес, адрес места работы и другие).
Архитектура
Архитектура GPS-навигатора может быть представлена уже известной нам иерархией из 5-ти системных уровней.
Первый, самый нижний, инфраструктурный уровень состоит из библиотек и утилит, которые используются сквозным образом во всем приложении. Поскольку навигационная система разрабатывалась с использованием языков программирования C и C++, то в число этих библиотек входили:
- библиотека stlport, которая содержит набор стандартных контейнеров и алгоритмов и собирается под разные платформы;
- библиотека zlib, которая позволяет сжимать и разжимать данные при помощи алгоритма deflate;
- библиотеки libpng и libjpeg для работы с изображениями растровой графики в форматах png и jpeg;
- библиотека aes для шифрования данных и создания электронных подписей;
- библиотека expat xml parser для чтения и разбора xml-файлов.
Уровень модели данных является наиболее сложным по сравнению с аналогичными уровнями в других рассмотренных приложениях. Дело в том, что скорость работы многих алгоритмов навигации определяется скоростью доступа к необходимым данным. Поэтому данные нужно организовать таким образом, чтобы скорость доступа была высокой.
Данные содержат картографическую информацию, которая затем используется для рисования карт, прокладки маршрута, поиска координат по адресу.
Уровень модели данных может быть разделен на два подуровня:
- база данных;
- слой доступа к данным.
База данных содержит навигационные данные: карты, почтовые индексы, точки интереса, историю поездок, список мест-фаворитов.
Слой доступа к данным получает данные из базы данных и предоставляет их вышестоящему уровню в виде соответствующих структур данных.
Изначально карты в исходном формате поступают на вход компилятора, который преобразует их в промежуточный формат. Как правило, в качестве исходного формата выступает GDF 3.0. Это текстовый файл с определенной разметкой. Нередко одна карта, представленная в GDF 3.0, “весит” более одного гигабайта. Чтобы представить дорожную сеть Великобритании, необходимо 7 таких карт. В качестве промежуточного выступает бинарный формат, который позволяет представить полную информацию в компактной форме.
Далее карты в промежуточном формате поступают на вход пяти компиляторам, которые преобразуют их в специализированные базы данных.
Первая специализированная база данных содержит информацию, необходимую для отрисовки карты. Она включает не только дороги, но и различные области такие, как парки, водоемы, леса. Основное требование к ней — быстрый доступ ко всем элементам, которые находятся в заданной области.
Вторая специализированная база данных хранит информацию, необходимую для построения маршрута. В отличие от базы данных для рисования, она содержит информацию только о дороге. Для построения маршрута не нужно что-либо знать о водоемах, лесах, парках и геометрии дорожных элементов. Требуется только длина дорожного элемента, максимальная скорость и направление движения. Основное требование к ней — это быстрый доступ к соседним дорожным элементам.
Третья специализированная база данных содержит адреса. Основное требование к ней — это быстрый поиск адреса по введенному тексту и дорожного элемента по адресу.
Четвертая специализированная база данных содержит информацию о почтовых индексах. Основное требование к ней — это быстрый поиск координат района по почтовому индексу.
Пятая база данных содержит информацию о различных точках интереса (ресторанах, кафе, автозаправочных станциях, банкоматах и т.п.). Основное требование к ней заключается в быстром нахождении точек интереса в пределах определенной области.
Для доступа к картографическим данным используется слой доступа к данным.
Таким образом, модель данных для GPS-навигатора включает в себя:
- карты в исходном (текстовом) формате;
- компилятор для преобразования карт из текстового формата в промежуточный бинарный;
- пять специализированных компиляторов, которые формируют специализированные базы данных:
- базу данных для рисования карт;
- базу данных для построения маршрута;
- базу данных для поиска координат по адресу;
- базу данных почтовых индексов;
- базу данных точек интереса;
- слой доступа к данным, которые обеспечивает доступ к данным из специализированных баз данных.
Поверх модели данных располагается уровень сервисов. Поскольку GPS-навигатор — это приложение, ориентированное на оказание услуг, то этот уровень включает несколько функционально-ориентированных модулей. Каждый модуль отвечает за какую-нибудь одну ключевую функцию или же группу похожих ключевых функций навигационной системы.
Можно выделить такие модули:
- Растеризатор карты. Формирует двумерное или трехмерное представление карты в заданной области.
- Модуль роутинга. Отвечает за прокладку маршрута из текущего местоположения в заданную точку.
- Модуль поиска адреса. Отвечает за поиск координат по адресу.
- Модуль поиска по индекса. Отвечает за поиск координат по почтовому индексу.
- Модуль поиска точек интереса. Отвечает за нахождение точек интереса в пределах заданной области.
- Навигатор. Отвечает за формирование и выдачу навигационных указаний при движении по маршруту.
Уровень редактирования является достаточно небольшим и не содержит слишком много кода. Причина этого заключается в том, что навигационная система предназначена для оказания услуг пользователя и не предназначена для создания и редактирования каких-то документов.
Можно выделить 3 основных функции, которые можно делегировать уровню редактирования:
- работа с профилями пользователей (у навигационной системы могут быть несколько пользователей, и данные каждого пользователя должны сохраняться в отдельном профиле);
- создание и управление списком мест-фаворитов;
- сохранение истории поездок (в простейшем случае — сохранение точек назначения).
В принципе, GPS-навигатор может спокойно обойтись и без слоя редактирования. Но если хочется поддержать профили, то работа с ними может быть делегирована такому слою.
Уровень взаимодействия с пользователем отвечает за представление информации для пользователя на экране или на звуковой карте устройства, а также — за обработку ввода со стороны пользователя. Он содержит различные экраны и органы управления. Уровень взаимодействия с пользователем может поддерживать скины, если есть необходимость сделать интерфейс удобным при работе на разных устройствах.
Итоги
Подведем итоги рассмотрения архитектур трех приложений.
- Архитектура клиентского приложения может быть представлена иерархией из пяти системных уровней:
- Инфраструктурный уровень содержит библиотеки, которые используются во всем приложении.
- Уровень модели данных содержит классы, описывающие предметную область.
- Уровень сервисов предоставляет услуги для пользователя. К таким услугам относятся: компиляция, прокладка маршрута, поиск координат по адресу и т.д. Каждая услуга помещается в отдельный модуль, который называется сервисом.
- Уровень редактирования предназначен для создания и редактирования документов. Он также поддерживает возможность отмены операции.
- Уровень взаимодействия с пользователем отвечает за представление данных в удобном для пользователя виде, а также — за обработку ввода. Он содержит различные органы управления.
- Каждый уровень может содержать один или несколько модулей, или же быть пустым. В последнем случае он остается в пятиуровневой модели только из общесистемных соображений.
- Количество модулей, расположенных на уровне, зависит от типа приложения. Если разрабатывается приложение, предназначенное для редактирования различных документов, то будут насыщенными уровни редактирования и взаимодействия с пользователем. Если же создается приложение, нацеленное на оказание услуг пользователю, то будет насыщенным уровень сервисов в то время, как уровень редактирования будет небольшим.
- Сложность модели данных зависит от объема данных и от требований по производительности. В ряде случаев модель данных можно реализовать в виде самосериализуемых в XML классов. И этого будет достаточно. В других случаях потребуется создать базы данных со сложными структурами данных, оптимизированные для произвольного доступа. При использовании таких баз данных потребуется разработка программ-компиляторов, которые тоже относятся к модели данных.
- Если требуется разработать линейку приложений, позволяющих редактировать или отображать однотипные данные (например, графы), то необходимо разделить модель данных на одну обобщенную и несколько специализированных (по числу разрабатываемых приложений). Такое разделение пройдет и через вышестоящие уровни: уровень редактирования и уровень взаимодействия с пользователем.
Литература
- Document/View Architecture
- Blueprint Visual Scripting/Unreal Engine 4 Documentation
- Introduction to Undo Architecture
- Gregory, Jason. Game Engine Architecture
- Руководство Microsoft по проектированию архитектуры приложений, 2-е издание
- Лебедев К.А., Сычев С.В. О потерянном уровне
- Фаулер, Мартин. Архитектура корпоративных программных приложений. – Пер. с англ. – М.: Издательский дом «Вильямс», 2006. – 544 с.: ил.
Автор благодарит своих коллег за помощь при подготовке статьи.
Комментарии (6)
Askofen
19.05.2017 14:54Статья описывает лишь небольшую часть подхода к проектированию клиентских приложений. Если использовать этот подход, то некоторые риски, связанные с тем, что непонятно, какая должна быть архитектура, какого дизайна следует придерживаться и т.д., просто уходят. Процесс разработки преобразуется в последовательность этапов (не только в управленческом смысле, но и в техническом), которые имеют свои конкретные результаты и которые просто нужно выполнить.
Методики проектирования каждого слоя (или в терминах данной статьи — уровня) различаются. Например, на каждом уровне используются свои подходы к выявлению классов.
TiesP
19.05.2017 17:50Уровень редактирования видится немного искусственным. Ведь перечисленные задачи относятся, по сути, к логике приложения(3-му уровню в вашей схеме)? Например, "редакторам" нужны эти задачи(что вы вкючили) наряду с др.задачами редакторов. А другим приложениям не нужны. А в целом, интересная статья.
Askofen
19.05.2017 17:55Спасибо!
Уровень редактирования поддерживает 2 важные вещи:
1. Архитектуру Документ-Вид (когда с каждым документом связывается один или несколько видов). В простейшем случае, для документа «визуальный эффект» создается окно редактирования этого визуального эффекта.
2. Undo/Redo стек, который поддерживает возможность отмены операции.
Эти две вещи довольно-таки общие для разных редакторов. По этой причине я и предлагаю вынести их в отдельный слой (уровень).
andreyverbin
19.05.2017 21:32Спасибо больше за подробно расписанные кейсы, этого как воздух не хватает. Все почему-то учатся проектировать на примерах уровня Hello World, а потом удивляется, что получается «лапша».
Я в итоге пришел к точно такой же схеме разбиения GUI приложений. Я рассуждал как-то так:
1. Пользователь, как правило, хочет запустить какую-нибудь «сложную функцию», с большим числом параметров.
2. Но при этом у него есть клавиатура и мышь/пальцы, то есть «за раз» много информации он ввести не может.
3. Значит надо иметь слой в котором кусочки информации будут накапливаться и агрегироваться. Это слой Редактирования у вас и слой View Model у меня.
4. Когда информации достаточно, надо запустить «сложную функцию». Из соображений повторного использования эту «сложную функцию» стоит определить в какой-нибудь сервис.
В итоге путь данных от пользователя в систему и назад выглядит аналогично вашей пятиуровневой модели
1. Вид
2. View Model, слой редактирования и накопления данных от пользователя
3. Сервисы — ради выполнения функции какого-то сервиса пользователь и использует приложение.
4. Модель данных — структуры данных, которые легко сохранить/загрузить и с которые работает сервис. Чаще всего они еще торчат наружу из сервисов.
Инфраструктура — она как-бы сбоку получается, потому что WPF это инфраструктура, но для вида, а LINQ это тоже инфраструктура, но уже скорее для сервисов/модели.
Я объясняю наличие этих 5 слоев тем, что пользователь использует конкретные устройства ввода, а слои это уже следствие. А вот API приложения уже другие, там «ввод» это JSON/XML поэтому слоя редактирования уже не нужно.
Вопрос который меня мучает последнее время — как найти оптимальное разбиение системы на слои в заданной ситуации. Или аналогично — как показать, что разбиение на эти 5 слоев является в некотором смысле оптимальным?Askofen
20.05.2017 11:51Мне кажется, что при проектировании не нужно сразу искать оптимальную декомпозицию системы на слои. Нужно сделать хоть какое-то разбиение. Это будет первым шагом к разработке структуры. Затем, анализируя дополнительные функции системы, Вам неприменно потребуется решить, к какому слою абстракции их отнести. Нужно понять, какая подсистема будет отвечать за реализацию каждой конкретной функции и где она будет находиться. Возможными ответами на эти вопросы будут: разбиение функций на подфункции, делегирование их разным подсистемам, размещение этих подсистем на разных системных уровнях и, возможно, создание дополнительных системных уровней.
killeralex
Статья понравилась. Вопрос: что даёт такой подход руководителю проекта кроме оценки сроков/потребных ресурсов и расчёта трудоёмкости (для оплаты коллективу)?