从零开始制作一个基础的粒子系统

这里我们来使用SDL2从零开始制作一个基础的粒子系统。

最后的成果像下面这样:

粒子系统

基础理论

首先我们来看一下实现粒子系统需要哪些基础理论。 粒子系统中最基本需要三个东西:

  • 世界:用于对发射出来的粒子操控,产生物理运动
  • 粒子
  • 发射器:用于发射粒子

我们在世界中会维护一个粒子池。每次发射器需要从粒子池里面将没有发射出去的粒子拿出来发射,世界会自动计算已经发射的粒子的物理运动,并且在他们死亡的时候在此放回粒子池里面。

每一个粒子,最基本需要一个生命值,这个生命值随着时间而减少。当减少到0的时候就是粒子死亡的时候,这个时候粒子需要回到粒子池里面。

这里让世界控制粒子而不是发射器控制粒子,首先方便了管理:所有的粒子都在粒子池里面,而不是零散的分散在发射器中。其次如果发射器被销毁了,其发射过的粒子仍然可以继续运动,不会出现粒子突然消失的情况。

实现

这里我们采用SDL2来实现粒子系统。 首先我们把所有的结构体全部给出来:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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)。然后在每一帧的时候世界会遍历粒子池里面的每一个粒子,对已经被唤醒的粒子计算物理运动。

这里有一些宏定义,先给出来:

1
2
3
4
#define WORLD_PARTICAL_INIT_NUM 100 //当世界创建的时候粒子池里面粒子的个数
#define PARTICAL_SINK_INC 50        //每次粒子池里面粒子不够用的时候,新增加的粒子数
#define PARTICAL_R 5                //粒子的半径
#define PARTICALS_PER_DEGREE 0.15   //每1度内包含的粒子数目(你也可以改成粒子密度,但是我这里为了简单就以每度的方式定义了)

首先我们把所有的创建函数给出来:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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;
}

然后是一些辅助函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
//这个函数在粒子池不够用的时候给粒子池扩容
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;
    }
}

然后就是发射粒子和对更新世界的函数了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//发射粒子,其实就是给粒子的各个属性赋值,然后设置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;
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
//旋转向量的代码在这里(如果看不懂可以参考我的“游戏编程中的旋转”一文)
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;
}

然后就是最重要的世界更新函数了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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;
    }
}

使用

最后给出我们的使用方式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#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;
}
updatedupdated2023-06-082023-06-08