如何在C#中有效地处理许多更新对象?

我正在使用C#和XNA开发2D头顶射击游戏。 我有一个我称之为“子弹”的类,需要在每一秒钟内更新许多这些实例。

我这样做的第一种方法是拥有一个通用的子弹列表,并根据需要简单地移除和添加新的子弹。 但是在这样做的过程中,GC经常出现,我的游戏有一些周期性的生涩延迟。 (很多代码被删除了,但只想展示一个简单的代码片段)

if (triggerButton) { bullets.Add(new bullet()); } if (bulletDestroyed) { bullets.Remove(bullet); } 

我的第二个也是当前的尝试是有一个单独的通用Stack子弹,当我完成一个子弹时我会推动它,如果堆栈中有任何东西,当我需要一个新子弹时弹出子弹。 如果堆栈中没有任何内容,那么我将新的项目符号添加到列表中。 它似乎减少了生涩的滞后,但又一次,有时还会出现一些生涩的滞后(尽管我不知道它是否相关)。

 if (triggerButton) { if (bulletStack.Count > 0) { bullet temp = bulletStack.Pop(); temp.resetPosition(); bullets.Add(temp); } else { bullets.Add(new bullet()); } } if (bulletDestroyed) { bulletStack.Push(bullet); bullets.Remove(bullet); } 

所以,我知道过早的优化是所有邪恶的根源,但这是非常明显的低效率,我可以提前赶上(这是在甚至不必担心敌人的子弹充满屏幕之前)。 所以我的问题是:将未使用的对象推送到堆栈会调用垃圾收集吗? 参考文件是保持活着还是被破坏的对象? 有没有更好的方法来处理更新许多不同的对象? 例如,我太过花哨了吗? 如果只是遍历列表并找到一个未使用的子弹就可以了吗?

这里有很多问题,这很难说。

首先,是bullet结构还是类? 如果bullet是一个类,那么无论何时构造一个类,然后将其解除根(让它超出范围或将其设置为null),您将要添加GC需要收集的内容。

如果您要制作其中的许多内容,并在每一帧中更新它们,您可能需要考虑使用List其中bullet是结构,并且List是预先分配的(生成它的大小足够大)保存所有子弹,因此在调用List.Add不会重新创建它。 这将极大地帮助GC压力。

还有,因为我需要咆哮:

所以,我知道过早优化是所有邪恶的根源,但这是非常明显的低效率

永远不要害怕优化你知道导致问题的例程。 如果您发现性能问题(即:滞后),则不再是过早优化。 是的,您不希望优化每行代码,但您需要优化代码 ,尤其是当您发现真正的性能问题时。 一旦发现问题就对它进行优化比以后尝试优化它要容易得多,因为在添加许多使用bullet类的其他代码之前,所需的任何设计更改都将更容易实现。

您可能会发现flyweight设计模式很有用。 只需要一个子弹对象,但是多个飞重可以为它指定不同的位置和速度。 flyweights可以存储在预分配的数组(例如100)中,并标记为有效或无效。

这应该完全消除垃圾收集,并可能减少跟踪每个子弹的可塑性属性所需的空间。

我承认我本身没有任何经验,但我会考虑使用传统arrays。 将数组初始化为您需要的大小,并且将是理论上最大的项目符号数,例如100.然后从0开始,在数组的开头指定项目符号,将最后一个元素保留为空。 因此,如果您有四个活动项目符号,您的数组将如下所示:

0 B 1 B 2 B 3 B 4 null … 99 null

好处是数组总是被分配,因此您不会处理更复杂的数据结构的开销。 这实际上与字符串的工作方式非常相似,因为它们实际上是带有null终止符的char []。

可能值得一试。 一个缺点是,在移除子弹时你将不得不做一些手动操作,可能会将该子弹后的所有内容移动到一个插槽中。 但是你只是在那一点上移动指针,所以我认为它不会像分配内存或GC一样受到高度惩罚。

假设通过将未使用的项目符号保留在堆栈中可以防止它们被垃圾收集,您是正确的。

至于滞后的原因,您是否尝试过任何分析工具? 只是为了找到问题所在。

您的基于堆栈的解决方案非常接近我编写的类,通常用于执行此类资源池:
http://codecube.net/2010/01/xna-resource-pool/

你提到这会使问题大部分消失,但它仍然会在这里和那里出现。 发生的事情是,使用这种基于堆栈/队列的池化方法,一旦您不再请求比池可提供的更多新对象,系统将达到稳定点。 但是,如果请求高于先前请求项的最大数量,则将导致您必须创建新实例来为请求提供服务(从而不时地调用GC)。

您可以采取的一种方法是通过并预先分配您认为在峰值时可能需要的实例。 这样,您将不会有任何新的分配(至少来自池化对象),并且不会触发GC 🙂

列表实际上具有内置容量,以防止每次添加/删除分配。 一旦超过容量,它就会增加更多(我想我每次都会加倍)。 问题可能是删除比添加更多。 添加将在第一个打开的位置下降,该位置由大小跟踪。 要删除,必须压缩列表以填充现在空的插槽。 如果您总是删除列表的前面,那么每个元素都需要向下滑动。

堆栈仍然使用数组作为其内部存储机制。 因此,您仍然受到数组的添加/删除属性的约束。

要使arrays工作,您需要创建所有子弹,每个子弹都有一个Active属性。 当您需要一个新的时,将Active标志填充为true并设置所有新的项目符号属性。 完成后,将Active标志设置为false。

如果你想尝试不再需要迭代列表(这可能会非常大,这取决于你要允许的内容)每次重绘,你可以尝试在数组中实现双链表。 当需要新子弹时,请向arrays询问第一个可用的免费条目。 转到最后一个活动项目符号(变量)并将新项目符号arrays位置添加到其下一个活动项目符号属性中。 什么时候删除它,转到上一个项目符号并将其活动项目符号属性更改为删除的下一个活动项。

 //I am using public fields for demonstration. You will want to make them properties public class Bullet { public bool Active; public int thisPosition; public int PrevBullet = -1; public int NextBullet = -1; public List list; public void Activate(Bullet lastBullet) { this.Active = true; this.PrevBullet = lastBullet.thisPosition; list[this.PrevBullet].NextBullet = this.thisPosition; } public void Deactivate() { this.Active = false; list[PrevBullet].NextBullet = this.NextBullet; list[NextBullet].PrevBullet= this.PrevBullet; } } 

这样,你有一个带有所有必需项目符号的预建数组,但是只有在数组中它们的位置不同时,paint才会命中活动的项目符号。 您只需要保持第一个活动项目符号的链接以启动绘制,并且最后一个活动项目符号需要知道列表重新开始的位置。

现在你只是担心内存占用整个列表而不是GC要清理的时候。