游戏对象应该如何相互了解?


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
我认为您正在寻找通用的C ++ RTTI自定义实现。但是,您的问题似乎不仅仅与明智的RTTI机制有关。您所要求的东西几乎是游戏将使用的任何中间件所必需的(动画系统,物理学等)。根据受支持查询的列表,您可以使用数组中的ID和索引来绕过RTTI,或者最终设计出一个完整的协议来支持dynamic_cast和type_info的更便宜的替代品。
teodron

我建议不要将类型系统用于游戏逻辑。例如,代替依赖于的结果dynamic_cast<Human*>,实现类似的东西bool GameObject::IsHuman()false默认情况下返回,但被重写以trueHuman类中返回。
congusbongus

一个额外的功能:您几乎永远不会向彼此可能感兴趣的实体发送大量对象。这是您必须真正考虑的显而易见的优化。
teodron

@congusbongus在我看来,使用vtable和自定义IsA替代仅比动态转换好一点。最好的办法是让用户尽可能拥有排序的数据列表,而不是盲目地遍历整个实体池。
teodron

4
@Jefffrey:理想情况下,您不必编写特定于类型的代码。您编写特定于接口的代码(一般而言为“接口”)。您对TeamASoldier和的逻辑TeamBSoldier是完全相同的-向其他团队的任何人射击。它所需要的其他实体都是GetTeam()最具体的方法,以congusbongus的示例为例,该方法甚至可以进一步抽象为IsEnemyOf(this)某种接口。该代码不需要关心士兵,僵尸,玩家等的分类分类。关注交互而不是类型。
肖恩·米德迪奇

Answers:


11

您可以选择使用控制器模式,而不是自己执行每个实体的决策。您将拥有中央控制器类,该类可了解所有对象(与它们有关)并控制它们的行为。

MovementController将处理所有可以移动的对象的移动(进行路径查找,根据当前移动矢量更新位置)。

MineBehaviorController会检查所有地雷和所有士兵,并命令士兵离得太近时炸毁地雷。

ZombieBehaviorController会检查所有僵尸和附近的士兵,为每个僵尸选择最佳目标,并命令其移动并攻击它(移动本身由MovementController处理)。

一个SoldierBehaviorController会分析整个情况,然后为所有士兵提供战术指示(您移到那里,射击,然后治愈那个人……)。这些较高级别命令的实际执行也将由较低级别的控制器处理。当您付出一些努力时,就可以使AI能够做出非常明智的合作决策。


1
可能这也称为“系统”,它管理实体组件体系结构中某些类型的组件的逻辑。
teodron

听起来像是C风格的解决方案。组件以std::maps 分组,而实体仅是ID,然后我们必须建立某种类型的类型系统(也许使用标签组件,因为渲染器将需要知道要绘制什么);如果我们不想这样做,我们将需要一个绘图组件:但是它需要位置组件知道要绘制的位置,因此我们在组件之间创建依赖关系,这些组件可以通过超复杂的消息传递系统来解决。这是您的建议吗?
2013年

1
@Jefffrey“这听起来像是C风格的解决方案”-即使那是正确的,但为什么它必然是一件坏事呢?其他问题可能是有效的,但是有解决方案。不幸的是,评论太短而无法正确地解决每个问题。
菲利普

1
@Jefffrey使用组件本身没有任何逻辑并且“系统”负责处理所有逻辑的方法不会在组件之间创建依赖关系,也不需要超复杂的消息传递系统(至少,不那么复杂) 。参见例如:gamadu.com/artemis/tutorial.html

1

首先,尝试实现功能,以使对象尽可能彼此独立。尤其是您要针对多线程执行此操作。在您的第一个代码示例中,可以将所有对象的集合分成与CPU内核数量匹配的集合,并非常有效地进行更新。

但是正如您所说,某些功能需要与其他对象进行交互。这意味着必须在某些点同步所有对象的状态。换句话说,您的应用程序必须先等待所有并行任务完成,然后再应用涉及交互的计算。最好减少这些同步点的数量,因为它们总是暗示某些线程必须等待其他线程完成。

因此,我建议从其他对象内部缓冲那些有关所需对象的信息。有了这样的全局缓冲区,您可以彼此独立地更新所有对象,而仅依赖于它们自己和全局缓冲区,这既更快又易于维护。以固定的时间步长,例如在每一帧之后,以当前对象的状态更新缓冲区。

因此,每帧执行一次操作:1.全局缓冲当前对象的状态; 2.根据自身和缓冲区更新所有对象; 3.绘制对象,然后从更新缓冲区开始。


1

使用基于组件的系统,在该系统中,准系统GameObject包含1个或多个定义其行为的组件。

例如,假设某个对象应该一直在左右移动(平台),则可以创建这样的组件并将其附加到GameObject。

现在说一个游戏对象应该一直在缓慢旋转,您可以创建一个单独的组件来做到这一点,并将其附加到GameObject上。

如果您希望拥有一个可以旋转的移动平台,而在传统的类层次结构中,如果不复制代码就很难做到这一点。

该系统的优点在于,您无需将Rotatable或MovingPlatform类都附加到GameObject上,而现在有了一个可自动旋转的MovingPlatform。

所有组件都有一个属性“ requiresUpdate”,当该属性为true时,GameObject将在该组件上调用“ update”方法。例如,假设您有一个Draggable组件,则将鼠标悬停在该组件上(如果它位于GameObject上)可以将'requiresUpdate'设置为true,然后在鼠标悬停时将其设置为false。仅在鼠标按下时才允许它跟随鼠标。

Tony Hawk Pro Skater的一名开发人员已在上面写下了事实证明,非常值得一读:http : //cowboyprogramming.com/2007/01/05/evolve-your-heirachy/


1

优先考虑组成而不是继承。

除此以外,我最有力的建议是:不要陷入“我希望这具有最大的灵活性”的思维定势。灵活性很棒,但是请记住,在某种程度上,在任何有限的系统(例如游戏)中,都有原子部分用于构造整体。一种或另一种方式,您的处理依赖于那些预定义的原子类型。换句话说,如果没有代码来处理“任何”类型的数据(如果可能的话),从长远来看将无济于事。从根本上讲,所有代码都必须基于已知的规范来解析/处理数据……这意味着预定义的类型集。那套多大?由你决定。

本文通过健壮和高性能的实体组件体系结构,洞察了游戏开发中继承而非继承的原理。

通过从一些预定义组件的超集的(不同的)子集中构建实体,您可以通过阅读那些参与者的组件状态,为AI提供理解世界和周围参与者的具体,零碎的方式。


1

我个人建议将draw函数保持在Object类本身之外。我什至建议将对象的位置/坐标保持在对象本身之外。

该draw()方法将处理OpenGL,Op​​enGL ES,Direct3D,这些API上的包装层或引擎API的低级渲染API。可能是您必须在那时之间进行交换(例如,如果要支持OpenGL + OpenGL ES + Direct3D。

该GameObject应该只包含有关其视觉外观的基本信息,例如Mesh或更大的捆绑包,包括着色器输入,动画状态等。

另外,您将需要灵活的图形管道。如果要根据对象到摄像机的距离来订购对象,该怎么办。或它们的材料类型。如果您想用不同的颜色绘制“选定的”对象,会发生什么。如果不是像在对象上调用draw函数那样实际渲染那么慢,而是将其放入渲染的操作命令列表中(线程可能需要),该怎么办呢?您可以使用其他系统来做这种事情,但这是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)查询(可能)摄像机可见的对象。

5A)您需要处理视觉信息。纹理已上传到GPU等。最好提前完成(例如在级别加载时),但最好在运行时完成(对于开放世界,您可以在接近块时加载内容,但仍应提前完成)。

5B)(可选)构建一个缓存的渲染树,如果您想对材质进行深度/材质排序或跟踪附近的对象,则以后可能会看到它们。否则,您每次可以根据游戏/性能要求查询空间索引。

您的渲染器可能需要一个RenderBinding对象,该对象将在Object,

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)处理。在那种情况下,只需将PhysicsScene和Position替换为Octree,并使用CollisionMesh.getPosition()之类的链接。


哇,看起来很好。我想我已经掌握了基本思想,但是如果没有更多示例,我将无法完全理解这一点。您是否还有其他参考资料或现场示例?(在此期间,我将继续阅读此答案一段时间)。
2013年

确实没有任何示例,这只是我打算在有时间时要做的事情。我将添加整体类的几个,看看有没有什么帮助,有这个这个。它更多地是关于对象类,而不是它们如何关联或呈现。由于我自己尚未实现,可能会有陷阱,需要解决的问题或性能方面的问题,但我认为总体结构还可以。
戴维·C·毕晓普
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.