Лучший способ организовать сущности в игре? - PullRequest
14 голосов
/ 19 апреля 2009

Допустим, я создаю игру OpenGL на C ++, в которой будет создано много объектов (врагов, персонажей, предметов и т. Д.). Мне интересно, как лучше организовать их, так как они будут создаваться и уничтожаться в режиме реального времени в зависимости от времени, позиции / действий игрока и т. Д.

Вот что я подумала до сих пор: У меня может быть глобальный массив для хранения указателей на эти объекты. Текстуры / контекст для этих объектов загружаются в их конструкторы. Эти объекты будут иметь разные типы, поэтому я могу привести указатели, чтобы получить их в массиве, но позже я хочу иметь функцию renderObjects (), которая будет использовать цикл для вызова функции ObjectN.render () для каждого существующего объекта.

Я думаю, что пробовал это раньше, но я не знал, с каким типом инициализировать массив, поэтому я выбрал произвольный тип объекта, а затем произвел все, что не было этого типа. Если я помню, это не сработало, потому что компилятор не хотел, чтобы я разыменовывал указатели, если он больше не знал их тип, даже если данная функция-член имела то же имя: (* Object5) .render () <-doesn не работает? </p>

Есть ли лучший способ? Как коммерческие игры типа HL2 справляются с этим? Я предполагаю, что должен быть какой-то модуль и т. Д., Который отслеживает все объекты.

Ответы [ 5 ]

33 голосов
/ 20 апреля 2009

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

Подробнее об этом можно узнать, выполнив поиск "разработка игр на основе компонентов". Известная статья Evolve Your Hierarchy из блога программирования Cowboy.

В моей системе сущности - это просто идентификаторы - длинные без знака, как в реляционной базе данных. Все данные и логика, связанные с моими сущностями, записаны в Компоненты. У меня есть Системы, которые связывают идентификаторы сущностей с их соответствующими компонентами. Примерно так:

typedef unsigned long EntityId;

class Component {
    Component(EntityId id) : owner(id) {}
    EntityId owner;
};

template <typename C> class System {
    std::map<EntityId, C * > components;
};

Тогда для каждого вида функциональности я пишу специальный компонент. Все объекты не имеют одинаковых компонентов. Например, у вас может быть статический каменный объект, который имеет WorldPositionComponent и ShapeComponent, и движущийся враг, имеющий те же компоненты плюс VelocityComponent. Вот пример:

class WorldPositionComponent : public Component {
    float x, y, z;
    WorldPositionComponent(EntityId id) : Component(id) {}
};

class RenderComponent : public Component {
    WorldPositionComponent * position;
    3DModel * model;
    RenderComponent(EntityId id, System<WorldPositionComponent> & wpSys)
        : Component(id), position(wpSys.components[owner]) {}
    void render() {
        model->draw(position);
    }
};

class Game {
    System<WorldPositionComponent> wpSys;
    System<RenderComponent> rSys;
    void init() {
        EntityId visibleObject = 1;
        // Watch out for memory leaks.
        wpSys.components[visibleObject] = new WorldPositionComponent(visibleObject);
        rSys.components[visibleObject] = new RenderComponent(visibleObject, wpSys);
        EntityId invisibleObject = 2;
        wpSys.components[invisibleObject] = new WorldPositionComponent(invisibleObject);
        // No RenderComponent for invisibleObject.
    }
    void gameLoop() {
        std::map<EntityId, RenderComponent *>::iterator it;
        for (it = rSys.components.iterator(); it != rSys.components.end(); ++it) {
            (*it).second->render();
        }
    }
};

Здесь у вас есть 2 компонента, WorldPosition и Render. Класс Game содержит 2 системы. Компонент Render имеет доступ к позиции объекта. Если у сущности нет компонента WorldPosition, вы можете выбрать значения по умолчанию или проигнорировать сущность. Метод Game :: gameLoop () будет отображать только visibleObject. Для неперекачиваемых компонентов нет отходов переработки.

Вы также можете разделить мой класс Game на два или три, чтобы отделить системы отображения и ввода от логики. Что-то вроде модели, вида и контроллера.

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

11 голосов
/ 19 апреля 2009

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

Вот пример. Сначала вам нужно настроить базовый класс для получения ваших объектов из:

class BaseObject
{
public:
    virtual void Render() = 0;
};

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

#include <set>

typedef std::set<BaseObject *> GAMEOBJECTS;
GAMEOBJECTS g_gameObjects;

Чтобы добавить объект, создайте производный класс и создайте его экземпляр:

class Enemy : public BaseObject
{
public:
    Enemy() { }
    virtual void Render()
    {
      // Rendering code goes here...
    }
};

g_gameObjects.insert(new Enemy());

Затем, чтобы получить доступ к объектам, просто переберите их:

for(GAMEOBJECTS::iterator it = g_gameObjects.begin();
    it != g_gameObjects.end();
    it++)
{
    (*it)->Render();
}

Чтобы создать различные типы объектов, просто выведите больше классов из класса BaseObject. Не забудьте удалить объекты при удалении их из коллекции.

8 голосов
/ 19 апреля 2009

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

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

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

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

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

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

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

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

Кроме того, разделение задач позволяет заменять слой дисплея любой технологией, которая вам нравится: Open GL, DirectX, программный рендеринг, Adobe Flash, Nintendo DS и т. Д. Без лишних хлопот с другими слоями.

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

2 голосов
/ 19 апреля 2009

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

1 голос
/ 20 апреля 2009

Есть ли лучший способ? Как коммерческие игры типа HL2 справляются с этим? Я предполагаю, что должен быть какой-то модуль и т. Д., Который отслеживает все объекты.

Коммерческие 3D-игры используют вариацию Графа сцены . Иерархия объектов, подобная той, которую описывает Адам, помещается в то, что обычно является древовидной структурой. Чтобы рендерить или манипулировать объектами, вы просто гуляете по дереву.

В нескольких книгах обсуждается это, и лучшее, что я нашел, это 3D Game Engine Design and Architecture, обе от David Eberly.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...