Как игровые объекты должны знать друг о друге?

18

Мне трудно найти способ упорядочить игровые объекты так, чтобы они были полиморфными, но в то же время не полиморфными.

Вот пример: предполагая, что мы хотим, чтобы все наши объекты были 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*)делегату, который мы обсуждали ранее.

Так какой же это идеальный способ организовать код, чтобы объекты могли знать о других объектах и ​​могли игнорировать их или предпринимать действия в зависимости от их типа?

башмак
источник
1
Я думаю, что вы ищете универсальную пользовательскую реализацию RT ++ C ++. Тем не менее, ваш вопрос, похоже, касается не только разумных механизмов RTTI. Вещи, которые вы запрашиваете, требуются практически любому промежуточному программному обеспечению, которое будет использовать игра (система анимации, физика и многие другие). В зависимости от списка поддерживаемых запросов, вы можете обманывать RTTI, используя идентификаторы и индексы в массивах, или вы в конечном итоге разработаете полноценный протокол для поддержки более дешевых альтернатив dynamic_cast и type_info.
Теодрон
Я бы посоветовал не использовать систему типов для игровой логики. Например, вместо того, чтобы зависеть от результата dynamic_cast<Human*>, реализуйте что-то вроде a bool GameObject::IsHuman(), которое возвращает falseпо умолчанию, но переопределяется для возврата trueв Humanкласс.
congusbongus
дополнительно: вы почти никогда не отправляете кучу объектов друг другу, которые могут быть заинтересованы в них. Это очевидная оптимизация, которую вы должны рассмотреть.
Теодрон
@congusbongus Использование виртуальной таблицы и пользовательских IsAпереопределений оказалось для меня лишь незначительно лучше, чем динамическое приведение на практике. Лучше всего, чтобы пользователь по возможности сортировал списки данных, а не выполнял итерацию вслепую по всему пулу сущностей.
Теодрон
4
@Jefffrey: в идеале вы не пишите код для конкретного типа. Вы пишете специфичный для интерфейса код («интерфейс» в общем смысле). Ваша логика для a TeamASoldierи TeamBSoldierв самом деле идентична - стреляйте по любому в другой команде. Все, что ему нужно для других сущностей, - это GetTeam()метод в его наиболее конкретном и, на примере congusbongus, который может быть абстрагирован еще дальше в IsEnemyOf(this)виде интерфейса. Код не должен заботиться о таксономической классификации солдат, зомби, игроков и т. Д. Сосредоточьтесь на взаимодействии, а не на типах.
Шон Мидлдич

Ответы:

11

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

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

Контроллер MineBehaviorController проверит все мины и всех солдат и даст команду взорваться, когда солдат подойдет слишком близко.

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

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

Philipp
источник
1
Вероятно, это также известно как «система», которая управляет логикой для определенных типов компонентов в архитектуре Entity-Component.
Теодрон
Это звучит как решение в стиле C. Компоненты сгруппированы в std::maps, а сущности - это только идентификаторы, и тогда мы должны создать какую-то систему типов (возможно, с компонентом тега, потому что средство визуализации должно знать, что рисовать); и если мы не хотим этого делать, нам понадобится компонент рисования: но ему нужен компонент положения, чтобы знать, где рисовать, поэтому мы создаем зависимости между компонентами, которые мы решаем с помощью сверхсложной системы обмена сообщениями. Это то, что вы предлагаете?
Чистка обуви
1
@Jefffrey "Это звучит как решение в стиле C" - даже если это будет правдой, почему это обязательно будет плохо? Другие проблемы могут быть действительными, но есть решения для них. К сожалению, комментарий слишком короткий, чтобы правильно адресовать каждый из них.
Филипп
1
@Jefffrey Использование подхода, при котором сами компоненты не имеют никакой логики, а «системы» отвечают за обработку всей логики, не создают зависимостей между компонентами и не требуют сверхсложной системы обмена сообщениями (по крайней мере, не такой сложной). , См. Например: gamadu.com/artemis/tutorial.html
1

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

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

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

То, что вы делаете один раз для каждого кадра, это: 1. буферизует текущее состояние объектов глобально, 2. обновляет все объекты на основе себя и буфера, 3. рисует ваши объекты и затем начинает заново с обновлением буфера.

Данияр
источник
1

Используйте систему на основе компонентов, в которой у вас есть базовый 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/

onedayitwillmake
источник
1

Подарите композицию наследству.

Мой самый сильный совет, кроме этого: не втягивайтесь в мышление «Я хочу, чтобы это было в высшей степени гибким». Гибкость велика, но помните, что на каком-то уровне, в любой конечной системе, такой как игра, есть атомарные части, которые используются для построения целого. Так или иначе, ваша обработка опирается на эти заранее определенные атомарные типы. Другими словами, обслуживание данных «любого» типа (если бы это было возможно) не помогло бы вам в долгосрочной перспективе, если у вас нет кода для его обработки. По сути, весь код должен анализировать / обрабатывать данные на основе известных спецификаций ... что означает предопределенный набор типов. Насколько велик этот набор? Вам решать.

Эта статья предлагает понимание принципа Composition over Inheritance при разработке игр с помощью надежной и эффективной архитектуры объектно-компонентных компонентов.

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

инженер
источник
1

Лично я рекомендую не использовать функцию 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) Создайте структуру местоположения:

 class Location { 
     float x, y, z; // Or a special Coordinates class, or a vec3 or whatever.
     SpacialIndex& spacialIndex; // Note this could be the area/level/map/whatever here
 };

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

Если вы беспокоитесь о динамическом распределении памяти, используйте пул памяти.

2) Связывание / связь между вашим объектом, его местоположением и графом сцены.

typedef std::pair<Object, Location> SpacialBinding.

3) Привязка добавляется к пространственному индексу внутри уровня в соответствующей точке.

Когда вы готовитесь к визуализации.

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

2) Получить SpacialBinding камеры.

3) Получить пространственный индекс из привязки.

4) Запрос объектов, которые (возможно) видны для камеры.

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

5B) При необходимости создайте кэшированное дерево рендеринга, если вы хотите выполнить глубину / сортировку материала или отслеживать близлежащие объекты, которые могут быть видны позже. В противном случае вы можете просто запросить пространственный индекс каждый раз, когда он будет зависеть от ваших требований к игре / производительности.

Вашему рендереру, вероятно, понадобится объект RenderBinding, который свяжет объект с координатами

class RenderBinding {
    Object& object;
    RenderInformation& renderInfo;
    Location& location // This could just be a coordinates class.
}

Затем при рендеринге просто запустите список.

Я использовал ссылки выше, но это могут быть умные указатели, необработанные указатели, дескрипторы объектов и так далее.

РЕДАКТИРОВАТЬ:

class Game {
    weak_ptr<Camera> camera;
    Level level1;

    void init() {
        Camera camera(75.0_deg, 1.025_ratio, 1000_meters);
        auto template_player = loadObject("Player.json")
        auto player = level1.addObject(move(player), Position(1.0, 2.0, 3.0));
        level1.addObject(move(camera), getRelativePosition(player));

        auto template_bad_guy = loadObject("BadGuy.json")
        level1.addObject(template_bad_guy, {10, 10, 20});
        level1.addObject(template_bad_guy, {10, 30, 20});
        level1.addObject(move(template_bad_guy), {50, 30, 20});
    }

    void render() {
        camera->getFrustrum();
        auto level = camera->getLocation()->getLevel();
        auto object = level.getVisible(camera);
        for(object : objects) {
            render(objects);
        }
    }

    void render(Object& object) {
        auto ri = object.getRenderInfo();
        renderVBO(ri.getVBO());
    }

    Object loadObject(string file) {
        Object object;
        // Load file from disk and set the properties
        // Upload mesh data, textures to GPU. Load shaders whatever.
        object.setHitPoints(// values from file);
        object.setRenderInfo(// data from 3D api);
    }
}

class Level {
    Octree octree;
    vector<ObjectPtr> objects;
    // NOTE: If your level is mesh based there might also be a BSP here. Or a hightmap for an openworld
    // There could also be a physics scene here.
    ObjectPtr addObject(Object&& object, Position& pos) {
        Location location(pos, level, object);
        objects.emplace_back(object);
        object->setLocation(location)
        return octree.addObject(location);
    }
    vector<Object> getVisible(Camera& camera) {
        auto f = camera.getFtrustrum();
        return octree.getObjectsInFrustrum(f);
    }
    void updatePosition(LocationPtr l) {
        octree->updatePosition(l);
    }
}

class Octree {
    OctreeNode root_node;
    ObjectPtr add(Location&& object) {
        return root_node.add(location);
    }
    vector<ObjectPtr> getObjectsInRadius(const vec3& position, const float& radius) { // pass to root_node };
    vector<ObjectPtr> getObjectsinFrustrum(const FrustrumShape frustrum;) {//...}
    void updatePosition(LocationPtr* l) {
        // Walk up from l.octree_node until you reach the new place
        // Check if objects are colliding
        // l.object.CollidedWith(other)
    }
}

class Object {
    Location location;
    RenderInfo render_info;
    Properties object_props;
    Position getPosition() { return getLocation().position; }
    Location getLocation() { return location; }
    void collidedWith(ObjectPtr other) {
        // if other.isPickup() && object.needs(other.pickupType()) pick it up, play sound whatever
    }
}

class Location {
    Position position;
    LevelPtr level;
    ObjectPtr object;
    OctreeNote octree_node;
    setPosition(Position position) {
        position = position;
        level.updatePosition(this);
    }
}

class Position {
    vec3 coordinates;
    vec3 rotation;
}

class RenderInfo {
    AnimationState anim;
}
class RenderInfo_OpenGL : public RenderInfo {
    GLuint vbo_object;
    GLuint texture_object;
    GLuint shader_object;
}

class Camera: public Object {
    Degrees fov;
    Ratio aspect;
    Meters draw_distance;
    Frustrum getFrustrum() {
        // Use above to make a skewed frustum box
    }
}

Что касается того, чтобы вещи «знали» друг о друге. Это обнаружение столкновений. Это будет реализовано в Octree, вероятно. Вам нужно будет предоставить некоторый обратный вызов в вашем главном объекте. Этот материал лучше всего обрабатывается соответствующим физическим движком, таким как Bullet. В этом случае просто замените Octree на PhysicsScene, а Position - ссылкой на что-то вроде CollisionMesh.getPosition ().

Дэвид С. Бишоп
источник
Вау, это выглядит очень хорошо. Я думаю, что я понял основную идею, но без большего количества примера я не могу вполне получить внешнее представление об этом. У вас есть еще ссылки или примеры из жизни? (Тем временем я продолжу читать этот ответ).
Чистка
На самом деле нет никаких примеров, это то, что я планирую делать, когда у меня будет время. Я добавлю еще несколько общих классов и посмотрю, поможет ли это. Есть это и это . это больше о классах объектов, чем о том, как они связаны или рендеринга. Поскольку я не реализовал это сам, могут быть подводные камни, биты, которые нужно проработать, или производительность, но я думаю, что общая структура в порядке.
Дэвид С. Бишоп