Каковы недостатки использования DrawableGameComponent для каждого экземпляра игрового объекта?

25

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

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

Кенджи Кина
источник
Мне любопытно, почему два года спустя вы удалили «принятую» отметку из моего ответа ? Обычно я не очень волнует - но в данном случае это вызывает VeraShackle в бешенство искажение моего ответа пузыриться к вершине :(
Эндрю Рассел
@AndrewRussell Я прочитал эту ветку не так давно (из-за некоторых ответов и голосов) и подумал, что у меня нет квалификации, чтобы высказать мнение о том, является ли ваш ответ правильным или нет. Однако, как вы говорите, я не думаю, что ответом VeraShackle должен быть тот, у кого больше голосов, поэтому я снова отмечаю ваш ответ как правильный.
Кенджи Кина
1
Я могу вкратце изложить свой ответ следующим образом: « Использование DrawableGameComponentблокирует вас в едином наборе сигнатур методов и конкретной модели сохраняемых данных. Обычно это неправильный выбор изначально, а блокировка существенно препятствует развитию кода в будущем. «Но позвольте мне рассказать вам, что здесь происходит ...
Эндрю Рассел
1
DrawableGameComponentявляется частью основного API XNA (опять же: в основном по историческим причинам). Начинающие программисты используют его, потому что он «там» и «работает», и они не знают ничего лучше. Они видят, что мой ответ говорит им, что они " не правы " и - из-за природы человеческой психологии - отвергают это и выбирают другую "сторону". Который, оказывается, является спорным не ответом Веры Шекл; который набрал обороты, потому что я не вызвал его сразу (см. эти комментарии). Я чувствую, что мое возможное опровержение там остается достаточным.
Эндрю Рассел
1
Если кто-то заинтересован в том, чтобы усомниться в правомерности моего ответа на основе положительных отзывов: хотя это правда, что (GDSE наводнен вышеупомянутыми новичками), ответ VeraShackle сумел получить на пару больше голосов, чем мой в последние годы, не забывайте что у меня есть еще тысячи откликов по всей сети, в основном на XNA. Я реализовал XNA API на нескольких платформах. И я профессиональный разработчик игр, использующий XNA. Родословная моего ответа безупречна.
Эндрю Рассел

Ответы:

22

«Игровые компоненты» были очень ранней частью дизайна XNA 1.0. Они должны были работать как элементы управления для WinForms. Идея заключалась в том, что вы сможете перетаскивать их в свою игру с помощью графического интерфейса пользователя (вроде как для добавления невизуальных элементов управления, таких как таймеры и т. Д.) И устанавливать для них свойства.

Таким образом, у вас могут быть такие компоненты, как камера или счетчик FPS? ... или ...?

Вы видите? К сожалению, эта идея на самом деле не работает для реальных игр. Компоненты, которые вы хотите для игры, на самом деле не пригодны для повторного использования. У вас может быть компонент «игрок», но он будет сильно отличаться от компонента «игрок» любой другой игры.

Я полагаю, что именно по этой причине и из-за простой нехватки времени GUI был загружен до XNA 1.0.

Но API все еще можно использовать ... Вот почему вы не должны использовать его:

В основном это сводится к тому, что проще всего писать, читать, отлаживать и поддерживать как разработчика игр. Делая что-то вроде этого в вашем коде загрузки:

player.DrawOrder = 100;
enemy.DrawOrder = 200;

Это гораздо менее понятно, чем просто делать это:

virtual void Draw(GameTime gameTime)
{
    player.Draw();
    enemy.Draw();
}

Особенно, когда в реальной игре вы можете получить что-то вроде этого:

GraphicsDevice.Viewport = cameraOne.Viewport;
player.Draw(cameraOne, spriteBatch);
enemy.Draw(cameraOne, spriteBatch);

GraphicsDevice.Viewport = cameraTwo.Viewport;
player.Draw(cameraTwo, spriteBatch);
enemy.Draw(cameraTwo, spriteBatch);

(По общему признанию Вы могли бы разделить камеру и spriteBatch, используя подобную Servicesархитектуру. Но это - также плохая идея по той же самой причине.)

Эта архитектура - или ее отсутствие - намного легче и гибче!

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

Это вроде как разница между декларативным и императивным программированием. Оказывается, для разработки игр императивное программирование гораздо предпочтительнее.

Эндрю Рассел
источник
2
У меня сложилось впечатление, что они были для компонентного программирования , которое, по-видимому, широко используется для программирования игр.
BlueRaja - Дэнни Пфлюгофт
3
Классы игровых компонентов XNA не имеют прямого отношения к этому шаблону. Этот шаблон предназначен для составления объектов из нескольких компонентов. Классы XNA ориентированы на один компонент на объект. Если они идеально вписываются в ваш дизайн, то обязательно используйте их. Но вы не должны строить свой дизайн вокруг них! Смотрите также мой ответ здесь - обратите внимание, где я упоминаю DrawableGameComponent.
Эндрю Рассел
1
Я согласен с тем, что архитектура GameComponent / Service не так уж полезна и похожа на концепцию Office или веб-приложений, встроенных в игровую среду. Но я бы предпочел использовать player.DrawOrder = 100;метод над императивным для порядка прорисовки. Я заканчиваю игру Flixel, которая рисует объекты в том порядке, в котором вы добавляете их на сцену. Система FlxGroup облегчает это, но было бы неплохо иметь встроенную сортировку по Z-индексу.
Майкл Бартнетт
@ michael.bartnett Обратите внимание, что Flixel - это API с сохранением режима, в то время как XNA (за исключением, очевидно, системы игровых компонентов) - это API с непосредственным режимом. Если вы используете API с сохраненным режимом или реализуете свой собственный , возможность изменять порядок прорисовки на лету, безусловно, важна. Фактически, необходимость изменить порядок прорисовки на лету является хорошей причиной для того, чтобы сделать что-то в режиме удержания (вспоминается пример системы управления окнами). Но если вы можете написать что-то в немедленном режиме, если API платформы (например, XNA) построен таким образом, то IMO вам следует.
Эндрю Рассел
Для дальнейшего ознакомления gamedev.stackexchange.com/a/112664/56
Кенджи Кина
26

Хммм. Я боюсь, что это вызов для баланса здесь.

Кажется, в ответе Эндрю 5 обвинений, поэтому я постараюсь разобраться с каждым по очереди:

1) Компоненты - устаревший и заброшенный дизайн.

Идея шаблона проектирования компонентов существовала задолго до XNA - по сути, это просто решение создавать объекты, а не чрезмерно изогнутый игровой объект, как дом их собственного поведения Update и Draw. Для объектов в своей коллекции компонентов игра будет вызывать эти методы (и несколько других) автоматически в соответствующее время - главное, чтобы игровой объект сам не должен был «знать» этот код. Если вы хотите повторно использовать свои игровые классы в разных играх (и, следовательно, в разных игровых объектах), такое разъединение объектов все еще является хорошей идеей. Беглый взгляд на последние (профессионально произведенные) демонстрационные версии XNA4.0 из AppHub подтверждает мое впечатление, что Microsoft все еще (на мой взгляд, вполне справедливо) стоит за этим.

2) Эндрю не может придумать более двух повторно используемых игровых классов.

Я могу. На самом деле я могу думать о нагрузках! Я только что проверил свою текущую разделяемую библиотеку, и она содержит более 80 повторно используемых компонентов и сервисов. Чтобы придать вам вкус, вы можете иметь:

  • Состояние игры / Экранные менеджеры
  • ParticleEmitters
  • Рисованные примитивы
  • Контроллеры AI
  • Контроллеры ввода
  • Контроллеры анимации
  • Рекламные щиты
  • Менеджеры по физике и столкновениям
  • камеры

Любая конкретная игровая идея потребует как минимум 5-10 вещей из этого набора - гарантировано - и они могут быть полностью функциональными в совершенно новой игре с одной строкой кода.

3) Управление порядком прорисовки по порядку явных вызовов отрисовки предпочтительнее, чем использование свойства DrawOrder компонента.

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

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

Зачем? Кроме того, во всех, кроме самых тривиальных проектов, ваша логика обновления и код Draw полностью затенят вызов вашего метода с точки зрения снижения производительности в любом случае.

5) Размещение кода в одном файле предпочтительнее при отладке, чем размещение его в файле отдельного объекта.

Ну, во-первых, когда я отлаживаю в Visual Studio, местоположение точки останова с точки зрения файлов на диске в наши дни почти невидимо - IDE работает с этим местоположением без каких-либо проблем. Но также подумайте, что у вас возникли проблемы с чертежом Skybox. Неужели переход к методу Draw в Skybox действительно более обременителен, чем поиск кода для рисования скайбокса в (предположительно очень продолжительном) методе мастер- рисования в объекте Game? Нет, не совсем - на самом деле я бы сказал, что все наоборот.

Итак, в заключение - пожалуйста, внимательно посмотрите на аргументы Эндрю здесь. Я не верю, что они действительно дают существенные основания для написания запутанного игрового кода. Достоинства развязанного шаблона компонентов (используются разумно) огромны.

VeraShackle
источник
2
Я только что заметил этот пост, и я должен выпустить решительное опровержение: у меня нет проблем с повторно используемыми классами! (У которых могут быть методы рисования и обновления и т. Д.) У меня возникла особая проблема с использованием ( Drawable) GameComponentдля обеспечения вашей игровой архитектуры из-за потери явного контроля над порядком рисования / обновления (с которым вы согласны в # 3) и жесткостью их интерфейсов (которые вы не обращаетесь).
Эндрю Рассел
1
Ваша точка зрения № 4 подразумевает, что я использовал слово «легкий» для обозначения производительности, где я на самом деле имею в виду архитектурную гибкость. Ваши оставшиеся пункты № 1, № 2 и особенно № 5, кажется, возводят абсурдного соломенного чучела, который, я думаю, большинство / весь код должен быть помещен в ваш основной игровой класс! Прочитайте мой ответ здесь для некоторых из моих более подробных мыслей об архитектуре.
Эндрю Рассел
5
Наконец: если вы собираетесь опубликовать ответ на мой ответ, сказав, что я ошибаюсь, было бы вежливо добавить ответ на мой ответ, чтобы я видел уведомление об этом, а не наткнулся на это через два месяца .
Эндрю Рассел
Возможно, я неправильно понял, но как это ответит на вопрос? Должен ли я использовать GameComponent для моих спрайтов или нет?
Superbest
Дальнейшее опровержение: gamedev.stackexchange.com/a/112664/56
Кенджи Кина
4

Я немного не согласен с Эндрю, но я также думаю, что есть время и место для всего. Я бы не стал использовать Drawable(GameComponent)для чего-то такого простого, как Спрайт или Враг. Тем не менее, я бы использовал его для SpriteManagerили EnemyManager.

Возвращаясь к тому, что сказал Эндрю о порядке прорисовки и обновления, я думаю, что Sprite.DrawOrderэто будет сбивать с толку многих спрайтов. Кроме того, его относительно легко настроить DrawOrderи UpdateOrderдля небольшого (или разумного) количества менеджеров, которые (Drawable)GameComponents.

Я думаю, что Microsoft покончила бы с ними, если бы они действительно были такими жесткими, и никто их не использовал. Но они этого не делают, потому что работают отлично, если роль правильная. Я сам использую их для разработки Game.Services, которые обновляются в фоновом режиме.

Эмбл Лайтгерц
источник
2

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

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

Superbest
источник
2

В комментариях я разместил сокращенную версию моего ответа старого ответа:

Использование DrawableGameComponentфиксирует вас в едином наборе сигнатур методов и конкретной сохраненной модели данных. Обычно это изначально неправильный выбор, и блокировка значительно препятствует развитию кода в будущем.

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


Корневой объект, поддерживающий состояние игрового мира, имеет Updateметод, который выглядит следующим образом:

void Update(UpdateContext updateContext, MultiInputState currentInput)

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

Есть также некоторые дополнительные методы, подобные обновлениям, такие как:

void PlayerJoin(int playerIndex, string playerName, UpdateContext updateContext)

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

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


Для рисования, из-за характера игры, существует специальная система сортировки и отбора, которая принимает данные от следующих методов:

virtual void RegisterToDraw(SortedDrawList sortedDrawList, Definitions definitions)
virtual void RegisterShadow(ShadowCasterList shadowCasterList, Definitions definitions)

И тогда, когда он выясняет, что рисовать, автоматически вызывает:

virtual void Draw(DrawContext drawContext, int tag)

tagПараметр необходим , поскольку некоторые участники могут держать на другие актер - и поэтому должны быть в состоянии сделать как выше , так и ниже задержанный актер. Система сортировки гарантирует, что это происходит в правильном порядке.

Существует также система меню для рисования. И иерархическая система для рисования пользовательского интерфейса, вокруг HUD, вокруг игры.


Теперь сравните эти требования с тем, что DrawableGameComponentобеспечивает:

public class DrawableGameComponent
{
    public virtual void Update(GameTime gameTime);
    public virtual void Draw(GameTime gameTime);
    public int UpdateOrder { get; set; }
    public int DrawOrder { get; set; }
}

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

Кроме того, очень важно отметить, что эти требования возникли не просто в начале проекта. Они развивались в течение многих месяцев разработки.


То, что люди обычно делают, если они начинают по DrawableGameComponentмаршруту, они начинают строить на взломах:

  • Вместо того, чтобы добавлять методы, они делают некрасивое приведение к своему собственному базовому типу (почему бы просто не использовать это напрямую?).

  • Вместо того, чтобы заменить код для сортировки и вызова Draw/ Update, они делают очень медленные вещи, чтобы изменить порядковые номера на лету.

  • И что хуже всего: вместо добавления параметров к методам они начинают «передавать» «параметры» в ячейки памяти, которые не являются стеком (использование Servicesдля этого популярно). Это запутанное, подверженное ошибкам, медленное, хрупкое, редко поточнобезопасное и может стать проблемой, если вы добавите сеть, создадите внешние инструменты и так далее.

    Это также делает код повторно использовать сложнее , потому что теперь ваша зависимость скрыта внутри «компоненты», а не опубликована на границе API.

  • Или они почти видят, как все это безумно, и начинают делать «менеджеров», DrawableGameComponentчтобы обойти эти проблемы. За исключением того, что у них все еще есть эти проблемы на границах "менеджера".

    Эти «менеджеры» всегда можно легко заменить серией вызовов методов в нужном порядке. Все остальное слишком сложно.

Конечно, как только вы начнете использовать эти хаки, вам потребуется реальная работа, чтобы отменить эти хаки, как только вы поймете, что вам нужно больше, чем то, что DrawableGameComponentобеспечивает. Вы становитесь " запертыми ". И часто возникает искушение продолжать накапливать взломы - что на практике вас утомит и ограничит виды игр, которые вы можете сделать, относительно упрощенными.


Многие люди, кажется, достигают этой точки и имеют внутреннюю реакцию - возможно, потому, что они используют DrawableGameComponentи не хотят признавать себя «неправильными». Таким образом, они фиксируют тот факт, что, по крайней мере, можно заставить его «работать».

Конечно, это можно сделать, чтобы «работать»! Мы программисты - заставить вещи работать под необычными ограничениями - это практически наша работа. Но эта сдержанность не нужна, полезна или рациональна ...


Что особенно flabbergasting о том , что это поразительно тривиальной боковой шаг весь этот вопрос. Здесь я даже дам вам код:

public class Actor
{
    public virtual void Update(GameTime gameTime);
    public virtual void Draw(GameTime gameTime);
}

List<Actor> actors = new List<Actor>();

foreach(var actor in actors)
    actor.Update(gameTime);

foreach(var actor in actors)
    actor.Draw(gameTime);

(Это просто добавить в сортировку согласно DrawOrderи UpdateOrder. Один простой способ - сохранить два списка и использовать List<T>.Sort(). Это также тривиально для обработки Load/ UnloadContent.)

Это не важно , что этот код на самом деле является . Важно то, что я могу тривиально изменить его .

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

И я могу сделать это немедленно. Мне не нужно браться за выбор DrawableGameComponentиз моего кода. Или еще хуже: взломать его каким-либо образом, что сделает его еще сложнее, когда в следующий раз возникнет такая необходимость.


На самом деле есть только один сценарий, в котором это хороший выбор DrawableGameComponent: если вы буквально создаете и публикуете промежуточное программное обеспечение в стиле перетаскивания для XNA. Что было его первоначальной целью. Который - добавлю - никто не делает!

(Точно так же это имеет смысл использоватьServices при создании пользовательских типов контента для ContentManager.)

Эндрю Рассел
источник
Хотя я согласен с вашими замечаниями, я также не вижу ничего особенно неправильного в использовании вещей, которые наследуются DrawableGameComponentдля определенных компонентов, особенно для таких вещей, как экраны меню (меню, множество кнопок, опции и прочее) или оверлеи FPS / debug ты упомянул. В этих случаях вам обычно не требуется детальный контроль над порядком прорисовки, и в итоге получается меньше кода, который нужно писать вручную (что хорошо, если вам не нужно писать этот код). Но я думаю, что это в значительной степени сценарий перетаскивания, о котором вы упоминали.
Groo