在设计粒子系统时,需要处理大量描述粒子的数据,这要求容器不仅易于扩展,而且足够快速。本文将讨论此类容器的选择、问题和可能的解决方案。
首先,来看一个常见的粒子类实现,然后分析其中存在的问题。
C++
class Particle {
public:
bool m_alive;
Vec4d m_pos;
Vec4d m_col;
float time;
// ... 其他字段
public:
// 构造函数...
void update(float deltaTime);
void render();
};
然后是使用这个类的方式:
std::vector particles;
// 更新函数:
for (auto &p : particles) p.update(dt);
// 渲染代码:
for (auto &p : particles) p.render();
对于简单的情况,这种实现方式可能是可行的。但是,让提出几个问题:
看起来这里违反了SRP。Particle类不仅负责持有数据,还执行更新、生成和渲染。也许更好的设计是有一个可配置的类来存储数据,其他系统/模块用于更新和渲染。
构建Particle类的方式限制了动态添加新属性的可能性。问题在于在这里使用的是AoS(数组的结构)模式,而不是SoA(结构的数组)。在SoA中,当想要添加更多的粒子属性时,只需创建/添加一个新的数组。
正如第一点提到的:违反了SRP,因此最好有一个单独的系统用于更新和渲染。对于简单的粒子系统,最初的解决方案是可行的,但当想要一些模块化/灵活性/可用性时,它就不够好了。
设计中至少有三个性能问题:
所有上述问题都必须在设计阶段解决。
上述代码中没有明显的问题,但粒子系统的另一个重要话题是添加和杀死粒子的算法:
void kill(particleID) { ?? }
void wake(particleID) { ?? }
如何高效地做到这一点?
看起来粒子需要一个动态数据结构——想要动态地添加和删除粒子。当然,可以使用列表或std::vector并在每次更改时进行更改,但那样会有效率吗?经常重新分配内存(每次创建粒子时)是好事吗?
可以最初假设的是,可以分配一个巨大的缓冲区,它将包含最大数量的粒子。这样,就不需要经常重新分配内存了。
解决了一个问题:经常缓冲区重新分配,但另一方面,现在面临着碎片化的问题。一些粒子是活的,一些不是。那么如何在一个单一的缓冲区中管理它们呢?
至少可以用两种方式管理缓冲区:
正如上面图片所示,当决定一个粒子需要被杀死时,将其与最后一个活跃的粒子交换。
这种方法比第一个想法更快: