如何避免玩家与世界之间的循环依赖?


60

我正在开发2D游戏,您可以在其中上下左右移动。我基本上有两个游戏逻辑对象:

  • 玩家:相对于世界的位置
  • 世界:绘制地图和玩家

到目前为止,World依赖于Player(即具有对它的引用),需要其位置来确定在何处绘制玩家角色以及要绘制地图的哪一部分。

现在,我想添加碰撞检测,以使玩家无法穿过墙壁。

我能想到的最简单的方法是让玩家询问世界是否可以进行预期的移动。但这会在播放器世界之间引入循环依赖关系(即,每个都有对另一个的引用),这似乎值得避免。我想出的唯一方法是让World移动Player,但是我发现这有点不直观。

我最好的选择是什么?还是避免循环依赖不值得?


4
您为什么认为循环依赖是一件坏事?stackoverflow.com/questions/1897537/...
Fuhrmanator

@Fuhrmanator我认为它们通常不是一件坏事,但是我必须在代码中使事情变得更加复杂以引入一个。
futlib 2012年

我发了一个关于我们的小讨论的帖子,虽然没有什么新内容:yannbane.com/2012/11/… ...
jcora 2012年

Answers:


61

世界不应自我吸引;渲染器应该绘制世界。玩家不应抽奖;渲染器应相对于世界绘制玩家。

玩家应向世界询问碰撞检测;或者也许应该由一个单独的类来处理冲突,该类不仅要检查静态世界的冲突检测,还要检查其他参与者的冲突检测。

我认为世界可能根本不应该意识到玩家。它应该是低级的原始对象,而不是上帝的对象。播放器可能需要间接调用某些World方法(冲突检测或检查交互式对象等)。


25
@ snake5-“可以”和“应该”之间有区别。任何东西都可以绘制任何东西-但是,当您需要更改处理绘图的代码时,转到“ Renderer”类比搜索要绘制的“ Anything”要容易得多。“迷恋分隔”是“凝聚力”的另一个词。
Nate 2012年

16
@野兽先生,不,他不是。他提倡好的设计。把所有的东西塞进一堂课中毫无意义。
jcora 2012年

23
哇,我不认为这会引起这样的反应:)我没有任何要补充的内容,但是我可以解释为什么给出它-因为我认为这很简单。不正确或不正确。我不希望它听起来那样。这对我来说比较简单,因为如果我发现自己处理的类职责过多,则拆分比强制使现有代码可读性要快。我喜欢我能理解的代码块,并且可以对@futlib遇到的问题进行重构。
丽山2012年

12
@ snake5说增加更多的类会增加程序员的负担,根据我的经验,这通常是完全错误的。在我看来,与单个1000行神类相比,具有信息性名称和明确定义的职责的10x100行类对于程序员来说更容易阅读,并且开销更少
马丁

7
至于什么画什么,一记Renderer某种是必要的,但这并不意味着逻辑如何被渲染的每一个事情是由处理Renderer,这需要绘制每一件事情或许应该从一个共同的接口,如继承IDrawableIRenderable(或等效的界面,无论您使用哪种语言)。Renderer我想世界可能是世界,但似乎这将超越它的责任,特别是如果它已经是一个IRenderable本身。
zzzzBov 2012年

35

这是典型的渲染引擎处理这些事情的方式:

在空间中的物体与物体的绘制方式之间存在根本的区别。

  1. 绘制对象

    通常,您有一个执行此操作的Renderer类。它只需要一个对象(Model)并在屏幕上绘制。它可以具有诸如drawSprite(Sprite),drawLine(..),drawModel(Model)之类的方法,无论您需要什么。这是一个渲染器,因此应该做所有这些事情。它还使用您下面的任何API,因此您可以拥有一个使用OpenGL的渲染器和一个使用DirectX的渲染器。如果您想将游戏移植到另一个平台,只需编写一个新的渲染器并使用该渲染器即可。很简单。

  2. 移动物件

    每个对象都附加到我们要称为SceneNode的对象上。您可以通过合成实现这一目标。SceneNode包含一个对象。而已。什么是SceneNode?这是一个简单的类,包含对象(通常相对于另一个SceneNode)的所有转换以及实际对象的所有转换(位置,旋转,比例)。

  3. 管理对象

    如何管理SceneNode?通过一个SceneManager。此类创建并跟踪场景中的每个SceneNode。您可以要求它提供特定的SceneNode(通常由诸如“ Player”或“ Table”之类的字符串名称标识)或所有节点的列表。

  4. 画世界

    现在,这应该已经很明显了。只需遍历场景中的每个SceneNode并让Renderer在正确的位置绘制它。通过让渲染器在渲染对象之前存储对象的变换,可以在正确的位置绘制它。

  5. 碰撞检测

    这并不总是琐碎的。通常,您可以查询场景中某个特定点在空间中的什么物体,或者射线将与哪些物体相交。这样,您可以从播放器向移动方向创建光线,并询问场景管理器光线相交的第一个对象是什么。然后,您可以选择将玩家移动到新位置,将其移动一个较小的数量(以使其靠近碰撞对象),也可以完全不移动。确保这些查询由单独的类处理。他们应该向SceneManager索要SceneNode的列表,但这是确定SceneNode是否覆盖空间中的点或与射线相交的另一项任务。请记住,SceneManager仅创建和存储节点。

那么,玩家是什么,世界是什么?

Player可以是包含SceneNode的类,后者又包含要渲染的模型。您可以通过更改场景节点的位置来移动播放器。世界只是SceneManager的一个实例。它包含所有对象(通过SceneNodes)。您可以通过查询场景的当前状态来处理碰撞检测。

这远不能完整或准确地描述大多数引擎内部发生的情况,但是它应该可以帮助您了解基本知识以及尊重SOLID强调的OOP原理的重要性。不要对重组代码太困难或对您没有真正帮助的想法不屑一顾。通过精心设计代码,您将来会赢得更多收益。


+1-我发现自己在构建游戏系统时像这样,发现它非常灵活。
Cypher 2012年

+1,好答案。比我自己的更具体,更切题。
jcora 2012年

+1,我从这个答案中学到了很多东西,甚至还有一个鼓舞人心的结局。感谢@rootlocus
joslinm 2012年

16

为什么要避免这种情况?如果要创建可重用的类,应避免循环依赖。但是Player根本不是需要重用的类。您是否想在没有世界的情况下使用播放器?可能不是。

请记住,类不过是功能的集合。问题是人们如何划分功能。做任何您需要做的。如果您需要循环衰退,那就这样吧。(顺便说一句,OOP的所有功能都一​​样。以某种目的实现代码的目的,而不仅仅是盲目遵循范式。)

编辑
好吧,回答这个问题:您可以通过使用回调来避免玩家需要了解世界进行碰撞检查:

World::checkForCollisions()
{
  [...]
  foreach(entityA in entityList)
    foreach(entityB in entityList)
      if([... entityA and entityB have collided ...])
         entityA.onCollision(entityB);
}

Player::onCollision(other)
{
  [... react on the collision ...]
}

如果您揭示实体的速度,那么您在问题中描述的物理学类型可以被世界处理:

World::calculatePhysics()
{ 
  foreach(entityA in entityList)
    foreach(entityB in entityList)
    {
      [... move entityA according to its velocity as far as possible ...]
      if([... entityA has collided with the world ...])
         entityA.onWorldCollision();
      [... calculate the movement of entityB in order to know if A has collided with B ...]
      if([... entityA and entityB have collided ...])
         entityA.onCollision(entityB);
    }
}

但是请注意,您迟早可能需要对世界的依赖,也就是您需要世界功能时:您想知道最近的敌人在哪里?您想知道下一个壁架有多远吗?是依赖。


4
+1循环依赖在这里并不是真正的问题。在这个阶段,没有理由担心。如果游戏不断发展并且代码成熟,那么无论如何都要重构子类中的Player和World类,拥有适当的基于组件的系统,用于输入处理的类,也许是Rendered,等等。一开始,没问题。
Laurent Couvidou 2012年

4
-1,这绝对不是不引入循环依赖项的唯一原因。通过不引入它们,可以使您的系统更易于扩展和更改。
jcora 2012年

4
@Bane如果没有这种胶水,您将无法编写任何代码。区别在于您添加了多少间接。如果您具有游戏->世界->实体类,或者您具有游戏->世界,SoundManager,InputManager,PhysicsEngine,ComponentManager等类。由于所有(语法上的)开销以及所隐含的复杂性,它使事情的可读性降低。某一时刻,您将需要组件之间的交互。这就是胶合剂类使事情比许多类中的所有事情都容易实现的地方。
API-Beast

3
不,您要移动球门柱。当然一定要打电话render(World)。争论的焦点是是否所有代码都应集中在一个类中,还是应该将代码划分为逻辑和功能单元,以便于维护,扩展和管理。顺便说一句,祝你好运,重用那些组件管理器,物理引擎和输入管理器,它们巧妙地加以区分和完全耦合。
jcora 2012年

1
@Bane除了引入新的类btw以外,还有其他方法可以将事物划分为逻辑块。您也可以添加新功能,也可以将文件分成由注释块分隔的多个部分。仅保持简单并不意味着代码会一团糟。
API-Beast

13

您当前的设计似乎与SOLID设计的第一原则背道而驰

通常,遵循此第一原则是“单一责任原则”,这是一个很好的指导原则,可以避免创建会永远伤害您设计的整体的,无所不能的对象。

为了具体化,您的World对象既负责更新和保持游戏状态,又负责绘制所有内容。

如果渲染代码更改/必须更改怎么办?为什么要更新与渲染实际上没有任何关系的两个类?正如Liosan所说,您应该有一个Renderer


现在,回答您的实际问题...

有很多方法可以做到这一点,而这只是解耦的一种方法:

  1. 世界不知道玩家是什么。
    • 它确实有一个Object播放器所在的s 列表,但并不取决于播放器类(使用继承来实现)。
  2. 播放器已更新InputManager
  3. 世界负责处理运动和碰撞检测,应用适当的物理更改并将更新发送给对象。
    • 例如,如果对象A和对象B发生碰撞,世界将通知他们,然后他们可以自己处理它。
    • 如果您的设计是这样,世界仍然会处理物理学。
    • 然后,两个对象都可以看到碰撞是否对它们感兴趣。例如,如果对象A是玩家,而对象B是尖峰,则玩家可能对其自身造成伤害。
    • 但是,这可以通过其他方式解决。
  4. Renderer绘制的所有对象。

您说世界不知道玩家是什么,但如果它是碰撞对象之一,则它会处理可能需要知道玩家属性的碰撞检测。
Markus von Broady 2012年

继承时,世界必须意识到可以以一般方式描述的某种对象。问题不在于世界只是引用玩家,而是它可能依赖于它作为一个类(即使用health仅此实例Player具有的字段)。
jcora 2012年

嗯,您的意思是世界没有引用播放器,只是有一系列实现ICollidable接口的对象,如果需要,还可以与播放器一起使用。
Markus von Broady 2012年

2
+1好答案。但是:“请忽略所有说好的软件设计并不重要的人”。共同。没有人这么说。
Laurent Couvidou 2012年

2
编辑!无论如何,这似乎确实是不必要的……
jcora 2012年

1

玩家应向世界询问有关碰撞检测的信息。避免循环依赖的方法是使World不依赖Player。世界需要知道自己绘制的位置:您可能希望将其抽象得更远,也许是引用Camera对象,而该对象又可以包含要跟踪的某些Entity的引用。

在循环引用方面,您要避免的是彼此之间不要过多地保持引用,而是在代码中显式地相互引用。


1

每当两种不同类型的对象可以互相询问时。它们将彼此依赖,因为它们需要持有对另一个引用以调用其方法。

您可以通过让世界询问玩家来避免循环依赖,但是玩家不能询问世界,反之亦然。这样,世界可以引用玩家,但是玩家不需要引用世界。或相反亦然。但这并不能解决问题,因为世界需要询问玩家是否有问题要问,并在下一次通话中告诉他们...

因此,您无法真正解决此“问题”,我认为无需担心。尽可能让设计愚蠢的事情简单化。


0

除去有关玩家和世界的详细信息,您有一个简单的情况就是不想在两个对象之间引入循环依赖关系(这取决于您的语言,甚至没有关系,请参见Fuhrmanator的注释中的链接)。至少有两个非常简单的结构解决方案可应用于此问题和类似问题:

1)单例模式引入您的世界一流水平。这将使玩家(和其他所有对象)可以轻松找到世界对象,而无需进行昂贵的搜索或永久保存链接。该模式的要旨只是该类对该类的唯一实例具有静态引用,该静态引用是在对象的实例化时设置的,并在删除对象时清除。

根据您的开发语言和所需的复杂性,您可以轻松地将其实现为超类或接口,并将其重用于项目中可能不希望使用的多个主要类。

2)如果您使用的语言支持(很多),请使用弱引用这是一个不会影响垃圾收集之类的参考。在这些情况下非常有用,只需确保不要对您弱引用的对象是否仍然存在做出任何假设。

在您的特定情况下,您的玩家可能对世界的看法不佳。这样做的好处(与单例一样)是,您无需在每个帧中都以某种方式搜索世界对象,也不需要具有永久性引用,而该引用会妨碍受循环引用(如垃圾收集)影响的进程。


0

正如其他人所说的,我想你World是做一件事情太多了:它试图包含游戏Map(这应该是一个独立的实体),并Renderer在同一时间。

因此,创建一个对象(GameMap可能称为),然后在其中存储地图级别数据。在其中编写与当前地图交互的函数。

然后,您需要一个Renderer对象。你可以让这个Renderer对象,无论是东西含有 GameMapPlayer(以及Enemies),也吸引过来。


-6

您可以通过不将变量添加为成员来避免循环依赖。为播放器或类似的东西使用静态CurrentWorld()函数。但是,不要发明与World中实现的接口不同的接口,这完全没有必要。

也有可能在销毁播放器对象之前/期间销毁引用,以有效阻止由循环引用引起的问题。


1
我和你在一起。OOP太高估了。在学习了基本的控制流程知识之后,教程和教育迅速转向了OO。OO程序通常比过程代码慢,因为对象之间存在官僚主义,您有很多指针访问,这会导致大量的高速缓存未命中。您的游戏正常运行,但速度很慢。真正的,非常快速且功能丰富的游戏,使用普通的全局数组以及经过手工优化的微调功能,可以避免所有内容丢失缓存。这可能导致性能提高十倍。
Calmarius
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.