为什么要从渲染中分离对象?


11

Disclamer:我知道什么是实体系统模式,但我没有使用它。

我已经阅读了很多有关分离对象和渲染的内容。关于游戏逻辑应独立于底层渲染引擎的事实。这一切都很好,很花哨,而且很合理,但同时也会引起很多其他麻烦:

  • 逻辑对象和渲染对象(保持动画,精灵等的状态)之间需要同步
  • 需要向公众开放逻辑对象,以便渲染对象读取逻辑对象的实际状态(通常使逻辑对象容易在笨拙的getter和setter对象中进行转换)

这听起来对我来说不是一个好的解决方案。另一方面,将对象想象为3d(或2d)表示形式非常直观,并且易于维护(也可能封装得更多)。

有没有一种方法可以将图形表示和游戏逻辑保持在一起(避免同步问题),但又抽象了渲染引擎?还是有一种方法可以将游戏逻辑和渲染分开,而不会导致上述缺点?

(可能带有示例,我不太擅长理解抽象演讲)


1
如果您提供一个示例,说明您未使用实体系统模式时的意思,以及您如何将其与应将呈现的关注与实体/游戏逻辑。
michael.bartnett

@ michael.bartnett,我没有将对象分解为系统可处理的小型可重用组件,而大多数模式实现都是这样做的。我的代码更多是尝试使用MVC模式。但这并不重要,因为问题不取决于任何代码(甚至不取决于语言)。我之所以如此,是因为我知道有些人会试图说服我使用ECS,这似乎可以治愈癌症。而且,正如您所看到的,它还是发生了。

Answers:


13

假设您有一个由世界玩家老板组成的场景哦,这是第三人称游戏,所以您也有摄像头

因此,您的场景如下所示:

class Scene {
    World* world
    Player* player
    Enemy* boss
    Camera* camera
}

(至少,这是基本数据。如何包含数据取决于您自己。)

您只想在玩游戏时而不是在暂停时或在主菜单中更新和渲染场景,因此您可以将其附加到游戏状态!

State* gameState = new State();
gameState->addScene(scene);

现在,您的游戏状态有了一个场景。接下来,您要在场景上运行逻辑并渲染场景。对于逻辑,您只需运行更新功能。

State::update(double delta) {
    scene->update(delta);
}

这样,您可以将所有游戏逻辑都保留在Scene类中。只是为了参考,实体组件系统可能会这样做:

State::update(double delta) {
    physicsSystem->applyPhysics(scene);
}

无论如何,您现在已经设法更新了场景。现在您要显示它!为此,我们执行与上面类似的操作:

State::render() {
    renderSystem->render(scene);
}

你去。renderSystem从场景中读取信息,并显示适当的图像。简化后,渲染场景的方法可能如下所示:

RenderSystem::renderScene(Scene* scene) {
    Camera* camera = scene->camera;
    lookAt(camera); // Set up the appropriate viewing matrices based on 
                    // the camera location and direction

    renderHeightmap(scene->getWorld()->getHeightMap()); // Just as an example, you might
                                                        // use a height map as your world
                                                        // representation.
    renderModel(scene->getPlayer()->getType()); // getType() will return, for example "orc"
                                                // or "human"

    renderModel(scene->getBoss()->getType());
}

确实简化了,例如,您仍然需要根据玩家所在的位置和观看的位置应用旋转和平移。(我的示例是3D游戏,如果使用2D,它将是在公园散步)。

我希望这是您想要的?正如您希望从上述内容中回忆到的那样,渲染系统不在乎游戏的逻辑。它仅使用场景的当前状态进行渲染,即从场景中提取必要的信息以进行渲染。和游戏逻辑?不管渲染器做什么。哎呀,根本不显示它!

而且您也不需要将渲染信息附加到场景。渲染器知道需要渲染兽人就足够了。您已经加载了一个Orc模型,渲染器随后便知道该模型可以显示。

这应该满足您的要求。图形表示和逻辑是耦合的,因为它们都使用相同的数据。但是它们是分开的,因为两者都不依赖对方!

编辑:只是为了回答为什么人们会这样做呢?因为更容易是最简单的原因。您无需考虑“这样的情况,我现在应该更新图形”。取而代之的是让事情发生,游戏的每个帧都会查看当前正在发生的事情,并以某种方式对其进行解释,从而为您提供屏幕上的效果。


7

您的标题提出的问题与身体内容提出的问题不同。在标题中,您询问为什么应该将逻辑和渲染分开,但是在正文中,您要求实现逻辑/图形/渲染系统。

前面已经解决了第二个问题,因此我将集中讨论第一个问题。

分离逻辑和渲染的原因:

  1. 人们普遍认为物体应该做一件事
  2. 如果要从2D转换为3D怎么办?如果您决定在项目中间从一个渲染系统更改为另一个渲染系统,该怎么办?您不想爬过所有代码并在游戏逻辑中间进行重大更改。
  3. 您可能有理由重复代码部分,这通常被认为是一个坏主意。
  4. 您可以构建系统来控制潜在的大量渲染或逻辑,而无需单独与小块进行通信。
  5. 如果您想将宝石分配给玩家,但系统因宝石拥有多少个面而减慢系统速度,该怎么办?如果您已经很好地抽象了渲染系统,则可以以不同的速率对其进行更新,以解决昂贵的渲染操作。
  6. 它使您可以考虑对您所做的事情真正重要的事情。当您要做的只是实现双重跳跃机制,画一张牌或装备一把剑时,为什么要动脑筋围绕矩阵变换,精灵偏移和屏幕坐标?您不希望仅将精灵从右手移到左侧就将精灵用明亮的粉红色表示出来,以显示装备好的剑。

在OOP设置中,实例化新对象是有代价的,但是以我的经验,系统资源的代价是考虑和实现需要完成的特定事情的能力所付出的代价很小。


6

这个答案只是为了建立直觉,说明为什么将渲染和逻辑分开很重要,而不是直接提出实际示例。

假设我们有一头大大象,房间里没人能看见整只大象。也许每个人甚至都对它的实际存在不同意见。因为每个人都看到大象的不同部分,并且只能处理那部分。但是最后这并没有改变它是一头大象的事实。

大象代表游戏对象的所有细节。但是,实际上没有人需要了解有关大象(游戏对象)的所有知识才能执行其功能。

将游戏逻辑和渲染耦合起来实际上就像是让所有人看到整个大象一样。如果有什么变化,每个人都需要知道。尽管在大多数情况下,他们只需要查看他们仅感兴趣的部分。如果某件事改变了了解此事的人,则只需要告诉对方该改变的结果,那对他来说才是重要的(将此视为通过消息或接口进行的通信)。

在此处输入图片说明

您提到的要点不是缺点,它们仅是在依赖项比引擎中应有的依赖多的情况下才是缺点,换句话说,系统看到大象的部分比应有的更多。这意味着发动机的设计不是“正确”的。

仅当使用多线程引擎时才需要使用它的形式定义进行同步,该引擎将逻辑和渲染置于两个不同的线程中,甚至在系统之间需要大量同步的引擎也不是特别设计的。

否则,处理这种情况的自然方法是将系统设计为输入/输出。更新执行逻辑并输出结果。仅将更新结果与提要一起提供。您实际上并不需要公开所有内容。您仅公开了在两个阶段之间进行通信的接口。引擎不同部分之间的通信应通过抽象(接口)和/或消息进行。不应公开内部逻辑或状态。

让我们以一个简单的场景图示例来解释这个想法。

通常通过称为游戏循环的单个循环(或可能通过多个游戏循环,每个循环在单独的线程中)完成更新。一旦循环更新过游戏对象。它仅需要通过消息传递或接口来告知对象1和2的更新位置,并将其提供给最终转换。

渲染系统进行最终变换,而不知道对象实际发生了什么变化(例如发生了特定的碰撞等)。现在,为了渲染该对象,它只需要该对象的ID和最终的转换即可。之后,渲染器将向渲染api提供网格和最终转换,而无需其他任何信息。

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.