2026-02-11 19:40:26 +08:00
|
|
|
|
# 06. 碰撞检测
|
|
|
|
|
|
|
|
|
|
|
|
Extra2D 提供了基于空间索引的高效碰撞检测系统,支持四叉树和空间哈希两种策略。
|
|
|
|
|
|
|
|
|
|
|
|
## 完整示例
|
|
|
|
|
|
|
|
|
|
|
|
参考示例代码:
|
|
|
|
|
|
- `examples/collision_demo/main.cpp` - 基础碰撞检测演示
|
|
|
|
|
|
- `examples/spatial_index_demo/main.cpp` - 空间索引性能演示
|
|
|
|
|
|
|
|
|
|
|
|
## 启用碰撞检测
|
|
|
|
|
|
|
|
|
|
|
|
### 1. 创建可碰撞节点
|
|
|
|
|
|
|
|
|
|
|
|
```cpp
|
|
|
|
|
|
class CollidableBox : public Node {
|
|
|
|
|
|
public:
|
|
|
|
|
|
CollidableBox(float width, float height, const Color& color)
|
|
|
|
|
|
: width_(width), height_(height), color_(color), isColliding_(false) {
|
|
|
|
|
|
// 启用空间索引 - 这是关键!
|
|
|
|
|
|
setSpatialIndexed(true);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 必须实现 getBoundingBox 方法
|
2026-02-26 00:55:13 +08:00
|
|
|
|
Rect boundingBox() const override {
|
2026-02-11 19:40:26 +08:00
|
|
|
|
Vec2 pos = getPosition();
|
|
|
|
|
|
return Rect(pos.x - width_ / 2, pos.y - height_ / 2, width_, height_);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void setColliding(bool colliding) { isColliding_ = colliding; }
|
|
|
|
|
|
|
|
|
|
|
|
void onRender(RenderBackend& renderer) override {
|
|
|
|
|
|
Vec2 pos = getPosition();
|
|
|
|
|
|
|
|
|
|
|
|
// 碰撞时变红色
|
|
|
|
|
|
Color fillColor = isColliding_ ? Color(1.0f, 0.2f, 0.2f, 0.8f) : color_;
|
|
|
|
|
|
renderer.fillRect(
|
|
|
|
|
|
Rect(pos.x - width_ / 2, pos.y - height_ / 2, width_, height_),
|
|
|
|
|
|
fillColor
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 绘制边框
|
|
|
|
|
|
Color borderColor = isColliding_ ? Color(1.0f, 0.0f, 0.0f, 1.0f)
|
|
|
|
|
|
: Color(1.0f, 1.0f, 1.0f, 0.5f);
|
|
|
|
|
|
renderer.drawRect(
|
|
|
|
|
|
Rect(pos.x - width_ / 2, pos.y - height_ / 2, width_, height_),
|
|
|
|
|
|
borderColor, 2.0f
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
|
float width_, height_;
|
|
|
|
|
|
Color color_;
|
|
|
|
|
|
bool isColliding_;
|
|
|
|
|
|
};
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 2. 执行碰撞检测
|
|
|
|
|
|
|
|
|
|
|
|
```cpp
|
|
|
|
|
|
class GameScene : public Scene {
|
|
|
|
|
|
public:
|
|
|
|
|
|
void onUpdate(float dt) override {
|
|
|
|
|
|
Scene::onUpdate(dt);
|
|
|
|
|
|
|
|
|
|
|
|
// 清除之前的碰撞状态
|
2026-02-12 22:31:34 +08:00
|
|
|
|
for (const auto& child : getChildren()) {
|
|
|
|
|
|
if (auto box = dynamic_cast<CollidableBox*>(child.get())) {
|
|
|
|
|
|
box->setColliding(false);
|
|
|
|
|
|
}
|
2026-02-11 19:40:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 使用场景的空间索引查询所有碰撞
|
|
|
|
|
|
auto collisions = queryCollisions();
|
|
|
|
|
|
|
|
|
|
|
|
// 处理碰撞
|
|
|
|
|
|
for (const auto& [nodeA, nodeB] : collisions) {
|
|
|
|
|
|
if (auto boxA = dynamic_cast<CollidableBox*>(nodeA)) {
|
|
|
|
|
|
boxA->setColliding(true);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (auto boxB = dynamic_cast<CollidableBox*>(nodeB)) {
|
|
|
|
|
|
boxB->setColliding(true);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-12 22:31:34 +08:00
|
|
|
|
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 管理节点生命周期
|
|
|
|
|
|
}
|
2026-02-11 19:40:26 +08:00
|
|
|
|
};
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-02-12 22:31:34 +08:00
|
|
|
|
**注意**:节点通过 `addChild()` 添加到场景后,由场景统一管理生命周期。不要额外存储 `shared_ptr` 到 vector 中,避免双重引用问题。
|
|
|
|
|
|
|
2026-02-11 19:40:26 +08:00
|
|
|
|
## 空间索引策略
|
|
|
|
|
|
|
|
|
|
|
|
### 切换策略
|
|
|
|
|
|
|
|
|
|
|
|
```cpp
|
|
|
|
|
|
// 获取空间管理器
|
|
|
|
|
|
auto& spatialManager = getSpatialManager();
|
|
|
|
|
|
|
|
|
|
|
|
// 切换到四叉树
|
|
|
|
|
|
spatialManager.setStrategy(SpatialStrategy::QuadTree);
|
|
|
|
|
|
|
|
|
|
|
|
// 切换到空间哈希
|
|
|
|
|
|
spatialManager.setStrategy(SpatialStrategy::SpatialHash);
|
|
|
|
|
|
|
|
|
|
|
|
// 获取当前策略名称
|
|
|
|
|
|
const char* name = spatialManager.getStrategyName();
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 策略对比
|
|
|
|
|
|
|
|
|
|
|
|
| 策略 | 适用场景 | 特点 |
|
|
|
|
|
|
|------|---------|------|
|
|
|
|
|
|
| QuadTree | 节点分布不均匀 | 分层划分,适合稀疏分布 |
|
|
|
|
|
|
| SpatialHash | 节点分布均匀 | 均匀网格,适合密集分布 |
|
|
|
|
|
|
|
|
|
|
|
|
## 性能演示
|
|
|
|
|
|
|
|
|
|
|
|
`examples/spatial_index_demo/main.cpp` 展示了空间索引的性能优势:
|
|
|
|
|
|
|
|
|
|
|
|
```cpp
|
|
|
|
|
|
class SpatialIndexDemoScene : public Scene {
|
|
|
|
|
|
public:
|
|
|
|
|
|
void onEnter() override {
|
|
|
|
|
|
Scene::onEnter();
|
|
|
|
|
|
|
|
|
|
|
|
// 创建100个碰撞节点
|
2026-02-12 22:31:34 +08:00
|
|
|
|
createPhysicsNodes(100);
|
2026-02-11 19:40:26 +08:00
|
|
|
|
|
|
|
|
|
|
E2D_LOG_INFO("空间索引已启用: {}", isSpatialIndexingEnabled());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void onUpdate(float dt) override {
|
|
|
|
|
|
Scene::onUpdate(dt);
|
|
|
|
|
|
|
2026-02-12 22:31:34 +08:00
|
|
|
|
// 更新所有物理节点位置(通过 getChildren() 访问)
|
|
|
|
|
|
for (const auto& child : getChildren()) {
|
|
|
|
|
|
if (auto node = dynamic_cast<PhysicsNode*>(child.get())) {
|
|
|
|
|
|
node->update(dt, screenWidth_, screenHeight_);
|
|
|
|
|
|
}
|
2026-02-11 19:40:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 使用空间索引进行碰撞检测
|
|
|
|
|
|
performCollisionDetection();
|
|
|
|
|
|
|
|
|
|
|
|
// 按 X 键切换索引策略
|
|
|
|
|
|
auto& input = Application::instance().input();
|
|
|
|
|
|
if (input.isButtonPressed(GamepadButton::X)) {
|
|
|
|
|
|
toggleSpatialStrategy();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
|
void performCollisionDetection() {
|
|
|
|
|
|
// 清除之前的碰撞状态
|
2026-02-12 22:31:34 +08:00
|
|
|
|
for (const auto& child : getChildren()) {
|
|
|
|
|
|
if (auto node = dynamic_cast<PhysicsNode*>(child.get())) {
|
|
|
|
|
|
node->setColliding(false);
|
|
|
|
|
|
}
|
2026-02-11 19:40:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 使用引擎自带的空间索引进行碰撞检测
|
|
|
|
|
|
auto collisions = queryCollisions();
|
|
|
|
|
|
|
|
|
|
|
|
// 标记碰撞的节点
|
|
|
|
|
|
for (const auto& [nodeA, nodeB] : collisions) {
|
|
|
|
|
|
if (auto boxA = dynamic_cast<PhysicsNode*>(nodeA)) {
|
|
|
|
|
|
boxA->setColliding(true);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (auto boxB = dynamic_cast<PhysicsNode*>(nodeB)) {
|
|
|
|
|
|
boxB->setColliding(true);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void toggleSpatialStrategy() {
|
|
|
|
|
|
auto& spatialManager = getSpatialManager();
|
|
|
|
|
|
SpatialStrategy currentStrategy = spatialManager.getCurrentStrategy();
|
|
|
|
|
|
|
|
|
|
|
|
if (currentStrategy == SpatialStrategy::QuadTree) {
|
|
|
|
|
|
spatialManager.setStrategy(SpatialStrategy::SpatialHash);
|
|
|
|
|
|
E2D_LOG_INFO("切换到空间哈希策略");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
spatialManager.setStrategy(SpatialStrategy::QuadTree);
|
|
|
|
|
|
E2D_LOG_INFO("切换到四叉树策略");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-12 22:31:34 +08:00
|
|
|
|
|
|
|
|
|
|
// 获取物理节点数量
|
|
|
|
|
|
size_t getPhysicsNodeCount() const {
|
|
|
|
|
|
size_t count = 0;
|
|
|
|
|
|
for (const auto& child : getChildren()) {
|
|
|
|
|
|
if (dynamic_cast<PhysicsNode*>(child.get())) {
|
|
|
|
|
|
++count;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return count;
|
|
|
|
|
|
}
|
2026-02-11 19:40:26 +08:00
|
|
|
|
};
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-02-12 22:31:34 +08:00
|
|
|
|
**关键改进**:
|
|
|
|
|
|
- 使用 `getChildren()` 代替私有 vector 存储节点引用
|
|
|
|
|
|
- 通过 `dynamic_cast` 筛选特定类型的子节点
|
|
|
|
|
|
- 避免双重引用,简化生命周期管理
|
|
|
|
|
|
|
2026-02-11 19:40:26 +08:00
|
|
|
|
## 关键要点
|
|
|
|
|
|
|
|
|
|
|
|
1. **必须调用 `setSpatialIndexed(true)`** - 启用节点的空间索引
|
2026-02-26 00:55:13 +08:00
|
|
|
|
2. **必须实现 `boundingBox()`** - 返回准确的边界框
|
2026-02-11 19:40:26 +08:00
|
|
|
|
3. **在 `onEnter()` 中调用 `Scene::onEnter()`** - 确保节点正确注册到空间索引
|
|
|
|
|
|
4. **使用 `queryCollisions()`** - 自动利用空间索引优化检测
|
|
|
|
|
|
|
|
|
|
|
|
## 下一步
|
|
|
|
|
|
|
|
|
|
|
|
- [07. UI 系统](./07_UI_System.md) - 学习 UI 控件使用
|
|
|
|
|
|
- [08. 音频系统](./08_Audio_System.md) - 学习音频播放
|