Мне трудно найти способ упорядочить игровые объекты так, чтобы они были полиморфными, но в то же время не полиморфными.
Вот пример: предполагая, что мы хотим, чтобы все наши объекты были update()
и draw()
. Для этого нам нужно определить базовый класс, GameObject
который имеет эти два виртуальных чистых метода и позволяет задействовать полиморфизм:
class World {
private:
std::vector<GameObject*> objects;
public:
// ...
update() {
for (auto& o : objects) o->update();
for (auto& o : objects) o->draw(window);
}
};
Предполагается, что метод update позаботится о любом состоянии, которое должен обновить конкретный объект класса. Дело в том, что каждый объект должен знать об окружающем их мире. Например:
- Мина должна знать, сталкивается ли кто-то с этим
- Солдат должен знать, находится ли солдат другой команды поблизости
- Зомби должен знать, где находится ближайший мозг в радиусе
Для пассивных взаимодействий (как и в первом) я думал, что обнаружение столкновений может делегировать, что делать в конкретных случаях столкновений, самому объекту с on_collide(GameObject*)
.
Большая часть другой информации (как и два других примера) может быть запрошена игровым миром, переданным update
методу. Теперь мир не различает объекты по их типу (он хранит все объекты в одном полиморфном контейнере), поэтому то, что он на самом деле вернет с идеалом, world.entities_in(center, radius)
- это контейнер GameObject*
. Но, конечно, солдат не хочет атаковать других солдат из своей команды, и зомби не относится к другим зомби. Поэтому нам нужно различать поведение. Решение может быть следующим:
void TeamASoldier::update(const World& world) {
auto list = world.entities_in(position, eye_sight);
for (const auto& e : list)
if (auto enemy = dynamic_cast<TeamBSoldier*>(e))
// shoot towards enemy
}
void Zombie::update(const World& world) {
auto list = world.entities_in(position, eye_sight);
for (const auto& e : list)
if (auto enemy = dynamic_cast<Human*>(e))
// go and eat brain
}
но, конечно, количество dynamic_cast<>
кадров в кадре может быть ужасно высоким, и мы все знаем, насколько медленным dynamic_cast
может быть. Та же проблема относится и к on_collide(GameObject*)
делегату, который мы обсуждали ранее.
Так какой же это идеальный способ организовать код, чтобы объекты могли знать о других объектах и могли игнорировать их или предпринимать действия в зависимости от их типа?
источник
dynamic_cast<Human*>
, реализуйте что-то вроде abool GameObject::IsHuman()
, которое возвращаетfalse
по умолчанию, но переопределяется для возвратаtrue
вHuman
класс.IsA
переопределений оказалось для меня лишь незначительно лучше, чем динамическое приведение на практике. Лучше всего, чтобы пользователь по возможности сортировал списки данных, а не выполнял итерацию вслепую по всему пулу сущностей.TeamASoldier
иTeamBSoldier
в самом деле идентична - стреляйте по любому в другой команде. Все, что ему нужно для других сущностей, - этоGetTeam()
метод в его наиболее конкретном и, на примере congusbongus, который может быть абстрагирован еще дальше вIsEnemyOf(this)
виде интерфейса. Код не должен заботиться о таксономической классификации солдат, зомби, игроков и т. Д. Сосредоточьтесь на взаимодействии, а не на типах.Ответы:
Вместо того, чтобы реализовывать принятие решений каждой сущностью в отдельности, вы могли бы альтернативно использовать шаблон контроллера. У вас будут классы центрального контроллера, которые знают обо всех объектах (которые имеют для них значение) и контролируют их поведение.
MovementController будет обрабатывать движение всех объектов, которые могут двигаться (поиск маршрута, обновление позиций на основе текущих векторов движения).
Контроллер MineBehaviorController проверит все мины и всех солдат и даст команду взорваться, когда солдат подойдет слишком близко.
Контроллер ZombieBehaviorControl проверяет всех зомби и солдат, находящихся поблизости, выбирает лучшую цель для каждого зомби и приказывает ему переместиться туда и атаковать его (сам ход обрабатывается контроллером MovementController).
SoldierBehaviorController будет анализировать всю ситуацию, а затем придумывать тактические инструкции для всех солдат (вы перемещаетесь туда, вы стреляете в этого, вы лечите этого парня ...). Фактическое выполнение этих команд более высокого уровня также будет обрабатываться контроллерами более низкого уровня. Когда вы приложите некоторые усилия, вы сможете сделать ИИ способным принимать разумные решения.
источник
std::map
s, а сущности - это только идентификаторы, и тогда мы должны создать какую-то систему типов (возможно, с компонентом тега, потому что средство визуализации должно знать, что рисовать); и если мы не хотим этого делать, нам понадобится компонент рисования: но ему нужен компонент положения, чтобы знать, где рисовать, поэтому мы создаем зависимости между компонентами, которые мы решаем с помощью сверхсложной системы обмена сообщениями. Это то, что вы предлагаете?Прежде всего, попробуйте реализовать функции, чтобы объекты оставались независимыми друг от друга, когда это возможно. Особенно вы хотите сделать это для многопоточности. В вашем первом примере кода набор всех объектов может быть разделен на наборы, соответствующие количеству ядер ЦП, и обновляться очень эффективно.
Но, как вы сказали, для некоторых функций необходимо взаимодействие с другими объектами. Это означает, что состояние всех объектов должно быть синхронизировано в некоторых точках. Другими словами, ваше приложение должно сначала дождаться завершения всех параллельных задач, а затем применить вычисления, которые включают взаимодействие. Полезно уменьшить количество этих точек синхронизации, поскольку они всегда подразумевают, что некоторые потоки должны ждать завершения других.
Поэтому я предлагаю буферизовать ту информацию об объектах, которая необходима внутри других объектов. При наличии такого глобального буфера вы можете обновлять все свои объекты независимо друг от друга, но только в зависимости от себя и глобального буфера, который быстрее и проще в обслуживании. На фиксированном временном шаге, скажем, после каждого кадра, обновляйте буфер с текущим состоянием объектов.
То, что вы делаете один раз для каждого кадра, это: 1. буферизует текущее состояние объектов глобально, 2. обновляет все объекты на основе себя и буфера, 3. рисует ваши объекты и затем начинает заново с обновлением буфера.
источник
Используйте систему на основе компонентов, в которой у вас есть базовый GameObject, который содержит 1 или более компонентов, которые определяют их поведение.
Например, предположим, что какой-то объект должен все время двигаться влево и вправо (платформа), вы можете создать такой компонент и прикрепить его к GameObject.
Теперь, скажем, игровой объект должен медленно вращаться все время, вы можете создать отдельный компонент, который будет делать это, и прикрепить его к GameObject.
Что делать, если вы хотите иметь движущуюся платформу, которая также вращается, в традиционной иерархии классов, которая становится трудной без дублирования кода.
Прелесть этой системы в том, что вместо того, чтобы иметь класс Rotatable или MovingPlatform, вы присоединяете оба этих компонента к GameObject, и теперь у вас есть MovingPlatform, которая автоматически поворачивается.
Все компоненты имеют свойство 'requireUpdate', которое, в то время как true, GameObject будет вызывать метод 'update' для указанного компонента. Например, скажем, у вас есть компонент Draggable, этот компонент при наведении мыши (если он был над GameObject) может установить для 'requireUpdate' значение true, а затем при наведении мыши установить его в false. Позволяет ему следовать за мышью, только когда мышь нажата.
У одного из разработчиков Tony Hawk Pro Skater есть описание, и его стоит прочитать: http://cowboyprogramming.com/2007/01/05/evolve-your-heirachy/
источник
Подарите композицию наследству.
Мой самый сильный совет, кроме этого: не втягивайтесь в мышление «Я хочу, чтобы это было в высшей степени гибким». Гибкость велика, но помните, что на каком-то уровне, в любой конечной системе, такой как игра, есть атомарные части, которые используются для построения целого. Так или иначе, ваша обработка опирается на эти заранее определенные атомарные типы. Другими словами, обслуживание данных «любого» типа (если бы это было возможно) не помогло бы вам в долгосрочной перспективе, если у вас нет кода для его обработки. По сути, весь код должен анализировать / обрабатывать данные на основе известных спецификаций ... что означает предопределенный набор типов. Насколько велик этот набор? Вам решать.
Эта статья предлагает понимание принципа Composition over Inheritance при разработке игр с помощью надежной и эффективной архитектуры объектно-компонентных компонентов.
Создавая сущности из (отличающихся) подмножеств некоторого надмножества предопределенных компонентов, вы предлагаете своим ИИ конкретные, частичные способы понимания мира и действующих лиц вокруг них, читая состояния компонентов этих субъектов.
источник
Лично я рекомендую не использовать функцию draw самого класса Object. Я даже рекомендую хранить расположение / координаты Объектов вне самого Объекта.
Этот метод draw () будет иметь дело с API-интерфейсом низкоуровневого рендеринга OpenGL, OpenGL ES, Direct3D, вашим слоем обертывания для этих API или API движков. Возможно, вам придется переключаться между ними (например, если вы хотите поддержать OpenGL + OpenGL ES + Direct3D.
Этот GameObject должен просто содержать основную информацию о его внешнем виде, такую как Mesh или, возможно, более крупный пакет, включая входы шейдеров, состояние анимации и так далее.
Также вам понадобится гибкий графический конвейер. Что произойдет, если вы хотите упорядочить объекты по их расстоянию до камеры. Или их тип материала. Что произойдет, если вы хотите нарисовать «выбранный» объект другим цветом. Как насчет того, чтобы вместо того, чтобы фактически выполнять рендеринг так же, как вы вызываете функцию рисования объекта, вместо этого он помещает ее в список командных действий для выполнения рендеринга (может потребоваться для многопоточности). Вы можете делать такие вещи с другой системой, но это PITA.
Я рекомендую вместо непосредственного рисования связывать все объекты, которые вы хотите, с другой структурой данных. Эта привязка действительно должна иметь ссылку на местоположение объектов и информацию рендеринга.
Ваши уровни / куски / области / карты / концентраторы / весь мир / все, что получает пространственный индекс, содержит объекты и возвращает их на основе координатных запросов и может быть простым списком или чем-то вроде Octree. Это также может быть обертка к чему-то, реализованному сторонним физическим движком как физическая сцена. Он позволяет вам выполнять такие действия, как «Запрос всех объектов, находящихся в поле зрения камеры, с некоторой дополнительной областью вокруг них», или для более простых игр, в которых вы можете просто визуализировать все, захватывая весь список.
Пространственные индексы не должны содержать фактическую информацию о позиционировании. Они работают, сохраняя объекты в древовидных структурах относительно расположения других объектов. Они могут быть своего рода кэшем с потерями, позволяющим быстро искать объект в зависимости от его положения. Нет необходимости дублировать ваши действительные координаты X, Y, Z. Сказав, что вы могли бы, если вы хотите сохранить
На самом деле ваши игровые объекты даже не должны содержать собственную информацию о местоположении. Например, объект, который не был помещен в уровень, не должен иметь координаты x, y, z, что не имеет смысла. Вы можете указать это в специальном указателе. Если вам нужно искать координаты объекта на основе его фактической привязки, тогда вы захотите иметь привязку между объектом и графом сцены (графы сцены предназначены для возврата объектов на основе координат, но медленно возвращают координаты на основе объектов) ,
Когда вы добавляете объект на уровень. Это сделает следующее:
1) Создайте структуру местоположения:
Это также может быть ссылка на объект в сторонних физических движках. Или это могут быть координаты смещения со ссылкой на другое местоположение (для камеры слежения или прикрепленного объекта или примера). С полиморфизмом это может зависеть от того, статический или динамический объект. Сохраняя ссылку на пространственный индекс здесь, когда координаты обновляются, пространственный индекс также может быть.
Если вы беспокоитесь о динамическом распределении памяти, используйте пул памяти.
2) Связывание / связь между вашим объектом, его местоположением и графом сцены.
3) Привязка добавляется к пространственному индексу внутри уровня в соответствующей точке.
Когда вы готовитесь к визуализации.
1) Получить камеру (это будет просто другой объект, за исключением того, что его местоположение будет отслеживать персонажа игроков, а ваш рендерер будет иметь специальную ссылку на него, фактически это все, что ему действительно нужно).
2) Получить SpacialBinding камеры.
3) Получить пространственный индекс из привязки.
4) Запрос объектов, которые (возможно) видны для камеры.
5А) Вам необходимо обработать визуальную информацию. Текстуры загружаются в GPU и так далее. Лучше всего это сделать заранее (например, при загрузке уровня), но, возможно, это можно сделать во время выполнения (для открытого мира вы можете загружать вещи, когда вы приближаетесь к чанку, но все же следует делать это заранее).
5B) При необходимости создайте кэшированное дерево рендеринга, если вы хотите выполнить глубину / сортировку материала или отслеживать близлежащие объекты, которые могут быть видны позже. В противном случае вы можете просто запросить пространственный индекс каждый раз, когда он будет зависеть от ваших требований к игре / производительности.
Вашему рендереру, вероятно, понадобится объект RenderBinding, который свяжет объект с координатами
Затем при рендеринге просто запустите список.
Я использовал ссылки выше, но это могут быть умные указатели, необработанные указатели, дескрипторы объектов и так далее.
РЕДАКТИРОВАТЬ:
Что касается того, чтобы вещи «знали» друг о друге. Это обнаружение столкновений. Это будет реализовано в Octree, вероятно. Вам нужно будет предоставить некоторый обратный вызов в вашем главном объекте. Этот материал лучше всего обрабатывается соответствующим физическим движком, таким как Bullet. В этом случае просто замените Octree на PhysicsScene, а Position - ссылкой на что-то вроде CollisionMesh.getPosition ().
источник