refactor(构建系统): 移除示例项目中的冗余资源打包逻辑

refactor(文档): 更新节点系统教程避免双重引用问题

fix(四叉树): 修复碰撞检测中的缓冲区越界问题

refactor(空间索引示例): 使用 getChildren() 替代私有 vector 存储节点

style(精灵批处理): 调整代码格式和初始化顺序
This commit is contained in:
ChestnutYueyue 2026-02-12 22:31:34 +08:00
parent 010e48753c
commit 1b72a1c992
10 changed files with 173 additions and 143 deletions

View File

@ -53,8 +53,8 @@ void main() {
)"; )";
GLSpriteBatch::GLSpriteBatch() GLSpriteBatch::GLSpriteBatch()
: vao_(0), vbo_(0), ibo_(0), currentTexture_(nullptr), currentIsSDF_(false), : vao_(0), vbo_(0), ibo_(0), vertexCount_(0), currentTexture_(nullptr),
vertexCount_(0), drawCallCount_(0), spriteCount_(0), batchCount_(0) { currentIsSDF_(false), drawCallCount_(0), spriteCount_(0), batchCount_(0) {
} }
GLSpriteBatch::~GLSpriteBatch() { shutdown(); } GLSpriteBatch::~GLSpriteBatch() { shutdown(); }
@ -111,7 +111,8 @@ bool GLSpriteBatch::init() {
glBindVertexArray(0); 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; return true;
} }
@ -140,18 +141,17 @@ void GLSpriteBatch::begin(const glm::mat4 &viewProjection) {
batchCount_ = 0; batchCount_ = 0;
} }
bool GLSpriteBatch::needsFlush(const Texture& texture, bool isSDF) const { bool GLSpriteBatch::needsFlush(const Texture &texture, bool isSDF) const {
if (currentTexture_ == nullptr) { if (currentTexture_ == nullptr) {
return false; return false;
} }
// 检查是否需要刷新纹理改变、SDF 状态改变或缓冲区已满 // 检查是否需要刷新纹理改变、SDF 状态改变或缓冲区已满
return (currentTexture_ != &texture) || return (currentTexture_ != &texture) || (currentIsSDF_ != isSDF) ||
(currentIsSDF_ != isSDF) ||
(vertexCount_ + VERTICES_PER_SPRITE > MAX_VERTICES); (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, glm::vec2 anchorOffset(data.size.x * data.anchor.x,
data.size.y * data.anchor.y); data.size.y * data.anchor.y);
@ -172,10 +172,14 @@ void GLSpriteBatch::addVertices(const SpriteData& data) {
// v0(左上) -- v1(右上) // v0(左上) -- v1(右上)
// | | // | |
// v3(左下) -- v2(右下) // v3(左下) -- v2(右下)
Vertex v0{transform(0, 0), glm::vec2(data.texCoordMin.x, data.texCoordMin.y), color}; Vertex v0{transform(0, 0), glm::vec2(data.texCoordMin.x, data.texCoordMin.y),
Vertex v1{transform(data.size.x, 0), glm::vec2(data.texCoordMax.x, data.texCoordMin.y), color}; color};
Vertex v2{transform(data.size.x, data.size.y), glm::vec2(data.texCoordMax.x, data.texCoordMax.y), color}; Vertex v1{transform(data.size.x, 0),
Vertex v3{transform(0, data.size.y), glm::vec2(data.texCoordMin.x, data.texCoordMax.y), color}; 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_++] = v0;
vertexBuffer_[vertexCount_++] = v1; vertexBuffer_[vertexCount_++] = v1;
@ -196,7 +200,8 @@ void GLSpriteBatch::draw(const Texture &texture, const SpriteData &data) {
spriteCount_++; spriteCount_++;
} }
void GLSpriteBatch::drawBatch(const Texture& texture, const std::vector<SpriteData>& sprites) { void GLSpriteBatch::drawBatch(const Texture &texture,
const std::vector<SpriteData> &sprites) {
if (sprites.empty()) { if (sprites.empty()) {
return; return;
} }
@ -231,7 +236,8 @@ void GLSpriteBatch::drawBatch(const Texture& texture, const std::vector<SpriteDa
batchCount_++; batchCount_++;
} }
void GLSpriteBatch::drawImmediate(const Texture& texture, const SpriteData& data) { void GLSpriteBatch::drawImmediate(const Texture &texture,
const SpriteData &data) {
// 立即绘制,不缓存 - 用于需要立即显示的情况 // 立即绘制,不缓存 - 用于需要立即显示的情况
flush(); // 先提交当前批次 flush(); // 先提交当前批次
@ -270,7 +276,8 @@ void GLSpriteBatch::flush() {
// 更新 VBO 数据 - 只更新实际使用的部分 // 更新 VBO 数据 - 只更新实际使用的部分
glBindBuffer(GL_ARRAY_BUFFER, 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_); glBindVertexArray(vao_);

View File

@ -248,9 +248,12 @@ void QuadTree::collectCollisions(
if (!current) if (!current)
continue; continue;
// 确保 ancestorEnd 不超过当前 buffer 大小
size_t validAncestorEnd = std::min(item.ancestorEnd, collisionBuffer_.size());
// 检测当前节点对象与祖先对象的碰撞 // 检测当前节点对象与祖先对象的碰撞
for (const auto &[obj, bounds] : current->objects) { 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]; const auto &[ancestorObj, ancestorBounds] = collisionBuffer_[i];
if (bounds.intersects(ancestorBounds)) { if (bounds.intersects(ancestorBounds)) {
collisions.emplace_back(ancestorObj, obj); collisions.emplace_back(ancestorObj, obj);
@ -277,10 +280,15 @@ void QuadTree::collectCollisions(
} }
// 恢复祖先列表(模拟递归返回) // 恢复祖先列表(模拟递归返回)
if (stack.empty() || // 只有当栈顶元素的祖先范围与当前不同时,才需要恢复
(stack.back().ancestorStart != oldSize && if (stack.empty()) {
stack.back().ancestorEnd != collisionBuffer_.size())) {
collisionBuffer_.resize(oldSize); collisionBuffer_.resize(oldSize);
} else {
const auto& nextItem = stack.back();
// 如果下一个节点的祖先范围与当前不同,则需要恢复到其祖先起始位置
if (nextItem.ancestorStart != oldSize) {
collisionBuffer_.resize(oldSize);
}
} }
} }
} }

View File

@ -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<Ptr<Sprite>> 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<Sprite*>(child.get())) {
// 处理 sprite
}
}
}
};
```
## 下一步 ## 下一步
- [04. 资源管理](./04_Resource_Management.md) - 深入了解资源加载 - [04. 资源管理](./04_Resource_Management.md) - 深入了解资源加载

View File

@ -64,9 +64,11 @@ public:
Scene::onUpdate(dt); Scene::onUpdate(dt);
// 清除之前的碰撞状态 // 清除之前的碰撞状态
for (auto& box : boxes_) { for (const auto& child : getChildren()) {
if (auto box = dynamic_cast<CollidableBox*>(child.get())) {
box->setColliding(false); box->setColliding(false);
} }
}
// 使用场景的空间索引查询所有碰撞 // 使用场景的空间索引查询所有碰撞
auto collisions = queryCollisions(); auto collisions = queryCollisions();
@ -82,11 +84,16 @@ public:
} }
} }
private: void createBox(float x, float y) {
std::vector<Ptr<CollidableBox>> boxes_; auto box = makePtr<CollidableBox>(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,19 +130,20 @@ public:
Scene::onEnter(); Scene::onEnter();
// 创建100个碰撞节点 // 创建100个碰撞节点
createNodes(100); createPhysicsNodes(100);
E2D_LOG_INFO("创建了 {} 个碰撞节点", nodes_.size());
E2D_LOG_INFO("空间索引已启用: {}", isSpatialIndexingEnabled()); E2D_LOG_INFO("空间索引已启用: {}", isSpatialIndexingEnabled());
} }
void onUpdate(float dt) override { void onUpdate(float dt) override {
Scene::onUpdate(dt); Scene::onUpdate(dt);
// 更新所有节点位置 // 更新所有物理节点位置(通过 getChildren() 访问)
for (auto& node : nodes_) { for (const auto& child : getChildren()) {
if (auto node = dynamic_cast<PhysicsNode*>(child.get())) {
node->update(dt, screenWidth_, screenHeight_); node->update(dt, screenWidth_, screenHeight_);
} }
}
// 使用空间索引进行碰撞检测 // 使用空间索引进行碰撞检测
performCollisionDetection(); performCollisionDetection();
@ -150,9 +158,11 @@ public:
private: private:
void performCollisionDetection() { void performCollisionDetection() {
// 清除之前的碰撞状态 // 清除之前的碰撞状态
for (auto& node : nodes_) { for (const auto& child : getChildren()) {
if (auto node = dynamic_cast<PhysicsNode*>(child.get())) {
node->setColliding(false); node->setColliding(false);
} }
}
// 使用引擎自带的空间索引进行碰撞检测 // 使用引擎自带的空间索引进行碰撞检测
auto collisions = queryCollisions(); auto collisions = queryCollisions();
@ -180,9 +190,25 @@ private:
E2D_LOG_INFO("切换到四叉树策略"); E2D_LOG_INFO("切换到四叉树策略");
} }
} }
// 获取物理节点数量
size_t getPhysicsNodeCount() const {
size_t count = 0;
for (const auto& child : getChildren()) {
if (dynamic_cast<PhysicsNode*>(child.get())) {
++count;
}
}
return count;
}
}; };
``` ```
**关键改进**
- 使用 `getChildren()` 代替私有 vector 存储节点引用
- 通过 `dynamic_cast` 筛选特定类型的子节点
- 避免双重引用,简化生命周期管理
## 关键要点 ## 关键要点
1. **必须调用 `setSpatialIndexed(true)`** - 启用节点的空间索引 1. **必须调用 `setSpatialIndexed(true)`** - 启用节点的空间索引

View File

@ -72,20 +72,5 @@ target("collision_demo")
print("Warning: romfs directory not found at " .. romfs) print("Warning: romfs directory not found at " .. romfs)
end end
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 end
target_end() target_end()

View File

@ -73,20 +73,5 @@ target("flappy_bird")
print("Warning: romfs directory not found at " .. romfs) print("Warning: romfs directory not found at " .. romfs)
end end
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 end
target_end() target_end()

View File

@ -72,20 +72,5 @@ target("hello_world")
print("Warning: romfs directory not found at " .. romfs) print("Warning: romfs directory not found at " .. romfs)
end end
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 end
target_end() target_end()

View File

@ -72,20 +72,5 @@ target("push_box")
print("Warning: romfs directory not found at " .. romfs) print("Warning: romfs directory not found at " .. romfs)
end end
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 end
target_end() target_end()

View File

@ -1,8 +1,5 @@
#include <cmath>
#include <extra2d/extra2d.h> #include <extra2d/extra2d.h>
#include <iomanip>
#include <random> #include <random>
#include <sstream>
using namespace extra2d; using namespace extra2d;
@ -106,19 +103,15 @@ public:
setBackgroundColor(Color(0.05f, 0.05f, 0.1f, 1.0f)); setBackgroundColor(Color(0.05f, 0.05f, 0.1f, 1.0f));
// 创建100个碰撞节点 // 创建100个碰撞节点
createNodes(100); createPhysicsNodes(100);
// 加载字体 // 加载字体
loadFonts(); loadFonts();
E2D_LOG_INFO("创建了 {} 个碰撞节点", nodes_.size());
E2D_LOG_INFO("空间索引已启用: {}", isSpatialIndexingEnabled()); E2D_LOG_INFO("空间索引已启用: {}", isSpatialIndexingEnabled());
} }
void onExit() override { void onExit() override {
// 先清理 nodes_ 向量
nodes_.clear();
// 显式移除所有子节点,确保在场景析构前正确清理空间索引 // 显式移除所有子节点,确保在场景析构前正确清理空间索引
// 这必须在 Scene::onExit() 之前调用,因为 onExit() 会将 running_ 设为 false // 这必须在 Scene::onExit() 之前调用,因为 onExit() 会将 running_ 设为 false
removeAllChildren(); removeAllChildren();
@ -131,10 +124,12 @@ public:
auto startTime = std::chrono::high_resolution_clock::now(); auto startTime = std::chrono::high_resolution_clock::now();
// 更新所有节点位置 // 更新所有物理节点位置
for (auto &node : nodes_) { for (const auto &child : getChildren()) {
if (auto node = dynamic_cast<PhysicsNode *>(child.get())) {
node->update(dt, screenWidth_, screenHeight_); node->update(dt, screenWidth_, screenHeight_);
} }
}
auto updateEndTime = std::chrono::high_resolution_clock::now(); auto updateEndTime = std::chrono::high_resolution_clock::now();
stats_.updateTime = stats_.updateTime =
@ -149,7 +144,8 @@ public:
collisionEndTime - updateEndTime) collisionEndTime - updateEndTime)
.count(); .count();
stats_.nodeCount = nodes_.size(); // 统计物理节点数量
stats_.nodeCount = getPhysicsNodeCount();
// 获取当前使用的空间索引策略 // 获取当前使用的空间索引策略
stats_.strategyName = getSpatialManager().getStrategyName(); 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::random_device rd;
std::mt19937 gen(rd()); std::mt19937 gen(rd());
std::uniform_real_distribution<float> posX(50.0f, screenWidth_ - 50.0f); std::uniform_real_distribution<float> posX(50.0f, screenWidth_ - 50.0f);
@ -326,38 +322,70 @@ private:
auto node = makePtr<PhysicsNode>(20.0f, color, static_cast<int>(i)); auto node = makePtr<PhysicsNode>(20.0f, color, static_cast<int>(i));
node->setPosition(Vec2(posX(gen), posY(gen))); node->setPosition(Vec2(posX(gen), posY(gen)));
addChild(node); addChild(node);
nodes_.push_back(node);
} }
} }
/**
* @brief
*/
size_t getPhysicsNodeCount() const {
size_t count = 0;
for (const auto &child : getChildren()) {
if (dynamic_cast<PhysicsNode *>(child.get())) {
++count;
}
}
return count;
}
/**
* @brief
*/
std::vector<PhysicsNode *> getPhysicsNodes() const {
std::vector<PhysicsNode *> nodes;
for (const auto &child : getChildren()) {
if (auto node = dynamic_cast<PhysicsNode *>(child.get())) {
nodes.push_back(node);
}
}
return nodes;
}
/** /**
* @brief * @brief
*/ */
void addNodes(size_t count) { void addNodes(size_t count) {
size_t currentCount = nodes_.size(); size_t currentCount = getPhysicsNodeCount();
if (currentCount + count > 5000) { if (currentCount + count > 5000) {
E2D_LOG_WARN("节点数量已达上限(5000)"); E2D_LOG_WARN("节点数量已达上限(5000)");
return; return;
} }
createNodes(count); createPhysicsNodes(count);
E2D_LOG_INFO("添加 {} 个节点,当前总数: {}", count, nodes_.size()); E2D_LOG_INFO("添加 {} 个节点,当前总数: {}", count, getPhysicsNodeCount());
} }
/** /**
* @brief * @brief
*/ */
void removeNodes(size_t count) { void removeNodes(size_t count) {
if (count >= nodes_.size()) { auto physicsNodes = getPhysicsNodes();
count = nodes_.size(); if (count >= physicsNodes.size()) {
count = physicsNodes.size();
} }
if (count == 0) if (count == 0)
return; return;
// 从后往前移除指定数量的节点
for (size_t i = 0; i < count; ++i) { 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<PhysicsNode *>(it->get())) {
removeChild(*it);
break;
} }
E2D_LOG_INFO("移除 {} 个节点,当前总数: {}", count, nodes_.size()); }
}
E2D_LOG_INFO("移除 {} 个节点,当前总数: {}", count, getPhysicsNodeCount());
} }
/** /**
@ -385,9 +413,11 @@ private:
*/ */
void performCollisionDetection() { void performCollisionDetection() {
// 清除之前的碰撞状态 // 清除之前的碰撞状态
for (auto &node : nodes_) { for (const auto &child : getChildren()) {
if (auto node = dynamic_cast<PhysicsNode *>(child.get())) {
node->setColliding(false); node->setColliding(false);
} }
}
// 使用引擎自带的空间索引进行碰撞检测 // 使用引擎自带的空间索引进行碰撞检测
// 这是核心Scene::queryCollisions() 会自动使用 SpatialManager // 这是核心Scene::queryCollisions() 会自动使用 SpatialManager
@ -442,7 +472,6 @@ private:
Color(1.0f, 0.2f, 0.2f, 0.9f)); Color(1.0f, 0.2f, 0.2f, 0.9f));
} }
std::vector<Ptr<PhysicsNode>> nodes_;
PerformanceStats stats_; PerformanceStats stats_;
float screenWidth_ = 1280.0f; float screenWidth_ = 1280.0f;
float screenHeight_ = 720.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::init();
Logger::setLevel(LogLevel::Debug); Logger::setLevel(LogLevel::Debug);

View File

@ -72,20 +72,5 @@ target("spatial_index_demo")
print("Warning: romfs directory not found at " .. romfs) print("Warning: romfs directory not found at " .. romfs)
end end
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 end
target_end() target_end()