面向对象的方式进行游戏设计

21

我正在设计一个简单的游戏,使用Java 2D和牛顿物理学。目前我的主要“游戏循环”看起来像这样:

do {
  for (GameEntity entity : entities) {
    entity.update(gameContext);
  }

  for (Drawable drawable : drawables) {
    drawable.draw(graphics2d);
  }
} while (gameRunning);
当一个实体被指示更新自身时,它会根据施加于其上的当前力量来调整其速度和位置。但是,我需要实体展示其他行为,例如玩家射杀“坏人”后,该实体应该被销毁并从游戏世界中移除。
我的问题是:以面向对象的方式,如何最好地实现这一点?到目前为止,我看到的所有示例都将游戏循环并入了一个名为“Game”的God类中,执行步骤:检测碰撞、检查是否击败坏人、检查是否击败玩家、重绘等,并封装了所有游戏状态(剩余生命等)。换句话说,它非常过程化,所有逻辑都在Game类中。有没有人能推荐更好的方法?
以下是我目前想到的选项:
1. 为每个实体传递GameContext,从其中可以删除实体(如果需要)或更新游戏状态(例如,如果玩家被杀,则"不运行")。 2. 将每个GameEntity注册为中央Game类的监听器,并采用事件导向的方法;例如,碰撞会导致CollisionEvent被触发到碰撞的两个参与者。

2
我想我找到了你的错误 - 它应该是 while (gameRunning) - oxbow_lakes
6个回答

14

我曾密切与两个商业游戏引擎合作,它们遵循相似的模式:

  • 对象表示游戏实体的组件或方面(例如物理、可渲染等),而不是整个实体。对于每种类型的组件,都有一个巨大的组件列表,其中包含具有该组件的每个实体示例。

  • “游戏实体”类型本身只是一个唯一的ID。每个巨大的组件列表都有一个映射,用于查找与实体ID对应的组件(如果存在)。

  • 如果一个组件需要更新,则由服务或系统对象调用。每个服务直接从游戏循环中进行更新。或者,您可以从依赖图确定更新顺序的调度程序对象中调用服务。

这种方法的优点如下:

  • 您可以自由地组合功能,而无需为每个组合编写新类或使用复杂的继承树。

  • 没有几乎所有游戏实体都可以假定的功能可以放在游戏实体基类中(例如灯光与赛车或天空盒有什么共同之处?)

  • ID到组件的查找可能看起来很昂贵,但服务通过迭代特定类型的所有组件执行大部分密集型工作。在这些情况下,将您需要的所有数据存储在单个整洁的列表中效果更好。


@Evan:谢谢你的回答。我的意思是问你:在这种情况下,你会如何处理实体的删除?例如,假设你有一个可碰撞的方面 Collidable,并且你的 CollisionManager 检测到了碰撞,这应该导致实体被删除。假定 CollisionManager 只引用实体的 Collidable 方面,那么你会采取什么方法从各种列表中删除实体的所有方面(Drawable、Collidable 等)? - Adamski
1
我没有提到的缺失部分是消息或事件。每个系统都可以订阅任何消息类型或发布消息。物理系统可能会发布一个“碰撞”消息,游戏玩法系统可能会订阅该消息,并可能响应地发布一个“删除实体”消息。负责创建和删除实体的另一个系统可能会订阅删除实体消息类型。这比直接函数调用要复杂得多,但这一切都是为了解耦。 - Evan Rogers

6
在我工作过的一个特定引擎中,我们将逻辑与图形表示解耦,并创建了对象来发送它们想要执行的操作的消息。我们这样做是为了让游戏在本地或网络上存在,并且从代码角度而言它们无法区分。(命令模式)
我们还有一个实际物理建模的单独对象,可以随时更改。这使我们可以轻松地操作重力等参数。
我们大量使用事件驱动代码(监听器模式)和计时器。
例如,我们有一个可相交类的基类对象,可以监听碰撞事件。我们派生出一个“恢复生命”盒子的子类。当被玩家实体击中时,它会向碰撞物发送一个命令,让其恢复生命值,并向所有听到声音的人广播一条消息,停用碰撞,启用动画以从场景图中移除图形,并设置一个计时器以后再实例化自己。听起来很复杂,但实际上并不是。
如果我没记错(已经过去12年了),我们有场景的抽象概念,因此游戏就是一系列场景。当场景完成时,将触发一个事件,通常会发送一个命令以关闭当前场景并启动另一个场景。

在这里使用中介者模式来促进事件调用/注册/处理和数据可能是一个好主意,否则你将不得不将所有子系统和游戏实体耦合在一起。 - dvide

3

我不同意因为您有一个主要的Game类,所有的逻辑都必须在那个类中发生。

这里对您的示例进行了简化,只是为了表明我的观点:

mainloop:
  moveEntities()
  resolveCollisions()   [objects may "disappear"/explode here]
  drawEntities()        [drawing before or after cleanEntitites() ain't an issue, a dead entity won't draw itself]
  cleanDeadEntities()

现在您有一个气泡类:
Bubble implements Drawable {

handle( Needle needle ) {
    if ( needle collide with us ) {
        exploded = true;
    }
}

draw (...) {
   if (!exploded) {
      draw();
     }
  }
}

所以,当然,有一个主循环来处理实体之间的消息传递,但与Bubble和Needle之间的碰撞相关的逻辑绝对不在主Game类中。
我相信即使在你的情况下,所有与移动相关的逻辑也不会发生在主类中。
所以我不同意你粗体写下的声明,“所有逻辑都发生在主类中。”
这是不正确的。
至于良好的设计:如果您可以轻松地提供游戏的另一个“视图”(例如,迷你地图),并且如果您可以轻松编写“逐帧完美的重放器”,那么您的设计可能并不那么糟糕(也就是说:仅记录输入和它们发生的时间,您应该能够准确地重新创建游戏。这就是《帝国时代》、《魔兽争霸3》等游戏制作重播的方式:仅记录用户输入和它们发生的时间[这也是重播文件通常如此小的原因])。

谢谢。我并不主张完全消除游戏循环,只是尽可能保持简单,所以你说的话很有道理。 - Adamski

2
我编写自己的引擎(原始且不太规范),但是一个具有良好面向对象模型的预构建引擎是Ogre。我建议看一下它(它的对象模型/API)。节点分配有点奇怪,但仔细观察后就会变得非常清晰。它还有大量可工作游戏示例的文档。

我也从中学到了一些技巧。


很棒的东西 - 我会去看看。 - Adamski

2

1

这个游戏是一个将模型和视图分离的实验。它使用观察者模式来通知视图对游戏状态的更改,但事件可能会提供更丰富的上下文。最初,模型由键盘输入驱动,但分离使得添加基于计时器的动画变得容易。

补充说明:您需要将游戏的模型保持分离,但可以将该模型重构为所需的任意数量的类。


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