Я бы предложил начать с чтения 3 большой лжи Майка Актона, потому что вы нарушаете две из них. Я серьезно, это изменит ваш дизайн кода: http://cellperformance.beyond3d.com/articles/2008/03/three-big-lies.html
Так что вы нарушаете?
Ложь № 3 - Код важнее данных
Вы говорите о внедрении зависимости, которая может быть полезна в некоторых (и только в некоторых) случаях, но всегда должна вызывать большой сигнал тревоги, если вы ее используете, особенно в разработке игр! Почему? Потому что это часто ненужная абстракция. И абстракции в неправильных местах ужасны. Итак, у вас есть игра. В игре есть менеджеры для разных компонентов. Компоненты все определены. Так что создайте класс где-нибудь в коде основного игрового цикла, в котором «есть» менеджеры. Подобно:
private CollissionManager _collissionManager;
private BulletManager _bulletManager;
Дайте ему некоторые функции получения, чтобы получить каждый класс менеджера (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 (), и вся система хорошо отделена от остальной части вашего движка.
Это взгляд с высоты птичьего полета, давайте посмотрим, как работают менеджеры компонентов. Вот что вам нужно:
- Создание данных компонента при вызове create (entityID)
- Удалить данные компонента, когда вызывается remove (entityID)
- Обновлять все (применимые) данные компонента при вызове update () (т.е. не все компоненты должны обновлять каждый кадр)
Последний - то, где вы определяете поведение / логику компонентов и полностью зависит от типа компонента, который вы пишете. Компонент AnimationComponent будет обновлять данные анимации на основе кадра, в котором он находится. DragableComponent обновит только компонент, который перетаскивается мышью. PhysicsComponent обновит данные в физической системе. Тем не менее, поскольку вы обновляете все компоненты одного типа за один раз, вы можете выполнить некоторые оптимизации, которые невозможны, когда каждый компонент представляет собой отдельный объект с функцией обновления, которая может быть вызвана в любое время.
Обратите внимание, что я до сих пор никогда не призывал к созданию класса XxxComponent для хранения данных компонентов. Это зависит от вас. Вам нравится Data Oriented Design? Затем структурируйте данные в отдельных массивах для каждой переменной. Вам нравится объектно-ориентированный дизайн? (Я бы не советовал, это все равно убьет вашу производительность во многих местах). Затем создайте объект XxxComponent, который будет содержать данные каждого компонента.
Самое замечательное в менеджерах - это инкапсуляция. В настоящее время инкапсуляция является одной из самых ужасно неправильно используемых философий в мире программирования. Вот как это должно быть использовано. Только менеджер знает, где хранятся данные компонента, как работает логика компонента. Есть несколько функций для получения / установки данных, но это все. Вы можете переписать весь менеджер и его базовые классы, и если вы не измените открытый интерфейс, никто даже не заметит. Изменен физический движок? Просто перепишите PhysicsComponentManager и все готово.
Тогда есть одна последняя вещь: связь и обмен данными между компонентами. Теперь это сложно, и не существует универсального решения. Вы можете создать функции get / set в менеджерах, чтобы, например, компонент столкновения мог получить позицию от компонента position (то есть PositionManager.getPosition (entityID)). Вы можете использовать систему событий. Вы можете хранить некоторые общие данные в сущности (на мой взгляд, самое уродливое решение). Вы можете использовать (это часто используется) систему обмена сообщениями. Или используйте комбинацию нескольких систем! У меня нет ни времени, ни опыта, чтобы заходить в каждую из этих систем, но поиск в google и stackoverflow ваши друзья.