将所有游戏对象存储在一个列表中是否可以接受?


24

对于我制作的每个游戏,我最终都将所有游戏对象(子弹,汽车,玩家)放在一个数组列表中,然后循环进行绘制和更新。每个实体的更新代码存储在其类中。

我一直在想,这是正确的处理方式,还是更快,更有效的方式?


4
我注意到您说的是“数组列表”,假设这是.Net,则应该升级到List <T>。几乎一样,只是List <T>是强类型的,因此,您不必在每次访问元素时都进行强制类型转换。这是文档:msdn.microsoft.com/en-us/library/6sh2ey19.aspx
John McDonald

Answers:


26

没有正确的方法。您正在描述的内容是可行的,而且速度足够快,这没关系,因为您似乎在问,这是因为您担心设计,而不是因为它引起性能问题。所以这是一个正确的方式,确保万无一失。

当然,您可以采取不同的方式,例如,为每种对象类型保留一个同质列表,或者确保将异类列表中的所有内容组合在一起,从而提高缓存中的代码的一致性,或者允许并行更新。如果不知道游戏的范围和规模,很难说出这样做是否会带来明显的性能提升。

您还可以进一步排除实体的责任-听起来像实体现在同时在更新和呈现自己-更好地遵循“单一责任原则”。通过将各个接口彼此分离,可以提高各个接口的可维护性和灵活性。但是同样,对于我是否很难知道我对你的游戏有什么了解(这基本上是一无是处),使我是否很难从额外的工作中受益?

我建议不要过分强调它,并且要提醒您,如果您发现性能或可维护性问题,则可能是潜在的改进领域。


16

正如其他人所说,如果速度足够快,那就足够快了。如果您想知道是否有更好的方法,问自己的问题是

我是否发现自己遍历所有对象但仅对它们的一部分进行操作?

如果这样做,则表明您可能希望拥有仅包含相关引用的多个列表。例如,如果您遍历每个游戏对象以渲染它们,但是只需要渲染一部分对象,则可能只需要为那些需要渲染的对象保留一个单独的“可渲染”列表即可。

这样可以减少循环遍历的对象数量,并跳过不必要的检查,因为您已经知道列表中的所有对象都将被处理。

您必须对其进行概要分析,以查看是否获得了很多性能提升。


我同意这一点,对于需要在每一帧进行更新的对象,我通常都有列表的子集,并且我一直在考虑对渲染的对象进行相同的处理。但是,当这些属性可以随时更改时,管理所有列表可能会很复杂。当and对象从可渲染变为不可渲染,或从可更新变为不可更新时,现在您一次要在多个列表中添加或删除它们。
Nic Foster 2012年

8

这几乎完全取决于您的特定游戏需求。它可以完美地用于简单的游戏,但不能在更复杂的系统上运行。如果它适用于您的游戏,请不要担心它,也不要尝试过度设计它,直到出现需求为止。

至于为什么简单方法在某些情况下可能会失败的原因,很难一概而论,因为可能性是无限的,并且完全取决于游戏本身。例如,还提到了其他答案,您可能希望将承担责任的对象归为一个单独的列表。

根据您的游戏设计,这可能是最常见的更改之一。我将趁此机会描述其他一些(更复杂的)示例,但请记住,还有许多其他可能的原因和解决方案。

首先,我将指出,在某些游戏中更新游戏对象并渲染它们可能有不同的要求,因此需要分开处理。更新和渲染游戏对象可能会出现一些问题:

更新游戏对象

这是游戏引擎架构摘录,我建议阅读:

在存在对象间相关性的情况下,必须对上述分阶段更新技术进行一些调整。这是因为对象间的依赖关系可能导致控制更新顺序的规则冲突。

换句话说,在某些游戏中,对象可能相互依赖并需要特定的更新顺序。在这种情况下,您可能需要设计一种比列表更复杂的结构来存储对象及其相互依赖关系。

在此处输入图片说明

渲染游戏对象

在某些游戏中,场景和对象会创建父子节点的层次结构,并应以正确的顺序进行渲染,并相对于其父级进行转换。在这些情况下,您可能需要一个更复杂的结构,例如场景图(树形结构),而不是一个列表:

在此处输入图片说明

例如,其他原因可能是,使用空间分区数据结构以提高执行视图视锥剔除的效率的方式组织对象。


4

答案是“是的,这很好。” 当我进行大型铁仿真时,性能是不可商议的,我惊讶地看到所有东西都聚集在一个大胖子(BIG和FAT)全局数组中。后来,我开发了一个面向对象的系统,并比较了两者。尽管这很丑陋,但是在调试编译的单线程普通旧C语言中的“一个数组可以全部处理所有”版本比C ++中编译的“漂亮”多线程OO系统-O2快几倍。我认为这与大胖数组版本中出色的缓存局部性(在空间和时间上)有关。

如果您想要性能,那么一个庞大的全局C数组将成为上限(根据经验)。


2

基本上,您要做的是根据需要组成列表。如果您一次性绘制地形对象,那么仅列出它们就很方便了。但是,要使这物有所值,您就需要成千上万的这些,以及许多不是地形的东西。否则,仔细阅读所有内容并使用“ if”拉出地形对象就足够快了。

无论我有多少其他列表,我总是需要一个主列表。通常,我将其制作为Map,键控表或字典,以便可以随机访问各个项目。但是一个简单的列表要简单得多。如果您不随机访问游戏对象,则不需要地图。偶尔地通过几千个条目进行顺序搜索以找到合适的条目不会花费很长时间。所以我想说,无论您做什么,都打算坚持使用主列表。如果地图变大,请考虑使用它。(尽管如果可以使用索引而不是键,则可以坚持使用数组,并拥有比Map更好的东西。我总是以索引号之间的巨大空白而告终,所以我不得不放弃它。)

关于数组的一句话:它们的速度令人难以置信。但是,当它们上升到大约100,000个元素时,它们开始适合垃圾收集器。如果您可以分配空间一次,然后再不碰它,那很好。但是,如果您不断扩展数组,则会不断分配和释放大块内存,并且易于使游戏一次暂停几秒钟,甚至会因内存错误而失败。

如此更快,更高效?当然。随机访问的地图。对于真正的大名单,链表会打一个数组,如果当你并不需要随机访问。多个地图/列表,可根据需要以各种方式组织对象。您可以对更新进行多线程处理。

但这是很多工作。单个阵列既快速又简单。您可以仅在需要时添加其他列表和事物,并且可能不需要它们。只是要知道如果您的游戏变大会出什么问题。您可能需要一个测试版本,其中对象的数量是真实版本中的10倍,以使您了解何时会遇到麻烦。(只有做到这一点,如果你的游戏越来越大了。)(不要尝试了多线程没有给它花了很多心思的。其他的一切很容易上手,你可以做多可少你喜欢并随时退出。使用多线程,您将付出巨大的代价,停留的代价很高,而且很难退出。)

换句话说,只要继续做自己正在做的事情即可。您最大的限制可能是您自己的时间,并且您正在充分利用这一时间。


我真的不认为链表在任何大小上都会胜过数组,除非您经常在中间添加或删除内容。
Zan Lynx

@ZanLynx:即使那样。我认为新的运行时优化对数组有很大帮助-并不是他们需要它。但是,如果像大型数组那样反复快速地分配和释放大块内存,则可以将垃圾收集器打成一团。(这里的内存总量也是一个因素。问题在于块的大小,释放和分配的频率以及可用内存的函数。)失败突然发生,程序挂起或崩溃。通常有很多可用内存。GC不能使用它。链接列表可避免此问题。
RalphChapin 2012年

另一方面,由于节点在内存中不连续,因此链表在迭代时缓存未命中的可能性要高得多。它并不是那么干脆的。您可以通过不这样做来减轻重新分配的问题(如果可能的话,尽可能提前分配,因为通常这样),并且可以通过池分配节点来缓解链表中的缓存一致性问题(尽管您仍然有参考的后续费用) ,这是相对琐碎的)。
2012年

是的,但是即使在数组中,您是否也只是存储指向对象的指针?(除非它是粒子系统之类的短暂数组。)如果是这样,那么仍然会有大量的高速缓存未命中。
托德·雷曼

1

对于简单游戏,我使用了某种类似的方法。当满足以下条件时,将所有内容存储在一个阵列中非常有用:

  1. 您的游戏对象数量很少或已知最多
  2. 与性能相比,您更喜欢简单(即:诸如树之类的更优化的数据结构不值得麻烦)

无论如何,我都将此解决方案实现为包含gameObject_ptr结构的固定大小的数组。该结构包含一个指向游戏对象本身的指针,以及一个表示对象是否为“活动”的布尔值。

布尔值的目的是加快创建和删除操作。我不会将游戏对象从仿真中删除时销毁它们,而只是将“ alive”标志设置为false

对对象执行计算时,代码将遍历数组,对标记为“活动”的对象执行计算,并且仅呈现标记为“活动”的对象。

另一个简单的优化方法是按对象类型组织数组。例如,所有项目符号都可以驻留在阵列的最后n个单元中。然后,通过存储带有索引值或指向数组元素的指针的int,可以以降低的成本引用特定类型。

毋庸置疑,此解决方案不适用于具有大量数据的游戏,因为该阵列将无法接受。它也不适用于具有内存限制的游戏,这些内存禁止未使用的对象占用内存。底线是,如果您知道某个时间点的游戏对象数量上限,则将它们存储在静态数组中没有任何问题。

这是我的第一篇文章!希望它能回答您的问题!


第一篇文章!好极了!
提利2012年

0

可以接受,尽管不寻常。“更快”和“更高效”等术语是相对的;通常,您会将游戏对象分为不同的类别(因此,一个子弹列表,一个敌人列表等等),以使编程工作更有条理(因此效率更高),但这并不是计算机会更快地遍历所有对象。


2
对于大多数XNA游戏而言,这确实足够正确,但是组织对象实际上可以使它们循环访问更快:相同类型的对象更有可能访问CPU的指令和数据缓存。高速缓存未命中的代价很高,尤其是在控制台上。例如,在Xbox 360上,二级缓存未命中将花费您约600个周期。这留下了很多表现。这是托管代码比本地代码更具优势的地方:在时间上紧密分配的对象也将在内存中关闭,并且通过垃圾回收(压缩)可以改善这种情况。
慢性游戏程序员

0

您说单个数组和对象。

因此,(没有您的代码可供查看)我猜测它们都与“ uberGameObject”相关。

所以也许有两个问题。

1)您可能有一个“神”对象-做很多东西,没有按其作用或作用真正细分。

2)您遍历此列表并强制输入?或拆箱。这有很大的性能损失。

考虑将不同类型(项目符号,坏人等)分解为自己的强类型列表。

List<BadGuyObj> BadGuys = new List<BadGuyObj>();
List<BulletsObj> Bullets = new List<BulletObj>();

 //Game 
OnUpdate(gameticks gt)
{  foreach (BulletObj thisbullet in Bullets) {thisbullet.Update();}  // much quicker.

如果存在某种通用基类或接口,并且具有将在更新循环中调用的所有方法,则无需执行类型转换update()
bummzack 2012年

0

我可以为每种对象类型在一般的单个列表和单独的列表之间提出一些折衷方案。

保持引用多个列表中的一个对象。这些列表由基本行为定义。例如,MarineSoldier对象将被引用的PhysicalObjListGraphicalObjListUserControlObjListHumanObjList。因此,当您处理物理学时,您要进行遍历PhysicalObjList,当过程受毒物损害时- HumanObjList如果士兵死亡,则将其从中移除,然后HumanObjList保留PhysicalObjList。为了使该机制正常工作,您需要在创建/删除对象时组织自动插入/删除对象。

该方法的优点:

  • 对象的类型化组织(无AllObjectsList更新时强制转换)
  • 列表基于逻辑而不是对象类
  • 具有组合功能的对象没有问题(MegaAirHumanUnderwaterObject将插入到相应列表中,而不是为其类型创建新列表)

PS:关于自我更新,当多个对象交互并且它们彼此依赖时,您会遇到问题。为了解决这个问题,我们需要从外部影响对象,而不是在自我更新内部。

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.