Развиваем иерархию игровых объектов

17.09.2012

Мы подготовили для вас перевод интересной статьи от Мика Веста. Ознакомиться с оригиналом можно здесь.

Рефакторинг игры, с помощью компонентов.

До последнего времени программисты пользовались «навороченной» иерархией классов для представления элементов игры. Но сейчас события начинают принимать другой оборот – происходит переход от использования иерархии классов, к различным методам, представляющим объекты игры, как совокупность компонентов. В этой статье дается объяснение, что это значит и исследуются некоторые преимущества и практическая реализация данного подхода. Я расскажу о своем личном опыте реализации данной системы, используя большую программную базу, в том числе поясню, как можно подать эту идею другим программистам и менеджерам.

Игровые элементы

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

  • Ракета
  • Автомобиль
  • Танк
  • Граната
  • Винтовка
  • Пехотинец
  • Инопланетянин
  • Прыжковый ранец
  • Аптечка
  • Скала

Как правило, игровые элементы совершают различные действия. Вот некоторые действия, которые они могут совершать:

  • Запуск сценария
  • Перемещение
  • Реакция на другой объект
  • Испускание частиц
  • Воспроизведение звука
  • Группирование игроком
  • Управление непосредственно игроком
  • Взрыв
  • Реакция на притяжение
  • Нацеливание игроком
  • Следование заданному маршруту
  • Анимация

Традиционная иерархия типов


Традиционный способ представляет собой набор игровых элементов, представленный в виде объектно-ориентированной декомпозиции множества элементов, которых мы хотим задействовать в игре. Обычно, это все делается с самыми благими намерениями, но частые изменения при развитии игры прогрессируют – особенно если игровой движок используется для различных игр. Обычно мы получаем нечто похожее на рис. 1, но с гораздо большим числом узлов в иерархии классов. По мере развития игры, мы просто добавляем различные точки функциональных возможностей элементов.  Новые объекты либо должны инкапсулировать функциональность в себе, либо должны быть унаследованы от объектов, которые уже содержат в себе эту функцию. Часто,  функциональность должна быть добавлена на уровне корневых классов, таких как CEntyty. У этого способа есть несомненный плюс – доступность функций во всех производных классах, а минусом является увеличение использования ресурсов (нерациональное использование ресурсов компьютера, появление ненужных функций у объектов) при создании экземпляров этих классов.

Даже простые объекты, такие как камень или граната, могут, в конечном счете, обладать гораздо большей функциональностью (и связанных с нею переменных, методов и свойств, ненужных при использовании этого объекта), чем это необходимо. Очень часто, создание традиционной иерархии объектов игры заканчивается созданием экземпляра класса, известного под названием «пузырь». «Пузырь» представляет собой классический «анти-паттерн» — один огромный класс (или же специализированную ветку иерархии классов) с большим количеством указателей и переплетений функциональных возможностей.

Когда «пузырь» появляется в корне иерархии классов, он так же отображается и в конечных узлах. Наиболее вероятным кандидатом на эту роль является класс представляющий игрового персонажа. Так как игра, как правило, создается вокруг одного персонажа, объект, представляющий этот персонаж, бывает перегружен функциональностью. Чаще всего это реализуется созданием большого количество методов класса, такого как CPlayer.

Результатом реализации функциональных возможностей в корневых классах, становиться перегруженность классов наследников ненужной функциональностью. Однако противоположенный метод – реализация функциональных возможностей в классах наследниках может привести к плачевным последствиям. Функциональные возможности в этом случае станут разобщенными – только те объекты, для которых созданы те или иные функции, могут их использовать. Но, в то же время, программисту придется часто писать повторяющийся код, чтобы реализовать нужные функции для различных объектов. В результате, при проведении рефакторинга иерархии классов для перемещения и сочетания функциональности может появиться «грязь» в коде.

Возьмем, например, функциональные возможности физически взаимодействующих твердых тел. Не все объекты в сцене должны участвовать в этих взаимодействиях. Как вы можете видеть на рис. 1, классы CRock и CGrenade являются производными от CRigid. Что произойдет, если мы эти функции применим к транспортным средствам? Вам придется передвинуть класс CRigid выше по иерархии, приближаясь к корневому классу, раздувая его во все более и более тяжелый «пузырь», сгруппировав в узкой цепочке классов все нужные методы для реализации нужных функций. При этом, большинство других классов надо будет вывести из этой ветки.

Объединение компонентов

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

Систему формирования объекта из объединения компонентов можно реализовать одним из трех способов, которые могут рассматриваться как отдельные этапы перехода от «пузыря» иерархии классов, к составному объекту.

Объект, как организованный «пузырь»

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

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

Недостатком этого метода является то, что «пузырь» все же останется. Все функции, по-прежнему, инкапсулируются в один большой объект. Вряд ли вы сможете полностью разделить ваш «пузырь» на суб-объекты. Все равно в нем останется существенное количество функций, увеличивающие «вес» этого вашего «облегченного» объекта. Кроме того, добавится головная боль – постоянная проверка всех нулевых указателей, — нуждаются ли они в обновлении.

Объект, как контейнер компонентов

Следующим этапом является исключение каждого компонента («суб-объекта» из предыдущего примера) из объекта. Они имеют общий базовый класс, поэтому мы можем хранить список компонентов внутри объекта.

Это промежуточное решение, так как у нас остается еще «корневой объект», представляющий игровой элемент. Тем не менее, это может быть разумным решением, более того – единственным практическим решением, если в большей части программного кода понятие игрового объекта принимается в качестве конкретного объекта.

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

Объект, как чистое объединение компонентов

В окончательной редакции примем объект, как сумму его частей. На рис. 2 показана схема, где игра по сути составлена из набора компонентов. Там нет «игрового элемента-объекта», как такового. Каждый столбец в диаграмме представляет собой список из одинаковых компонентов. Каждая строка – задействованные в игре объекты. Компоненты сами по себе независимы от тех объектов, которые они составляют.


Практический опыт

Я впервые применил систему объектов состоящих из компонентов во время работы в Neversoft, над серией игр Tony Hawk. Наша система игровых объектов была разработана в трех последующих играх, где у нас была иерархия игровых объектов напоминавшая «пузыри», которые я описал ранее. Она страдала от тех же самых проблем: объекты, как правило, были «перетяжеленные». Они содержали в себе много ненужных данных и функций. Иногда, из-за ненужной функциональности, замедлялась игра. Функциональные возможности очень часто дублировались в разных ветках иерархического дерева.

Я узнал об системах объектов, базирующихся на компонентах,  из рассылок SWEng GameDev (Software Engineering as Game Development), и решил для себя что идея, по сути, хорошая. Я тут же принялся за реорганизацию базы программного кода, и через два года это было сделано.

Почему так долго? Ну, во-первых, мы «штамповали» игры серии Tony Hawk по одной версии в год, так что было очень мало времени между работой над играми, чтобы посвятить себя рефакторингу. Во-вторых, я просчитался с масштабом проблемы. Трехлетняя программная база содержит в себе очень много строк кода. Так много, что программный код, создававшийся на протяжении многих лет, стал очень «негибким». Поскольку программный код опирался на игровые объекты, сидящие на игровых объектах, подгоняемых кнутиком других игровых объектов, пришлось приложить массу усилий, чтобы превратить их в компоненты.

Возможные проблемы

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

Менеджеры тоже с трудом принимают эту идею. Вы должны уметь объяснить происходящее простыми словами для того, чтобы делать игры быстрее. Приготовьте фразы вроде этой:

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

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

После чего, я показал это остальным программистам. Было некоторое недовольство и сопротивление, но так как методика была реализована и нормально работала, у моих противников не было аргументов.

Медленный прогресс

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

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

Решение здесь кроется в создании «пузыря компонентов». Это был один огромный компонент, который инкапсулировал большую часть функциональных возможностей класса scater. Несколько других компонентов «пузыря» должны были быть в других местах и мы, в конечном итоге, включили всю систему объектов в коллекцию компонентов. Как только они все были расставлены по местам,  мы смогли постепенно разделить «пузырь» компонентов на, собственно, компоненты.

Результаты

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

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

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

Детали реализации

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

Так как все компоненты имеют общий интерфейс, очень легко можно добавить функцию отладки в каждый компонент. Это дает возможность довольно просто добавить инспектор объектов, который может сбросить отладочную информацию о компонентах составного объекта в понятной для человека форме. Позже, это превратится в сложный удаленный инструмент отладки, дающий возможность получать информацию от всех возможных типов игровых объектов. Это было бы сложно и утомительно — осуществить и поддерживать, при использовании традиционной иерархии классов.

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

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

После создания объекта в потоке данных, он может создать проблемы в том случае, если список компонентов не упорядочен. Если в одном объекте происходит обновление компонента физических взаимодействий перед анимацией, а в другом – наоборот, они могут не синхронизироваться друг с другом. Последовательность того, как это должно происходить,  необходимо идентифицировать и отразить в программном коде.

Выводы

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

Заполнен: ООПТуториалы
Присвоен тэг:

avatar

Об Авторе ()

Коллективный разум нашей редакции. Все человеческое здесь заканчивается и начинается C++!

Комментирование закрыто.

Наверх