![image](https://habrastorage.org/getpro/habr/post_images/84d/f63/55e/84df6355e66e2b7f31faa0a8a0557496.jpg)
Сегодня мы продолжаем начатый неделю назад рассказ об интерфейсе игры World of Tanks.
Текущее состояние проекта
Освежим информацию из первой части статьи.
Сейчас для рендеринга GUI в проекте используется технология Autodesk Scaleform, которая позволяет использовать Flash как среду разработки.
Кто знаком с Flash, тот знает, что языком программирования в этой среде является ActionScript. У этого языка есть несколько версий, но самые широко используемые — ActionScript2 (AS2) и ActionScript3 (AS3).
В нашем проекте на текущий момент используются обе версии. Обусловлено это тем, что разработка начиналась на AS2, так как Scaleform на тот момент еще не поддерживал AS3. Со временем, когда поддержка AS3 в Scaleform появилась и ее реализация стала достаточно надежной, начался перевод сервисных (не боевых) интерфейсов на AS3.
Почему переход на AS3 начался с миграции на него именно сервисных интерфейсов? Все очень просто. Бодрое танковое рубилово — это наше все. Любые помехи, лаги или баги в боевом интерфейсе могут испортить ощущения пользователя в процессе игры. На такой риск мы пойти не могли и начали миграцию с менее требовательной с точки зрения ресурсов и не такой критичной для игрового процесса части. Такой подход полностью себя оправдал. Мы наткнулись на проблемы с производительностью и потреблением памяти. Большую часть из них мы решили до релиза обновленного ангара, какие-то отлавливали и исправляли уже в продакшене, опираясь на баг-репорты от игроков. Обошлось без серьезных проколов, что не может не радовать.
В предыдущей статье я приводил список проблем, с которыми нам приходилось жить в AS2. Освежу этот список:
- проблемы с различными версиями кода в разных SWF-файлах;
- проблема коммуникации Flash <—> Python;
- отсутствие стандартизации и унификации в коде;
- отсутствие четких правил и процедур по добавлению нового функционала;
- отсутствие автоматизации сборки проекта.
Эти проблемы вносили свой немалый вклад в трудозатратность при добавлении нового функционала в проект, и это еще одна причина, почему переход на AS3 начали с сервисных интерфейсов. С точки зрения GUI, ангар — часть проекта с наибольшей концентрацией нового функционала на релиз, и, соответственно, наиболее затратная по человеческим ресурсам. В свою очередь, боевой интерфейс уже работает на AS2, обновляется не так часто и больших нареканий не вызывает. Зачем ломать то, что и так работает?
Обзор архитектуры и процесса разработки
Но не Flash-ем единым живет GUI-разработчик WoT. В качестве скриптового языка в проекте используется Python. Всю красоту, которую мы сделали во Flash, нужно подключить в игре, наполнить данными, обработать и транслировать пользовательский ввод в реальные действия в игре. Все это как раз и делается в Python.
Вот упрощенная схема основных элементов GUI в клиенте:
![image](https://habrastorage.org/getpro/habr/post_images/9a5/5c7/0f0/9a55c70f021fded1ddd745dd5d79a046.jpg)
Давайте более детально рассмотрим, что же стоит за этими разноцветными прямоугольниками.
AS2 и его взаимодействие с Python
При входе в бой начинается загрузка battle.swf. В этом файле содержатся практически все основные элементы боевого интерфейса (HUD): списки команд, чат, панель состояния танка, мини-карта, панель расходников и т. д.
Практически за каждый элемент, присутствующий в battle.swf, отвечает один из классов в Battle.py. При завершении загрузки содержимое файла растягивается на весь viewport игрового клиента и отрисовывается поверх 3D-сцены. Кроме battle.swf также загружаются SWF-файлы для отображения маркеров и прицелов. Они помещаются под battle.swf для того, чтобы маркеры и прицелы не накладывались на элементы HUD, а прятались под ними.
Также для систем с несколькими мониторами SWF-файлы с прицелами и маркерами растягиваются на все мониторы, а battle.swf отрисовывается только на главном мониторе (чтобы HUD не разлетелся по разным углам этих мониторов и осталась возможность адекватно читать боевую информацию).
За состоянием маркеров и прицелов следит Python. В нем обрабатываются игровые события и данные, создаются и уничтожаются маркеры и прицелы. Для каждого типа прицелов есть свой Python-класс. За маркерами следит VehicleMarkersManager. За расположение маркеров и прицелов на экране отвечает клиентский C++ код.
Для коммуникации Flash и Python в бою используются два подхода:
1) Наиболее частый — передача данных массивом скалярных данных через API, предоставляемое в Scaleform. В предыдущей статье я описал этот метод и объяснял его недостатки (неудобно читать код, рефакторить и поддерживать).
2) Второй подход — Direct Access API (DAAPI). О нем я только упоминал, а теперь расскажу подробнее.
У этого подхода есть два основных преимущества: простота и скорость работы.
Простота заключается в том, что передавать сложные объекты в обе стороны можно не думая о сериализации/десериализации — все происходит автоматически в C++. Скорость работы достигается за счет того, что вызовы методов в обе стороны происходят «напрямую». То есть, имея ссылку на Flash-объект в Python, можно вызвать его метод и передать в него аргументы так, как будто это Python-объект. Без использования DAAPI вызов методов происходит по указанию пути к Flash-объекту в дереве визуальных компонентов и поиску этого компонента в иерархии, что дорого.
Кроме этих явных плюсов есть еще один, но очень важный — можно назначить Python-объект в качестве обработчика для Flash-объекта.
Нарисую и поясню, что происходит, по шагам.
![image](https://habrastorage.org/getpro/habr/post_images/02c/953/96d/02c95396dc89534f72f63d93b3f46775.jpg)
- Во Flash создаем класс с объявлением публичных полей с типом Function. Значения этих полей не инициализируем, т. е. они == null.
- В Python создаем класс с реализацией для этих методов. Именование методов должно совпадать с именованием полей во Flash-классе.
- Создаем экземпляры этих классов во Flash и Python.
- Передаем ссылку на Flash-экземпляр в Python.
- Назначаем экземпляр Python-класса как обработчик для экземпляра Flash-класса (устанавливаем DAAPI соединение).
- Вызываем метод из первого пункта во Flash-классе.
- Благодаря DAAPI происходит вызов метода, реализованного в Python. Причем для Flash это выглядит как вызов обычного метода во Flash. В качестве параметров могут выступать как скалярные типы данных, так и сложные объекты.
Эта возможность лежит в сердце новой архитектуры, которая использована в AS3 части проекта.
Как устроена AS3 часть проекта
В первую очередь, нужно сказать о том, что сборка проекта происходит с помощью Apache Maven и Apache Ant. Логически проект разбит на несколько слоев:
- Common — SWC-библиотека с набором общих файлов, используемых в проекте. Сюда входит:
— библиотека Scaleform CLIK (набор UI-компонентов и менеджеров);
— базовые классы и интерфейсы инфраструктурной части проекта;
— код сторонних библиотек, используемых в проекте. - GUI — SWC-библиотека, в которой собран код всех вьюшек, окошек и других визуальных объектов.
- App — содержит имплементации всех менеджеров и инфраструктурных классов. Сборка этого проекта дает на выходе application.swf.
Точкой входа в AS3-приложение является application.swf, содержащий класс net.wg.app.impl.Application. Этот файл всегда загружается при старте клиента. Он решает три основных задачи:
- создание и инициализация инфраструктурных частей проекта в AS3 (менеджеры, контейнеры для вьюшек и т. п.);
- загрузка всех определений классов, используемых в приложении (это помогает решить проблему с динамической подгрузкой разных версий одного и того же класса, о которой я говорил в предыдущей статье);
- загрузка и выгрузка ресурсов для вьюшек.
У класса Application из AS3 есть вторая половинка в Python — класс AppEntry. Задачи этого класса:
- cоздание и инициализация инфраструктурных частей проекта в Python (все абсолютно симметрично первому пункту из AS3 части);
- загрузка конфигурации, описывающей взаимосвязи между AS3 и Python частями проекта (подробнее об этом буквально через несколько абзацев);
- инициализация DAAPI-соединения для самого Application и всех инфраструктурных классов.
В процессе сборки Common- и GUI-библиотек мы используем YAML для генерации кода. Вот список задач, которые мы решаем, используя YAML:
- описания интерфейсов взаимодействия Flash- и Python-половинок одного модуля (модулем можно считать вьюшку или менеджер);
- создания классов констант, общих для Flash и Python;
- создания классов констант для локализационных сообщений;
- создания классов констант для изображений, подгружаемых на рантайме;
- создания ClassLoader — класса, содержащего импорты и ссылки на все используемые в приложении AS3 классы. ClassLoader используется для упаковывания всего кода проекта в application.swf.
Любой класс в AS3 (будь то класс, отвечающий за работу какой-то вьюшки, или менеджер без визуального представления), которому нужна коммуникация с Python, должен быть создан с помощью описания в YAML. Давайте рассмотрим это на примере окна послебоевой статистики:
BattleResultsMeta.yaml
!net.wg.WoT.models.DAAPIModel
type: window # определяет базовый класс для Flash классов
python: # перечень Python методов, доступных для вызова из Flash
- BattleResultsMetaPy: [ eventBus : net.wg.py.app.EventBusPy ] # конструктор
constructor: eventBus
- saveSorting: [ iconType : String , sortDirection : String , bonusType : int ] # сохраняет предпочтения по сортировке
- showEventsWindow: [ questID : String ] # вызвает показ окна боевых задач
flash: # перечень Flash методов, доступных для вызова из Python
- BattleResultsMeta: [ ] # конструктор
constructor:
- as_setData: [data : Object] # передача готовых данных с результатами боя во Flash
После обработки этого YAML-файла в ходе сборки проекта сгенерируется следующий набор классов и интерфейсов:
BattleResultsMeta — базовый Python-класс. В нем объявлен набор всех методов из python-секции YAML для их последующей перегрузки в классе-наследнике и набор методов из flash-секции, для возможности доступа к ним из Python по сигнатуре, объявленной в YAML.
class BattleResultsMeta(DAAPIModule):
def saveSorting(self, iconType, sortDirection, bonusType):
self._printOverrideError('saveSorting')
def showEventsWindow(self, questID):
self._printOverrideError('showEventsWindow')
def as_setDataS(self, data):
if self._isDAAPIInited():
return self.flashObject.as_setData(data)
BattleResultsMeta — базовый AS3 класс. В нем объявлен набор всех методов из python-секции YAML, для возможности доступа к ним из Flash по сигнатуре, объявленной в YAML. Именно эти методы объявляются как поля с типом Function, и они будут заменены на Python-методы из предыдущего пункта, после установки DAAPI-соединения.
public class BattleResultsMeta extends AbstractWindowView {
public var saveSorting : Function = null;
public var showEventsWindow : Function = null;
public function saveSortingS(iconType : String, sortDirection : String, bonusType : int) : void {
App.utils.asserter.assertNotNull(saveSorting);
saveSorting(iconType, sortDirection, bonusType);
}
public function showEventsWindowS(questID : String) : void {
App.utils.asserter.assertNotNull(showEventsWindow);
showEventsWindow(questID);
}
}
IBattleResultsMeta — AS3 интерфейс. В нем объявлен набор всех методов из python- и flash-секций YAML. Класс, реализующий функциональность окна послебоевой статистики, должен реализовывать этот интерфейс.
public interface IBattleResultsMeta extends IEventDispatcher {
function saveSortingS(iconType : String, sortDirection : String, bonusType : int) : void;
function showEventsWindowS(questID : String) : void;
function as_setData(data : Object) : void;
}
Теперь для интеграции нового окна в клиент нужно выполнить несколько шагов.
- Создать FLA-файл, в котором будет содержаться визуальная часть окна.
- Во Flash создать класс BattleResults, наследуемый от BattleResultsMeta и реализующий интерфейс IBattleResultsMeta.
- В Python создать класс BattleResults, наследуемый от BattleResultsMeta.
- В Python создать константу, используемую как уникальный идентификатор нового компонента.
- Добавить конфигурационные данные в Python, где будет указано соответствие между:
— уникальным идентификатором нового компонента,
— типом нового компонента (в нашем случае это окно),
— файлом с визуальным представлением,
— Python-классом, отвечающим за новый компонент.
Некоторые из этих шагов тоже можно автоматизировать в рамках выполнения YamlTask. Я выбрал самый простой подход, дабы не усложнять рассказ.
Танцы с бубном закончены. Чтобы показать новое окно в клиенте, достаточно у Python-части нашего Application вызвать метод loadView, в который передается уникальный идентификатор нового компонента.
Что происходит, после того как мы вызвали загрузку нашего окна
Вот упрощенный алгоритм работы по загрузке и инициализации модуля (голубым цветом отмечены шаги в Python, розовым — во Flash):
![image](https://habrastorage.org/getpro/habr/post_images/139/10a/3fd/13910a3fd88342a2755cb6a7d62a88ff.png)
Все эти шаги выполняются различными частями нашей инфраструктурной прослойки, которая изолирует разработчика функциональности от необходимости делать эти шаги руками. Эта прослойка решает следующие задачи:
- управление загрузкой SWF-файлов;
- управление распределением вьюшек по слоям и управление перехода фокуса между ними;
- управление тултипами;
- работа с локализационными строками;
- управление звуковыми событиями;
- управление контекстными меню;
- управление анимацией;
- управление и работа с цветовыми схемами;
- другие задачи.
Планы на будущее
Самая объемная задача на обозримое будущее — перевод HUD на AS3. В текущий момент мы ведем исследования и готовим проект к такому переходу. Главное, чего мы хотим добиться в рамках этого перехода, — унифицировать механизмы добавления и разработки новой функциональности и избавиться от того разнообразия подходов, которое есть сейчас.
Мы также хотим как минимум не ухудшить производительность и потребление памяти новым HUD, а в лучшем случае — добиться прироста производительности и уменьшения объемов памяти, требуемых для работы HUD.
Помимо этого у нас проходит ряд активностей, нацеленных на упрощение и улучшение процесса взаимодействия со смежными отделами при проработке и разработке нового функционала.
Самое узкое место — взаимодействие с отделом User Experience (UX). Сейчас при проработке новых игровых концепций или новой функциональности часто необходимо создать прототип, на базе которого можно (до передачи задачи в разработку) оценить удобство или простоту понимания основных механик простыми игроками.
Создание таких прототипов вне клиента дорого и не всегда может дать желаемый эффект. Не имея контекста или окружения, которые мы имеем в клиенте, мы не можем добиться нужного уровня погружения в игру. Остается создание прототипов внутри игрового клиента. Это тоже очень дорого, поскольку для этого приходится отвлекать разработчиков от их основного занятия и перенаправлять их усилия на выполнение работы, которая, возможно, будет полностью забыта и не востребована после проведения теста.
Даже если тест прошел успешно, то есть вероятность, что все придется переделать заново. В погоне за уменьшением затрат и из-за сжатых сроков на выполнение прототипа нам приходится зачастую прибегать к использованию «костылей», крупнозерновых «напильников» и заплаток.
Для решения этой проблемы мы планируем:
• создать набор стандартных компонентов (кнопки, индикаторы, списки и т. п.);
Такой шаг обусловлен тем, что до последнего времени практически каждая крупная новая задача привносит, помимо самой новой функциональности, еще и набор новых компонентов. Их приходится делать на базе старых, меняя их внешнее оформление и внося изменения в логику их работы, или же реализовывать эти компоненты заново. Отдел UX занимается тем, создает список стандартных компонентов и формирует требования к ним.
• создать инструменты быстрого прототипирования.
Опираясь на библиотеку стандартных компонентов, мы планируем создать отдельный инструмент или режим в клиенте, в котором специалисты UX смогут без вмешательства программистов создавать новые интерфейсы и настраивать простые правила их взаимодействия между собой. Такие прототипы будут строиться только из стандартных компонентов. Их можно будет сохранить и загрузить для выполнения в клиенте и проводить тестирование с минимальными затратами.
Вот и все, что я хотел и мог рассказать вам о GUI в World of Tanks. Задавайте вопросы в комментариях. Спасибо за внимание.