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

Posted

技术标签:

【中文标题】在游戏中组织实体的最佳方式?【英文标题】:Best way to organize entities in a game? 【发布时间】:2009-04-19 04:23:41 【问题描述】:

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

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

我想我之前已经尝试过,但我不知道用什么类型来初始化数组,所以我选择了一个任意对象类型,然后强制转换任何不属于该类型的对象。如果我记得的话,这是行不通的,因为如果编译器不再知道它们的类型,编译器不希望我取消对指针的引用,即使给定的成员函数具有相同的名称: (*Object5).render()

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

【问题讨论】:

您还可以检查 3d 引擎的结构,例如这个:ogre3d.org/docs/manual/manual_4.html#SEC4 【参考方案1】:

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

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

在我的系统中,实体只是 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();
        
    
;

这里有 2 个组件,WorldPosition 和 Render。 Game 类包含 2 个系统。 Render 组件可以访问对象的位置。如果实体没有 WorldPosition 组件,您可以选择默认值,或忽略实体。 Game::gameLoop() 方法只会渲染可见对象。不会浪费处理不可渲染的组件。

您还可以将我的 Game 类拆分为两个或三个,以将显示和输入系统与逻辑分开。模型、视图和控制器之类的东西。

我发现根据组件来定义我的游戏逻辑并让实体只具有他们需要的功能是很巧妙的——不再需要空的 render() 或无用的碰撞检测检查。

【讨论】:

谢谢,这很优雅。不过有人认为,当我尝试编译它时,最后一行代码必须是:(*it).second-&gt;render();. 好,优雅的工作!我很好奇:您是否使用 EventManager 来管理通信? 以及如何安全管理组件之间的依赖关系,比如RenderComponent? 对于事件,我将委托放在相关组件中。我使用 FastDelegate (codeproject.com/KB/cpp/FastDelegate.aspx)。至于依赖关系,我不确定你在说什么。这里 RenderComponent 依赖 WorldPositionComponent,System 传入 RenderComponent 的构造函数。依赖关系由编译器强制执行。我的系统的定义和构建不是数据驱动的,这不是我需要的功能。 避免消息总线的有趣方法,我也对您设法在运行时创建组件同时满足组件之间的依赖关系(例如创建顺序)感兴趣:感谢您对此已过时的回复顺便回答一下。【参考方案2】:

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

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

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

【讨论】:

太棒了。我没有想过使用继承。 也别忘了聚合!【参考方案3】:

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

然后下一层是一个程序,它的工作只是按顺序生成这些对象的列表。如果列表中的每个对象都有某种唯一的 ID,这将很有帮助,因为它使视图层中的某些优化策略成为可能。生成显示对象列表是一项比尝试为每种角色弄清楚其将如何物理呈现自身的任务要轻松得多的任务。

Z 排序很简单。您的显示对象生成代码只需要按照您想要的顺序生成列表,您可以使用任何您需要的方式到达那里。

在我们的显示对象列表程序中,每个角色、道具和NPC都有两部分:资源数据库助手和角色实例。数据库助手为每个角色提供一个简单的界面,每个角色都可以从中提取角色需要的任何图像/统计/动画/排列等。您可能希望提出一个相当统一的接口来获取数据,但它会因对象而异。例如,一棵树或一块石头不需要像一个完全动画的 NPC 那么多的东西。

然后您需要某种方法为每种类型的对象生成一个实例。您可以使用您的语言内置的类/实例系统来实现这种二分法,或者根据您的需要,您可能需要做一些超出此范围的工作。例如,让每个资源数据库都是资源数据库类的一个实例,每个字符实例都是“字符”类的一个实例。这使您不必为系统中的每个小对象编写大量代码。这样,您只需要为广泛的对象类别编写代码,并且只需要更改一些很小的事情,例如从数据库的哪一行获取图像。

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

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

这会让您在一个世界中获得一组角色实例,这些实例或多或少地忽略了如何在物理屏幕上显示它们的细节,或多或少地忽略了如何获取图像的细节来自硬盘的数据。这很好 - 它为您提供了一种柏拉图式的“纯粹”角色世界,您可以在其中实现您的游戏逻辑,而不必担心从屏幕边缘掉下来之类的事情。想一想如果您要将脚本语言放入您的游戏引擎中,您想要什么样的界面。尽可能简单吧?尽可能以模拟世界为基础,而不用担心很少的技术实现细节,对吧?这就是这个策略可以让你做的事情。

此外,关注点分离让您可以使用任何您喜欢的技术替换显示层:Open GL、DirectX、软件渲染、Adobe Flash、Nintendo DS 等等——而不必对其他层大惊小怪。

此外,您实际上可以换出数据库层来执行诸如重新设置所有角色的皮肤之类的操作 - 或者根据您构建它的方式,换入一个全新的游戏,其中包含重用大部分角色交互/碰撞的新内容你在中间层编写的检测/路径查找器代码。

【讨论】:

这是一个很好的答案。我的游戏是 2D 的并且相当简单(我在任何时候都只会存在 30-40 个对象。我经常想象我将如何实现一个实际上需要一个“数据库”来保存有关对象的大量信息的引擎,并且您提出了一个很好的解决方案。谢谢!【参考方案4】:

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

【讨论】:

【参考方案5】:

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

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

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

【讨论】:

以上是关于在游戏中组织实体的最佳方式?的主要内容,如果未能解决你的问题,请参考以下文章

使用 Magicalrecord 拥有单个实体的最佳方式

java spring web应用中实体本地化的最佳实践

通过 Web 传输实体框架对象并通过 JSON 返回的最佳方式

进程的定义组成组织方式特征

操作系统——2.1-1进程的定义,组成,组织方式,特征

DTO、DAO 和实体?是实体需要吗?那三个的最佳实践?