这里我们来使用SDL2从零开始制作一个基础的粒子系统。
最后的成果像下面这样:
基础理论
首先我们来看一下实现粒子系统需要哪些基础理论。
粒子系统中最基本需要三个东西:
- 世界:用于对发射出来的粒子操控,产生物理运动
- 粒子
- 发射器:用于发射粒子
我们在世界中会维护一个粒子池。每次发射器需要从粒子池里面将没有发射出去的粒子拿出来发射,世界会自动计算已经发射的粒子的物理运动,并且在他们死亡的时候在此放回粒子池里面。
每一个粒子,最基本需要一个生命值,这个生命值随着时间而减少。当减少到0的时候就是粒子死亡的时候,这个时候粒子需要回到粒子池里面。
这里让世界控制粒子而不是发射器控制粒子,首先方便了管理:所有的粒子都在粒子池里面,而不是零散的分散在发射器中。其次如果发射器被销毁了,其发射过的粒子仍然可以继续运动,不会出现粒子突然消失的情况。
实现
这里我们采用SDL2来实现粒子系统。
首先我们把所有的结构体全部给出来:
typedef struct{
int hp; /**< 粒子的生命值,这决定了粒子能喷多远*/
SDL_Vector direct; /**< 粒子的发射方向*/
bool isdead; /**< 粒子是否死亡*/
SDL_Color color; /**< 粒子的颜色*/
SDL_Pointf position; /**< 粒子的位置*/
}_PS_Partical;
typedef struct{
SDL_Vector gravity; /**< 重力*/
int partical_num; /**< 粒子池中的粒子个数*/
_PS_Partical* particals; /**< 粒子池*/
SDL_Renderer* render; /**< SDL2要求的渲染器*/
}PS_World;
typedef struct{
SDL_Vector shoot_dir; /**< 粒子将要发射出去的方向*/
int partical_hp; /**< 每个粒子的生命值*/
float half_degree; /**< 发射器的最大张角*/
SDL_Color color; /**< 粒子的颜色*/
PS_World* world; /**< 发射器所在的世界*/
int shoot_num; /**< 一次性发射出去的粒子个数*/
SDL_Point position; /**< 粒子发射器的位置*/
}PS_ParticalLauncher;
这里我们不希望将粒子暴露给其他程序员,所以这里加上_
表示私有的,不想要被访问。
这里的思路是这样的:首先我们需要创造一个世界,然后需要创造一个粒子发射器。粒子发射器会从世界的粒子池里面找到isdead=true的粒子,设置它的属性,并且将其唤醒(isdead=false)。然后在每一帧的时候世界会遍历粒子池里面的每一个粒子,对已经被唤醒的粒子计算物理运动。
这里有一些宏定义,先给出来:
#define WORLD_PARTICAL_INIT_NUM 100 //当世界创建的时候粒子池里面粒子的个数
#define PARTICAL_SINK_INC 50 //每次粒子池里面粒子不够用的时候,新增加的粒子数
#define PARTICAL_R 5 //粒子的半径
#define PARTICALS_PER_DEGREE 0.15 //每1度内包含的粒子数目(你也可以改成粒子密度,但是我这里为了简单就以每度的方式定义了)
首先我们把所有的创建函数给出来:
PS_World PS_CreateWorld(SDL_Vector gravity, SDL_Renderer* render){
//初始化随机数生成器
srand((unsigned)time(NULL));
PS_World world;
//赋值属性
world.gravity = gravity;
world.render = render;
world.partical_num = WORLD_PARTICAL_INIT_NUM;
world.particals = (_PS_Partical*)malloc(sizeof(_PS_Partical)*WORLD_PARTICAL_INIT_NUM); //malloc粒子池
//如果malloc失败报错
if(world.particals == NULL)
SDL_LogError(SDL_LOG_CATEGORY_ERROR, "memory not enough, world partical malloc failed!!");
//将粒子池里面的所有粒子设为死亡状态
for(int i=0;i<world.partical_num;i++)
world.particals[i].isdead = true; //false和true是C99标准新增的,在头文件<stdbool.h>中
return world;
}
PS_ParticalLauncher PS_CreateLauncher(SDL_Point position, SDL_Vector shoot_dir, int partical_hp, float half_degree, SDL_Color color, PS_World* world, int shoot_num){
PS_ParticalLauncher launcher;
//赋值属性
launcher.color = color;
launcher.half_degree = half_degree;
launcher.partical_hp = partical_hp;
launcher.shoot_dir = shoot_dir;
launcher.world = world;
//根据角度计算一次性发射的粒子总数
launcher.shoot_num = (int)ceil(half_degree*2*PARTICALS_PER_DEGREE);
launcher.position = position;
return launcher;
}
然后是一些辅助函数:
//这个函数在粒子池不够用的时候给粒子池扩容
void _PS_IncreaseParticalSink(PS_World* world){
world->particals = (_PS_Partical*)realloc(world->particals, sizeof(_PS_Partical)*(world->partical_num+PARTICAL_SINK_INC));
if(world->particals == NULL)
SDL_LogError(SDL_LOG_CATEGORY_ERROR, "memory not enough, partical sink realloc failed!!");
for(int i=world->partical_num-1;i<world->partical_num+PARTICAL_SINK_INC;i++)
world->particals[i].isdead = true;
world->partical_num += PARTICAL_SINK_INC;
}
//这个函数在粒子池中从idx开始寻找下一个死亡的粒子,并且返回这个粒子,将这个粒子的下标赋值给idx(idx相当于迭代器)
_PS_Partical* _PS_GetNextDeadPartical(PS_World* world, int* idx){
int sum = 0;
(*idx)++;
if(*idx >= world->partical_num)
*idx = 0;
while(world->particals[*idx].isdead != true){
(*idx)++;
if(*idx >= world->partical_num)
(*idx) = 0;
sum++;
if(sum >= world->partical_num)
break;
}
if(sum >= world->partical_num)
return NULL;
return &world->particals[*idx];
}
//这个函数和上面的一样,只不过是找到下一个没有死亡的粒子
_PS_Partical* _PS_GetNextUndeadPartical(PS_World* world, int* idx){
int sum = 0;
(*idx)++;
if(*idx >= world->partical_num)
*idx = 0;
while(world->particals[*idx].isdead == true){
(*idx)++;
if(*idx >= world->partical_num)
(*idx) = 0;
sum++;
if(sum >= world->partical_num)
return NULL;
}
return &world->particals[*idx];
}
//这个函数绘制粒子
void _PS_DrawPartical(SDL_Renderer* render, _PS_Partical* partical){
SDL_Color* color = &partical->color;
SDL_SetRenderDrawColor(render, color->r, color->g, color->b, color->a);
SDL_RenderDrawCircle(render, partical->position.x, partical->position.y, PARTICAL_R); //这个函数是我自己封装的,SDL2本身是不带有的。绘制圆的函数。
}
//绘制圆函数的实现
void SDL_RenderDrawCircle(SDL_Renderer* render, int x, int y, int r){
float angle = 0;
const float delta = 5;
for(int i=0;i<360/delta;i++){
float prevradian = Degree2Radian(angle),
nextradian = Degree2Radian(angle+delta);
SDL_RenderDrawLine(render, x+r*cosf(prevradian), y+r*sinf(prevradian), x+r*cosf(nextradian), y+r*sinf(nextradian));
angle += delta;
}
}
然后就是发射粒子和对更新世界的函数了
//发射粒子,其实就是给粒子的各个属性赋值,然后设置isdead为false
void PS_ShootPartical(PS_ParticalLauncher* launcher){
PS_World* world = launcher->world;
int idx = 0;
//这里需要发射shoot_num个粒子
for(int i=0;i<launcher->shoot_num;i++){
_PS_Partical* partical;
//这里循环获得下一个死亡的粒子。如果返回NULL表示粒子池里面的粒子都在活动,这个时候就要扩充粒子池。
while((partical=_PS_GetNextDeadPartical(world, &idx))==NULL){
_PS_IncreaseParticalSink(world);
}
//这里对其发射的角度进行随机(在half_degree里)
int randnum = rand()%(int)(2*launcher->half_degree*1000+1)-(int)launcher->half_degree*1000;
float randdegree = randnum/1000.0f;
//TODO 这个地方的赋值要不要使用指针呢?放在最后的时候优化吧
partical->color = launcher->color;
SDL_Vector direct = Vec_Rotate(&launcher->shoot_dir, randdegree); //旋转发射向量
partical->direct = direct;
partical->hp = launcher->partical_hp + rand()%(10+1)-5;
partical->isdead = false;
partical->position.x = launcher->position.x;
partical->position.y = launcher->position.y;
}
}
//旋转向量的代码在这里(如果看不懂可以参考我的“游戏编程中的旋转”一文)
typedef struct{
float x;
float y;
}SDL_Pointf;
typedef SDL_Pointf SDL_Vector;
inline float Degree2Radian(float degree){
return degree*M_PI/180.0f;
}
SDL_Vector Vec_Rotate(SDL_Vector* v, float degree){
float radian = Degree2Radian(degree);
SDL_Vector ret = {cosf(radian)*v->x-sinf(radian)*v->y, sinf(radian)*v->x+cosf(radian)*v->y};
return ret;
}
然后就是最重要的世界更新函数了:
void PS_WorldUpdate(PS_World* world){
_PS_Partical* partical;
//遍历粒子池里面每一个粒子
for(int i=0;i<world->partical_num;i++){
partical = &world->particals[i];
//如果是活的,就计算其下一帧的位置
if(partical->isdead == false){
if(partical->hp > 0){
partical->position.x += partical->direct.x+world->gravity.x/2.0;
partical->position.y += partical->direct.y+world->gravity.y/2.0;
_PS_DrawPartical(world->render, partical);
}
partical->hp--;
}
if(partical->hp <= 0)
partical->isdead = true;
}
}
使用
最后给出我们的使用方式:
#include "SDL.h"
#include "particalSystem.h"
#include "log.h"
#define TEST_ALL
int main(int argc, char** argv){
SDL_Init(SDL_INIT_EVERYTHING);
SDL_Window* window;
SDL_Renderer* render;
SDL_CreateWindowAndRenderer(800, 800, SDL_WINDOW_SHOWN, &window, &render);
SDL_Event event;
bool isquit = false;
SDL_Vector gravity = {0, 0};
SDL_Color color = {0, 255, 0, 255};
SDL_Color explodecolor = {255, 0, 0, 255};
SDL_Vector direct = {5, -5};
SDL_Point position = {400, 400};
SDL_Point explodePositon = {300, 300};
int partical_hp = 50;
PS_World world;
world = PS_CreateWorld(gravity, render);
PS_ParticalLauncher launcher = PS_CreateLauncher(position, direct, partical_hp, 30, color, &world, 10);
while(!isquit){
SDL_SetRenderDrawColor(render, 100, 100, 100, 255);
SDL_RenderClear(render);
while(SDL_PollEvent(&event)){
if(event.type == SDL_QUIT)
isquit = true;
if(event.type == SDL_KEYDOWN){
switch(event.key.keysym.sym){
case SDLK_SPACE:
PS_Explode(&world, explodecolor, explodePositon, 100);
break;
case SDLK_d:
launcher.shoot_dir = Vec_Rotate(&launcher.shoot_dir, 5);
break;
case SDLK_a:
launcher.shoot_dir = Vec_Rotate(&launcher.shoot_dir, -5);
break;
case SDLK_w:
launcher.partical_hp+=2;
break;
case SDLK_s:
if(launcher.partical_hp > 0)
launcher.partical_hp-=2;
break;
}
}
}
PS_ShootPartical(&launcher); //发射粒子
PS_WorldUpdate(&world); //世界更新
SDL_SetRenderDrawColor(render, 255, 0, 0, 255);
SDL_RenderDrawLine(render, launcher.position.x, launcher.position.y, launcher.position.x+launcher.shoot_dir.x*50, launcher.position.y+launcher.shoot_dir.y*50);
SDL_RenderPresent(render);
SDL_Delay(30);
}
PS_DestroyLauncher(&launcher);
PS_DestroyWorld(&world);
SDL_DestroyRenderer(render);
SDL_DestroyWindow(window);
SDL_Quit();
return 0;
}