Code - ECS ? Particles + arena + Quad Tree

ECS Arena QuadTree

ECS是𠁭,可以吃嗎? Arena , QuadTree 這東東又有何特別? 這次用30000個Particles來實驗一下下,有沒有廣告說的這麼強 !!!

o_O

#include "raylib.h"
#include "raymath.h"
#include <stdio.h> // 增加用於調試輸出的引用 (如果需要)
#include <stdlib.h> // 用於 rand(), malloc(), free()

#define ARENA_IMPLEMENTATION
#include "arena.h"

#define MAX_PARTICLES 30000 // 最大粒子數量
#define MAX_PARTICLES_PER_NODE 4 // 四叉樹每個葉節點能容納的最大粒子索引數量

// 粒子組件結構,包含位置和速度
typedef struct {
    Vector2 position;
    Vector2 velocity;
} ParticleComponent;

// 粒子系統結構,包含所有粒子和當前粒子數量
typedef struct {
    ParticleComponent particles[MAX_PARTICLES]; // 粒子數據陣列
    int count; // 當前粒子數量 (此範例中固定為 MAX_PARTICLES)
} ParticleSystem;

// 四叉樹節點結構
typedef struct QuadTreeNode {
    Rectangle bounds; // 此節點代表的空間範圍
    int particles[MAX_PARTICLES_PER_NODE]; // 儲存在此葉節點中的粒子 *索引* (僅葉節點有效)
    int count; // 儲存在此節點的粒子數量 (僅葉節點有效)
    struct QuadTreeNode* children[4]; // 子節點指標 (左上, 右上, 左下, 右下)
                                      // 如果 children[0] == NULL,表示這是葉節點
} QuadTreeNode;

// --- 函數原型宣告 ---
// 初始化粒子系統
void InitParticleSystem(ParticleSystem* system);
// 更新粒子系統狀態 (包含物理模擬和四叉樹交互)
void UpdateParticleSystem_NoQuadTree(ParticleSystem* system, Vector2 mousePos, float deltaTime);
void UpdateParticleSystem(ParticleSystem* system, Vector2 mousePos, float deltaTime);
// 繪製粒子系統
void DrawParticleSystem(ParticleSystem* system);
// 創建一個新的四叉樹節點
QuadTreeNode* CreateQuadTreeNode(Rectangle bounds);
// 分裂一個滿載的四叉樹葉節點
void SplitQuadTreeNode(QuadTreeNode* node, ParticleSystem* system);
// 將粒子插入到四叉樹節點 (會遞迴處理)
void InsertParticleToNode(QuadTreeNode* node, int particleIndex, ParticleSystem* system);
// 建立整個四叉樹
QuadTreeNode* BuildQuadTree(ParticleSystem* system, Rectangle bounds);
// 查詢四叉樹中與指定矩形範圍重疊的粒子
void QueryQuadTree(QuadTreeNode* node, Rectangle queryRect, int* result, int* count);
// 釋放整個四叉樹的記憶體
void FreeQuadTree(QuadTreeNode* node);

// 主函數
Arena arena = { 0 };

int main()
{
    // 螢幕尺寸
    const int screenWidth = 800;
    const int screenHeight = 600;
    arena_init(&arena);
    // 初始化 Raylib 窗口
    InitWindow(screenWidth, screenHeight, "粒子系統與四叉樹優化 (Particle System with QuadTree)");
    // 創建粒子系統實例
    ParticleSystem system;
    // 初始化粒子
    InitParticleSystem(&system);
    // 設定目標幀率
    SetTargetFPS(60);
    // 主遊戲迴圈
    while (!WindowShouldClose()) { // 持續執行直到使用者關閉視窗
        // 獲取幀時間差 (用於物理計算)
        float deltaTime = GetFrameTime();
        // 獲取滑鼠當前位置
        Vector2 mousePos = GetMousePosition();
        // 更新粒子系統狀態
        UpdateParticleSystem(&system, mousePos, deltaTime);
        // UpdateParticleSystem_NoQuadTree(&system, mousePos, deltaTime);
        //  開始繪圖
        BeginDrawing();
        // 清空背景為黑色
        ClearBackground(BLACK);
        // 繪製所有粒子
        DrawParticleSystem(&system);
        // 在左上角顯示當前 FPS
        DrawFPS(10, 10);
        // 結束繪圖
        EndDrawing();
    }
    arena_destroy(&arena);
    // 關閉 Raylib 窗口並釋放資源
    CloseWindow();
    // 程式正常結束
    return 0;
}

// 初始化粒子系統
void InitParticleSystem(ParticleSystem* system)
{
    system->count = MAX_PARTICLES; // 設定粒子數量
    // 遍歷所有粒子進行初始化
    for (int i = 0; i < MAX_PARTICLES; i++) {
        // 在螢幕範圍內隨機設定初始位置
        system->particles[i].position.x = rand() % 800;
        system->particles[i].position.y = rand() % 600;
        // 設定隨機初始速度 (-10 到 +10 之間)
        system->particles[i].velocity.x = (rand() % 100 - 50) / 5.0f;
        system->particles[i].velocity.y = (rand() % 100 - 50) / 5.0f;
    }
}
void UpdateParticleSystem_NoQuadTree(ParticleSystem* system, Vector2 mousePos, float deltaTime)
{
    // --- 直接處理所有粒子與滑鼠的互動 ---
    for (int i = 0; i < system->count; i++) {
        ParticleComponent* p = &system->particles[i];

        // 計算粒子到滑鼠的向量和距離 (與原碼相同)
        Vector2 toMouse = Vector2Subtract(mousePos, p->position);
        float distance = Vector2Length(toMouse);

        // 如果粒子在滑鼠 100 像素範圍內 (與原碼相同)
        if (distance > 1.0f && distance < 100.0f) {
            Vector2 direction = Vector2Normalize(toMouse);
            // 施加吸引力 (與原碼相同)
            p->velocity = Vector2Add(p->velocity, Vector2Scale(direction, 50.0f * (100.0f - distance) / 100.0f * deltaTime));
            // 增加阻尼 (與原碼相同)
            p->velocity = Vector2Scale(p->velocity, 1.0f - 0.1f * deltaTime); // 可以移到下面統一處理
        }

        // 可以在這裡統一應用阻尼,即使沒受到滑鼠影響
        // p->velocity = Vector2Scale(p->velocity, 1.0f - 0.1f * deltaTime);
    }

    // --- 更新所有粒子的位置 (與原碼相同) ---
    for (int i = 0; i < system->count; i++) {
        ParticleComponent* p = &system->particles[i];
        p->position = Vector2Add(p->position, Vector2Scale(p->velocity, deltaTime));

        // 處理邊界條件 (與原碼相同)
        if (p->position.x < 0)
            p->position.x = 800;
        if (p->position.x > 800)
            p->position.x = 0;
        if (p->position.y < 0)
            p->position.y = 600;
        if (p->position.y > 600)
            p->position.y = 0;
    }
}
// 更新粒子系統狀態
void UpdateParticleSystem(ParticleSystem* system, Vector2 mousePos, float deltaTime)
{
    // --- 效能瓶頸核心區域開始 ---
    // 定義滑鼠周圍的查詢矩形範圍 (200x200)
    Rectangle queryRect = { mousePos.x - 100, mousePos.y - 100, 200, 200 };

    // **關鍵效能問題**: 每幀重新建立完整的四叉樹
    // 理想情況下,應更新現有樹或使用記憶體池來減少分配開銷
    QuadTreeNode* root = BuildQuadTree(system, (Rectangle) { 0, 0, 800, 600 }); // 根節點範圍為整個螢幕

    // 用於儲存查詢結果的粒子索引陣列
    int result[MAX_PARTICLES]; // 注意: 這裡的大小是 MAX_PARTICLES,以防萬一,但通常遠小於此
    int count = 0; // 查詢結果的數量

    // 使用四叉樹查詢在滑鼠附近的粒子
    QueryQuadTree(root, queryRect, result, &count);

    // --- 僅處理靠近滑鼠的粒子 ---
    // 這個迴圈只遍歷被四叉樹查詢到的粒子,數量相對較少
    for (int i = 0; i < count; i++) {
        int index = result[i]; // 獲取粒子在 system->particles 中的索引
        ParticleComponent* p = &system->particles[index]; // 獲取該粒子的指針

        // 計算粒子到滑鼠的向量
        Vector2 toMouse = Vector2Subtract(mousePos, p->position);
        // 計算粒子到滑鼠的距離
        float distance = Vector2Length(toMouse);

        // 如果粒子在滑鼠 100 像素範圍內
        if (distance > 1.0f && distance < 100.0f) { // 避免除以零或距離過近時力過大
            // 計算從粒子指向滑鼠的單位向量 (方向)
            Vector2 direction = Vector2Normalize(toMouse);
            // 施加一個排斥力 (方向相反),力的大小與距離成反比
            // 使用 deltaTime 來確保力的作用與幀率無關
            // p->velocity = Vector2Subtract(p->velocity, Vector2Scale(direction, 100.0f / distance * deltaTime)); // 原始排斥
            p->velocity = Vector2Add(p->velocity, Vector2Scale(direction, 50.0f * (100.0f - distance) / 100.0f * deltaTime)); // 吸引力示例
        }
        // 可以增加阻尼效果,讓粒子移動更平滑
        p->velocity = Vector2Scale(p->velocity, 1.0f - 0.1f * deltaTime);
    }

    // --- 更新所有粒子的位置 ---
    // 這個迴圈仍然需要遍歷所有粒子
    for (int i = 0; i < system->count; i++) {
        ParticleComponent* p = &system->particles[i];
        // 根據速度更新位置: position += velocity * deltaTime
        p->position = Vector2Add(p->position, Vector2Scale(p->velocity, deltaTime));

        // 處理邊界條件 (穿牆效果)
        if (p->position.x < 0)
            p->position.x = 800; // 從左邊出去,右邊回來
        if (p->position.x > 800)
            p->position.x = 0; // 從右邊出去,左邊回來
        if (p->position.y < 0)
            p->position.y = 600; // 從上面出去,下面回來
        if (p->position.y > 600)
            p->position.y = 0; // 從下面出去,上面回來
    }

    // **關鍵效能問題**: 每幀釋放整個四叉樹的記憶體
    FreeQuadTree(root);
    // --- 效能瓶頸核心區域結束 ---
}

// 繪製粒子系統
void DrawParticleSystem(ParticleSystem* system)
{
    // 遍歷所有粒子並繪製
    for (int i = 0; i < system->count; i++) {
        ParticleComponent* p = &system->particles[i];
        // 在粒子的位置繪製一個白色的圓點 (半徑 2 像素)
        //        DrawCircle(p->position.x, p->position.y, 2, WHITE);
        DrawPixel(p->position.x, p->position.y, WHITE);
    }
}

// 創建一個新的四叉樹節點
QuadTreeNode* CreateQuadTreeNode(Rectangle bounds)
{
    // 為新節點分配記憶體
    // 優化提示: 這裡可以使用記憶體池來代替 malloc
    //    QuadTreeNode* node = malloc(sizeof(QuadTreeNode));
    QuadTreeNode* node = arena_alloc(&arena, sizeof(QuadTreeNode));
    if (!node) {
        // 處理記憶體分配失敗的情況 (例如,打印錯誤訊息並退出)
        perror("Failed to allocate memory for QuadTreeNode");
        exit(EXIT_FAILURE);
    }
    node->bounds = bounds; // 設定節點的空間範圍
    node->count = 0; // 初始化粒子計數為 0 (僅葉節點有效)
    // 初始化所有子節點為 NULL (表示當前是葉節點)
    for (int i = 0; i < 4; i++) {
        node->children[i] = NULL;
    }
    // 注意: 'particles' 陣列不需要在此處初始化,因為它是節點結構的一部分
    return node;
}

// 分裂一個滿載的四叉樹葉節點
void SplitQuadTreeNode(QuadTreeNode* node, ParticleSystem* system)
{
    // 計算子節點的寬度和高度
    float subWidth = node->bounds.width / 2;
    float subHeight = node->bounds.height / 2;
    // 獲取父節點的左上角座標
    float x = node->bounds.x;
    float y = node->bounds.y;

    // 創建四個子節點,分別對應四個象限
    // 優化提示: 這裡也可以使用記憶體池
    node->children[0] = CreateQuadTreeNode((Rectangle) { x, y, subWidth, subHeight }); // 左上
    node->children[1] = CreateQuadTreeNode((Rectangle) { x + subWidth, y, subWidth, subHeight }); // 右上
    node->children[2] = CreateQuadTreeNode((Rectangle) { x, y + subHeight, subWidth, subHeight }); // 左下
    node->children[3] = CreateQuadTreeNode((Rectangle) { x + subWidth, y + subHeight, subWidth, subHeight }); // 右下

    // --- 將原來儲存在父節點(現在變為內部節點)的粒子重新分配到新的子節點中 ---
    // 臨時儲存父節點的粒子索引和數量
    int tempParticles[MAX_PARTICLES_PER_NODE];
    int tempCount = node->count;
    for (int i = 0; i < tempCount; ++i) {
        tempParticles[i] = node->particles[i];
    }

    // 清空父節點的粒子計數 (因為它不再是葉節點)
    node->count = 0;

    // 遍歷臨時儲存的粒子索引
    for (int i = 0; i < tempCount; i++) {
        int index = tempParticles[i]; // 獲取粒子索引
        Vector2 pos = system->particles[index].position; // 獲取粒子位置

        // 檢查粒子屬於哪個子節點,並將其插入
        for (int j = 0; j < 4; j++) {
            if (CheckCollisionPointRec(pos, node->children[j]->bounds)) {
                InsertParticleToNode(node->children[j], index, system);
                break; // 粒子只會屬於一個子節點,找到後跳出內層迴圈
            }
        }
    }
}

// 將粒子插入到四叉樹節點 (會遞迴處理)
void InsertParticleToNode(QuadTreeNode* node, int particleIndex, ParticleSystem* system)
{
    // 獲取要插入粒子的位置
    Vector2 pos = system->particles[particleIndex].position;
    // 檢查當前節點是否為葉節點 (沒有子節點)
    if (node->children[0] == NULL) { // 是葉節點
        // 檢查葉節點是否還有空間容納更多粒子
        if (node->count < MAX_PARTICLES_PER_NODE) {
            // --- 修改點: 直接存入靜態陣列 ---
            node->particles[node->count] = particleIndex; // 將粒子索引存入陣列
            node->count++; // 增加節點的粒子計數
        } else {
            // 葉節點已滿,需要分裂
            SplitQuadTreeNode(node, system);
            // 分裂後,將 *當前正要插入的* 粒子插入到對應的新子節點中
            for (int i = 0; i < 4; i++) {
                if (CheckCollisionPointRec(pos, node->children[i]->bounds)) {
                    InsertParticleToNode(node->children[i], particleIndex, system);
                    break;
                }
            }
        }
    } else { // 不是葉節點 (是內部節點)
        // 將粒子遞迴地插入到對應的子節點中
        for (int i = 0; i < 4; i++) {
            if (CheckCollisionPointRec(pos, node->children[i]->bounds)) {
                InsertParticleToNode(node->children[i], particleIndex, system);
                break;
            }
        }
    }
}

// 建立整個四叉樹
QuadTreeNode* BuildQuadTree(ParticleSystem* system, Rectangle bounds)
{
    // 創建根節點,範圍由參數指定 (通常是整個螢幕)
    QuadTreeNode* root = CreateQuadTreeNode(bounds);
    // 遍歷系統中的所有粒子
    for (int i = 0; i < system->count; i++) {
        // 將每個粒子插入到四叉樹中 (從根節點開始)
        InsertParticleToNode(root, i, system);
    }
    // 返回構建好的四叉樹的根節點
    return root;
}

// 查詢四叉樹中與指定矩形範圍重疊的粒子
void QueryQuadTree(QuadTreeNode* node, Rectangle queryRect, int* result, int* count)
{
    // 剪枝: 如果節點的範圍與查詢範圍完全沒有重疊,則無需繼續查詢此分支
    if (!CheckCollisionRecs(node->bounds, queryRect)) {
        return;
    }

    // 檢查是否為葉節點
    if (node->children[0] == NULL) { // 是葉節點
        // 遍歷此葉節點中儲存的所有粒子索引
        for (int i = 0; i < node->count; i++) {
            // 這裡可以選擇性地再次檢查粒子實際位置是否真的在 queryRect 內 (更精確)
            // if (CheckCollisionPointRec(system->particles[node->particles[i]].position, queryRect)) {
                // 將粒子索引添加到結果陣列中
                result[*count] = node->particles[i];
                (*count)++; // 增加結果計數
            // }
        }
    } else { // 是內部節點
        // 遞迴地查詢四個子節點
        for (int i = 0; i < 4; i++) {
            QueryQuadTree(node->children[i], queryRect, result, count);
        }
    }
}

// 遞迴地釋放整個四叉樹的記憶體
void FreeQuadTree(QuadTreeNode* node)
{
/*    
    // 檢查是否為內部節點
    if (node->children[0] != NULL) {
        // 遞迴地釋放所有子節點
        for (int i = 0; i < 4; i++) {
            FreeQuadTree(node->children[i]);
            node->children[i] = NULL; // 避免懸空指針 (好習慣)
        }
    }
    free(node);
*/
    arena_reset(&arena);    
}

See also