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