- PVSM.RU - https://www.pvsm.ru -

Сегодня мы продолжаем начатый неделю назад [1] рассказ об интерфейсе игры World of Tanks.
Освежим информацию из первой части статьи.
Сейчас для рендеринга GUI в проекте используется технология Autodesk Scaleform, которая позволяет использовать Flash как среду разработки.
Кто знаком с Flash, тот знает, что языком программирования в этой среде является ActionScript. У этого языка есть несколько версий, но самые широко используемые — ActionScript2 (AS2) и ActionScript3 (AS3).
В нашем проекте на текущий момент используются обе версии. Обусловлено это тем, что разработка начиналась на AS2, так как Scaleform на тот момент еще не поддерживал AS3. Со временем, когда поддержка AS3 в Scaleform появилась и ее реализация стала достаточно надежной, начался перевод сервисных (не боевых) интерфейсов на AS3.
Почему переход на AS3 начался с миграции на него именно сервисных интерфейсов? Все очень просто. Бодрое танковое рубилово — это наше все. Любые помехи, лаги или баги в боевом интерфейсе могут испортить ощущения пользователя в процессе игры. На такой риск мы пойти не могли и начали миграцию с менее требовательной с точки зрения ресурсов и не такой критичной для игрового процесса части. Такой подход полностью себя оправдал. Мы наткнулись на проблемы с производительностью и потреблением памяти. Большую часть из них мы решили до релиза обновленного ангара, какие-то отлавливали и исправляли уже в продакшене, опираясь на баг-репорты от игроков. Обошлось без серьезных проколов, что не может не радовать.
В предыдущей статье я приводил список проблем, с которыми нам приходилось жить в AS2. Освежу этот список:
Эти проблемы вносили свой немалый вклад в трудозатратность при добавлении нового функционала в проект, и это еще одна причина, почему переход на AS3 начали с сервисных интерфейсов. С точки зрения GUI, ангар — часть проекта с наибольшей концентрацией нового функционала на релиз, и, соответственно, наиболее затратная по человеческим ресурсам. В свою очередь, боевой интерфейс уже работает на AS2, обновляется не так часто и больших нареканий не вызывает. Зачем ломать то, что и так работает?
Но не Flash-ем единым живет GUI-разработчик WoT. В качестве скриптового языка в проекте используется Python. Всю красоту, которую мы сделали во Flash, нужно подключить в игре, наполнить данными, обработать и транслировать пользовательский ввод в реальные действия в игре. Все это как раз и делается в Python.
Вот упрощенная схема основных элементов GUI в клиенте:

Давайте более детально рассмотрим, что же стоит за этими разноцветными прямоугольниками.
При входе в бой начинается загрузка 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-объекта.
Нарисую и поясню, что происходит, по шагам.

Эта возможность лежит в сердце новой архитектуры, которая использована в AS3 части проекта.
В первую очередь, нужно сказать о том, что сборка проекта происходит с помощью Apache Maven и Apache Ant. Логически проект разбит на несколько слоев:
Точкой входа в AS3-приложение является application.swf, содержащий класс net.wg.app.impl.Application. Этот файл всегда загружается при старте клиента. Он решает три основных задачи:
У класса Application из AS3 есть вторая половинка в Python — класс AppEntry. Задачи этого класса:
В процессе сборки Common- и GUI-библиотек мы используем YAML для генерации кода. Вот список задач, которые мы решаем, используя YAML:
Любой класс в 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;
}
Теперь для интеграции нового окна в клиент нужно выполнить несколько шагов.
Некоторые из этих шагов тоже можно автоматизировать в рамках выполнения YamlTask. Я выбрал самый простой подход, дабы не усложнять рассказ.
Танцы с бубном закончены. Чтобы показать новое окно в клиенте, достаточно у Python-части нашего Application вызвать метод loadView, в который передается уникальный идентификатор нового компонента.
Вот упрощенный алгоритм работы по загрузке и инициализации модуля (голубым цветом отмечены шаги в Python, розовым — во Flash):

Все эти шаги выполняются различными частями нашей инфраструктурной прослойки, которая изолирует разработчика функциональности от необходимости делать эти шаги руками. Эта прослойка решает следующие задачи:
Самая объемная задача на обозримое будущее — перевод HUD на AS3. В текущий момент мы ведем исследования и готовим проект к такому переходу. Главное, чего мы хотим добиться в рамках этого перехода, — унифицировать механизмы добавления и разработки новой функциональности и избавиться от того разнообразия подходов, которое есть сейчас.
Мы также хотим как минимум не ухудшить производительность и потребление памяти новым HUD, а в лучшем случае — добиться прироста производительности и уменьшения объемов памяти, требуемых для работы HUD.
Помимо этого у нас проходит ряд активностей, нацеленных на упрощение и улучшение процесса взаимодействия со смежными отделами при проработке и разработке нового функционала.
Самое узкое место — взаимодействие с отделом User Experience (UX). Сейчас при проработке новых игровых концепций или новой функциональности часто необходимо создать прототип, на базе которого можно (до передачи задачи в разработку) оценить удобство или простоту понимания основных механик простыми игроками.
Создание таких прототипов вне клиента дорого и не всегда может дать желаемый эффект. Не имея контекста или окружения, которые мы имеем в клиенте, мы не можем добиться нужного уровня погружения в игру. Остается создание прототипов внутри игрового клиента. Это тоже очень дорого, поскольку для этого приходится отвлекать разработчиков от их основного занятия и перенаправлять их усилия на выполнение работы, которая, возможно, будет полностью забыта и не востребована после проведения теста.
Даже если тест прошел успешно, то есть вероятность, что все придется переделать заново. В погоне за уменьшением затрат и из-за сжатых сроков на выполнение прототипа нам приходится зачастую прибегать к использованию «костылей», крупнозерновых «напильников» и заплаток.
Для решения этой проблемы мы планируем:
• создать набор стандартных компонентов (кнопки, индикаторы, списки и т. п.);
Такой шаг обусловлен тем, что до последнего времени практически каждая крупная новая задача привносит, помимо самой новой функциональности, еще и набор новых компонентов. Их приходится делать на базе старых, меняя их внешнее оформление и внося изменения в логику их работы, или же реализовывать эти компоненты заново. Отдел UX занимается тем, создает список стандартных компонентов и формирует требования к ним.
• создать инструменты быстрого прототипирования.
Опираясь на библиотеку стандартных компонентов, мы планируем создать отдельный инструмент или режим в клиенте, в котором специалисты UX смогут без вмешательства программистов создавать новые интерфейсы и настраивать простые правила их взаимодействия между собой. Такие прототипы будут строиться только из стандартных компонентов. Их можно будет сохранить и загрузить для выполнения в клиенте и проводить тестирование с минимальными затратами.
Вот и все, что я хотел и мог рассказать вам о GUI в World of Tanks. Задавайте вопросы в комментариях. Спасибо за внимание.
Автор: Dichkovsky
Источник [2]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/python/64532
Ссылки в тексте:
[1] начатый неделю назад: http://habrahabr.ru/company/wargaming/blog/228309/
[2] Источник: http://habrahabr.ru/post/229261/
Нажмите здесь для печати.