Вместо привычного тяжелого игрового движка я играю с компонентным подходом. Однако мне трудно объяснить, где позволить компонентам делать свое дело.
Скажем, у меня есть простая сущность, у которой есть список компонентов. Конечно, сущность не знает, что это за компоненты. Может присутствовать компонент, который дает объекту положение на экране, другой может быть там, чтобы нарисовать объект на экране.
Чтобы эти компоненты работали, они должны обновлять каждый кадр, самый простой способ сделать это - пройтись по дереву сцены, а затем для каждой сущности обновить каждый компонент. Но некоторым компонентам может потребоваться немного больше управления. Например, компонент, который делает объект конфликтным, должен управляться чем-то, что может контролировать все конфликтующие компоненты. Компонент, который делает объект доступным для рисования, требует, чтобы кто-то наблюдал за всеми другими объектами для рисования, чтобы выяснить порядок рисования и т. Д.
Итак, мой вопрос: где я могу обновить компоненты, как это можно донести до менеджеров?
Я думал об использовании объекта-менеджера синглтона для каждого из типов компонентов, но у него есть обычные недостатки использования синглтона, способ смягчить это немного - использовать внедрение зависимостей, но это звучит как излишнее для этой проблемы. Я мог бы также пройтись по дереву сцены и затем собрать различные компоненты в списки, используя какой-то шаблон наблюдателя, но это кажется немного расточительным для каждого кадра.
источник
Ответы:
Я бы предложил начать с чтения 3 большой лжи Майка Актона, потому что вы нарушаете две из них. Я серьезно, это изменит ваш дизайн кода: http://cellperformance.beyond3d.com/articles/2008/03/three-big-lies.html
Так что вы нарушаете?
Ложь № 3 - Код важнее данных
Вы говорите о внедрении зависимости, которая может быть полезна в некоторых (и только в некоторых) случаях, но всегда должна вызывать большой сигнал тревоги, если вы ее используете, особенно в разработке игр! Почему? Потому что это часто ненужная абстракция. И абстракции в неправильных местах ужасны. Итак, у вас есть игра. В игре есть менеджеры для разных компонентов. Компоненты все определены. Так что создайте класс где-нибудь в коде основного игрового цикла, в котором «есть» менеджеры. Подобно:
Дайте ему некоторые функции получения, чтобы получить каждый класс менеджера (getBulletManager ()). Может быть, этот класс сам по себе является синглтоном или доступен из него (возможно, у вас где-то есть центральный синглтон игры). Нет ничего плохого в четко определенных жестко закодированных данных и поведении.
Не создавайте ManagerManager, который позволяет регистрировать менеджеров с помощью ключа, который может быть получен с помощью этого ключа другими классами, которые хотят использовать менеджер. Это отличная система и очень гибкая, но здесь речь идет об игре. Вы точно знаете , какие системы есть в игре. Почему притворяешься, как ты? Потому что это система для людей, которые думают, что код важнее данных. Они скажут: «Код гибкий, данные просто заполняют его». Но код это просто данные. Система, которую я описал, гораздо проще, надежнее, проще в обслуживании и намного более гибкой (например, если поведение одного менеджера отличается от других менеджеров, вам нужно всего лишь изменить несколько строк вместо того, чтобы переделывать всю систему)
Ложь № 2 - Код должен быть разработан вокруг модели мира
Итак, у вас есть сущность в игровом мире. У сущности есть ряд компонентов, определяющих ее поведение. Таким образом, вы создаете класс Entity со списком объектов Component и функцию Update (), которая вызывает функцию Update () каждого компонента. Правильно?
Нет :) Это строится вокруг модели мира: в вашей игре есть пуля, поэтому вы добавляете класс Bullet. Затем вы обновляете каждую пулю и переходите к следующей. Это абсолютно убьет вашу производительность и даст вам ужасную запутанную кодовую базу с дублирующимся кодом везде и без логической структуризации аналогичного кода. (Ознакомьтесь с моим ответом здесь для более подробного объяснения того, почему традиционный ОО-проект отстой, или посмотрите на Data-Oriented Design)
Давайте посмотрим на ситуацию без нашей предвзятости. Мы хотим следующее, не больше и не меньше (обратите внимание, что нет необходимости создавать класс для сущности или объекта):
И давайте посмотрим на ситуацию. Ваша система компонентов будет обновлять поведение каждого объекта в игре каждый кадр. Это определенно критическая система вашего двигателя. Производительность здесь важна!
Если вы знакомы либо с архитектурой компьютера, либо с ориентированным на данные дизайном, вы знаете, как достигается лучшая производительность: плотно упакованная память и группировка выполнения кода. Если вы выполняете фрагменты кода A, B и C, например: ABCABCABC, вы не получите такую же производительность, как при ее выполнении, например: AAABBBCCC. Это не только потому, что кэш инструкций и данных будет использоваться более эффективно, но и потому, что если вы выполняете все буквы «А» один за другим, есть много возможностей для оптимизации: удаление дублирующего кода, предварительный расчет данных, которые используются все буквы "А" и т. д.
Поэтому, если мы хотим обновить все компоненты, давайте не будем делать их классами / объектами с помощью функции обновления. Давайте не будем вызывать эту функцию обновления для каждого компонента в каждой сущности. Это решение "ABCABCABC". Давайте сгруппируем все идентичные обновления компонентов вместе. Затем мы можем обновить все A-компоненты, затем B и т. Д. Что нам нужно для этого?
Во-первых, нам нужны менеджеры компонентов. Для каждого типа компонента в игре нам нужен класс менеджера. Он имеет функцию обновления, которая обновит все компоненты этого типа. Он имеет функцию создания, которая добавит новый компонент этого типа, и функцию удаления, которая уничтожит указанный компонент. Могут быть и другие вспомогательные функции для получения и установки данных, специфичных для этого компонента (например, установка трехмерной модели для компонента модели). Обратите внимание, что менеджер в некотором смысле является черным ящиком для внешнего мира. Мы не знаем, как хранятся данные каждого компонента. Мы не знаем, как обновляется каждый компонент. Нам все равно, пока компоненты ведут себя так, как должны.
Далее нам нужна сущность. Вы могли бы сделать это классом, но это вряд ли необходимо. Сущность может быть не чем иным, как уникальным целочисленным идентификатором или хешированной строкой (также целочисленным). Когда вы создаете компонент для сущности, вы передаете идентификатор в качестве аргумента менеджеру. Если вы хотите удалить компонент, вы передаете идентификатор еще раз. Могут быть некоторые преимущества добавления немного большего количества данных в сущность, а не просто в качестве идентификатора, но это будут только вспомогательные функции, потому что, как я перечислил в требованиях, все поведение сущности определяется самими компонентами. Хотя это ваш двигатель, поэтому делайте то, что имеет для вас смысл.
Нам нужен Entity Manager. Этот класс будет либо генерировать уникальные идентификаторы, если вы используете решение только для идентификаторов, либо его можно использовать для создания / управления объектами сущностей. Он также может вести список всех сущностей в игре, если вам это нужно. Entity Manager может быть центральным классом вашей системы компонентов, храня ссылки на все ComponentManager в вашей игре и вызывая их функции обновления в правильном порядке. Таким образом, весь игровой цикл должен вызывать EntityManager.update (), и вся система хорошо отделена от остальной части вашего движка.
Это взгляд с высоты птичьего полета, давайте посмотрим, как работают менеджеры компонентов. Вот что вам нужно:
Последний - то, где вы определяете поведение / логику компонентов и полностью зависит от типа компонента, который вы пишете. Компонент AnimationComponent будет обновлять данные анимации на основе кадра, в котором он находится. DragableComponent обновит только компонент, который перетаскивается мышью. PhysicsComponent обновит данные в физической системе. Тем не менее, поскольку вы обновляете все компоненты одного типа за один раз, вы можете выполнить некоторые оптимизации, которые невозможны, когда каждый компонент представляет собой отдельный объект с функцией обновления, которая может быть вызвана в любое время.
Обратите внимание, что я до сих пор никогда не призывал к созданию класса XxxComponent для хранения данных компонентов. Это зависит от вас. Вам нравится Data Oriented Design? Затем структурируйте данные в отдельных массивах для каждой переменной. Вам нравится объектно-ориентированный дизайн? (Я бы не советовал, это все равно убьет вашу производительность во многих местах). Затем создайте объект XxxComponent, который будет содержать данные каждого компонента.
Самое замечательное в менеджерах - это инкапсуляция. В настоящее время инкапсуляция является одной из самых ужасно неправильно используемых философий в мире программирования. Вот как это должно быть использовано. Только менеджер знает, где хранятся данные компонента, как работает логика компонента. Есть несколько функций для получения / установки данных, но это все. Вы можете переписать весь менеджер и его базовые классы, и если вы не измените открытый интерфейс, никто даже не заметит. Изменен физический движок? Просто перепишите PhysicsComponentManager и все готово.
Тогда есть одна последняя вещь: связь и обмен данными между компонентами. Теперь это сложно, и не существует универсального решения. Вы можете создать функции get / set в менеджерах, чтобы, например, компонент столкновения мог получить позицию от компонента position (то есть PositionManager.getPosition (entityID)). Вы можете использовать систему событий. Вы можете хранить некоторые общие данные в сущности (на мой взгляд, самое уродливое решение). Вы можете использовать (это часто используется) систему обмена сообщениями. Или используйте комбинацию нескольких систем! У меня нет ни времени, ни опыта, чтобы заходить в каждую из этих систем, но поиск в google и stackoverflow ваши друзья.
источник
Это типичный наивный подход к обновлению компонентов (и нет ничего плохого в том, что он наивный, если он работает для вас). Одна из больших проблем, с которой вы на самом деле коснулись - вы работаете через интерфейс компонента (например
IComponent
), поэтому вы ничего не знаете о том, что именно вы только что обновили. Вы, вероятно, также ничего не знаете о порядке компонентов внутри объекта, поэтомуСинглтон здесь на самом деле не нужен, поэтому вам следует избегать его, потому что он несет в себе недостатки, о которых вы упомянули. Внедрение зависимостей не является чрезмерным - суть концепции заключается в том, что вы передаете вещи, которые нужны объекту, этому объекту, в идеале в конструкторе. Для этого вам не нужен тяжелый DI-каркас (например, Ninject ) - просто передайте дополнительный параметр конструктору.
Рендерер - это фундаментальная система, и она, вероятно, поддерживает создание и управление временем жизни группы визуализируемых объектов, которые соответствуют визуальным элементам в вашей игре (вероятно, спрайты или модели). Точно так же физический движок, вероятно, имеет контроль над всем, что представляет собой сущности, которые могут двигаться в физическом моделировании (твердые тела). Каждая из этих соответствующих систем должна в определенной степени владеть этими объектами и нести ответственность за их обновление.
Компоненты, которые вы используете в своей системе компоновки игровых сущностей, должны быть просто обернутыми вокруг экземпляров этих систем более низкого уровня - ваш компонент положения может просто обернуть твердое тело, ваш визуальный компонент просто обернет визуализируемый спрайт или модель и так далее.
Тогда сама система, которая владеет объектами нижнего уровня, отвечает за их обновление и может делать это массово и таким способом, который позволяет при необходимости выполнять многопоточное обновление. Ваш основной игровой цикл управляет грубым порядком, в котором эти системы обновляются (сначала физика, затем рендер и т. Д.). Если у вас есть подсистема, которая не имеет срока действия или не контролирует обновления экземпляров, которые она раздает, вы можете создать простую оболочку, которая будет также обрабатывать обновление всех компонентов, относящихся к этой системе, и решать, где ее разместить. обновление по отношению к остальным обновлениям вашей системы (я часто нахожу, что это происходит с компонентами «скрипта»).
Этот подход иногда называют подходом внешних компонентов , если вам нужна более подробная информация.
источник