我正在使用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);
}
所以,我知道过早的优化是所有邪恶的根源,但这是非常明显的低效率,我可以提前赶上(这是在甚至不必担心敌人的子弹充满屏幕之前)。所以我的问题是:将未使用的对象推送到堆栈会调用垃圾收集吗?参考文件是保持活着还是被破坏的对象?有没有更好的方法来处理更新许多不同的对象?例如,我是否太过花哨?如果只是遍历列表并找到一个未使用的子弹就可以了吗?
答案 0 :(得分:11)
这里有很多问题,这很难说。
首先,bullet
结构或类是什么?如果bullet是一个类,那么无论何时构造一个类,然后将其unroot(让它超出范围或将其设置为null),您将要添加GC需要收集的内容。
如果您要制作其中的许多内容,并在每一帧更新它们,您可能需要考虑使用List<bullet>
bullet
作为结构,并且预先分配List (生成它的大小足以容纳所有子弹,因此在调用List.Add
时不会重新创建它)。这将极大地帮助GC压力。
另外,仅仅因为我需要咆哮:
所以,我知道过早优化是所有邪恶的根源,但这是非常明显的低效率
永远不要害怕优化你知道导致问题的常规。如果您发现性能问题(即:滞后),则不再是过早优化。是的,您不希望优化每一行代码,但确实需要优化代码,尤其是当您发现真正的性能问题时。一旦发现问题就优化它比稍后尝试优化它更容易,因为在添加许多使用bullet
类的其他代码之前,所需的任何设计更改都将更容易实现
答案 1 :(得分:4)
您可能会发现flyweight design pattern很有用。只需要一个子弹对象,但是多个飞重可以为它指定不同的位置和速度。 flyweights可以存储在预分配的数组(例如,100)中,并标记为活动或不活动。
这应该完全消除垃圾收集,并可能减少跟踪每个子弹的可塑性属性所需的空间。
答案 2 :(得分:2)
我承认我本身没有任何经验,但我会考虑使用传统阵列。将数组初始化为您需要的大小,并且将是理论上最大的项目符号数,例如100.然后从0开始,在数组的开头指定项目符号,将最后一个元素保留为空。因此,如果您有四个活动项目符号,您的数组将如下所示:
0 B. 1 B 2 B. 3 B. 4 null ... 99 null
好处是数组总是会被分配,因此您不会处理更复杂的数据结构的开销。这实际上与字符串的工作方式非常相似,因为它们实际上是带有空终止符的char []。
可能值得一试。一个缺点是,你需要在移除子弹时进行一些手动操作,可能会在弹出一个插槽之后移动所有东西。但是你只是在那一点上移动指针,所以我认为它不会像分配内存或GC一样受到高度惩罚。
答案 3 :(得分:2)
你可以正确地假设通过将未使用的项目符号保留在堆栈中来防止它们被垃圾收集。
至于延迟的原因,您是否尝试过任何分析工具?只是为了找出问题所在。
答案 4 :(得分:2)
你的基于堆栈的解决方案非常接近我写的一般用于这种资源池的类:
http://codecube.net/2010/01/xna-resource-pool/
你提到这会使问题大部分消失,但它仍然会在这里和那里出现。发生的事情是,使用这种基于堆栈/队列的池化方法,一旦您不再请求比池可供应的更多新对象,系统将达到稳定点。但是,如果请求高于先前请求项的最大数量,则会导致您必须创建一个新实例来为请求提供服务(从而不时地调用GC)。
您可以采取的一种方法是通过并预先分配您认为在峰值时可能需要的实例。这样,您将不会有任何新的分配(至少来自池化对象),并且不会触发GC: - )
答案 5 :(得分:1)
列表实际上具有内置容量,可防止每次添加/删除时分配。一旦超出容量,它就会增加更多(我想我每次都会加倍)。问题可能在于删除而不是添加。添加将在第一个打开的位置下降,该位置由大小跟踪。要删除,必须压缩列表以填充现在空的插槽。如果你总是删除列表的前面,那么每个元素都需要向下滑动。
Stack仍然使用数组作为其内部存储机制。所以你仍然受到数组的添加/删除属性的约束。
要使阵列正常工作,您需要创建所有子弹,每个子弹都有一个Active属性。当您需要一个新的时,将Active标志填充为true并设置所有新的项目符号属性。完成后,将Active标志设置为false。
如果你想尝试消除迭代列表的需要(根据你要允许的内容可能非常大),每次重绘都可以尝试在数组中实现双链表。当需要新的子弹时,向阵列询问第一个可用的免费入口。转到最后一个活动项目符号(变量)并将新项目符号阵列位置添加到其下一个活动项目符号属性中。当需要删除它时,请转到上一个项目符号并将其活动项目符号属性更改为已删除的下一个活动项。
//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<Bullet> 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;
}
}
这样,你有一个带有所有必需子弹的预制数组,但是只有在阵列中的位置不同时,绘制才会击中活动的子弹。你只需要保持一个链接到第一个活动项目符号来启动画颜,最后一个活动项目符号就可以知道列表重新开始的位置。
现在你只是担心要保存整个列表的内存而不是GC要清理的时候。