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

15
假设我正在用C++创建一个基于OpenGL的游戏,需要创建许多对象(敌人、玩家角色、物品等),这些对象将根据时间、玩家位置/操作等实时创建和销毁。对此,我想知道最好的组织方式是什么。
目前我考虑到的有以下几种方式: 1. 使用全局数组来存储指向这些对象的指针,在它们的构造函数中加载纹理/上下文。这些对象将具有不同的类型,因此我可以强制转换指针以便将它们放入数组中,但是我希望稍后能够使用循环调用每个现有对象的ObjectN.render()函数的renderObjects()函数。 2. 我记得之前尝试过这种方法,但是我不知道应该用什么类型来初始化数组,所以我随意选择了一种对象类型,然后强制转换任何不属于该类型的对象。如果我没记错,这并没有起作用,因为即使成员函数名称相同,编译器也不允许我在不知道它们类型的情况下取消引用指针:(*Object5).render() <-行不通?
是否有更好的方法?商业游戏如HL2是如何处理这个问题的?我想象中必须有一些模块等来跟踪所有的对象。

你可以查看一个3D引擎是如何构建的,例如这个网址:http://www.ogre3d.org/docs/manual/manual_4.html#SEC4 - gblazex
5个回答

36

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

你可以通过搜索“基于组件的游戏开发”来了解更多信息。一篇著名的文章是Cowboy编程博客上的《演化你的层次结构》

在我的系统中,实体只是无符号长整型的ID,有点像关系数据库中的ID。所有与实体相关的数据和逻辑都写入组件中。我有一些系统将实体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,以及一个移动的敌人,它除了这些组件之外还有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();
        }
    }
};

这里有两个组件,WorldPosition和Render。Game类持有这两个系统。Render组件可以访问对象的位置。如果实体没有WorldPosition组件,则可以选择默认值或忽略该实体。 Game::gameLoop()方法仅呈现visibleObject。不会浪费处理非渲染组件的资源。

您还可以将我的Game类分为两个或三个,以将显示和输入系统与逻辑分离。类似于Model,View和Controller。

我认为使用组件来定义游戏逻辑,并且只为需要的功能创建实体非常整洁 - 不再存在空的render()或无用的碰撞检测检查。


谢谢,这很优雅。不过有一点,当我尝试编译它时,代码的最后一行需要是:(*it).second->render(); - sharvey
好的,优雅的工作!我很好奇:你是否使用EventManager来管理通信? - Manuel
如何安全地管理组件之间的依赖关系,比如RenderComponent? - Manuel
对于事件,我将委托放在相关组件中。我使用FastDelegate(http://www.codeproject.com/KB/cpp/FastDelegate.aspx)。 至于依赖项,我不确定你在说什么。这里RenderComponent依赖于WorldPositionComponent,该系统被传递到RenderComponent的构造函数中。依赖关系由编译器强制执行。我的系统的定义和构建不是数据驱动的,这不是我需要的功能。 - Splo
有趣的方法可以避免使用消息总线,我也对您如何在运行时创建组件并满足组件之间的依赖关系(例如创建顺序等)感到感兴趣。顺便说一下,感谢您对这个现在已过时的答案做出回应。 - Manuel

11

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

这里是一个例子。首先,你需要设置一个基类来派生你的对象:

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

然后创建指针数组。我使用STL set,因为这使得随机添加和删除成员变得容易:

#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类派生更多的类。在将它们从集合中删除时,不要忘记删除这些对象。


太好了,我没有想到使用继承。 - Tony R
3
别忘了聚合! - Manuel

8
我一直采用的方法是拥有一个显示层,它对游戏世界本身一无所知。它唯一的工作就是接收一个按顺序排列的对象列表,将它们绘制到屏幕上,这些对象都符合图形对象的统一格式。例如,如果是2D游戏,你的显示层将接收一系列图像以及它们的缩放因子、不透明度、旋转、翻转和源纹理等其他属性,视图还可能负责接收与这些显示对象相关的高级鼠标交互,并将其分派到适当的位置。但重要的是,视图层不知道它所显示的内容的语义。只知道它是某种具有表面积和某些属性的方形物体。
然后下一层是一个程序,它的工作就是简单地按顺序生成这些对象的列表。如果列表中的每个对象都有某种唯一的ID,那么在视图层中就可以使用某些优化策略。生成显示对象列表比尝试弄清楚每种字符如何物理渲染要容易得多。
Z排序很简单。您的显示对象生成代码只需要按照您想要的顺序生成列表,您可以使用任何需要的手段来完成此操作。
在我们的显示对象列表程序中,每个角色、道具和NPC都有两个部分:资源数据库助手和角色实例。数据库助手为每个角色提供一个简单的接口,从中可以拉取角色需要的任何图像/统计数据/动画/排列等等。您可能希望为获取数据制定一个相当统一的接口,但它会因对象而异。例如,树木或岩石不需要像完全动态的NPC那样多的东西。
然后您需要一种方法来为每种对象类型生成一个实例。您可以使用语言内置的类/实例系统来实现这种二分法,或者根据您的需求,您可能需要超越这种方法。例如,将每个资源数据库作为资源数据库类的实例,并将每个角色实例作为“角色”类的实例。这样可以避免为系统中的每个小对象编写一大块代码。这样,您只需要为广泛的对象类编写代码,并仅更改诸如从数据库的哪一行中获取图像之类的小细节。
然后,不要忘记拥有一个代表您的摄像机的内部对象。然后,摄像机的工作是查询每个角色与摄像机的相对位置。它基本上是围绕每个角色实例并询问其显示对象。“你长什么样子,你在哪里?”
每个角色实例依次具有自己的小型资源数据库助手,可以查询所有需要告诉摄像机所需信息的信息。
这意味着你拥有一组字符实例,而这个世界对它们在物理屏幕上的显示方式以及从硬盘中提取图像数据的细节几乎一无所知。这是件好事——它让你可以在一个几乎纯粹的字符世界中实现游戏逻辑,而不必担心像掉出屏幕边缘这样的问题。如果你要将脚本语言放入游戏引擎中,想象一下你希望什么样的接口。尽可能简单吧?尽可能接近模拟世界,不必担心小的技术实现细节,对吧?这就是这种策略能够实现的。
此外,这种责任分离允许你使用任何你喜欢的技术来交换显示层:Open GL、DirectX、软件渲染、Adobe Flash、Nintendo DS等等,而不必过多地操心其他层。
另外,你实际上可以替换数据库层,以实现重新设计所有角色的皮肤,或者根据你构建的方式,替换一个完全新的游戏,并重用你在中间层编写的大部分角色交互/碰撞检测/路径查找代码。

1
这是一个很好的答案。我的游戏是2D和相当简单的(我只会有30-40个对象在任何时候存在)。我经常想象如何实现一个需要“数据库”来保存大量对象信息的引擎,你提供了一个很好的解决方案。谢谢! - Tony R

2
你应该创建一个超类,它包含一个通用的render()方法。将这个方法声明为虚拟的,然后让每个子类以自己的方式实现它。

1
有更好的方法吗?类似HL2这样的商业游戏如何处理?我想必须有一些模块等来跟踪所有对象。 商业3D游戏使用变体场景图。像Adam描述的对象层次结构被放置在通常是树形结构的位置。要渲染或操作对象,只需遍历树即可。有几本书讨论了这个问题,我发现最好的是David Eberly的两本书:《3D 游戏引擎设计》和《架构》。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接