Всем привет.

Если начинать путь в 3D графике с воксельного рендеринга и дойти до многопоточной обработки бесконечных чанков, рано или поздно наступает момент истины. Вы осознаете: всё, что отображается на экране — это лишь визуальная репрезентация «физического» слоя данных. То, как эти данные структурированы и хранятся, напрямую определяет возможности взаимодействия с ними. Именно поэтому я всегда рекомендую воксельное представление мира как лучшую школу для знакомства с трехмерной графикой.

Сегодня мы поговорим не о самих вокселях, но они станут тем «кирпичиком», который поможет понять глобальный процесс. Наша тема — деревья BVH (Bounding Volume Hierarchy) и то, как простые кубики — AABB (Axis-Aligned Bounding Box) — ускоряют прототипирование и понимание пространственной логики. Я поделюсь опытом и решениями, к которым пришел, пытаясь реализовать свои идеи в графике.

Почему BVH — это база?

BVH (Иерархия ограничивающих объемов) — это бинарное дерево, которое радикально ускоряет поиск объектов в пространстве: будь то расчет пересечения луча (raytracing) или проверка столкновений. Такие деревья могут быть статическими или динамическими.

Данная реализация основана на концепциях, которые я почерпнул из открытых источников и, в частности, из материалов и презентаций Эрина Катто (Erin Catto), создателя физического движка Box2D. В тот момент, когда я столкнулся с трудностями «наивных» реализаций, именно его подход помог мне структурировать мышление.

Концепция BVH дает ряд критических преимуществ:

  1. Пространственная вложенность: поиск по иерархии меток работает в разы быстрее последовательного перебора.

  2. Объемное мышление: структура приучает мыслить категориями объемов, а не отдельных точек.

  3. Структура сцены: дерево может выступать каркасом всей сцены. Пучки данных легко поддаются персистентности — их гораздо проще сохранять, восстанавливать и передавать между узлами.

По началу я пытался делать на прямую, без структурирования, но в какой-то момент это стало неуправляемо, где-то с третьего раза почитав статью про BVH, я примерно понял как выйти элегантнее через физику в отрисовку. Так же следует учесть, что есть разные подходы сборки таких деревьев.

Реализация динамического мира (C++)

Ниже представлен полный код системы(за исключением реализации стека). Это динамическое самобалансирующееся дерево, которое оперирует пока одним «пучком», успешно справляясь с интенсивным взаимодействием объектов в игровом цикле. Правда с оговоркой до многопоточности, тоесть около 1000 - еще нормально, 10000 - это максимум без многопоточки.

Скрытый текст
// Как использовать этот файл:
// Сохраните его как DynamicWorld.hpp.
// Подключите в свой main.cpp.
// Теперь у вас есть всё необходимое для создания сложной игровой логики.
// Ваш следующий шаг:
// Теперь вы можете создать Player Controller. Вызывайте world.UpdatePosition каждый кадр, а затем world.Query вокруг игрока. Если в списке окажется триггер с заданным onTrigger — просто вызывайте его!

#include <iostream>
#include <vector>
#include <map>
#include <memory>
#include <functional>
#include <algorithm>
#include <limits>
#include <iomanip>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_access.hpp>
// --- Константы и Слои ---
enum CollisionLayer : uint32_t {
    LAYER_NONE    = 0,
    LAYER_STATIC  = 1 << 0,
    LAYER_PLAYER  = 1 << 1,
    LAYER_ENEMY   = 1 << 2,
    LAYER_TRIGGER = 1 << 3,
    LAYER_ALL     = 0xFFFFFFFF
};

// --- Базовая Математика ---
struct AABB {
    glm::vec3 lowerBound;
    glm::vec3 upperBound;

    inline bool Contains(const AABB& other) const {
        return other.lowerBound.x >= lowerBound.x && other.lowerBound.y >= lowerBound.y &&
        other.lowerBound.z >= lowerBound.z && other.upperBound.x <= upperBound.x &&
        other.upperBound.y <= upperBound.y && other.upperBound.z <= upperBound.z;
    }

    static AABB Union(const AABB& a, const AABB& b) {
        return { glm::min(a.lowerBound, b.lowerBound), glm::max(a.upperBound, b.upperBound) };
    }

    static float Area(const AABB& a) {
        glm::vec3 d = a.upperBound - a.lowerBound;
        return 2.0f * (d.x * d.y + d.y * d.z + d.z * d.x);
    }
};

struct PendingTask {
    float timeLeft;
    std::function<void()> action;
};
// --- Сущность Мира ---
// Данные для ИИ и Боя
enum EntityType { PLAYER, FRIENDLY, BITER, SHOOTER, LOOT };
enum MoveType   { STATIC, ELEVATOR, HORIZONTAL };
struct EntityData {
    EntityType type = FRIENDLY;
    MoveType moveType = STATIC;
    glm::vec3 startPos;
    float moveTimer = 0.0f;
    glm::vec3 lastOffset{0.0f}; // Чтобы передать движение игроку
    float health = 100.0f;
    float speed = 0.2f;
    float maxHealth = 100.0f;
    float attackRange = 1.5f;
    float attackCooldown = 1.0f;
    bool isAggressive = false; // Становится true, если игрок ударил первым
    uint32_t lootType = 0;// 0 - мана, 1 - хп
    bool isDead = false;
    float lifetime = -1.0f; // -1 значит "живет вечно"
    bool markedForDeletion = false;
};

struct Entity {
    int id;
    glm::vec3 position;
    glm::vec3 size;
    uint32_t category;
    uint32_t mask;

    EntityData gameplay; // Геймплейные статы

    std::function<void(int)> onTrigger = nullptr;
    std::function<void()> onInteract = nullptr;

    Entity(int _id, glm::vec3 _pos, glm::vec3 _size, uint32_t _cat, uint32_t _mask)
    : id(_id), position(_pos), size(_size), category(_cat), mask(_mask) {}

    AABB getAABB() const {
        return { position - (size * 0.5f), position + (size * 0.5f) };
    }
};
// --- Вспомогательный Стек ---
template <typename T>
class Stack {}

// --- Узел Дерева ---
struct Node {
    AABB box;
    int objectIndex = -1;
    int parentIndex = -1;
    int child1 = -1, child2 = -1;
    int height = 0;
    bool isLeaf = false;
    int next = -1; // Для пула свободных узлов
};

// --- Динамическое BVH Дерево ---
class DynamicTree {
public:
    std::vector<Node> nodes;
    int rootIndex = -1;
    int freeList = -1;
    const float margin = 0.2f; // Fat AABB запас

    int AllocateNode() {
        if (freeList != -1) {
            int index = freeList;
            freeList = nodes[index].next;
            nodes[index] = Node();
            return index;
        }
        nodes.emplace_back();
        return (int)nodes.size() - 1;
    }

    void FreeNode(int index) {
        nodes[index].next = freeList;
        nodes[index].parentIndex = -1;
        nodes[index].objectIndex = -1;
        nodes[index].isLeaf = false;
        freeList = index;
    }

    int InsertLeaf(int objIdx, const AABB& box) {
        int leaf = AllocateNode();
        // Добавляем запас (Fat AABB)
        nodes[leaf].box = { box.lowerBound - margin, box.upperBound + margin };
        nodes[leaf].objectIndex = objIdx;
        nodes[leaf].isLeaf = true;

        if (rootIndex == -1) { rootIndex = leaf; return leaf; }

        int sibling = rootIndex;
        while (!nodes[sibling].isLeaf) {
            int c1 = nodes[sibling].child1, c2 = nodes[sibling].child2;
            float area = AABB::Area(nodes[sibling].box);
            float combinedArea = AABB::Area(AABB::Union(nodes[sibling].box, box));
            float cost = 2.0f * combinedArea;
            float inheritanceCost = 2.0f * (combinedArea - area);

            auto getCost = [&](int c) {
                float res = AABB::Area(AABB::Union(nodes[c].box, box)) + inheritanceCost;
                if (!nodes[c].isLeaf) res -= AABB::Area(nodes[c].box);
                return res;
            };

            float cost1 = getCost(c1), cost2 = getCost(c2);
            if (cost < cost1 && cost < cost2) break;
            sibling = (cost1 < cost2) ? c1 : c2;
        }

        int oldP = nodes[sibling].parentIndex;
        int newP = AllocateNode();
        nodes[newP].parentIndex = oldP;
        nodes[newP].box = AABB::Union(box, nodes[sibling].box);
        nodes[newP].child1 = sibling; nodes[newP].child2 = leaf;
        nodes[sibling].parentIndex = newP; nodes[leaf].parentIndex = newP;

        if (oldP != -1) {
            if (nodes[oldP].child1 == sibling) nodes[oldP].child1 = newP;
            else nodes[oldP].child2 = newP;
        } else rootIndex = newP;

        SyncHierarchy(nodes[leaf].parentIndex);
        return leaf;
    }

    void RemoveLeaf(int leaf) {
        if (leaf == rootIndex) { rootIndex = -1; FreeNode(leaf); return; }
        int p = nodes[leaf].parentIndex, gp = nodes[p].parentIndex;
        int sib = (nodes[p].child1 == leaf) ? nodes[p].child2 : nodes[p].child1;
        if (gp != -1) {
            if (nodes[gp].child1 == p) nodes[gp].child1 = sib; else nodes[gp].child2 = sib;
            nodes[sib].parentIndex = gp; FreeNode(p); SyncHierarchy(gp);
        } else {
            rootIndex = sib; nodes[sib].parentIndex = -1; FreeNode(p);
        }
        FreeNode(leaf);
    }

private:
    void SyncHierarchy(int i) {
        while (i != -1) {
            i = Balance(i);
            UpdateNode(i); // Явно обновляем высоту и AABB
            i = nodes[i].parentIndex;
        }
    }

    void UpdateNode(int i) {
        int c1 = nodes[i].child1;
        int c2 = nodes[i].child2;
        nodes[i].box = AABB::Union(nodes[c1].box, nodes[c2].box);
        nodes[i].height = 1 + std::max(nodes[c1].height, nodes[c2].height);
    }

    int Balance(int i) {
        if (nodes[i].isLeaf || nodes[i].height < 2) return i;

        int c1 = nodes[i].child1;
        int c2 = nodes[i].child2;
        int balance = nodes[c2].height - nodes[c1].height;

        // Rotate C2 up
        if (balance > 1) {
            int r = c2;
            int rl = nodes[r].child1;
            int rr = nodes[r].child2;

            nodes[r].child1 = i;
            nodes[r].parentIndex = nodes[i].parentIndex;
            nodes[i].parentIndex = r;

            if (nodes[r].parentIndex != -1) {
                if (nodes[nodes[r].parentIndex].child1 == i) nodes[nodes[r].parentIndex].child1 = r;
                else nodes[nodes[r].parentIndex].child2 = r;
            } else rootIndex = r;

            if (nodes[rl].height > nodes[rr].height) {
                nodes[r].child2 = rl;
                nodes[i].child2 = rr;
                nodes[rr].parentIndex = i;
                UpdateNode(i);
                UpdateNode(r);
            } else {
                nodes[r].child2 = rr;
                nodes[i].child2 = rl;
                nodes[rl].parentIndex = i;
                UpdateNode(i);
                UpdateNode(r);
            }
            return r;
        }

        // Rotate C1 up
        if (balance < -1) {
            int l = c1;
            int ll = nodes[l].child1;
            int lr = nodes[l].child2;

            nodes[l].child1 = i;
            nodes[l].parentIndex = nodes[i].parentIndex;
            nodes[i].parentIndex = l;

            if (nodes[l].parentIndex != -1) {
                if (nodes[nodes[l].parentIndex].child1 == i) nodes[nodes[l].parentIndex].child1 = l;
                else nodes[nodes[l].parentIndex].child2 = l;
            } else rootIndex = l;

            if (nodes[ll].height > nodes[lr].height) {
                nodes[l].child2 = ll;
                nodes[i].child1 = lr;
                nodes[lr].parentIndex = i;
                UpdateNode(i);
                UpdateNode(l);
            } else {
                nodes[l].child2 = lr;
                nodes[i].child1 = ll;
                nodes[ll].parentIndex = i;
                UpdateNode(i);
                UpdateNode(l);
            }
            return l;
        }

        return i;
    }
};

// --- Финальный Класс Мира ---
class World {
public:
    DynamicTree bvh;
    std::map<int, std::unique_ptr<Entity>> registry;
    std::map<int, int> entityToNode;
    int nextId = 0;
    std::vector<int> deletionQueue;
    std::vector<PendingTask> tasks;
    void AddTask(float delay, std::function<void()> action) {
        tasks.push_back({delay, action});
    }

    void UpdateTasks(float dt) {
        for (auto it = tasks.begin(); it != tasks.end();) {
            it->timeLeft -= dt;
            if (it->timeLeft <= 0) {
                it->action(); // Выполняем (например, спавним моба)
                it = tasks.erase(it);
            } else {
                ++it;
            }
        }
    }
    int CreateEntity(glm::vec3 pos, glm::vec3 size, uint32_t cat, uint32_t mask) {
        int id = nextId++;
        auto e = std::make_unique<Entity>(id, pos, size, cat, mask);
        entityToNode[id] = bvh.InsertLeaf(id, e->getAABB());
        registry[id] = std::move(e);
        return id;
    }

    void UpdatePosition(int id, glm::vec3 newPos) {
        auto& e = registry[id];
        e->position = newPos;
        AABB real = e->getAABB();
        int nodeIdx = entityToNode[id];
        if (!bvh.nodes[nodeIdx].box.Contains(real)) {
            bvh.RemoveLeaf(nodeIdx);
            entityToNode[id] = bvh.InsertLeaf(id, real);
        }
    }
    void Query(const AABB& box, std::vector<int>& out) const {
        if (bvh.rootIndex == -1) return;
        Stack<int> s; s.Push(bvh.rootIndex);
        while (!s.IsEmpty()) {
            int i = s.Pop();
            const Node& n = bvh.nodes[i];
            if (!(n.box.lowerBound.x <= box.upperBound.x && n.box.upperBound.x >= box.lowerBound.x &&
                n.box.lowerBound.y <= box.upperBound.y && n.box.upperBound.y >= box.lowerBound.y &&
                n.box.lowerBound.z <= box.upperBound.z && n.box.upperBound.z >= box.lowerBound.z)) continue;
            if (n.isLeaf) out.push_back(n.objectIndex);
            else { s.Push(n.child1); s.Push(n.child2); }
        }
    }

    int Raycast(glm::vec3 p1, glm::vec3 p2, uint32_t mask) const {
        if (bvh.rootIndex == -1) return -1;
        glm::vec3 dir = glm::normalize(p2 - p1);
        glm::vec3 invDir = 1.0f / (dir + 1e-9f);
        Stack<int> s; s.Push(bvh.rootIndex);
        int hitId = -1; float minDist = 1e9f;
        while (!s.IsEmpty()) {
            int i = s.Pop();
            const Node& n = bvh.nodes[i];
            glm::vec3 t0 = (n.box.lowerBound - p1) * invDir, t1 = (n.box.upperBound - p1) * invDir;
            float tmin = glm::max(glm::max(glm::min(t0.x, t1.x), glm::min(t0.y, t1.y)), glm::min(t0.z, t1.z));
            float tmax = glm::min(glm::min(glm::max(t0.x, t1.x), glm::max(t0.y, t1.y)), glm::max(t0.z, t1.z));
            if (tmax < tmin || tmax < 0) continue;
            if (n.isLeaf) {
                if (registry.at(n.objectIndex)->category & mask) {
                    float d = glm::distance(p1, registry.at(n.objectIndex)->position);
                    if (d < minDist) { minDist = d; hitId = n.objectIndex; }
                }
            } else { s.Push(n.child1); s.Push(n.child2); }
        }
        return hitId;
    }
    //canmoveto
    bool CanMoveTo(int entityId, glm::vec3 targetPos) const {
        if (registry.find(entityId) == registry.end()) return false;

        const auto& entity = registry.at(entityId);
        // Создаем временный AABB для новой позиции (без запаса margin)
        AABB targetBox = {
            targetPos - (entity->size * 0.5f),
            targetPos + (entity->size * 0.5f)
        };

        std::vector<int> collisions;
        Query(targetBox, collisions);

        for (int hitId : collisions) {
            if (hitId == entityId) continue; // Игнорируем себя

            const auto& target = registry.at(hitId);
            // Проверяем: совпадает ли категория цели с маской сущности?
            if (target->category & entity->mask) {
                return false; // Путь прегражден (например, LAYER_STATIC)
            }
        }
        return true; // Путь свободен
    }
    //frustum
    void QueryFrustum(const std::vector<Plane>& frustumPlanes, std::vector<int>& outVisible) const {
        if (bvh.rootIndex == -1) return;

        Stack<int> s;
        s.Push(bvh.rootIndex);

        while (!s.IsEmpty()) {
            int i = s.Pop();
            const Node& n = bvh.nodes[i];

            // 1. Проверяем: виден ли бокс узла хотя бы одной плоскостью?
            bool visible = true;
            for (const auto& plane : frustumPlanes) {
                if (!plane.IsInFront(n.box)) {
                    visible = false; // Бокс полностью за плоскостью
                    break;
                }
            }

            if (visible) {
                if (n.isLeaf) {
                    outVisible.push_back(n.objectIndex);
                } else {
                    s.Push(n.child1);
                    s.Push(n.child2);
                }
            }
        }
    }
    glm::vec3 ResolveOverlap(int entityId) {
        auto& ent = registry[entityId];
        AABB box = ent->getAABB();
        std::vector<int> neighbors;
        Query(box, neighbors);

        glm::vec3 totalPush(0.0f);

        for (int otherId : neighbors) {
            if (otherId == entityId) continue;

            auto& other = registry[otherId];
            // Выталкиваемся только от стен или других живых существ
            if (!(other->category & (LAYER_STATIC | LAYER_ENEMY | LAYER_PLAYER))) continue;

            AABB otherBox = other->getAABB();

            // Вычисляем глубину проникновения по каждой оси
            float dx1 = otherBox.upperBound.x - box.lowerBound.x;
            float dx2 = box.upperBound.x - otherBox.lowerBound.x;
            float dy1 = otherBox.upperBound.y - box.lowerBound.y;
            float dy2 = box.upperBound.y - otherBox.lowerBound.y;
            float dz1 = otherBox.upperBound.z - box.lowerBound.z;
            float dz2 = box.upperBound.z - otherBox.lowerBound.z;

            // Находим минимальную глубину (ось наименьшего сопротивления)
            float minX = std::min(dx1, dx2);
            float minY = std::min(dy1, dy2);
            float minZ = std::min(dz1, dz2);

            if (minX < minY && minX < minZ) {
                totalPush.x += (dx1 < dx2) ? minX : -minX;
            } else if (minY < minX && minY < minZ) {
                totalPush.y += (dy1 < dy2) ? minY : -minY;
            } else {
                totalPush.z += (dz1 < dz2) ? minZ : -minZ;
            }
        }
        return totalPush;
    }
    void MarkForDeletion(int id) {
        if (registry.count(id) && !registry[id]->gameplay.markedForDeletion) {
            registry[id]->gameplay.markedForDeletion = true;
            deletionQueue.push_back(id);
        }
    }

    // Очистка в конце каждого кадра
    void Cleanup() {
        for (int id : deletionQueue) {
            if (entityToNode.count(id)) {
                bvh.RemoveLeaf(entityToNode[id]);
                entityToNode.erase(id);
            }
            registry.erase(id);
        }
        deletionQueue.clear();
    }
};



int main(){
    World world;

    std::cout << std::fixed << std::setprecision(2);
    std::cout << "=== ИНИЦИАЛИЗАЦИЯ МИРА ===\n";

    // 1. Создаем статичную стену
    int wallId = world.CreateEntity({10.0f, 0.0f, 0.0f}, {1.0f, 10.0f, 10.0f}, LAYER_STATIC, LAYER_NONE);
    std::cout << "[World]: Создана стена ID: " << wallId << " на x=10\n";

    // 2. Создаем ядовитую зону (Trigger)
    int poisonZone = world.CreateEntity({5.0f, 0.0f, 0.0f}, {2.0f, 2.0f, 2.0f}, LAYER_TRIGGER, LAYER_NONE);
    world.registry[poisonZone]->onTrigger = [](int visitorId) {
        std::cout << "  [EVENT]: Объект " << visitorId << " вступил в ЯДОВИТУЮ ЗОНУ! -10 HP\n";
    };

    // 3. Создаем рычаг (Trigger)
    int leverId = world.CreateEntity({8.0f, 0.0f, 2.0f}, {0.5f, 0.5f, 0.5f}, LAYER_TRIGGER, LAYER_NONE);
    world.registry[leverId]->onInteract = []() {
        std::cout << "  [INTERACT]: Рычаг нажат! Секретная дверь открыта.\n";
    };

    // 4. Создаем игрока
    int playerId = world.CreateEntity({0.0f, 0.0f, 0.0f}, {0.6f, 1.8f, 0.6f}, LAYER_PLAYER, LAYER_STATIC | LAYER_TRIGGER);
    std::cout << "[World]: Игрок создан ID: " << playerId << "\n\n";

    // --- ТЕСТ 1: ДВИЖЕНИЕ СКВОЗЬ ТРИГГЕР ---
    std::cout << "=== ТЕСТ 1: ДВИЖЕНИЕ СКВОЗЬ ТРИГГЕР ===\n";
    for (float x = 0.0f; x <= 6.0f; x += 2.0f) {
        world.UpdatePosition(playerId, {x, 0.0f, 0.0f});
        std::cout << "Игрок переместился в x=" << x << "\n";

        // Проверяем сенсоры (триггеры) вокруг игрока
        std::vector<int> nearby;
        world.Query(world.registry[playerId]->getAABB(), nearby);
        for (int id : nearby) {
            if (id != playerId && world.registry[id]->onTrigger) {
                world.registry[id]->onTrigger(playerId);
            }
        }
    }

    // --- ТЕСТ 2: ВЗАИМОДЕЙСТВИЕ (RAYCAST) ---
    std::cout << "\n=== ТЕСТ 2: ВЗАИМОДЕЙСТВИЕ (RAYCAST) ===\n";
    glm::vec3 cameraPos = {7.0f, 0.0f, 2.0f}; // Встали рядом с рычагом
    glm::vec3 lookDir = {1.0f, 0.0f, 0.0f};   // Смотрим в сторону рычага

    std::cout << "Игрок смотрит вперед из точки (7, 0, 2)...\n";
    int hit = world.Raycast(cameraPos, cameraPos + lookDir * 5.0f, LAYER_TRIGGER);

    if (hit != -1 && world.registry[hit]->onInteract) {
        std::cout << "Найден объект для взаимодействия: " << hit << "\n";
        world.registry[hit]->onInteract();
    }

    // --- ТЕСТ 3: СТОЛКНОВЕНИЕ СО СТЕНОЙ ---
    std::cout << "\n=== ТЕСТ 3: КОЛЛИЗИЯ СО СТЕНОЙ ===\n";
    glm::vec3 nextPos = {10.0f, 0.0f, 0.0f}; // Пытаемся зайти в стену
    int wallHit = world.Raycast(world.registry[playerId]->position, nextPos, LAYER_STATIC);

    if (wallHit != -1) {
        std::cout << "[BLOCK]: Движение заблокировано! Впереди стена ID: " << wallHit << "\n";
    }
    for (auto& [id, ent] : world.registry) {
        int nodeIdx = world.entityToNode[id];
        world.bvh.RemoveLeaf(nodeIdx);
    }
    //--- ТЕСТ 4: Очистка ---
    std::cout << "\n=== ТЕСТ 4: Очистка ===\n";
    world.registry.clear();     // Очистка данных
    world.entityToNode.clear(); // Очистка связей
    world.nextId = 0;           // Сброс счетчика ID
    world.Cleanup();
    std::cout << "[LOG]: All entities removed. Root: " << world.bvh.rootIndex << "\n";
    std::cout << "\n=== ВСЕ ТЕСТЫ ЗАВЕРШЕНЫ ===\n";
    return 0;
}

Здесь есть параметры на передвижение для платформ, и параметры типа сущностей. Так же используются триггеры, и небольшой тест как это работает.

Чтобы это визуализировать надо собрать точки - в координатах, в которых должны появиться коллайдеры( геометрия - например куб, этот вид отрисовки показывает тот самый ограничивающий обьём обьекта в сцене, тоесть визуализация обьекта по которому будет происходить расчет столкновения - обычно его не рисуют, а рисуют основную модель с анимациями или статичную модель ) для расчета столкновений, внутри обновления отрисовки можно вызывать сбор(Query - механика "сбора" работает тоже в ограниченном обьеме), обновляя состояние мира. Это минимальный пример, возможно можно сделать и лучше, но у меня пока так и меня впечатляет работа на 40 сущностях.

Пример игрового цикла с бесконечным спавном

Что мне понравилось, в такой ситуации если в пучке до 100 сущностей, утечки мной не обнаружено. Такой подход структурирует сразу с самых первых строк, в сравнении с наивными попытками.

В написании кода я пользовался поддержкой ИИ.

Итог

Это мощная база для создания прототипа или для того как включить физику в другом движке, например буллет. Благодаря балансировке и SAH-подобной вставке, поиск объектов остается быстрым даже при большом количестве сущностей.

Разработка через физический слой позволяет сразу видеть логику мира. Следующим логичным шагом станет внедрение многопоточности и распределение дерева на чанки(эту категорию чанков я пока называю пучок) для действительно бесконечных масштабов.

Комментарии (0)