Войти
ПрограммированиеСтатьиОбщее

Compile-time vs run-time: назад в будущее

Автор:

“In theory, there is no difference between theory and practice.
But, in practice, there is.”

– Jan L.A. van de Snepscheut

[Данная статья является продолжением дискуссии, начатой после моего выступления на КРИ]

Лет пять назад я был фанатом компонентных архитектур, управляемых данными.  Моим идеалом были игры, отличавшиеся только игровыми DLL да ресурсными файлами.  Я не представлял себе движка без фабрик.  Мне нужны были библиотеки типов с сериализацией.  «Подписчики на события вместо вызовов функций и делегирование с композицией вместо наследования», – таким был в то время мой девиз.  Таким стал и движок, на котором был выпущен Fighter Ace 3.x.

Время поставило все на свои места.

Чуть больше полутора лет назад мы начали построение системы разработки игр, управляемой спецификациями, и в ноябре 2002 выпустили PulseRacer для Xbox. Теперь я точно знаю, что где-то ошибся, если перед игровым программистом встает необходимость перенести момент принятия решения на время исполнения (run-time). Что-то пошло не так, если игровой программист вынужден использовать фабрику, выполнить сериализацию объекта или просто спросить меня, где у нас находится заголовочный файл, описывающий объект рендера (окно GUI, источник звука, абстрактный игровой объект, узел scene graph – нужное подчеркнуть). 

Если игровому программисту что-то понадобилось от «низкоуровневых» подсистем (рендер, GUI, звук и т.п.), он просто пишет в спецификации, что ему нужно.  По спецификации генерируется код, и программист получает оптимальную, заточенную под его требования структуру на этапе компиляции.  Мы используем полностью автоматический сборщик ресурсов, дающий нам 100% гарантии их целостности, скорость загрузки, сравнимую со скоростью ввода/вывода и минимальные расходы памяти.  Мы сохранили гибкость.  Мы увеличили производительность.  Мы избавились от уродливого кода. Вместо монстра мы получили простое, изящное и элегантное решение, применимое как для консолей, так и для PC.

Большинство решений принимается в итеративном процессе проектирования (design-time), проводится в жизнь во время компиляции (compile-time) и проверяется в момент сборки ресурсов (bundle-time). В run-time код просто работает.

Но сегодня, изучая материалы GDC 2003, я наткнулся на статьи модератора круглого стола “The GDC 2003 Game Object Structure Roundtable” Кайла Вильсона (Kyle Wilson, Day 1 Studios), посвященные ключевым вопросам архитектуры современных игровых движков и систем разработки игр.

Я был потрясен тем, что идеология игровых движков и видение проектировщиков мало изменились за прошедшие пять лет.

С одной стороны, основные положения статей Кайла и проблемы, обсуждавшиеся на круглом столе, достаточно четко отражают существующее положение вещей в отрасли.  Времена написания игр по принципу «я его слепила из того, что было» безвозвратно ушли.  Объем и структурная сложность игрового контента достигли критического порога.  Важнейшей областью инноваций стала организация производственного процесса разработки игр и конвейер изготовления и настройки игрового контента.

Иными словами, игровая индустрия вышла из детского возраста.  Гаражные способы организации производственного процесса отмирают, и на их место приходят промышленные.

Тем не менее, проблемы, затронутые на круглом столе и в статьях Кайла, во многом связаны с последствиями применения техник, переносящих момент принятия решений со времени компиляции кода (compile-time) на время исполнения (run-time).  Речь идет о системах, приобретающих законченную внутреннюю структуру в процессе подключения компонентов и загрузки ресурсов.  Гибкость и расширяемость такой системы во многом обеспечивается добавлением уровней косвенности.  Акценты смещаются со статической организации системы на ее динамическую структуру.  Уменьшается зацепление (coupling) объектов.  Сложные динамические системы строятся из простых компонент.

В той или иной степени, все классические техники, используемые в современных системах разработки игр, приводят к откладыванию момента связывания на время исполнения.  Скажем, фабрика переносит момент принятия решения о конкретном типе создаваемого объекта и о конкретном способе создания объекта в run-time.  Отправитель сообщения ничего не знает о подписчиках.  Данные загружаются в run-time, и могут определять свойства объектов, правила их структурной композиции и их поведение.

Примерами этих (динамизирующих:)) техник являются компонентно-ориентированные архитектуры, управляемые данными, универсальные способы представления игрового контента, обобщенные форматы файлов, библиотеки типов с поддержкой сериализации, использование делегирования и композиции вместо наследования и т.д.

В данной статье я буду предполагать, что движок и игра пишутся на C++ и, возможно, некотором скриптовом языке.  Кроме того, мы будем рассматривать только грамотную реализацию динамизирующих техник. 

В 1995 году статьи Бартона-Нэкмана (John Barton, Lee Nackman) положили начало пристальному изучению потенциала шаблонов C++.  Позже появилась работа Алекса Степанова, посвященная дизайну STL.  В 1998 году большая часть STL перешла в Стандартную библиотеку C++.  В начале 2001 года Андрей Александреску продемонстрировал изумительную мощь современного C++. 

На текущий момент известны блестящие способы реализации многих динамизирующих техник для современных компиляторов, так что мы можем с чистой совестью игнорировать проблемы, связанные с неудачной реализацией.  Иными словами, мы будем обсуждать только те проблемы, которые внутренне присущи самой идее откладывания момента связывания в run-time.

Основными мотивами применения динамизирующих техник являются требования гибкости, расширяемости и повторной используемости движка и рационализация процесса изготовления и настройки игрового контента.

Впрочем, ни одно из этих требований фактически ничего не дает конкретной игре.  Их выполнение необходимо (и жизненно важно) лишь во время ее изготовления.  Самое смешное, что динамизирующие техники во многих случаях не упрощают работу игрового программиста.  Напротив, взаимодействие объектов и подсистем становится все менее прозрачным.

“A game is just a real-time database with a pretty front end.”
– Dave Roderick

Рассмотрим простой пример, который я приводил в своем докладе на КРИ 2003. Он может показаться несколько натянутым (в конце концов, это лишь пример), но я уверен, что вы сможете с легкостью представить себе его аналоги из реальной жизни. Основная идея примера – понимание фундаментальной разницы между гибкостью и вседозволенностью.

Пусть мы делаем авиасимулятор, и вам (игровому программисту) нужно получить, скажем, bounding box левого закрылка 3D-модели самолета. (С тем же успехом вы можете захотеть поменять его матрицу или изменить параметр шейдера, управляющий заржавленностью модели.) Допустим для простоты, что вы можете как-то найти закрылок в рендерном объекте.

Типичным уродцем, характерным для движков конца 90-х годов, будет подобный код (псевдокод):

// получение бокса левого закрылка
// - найти закрылок по какому-либо уникальному ID (enum, строка и т.п.)
render::node n = Plane.find_node_recursive(“FlapsLeft”); 
// - убедиться, что закрылок найден
assert( n != null ); 
// - бокс бывает только у мешей (например), поэтому преобразовать в меш
render::mesh m = (render::mesh)n.get_robject(); 
// - убедиться, что это - меш
assert( m != null ); 
// - получить, наконец, собственно бокс
render::box b = m.get_box(); 

Что мы наблюдаем? Массу ошибок проектировщика движка.

Во-первых, мы точно знаем, что в самолете должен быть левый закрылок.  Если художник забыл его намоделить, хотелось бы об этом узнать не во время игры, а на этапе сборки ресурсов.  Искать закрылок каждый раз – пустая трата ресурсов, поэтому в реальном игровом коде мы закэшировали бы результат поиска (или сам бокс), незначительно усложнив при этом код.  Но нам пришлось бы принять какое-либо проектное решение (кэшировать или не кэшировать).

Во-вторых, мы точно знаем, что все, что нам нужно – это только бокс закрылка, а не закрылок сам по себе.  Иными словами, мы его не двигаем, не прячем, не деформируем и т.д.  Имея эту информацию на руках, хорошая система сборки ресурсов могла бы сохранить где-нибудь бокс закрылка и приаттачить закрылок к фюзеляжу.  Это увеличило бы производительность движка без потери игровой функциональности.

В-третьих, мы провоцируем игрового программиста на возню с интимными подробностями внутреннего представления 3D-модели.

Страницы: 1 2 Следующая »

#архитектура, #движок, #спецификации

29 июля 2003 (Обновление: 23 фев 2011)

Комментарии [364]