在游戏中组织实体的最佳方式?

时间:2009-04-19 04:23:41

标签: c++ opengl

假设我正在用C ++创建一个OpenGL游戏,它将创建许多对象(敌人,玩家角色,物品等)。我想知道组织这些的最佳方式,因为它们将根据时间,玩家位置/动作等实时创建和销毁。

这是我到目前为止所想到的: 我可以有一个全局数组来存储指向这些对象的指针。这些对象的纹理/上下文在其构造函数中加载。这些对象将有不同的类型,因此我可以转换指针以在数组中获取它们,但我想稍后有一个renderObjects()函数,它将使用循环为每个现有对象调用ObjectN.render()函数。

我想我以前尝试过这个但是我不知道用什么类型来初始化数组,所以我选择了一个任意的对象类型,然后抛出那些不属于那种类型的东西。如果我记得,这不起作用,因为编译器不希望我取消引用指针,如果它不再知道它们的类型,即使给定的成员函数具有相同的名称:(* Object5).render()< - 不起作用?

有更好的方法吗?如何像HL2这样的商业游戏处理这个?我想必须有一些模块等跟踪所有对象。

5 个答案:

答案 0 :(得分:33)

对于我即将成为个人游戏的项目,我使用基于组件的实体系统。

您可以通过搜索“基于组件的游戏开发”来阅读更多相关信息。一篇着名的文章是来自Cowboy编程博客的Evolve Your Hierarchy

在我的系统中,实体只是id - unsigned long,有点像在关系数据库中。 与我的实体关联的所有数据和逻辑都写入组件。我有系统将实体ID与其各自的组件链接。这样的事情:

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的静态rock对象,以及一个具有相同组件和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类拆分为两个或三个,以将显示和输入系统与逻辑分开。像模型,视图和控制器之类的东西。

我发现根据组件定义我的游戏逻辑,并且让实体只具有他们需要的功能 - 不再需要空渲染()或无用的碰撞检测检查。

答案 1 :(得分:11)

我不确定我是否完全理解这个问题,但我认为你想要创建一个多态对象的集合。访问多态对象时,必须始终通过指针或引用来引用它。

这是一个例子。首先,您需要设置一个基类来从中派生对象:

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派生更多类。从集合中删除对象时,不要忘记删除对象。

答案 2 :(得分:8)

我接近它的方式是拥有一个对游戏世界本身一无所知的显示层。它唯一的工作就是收到一个有序的对象列表,以便在屏幕上绘制所有符合统一格式的图形对象。例如,如果它是2D游戏,您的显示层将接收图像列表及其缩放因子,不透明度,旋转,翻转和源纹理,以及显示对象可能具有的任何其他属性。该视图还可以负责接收与这些显示对象的高级鼠标交互并在适当的地方分派它们。但重要的是,视图层不能在任何方面知道它正在显示的内容。只是它是某种具有表面区域的方形和一些属性。

然后下一层是一个程序,它的工作就是按顺序生成这些对象的列表。如果列表中的每个对象都具有某种唯一ID,则会很有用,因为它可以在视图层中实现某些优化策略。生成显示对象列表是一项不那么令人生畏的任务,而不是试图找出每种角色如何物理渲染自身。

Z排序很简单。生成代码的显示对象只需要按照您想要的顺序生成列表,您可以使用任何方法来实现目标。

在我们的显示对象列表程序中,每个字符,prop和NPC有两部分:资源数据库助手和字符实例。数据库助手为每个角色提供一个简单的界面,每个角色可以从中提取角色所需的任何图像/统计/动画/排列等。你可能想要提供一个相当统一的接口来获取数据,但是它会随着对象的不同而变化。例如,树或岩石不需要像完全动画NPC那样多的东西。

然后,您需要某种方法为每种类型的对象生成实例。您可以使用您的语言内置的类/实例系统来实现这种二分法,或者根据您的需要,您可能需要稍微超越一点。例如,让每个资源数据库都是资源数据库类的实例,并且每个字符实例都是“字符”类的实例。这样可以避免为系统中的每个小对象编写一大块代码。这样,您只需要为广泛的对象类别编写代码,并且只需要更改像数据库的哪一行那样的小东西来获取图像。

然后,不要忘记有一个代表你的相机的内部物体。然后,您的相机的工作是查询每个角色与相机相关的位置。它基本上围绕每个角色实例并询问其显示对象。 “你看起来像什么,你在哪里?”

每个角色实例依次拥有自己的小资源数据库助手来查询。因此,每个角色实例都可以使用它来告诉摄像机需要知道的所有信息。

这给你留下了一组角色实例,这个实例或多或少地忽略了它们如何在物理屏幕上显示的细节,并且或多或少地忘记了如何获取图像的细节来自硬盘的数据。这很好 - 它为您提供了一个尽可能干净的平台,以获得一种柏拉图式的“纯粹”角色世界,您可以在其中实现游戏逻辑,而不必担心从屏幕边缘掉下来等事情。想想如果要将脚本语言放入游戏引擎中,您希望使用哪种界面。尽可能简单吧?尽可能在模拟世界中扎根,而不必担心技术实施细节不多吗?这就是这个策略让你做的事情。

此外,关注点的分离让您可以使用您喜欢的任何技术更换显示层:Open GL,DirectX,软件渲染,Adobe Flash,Nintendo DS,无论如何 - 无需与其他层进行太多讨论。

此外,您实际上可以换出数据库层来执行诸如重新设置所有角色之类的事情 - 或者取决于您如何构建它,在一个全新的游戏中交换新内容,重用大部分角色互动/碰撞你在中间层写的检测/路径查找器代码。

答案 3 :(得分:2)

您应该创建具有通用render()方法的所有对象的超类。将此方法声明为virtual,并让每个子类以自己的方式实现它。

答案 4 :(得分:1)

  

有更好的方法吗?如何像HL2这样的商业游戏处理这个?我想必须有一些模块跟踪所有对象。

商业3D游戏使用Scene Graph的变体。像Adam描述的对象层次结构放置在通常为树结构的对象中。要渲染或操纵对象,只需走树即可。

有几本书讨论过这个问题,我发现最好的是3D游戏引擎设计和架构,都是David Eberly。