From 1b72a1c9923b42301aa2f0fcc987754087ce0d25 Mon Sep 17 00:00:00 2001 From: ChestnutYueyue <952134128@qq.com> Date: Thu, 12 Feb 2026 22:31:34 +0800 Subject: [PATCH] =?UTF-8?q?refactor(=E6=9E=84=E5=BB=BA=E7=B3=BB=E7=BB=9F):?= =?UTF-8?q?=20=E7=A7=BB=E9=99=A4=E7=A4=BA=E4=BE=8B=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E4=B8=AD=E7=9A=84=E5=86=97=E4=BD=99=E8=B5=84=E6=BA=90=E6=89=93?= =?UTF-8?q?=E5=8C=85=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refactor(文档): 更新节点系统教程避免双重引用问题 fix(四叉树): 修复碰撞检测中的缓冲区越界问题 refactor(空间索引示例): 使用 getChildren() 替代私有 vector 存储节点 style(精灵批处理): 调整代码格式和初始化顺序 --- .../src/graphics/opengl/gl_sprite_batch.cpp | 57 +++++++------ Extra2D/src/spatial/quadtree.cpp | 16 +++- docs/API_Tutorial/03_Node_System.md | 36 ++++++++ docs/API_Tutorial/06_Collision_Detection.md | 48 ++++++++--- examples/collision_demo/xmake.lua | 15 ---- examples/flappy_bird/xmake.lua | 15 ---- examples/hello_world/xmake.lua | 15 ---- examples/push_box/xmake.lua | 15 ---- examples/spatial_index_demo/main.cpp | 84 ++++++++++++------- examples/spatial_index_demo/xmake.lua | 15 ---- 10 files changed, 173 insertions(+), 143 deletions(-) diff --git a/Extra2D/src/graphics/opengl/gl_sprite_batch.cpp b/Extra2D/src/graphics/opengl/gl_sprite_batch.cpp index ef9cc86..bcbd9af 100644 --- a/Extra2D/src/graphics/opengl/gl_sprite_batch.cpp +++ b/Extra2D/src/graphics/opengl/gl_sprite_batch.cpp @@ -53,8 +53,8 @@ void main() { )"; GLSpriteBatch::GLSpriteBatch() - : vao_(0), vbo_(0), ibo_(0), currentTexture_(nullptr), currentIsSDF_(false), - vertexCount_(0), drawCallCount_(0), spriteCount_(0), batchCount_(0) { + : vao_(0), vbo_(0), ibo_(0), vertexCount_(0), currentTexture_(nullptr), + currentIsSDF_(false), drawCallCount_(0), spriteCount_(0), batchCount_(0) { } GLSpriteBatch::~GLSpriteBatch() { shutdown(); } @@ -111,7 +111,8 @@ bool GLSpriteBatch::init() { glBindVertexArray(0); - E2D_LOG_INFO("GLSpriteBatch initialized with capacity for {} sprites", MAX_SPRITES); + E2D_LOG_INFO("GLSpriteBatch initialized with capacity for {} sprites", + MAX_SPRITES); return true; } @@ -140,18 +141,17 @@ void GLSpriteBatch::begin(const glm::mat4 &viewProjection) { batchCount_ = 0; } -bool GLSpriteBatch::needsFlush(const Texture& texture, bool isSDF) const { +bool GLSpriteBatch::needsFlush(const Texture &texture, bool isSDF) const { if (currentTexture_ == nullptr) { return false; } - + // 检查是否需要刷新:纹理改变、SDF 状态改变或缓冲区已满 - return (currentTexture_ != &texture) || - (currentIsSDF_ != isSDF) || + return (currentTexture_ != &texture) || (currentIsSDF_ != isSDF) || (vertexCount_ + VERTICES_PER_SPRITE > MAX_VERTICES); } -void GLSpriteBatch::addVertices(const SpriteData& data) { +void GLSpriteBatch::addVertices(const SpriteData &data) { // 计算变换后的顶点位置 glm::vec2 anchorOffset(data.size.x * data.anchor.x, data.size.y * data.anchor.y); @@ -172,10 +172,14 @@ void GLSpriteBatch::addVertices(const SpriteData& data) { // v0(左上) -- v1(右上) // | | // v3(左下) -- v2(右下) - Vertex v0{transform(0, 0), glm::vec2(data.texCoordMin.x, data.texCoordMin.y), color}; - Vertex v1{transform(data.size.x, 0), glm::vec2(data.texCoordMax.x, data.texCoordMin.y), color}; - Vertex v2{transform(data.size.x, data.size.y), glm::vec2(data.texCoordMax.x, data.texCoordMax.y), color}; - Vertex v3{transform(0, data.size.y), glm::vec2(data.texCoordMin.x, data.texCoordMax.y), color}; + Vertex v0{transform(0, 0), glm::vec2(data.texCoordMin.x, data.texCoordMin.y), + color}; + Vertex v1{transform(data.size.x, 0), + glm::vec2(data.texCoordMax.x, data.texCoordMin.y), color}; + Vertex v2{transform(data.size.x, data.size.y), + glm::vec2(data.texCoordMax.x, data.texCoordMax.y), color}; + Vertex v3{transform(0, data.size.y), + glm::vec2(data.texCoordMin.x, data.texCoordMax.y), color}; vertexBuffer_[vertexCount_++] = v0; vertexBuffer_[vertexCount_++] = v1; @@ -196,50 +200,52 @@ void GLSpriteBatch::draw(const Texture &texture, const SpriteData &data) { spriteCount_++; } -void GLSpriteBatch::drawBatch(const Texture& texture, const std::vector& sprites) { +void GLSpriteBatch::drawBatch(const Texture &texture, + const std::vector &sprites) { if (sprites.empty()) { return; } - + // 如果当前有未提交的批次且纹理不同,先刷新 if (currentTexture_ != nullptr && currentTexture_ != &texture) { flush(); } - + currentTexture_ = &texture; currentIsSDF_ = sprites[0].isSDF; // 假设批量中的精灵 SDF 状态一致 - + // 分批处理,避免超过缓冲区大小 size_t index = 0; while (index < sprites.size()) { size_t remainingSpace = (MAX_VERTICES - vertexCount_) / VERTICES_PER_SPRITE; size_t batchSize = std::min(sprites.size() - index, remainingSpace); - + for (size_t i = 0; i < batchSize; ++i) { addVertices(sprites[index + i]); spriteCount_++; } - + index += batchSize; - + // 如果还有更多精灵,刷新当前批次 if (index < sprites.size()) { flush(); } } - + batchCount_++; } -void GLSpriteBatch::drawImmediate(const Texture& texture, const SpriteData& data) { +void GLSpriteBatch::drawImmediate(const Texture &texture, + const SpriteData &data) { // 立即绘制,不缓存 - 用于需要立即显示的情况 flush(); // 先提交当前批次 - + currentTexture_ = &texture; currentIsSDF_ = data.isSDF; addVertices(data); spriteCount_++; - + flush(); // 立即提交 } @@ -270,7 +276,8 @@ void GLSpriteBatch::flush() { // 更新 VBO 数据 - 只更新实际使用的部分 glBindBuffer(GL_ARRAY_BUFFER, vbo_); - glBufferSubData(GL_ARRAY_BUFFER, 0, vertexCount_ * sizeof(Vertex), vertexBuffer_.data()); + glBufferSubData(GL_ARRAY_BUFFER, 0, vertexCount_ * sizeof(Vertex), + vertexBuffer_.data()); // 绘制 glBindVertexArray(vao_); @@ -280,7 +287,7 @@ void GLSpriteBatch::flush() { drawCallCount_++; batchCount_++; - + // 重置状态 vertexCount_ = 0; currentTexture_ = nullptr; diff --git a/Extra2D/src/spatial/quadtree.cpp b/Extra2D/src/spatial/quadtree.cpp index 115c83e..5d6672e 100644 --- a/Extra2D/src/spatial/quadtree.cpp +++ b/Extra2D/src/spatial/quadtree.cpp @@ -248,9 +248,12 @@ void QuadTree::collectCollisions( if (!current) continue; + // 确保 ancestorEnd 不超过当前 buffer 大小 + size_t validAncestorEnd = std::min(item.ancestorEnd, collisionBuffer_.size()); + // 检测当前节点对象与祖先对象的碰撞 for (const auto &[obj, bounds] : current->objects) { - for (size_t i = item.ancestorStart; i < item.ancestorEnd; ++i) { + for (size_t i = item.ancestorStart; i < validAncestorEnd; ++i) { const auto &[ancestorObj, ancestorBounds] = collisionBuffer_[i]; if (bounds.intersects(ancestorBounds)) { collisions.emplace_back(ancestorObj, obj); @@ -277,10 +280,15 @@ void QuadTree::collectCollisions( } // 恢复祖先列表(模拟递归返回) - if (stack.empty() || - (stack.back().ancestorStart != oldSize && - stack.back().ancestorEnd != collisionBuffer_.size())) { + // 只有当栈顶元素的祖先范围与当前不同时,才需要恢复 + if (stack.empty()) { collisionBuffer_.resize(oldSize); + } else { + const auto& nextItem = stack.back(); + // 如果下一个节点的祖先范围与当前不同,则需要恢复到其祖先起始位置 + if (nextItem.ancestorStart != oldSize) { + collisionBuffer_.resize(oldSize); + } } } } diff --git a/docs/API_Tutorial/03_Node_System.md b/docs/API_Tutorial/03_Node_System.md index 4122c6c..00525d8 100644 --- a/docs/API_Tutorial/03_Node_System.md +++ b/docs/API_Tutorial/03_Node_System.md @@ -528,6 +528,42 @@ void GameOverLayer::initPanel(int score, float screenHeight) { - 引擎会自动在渲染前调用 - 如果需要强制更新变换,可以手动调用 +7. **避免双重引用**: + - 节点通过 `addChild()` 添加到场景后,由场景统一管理 + - **不要**额外存储 `shared_ptr` 到 vector 中,避免双重引用问题 + - 使用 `getChildren()` 访问子节点,配合 `dynamic_cast` 筛选特定类型 + + ```cpp + // ❌ 错误:双重引用 + class BadScene : public Scene { + private: + std::vector> sprites_; // 不要这样做! + public: + void createSprite() { + auto sprite = Sprite::create(texture); + addChild(sprite); + sprites_.push_back(sprite); // 双重引用! + } + }; + + // ✅ 正确:通过 getChildren() 访问 + class GoodScene : public Scene { + public: + void createSprite() { + auto sprite = Sprite::create(texture); + addChild(sprite); // 场景统一管理 + } + + void updateSprites() { + for (const auto& child : getChildren()) { + if (auto sprite = dynamic_cast(child.get())) { + // 处理 sprite + } + } + } + }; + ``` + ## 下一步 - [04. 资源管理](./04_Resource_Management.md) - 深入了解资源加载 diff --git a/docs/API_Tutorial/06_Collision_Detection.md b/docs/API_Tutorial/06_Collision_Detection.md index 1fd85bf..9347d26 100644 --- a/docs/API_Tutorial/06_Collision_Detection.md +++ b/docs/API_Tutorial/06_Collision_Detection.md @@ -64,8 +64,10 @@ public: Scene::onUpdate(dt); // 清除之前的碰撞状态 - for (auto& box : boxes_) { - box->setColliding(false); + for (const auto& child : getChildren()) { + if (auto box = dynamic_cast(child.get())) { + box->setColliding(false); + } } // 使用场景的空间索引查询所有碰撞 @@ -82,11 +84,16 @@ public: } } -private: - std::vector> boxes_; + void createBox(float x, float y) { + auto box = makePtr(50.0f, 50.0f, Color(0.3f, 0.7f, 1.0f, 0.8f)); + box->setPosition(Vec2(x, y)); + addChild(box); // 通过 addChild 管理节点生命周期 + } }; ``` +**注意**:节点通过 `addChild()` 添加到场景后,由场景统一管理生命周期。不要额外存储 `shared_ptr` 到 vector 中,避免双重引用问题。 + ## 空间索引策略 ### 切换策略 @@ -123,18 +130,19 @@ public: Scene::onEnter(); // 创建100个碰撞节点 - createNodes(100); + createPhysicsNodes(100); - E2D_LOG_INFO("创建了 {} 个碰撞节点", nodes_.size()); E2D_LOG_INFO("空间索引已启用: {}", isSpatialIndexingEnabled()); } void onUpdate(float dt) override { Scene::onUpdate(dt); - // 更新所有节点位置 - for (auto& node : nodes_) { - node->update(dt, screenWidth_, screenHeight_); + // 更新所有物理节点位置(通过 getChildren() 访问) + for (const auto& child : getChildren()) { + if (auto node = dynamic_cast(child.get())) { + node->update(dt, screenWidth_, screenHeight_); + } } // 使用空间索引进行碰撞检测 @@ -150,8 +158,10 @@ public: private: void performCollisionDetection() { // 清除之前的碰撞状态 - for (auto& node : nodes_) { - node->setColliding(false); + for (const auto& child : getChildren()) { + if (auto node = dynamic_cast(child.get())) { + node->setColliding(false); + } } // 使用引擎自带的空间索引进行碰撞检测 @@ -180,9 +190,25 @@ private: E2D_LOG_INFO("切换到四叉树策略"); } } + + // 获取物理节点数量 + size_t getPhysicsNodeCount() const { + size_t count = 0; + for (const auto& child : getChildren()) { + if (dynamic_cast(child.get())) { + ++count; + } + } + return count; + } }; ``` +**关键改进**: +- 使用 `getChildren()` 代替私有 vector 存储节点引用 +- 通过 `dynamic_cast` 筛选特定类型的子节点 +- 避免双重引用,简化生命周期管理 + ## 关键要点 1. **必须调用 `setSpatialIndexed(true)`** - 启用节点的空间索引 diff --git a/examples/collision_demo/xmake.lua b/examples/collision_demo/xmake.lua index f8a1fd9..abf2f2e 100644 --- a/examples/collision_demo/xmake.lua +++ b/examples/collision_demo/xmake.lua @@ -72,20 +72,5 @@ target("collision_demo") print("Warning: romfs directory not found at " .. romfs) end end) - - -- 打包时将资源复制到 package 目录 - after_package(function (target) - local target_dir = path.directory(target:targetfile()) - local assets_dir = path.join(target_dir, "assets") - local package_dir = target:packagedir() - if os.isdir(assets_dir) and package_dir then - local package_assets = path.join(package_dir, "assets") - if not os.isdir(package_assets) then - os.mkdir(package_assets) - end - os.cp(path.join(assets_dir, "**"), package_assets) - print("Copied assets to package: " .. package_assets) - end - end) end target_end() diff --git a/examples/flappy_bird/xmake.lua b/examples/flappy_bird/xmake.lua index 5984382..9959fce 100644 --- a/examples/flappy_bird/xmake.lua +++ b/examples/flappy_bird/xmake.lua @@ -73,20 +73,5 @@ target("flappy_bird") print("Warning: romfs directory not found at " .. romfs) end end) - - -- 打包时将资源复制到 package 目录 - after_package(function (target) - local target_dir = path.directory(target:targetfile()) - local assets_dir = path.join(target_dir, "assets") - local package_dir = target:packagedir() - if os.isdir(assets_dir) and package_dir then - local package_assets = path.join(package_dir, "assets") - if not os.isdir(package_assets) then - os.mkdir(package_assets) - end - os.cp(path.join(assets_dir, "**"), package_assets) - print("Copied assets to package: " .. package_assets) - end - end) end target_end() diff --git a/examples/hello_world/xmake.lua b/examples/hello_world/xmake.lua index 3c71906..8998521 100644 --- a/examples/hello_world/xmake.lua +++ b/examples/hello_world/xmake.lua @@ -72,20 +72,5 @@ target("hello_world") print("Warning: romfs directory not found at " .. romfs) end end) - - -- 打包时将资源复制到 package 目录 - after_package(function (target) - local target_dir = path.directory(target:targetfile()) - local assets_dir = path.join(target_dir, "assets") - local package_dir = target:packagedir() - if os.isdir(assets_dir) and package_dir then - local package_assets = path.join(package_dir, "assets") - if not os.isdir(package_assets) then - os.mkdir(package_assets) - end - os.cp(path.join(assets_dir, "**"), package_assets) - print("Copied assets to package: " .. package_assets) - end - end) end target_end() diff --git a/examples/push_box/xmake.lua b/examples/push_box/xmake.lua index 9d680df..f550124 100644 --- a/examples/push_box/xmake.lua +++ b/examples/push_box/xmake.lua @@ -72,20 +72,5 @@ target("push_box") print("Warning: romfs directory not found at " .. romfs) end end) - - -- 打包时将资源复制到 package 目录 - after_package(function (target) - local target_dir = path.directory(target:targetfile()) - local assets_dir = path.join(target_dir, "assets") - local package_dir = target:packagedir() - if os.isdir(assets_dir) and package_dir then - local package_assets = path.join(package_dir, "assets") - if not os.isdir(package_assets) then - os.mkdir(package_assets) - end - os.cp(path.join(assets_dir, "**"), package_assets) - print("Copied assets to package: " .. package_assets) - end - end) end target_end() diff --git a/examples/spatial_index_demo/main.cpp b/examples/spatial_index_demo/main.cpp index 2f95c09..4fa89d8 100644 --- a/examples/spatial_index_demo/main.cpp +++ b/examples/spatial_index_demo/main.cpp @@ -1,8 +1,5 @@ -#include #include -#include #include -#include using namespace extra2d; @@ -106,19 +103,15 @@ public: setBackgroundColor(Color(0.05f, 0.05f, 0.1f, 1.0f)); // 创建100个碰撞节点 - createNodes(100); + createPhysicsNodes(100); // 加载字体 loadFonts(); - E2D_LOG_INFO("创建了 {} 个碰撞节点", nodes_.size()); E2D_LOG_INFO("空间索引已启用: {}", isSpatialIndexingEnabled()); } void onExit() override { - // 先清理 nodes_ 向量 - nodes_.clear(); - // 显式移除所有子节点,确保在场景析构前正确清理空间索引 // 这必须在 Scene::onExit() 之前调用,因为 onExit() 会将 running_ 设为 false removeAllChildren(); @@ -131,9 +124,11 @@ public: auto startTime = std::chrono::high_resolution_clock::now(); - // 更新所有节点位置 - for (auto &node : nodes_) { - node->update(dt, screenWidth_, screenHeight_); + // 更新所有物理节点位置 + for (const auto &child : getChildren()) { + if (auto node = dynamic_cast(child.get())) { + node->update(dt, screenWidth_, screenHeight_); + } } auto updateEndTime = std::chrono::high_resolution_clock::now(); @@ -149,7 +144,8 @@ public: collisionEndTime - updateEndTime) .count(); - stats_.nodeCount = nodes_.size(); + // 统计物理节点数量 + stats_.nodeCount = getPhysicsNodeCount(); // 获取当前使用的空间索引策略 stats_.strategyName = getSpatialManager().getStrategyName(); @@ -310,9 +306,9 @@ private: } /** - * @brief 创建指定数量的节点 + * @brief 创建指定数量的物理节点 */ - void createNodes(size_t count) { + void createPhysicsNodes(size_t count) { std::random_device rd; std::mt19937 gen(rd()); std::uniform_real_distribution posX(50.0f, screenWidth_ - 50.0f); @@ -326,38 +322,70 @@ private: auto node = makePtr(20.0f, color, static_cast(i)); node->setPosition(Vec2(posX(gen), posY(gen))); addChild(node); - nodes_.push_back(node); } } + /** + * @brief 获取物理节点数量 + */ + size_t getPhysicsNodeCount() const { + size_t count = 0; + for (const auto &child : getChildren()) { + if (dynamic_cast(child.get())) { + ++count; + } + } + return count; + } + + /** + * @brief 获取所有物理节点 + */ + std::vector getPhysicsNodes() const { + std::vector nodes; + for (const auto &child : getChildren()) { + if (auto node = dynamic_cast(child.get())) { + nodes.push_back(node); + } + } + return nodes; + } + /** * @brief 添加节点 */ void addNodes(size_t count) { - size_t currentCount = nodes_.size(); + size_t currentCount = getPhysicsNodeCount(); if (currentCount + count > 5000) { E2D_LOG_WARN("节点数量已达上限(5000)"); return; } - createNodes(count); - E2D_LOG_INFO("添加 {} 个节点,当前总数: {}", count, nodes_.size()); + createPhysicsNodes(count); + E2D_LOG_INFO("添加 {} 个节点,当前总数: {}", count, getPhysicsNodeCount()); } /** * @brief 移除节点 */ void removeNodes(size_t count) { - if (count >= nodes_.size()) { - count = nodes_.size(); + auto physicsNodes = getPhysicsNodes(); + if (count >= physicsNodes.size()) { + count = physicsNodes.size(); } if (count == 0) return; + // 从后往前移除指定数量的节点 for (size_t i = 0; i < count; ++i) { - removeChild(nodes_.back()); - nodes_.pop_back(); + // 找到最后一个物理节点对应的子节点并移除 + for (auto it = getChildren().rbegin(); it != getChildren().rend(); ++it) { + if (dynamic_cast(it->get())) { + removeChild(*it); + break; + } + } } - E2D_LOG_INFO("移除 {} 个节点,当前总数: {}", count, nodes_.size()); + E2D_LOG_INFO("移除 {} 个节点,当前总数: {}", count, getPhysicsNodeCount()); } /** @@ -385,8 +413,10 @@ private: */ void performCollisionDetection() { // 清除之前的碰撞状态 - for (auto &node : nodes_) { - node->setColliding(false); + for (const auto &child : getChildren()) { + if (auto node = dynamic_cast(child.get())) { + node->setColliding(false); + } } // 使用引擎自带的空间索引进行碰撞检测 @@ -442,7 +472,6 @@ private: Color(1.0f, 0.2f, 0.2f, 0.9f)); } - std::vector> nodes_; PerformanceStats stats_; float screenWidth_ = 1280.0f; float screenHeight_ = 720.0f; @@ -473,8 +502,7 @@ private: // 程序入口 // ============================================================================ -int main(int argc, char **argv) -{ +int main(int argc, char **argv) { Logger::init(); Logger::setLevel(LogLevel::Debug); diff --git a/examples/spatial_index_demo/xmake.lua b/examples/spatial_index_demo/xmake.lua index e3fb673..307b3e9 100644 --- a/examples/spatial_index_demo/xmake.lua +++ b/examples/spatial_index_demo/xmake.lua @@ -72,20 +72,5 @@ target("spatial_index_demo") print("Warning: romfs directory not found at " .. romfs) end end) - - -- 打包时将资源复制到 package 目录 - after_package(function (target) - local target_dir = path.directory(target:targetfile()) - local assets_dir = path.join(target_dir, "assets") - local package_dir = target:packagedir() - if os.isdir(assets_dir) and package_dir then - local package_assets = path.join(package_dir, "assets") - if not os.isdir(package_assets) then - os.mkdir(package_assets) - end - os.cp(path.join(assets_dir, "**"), package_assets) - print("Copied assets to package: " .. package_assets) - end - end) end target_end()