T O P

[资源分享]     从零开始制作一个粒子系统

  • By - 楼主

  • 2021-09-03 15:10:03
  • 这里我们来使用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;
    }
    

    本帖子中包含资源

    您需要 登录 才可以下载,没有帐号?立即注册