1. 程式人生 > 其它 >從零開始製作一個粒子系統

從零開始製作一個粒子系統

這裡我們來使用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;
}