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()
: 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;
}
@ -146,8 +147,7 @@ bool GLSpriteBatch::needsFlush(const Texture& texture, bool isSDF) const {
}
// 检查是否需要刷新纹理改变、SDF 状态改变或缓冲区已满
return (currentTexture_ != &texture) ||
(currentIsSDF_ != isSDF) ||
return (currentTexture_ != &texture) || (currentIsSDF_ != isSDF) ||
(vertexCount_ + VERTICES_PER_SPRITE > MAX_VERTICES);
}
@ -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,7 +200,8 @@ void GLSpriteBatch::draw(const Texture &texture, const SpriteData &data) {
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()) {
return;
}
@ -231,7 +236,8 @@ void GLSpriteBatch::drawBatch(const Texture& texture, const std::vector<SpriteDa
batchCount_++;
}
void GLSpriteBatch::drawImmediate(const Texture& texture, const SpriteData& data) {
void GLSpriteBatch::drawImmediate(const Texture &texture,
const SpriteData &data) {
// 立即绘制,不缓存 - 用于需要立即显示的情况
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_);

View File

@ -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);
}
}
}
}

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) - 深入了解资源加载

View File

@ -64,9 +64,11 @@ public:
Scene::onUpdate(dt);
// 清除之前的碰撞状态
for (auto& box : boxes_) {
for (const auto& child : getChildren()) {
if (auto box = dynamic_cast<CollidableBox*>(child.get())) {
box->setColliding(false);
}
}
// 使用场景的空间索引查询所有碰撞
auto collisions = queryCollisions();
@ -82,11 +84,16 @@ public:
}
}
private:
std::vector<Ptr<CollidableBox>> boxes_;
void createBox(float x, float y) {
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();
// 创建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_) {
// 更新所有物理节点位置(通过 getChildren() 访问)
for (const auto& child : getChildren()) {
if (auto node = dynamic_cast<PhysicsNode*>(child.get())) {
node->update(dt, screenWidth_, screenHeight_);
}
}
// 使用空间索引进行碰撞检测
performCollisionDetection();
@ -150,9 +158,11 @@ public:
private:
void performCollisionDetection() {
// 清除之前的碰撞状态
for (auto& node : nodes_) {
for (const auto& child : getChildren()) {
if (auto node = dynamic_cast<PhysicsNode*>(child.get())) {
node->setColliding(false);
}
}
// 使用引擎自带的空间索引进行碰撞检测
auto collisions = queryCollisions();
@ -180,9 +190,25 @@ private:
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)`** - 启用节点的空间索引

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -1,8 +1,5 @@
#include <cmath>
#include <extra2d/extra2d.h>
#include <iomanip>
#include <random>
#include <sstream>
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,10 +124,12 @@ public:
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_);
}
}
auto updateEndTime = std::chrono::high_resolution_clock::now();
stats_.updateTime =
@ -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<float> posX(50.0f, screenWidth_ - 50.0f);
@ -326,38 +322,70 @@ private:
auto node = makePtr<PhysicsNode>(20.0f, color, static_cast<int>(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<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
*/
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<PhysicsNode *>(it->get())) {
removeChild(*it);
break;
}
E2D_LOG_INFO("移除 {} 个节点,当前总数: {}", count, nodes_.size());
}
}
E2D_LOG_INFO("移除 {} 个节点,当前总数: {}", count, getPhysicsNodeCount());
}
/**
@ -385,9 +413,11 @@ private:
*/
void performCollisionDetection() {
// 清除之前的碰撞状态
for (auto &node : nodes_) {
for (const auto &child : getChildren()) {
if (auto node = dynamic_cast<PhysicsNode *>(child.get())) {
node->setColliding(false);
}
}
// 使用引擎自带的空间索引进行碰撞检测
// 这是核心Scene::queryCollisions() 会自动使用 SpatialManager
@ -442,7 +472,6 @@ private:
Color(1.0f, 0.2f, 0.2f, 0.9f));
}
std::vector<Ptr<PhysicsNode>> 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);

View File

@ -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()