Gamedev LectureСтатьи

Лекция #33. Плюсы и минусы игровых "орхетектур". [Лектор - dDIMA]

Автор:

Disclaimer: из лога вырезаны куски, ну и так далее. полный лог

[21:00] <dDIMA> Всем доброго getlocaltime();

[21:00] <dDIMA> Тема сегодняшней лекции - "Плюсы и минусы игровых <орхетектур>".
[21:01] <dDIMA> Я постараюсь кратко рассказать про основные решения, которые приходится принимать на верхнем уровне разработки игрового проекта, какие здесь бывают варианты реализации, чем они плохи и хороши.
[21:01] <dDIMA> Предварительно одно небольшое предупреждение  я, разумеется, буду приводить разные примеры из жизни  с какими интересными вещами мне приходилось встречаться, но ссылок на конкретные компании и конкретные игровые движки приводить не буду.
[21:01] <dDIMA> И на вопросы <у кого именно встретилось такое г.> отвечать тоже, естественно, не буду :)
[21:01] <dDIMA> Ни на канале, ни в привате.

[21:01] <dDIMA> Сразу <с места в карьер> - первый ключевой вопрос  это каким образом ведется разделение кода на столь модное понятие <движок> и <игра>.
[21:02] <dDIMA> Можно явно выделить 2 крайних подхода, ну и соответственно, множество промежуточных вариантов.
[21:02] <dDIMA> http://www.ddima.ru/articles/v_lecture1/engine_game.jpg (http://www.gamedev.ru/files/images/?id=48038)
[21:03] <dDIMA> Левая картинка иллюстрирует подход, при котором игровой движок и сама игра не разделаются по сущностям.
[21:03] <dDIMA> Мы начинаем разработку игры (ширина трапеций символизирует объем кода, по оси Y вниз идет время) под названием Project 1.
[21:03] <dDIMA> Когда игра сделана и (дай бог) выпущена, мы на основе всего имеющегося кода делаем второй проект (естественно, схожего жанра).
[21:03] <dDIMA> К середине разработки второго проекта мы сильно разрослись и решаем запустить третий проект.
[21:03] <dDIMA> Для этого мы копируем полное содержимое Project 2 в Project 3 и продолжаем разработку.
[21:03] <dDIMA> Незадолго до окончания второго проекта начинаются работы по четвертому проекту, а третий проект бренчуется в Project 5.
[21:04] <dDIMA> Правая картинка иллюстрирует подход с организацией отдельного независимого движка (это центральный ствол <дерева>), на котором <висят> отдельные ветки Project1 ... Project5.
[21:04] <dDIMA> Мы тщательно следим за тем, чтобы главный ствол рос и развивался,
[21:04] <dDIMA> и чтобы в него не попадали разные паразитные части, которые Project specific
[21:04] <dDIMA> и которые в будущем могут не позволить нам нормально развивать движок для других нужд.
[21:05] <dDIMA> Естественно, что в случае достаточно мощного и полного движка не все его части будут использованы.
[21:05] <dDIMA> Это символизируют дополнительные линии, отсекающие части главного ствола для нужд того или иного проекта.

[21:05] <dDIMA> Каковы плюсы и минусы указанных решений?

[21:05] <dDIMA> Вариант слева выглядит достаточно простым и удобным для реализации
[21:05] <dDIMA> к тому же позволяет использовать максимум имеющегося кода в новых проектах.
[21:05] <dDIMA> Однако он не лишен существенных недостатков.
[21:06] <dDIMA> Ключевая проблема в организации такого способа работы  это тесное переплетение разных <системных> и <игровых> подсистем в один чудовищный клубок.
[21:06] <dDIMA> Причем чем дальше  тем сильнее этот клубок завязывается.
[21:06] <dDIMA> Я видел примеры игровых движков, в которых базовая система анимации персонажей имела режимы анимации Male, Female и MegoBoss
[21:06] <dDIMA> причем логика применения анимационных секвенций зависела от этого флага!
[21:06] <dDIMA> Второй нюанс  тупое выделение полного проекта в новый бренч обязательно должно сопровождаться рефактором и удалением старых более не нужных объектов
[21:07] <dDIMA> В противном случае объем неиспользуемого в проекте <мусора> будет сильно возрастать и может достигать цифр в 20-40% общего объема кода
[21:07] <dDIMA> А это и лишние перекомпиляции и усложненная навигация, и общая потеря понимания, что происходит в проекте
[21:08] <dDIMA> Третий аспект - переплетение системного и игрового кода приводит к тому, что общая разработка библиотеки для нескольких одновременно идущих проектов становится крайне сложной задачей
[21:08] <dDIMA> На левом рисунке есть пересечение по времени между Project 4, 5 и 3, но совместная разработка какой-нибудь новой компоненты (например, сетевой игры) потребует достаточно долгой и мучительной вставки требуемого кода в исходники
[21:08] <dDIMA> (проверено на собственном опыте)
[21:08] <dDIMA> Потому что весь код представляет из себя большой <клубок>,
[21:08] <dDIMA> и вам потребуется править несколько сотен файлов по дюжине строк, чтобы оно все взлетело
[21:09] <dDIMA> Фактически это приводит к тому, что компания остается <компанией одного проекта>, так как...
[21:09] <dDIMA> на приведенном рисунке активный код продолжает использоваться только в линии Project 1 - 2 - 3 - 5, причем не самым оптимальным способом :(
[21:09] <dDIMA> Когда объемы данных начинают конфликтовать с заложенными в движок ограничениями, наступает коллапс
[21:09] <dDIMA> Вы более не можете создавать игры, основываясь на такой технологии,
[21:09] <dDIMA> вам надо будет бросать имеющуюся разработку и фактически начинать делать новый движок с нуля
[21:10] <dDIMA> Цикл развития компании по такой схеме составляет 5-8 лет, после чего технология распадается
[21:10] <dDIMA> Картинка справа выглядит посимпатичнее - в ней имеется четко выделенный ствол, который растет и развивается
[21:10] <dDIMA> Если главный движок качественно написан и состоит из достаточно независимых модулей, то вы можете разрабатывать новые компоненты движка, причем одновременно для всех текущих проектов
[21:10] <dDIMA> Минусы данного подхода заключаются в том,
[21:10] <dDIMA> что для создания такой структуры проекта требуется крайне высокая квалификация программистов,
[21:11] <dDIMA> а также наличие отдельных людей, которые разрабатывают и модернизируют движок, не привязываясь к нуждам отдельно взятых проектов
[21:11] <dDIMA> Ибо как только в движок пролезает game specific код, начинаются проблемы у всех остальных команд
[21:11] <dDIMA> Крайне важно обеспечивать единость движка для всех проектов
[21:11] <dDIMA> Как только вы для собственного удобства делаете отдельные бренчи системного движка для отдельных игр, вы мгновенно переползаете в вариант разработки, представленный на левой картинке, со всеми вытекающими последствиями
[21:12] <dDIMA> Также из минусов подхода отмечу, что универсальность практически всегда означает определенную потерю перформанса
[21:12] <dDIMA> Делая универсальное решение вы теряете возможность сэкономить на каких-то game-specific моментах и, естественно, решение оказывается не самым высокопроизводительным
[21:12] <dDIMA> Цикл развития компании по такой схеме составляет 10-15 лет.
[21:12] <dDIMA> Технология либо обновляется в течение этого времени,
[21:12] <dDIMA> либо распадается в том случае, если заложенные архитектурные ограничения оказываются непреодолимыми на новом железе, платформе и т. п.
[21:13] <dDIMA> Цифры чисто практические на текущем железе, с ростом next-next-генов время может сокращаться
[21:13] <dDIMA> Естественно, в чистом виде оба подхода встречаются редко.
[21:14] <dDIMA> Большинство компаний стремится иметь выделенный системный код, но при этом <не гнушаются> возможностью скопировать чего-нибудь побольше из проекта в проект
[21:14] <dDIMA> Что касается начинающих разработчиков, то я, конечно же, ратую за второй подход, но скорее всего, первые ваши 2-3 пилотных проекта не смогут выдержать строгую схему движок-игра
[21:14] <dDIMA> Первый вариант разработки можно применять только при условии внимательного анализа, а что же на самом деле в проекте является универсальным, а что  специфичным для игры
[21:15] <dDIMA> Такой анализ должен сопровождаться постоянным рефактором (хотя о его вреде я еще скажу ниже)
[21:15] <dDIMA> и стремлением выделить <на будущее> универсальный системный и игровой код
[21:15] <dDIMA> Так,
[21:16] <dDIMA> Первый вопрос я кратко осветил :)
[21:16] <dDIMA> теперь плавно переходим ко второму пункту лекции,
[21:16] <dDIMA> если есть вопросы по первой части, готов выслушать.

[21:16] <Zeux> Правая картинка про движок для одной платформы?
[21:16] <dDIMA> Может быть для одной, может для нескольких
[21:16] <dDIMA> Для нескольких надо стараться, чтобы "ветки" дерева были максимально независимыми от платформ

[21:16] <cppg> Разделение движок-игра - это только разделение на шаред/специфик код?
[21:17] <dDIMA> гуру, недопонял. Поясни вопрос, плз
[21:17] <cppg> Ну, вопрос, что ты называешь движком и что игрой?
[21:17] <dDIMA> движком - что-то общее, что можно выделить :)
[21:18] <cppg> Ага
[21:18] <_ShaMan_> я обычно придерживаюсь разделение по подсистемам
[21:18] <Cunter> Движок - графа, звук, управление, сеть, физика, аи, скрипт?
[21:18] <_ShaMan_> графический, игровой и т.д.
[21:21] <dDIMA> графика, звук (кроме процедурных примочек), управление, скрипты - это движок
[21:21] <dDIMA> Физика - часть безусловно системная, но очень много игровой специфики
[21:21] <dDIMA> да, забыл упомянуть, middleware тоже фактически представляет собой ствол
[21:22] <dDIMA> Причем за счет многолетней обкатанности - очень хороший ствол :)

[21:17] <Darth> Вариант с готовым движком? Тут по идее и туда и туда тоже может уйти.
[21:17] <dDIMA> конечно
[21:18] <dDIMA> Darth: особенно хорошо он туда уходит, если поставляется без сорсов
[21:19] <dDIMA> Darth: Были примеры, когда народ правил системный код, а потом оригинальная игра начинала падать в единственном месте
[21:19] <dDIMA> (обнаружили через несколько месяцев)

[21:18] <_Winnie> Интересно, если проекта три, и у любых двух есть пересечение, а у трёх вместе - нет...
[21:19] <dDIMA> Я специально на правом рисунке выделил, что необязательно все проекты юзают весь функционал движка
[21:20] <dDIMA> Т.е. если мы делаем сингплеер, нафиг нам сеть? ;)
[21:20] <dDIMA> А функционал - с ним посложнее будет :)

[21:20] <_ShaMan_> ну я так понимаю что во втором подходе игра как бы собирается из кирпичиков?
[21:21] <_ShaMan_> т.е. берётся общая кодобаза и на ней строится игра
[21:22] <dDIMA> _ShaMan_: фактически да, из кирпичиков
[21:22] <dDIMA> То есть вариант 1 - это реконструкция имеющегося дома
[21:23] <dDIMA> вариант 2 - это сборка нового дома из блоков

[21:21] <Cunter> Модульность позволит движку даже с ростом технологий держаться на плаву?
[21:23] <dDIMA> модульность позволяет держаться на плаву, и при необходимости менять отдельные "тяжелые" модули (если повезет)

[21:24] <dDIMA> Я еще на 1 вопрос из привата отвечу
[21:24] <dDIMA> > Какой примерно процент успешных созданий живучего движка для нескольких игр одной компании?
[21:24] <dDIMA> Не все компании работают по второму варианту
[21:24] <dDIMA> Хотя многие пробуют создать свою технологию
[21:25] <dDIMA> Успешных - так сходу могу прикинуть примерно треть
[21:25] <dDIMA> Есть компании, которые вполне успешно работают по первому варианту

[21:26] <dDIMA> продолжаем
[21:26] <dDIMA> Теперь про DG
[21:26] <dDIMA> С точки зрения организации взаимодействия объектов в игровом мире можно выделить 2 основные схемы работы:
[21:27] <dDIMA> 1. Если кому-то из объектов требуется обновление, он вызывает другой объект с запросом на Update()
[21:27] <dDIMA> 2. Система знает о порядке вызова объектов и сама вызывает им Update в требуемом порядке
[21:27] <dDIMA> Оба варианта проиллюстрированы на следующем рисунке
[21:27] <dDIMA> http://www.ddima.ru/articles/v_lecture1/dep_graph.jpg (http://www.gamedev.ru/files/images/?id=48037)
[21:28] <dDIMA> Вариант 1 более подробно
[21:28] <dDIMA> World перед тем, как обновляться, вызывает Player.Update(), SkyBox.Update() и BackGroundSound.Update()
[21:28] <dDIMA> Игрок из себя вызывает WeaponMng.Update() и Camera.Update()
[21:28] <dDIMA> Аналогично послупают и все остальные объекты в сцене
[21:28] <dDIMA> Чтобы предотвратить рекурсивные вызовы, обычно делается темплейтный или базовый класс-переходник, в котором записывается счетчик кадров
[21:29] <dDIMA> Если на текущем кадре еще не было обновления объекта, то он будет обновляться, в противном случае ничего делаться не будет
[21:29] <dDIMA> http://www.everfall.com/paste/id.php?rhk0dppm9tth
[21:29] <dDIMA> Вариант 2 более подробно
[21:30] <dDIMA> Существует некоторый базовый класс (условно назовем его GameObject) с виртуальным методом virtual void Update()=0
[21:30] <dDIMA> Все перечисленные объекты наследуются от данного класса, переопределяют Update() и каким то образом сообщают в Update Manager, что объекты существуют, причем между ними есть такие-то зависимости
[21:30] <dDIMA> Update Manager на каждом фрейме вызывает обновление указанных объектов
[21:31] <dDIMA> Подход №1 прост и удобен для реализации
[21:31] <dDIMA> Все связи между объектами видны прямо в коде, отладка проста и эффективна
[21:31] <dDIMA> Однако этот вариант приводит к сильной связанности компонент приложения
[21:31] <dDIMA> Фактически он (если применяется в чистом виде) форсирует нас на создание не движка+игра, а большой кучи малы
[21:31] <dDIMA> см. еще раз на http://www.ddima.ru/articles/v_lecture1/engine_game.jpg, рисунок слева
[21:32] <dDIMA> Этот вариант также является более произволительным
[21:32] <dDIMA> так как если что-то не потребовалось апдейтить на текущем фрейме, то оно и не будет вызвано :)
[21:32] <dDIMA> Второй вариант более удобен для слабого связывания компонент игры и движка, но сложен как для реализации, так и для отладки
[21:32] <dDIMA> Нам требуется каким-то образом задавать порядок вызова объектов
[21:32] <dDIMA> и по шагам программу пройти очень и очень сложно
[21:32] <dDIMA> Тут вылезают наружу все тонкости событийно-управляемой системы и отладки таких приложений
[21:33] <dDIMA> На практике первая схема используется часто, а вот вторая в чистом виде используется крайне редко
[21:33] <dDIMA> Как правило, разработчики ищут компромисс с использованием событийной схемы,
[21:33] <dDIMA> а внутренности крупных сущностей самостоятельно вызывают внутренние апдейты для своих <детей>
[21:34] <dDIMA> То есть например Player является GameObject'ом, а вот внутри него ручками вызывается обновление Inventory, Camera, WeaponMng и т.п.
[21:34] <dDIMA> SoundManager является GameObject'ом, а вот внутри него он сам напрямую вызывает BGSound, обновляет 3D источники и т.п.
[21:35] <dDIMA> Короткий комментарий по вопросам
[21:35] <dDIMA> приведенный пейст сделан для варианта, что Update() вызывается именно 1 раз на фрейм
[21:35] <dDIMA> Это как раз позволяет всем нахально вызывать DoUpdate() столько, сколько хочется
[21:35] <dDIMA> а реальный Update() придет только 1 раз
[21:36] <dDIMA> Если почему-то захотелось вызвать Update() дважды, можно сделать метод типа Invalidate()
[21:36] <dDIMA> Но будьте аккуратны - легко можно провалиться в бесконечную рекурсию :)
[21:36] <dDIMA> Возвращаясь ко второму моменту:
[21:36] <dDIMA> *второму варианту
[21:37] <dDIMA> Во второй схеме существенный нюанс - это порядок вызовов объектов
[21:37] <dDIMA> Его надо сохранять в правильном порядке, ибо если камера обновится раньше игрока, мы получим на экране отставание на фрейм с неприятными дрожаниями и прочими эффектами
[21:37] <dDIMA> Вариантов сортировки подписчиков Update() есть довольно много
[21:38] <dDIMA> самые популярные - это разные магические цифры, определяющие порядок
[21:38] <dDIMA> или задание зависимостей между классами
[21:38] <dDIMA> Как правило цифры удобны в комбинированной схеме, когда подписчиков Update() немного, а внутренние Update вызываются ручками
[21:39] <dDIMA> Там можно даже не городить огород с DoUpdate() - риск свалится в рекурсию минимальный
[21:39] <dDIMA> Также я еще рассмотрю подробнее организацию GameObject в виде единственного класса или группы классов
[21:39] <dDIMA> Недостатком общего единого GameObject является то, что он будет к концу разработки сильно перегружен различными методами
[21:39] <dDIMA> Основываясь на опыте работы с такими движками, можно прогнозировать цифру в 100-300 методов
[21:40] <dDIMA> Поэтому часто разбивают базовую сущность GameObject на несколько
[21:40] <dDIMA> Например что-то в таком роде: GameSystemObject, GameObject, SpriteObject и т.п.
[21:40] <dDIMA> Это позволяет как-то разнести интерфейсы и (тоже заодно) отсортировать порядки вызова
[21:40] <dDIMA> Но схема не лишена недостатков, ключевой из которых - жесткая фиксация различных типов объектов
[21:41] <dDIMA> То есть если появится какая-нибудь динамическая лава, которая взаимодействует с игроком, хитро светит на environment и т.п., ей может не найтись места среди заготовленных базовых классов
[21:41] <dDIMA> Как вы организуете иерархию объектов - ваше личное право
[21:41] <dDIMA> Можете сделать Hero public MoveableObject public GameObject public Object
[21:41] <dDIMA> Можете сделать 1 уровень интерфейса
[21:42] <dDIMA> Лично мне больше нравится более простая с точки зрения иерархии схема, как я писал ранее в ЖЖ, сильно хочется, чтобы компиляторы выдавали синтаксическую ошибку в том случае, если глубина наследования больше 3-4 :)
[21:42] <dDIMA> Мне сильнее импонирует схема наследования, когда в качестве базовых выступают не логически-игровые Game, Moveable, Sprite и прочие Objects,
[21:43] <dDIMA> а очевидные с точки зрения действия объекты Update, Render и т.п.
[21:43] <dDIMA> В данной схеме игровой цикл разворачивается в вызов обсерверов в некотором упорядоченном порядке, а конкретные объекты имплементируют те интерфейсы, которые им самим актуально требуются
[21:43] <dDIMA> Как водится, на практике часто применяется комбинированный вариант - с универсальными GameObject + дополнительные имплементации
[21:43] <dDIMA> Более подробно я займусь разделеняими на иерархии объектов чуть позже.
[21:44] <dDIMA> Сейчас небольшое лирическое отступление с красивыми картинками :)
[21:44] <dDIMA> Но еще раньше - давайте сделаем небольшой перерыв, даже перекур?
[21:44] <dDIMA> Пока что есть текущие вопросы?

[21:44] <Zeux> Что является ключевым в первой схеме и что во второй?
[21:44] <Zeux> т.е. первая схема - это про то, что явный immediate вызов
[21:45] <dDIMA> ага
[21:45] <dDIMA> И как следствие - сильная связанность компонент
[21:45] <Zeux> точнее про то, что явный вызов или про то, что мгновенный?
[21:45] <Zeux> аналогично, вторая схема - про то, что есть один большой граф?
[21:45] <Zeux> или про что-то еще
[21:45] <dDIMA> Ну мгновенный и связанный - вещи одного плана
[21:46] <dDIMA> Я уже говорил, что за универсальность приходится платить
[21:46] <dDIMA> второй вариант более универсален, но соответственно, и тормознутее

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

15 июня 2007 (Обновление: 13 ноя 2009)

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