我目前正在使用C#/ XNA开发2D游戏。游戏的核心功能是具有截然不同的行为的子弹(这将是一种子弹地狱游戏)。 更新所有项目符号可能需要相当长的时间,因为它们的行为可能无限复杂。而且所有人都必须进行1次碰撞检查。 最初我只是将它们存储在List中并更新并绘制所有这些,从每个帧中删除列表中的非活动项目符号。 然而,当屏幕上有8k子弹时,这很快就被证明会减慢游戏速度,所以我决定实现多线程并使用LINQ来帮助提高性能。
事情是它仍在减速约16k子弹。我被告知如果我做得对,我可以获得高达700万的活跃子弹,所以我对16k不满意......
我还能做些什么来提高性能吗?
代码前的附加信息:我的项目符号包含速度,方向,角速度,加速度,速度限制和行为的字段。 提到的唯一特殊事情是行为。它可以随时修改任何项目符号字段或产生更多项目符号甚至将其自身植入其中,因此我很难应用数据驱动的解决方案并只将所有这些字段存储在数组中而不是列出子弹。
internal class BulletManager : GameComponent
{
public static float CurrentDrawDepth = .82f;
private readonly List<Bullet> _bullets = new List<Bullet>();
private readonly int _processorCount;
private int _counter;
private readonly Task[] _tasks;
public BulletManager(Game game)
: base(game)
{
_processorCount = VariableProvider.ProcessorCount;
_tasks = new Task[_processorCount];
}
public void ClearAllBullets()
{
_bullets.Clear();
}
public void AddBullet(Bullet bullet)
{
_bullets.Add(bullet);
}
public override void Update(GameTime gameTime)
{
if (StateManager.GameState != GameStates.Ingame &&
(StateManager.GameState != GameStates.Editor || EngineStates.GameStates != EEngineStates.Running))
return;
var bulletCount = _bullets.Count;
var bulletsToProcess = bulletCount / _processorCount;
//Split up the bullets to update among all available cores using Tasks and a lambda expression
for (var i = 0; i < _processorCount; ++i )
{
var x = i;
_tasks[i] = Task.Factory.StartNew( () =>
{
for(var j = bulletsToProcess * x; j < bulletsToProcess * x + bulletsToProcess; ++j)
{
if (_bullets[j].Active)
_bullets[j].Update();
}
});
}
//Update the remaining bullets (if any)
for (var i = bulletsToProcess * _processorCount; i < bulletCount; ++i)
{
if (_bullets[i].Active)
_bullets[i].Update();
}
//Wait for all tasks to finish
Task.WaitAll(_tasks);
//This is an attempt to reduce the load per frame, originally _bullets.RemoveAll(s => !s.Active) ran every frame.
++_counter;
if (_counter != 300) return;
_counter = 0;
_bullets.RemoveAll(s => !s.Active);
}
public void Draw(SpriteBatch spriteBatch)
{
if (StateManager.GameState != GameStates.Ingame && StateManager.GameState != GameStates.Editor) return;
spriteBatch.DrawString(FontProvider.GetFont("Mono14"), _bullets.Count.ToString(), new Vector2(100, 20),
Color.White);
//Using some LINQ to only draw bullets in the viewport
foreach (var bullet in _bullets.Where(bullet => Camera.ViewPort.Contains(bullet.CircleCollisionCenter.ToPoint())))
{
bullet.Draw(spriteBatch);
CurrentDrawDepth -= .82e-5f;
}
CurrentDrawDepth = .82f;
}
}
答案 0 :(得分:7)
哇。你发布的代码有很多错误(也可能是你没有发布的代码)。以下是您需要做的事情,大致按重要性/必要性的降序排列:
测量性能。在最基本的级别上,帧速率计数器(或者更好的是帧时间计数器)。你想检查一下你是做得更好。
在游戏循环期间不要分配内存。检查自己是否最好的方法是使用CLR Profiler。虽然您可能没有使用new
(分配class
类型,structs
没问题),但如果LINQ的大部分内容都是在幕后分配内存,那就不会让我感到惊讶。 / p>
请注意ToString
将分配内存。如果您需要,可以使用无分配方式(使用StringBuilder
)来绘制数字。
不要使用LINQ。 LINQ简单方便,绝对不是操作集合的最快或最节省内存的方法。
使用数据驱动的方法。数据驱动方法背后的关键思想是保持缓存一致性(more info)。也就是说:所有Bullet
数据都以线性方式存储在内存中。为此,请确保Bullet
为struct
并将其存储在List<Bullet>
中。这意味着当一个Bullet
被加载到CPU缓存中时,它会带来其他内容(内存以大块的形式加载到缓存中),从而减少CPU花费等待内存加载的时间。
要快速删除项目符号,请使用列表中的最后一个项目符号覆盖要删除的项目符号,然后删除最后一个项目。这允许您删除元素而不复制大部分列表。
谨慎使用SpriteBatch
。为子弹执行单独的一批精灵(Begin()/End()
阻止)。使用SpriteSortMode.Deferred
- 它是迄今为止最快的模式。进行排序(如CurrentDrawDepth
所暗示的那样)很慢!确保所有项目符号都使用相同的纹理(必要时使用纹理图集)。请记住,如果连续的精灵共享一个纹理,那么批处理只会提高性能。 (More info)
如果您正在使用SpriteBatch
,那么可能可以更快地绘制所有精灵,然后让GPU剔除它们,如果它们在屏幕外。
(可选)为每种行为维护不同的列表。这减少了代码中的分支数量,并可能使代码本身(即:指令,而不是数据)更加缓存一致。与上述要点不同,这只会带来很小的性能提升,因此只有在需要时才能实现。
(注意:除此之外,这些更改很难实现,会使您的代码难以阅读,甚至可能使代码变慢。只有在绝对必要时才实现这些更改并且您正在测量性能。)
(可选)内联您的代码。一旦开始涉及成千上万的项目符号,您可能需要内联代码(删除方法调用)以挤出更多性能。 C#编译器没有内联,JIT只做了一点,所以你需要手动内联。方法调用包括您可能在向量上使用的+
和*
运算符之类的内容 - 内联这些将提高性能。
(可选)使用自定义着色器。如果您希望获得比仅使用SpriteBatch
更高的性能,请编写一个自定义着色器,它会获取您的Bullet
数据并进行尽可能多的计算尽可能在GPU上。
(可选)使您的数据更小,并且(如果可能)不可变。将初始条件(位置,方向,时间戳)存储在Bullet
结构中。然后使用基本equations of motion仅在您需要时计算当前位置/速度/等。您通常可以“免费”进行这些计算 - 因为您在等待内存时可能没有未使用的CPU时间。
如果您的数据是不可变的,那么您可以避免每帧都将其传输到GPU上! (如果你要添加/删除子弹,你必须在这些帧上的GPU上更新它。)
如果你实施了所有这些项目,我想你可能会在一台好机器上获得多达700万颗子弹。虽然这可能会留下很少的CPU时间留给游戏的其余部分。
答案 1 :(得分:1)
答案 2 :(得分:1)
为什么要删除不活动的子弹?
我认为这种类型的东西经常被一个“池”的概念所解决 - 也许我从你的代码中遗漏了一些东西,但似乎你已经有了一个活跃的概念,所以为什么要删除一个不活动的一个然后创建一个新的子弹,在某些时候将再次删除由GC处理。只需重复使用非活动子弹。
另外,我无法告诉你它有多痛,但是你的抽奖中每秒30次使用ToString()会产生垃圾清理。
答案 3 :(得分:0)
如果子弹的Update()
方法是瓶颈(请确保按照@PiRX建议并首先使用分析器来查找瓶颈),您可以:
a)仅更新每帧的可见项目符号,并更频繁地更新不可见的项目符号。
b)简化更新过程:比如说,子弹每10帧(每0.5秒,无论如何)只执行其特定的(耗时的)行为,并且在其余时间做一些简单的事情(比如直接飞行)。这两项建议当然是性能和准确性之间的权衡。
答案 4 :(得分:0)
至少对于屏幕外的子弹(或屏幕外的子弹),您可以通过检查它应该击中的内容来替换子弹的整个使用,并将延迟的消息发送到受到命中的命中的目标。在N时代的一颗子弹。然后,延迟消息将替换这些子弹的整个UPDATE计算,并仍然会施加损坏。
答案 5 :(得分:0)
Update()
方法尽可能优化是至关重要的。
我还会尝试重构嵌套的for
周期以减少迭代次数,如下所示(未经测试,在我的头顶代码之后):
_tasks.ForEach(i=>
{
i.Factory.StartNew(()=>
{
_bullets.Where(j=> _bullets.IndexOf(j)%_tasks.IndexOf(i)==0 && j.Active).Update();
}
}
);