refactor: 移除空间索引系统及相关示例代码

移除空间索引系统(QuadTree/SpatialHash)及相关实现代码
删除push_box和flappy_bird示例游戏及其资源文件
清理不再需要的空间索引相关函数调用和头文件引用
This commit is contained in:
ChestnutYueyue 2026-02-26 19:41:57 +08:00
parent 98bca638d0
commit f41600306e
91 changed files with 5 additions and 5905 deletions

View File

@ -1,127 +0,0 @@
// ============================================================================
// BaseScene.cpp - Flappy Bird 基础场景实现
// ============================================================================
#include "BaseScene.h"
#include <scene/transition_scene.h>
#include <utils/logger.h>
namespace flappybird {
BaseScene::BaseScene() {
// 设置背景颜色为黑色(窗口四周会显示这个颜色)
setBackgroundColor(extra2d::Color(0.0f, 0.0f, 0.0f, 1.0f));
}
void BaseScene::onEnter() {
extra2d::Scene::onEnter();
// 计算并更新视口
updateViewport();
}
void BaseScene::updateViewport() {
auto &app = extra2d::Application::instance();
float windowWidth = static_cast<float>(app.window().width());
float windowHeight = static_cast<float>(app.window().height());
// 计算游戏内容在窗口中的居中位置
// 保持游戏原始宽高比,进行"黑边"适配
float scaleX = windowWidth / GAME_WIDTH;
float scaleY = windowHeight / GAME_HEIGHT;
// 使用较小的缩放比例,确保游戏内容完整显示在窗口中
float scale = std::min(scaleX, scaleY);
scaledGameWidth_ = GAME_WIDTH * scale;
scaledGameHeight_ = GAME_HEIGHT * scale;
// 计算居中偏移,使游戏内容在窗口中水平和垂直居中
viewportOffsetX_ = (windowWidth - scaledGameWidth_) * 0.5f;
viewportOffsetY_ = (windowHeight - scaledGameHeight_) * 0.5f;
// 设置视口大小为游戏逻辑分辨率
setViewportSize(GAME_WIDTH, GAME_HEIGHT);
// 创建并设置相机
auto camera = extra2d::makePtr<extra2d::Camera>();
// 设置正交投影,覆盖整个游戏逻辑区域
// 注意对于2D游戏Y轴向下增长所以bottom > top
camera->setViewport(0.0f, GAME_WIDTH, GAME_HEIGHT, 0.0f);
setCamera(camera);
}
void BaseScene::onRender(extra2d::Renderer &renderer) {
// 检查窗口大小是否改变,如果改变则更新视口
auto &app = extra2d::Application::instance();
float currentWindowWidth = static_cast<float>(app.window().width());
float currentWindowHeight = static_cast<float>(app.window().height());
// 如果窗口大小改变,重新计算视口
float expectedWidth = scaledGameWidth_ + viewportOffsetX_ * 2.0f;
float expectedHeight = scaledGameHeight_ + viewportOffsetY_ * 2.0f;
if (std::abs(currentWindowWidth - expectedWidth) > 1.0f ||
std::abs(currentWindowHeight - expectedHeight) > 1.0f) {
E2D_LOG_INFO("BaseScene::onRender - window size changed from ({} x {}) to "
"({} x {}), updating viewport",
expectedWidth, expectedHeight, currentWindowWidth,
currentWindowHeight);
updateViewport();
}
// 设置视口为居中区域
renderer.setViewport(
static_cast<int>(viewportOffsetX_), static_cast<int>(viewportOffsetY_),
static_cast<int>(scaledGameWidth_), static_cast<int>(scaledGameHeight_));
// 调用父类的 onRender 进行实际渲染
extra2d::Scene::onRender(renderer);
}
void BaseScene::renderContent(extra2d::Renderer &renderer) {
// 如果视口参数未初始化onEnter 还没被调用),先初始化
if (scaledGameWidth_ <= 0.0f || scaledGameHeight_ <= 0.0f) {
updateViewport();
}
// 检查窗口大小是否改变
auto &app = extra2d::Application::instance();
float currentWindowWidth = static_cast<float>(app.window().width());
float currentWindowHeight = static_cast<float>(app.window().height());
float expectedWidth = scaledGameWidth_ + viewportOffsetX_ * 2.0f;
float expectedHeight = scaledGameHeight_ + viewportOffsetY_ * 2.0f;
if (std::abs(currentWindowWidth - expectedWidth) > 1.0f ||
std::abs(currentWindowHeight - expectedHeight) > 1.0f) {
updateViewport();
}
// 检查当前场景是否作为 TransitionScene 的子场景被渲染
bool isChildOfTransition = false;
if (auto parentNode = parent()) {
if (dynamic_cast<extra2d::TransitionScene *>(parentNode.get())) {
isChildOfTransition = true;
}
}
if (isChildOfTransition) {
// 作为 TransitionScene 的子场景时,需要设置正确的投影矩阵
// 使用游戏逻辑分辨率作为投影区域,让 TransitionScene 控制整体视口
auto camera = getActiveCamera();
if (camera) {
// 设置投影矩阵覆盖整个游戏逻辑区域
renderer.setViewProjection(camera->getViewProjectionMatrix());
}
// 渲染场景内容(投影矩阵已设置,直接渲染)
batchUpdateTransforms();
renderer.beginSpriteBatch();
render(renderer);
renderer.endSpriteBatch();
} else {
// 正常渲染时,调用父类的 renderContent 处理视口和投影
renderer.setViewport(static_cast<int>(viewportOffsetX_),
static_cast<int>(viewportOffsetY_),
static_cast<int>(scaledGameWidth_),
static_cast<int>(scaledGameHeight_));
extra2d::Scene::renderContent(renderer);
}
}
} // namespace flappybird

View File

@ -1,57 +0,0 @@
// ============================================================================
// BaseScene.h - Flappy Bird 基础场景类
// 描述: 提供统一的居中视口适配功能,所有游戏场景都应继承此类
// ============================================================================
#pragma once
#include <extra2d.h>
namespace flappybird {
// 游戏逻辑分辨率(原始 Flappy Bird 尺寸)
static constexpr float GAME_WIDTH = 288.0f;
static constexpr float GAME_HEIGHT = 512.0f;
/**
* @brief Flappy Bird
*
*/
class BaseScene : public extra2d::Scene {
public:
/**
* @brief
*/
BaseScene();
/**
* @brief
*/
void onEnter() override;
/**
* @brief
* @param renderer
*/
void onRender(extra2d::Renderer &renderer) override;
/**
* @brief
* @param renderer
*/
void renderContent(extra2d::Renderer &renderer) override;
protected:
/**
* @brief 使
*/
void updateViewport();
// 视口适配参数(用于在窗口中居中显示游戏内容)
float scaledGameWidth_ = 0.0f; // 缩放后的游戏宽度
float scaledGameHeight_ = 0.0f; // 缩放后的游戏高度
float viewportOffsetX_ = 0.0f; // 视口水平偏移
float viewportOffsetY_ = 0.0f; // 视口垂直偏移
};
} // namespace flappybird

View File

@ -1,209 +0,0 @@
// ============================================================================
// GameOverLayer.cpp - 游戏结束层实现
// ============================================================================
#include "GameOverLayer.h"
#include "BaseScene.h"
#include "GameScene.h"
#include "Number.h"
#include "ResLoader.h"
#include "StartScene.h"
#include <animation/tween.h>
namespace flappybird {
GameOverLayer::GameOverLayer(int score) : score_(score) {
// 注意:不要在构造函数中创建子节点
// 因为此时 weak_from_this() 还不能使用
}
void GameOverLayer::onEnter() {
Node::onEnter();
// 在 onEnter 中初始化,此时 weak_from_this() 可用
// 使用游戏逻辑分辨率
float screenWidth = GAME_WIDTH;
float screenHeight = GAME_HEIGHT;
// 整体居中x 坐标相对于屏幕中心)
// 初始位置在屏幕底部,然后向上滑出
setPosition(extra2d::Vec2(screenWidth / 2.0f, screenHeight + 200.0f));
// 显示 "Game Over" 文字y=120从顶部开始
auto gameOverFrame = ResLoader::getKeyFrame("text_game_over");
if (gameOverFrame) {
auto gameOver = extra2d::Sprite::create(gameOverFrame->getTexture(),
gameOverFrame->getRect());
gameOver->setAnchor(extra2d::Vec2(0.5f, 0.0f));
gameOver->setPosition(extra2d::Vec2(0.0f, 120.0f)); // x=0 表示相对于中心点
addChild(gameOver);
}
// 初始化得分面板
initPanel(score_, screenHeight);
// 初始化按钮
initButtons();
// 向上滑出动画
extra2d::TweenOptions opts;
opts.easing = extra2d::TweenEasing::BackOut;
tween()
.to(0.5f,
extra2d::tween::pos(screenWidth / 2.0f, screenHeight / 2.0f - 300.0f),
opts)
.call([this]() {
animationDone_ = true;
if (restartBtn_)
restartBtn_->setEnabled(true);
if (menuBtn_)
menuBtn_->setEnabled(true);
if (shareBtn_)
shareBtn_->setEnabled(true);
})
.start();
}
void GameOverLayer::initPanel(int score, float screenHeight) {
// 显示得分板(在屏幕中间)
auto panelFrame = ResLoader::getKeyFrame("score_panel");
if (!panelFrame)
return;
auto panel =
extra2d::Sprite::create(panelFrame->getTexture(), panelFrame->getRect());
panel->setAnchor(extra2d::Vec2(0.5f, 0.5f));
panel->setPosition(
extra2d::Vec2(0.0f, screenHeight / 2.0f)); // x=0 表示相对于中心点
addChild(panel);
// 获取最高分(从存储中读取)
static int bestScore = 0;
if (score > bestScore) {
bestScore = score;
}
// 显示 "New" 标记(如果破了记录)
if (score >= bestScore && score > 0) {
auto newFrame = ResLoader::getKeyFrame("new");
if (newFrame) {
auto newSprite =
extra2d::Sprite::create(newFrame->getTexture(), newFrame->getRect());
newSprite->setAnchor(extra2d::Vec2(0.5f, 0.5f));
// 调整位置使其在面板内部,靠近 BEST 分数
newSprite->setPosition(
extra2d::Vec2(30.0f, 25.0f)); // 相对于面板的坐标,在 BEST 右侧
panel->addChild(newSprite);
}
}
// 显示奖牌
auto medalFrame = getMedal(score);
if (medalFrame) {
auto medal = extra2d::Sprite::create(medalFrame->getTexture(),
medalFrame->getRect());
medal->setAnchor(extra2d::Vec2(0.5f, 0.5f));
medal->setPosition(extra2d::Vec2(54.0f, 68.0f)); // 相对于面板的坐标
panel->addChild(medal);
}
// 显示本局得分
auto scoreNumber = extra2d::makePtr<Number>();
scoreNumber->setLittleNumber(score);
scoreNumber->setPosition(
extra2d::Vec2(80.0f, -15.0f)); // 相对于面板的坐标,右侧对齐
panel->addChild(scoreNumber);
// 显示最高分
auto bestNumber = extra2d::makePtr<Number>();
bestNumber->setLittleNumber(bestScore);
bestNumber->setPosition(
extra2d::Vec2(80.0f, 25.0f)); // 相对于面板的坐标,右侧对齐
panel->addChild(bestNumber);
}
void GameOverLayer::initButtons() {
auto restartFrame = ResLoader::getKeyFrame("button_restart");
if (restartFrame) {
restartBtn_ = extra2d::Button::create();
restartBtn_->setBackgroundImage(restartFrame->getTexture(),
restartFrame->getRect());
restartBtn_->setAnchor(extra2d::Vec2(0.5f, 0.5f));
restartBtn_->setPosition(extra2d::Vec2(0.0f, 360.0f));
restartBtn_->setEnabled(false);
restartBtn_->setOnClick([]() {
ResLoader::playMusic(MusicType::Click);
auto &app = extra2d::Application::instance();
app.scenes().replaceScene(extra2d::makePtr<GameScene>(),
extra2d::TransitionType::Fade, 0.5f);
});
addChild(restartBtn_);
}
auto menuFrame = ResLoader::getKeyFrame("button_menu");
if (menuFrame) {
menuBtn_ = extra2d::Button::create();
menuBtn_->setBackgroundImage(menuFrame->getTexture(), menuFrame->getRect());
menuBtn_->setAnchor(extra2d::Vec2(0.5f, 0.5f));
menuBtn_->setPosition(extra2d::Vec2(0.0f, 420.0f));
menuBtn_->setEnabled(false);
menuBtn_->setOnClick([]() {
ResLoader::playMusic(MusicType::Click);
auto &app = extra2d::Application::instance();
app.scenes().replaceScene(extra2d::makePtr<StartScene>(),
extra2d::TransitionType::Fade, 0.5f);
});
addChild(menuBtn_);
}
auto shareFrame = ResLoader::getKeyFrame("button_share");
if (shareFrame) {
shareBtn_ = extra2d::Button::create();
shareBtn_->setBackgroundImage(shareFrame->getTexture(),
shareFrame->getRect());
shareBtn_->setAnchor(extra2d::Vec2(0.5f, 0.5f));
shareBtn_->setPosition(extra2d::Vec2(0.0f, 460.0f));
shareBtn_->setEnabled(false);
shareBtn_->setOnClick([]() { ResLoader::playMusic(MusicType::Click); });
addChild(shareBtn_);
}
}
void GameOverLayer::onUpdate(float dt) {
Node::onUpdate(dt);
if (!animationDone_)
return;
auto &input = extra2d::Application::instance().input();
if (input.isButtonPressed(extra2d::GamepadButton::A)) {
ResLoader::playMusic(MusicType::Click);
auto &app = extra2d::Application::instance();
app.scenes().replaceScene(extra2d::makePtr<GameScene>(),
extra2d::TransitionType::Fade, 0.5f);
}
if (input.isButtonPressed(extra2d::GamepadButton::B)) {
ResLoader::playMusic(MusicType::Click);
auto &app = extra2d::Application::instance();
app.scenes().replaceScene(extra2d::makePtr<StartScene>(),
extra2d::TransitionType::Fade, 0.5f);
}
}
extra2d::Ptr<extra2d::SpriteFrame> GameOverLayer::getMedal(int score) {
if (score < 10) {
return nullptr; // 无奖牌
} else if (score < 20) {
return ResLoader::getKeyFrame("medals_0"); // 铜牌
} else if (score < 30) {
return ResLoader::getKeyFrame("medals_1"); // 银牌
} else if (score < 50) {
return ResLoader::getKeyFrame("medals_2"); // 金牌
} else {
return ResLoader::getKeyFrame("medals_3"); // 钻石奖牌
}
}
} // namespace flappybird

View File

@ -1,62 +0,0 @@
// ============================================================================
// GameOverLayer.h - 游戏结束层
// 描述: 显示游戏结束界面、得分和奖牌
// ============================================================================
#pragma once
#include <extra2d.h>
namespace flappybird {
/**
* @brief
*
*/
class GameOverLayer : public extra2d::Node {
public:
/**
* @brief
* @param score
*/
GameOverLayer(int score);
/**
* @brief
*/
void onEnter() override;
/**
* @brief
* @param dt
*/
void onUpdate(float dt) override;
private:
/**
* @brief
* @param score
* @param screenHeight
*/
void initPanel(int score, float screenHeight);
/**
* @brief
*/
void initButtons();
/**
* @brief
* @param score
* @return
*/
extra2d::Ptr<extra2d::SpriteFrame> getMedal(int score);
int score_ = 0; // 本局得分
bool animationDone_ = false; // 动画是否完成
extra2d::Ptr<extra2d::Button> restartBtn_; // 重新开始按钮
extra2d::Ptr<extra2d::Button> menuBtn_; // 菜单按钮
extra2d::Ptr<extra2d::Button> shareBtn_; // 分享按钮
};
} // namespace flappybird

View File

@ -1,226 +0,0 @@
// ============================================================================
// GameScene.cpp - 游戏主场景实现
// ============================================================================
#include "GameScene.h"
#include "GameOverLayer.h"
#include "ResLoader.h"
#include "platform/input.h"
namespace flappybird {
GameScene::GameScene() {
// 基类 BaseScene 已经处理了视口设置和背景颜色
}
void GameScene::onEnter() {
BaseScene::onEnter();
// 游戏坐标系:使用游戏逻辑分辨率
float screenWidth = GAME_WIDTH;
float screenHeight = GAME_HEIGHT;
// 添加背景(使用左上角锚点,与原游戏一致)
auto bgFrame = ResLoader::getKeyFrame("bg_day");
if (bgFrame) {
auto background =
extra2d::Sprite::create(bgFrame->getTexture(), bgFrame->getRect());
background->setAnchor(extra2d::Vec2(0.0f, 0.0f));
background->setPosition(extra2d::Vec2(0.0f, 0.0f));
addChild(background);
}
// 添加水管(初始时隐藏,游戏开始后才显示)
auto pipes = extra2d::makePtr<Pipes>();
pipes_ = pipes.get();
pipes->setVisible(false);
addChild(pipes);
// 添加小鸟(在屏幕中间偏左位置)
auto bird = extra2d::makePtr<Bird>();
bird->setPosition(
extra2d::Vec2(screenWidth / 2.0f - 50.0f, screenHeight / 2.0f));
bird_ = bird.get();
addChild(bird);
// 添加地面
auto ground = extra2d::makePtr<Ground>();
ground_ = ground.get();
addChild(ground);
// 添加得分(屏幕顶部中央)
auto scoreNumber = extra2d::makePtr<Number>();
scoreNumber->setPosition(extra2d::Vec2(screenWidth / 2.0f, 50.0f));
scoreNumber->setNumber(0);
scoreNumber_ = scoreNumber.get();
addChild(scoreNumber);
// 添加 ready 图片(屏幕中央偏上)
auto readyFrame = ResLoader::getKeyFrame("text_ready");
if (readyFrame) {
readySprite_ = extra2d::Sprite::create(readyFrame->getTexture(),
readyFrame->getRect());
readySprite_->setAnchor(extra2d::Vec2(0.5f, 0.5f));
readySprite_->setPosition(
extra2d::Vec2(screenWidth / 2.0f, screenHeight / 2.0f - 70.0f));
addChild(readySprite_);
}
// 添加教程图片(屏幕中央偏下)
auto tutorialFrame = ResLoader::getKeyFrame("tutorial");
if (tutorialFrame) {
tutorialSprite_ = extra2d::Sprite::create(tutorialFrame->getTexture(),
tutorialFrame->getRect());
tutorialSprite_->setAnchor(extra2d::Vec2(0.5f, 0.5f));
tutorialSprite_->setPosition(
extra2d::Vec2(screenWidth / 2.0f, screenHeight / 2.0f + 30.0f));
addChild(tutorialSprite_);
}
// 播放转场音效
ResLoader::playMusic(MusicType::Swoosh);
// 初始化状态
started_ = false;
score_ = 0;
}
void GameScene::onUpdate(float dt) {
if (!gameOver_) {
if (!bird_)
return;
auto &input = extra2d::Application::instance().input();
if (input.isButtonPressed(extra2d::GamepadButton::A) ||
input.isMousePressed(extra2d::MouseButton::Left)) {
if (!started_) {
started_ = true;
startGame();
}
bird_->jump();
}
if (started_) {
bird_->fall(dt);
if (pipes_) {
Pipe *firstPipe = pipes_->getPipe(0);
if (firstPipe && !firstPipe->scored) {
float birdX = bird_->pos().x;
float pipeX = firstPipe->pos().x;
if (pipeX <= birdX) {
score_++;
scoreNumber_->setNumber(score_);
firstPipe->scored = true;
ResLoader::playMusic(MusicType::Point);
}
}
}
if (bird_->isLiving() && checkCollision()) {
onHit();
}
if (bird_->isLiving() && GAME_HEIGHT - bird_->pos().y <= 123.0f) {
bird_->setPosition(extra2d::Vec2(bird_->pos().x, GAME_HEIGHT - 123.0f));
bird_->setStatus(Bird::Status::Still);
onHit();
}
}
}
BaseScene::onUpdate(dt);
}
void GameScene::startGame() {
// 隐藏 ready 和 tutorial 图片
if (readySprite_) {
readySprite_->setVisible(false);
}
if (tutorialSprite_) {
tutorialSprite_->setVisible(false);
}
// 显示并开始移动水管
if (pipes_) {
pipes_->setVisible(true);
pipes_->start();
}
// 设置小鸟状态
if (bird_) {
bird_->setStatus(Bird::Status::StartToFly);
}
}
bool GameScene::checkCollision() {
if (!bird_ || !pipes_)
return false;
extra2d::Rect birdBox = bird_->boundingBox();
// 检查与每个水管的碰撞
for (int i = 0; i < 3; ++i) {
Pipe *pipe = pipes_->getPipe(i);
if (!pipe)
continue;
// 检查与上水管的碰撞
extra2d::Rect topBox = pipe->getTopPipeBox();
if (birdBox.intersects(topBox)) {
return true;
}
// 检查与下水管的碰撞
extra2d::Rect bottomBox = pipe->getBottomPipeBox();
if (birdBox.intersects(bottomBox)) {
return true;
}
}
return false;
}
void GameScene::onHit() {
if (!bird_->isLiving())
return;
// 小鸟死亡
bird_->die();
// 停止地面滚动
if (ground_) {
ground_->stop();
}
// 停止水管移动
if (pipes_) {
pipes_->stop();
}
// 停止小鸟动画
if (bird_) {
bird_->setStatus(Bird::Status::Still);
}
// 隐藏得分
if (scoreNumber_) {
scoreNumber_->setVisible(false);
}
gameOver();
}
void GameScene::gameOver() {
if (gameOver_)
return;
started_ = false;
gameOver_ = true;
auto gameOverLayer = extra2d::makePtr<GameOverLayer>(score_);
addChild(gameOverLayer);
}
} // namespace flappybird

View File

@ -1,72 +0,0 @@
// ============================================================================
// GameScene.h - 游戏主场景
// 描述: 游戏的核心场景,包含小鸟、水管、地面和得分系统
// ============================================================================
#pragma once
#include "BaseScene.h"
#include "Bird.h"
#include "Pipes.h"
#include "Ground.h"
#include "Number.h"
namespace flappybird {
/**
* @brief
*
*/
class GameScene : public BaseScene {
public:
/**
* @brief
*/
GameScene();
/**
* @brief
*/
void onEnter() override;
/**
* @brief
* @param dt
*/
void onUpdate(float dt) override;
private:
/**
* @brief
*/
void startGame();
/**
* @brief
*/
void onHit();
/**
* @brief
*/
void gameOver();
/**
* @brief
* @return
*/
bool checkCollision();
Bird* bird_ = nullptr; // 小鸟
Pipes* pipes_ = nullptr; // 水管管理器
Ground* ground_ = nullptr; // 地面
Number* scoreNumber_ = nullptr; // 得分显示
extra2d::Ptr<extra2d::Sprite> readySprite_; // "Get Ready" 提示
extra2d::Ptr<extra2d::Sprite> tutorialSprite_; // 操作教程提示
bool started_ = false; // 游戏是否已开始
bool gameOver_ = false; // 游戏是否已结束
int score_ = 0; // 当前得分
};
} // namespace flappybird

View File

@ -1,66 +0,0 @@
// ============================================================================
// Number.cpp - 数字显示类实现
// ============================================================================
#include "Number.h"
#include "ResLoader.h"
namespace flappybird {
Number::Number() : number_(0) {
}
void Number::setNumber(int number) {
number_ = number;
createNumberSprites(number, "number_big_");
}
void Number::setLittleNumber(int number) {
number_ = number;
createNumberSprites(number, "number_medium_");
}
void Number::createNumberSprites(int number, const std::string& prefix) {
// 清除之前的数字精灵
removeAllChildren();
// 获取数字 0 的高度作为参考
auto zeroFrame = ResLoader::getKeyFrame(prefix + "0");
float digitHeight = zeroFrame ? zeroFrame->getRect().size.height : 36.0f;
// 收集所有数字位
std::vector<int> digits;
if (number == 0) {
digits.push_back(0);
} else {
while (number > 0) {
digits.push_back(number % 10);
number /= 10;
}
}
// 计算总宽度
float totalWidth = 0.0f;
std::vector<float> digitWidths;
for (int digit : digits) {
auto frame = ResLoader::getKeyFrame(prefix + std::to_string(digit));
float width = frame ? frame->getRect().size.width : 24.0f;
digitWidths.push_back(width);
totalWidth += width;
}
// 创建数字精灵并居中排列
float currentX = -totalWidth / 2.0f;
for (size_t i = 0; i < digits.size(); ++i) {
auto frame = ResLoader::getKeyFrame(prefix + std::to_string(digits[i]));
if (frame) {
auto digitSprite = extra2d::Sprite::create(frame->getTexture(), frame->getRect());
digitSprite->setAnchor(extra2d::Vec2(0.0f, 0.0f));
digitSprite->setPosition(extra2d::Vec2(currentX, -digitHeight / 2.0f));
addChild(digitSprite);
}
currentX += digitWidths[i];
}
}
} // namespace flappybird

View File

@ -1,52 +0,0 @@
// ============================================================================
// Number.h - 数字显示类
// 描述: 将整数数字转换为精灵图片显示
// ============================================================================
#pragma once
#include <extra2d.h>
namespace flappybird {
/**
* @brief
*
*/
class Number : public extra2d::Node {
public:
/**
* @brief
*/
Number();
/**
* @brief
* @param number
*/
void setNumber(int number);
/**
* @brief
* @param number
*/
void setLittleNumber(int number);
/**
* @brief
* @return
*/
int getNumber() const { return number_; }
private:
/**
* @brief
* @param number
* @param prefix "number_big_" "number_medium_"
*/
void createNumberSprites(int number, const std::string& prefix);
int number_ = 0; // 当前数字
};
} // namespace flappybird

View File

@ -1,99 +0,0 @@
// ============================================================================
// ResLoader.cpp - 资源加载器实现
// ============================================================================
#include "ResLoader.h"
#include <nlohmann/json.hpp>
namespace flappybird {
extra2d::Ptr<extra2d::Texture> ResLoader::atlasTexture_;
std::map<std::string, ResLoader::ImageInfo> ResLoader::imageMap_;
std::map<MusicType, extra2d::Ptr<extra2d::Sound>> ResLoader::soundMap_;
void ResLoader::init() {
auto &resources = extra2d::Application::instance().resources();
// 加载图集纹理
atlasTexture_ = resources.loadTexture("assets/images/atlas.png");
if (!atlasTexture_) {
E2D_LOG_ERROR("无法加载图集纹理 atlas.png");
return;
}
// 使用资源管理器加载 JSON 文件
std::string jsonContent = resources.loadJsonFile("assets/images/atlas.json");
if (jsonContent.empty()) {
E2D_LOG_ERROR("无法加载 atlas.json 文件");
return;
}
// 解析 JSON 图集数据
try {
nlohmann::json jsonData = nlohmann::json::parse(jsonContent);
for (const auto &sprite : jsonData["sprites"]) {
std::string name = sprite["name"];
float x = sprite["x"];
float y = sprite["y"];
float width = sprite["width"];
float height = sprite["height"];
ImageInfo info = {width, height, x, y};
imageMap_[name] = info;
}
E2D_LOG_INFO("成功加载 {} 个精灵帧", imageMap_.size());
} catch (const std::exception &e) {
E2D_LOG_ERROR("解析 atlas.json 失败: {}", e.what());
return;
}
// 加载音效
soundMap_[MusicType::Click] = resources.loadSound("assets/sound/click.wav");
soundMap_[MusicType::Hit] = resources.loadSound("assets/sound/hit.wav");
soundMap_[MusicType::Fly] = resources.loadSound("assets/sound/fly.wav");
soundMap_[MusicType::Point] = resources.loadSound("assets/sound/point.wav");
soundMap_[MusicType::Swoosh] = resources.loadSound("assets/sound/swoosh.wav");
E2D_LOG_INFO("资源加载完成");
}
extra2d::Ptr<extra2d::SpriteFrame>
ResLoader::getKeyFrame(const std::string &name) {
auto it = imageMap_.find(name);
if (it == imageMap_.end()) {
E2D_LOG_WARN("找不到精灵帧: %s", name.c_str());
return nullptr;
}
const ImageInfo &info = it->second;
E2D_LOG_INFO("加载精灵帧: name={}, w={}, h={}, x={}, y={}", name, info.width,
info.height, info.x, info.y);
// 检查纹理尺寸
if (atlasTexture_) {
E2D_LOG_INFO("图集纹理尺寸: {}x{}", atlasTexture_->width(),
atlasTexture_->height());
}
return extra2d::makePtr<extra2d::SpriteFrame>(
atlasTexture_, extra2d::Rect(info.x, info.y, info.width, info.height));
}
void ResLoader::playMusic(MusicType type) {
auto it = soundMap_.find(type);
if (it == soundMap_.end()) {
E2D_LOG_WARN("ResLoader::playMusic: sound type not found");
return;
}
if (!it->second) {
E2D_LOG_WARN("ResLoader::playMusic: sound pointer is null");
return;
}
if (!it->second->play()) {
E2D_LOG_WARN("ResLoader::playMusic: failed to play sound");
}
}
} // namespace flappybird

View File

@ -1,63 +0,0 @@
// ============================================================================
// ResLoader.h - 资源加载器
// 描述: 管理游戏资源的加载和访问
// ============================================================================
#pragma once
#include <extra2d.h>
#include <map>
#include <string>
namespace flappybird {
/**
* @brief
*/
enum class MusicType {
Click, // 按键声音
Hit, // 小鸟死亡声音
Fly, // 小鸟飞翔声音
Point, // 得分声音
Swoosh // 转场声音
};
/**
* @brief
*
*/
class ResLoader {
public:
/**
* @brief
*/
static void init();
/**
* @brief
* @param name
* @return
*/
static extra2d::Ptr<extra2d::SpriteFrame> getKeyFrame(const std::string& name);
/**
* @brief
* @param type
*/
static void playMusic(MusicType type);
private:
/**
* @brief
* atlas.txt : width height x y
*/
struct ImageInfo {
float width, height, x, y;
};
static extra2d::Ptr<extra2d::Texture> atlasTexture_; // 图集纹理
static std::map<std::string, ImageInfo> imageMap_; // 图片信息映射
static std::map<MusicType, extra2d::Ptr<extra2d::Sound>> soundMap_; // 音效映射
};
} // namespace flappybird

View File

@ -1,49 +0,0 @@
// ============================================================================
// SplashScene.cpp - 启动场景实现
// ============================================================================
#include "SplashScene.h"
#include "ResLoader.h"
#include "StartScene.h"
#include <utils/logger.h>
namespace flappybird {
SplashScene::SplashScene() {
// 基类 BaseScene 已经处理了视口设置和背景颜色
}
void SplashScene::onEnter() {
BaseScene::onEnter();
// 尝试加载 splash 图片
auto splashFrame = ResLoader::getKeyFrame("splash");
if (splashFrame) {
auto splash = extra2d::Sprite::create(splashFrame->getTexture(),
splashFrame->getRect());
splash->setAnchor(0.5f, 0.5f);
// splash 图片是全屏的(288x512),将其中心放在游戏区域中心
splash->setPosition(GAME_WIDTH / 2.0f, GAME_HEIGHT / 2.0f);
addChild(splash);
}
// 播放转场音效
ResLoader::playMusic(MusicType::Swoosh);
}
void SplashScene::onUpdate(float dt) {
BaseScene::onUpdate(dt);
// 计时
timer_ += dt;
if (timer_ >= delay_) {
gotoStartScene();
}
}
void SplashScene::gotoStartScene() {
auto &app = extra2d::Application::instance();
app.scenes().replaceScene(extra2d::makePtr<StartScene>(),
extra2d::TransitionType::Fade, 2.0f);
}
} // namespace flappybird

View File

@ -1,44 +0,0 @@
// ============================================================================
// SplashScene.h - 启动场景
// 描述: 显示游戏 Logo2秒后自动跳转到开始场景
// ============================================================================
#pragma once
#include "BaseScene.h"
namespace flappybird {
/**
* @brief
* Logo
*/
class SplashScene : public BaseScene {
public:
/**
* @brief
*/
SplashScene();
/**
* @brief
*/
void onEnter() override;
/**
* @brief
* @param dt
*/
void onUpdate(float dt) override;
private:
/**
* @brief
*/
void gotoStartScene();
float timer_ = 0.0f; // 计时器
const float delay_ = 2.0f; // 延迟时间(秒)
};
} // namespace flappybird

View File

@ -1,145 +0,0 @@
// ============================================================================
// StartScene.cpp - 开始菜单场景实现
// ============================================================================
#include "StartScene.h"
#include "Bird.h"
#include "GameScene.h"
#include "Ground.h"
#include "ResLoader.h"
#include "event/input_codes.h"
namespace flappybird {
StartScene::StartScene() {
// 基类 BaseScene 已经处理了视口设置和背景颜色
}
void StartScene::onEnter() {
BaseScene::onEnter();
// 使用游戏逻辑分辨率
float screenWidth = GAME_WIDTH;
float screenHeight = GAME_HEIGHT;
// 添加背景(使用左上角锚点)
auto bgFrame = ResLoader::getKeyFrame("bg_day");
if (bgFrame) {
auto background =
extra2d::Sprite::create(bgFrame->getTexture(), bgFrame->getRect());
background->setAnchor(0.0f, 0.0f);
background->setPosition(0.0f, 0.0f);
addChild(background);
E2D_LOG_INFO("背景已添加: size={} x {}", bgFrame->getRect().size.width,
bgFrame->getRect().size.height);
} else {
E2D_LOG_ERROR("无法加载背景图片");
}
// 添加地面
auto ground = extra2d::makePtr<Ground>();
addChild(ground);
// 添加标题图片(在上方)
auto titleFrame = ResLoader::getKeyFrame("title");
if (titleFrame) {
auto title = extra2d::Sprite::create(titleFrame->getTexture(),
titleFrame->getRect());
title->setAnchor(0.5f, 0.5f);
// 标题在屏幕上方
title->setPosition(screenWidth / 2.0f, 150.0f);
addChild(title);
E2D_LOG_INFO("标题已添加: size={} x {}", titleFrame->getRect().size.width,
titleFrame->getRect().size.height);
} else {
E2D_LOG_ERROR("无法加载标题图片");
}
// 添加小鸟(在标题下方)
auto bird = extra2d::makePtr<Bird>();
bird->setAnchor(0.5f, 0.5f);
bird->setPosition(screenWidth / 2.0f, screenHeight / 2.0f);
bird->setStatus(Bird::Status::Idle);
addChild(bird);
// 添加开始按钮 - 在小鸟下方
auto playFrame = ResLoader::getKeyFrame("button_play");
if (playFrame) {
float btnWidth = playFrame->getRect().size.width;
float btnHeight = playFrame->getRect().size.height;
playBtn_ = extra2d::Button::create();
playBtn_->setBackgroundImage(playFrame->getTexture(), playFrame->getRect());
// 使用世界坐标,中心锚点
playBtn_->setAnchor(0.5f, 0.5f);
// PLAY 按钮在小鸟下方
playBtn_->setPosition(screenWidth / 2.0f,
screenHeight - playBtn_->size().height - 100.0f);
playBtn_->setOnClick([this]() {
ResLoader::playMusic(MusicType::Click);
startGame();
});
addChild(playBtn_);
}
// 添加分享按钮 - 在 PLAY 按钮下方,靠近地面
auto shareFrame = ResLoader::getKeyFrame("button_share");
if (shareFrame) {
float btnWidth = shareFrame->getRect().size.width;
float btnHeight = shareFrame->getRect().size.height;
shareBtn_ = extra2d::Button::create();
shareBtn_->setBackgroundImage(shareFrame->getTexture(),
shareFrame->getRect());
// 使用世界坐标,中心锚点
shareBtn_->setAnchor(0.5f, 0.5f);
// SHARE 按钮在 PLAY 按钮下方,靠近地面
shareBtn_->setPosition(screenWidth / 2.0f,
screenHeight - shareBtn_->size().height - 80.0f);
shareBtn_->setOnClick([this]() {
ResLoader::playMusic(MusicType::Click);
// 分享功能暂不实现
});
addChild(shareBtn_);
}
// 添加 copyright 图片(在底部)
auto copyrightFrame = ResLoader::getKeyFrame("brand_copyright");
if (copyrightFrame) {
auto copyright = extra2d::Sprite::create(copyrightFrame->getTexture(),
copyrightFrame->getRect());
copyright->setAnchor(0.5f, 0.5f);
// Copyright 在屏幕底部
copyright->setPosition(screenWidth / 2.0f, screenHeight - 20.0f);
addChild(copyright);
}
// 播放转场音效
ResLoader::playMusic(MusicType::Swoosh);
}
void StartScene::onUpdate(float dt) {
BaseScene::onUpdate(dt);
// 检测 A 键或空格开始游戏
auto &input = extra2d::Application::instance().input();
if (input.isButtonPressed(extra2d::GamepadButton::A)) {
ResLoader::playMusic(MusicType::Click);
startGame();
}
// 检测 BACK 键退出游戏
if (input.isButtonPressed(extra2d::GamepadButton::Start)) {
ResLoader::playMusic(MusicType::Click);
auto &app = extra2d::Application::instance();
app.quit();
}
}
void StartScene::startGame() {
auto &app = extra2d::Application::instance();
app.scenes().replaceScene(extra2d::makePtr<GameScene>(),
extra2d::TransitionType::Fade, 0.5f);
}
} // namespace flappybird

View File

@ -1,53 +0,0 @@
// ============================================================================
// StartScene.h - 开始菜单场景
// 描述: 显示游戏标题、开始按钮和版权信息
// ============================================================================
#pragma once
#include "BaseScene.h"
namespace flappybird {
/**
* @brief
*
*/
class StartScene : public BaseScene {
public:
/**
* @brief
*/
StartScene();
/**
* @brief
*/
void onEnter() override;
/**
* @brief
* @param dt
*/
void onUpdate(float dt) override;
private:
/**
* @brief
*/
void createMenuButtons();
/**
* @brief
*/
void startGame();
extra2d::Ptr<extra2d::Button> playBtn_; // 开始按钮
extra2d::Ptr<extra2d::Button> shareBtn_; // 分享按钮
extra2d::Ptr<extra2d::Sprite> title_; // 标题精灵
float titleFinalY_ = 0.0f; // 标题最终Y位置
float titleAnimTime_ = 0.0f; // 标题动画时间
static constexpr float TITLE_ANIM_DURATION = 0.5f; // 标题动画持续时间
};
} // namespace flappybird

View File

@ -1,195 +0,0 @@
// ============================================================================
// Bird.cpp - 小鸟类实现
// ============================================================================
#include "Bird.h"
#include "ResLoader.h"
namespace flappybird {
Bird::Bird() { setStatus(Status::Idle); }
Bird::~Bird() = default;
void Bird::onEnter() {
Node::onEnter();
if (!sprite_) {
initAnimations();
}
}
void Bird::initAnimations() {
// 随机选择小鸟颜色0-2
int colorMode = extra2d::randomInt(0, 2);
std::string prefix = "bird" + std::to_string(colorMode) + "_";
// 加载动画帧序列: 0 -> 1 -> 2 -> 1
int frameSequence[] = {0, 1, 2, 1};
for (int frameIndex : frameSequence) {
auto frameSprite =
ResLoader::getKeyFrame(prefix + std::to_string(frameIndex));
if (frameSprite) {
frames_.push_back(frameSprite);
} else {
E2D_LOG_WARN("无法加载动画帧: {}{}", prefix, frameIndex);
}
}
// 创建精灵
if (!frames_.empty()) {
sprite_ = extra2d::Sprite::create();
setCurrentFrame(0);
addChild(sprite_);
E2D_LOG_INFO("小鸟动画创建成功: 颜色={}, 帧数={}", colorMode,
frames_.size());
} else {
E2D_LOG_ERROR("小鸟动画创建失败: 没有找到任何动画帧");
}
}
void Bird::setCurrentFrame(int frameIndex) {
if (frames_.empty() || !sprite_)
return;
frameIndex = frameIndex % static_cast<int>(frames_.size());
currentFrame_ = frameIndex;
auto &frame = frames_[frameIndex];
sprite_->setTexture(frame->getTexture());
sprite_->setTextureRect(frame->getRect());
}
void Bird::updateFrameAnimation(float dt) {
if (frames_.empty() || status_ == Status::Still)
return;
frameTimer_ += dt;
float interval = frameInterval_;
if (status_ == Status::StartToFly) {
interval = 0.05f; // 2倍速度
}
while (frameTimer_ >= interval) {
frameTimer_ -= interval;
setCurrentFrame((currentFrame_ + 1) % static_cast<int>(frames_.size()));
}
}
void Bird::onUpdate(float dt) {
extra2d::Node::onUpdate(dt);
// 更新帧动画
updateFrameAnimation(dt);
// 处理闲置动画(上下浮动)
if (status_ == Status::Idle) {
idleTimer_ += dt;
idleOffset_ = std::sin(idleTimer_ * 5.0f) * 4.0f;
}
}
void Bird::onRender(extra2d::Renderer &renderer) {
// 精灵会自动渲染,这里只需要处理旋转和偏移
if (sprite_) {
sprite_->setRotation(rotation_);
// 应用闲置偏移
if (status_ == Status::Idle) {
sprite_->setPosition(extra2d::Vec2(0.0f, idleOffset_));
} else {
sprite_->setPosition(extra2d::Vec2(0.0f, 0.0f));
}
}
// 调用父类的 onRender 来渲染子节点
Node::onRender(renderer);
}
void Bird::fall(float dt) {
if (!living_)
return;
// 更新垂直位置
extra2d::Vec2 position = pos();
position.y += speed_ * dt;
setPosition(position);
// 应用重力
speed_ += gravity * dt;
// 限制顶部边界
if (position.y < 0) {
position.y = 0;
setPosition(position);
speed_ = 0;
}
// 根据速度计算旋转角度
// 上升时抬头(-15度),下降时低头(最大90度)
if (speed_ < 0) {
rotation_ = -15.0f;
} else {
rotation_ = std::min(90.0f, speed_ * 0.15f);
}
}
void Bird::jump() {
if (!living_)
return;
// 给小鸟向上的速度
speed_ = -jumpSpeed;
// 设置状态为飞行
setStatus(Status::Fly);
// 播放音效
ResLoader::playMusic(MusicType::Fly);
}
void Bird::die() {
living_ = false;
// 播放死亡音效
ResLoader::playMusic(MusicType::Hit);
}
void Bird::setStatus(Status status) {
status_ = status;
switch (status) {
case Status::Still:
// 停止所有动画
frameTimer_ = 0.0f;
break;
case Status::Idle:
// 开始闲置动画
frameInterval_ = 0.1f; // 正常速度
idleTimer_ = 0.0f;
break;
case Status::StartToFly:
// 停止闲置动画,加速翅膀扇动
idleOffset_ = 0.0f;
break;
case Status::Fly:
// 飞行状态
break;
default:
break;
}
}
extra2d::Rect Bird::boundingBox() const {
extra2d::Vec2 position = pos();
// 小鸟碰撞框大小约为 24x24
float halfSize = 12.0f;
return extra2d::Rect(position.x - halfSize, position.y - halfSize,
halfSize * 2.0f, halfSize * 2.0f);
}
} // namespace flappybird

View File

@ -1,130 +0,0 @@
// ============================================================================
// Bird.h - 小鸟类
// 描述: 玩家控制的小鸟角色,包含飞行动画和物理效果
// ============================================================================
#pragma once
#include <extra2d.h>
namespace flappybird {
/**
* @brief
*
*/
class Bird : public extra2d::Node {
public:
/**
* @brief
*/
enum class Status {
Still, // 静止不动
Idle, // 上下浮动(菜单展示)
StartToFly, // 开始飞行
Fly // 飞行中
};
/**
* @brief
*/
Bird();
/**
* @brief
*/
~Bird();
/**
* @brief
* @param dt
*/
void onUpdate(float dt) override;
/**
* @brief
* @param renderer
*/
void onRender(extra2d::Renderer &renderer) override;
/**
* @brief
*/
void onEnter() override;
/**
* @brief
* @param dt
*/
void fall(float dt);
/**
* @brief
*/
void jump();
/**
* @brief
*/
void die();
/**
* @brief
* @param status
*/
void setStatus(Status status);
/**
* @brief
* @return
*/
Status getStatus() const { return status_; }
/**
* @brief
* @return
*/
bool isLiving() const { return living_; }
/**
* @brief
* @return
*/
extra2d::Rect boundingBox() const override;
private:
/**
* @brief
*/
void initAnimations();
/**
* @brief
*/
void updateFrameAnimation(float dt);
/**
* @brief
*/
void setCurrentFrame(int frameIndex);
bool living_ = true; // 是否存活
float speed_ = 0.0f; // 垂直速度
float rotation_ = 0.0f; // 旋转角度
Status status_ = Status::Idle; // 当前状态
// 动画相关
extra2d::Ptr<extra2d::Sprite> sprite_; // 精灵
std::vector<extra2d::Ptr<extra2d::SpriteFrame>> frames_; // 动画帧
int currentFrame_ = 0; // 当前帧索引
float frameTimer_ = 0.0f; // 帧计时器
float frameInterval_ = 0.1f; // 帧间隔(秒)
float idleTimer_ = 0.0f; // 闲置动画计时器
float idleOffset_ = 0.0f; // 闲置偏移量
// 物理常量
static constexpr float gravity = 1440.0f; // 重力加速度
static constexpr float jumpSpeed = 432.0f; // 跳跃初速度
};
} // namespace flappybird

View File

@ -1,79 +0,0 @@
// ============================================================================
// Ground.cpp - 地面类实现
// ============================================================================
#include "Ground.h"
#include "BaseScene.h"
#include "ResLoader.h"
namespace flappybird {
Ground::Ground() {
moving_ = true;
// 使用游戏逻辑高度,而不是窗口高度
float screenHeight = GAME_HEIGHT;
// 获取地面纹理帧
auto landFrame = ResLoader::getKeyFrame("land");
if (!landFrame)
return;
// 获取地面纹理和矩形
auto texture = landFrame->getTexture();
auto rect = landFrame->getRect();
float groundWidth = rect.size.width;
float groundHeight = rect.size.height;
// 创建第一块地面
ground1_ = extra2d::Sprite::create(texture, rect);
ground1_->setAnchor(extra2d::Vec2(0.0f, 1.0f)); // 锚点设在左下角
ground1_->setPosition(extra2d::Vec2(0.0f, screenHeight));
addChild(ground1_);
// 创建第二块地面,紧挨在第一块右边
ground2_ = extra2d::Sprite::create(texture, rect);
ground2_->setAnchor(extra2d::Vec2(0.0f, 1.0f));
ground2_->setPosition(extra2d::Vec2(groundWidth - 1.0f, screenHeight));
addChild(ground2_);
}
void Ground::onUpdate(float dt) {
extra2d::Node::onUpdate(dt);
if (!moving_)
return;
if (!ground1_ || !ground2_)
return;
// 获取地面宽度(从纹理矩形获取)
float groundWidth = ground1_->getTextureRect().size.width;
// 移动两块地面
extra2d::Vec2 pos1 = ground1_->pos();
extra2d::Vec2 pos2 = ground2_->pos();
pos1.x -= speed * dt;
pos2.x -= speed * dt;
// 当地面完全移出屏幕左侧时,重置到右侧
if (pos1.x <= -groundWidth) {
pos1.x = pos2.x + groundWidth - 1.0f;
}
if (pos2.x <= -groundWidth) {
pos2.x = pos1.x + groundWidth - 1.0f;
}
ground1_->setPosition(pos1);
ground2_->setPosition(pos2);
}
void Ground::stop() { moving_ = false; }
float Ground::height() const {
auto landFrame = ResLoader::getKeyFrame("land");
return landFrame ? landFrame->getRect().size.height : 112.0f;
}
} // namespace flappybird

View File

@ -1,48 +0,0 @@
// ============================================================================
// Ground.h - 地面类
// 描述: 游戏底部不断向左滚动的地面
// ============================================================================
#pragma once
#include <extra2d.h>
namespace flappybird {
/**
* @brief
*
*/
class Ground : public extra2d::Node {
public:
/**
* @brief
*/
Ground();
/**
* @brief
* @param dt
*/
void onUpdate(float dt) override;
/**
* @brief
*/
void stop();
/**
* @brief
* @return
*/
float height() const;
private:
extra2d::Ptr<extra2d::Sprite> ground1_; // 第一块地面
extra2d::Ptr<extra2d::Sprite> ground2_; // 第二块地面
static constexpr float speed = 120.0f; // 滚动速度(像素/秒)
bool moving_ = true; // 是否正在滚动
};
} // namespace flappybird

View File

@ -1,53 +0,0 @@
// ============================================================================
// FlappyBird - Extra2D 示例程序
// 作者: Extra2D Team
// 描述: 经典的 Flappy Bird 游戏实现
// ============================================================================
#include "ResLoader.h"
#include "SplashScene.h"
#include <extra2d.h>
using namespace extra2d;
/**
* @brief
*/
int main(int argc, char **argv) {
// 初始化日志系统
Logger::init();
Logger::setLevel(LogLevel::Debug);
E2D_LOG_INFO("========================");
E2D_LOG_INFO("Extra2D FlappyBird");
E2D_LOG_INFO("========================");
// 获取应用实例
auto &app = Application::instance();
// 配置应用
AppConfig config;
config.title = "Extra2D - FlappyBird";
config.width = 1280; // 窗口宽度 (720P 分辨率)
config.height = 720; // 窗口高度 (720P 分辨率)
config.vsync = true;
config.fpsLimit = 60;
// 初始化应用
if (!app.init(config)) {
E2D_LOG_ERROR("应用初始化失败!");
return -1;
}
// 初始化资源加载器
flappybird::ResLoader::init();
// 进入启动场景
app.enterScene(makePtr<flappybird::SplashScene>());
E2D_LOG_INFO("开始主循环...");
app.run();
E2D_LOG_INFO("应用结束");
return 0;
}

View File

@ -1,107 +0,0 @@
// ============================================================================
// Pipe.cpp - 水管类实现
// ============================================================================
#include "Pipe.h"
#include "BaseScene.h"
#include "ResLoader.h"
namespace flappybird {
Pipe::Pipe() {
scored = false;
// 注意:不要在构造函数中创建子节点
// 因为此时 weak_from_this() 还不能使用
}
void Pipe::onEnter() {
Node::onEnter();
// 在 onEnter 中创建子节点,此时 weak_from_this() 可用
if (!topPipe_ && !bottomPipe_) {
// 使用游戏逻辑高度
float screenHeight = GAME_HEIGHT;
// 获取地面高度
auto landFrame = ResLoader::getKeyFrame("land");
float landHeight = landFrame ? landFrame->getRect().size.height : 112.0f;
// 随机生成水管高度
// 范围:与屏幕顶部最小距离不小于 100 像素
// 与屏幕底部最小距离不小于地面上方 100 像素
float minHeight = 100.0f;
float maxHeight = screenHeight - landHeight - 100.0f - gapHeight_;
float height = static_cast<float>(extra2d::randomInt(
static_cast<int>(minHeight), static_cast<int>(maxHeight)));
// 创建上水管
auto topFrame = ResLoader::getKeyFrame("pipe_above");
if (topFrame) {
topPipe_ =
extra2d::Sprite::create(topFrame->getTexture(), topFrame->getRect());
topPipe_->setAnchor(extra2d::Vec2(0.5f, 1.0f)); // 锚点设在底部中心
topPipe_->setPosition(extra2d::Vec2(0.0f, height - gapHeight_ / 2.0f));
addChild(topPipe_);
}
// 创建下水管
auto bottomFrame = ResLoader::getKeyFrame("pipe_below");
if (bottomFrame) {
bottomPipe_ = extra2d::Sprite::create(bottomFrame->getTexture(),
bottomFrame->getRect());
bottomPipe_->setAnchor(extra2d::Vec2(0.5f, 0.0f)); // 锚点设在顶部中心
bottomPipe_->setPosition(extra2d::Vec2(0.0f, height + gapHeight_ / 2.0f));
addChild(bottomPipe_);
}
}
}
Pipe::~Pipe() = default;
extra2d::Rect Pipe::boundingBox() const {
// 返回整个水管的边界框(包含上下两根)
extra2d::Vec2 position = pos();
// 水管宽度约为 52
float pipeWidth = 52.0f;
float halfWidth = pipeWidth / 2.0f;
// 使用游戏逻辑高度
float screenHeight = GAME_HEIGHT;
return extra2d::Rect(position.x - halfWidth, 0.0f, pipeWidth, screenHeight);
}
extra2d::Rect Pipe::getTopPipeBox() const {
if (!topPipe_)
return extra2d::Rect();
extra2d::Vec2 position = pos();
extra2d::Vec2 topPos = topPipe_->pos();
// 上水管尺寸
float pipeWidth = 52.0f;
float pipeHeight = 320.0f;
return extra2d::Rect(position.x - pipeWidth / 2.0f,
position.y + topPos.y - pipeHeight, pipeWidth,
pipeHeight);
}
extra2d::Rect Pipe::getBottomPipeBox() const {
if (!bottomPipe_)
return extra2d::Rect();
extra2d::Vec2 position = pos();
extra2d::Vec2 bottomPos = bottomPipe_->pos();
// 下水管尺寸
float pipeWidth = 52.0f;
float pipeHeight = 320.0f;
return extra2d::Rect(position.x - pipeWidth / 2.0f, position.y + bottomPos.y,
pipeWidth, pipeHeight);
}
} // namespace flappybird

View File

@ -1,59 +0,0 @@
// ============================================================================
// Pipe.h - 水管类
// 描述: 游戏中的障碍物,由上下两根水管组成
// ============================================================================
#pragma once
#include <extra2d.h>
namespace flappybird {
/**
* @brief
*
*/
class Pipe : public extra2d::Node {
public:
/**
* @brief
*/
Pipe();
/**
* @brief
*/
~Pipe();
/**
* @brief
*/
void onEnter() override;
/**
* @brief
* @return
*/
extra2d::Rect boundingBox() const override;
/**
* @brief
* @return
*/
extra2d::Rect getTopPipeBox() const;
/**
* @brief
* @return
*/
extra2d::Rect getBottomPipeBox() const;
bool scored = false; // 是否已计分
private:
extra2d::Ptr<extra2d::Sprite> topPipe_; // 上水管
extra2d::Ptr<extra2d::Sprite> bottomPipe_; // 下水管
float gapHeight_ = 120.0f; // 间隙高度
};
} // namespace flappybird

View File

@ -1,106 +0,0 @@
// ============================================================================
// Pipes.cpp - 水管管理器实现
// ============================================================================
#include "Pipes.h"
#include "BaseScene.h"
namespace flappybird {
Pipes::Pipes() {
pipeCount_ = 0;
moving_ = false;
// 初始化水管数组
for (int i = 0; i < maxPipes; ++i) {
pipes_[i] = nullptr;
}
// 注意:不要在构造函数中添加水管
// 因为此时 weak_from_this() 还不能使用
}
void Pipes::onEnter() {
Node::onEnter();
// 在 onEnter 中初始化水管,此时 weak_from_this() 可用
if (pipeCount_ == 0) {
addPipe();
addPipe();
addPipe();
}
}
Pipes::~Pipes() = default;
void Pipes::onUpdate(float dt) {
extra2d::Node::onUpdate(dt);
if (!moving_) return;
// 移动所有水管
for (int i = 0; i < pipeCount_; ++i) {
if (pipes_[i]) {
extra2d::Vec2 pos = pipes_[i]->pos();
pos.x -= pipeSpeed * dt;
pipes_[i]->setPosition(pos);
}
}
// 检查最前面的水管是否移出屏幕
if (pipes_[0] && pipes_[0]->pos().x <= -30.0f) {
// 移除第一个水管(通过名称查找并移除)
// 由于 removeChild 需要 Ptr<Node>,我们使用 removeChildByName 或直接操作
// 这里我们直接移除第一个子节点(假设它是水管)
auto childNodes = children();
if (!childNodes.empty()) {
removeChild(childNodes[0]);
}
// 将后面的水管前移
for (int i = 0; i < pipeCount_ - 1; ++i) {
pipes_[i] = pipes_[i + 1];
}
pipes_[pipeCount_ - 1] = nullptr;
pipeCount_--;
// 添加新水管
addPipe();
}
}
void Pipes::addPipe() {
if (pipeCount_ >= maxPipes) return;
// 创建新水管
auto pipe = extra2d::makePtr<Pipe>();
// 设置水管位置
if (pipeCount_ == 0) {
// 第一个水管在屏幕外 130 像素处
pipe->setPosition(extra2d::Vec2(
GAME_WIDTH + 130.0f,
0.0f
));
} else {
// 其他水管在前一个水管后方
float prevX = pipes_[pipeCount_ - 1]->pos().x;
pipe->setPosition(extra2d::Vec2(prevX + pipeSpacing, 0.0f));
}
// 保存水管指针
pipes_[pipeCount_] = pipe.get();
pipeCount_++;
// 添加到场景
addChild(pipe);
}
void Pipes::start() {
moving_ = true;
}
void Pipes::stop() {
moving_ = false;
}
} // namespace flappybird

View File

@ -1,71 +0,0 @@
// ============================================================================
// Pipes.h - 水管管理器
// 描述: 管理多个水管的生成、移动和回收
// ============================================================================
#pragma once
#include <extra2d.h>
#include "Pipe.h"
namespace flappybird {
/**
* @brief
*
*/
class Pipes : public extra2d::Node {
public:
/**
* @brief
*/
Pipes();
/**
* @brief
*/
~Pipes();
/**
* @brief
* @param dt
*/
void onUpdate(float dt) override;
/**
* @brief
*/
void onEnter() override;
/**
* @brief
*/
void start();
/**
* @brief
*/
void stop();
/**
* @brief
* @return
*/
Pipe* getPipe(int index) { return (index >= 0 && index < 3) ? pipes_[index] : nullptr; }
private:
/**
* @brief
*/
void addPipe();
static constexpr int maxPipes = 3; // 最大水管数量
static constexpr float pipeSpeed = 120.0f; // 水管移动速度(像素/秒)
static constexpr float pipeSpacing = 200.0f; // 水管间距
Pipe* pipes_[maxPipes]; // 水管数组
int pipeCount_ = 0; // 当前水管数量
bool moving_ = false; // 是否正在移动
};
} // namespace flappybird

View File

@ -1,78 +0,0 @@
{
"sprites": [
{"name": "splash", "width": 288, "height": 512, "x": 292, "y": 515},
{"name": "bg_day", "width": 288, "height": 512, "x": 0, "y": 0},
{"name": "bg_night", "width": 288, "height": 512, "x": 292, "y": 0},
{"name": "bird0_0", "width": 34, "height": 24, "x": 5, "y": 982},
{"name": "bird0_1", "width": 34, "height": 24, "x": 61, "y": 982},
{"name": "bird0_2", "width": 34, "height": 24, "x": 117, "y": 982},
{"name": "bird1_0", "width": 34, "height": 24, "x": 173, "y": 982},
{"name": "bird1_1", "width": 34, "height": 24, "x": 229, "y": 658},
{"name": "bird1_2", "width": 34, "height": 24, "x": 229, "y": 710},
{"name": "bird2_0", "width": 34, "height": 24, "x": 229, "y": 762},
{"name": "bird2_1", "width": 34, "height": 24, "x": 229, "y": 814},
{"name": "bird2_2", "width": 34, "height": 24, "x": 229, "y": 866},
{"name": "black", "width": 32, "height": 32, "x": 584, "y": 412},
{"name": "blink_00", "width": 10, "height": 10, "x": 276, "y": 682},
{"name": "blink_01", "width": 10, "height": 10, "x": 276, "y": 734},
{"name": "blink_02", "width": 10, "height": 10, "x": 276, "y": 786},
{"name": "brand_copyright", "width": 126, "height": 14, "x": 884, "y": 182},
{"name": "button_ok", "width": 80, "height": 28, "x": 924, "y": 84},
{"name": "button_pause", "width": 26, "height": 28, "x": 242, "y": 612},
{"name": "button_rate", "width": 74, "height": 48, "x": 924, "y": 0},
{"name": "button_resume", "width": 26, "height": 28, "x": 668, "y": 284},
{"name": "button_score", "width": 116, "height": 70, "x": 822, "y": 234},
{"name": "button_restart", "width": 116, "height": 70, "x": 702, "y": 316},
{"name": "button_share_big", "width": 116, "height": 70, "x": 822, "y": 316},
{"name": "button_play", "width": 116, "height": 70, "x": 702, "y": 234},
{"name": "button_menu", "width": 80, "height": 28, "x": 924, "y": 52},
{"name": "button_share", "width": 80, "height": 28, "x": 584, "y": 284},
{"name": "land", "width": 336, "height": 112, "x": 584, "y": 0},
{"name": "medals_0", "width": 44, "height": 44, "x": 224, "y": 954},
{"name": "medals_1", "width": 44, "height": 44, "x": 224, "y": 906},
{"name": "medals_2", "width": 44, "height": 44, "x": 242, "y": 564},
{"name": "medals_3", "width": 44, "height": 44, "x": 242, "y": 516},
{"name": "new", "width": 32, "height": 14, "x": 224, "y": 1002},
{"name": "number_big_0", "width": 24, "height": 36, "x": 992, "y": 120},
{"name": "number_big_1", "width": 16, "height": 36, "x": 272, "y": 910},
{"name": "number_big_2", "width": 24, "height": 36, "x": 584, "y": 320},
{"name": "number_big_3", "width": 24, "height": 36, "x": 612, "y": 320},
{"name": "number_big_4", "width": 24, "height": 36, "x": 640, "y": 320},
{"name": "number_big_5", "width": 24, "height": 36, "x": 668, "y": 320},
{"name": "number_big_6", "width": 24, "height": 36, "x": 584, "y": 368},
{"name": "number_big_7", "width": 24, "height": 36, "x": 612, "y": 368},
{"name": "number_big_8", "width": 24, "height": 36, "x": 640, "y": 368},
{"name": "number_big_9", "width": 24, "height": 36, "x": 668, "y": 368},
{"name": "number_medium_0", "width": 16, "height": 20, "x": 272, "y": 612},
{"name": "number_medium_1", "width": 16, "height": 20, "x": 272, "y": 954},
{"name": "number_medium_2", "width": 16, "height": 20, "x": 272, "y": 978},
{"name": "number_medium_3", "width": 16, "height": 20, "x": 260, "y": 1002},
{"name": "number_medium_4", "width": 16, "height": 20, "x": 1002, "y": 0},
{"name": "number_medium_5", "width": 16, "height": 20, "x": 1002, "y": 24},
{"name": "number_medium_6", "width": 16, "height": 20, "x": 1008, "y": 52},
{"name": "number_medium_7", "width": 16, "height": 20, "x": 1008, "y": 84},
{"name": "number_medium_8", "width": 16, "height": 20, "x": 584, "y": 484},
{"name": "number_medium_9", "width": 16, "height": 20, "x": 620, "y": 412},
{"name": "number_small_0", "width": 12, "height": 14, "x": 276, "y": 646},
{"name": "number_small_1", "width": 12, "height": 14, "x": 276, "y": 664},
{"name": "number_small_2", "width": 12, "height": 14, "x": 276, "y": 698},
{"name": "number_small_3", "width": 12, "height": 14, "x": 276, "y": 716},
{"name": "number_small_4", "width": 12, "height": 14, "x": 276, "y": 750},
{"name": "number_small_5", "width": 12, "height": 14, "x": 276, "y": 768},
{"name": "number_small_6", "width": 12, "height": 14, "x": 276, "y": 802},
{"name": "number_small_7", "width": 12, "height": 14, "x": 276, "y": 820},
{"name": "number_small_8", "width": 12, "height": 14, "x": 276, "y": 854},
{"name": "number_small_9", "width": 12, "height": 14, "x": 276, "y": 872},
{"name": "number_context_10", "width": 12, "height": 14, "x": 992, "y": 164},
{"name": "pipe_above_2", "width": 52, "height": 320, "x": 0, "y": 646},
{"name": "pipe_below_2", "width": 52, "height": 320, "x": 56, "y": 646},
{"name": "pipe_above", "width": 52, "height": 320, "x": 112, "y": 646},
{"name": "pipe_below", "width": 52, "height": 320, "x": 168, "y": 646},
{"name": "score_panel", "width": 238, "height": 126, "x": 0, "y": 516},
{"name": "text_game_over", "width": 204, "height": 54, "x": 784, "y": 116},
{"name": "text_ready", "width": 196, "height": 62, "x": 584, "y": 116},
{"name": "title", "width": 178, "height": 48, "x": 702, "y": 182},
{"name": "tutorial", "width": 114, "height": 98, "x": 584, "y": 182},
{"name": "white", "width": 32, "height": 32, "x": 584, "y": 448}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

View File

@ -1,76 +0,0 @@
-- ==============================================
-- FlappyBird 示例 - Xmake 构建脚本
-- 支持平台: MinGW (Windows), Nintendo Switch
-- ==============================================
-- 获取当前脚本所在目录(示例根目录)
local example_dir = os.scriptdir()
-- 可执行文件目标
target("flappy_bird")
set_kind("binary")
add_files("*.cpp")
add_includedirs("../../include")
add_deps("extra2d")
-- 使用与主项目相同的平台配置
if is_plat("switch") then
set_targetdir("../../build/examples/flappy_bird")
-- 构建后生成 NRO 文件
after_build(function (target)
local devkitPro = os.getenv("DEVKITPRO") or "C:/devkitPro"
local elf_file = target:targetfile()
local output_dir = path.directory(elf_file)
local nacp_file = path.join(output_dir, "flappy_bird.nacp")
local nro_file = path.join(output_dir, "flappy_bird.nro")
local nacptool = path.join(devkitPro, "tools/bin/nacptool.exe")
local elf2nro = path.join(devkitPro, "tools/bin/elf2nro.exe")
if os.isfile(nacptool) and os.isfile(elf2nro) then
os.vrunv(nacptool, {"--create", "FlappyBird", "Extra2D Team", "1.0.0", nacp_file})
local romfs = path.join(example_dir, "romfs")
if os.isdir(romfs) then
os.vrunv(elf2nro, {elf_file, nro_file, "--nacp=" .. nacp_file, "--romfsdir=" .. romfs})
else
os.vrunv(elf2nro, {elf_file, nro_file, "--nacp=" .. nacp_file})
end
print("Generated NRO: " .. nro_file)
end
end)
-- 打包时将 NRO 文件复制到 package 目录
after_package(function (target)
local nro_file = path.join(target:targetdir(), "flappy_bird.nro")
local package_dir = target:packagedir()
if os.isfile(nro_file) and package_dir then
os.cp(nro_file, package_dir)
print("Copied NRO to package: " .. package_dir)
end
end)
elseif is_plat("mingw") then
set_targetdir("../../build/examples/flappy_bird")
-- add_ldflags("-mwindows", {force = true})
-- 复制资源到输出目录
after_build(function (target)
local romfs = path.join(example_dir, "romfs")
if os.isdir(romfs) then
local target_dir = path.directory(target:targetfile())
local assets_dir = path.join(target_dir, "assets")
-- 创建 assets 目录
if not os.isdir(assets_dir) then
os.mkdir(assets_dir)
end
-- 复制所有资源文件(包括子目录)
os.cp(path.join(romfs, "assets/**"), assets_dir)
print("Copied assets from " .. romfs .. " to " .. assets_dir)
else
print("Warning: romfs directory not found at " .. romfs)
end
end)
end
target_end()

View File

@ -1,138 +0,0 @@
// ============================================================================
// BaseScene.cpp - Push Box 基础场景实现
// ============================================================================
#include "BaseScene.h"
#include <scene/transition_scene.h>
#include <utils/logger.h>
namespace pushbox {
BaseScene::BaseScene() {
// 设置背景颜色为黑色(窗口四周会显示这个颜色)
setBackgroundColor(extra2d::Color(0.0f, 0.0f, 0.0f, 1.0f));
}
void BaseScene::onEnter() {
extra2d::Scene::onEnter();
// 计算并更新视口
updateViewport();
}
/**
* @brief 使
*/
void BaseScene::updateViewport() {
auto &app = extra2d::Application::instance();
float windowWidth = static_cast<float>(app.window().width());
float windowHeight = static_cast<float>(app.window().height());
// 计算游戏内容在窗口中的居中位置
// 保持游戏原始宽高比,进行"黑边"适配
float scaleX = windowWidth / GAME_WIDTH;
float scaleY = windowHeight / GAME_HEIGHT;
// 使用较小的缩放比例,确保游戏内容完整显示在窗口中
float scale = std::min(scaleX, scaleY);
scaledGameWidth_ = GAME_WIDTH * scale;
scaledGameHeight_ = GAME_HEIGHT * scale;
// 计算居中偏移,使游戏内容在窗口中水平和垂直居中
viewportOffsetX_ = (windowWidth - scaledGameWidth_) * 0.5f;
viewportOffsetY_ = (windowHeight - scaledGameHeight_) * 0.5f;
// 设置视口大小为游戏逻辑分辨率
setViewportSize(GAME_WIDTH, GAME_HEIGHT);
// 创建并设置相机
auto camera = extra2d::makePtr<extra2d::Camera>();
// 设置正交投影,覆盖整个游戏逻辑区域
// 注意对于2D游戏Y轴向下增长所以bottom > top
camera->setViewport(0.0f, GAME_WIDTH, GAME_HEIGHT, 0.0f);
setCamera(camera);
}
/**
* @brief
* @param renderer
*/
void BaseScene::onRender(extra2d::Renderer &renderer) {
// 检查窗口大小是否改变,如果改变则更新视口
auto &app = extra2d::Application::instance();
float currentWindowWidth = static_cast<float>(app.window().width());
float currentWindowHeight = static_cast<float>(app.window().height());
// 如果窗口大小改变,重新计算视口
float expectedWidth = scaledGameWidth_ + viewportOffsetX_ * 2.0f;
float expectedHeight = scaledGameHeight_ + viewportOffsetY_ * 2.0f;
if (std::abs(currentWindowWidth - expectedWidth) > 1.0f ||
std::abs(currentWindowHeight - expectedHeight) > 1.0f) {
E2D_LOG_INFO("BaseScene::onRender - window size changed from ({} x {}) to "
"({} x {}), updating viewport",
expectedWidth, expectedHeight, currentWindowWidth,
currentWindowHeight);
updateViewport();
}
// 设置视口为居中区域
renderer.setViewport(
static_cast<int>(viewportOffsetX_), static_cast<int>(viewportOffsetY_),
static_cast<int>(scaledGameWidth_), static_cast<int>(scaledGameHeight_));
// 调用父类的 onRender 进行实际渲染
extra2d::Scene::onRender(renderer);
}
/**
* @brief
* @param renderer
*/
void BaseScene::renderContent(extra2d::Renderer &renderer) {
// 如果视口参数未初始化onEnter 还没被调用),先初始化
if (scaledGameWidth_ <= 0.0f || scaledGameHeight_ <= 0.0f) {
updateViewport();
}
// 检查窗口大小是否改变
auto &app = extra2d::Application::instance();
float currentWindowWidth = static_cast<float>(app.window().width());
float currentWindowHeight = static_cast<float>(app.window().height());
float expectedWidth = scaledGameWidth_ + viewportOffsetX_ * 2.0f;
float expectedHeight = scaledGameHeight_ + viewportOffsetY_ * 2.0f;
if (std::abs(currentWindowWidth - expectedWidth) > 1.0f ||
std::abs(currentWindowHeight - expectedHeight) > 1.0f) {
updateViewport();
}
// 检查当前场景是否作为 TransitionScene 的子场景被渲染
bool isChildOfTransition = false;
if (auto parentNode = parent()) {
if (dynamic_cast<extra2d::TransitionScene *>(parentNode.get())) {
isChildOfTransition = true;
}
}
if (isChildOfTransition) {
// 作为 TransitionScene 的子场景时,需要设置正确的投影矩阵
// 使用游戏逻辑分辨率作为投影区域,让 TransitionScene 控制整体视口
auto camera = getActiveCamera();
if (camera) {
// 设置投影矩阵覆盖整个游戏逻辑区域
renderer.setViewProjection(camera->getViewProjectionMatrix());
}
// 渲染场景内容(投影矩阵已设置,直接渲染)
batchUpdateTransforms();
renderer.beginSpriteBatch();
render(renderer);
renderer.endSpriteBatch();
} else {
// 正常渲染时,调用父类的 renderContent 处理视口和投影
renderer.setViewport(static_cast<int>(viewportOffsetX_),
static_cast<int>(viewportOffsetY_),
static_cast<int>(scaledGameWidth_),
static_cast<int>(scaledGameHeight_));
extra2d::Scene::renderContent(renderer);
}
}
} // namespace pushbox

View File

@ -1,57 +0,0 @@
// ============================================================================
// BaseScene.h - Push Box 基础场景类
// 描述: 提供统一的居中视口适配功能,所有游戏场景都应继承此类
// ============================================================================
#pragma once
#include <extra2d.h>
namespace pushbox {
// 游戏逻辑分辨率
static constexpr float GAME_WIDTH = 640.0f;
static constexpr float GAME_HEIGHT = 480.0f;
/**
* @brief Push Box
*
*/
class BaseScene : public extra2d::Scene {
public:
/**
* @brief
*/
BaseScene();
/**
* @brief
*/
void onEnter() override;
/**
* @brief
* @param renderer
*/
void onRender(extra2d::Renderer &renderer) override;
/**
* @brief
* @param renderer
*/
void renderContent(extra2d::Renderer &renderer) override;
protected:
/**
* @brief 使
*/
void updateViewport();
// 视口适配参数(用于在窗口中居中显示游戏内容)
float scaledGameWidth_ = 0.0f; // 缩放后的游戏宽度
float scaledGameHeight_ = 0.0f; // 缩放后的游戏高度
float viewportOffsetX_ = 0.0f; // 视口水平偏移
float viewportOffsetY_ = 0.0f; // 视口垂直偏移
};
} // namespace pushbox

View File

@ -1,488 +0,0 @@
// ============================================================================
// PlayScene.cpp - Push Box 游戏场景实现
// ============================================================================
#include "PlayScene.h"
#include "StartScene.h"
#include "SuccessScene.h"
#include "audio_manager.h"
#include "storage.h"
#include <extra2d.h>
#include <utils/object_pool.h>
namespace pushbox {
/**
* @brief
* @param size
*/
static extra2d::Ptr<extra2d::FontAtlas> loadFont(int size) {
auto &resources = extra2d::Application::instance().resources();
auto font = resources.loadFont("assets/font.ttf", size);
return font;
}
PlayScene::PlayScene(int level) : BaseScene() {
auto &app = extra2d::Application::instance();
auto &resources = app.resources();
E2D_LOG_INFO("PlayScene: Loading textures...");
texWall_ = resources.loadTexture("assets/images/wall.gif");
texPoint_ = resources.loadTexture("assets/images/point.gif");
texFloor_ = resources.loadTexture("assets/images/floor.gif");
texBox_ = resources.loadTexture("assets/images/box.gif");
texBoxInPoint_ = resources.loadTexture("assets/images/boxinpoint.gif");
texMan_[1] = resources.loadTexture("assets/images/player/manup.gif");
texMan_[2] = resources.loadTexture("assets/images/player/mandown.gif");
texMan_[3] = resources.loadTexture("assets/images/player/manleft.gif");
texMan_[4] = resources.loadTexture("assets/images/player/manright.gif");
texManPush_[1] = resources.loadTexture("assets/images/player/manhandup.gif");
texManPush_[2] =
resources.loadTexture("assets/images/player/manhanddown.gif");
texManPush_[3] =
resources.loadTexture("assets/images/player/manhandleft.gif");
texManPush_[4] =
resources.loadTexture("assets/images/player/manhandright.gif");
font28_ = loadFont(28);
font20_ = loadFont(20);
// 使用游戏逻辑分辨率
float screenW = GAME_WIDTH;
float screenH = GAME_HEIGHT;
// 计算游戏区域居中偏移
float offsetX = (screenW - GAME_WIDTH) / 2.0f;
float offsetY = (screenH - GAME_HEIGHT) / 2.0f;
// 音效开关按钮(使用 Button 的切换模式)
auto soundOn = resources.loadTexture("assets/images/soundon.png");
auto soundOff = resources.loadTexture("assets/images/soundoff.png");
if (soundOn && soundOff) {
soundBtn_ = extra2d::Button::create();
soundBtn_->setToggleMode(true);
soundBtn_->setStateBackgroundImage(soundOff, soundOn);
soundBtn_->setOn(g_SoundOpen);
soundBtn_->setAnchor(0.0f, 0.0f);
soundBtn_->setPosition(offsetX + 50.0f, offsetY + 50.0f);
soundBtn_->setOnStateChange([](bool isOn) {
g_SoundOpen = isOn;
AudioManager::instance().setEnabled(isOn);
});
addChild(soundBtn_);
}
levelText_ = extra2d::Text::create("", font28_);
levelText_->setPosition(offsetX + 520.0f, offsetY + 30.0f);
levelText_->setTextColor(extra2d::Colors::White);
addChild(levelText_);
stepText_ = extra2d::Text::create("", font20_);
stepText_->setPosition(offsetX + 520.0f, offsetY + 100.0f);
stepText_->setTextColor(extra2d::Colors::White);
addChild(stepText_);
bestText_ = extra2d::Text::create("", font20_);
bestText_->setPosition(offsetX + 520.0f, offsetY + 140.0f);
bestText_->setTextColor(extra2d::Colors::White);
addChild(bestText_);
// 创建菜单文本(使用颜色变化指示选中)
restartText_ = extra2d::Text::create("Y键重开", font20_);
restartText_->setPosition(offsetX + 520.0f, offsetY + 290.0f);
addChild(restartText_);
soundToggleText_ = extra2d::Text::create("X键切换音效", font20_);
soundToggleText_->setPosition(offsetX + 520.0f, offsetY + 330.0f);
addChild(soundToggleText_);
// 撤销提示(对象池使用示例)
undoText_ = extra2d::Text::create("Z键撤销", font20_);
undoText_->setPosition(offsetX + 520.0f, offsetY + 370.0f);
addChild(undoText_);
mapLayer_ = extra2d::makePtr<extra2d::Node>();
mapLayer_->setAnchor(0.0f, 0.0f);
mapLayer_->setPosition(0.0f, 0.0f);
addChild(mapLayer_);
setLevel(level);
}
void PlayScene::onEnter() {
BaseScene::onEnter();
if (soundBtn_) {
soundBtn_->setOn(g_SoundOpen);
}
updateMenuColors();
}
/**
* @brief
*/
void PlayScene::updateMenuColors() {
// 选中的项用红色,未选中的用白色
if (restartText_) {
restartText_->setTextColor(menuIndex_ == 0 ? extra2d::Colors::Red
: extra2d::Colors::White);
}
if (soundToggleText_) {
soundToggleText_->setTextColor(menuIndex_ == 1 ? extra2d::Colors::Red
: extra2d::Colors::White);
}
if (undoText_) {
undoText_->setTextColor(menuIndex_ == 2 ? extra2d::Colors::Red
: extra2d::Colors::White);
}
}
void PlayScene::onUpdate(float dt) {
BaseScene::onUpdate(dt);
auto &app = extra2d::Application::instance();
auto &input = app.input();
// B 键返回主菜单
if (input.isButtonPressed(extra2d::GamepadButton::B)) {
app.scenes().replaceScene(extra2d::makePtr<StartScene>(),
extra2d::TransitionType::Fade, 0.5f);
return;
}
// Y 键重开
if (input.isButtonPressed(extra2d::GamepadButton::Y)) {
setLevel(g_CurrentLevel);
return;
}
// X键直接切换音效备用按钮也可点击切换
if (input.isButtonPressed(extra2d::GamepadButton::X)) {
g_SoundOpen = !g_SoundOpen;
AudioManager::instance().setEnabled(g_SoundOpen);
if (soundBtn_) {
soundBtn_->setOn(g_SoundOpen);
}
return;
}
// Z 键撤销(对象池使用示例)
if (input.isKeyPressed(extra2d::Key::Z)) {
undoMove();
return;
}
// A 键执行选中的菜单项
if (input.isButtonPressed(extra2d::GamepadButton::A)) {
executeMenuItem();
return;
}
// 方向键移动
if (input.isButtonPressed(extra2d::GamepadButton::DPadUp)) {
move(0, -1, 1);
flush();
} else if (input.isButtonPressed(extra2d::GamepadButton::DPadDown)) {
move(0, 1, 2);
flush();
} else if (input.isButtonPressed(extra2d::GamepadButton::DPadLeft)) {
move(-1, 0, 3);
flush();
} else if (input.isButtonPressed(extra2d::GamepadButton::DPadRight)) {
move(1, 0, 4);
flush();
} else {
return;
}
// 检查是否通关
for (int i = 0; i < map_.width; i++) {
for (int j = 0; j < map_.height; j++) {
Piece p = map_.value[j][i];
if (p.type == TYPE::Box && p.isPoint == false) {
return;
}
}
}
gameOver();
}
/**
* @brief
*/
void PlayScene::executeMenuItem() {
switch (menuIndex_) {
case 0: // 重开
setLevel(g_CurrentLevel);
break;
case 1: // 切换音效
g_SoundOpen = !g_SoundOpen;
AudioManager::instance().setEnabled(g_SoundOpen);
if (soundBtn_) {
soundBtn_->setOn(g_SoundOpen);
}
break;
case 2: // 撤销
undoMove();
break;
}
}
/**
* @brief
*/
void PlayScene::flush() {
mapLayer_->removeAllChildren();
int tileW = texFloor_ ? texFloor_->width() : 32;
int tileH = texFloor_ ? texFloor_->height() : 32;
// 使用游戏逻辑分辨率
float gameWidth = GAME_WIDTH;
float gameHeight = GAME_HEIGHT;
float baseOffsetX = 0.0f;
float baseOffsetY = 0.0f;
// 在 12x12 网格中居中地图
float mapOffsetX = static_cast<float>((12.0f - map_.width) / 2.0f) * tileW;
float mapOffsetY = static_cast<float>((12.0f - map_.height) / 2.0f) * tileH;
float offsetX = baseOffsetX + mapOffsetX;
float offsetY = baseOffsetY + mapOffsetY;
for (int i = 0; i < map_.width; i++) {
for (int j = 0; j < map_.height; j++) {
Piece piece = map_.value[j][i];
extra2d::Ptr<extra2d::Texture> tex;
if (piece.type == TYPE::Wall) {
tex = texWall_;
} else if (piece.type == TYPE::Ground && piece.isPoint) {
tex = texPoint_;
} else if (piece.type == TYPE::Ground) {
tex = texFloor_;
} else if (piece.type == TYPE::Box && piece.isPoint) {
tex = texBoxInPoint_;
} else if (piece.type == TYPE::Box) {
tex = texBox_;
} else if (piece.type == TYPE::Man && g_Pushing) {
tex = texManPush_[g_Direct];
} else if (piece.type == TYPE::Man) {
tex = texMan_[g_Direct];
} else {
continue;
}
if (!tex) {
continue;
}
auto sprite = extra2d::Sprite::create(tex);
sprite->setAnchor(0.0f, 0.0f);
sprite->setPosition(offsetX + static_cast<float>(i * tileW),
offsetY + static_cast<float>(j * tileH));
mapLayer_->addChild(sprite);
}
}
}
/**
* @brief
* @param level
*/
void PlayScene::setLevel(int level) {
g_CurrentLevel = level;
saveCurrentLevel(g_CurrentLevel);
// 清空移动历史(智能指针自动回收到对象池)
while (!moveHistory_.empty()) {
moveHistory_.pop();
}
if (levelText_) {
levelText_->setText("" + std::to_string(level) + "");
}
setStep(0);
int bestStep = loadBestStep(level, 0);
if (bestText_) {
if (bestStep != 0) {
bestText_->setText("最佳" + std::to_string(bestStep) + "");
} else {
bestText_->setText("");
}
}
// 深拷贝地图数据
Map &sourceMap = g_Maps[level - 1];
map_.width = sourceMap.width;
map_.height = sourceMap.height;
map_.roleX = sourceMap.roleX;
map_.roleY = sourceMap.roleY;
for (int i = 0; i < 12; i++) {
for (int j = 0; j < 12; j++) {
map_.value[i][j] = sourceMap.value[i][j];
}
}
g_Direct = 2;
g_Pushing = false;
flush();
}
/**
* @brief
* @param step
*/
void PlayScene::setStep(int step) {
step_ = step;
if (stepText_) {
stepText_->setText("当前" + std::to_string(step) + "");
}
}
/**
* @brief
* @param dx X方向偏移
* @param dy Y方向偏移
* @param direct 1=2=3=4=
*/
void PlayScene::move(int dx, int dy, int direct) {
int targetX = dx + map_.roleX;
int targetY = dy + map_.roleY;
g_Direct = direct;
if (targetX < 0 || targetX >= map_.width || targetY < 0 ||
targetY >= map_.height) {
return;
}
if (map_.value[targetY][targetX].type == TYPE::Wall) {
return;
}
// 使用对象池创建移动记录(自动管理内存)
auto record = E2D_MAKE_POOLED(MoveRecord, map_.roleX, map_.roleY, targetX,
targetY, false);
if (map_.value[targetY][targetX].type == TYPE::Ground) {
g_Pushing = false;
map_.value[map_.roleY][map_.roleX].type = TYPE::Ground;
map_.value[targetY][targetX].type = TYPE::Man;
AudioManager::instance().playManMove();
} else if (map_.value[targetY][targetX].type == TYPE::Box) {
g_Pushing = true;
int boxX = 0;
int boxY = 0;
switch (g_Direct) {
case 1:
boxX = targetX;
boxY = targetY - 1;
break;
case 2:
boxX = targetX;
boxY = targetY + 1;
break;
case 3:
boxX = targetX - 1;
boxY = targetY;
break;
case 4:
boxX = targetX + 1;
boxY = targetY;
break;
default:
return;
}
if (boxX < 0 || boxX >= map_.width || boxY < 0 || boxY >= map_.height) {
return;
}
if (map_.value[boxY][boxX].type == TYPE::Wall ||
map_.value[boxY][boxX].type == TYPE::Box) {
return;
}
// 记录箱子移动
record->pushedBox = true;
record->boxFromX = targetX;
record->boxFromY = targetY;
record->boxToX = boxX;
record->boxToY = boxY;
map_.value[boxY][boxX].type = TYPE::Box;
map_.value[targetY][targetX].type = TYPE::Man;
map_.value[map_.roleY][map_.roleX].type = TYPE::Ground;
AudioManager::instance().playBoxMove();
} else {
return;
}
// 保存移动记录到历史栈
moveHistory_.push(record);
map_.roleX = targetX;
map_.roleY = targetY;
setStep(step_ + 1);
}
/**
* @brief
*/
void PlayScene::gameOver() {
int bestStep = loadBestStep(g_CurrentLevel, 0);
if (bestStep == 0 || step_ < bestStep) {
saveBestStep(g_CurrentLevel, step_);
}
if (g_CurrentLevel == MAX_LEVEL) {
extra2d::Application::instance().scenes().pushScene(
extra2d::makePtr<SuccessScene>(), extra2d::TransitionType::Fade, 0.5f);
return;
}
setLevel(g_CurrentLevel + 1);
}
/**
* @brief 使
*
*/
void PlayScene::undoMove() {
if (moveHistory_.empty()) {
E2D_LOG_INFO("No moves to undo");
return;
}
auto record = moveHistory_.top();
moveHistory_.pop();
// 恢复玩家位置
map_.value[map_.roleY][map_.roleX].type = TYPE::Ground;
map_.value[record->fromY][record->fromX].type = TYPE::Man;
map_.roleX = record->fromX;
map_.roleY = record->fromY;
// 如果推了箱子,恢复箱子位置
if (record->pushedBox) {
map_.value[record->boxToY][record->boxToX].type = TYPE::Ground;
map_.value[record->boxFromY][record->boxFromX].type = TYPE::Box;
}
// record 智能指针离开作用域后自动回收到对象池
setStep(step_ - 1);
flush();
E2D_LOG_INFO("Undo move, step: {}", step_);
}
} // namespace pushbox

View File

@ -1,113 +0,0 @@
// ============================================================================
// PlayScene.h - Push Box 游戏场景
// ============================================================================
#pragma once
#include "BaseScene.h"
#include "data.h"
#include <extra2d.h>
#include <utils/object_pool.h>
#include <stack>
namespace pushbox {
/**
* @brief Push Box
*/
class PlayScene : public BaseScene {
public:
/**
* @brief
* @param level
*/
explicit PlayScene(int level);
/**
* @brief
*/
void onEnter() override;
/**
* @brief
* @param dt
*/
void onUpdate(float dt) override;
private:
/**
* @brief
*/
void updateMenuColors();
/**
* @brief
*/
void executeMenuItem();
/**
* @brief
*/
void flush();
/**
* @brief
* @param level
*/
void setLevel(int level);
/**
* @brief
* @param step
*/
void setStep(int step);
/**
* @brief
* @param dx X方向偏移
* @param dy Y方向偏移
* @param direct 1=2=3=4=
*/
void move(int dx, int dy, int direct);
/**
* @brief
*/
void gameOver();
/**
* @brief 使
*/
void undoMove();
int step_ = 0;
int menuIndex_ = 0;
Map map_{};
extra2d::Ptr<extra2d::FontAtlas> font28_;
extra2d::Ptr<extra2d::FontAtlas> font20_;
extra2d::Ptr<extra2d::Text> levelText_;
extra2d::Ptr<extra2d::Text> stepText_;
extra2d::Ptr<extra2d::Text> bestText_;
extra2d::Ptr<extra2d::Text> restartText_;
extra2d::Ptr<extra2d::Text> soundToggleText_;
extra2d::Ptr<extra2d::Text> undoText_;
extra2d::Ptr<extra2d::Node> mapLayer_;
extra2d::Ptr<extra2d::Button> soundBtn_;
extra2d::Ptr<extra2d::Texture> texWall_;
extra2d::Ptr<extra2d::Texture> texPoint_;
extra2d::Ptr<extra2d::Texture> texFloor_;
extra2d::Ptr<extra2d::Texture> texBox_;
extra2d::Ptr<extra2d::Texture> texBoxInPoint_;
extra2d::Ptr<extra2d::Texture> texMan_[5];
extra2d::Ptr<extra2d::Texture> texManPush_[5];
// 对象池使用示例:使用智能指针管理 MoveRecord
std::stack<extra2d::Ptr<MoveRecord>> moveHistory_;
};
} // namespace pushbox

View File

@ -1,227 +0,0 @@
// ============================================================================
// StartScene.cpp - Push Box 开始场景实现
// ============================================================================
#include "StartScene.h"
#include "PlayScene.h"
#include "audio_manager.h"
#include "data.h"
#include <extra2d.h>
namespace pushbox {
StartScene::StartScene() : BaseScene() {
// BaseScene 已处理视口设置
}
/**
* @brief
*/
static extra2d::Ptr<extra2d::FontAtlas> loadMenuFont() {
auto &resources = extra2d::Application::instance().resources();
auto font = resources.loadFont("assets/font.ttf", 28, true);
return font;
}
void StartScene::onEnter() {
BaseScene::onEnter();
auto &app = extra2d::Application::instance();
auto &resources = app.resources();
if (children().empty()) {
// 使用游戏逻辑分辨率
float screenW = GAME_WIDTH;
float screenH = GAME_HEIGHT;
auto bgTex = resources.loadTexture("assets/images/start.jpg");
if (bgTex) {
auto background = extra2d::Sprite::create(bgTex);
float bgWidth = static_cast<float>(bgTex->width());
float bgHeight = static_cast<float>(bgTex->height());
float offsetX = (screenW - bgWidth) / 2.0f;
float offsetY = (screenH - bgHeight) / 2.0f;
background->setAnchor(0.0f, 0.0f);
background->setPosition(offsetX, offsetY);
addChild(background);
float centerX = screenW / 2.0f;
font_ = loadMenuFont();
if (!font_) {
return;
}
// 创建菜单按钮(使用 Button 实现文本居中)
startBtn_ = extra2d::Button::create();
startBtn_->setFont(font_);
startBtn_->setText("新游戏");
startBtn_->setTextColor(extra2d::Colors::Black);
startBtn_->setBackgroundColor(extra2d::Colors::Transparent,
extra2d::Colors::Transparent,
extra2d::Colors::Transparent);
startBtn_->setBorder(extra2d::Colors::Transparent, 0.0f);
startBtn_->setPadding(extra2d::Vec2(0.0f, 0.0f));
startBtn_->setCustomSize(200.0f, 40.0f);
startBtn_->setAnchor(0.5f, 0.5f);
startBtn_->setPosition(centerX, offsetY + 260.0f);
addChild(startBtn_);
resumeBtn_ = extra2d::Button::create();
resumeBtn_->setFont(font_);
resumeBtn_->setText("继续关卡");
resumeBtn_->setTextColor(extra2d::Colors::Black);
resumeBtn_->setBackgroundColor(extra2d::Colors::Transparent,
extra2d::Colors::Transparent,
extra2d::Colors::Transparent);
resumeBtn_->setBorder(extra2d::Colors::Transparent, 0.0f);
resumeBtn_->setPadding(extra2d::Vec2(0.0f, 0.0f));
resumeBtn_->setCustomSize(200.0f, 40.0f);
resumeBtn_->setAnchor(0.5f, 0.5f);
resumeBtn_->setPosition(centerX, offsetY + 300.0f);
addChild(resumeBtn_);
exitBtn_ = extra2d::Button::create();
exitBtn_->setFont(font_);
exitBtn_->setText("退出");
exitBtn_->setTextColor(extra2d::Colors::Black);
exitBtn_->setBackgroundColor(extra2d::Colors::Transparent,
extra2d::Colors::Transparent,
extra2d::Colors::Transparent);
exitBtn_->setBorder(extra2d::Colors::Transparent, 0.0f);
exitBtn_->setPadding(extra2d::Vec2(0.0f, 0.0f));
exitBtn_->setCustomSize(200.0f, 40.0f);
exitBtn_->setAnchor(0.5f, 0.5f);
exitBtn_->setPosition(centerX, offsetY + 340.0f);
addChild(exitBtn_);
// 音效开关按钮(使用 Button 的切换模式)
auto soundOn = resources.loadTexture("assets/images/soundon.png");
auto soundOff = resources.loadTexture("assets/images/soundoff.png");
if (soundOn && soundOff) {
soundBtn_ = extra2d::Button::create();
soundBtn_->setToggleMode(true);
soundBtn_->setStateBackgroundImage(soundOff, soundOn);
soundBtn_->setOn(g_SoundOpen);
soundBtn_->setAnchor(0.0f, 0.0f);
soundBtn_->setPosition(offsetX + 50.0f, offsetY + 50.0f);
soundBtn_->setOnStateChange([](bool isOn) {
g_SoundOpen = isOn;
AudioManager::instance().setEnabled(isOn);
});
addChild(soundBtn_);
}
}
}
// 始终有3个菜单项
menuCount_ = 3;
updateMenuColors();
}
void StartScene::onUpdate(float dt) {
BaseScene::onUpdate(dt);
auto &app = extra2d::Application::instance();
auto &input = app.input();
// 方向键上下切换选择
if (input.isButtonPressed(extra2d::GamepadButton::DPadUp)) {
selectedIndex_ = (selectedIndex_ - 1 + menuCount_) % menuCount_;
updateMenuColors();
} else if (input.isButtonPressed(extra2d::GamepadButton::DPadDown)) {
selectedIndex_ = (selectedIndex_ + 1) % menuCount_;
updateMenuColors();
}
// A键确认
if (input.isButtonPressed(extra2d::GamepadButton::A)) {
executeMenuItem();
}
// X键切换音效备用按钮也可点击切换
if (input.isButtonPressed(extra2d::GamepadButton::X)) {
g_SoundOpen = !g_SoundOpen;
AudioManager::instance().setEnabled(g_SoundOpen);
if (soundBtn_) {
soundBtn_->setOn(g_SoundOpen);
}
}
}
/**
* @brief
*/
void StartScene::updateMenuColors() {
// 根据选中状态更新按钮文本颜色
// 选中的项用红色,未选中的用黑色,禁用的项用深灰色
if (startBtn_) {
startBtn_->setTextColor(selectedIndex_ == 0 ? extra2d::Colors::Red
: extra2d::Colors::Black);
}
if (resumeBtn_) {
// "继续关卡"始终显示,但当 g_CurrentLevel == 1 时禁用(深灰色)
if (g_CurrentLevel > 1) {
// 可用状态:选中为红色,未选中为黑色
resumeBtn_->setTextColor(selectedIndex_ == 1 ? extra2d::Colors::Red
: extra2d::Colors::Black);
} else {
// 禁用状态:深灰色 (RGB: 80, 80, 80)
resumeBtn_->setTextColor(extra2d::Color(80, 80, 80, 255));
}
}
if (exitBtn_) {
exitBtn_->setTextColor(selectedIndex_ == 2 ? extra2d::Colors::Red
: extra2d::Colors::Black);
}
}
/**
* @brief
*/
void StartScene::executeMenuItem() {
// 始终有3个选项但"继续关卡"(索引1)在 g_CurrentLevel == 1 时禁用
switch (selectedIndex_) {
case 0:
startNewGame();
break;
case 1:
// 只有当 g_CurrentLevel > 1 时才能选择"继续关卡"
if (g_CurrentLevel > 1) {
continueGame();
}
break;
case 2:
exitGame();
break;
}
}
/**
* @brief
*/
void StartScene::startNewGame() {
extra2d::Application::instance().scenes().replaceScene(
extra2d::makePtr<PlayScene>(1), extra2d::TransitionType::Fade, 0.5f);
}
/**
* @brief
*/
void StartScene::continueGame() {
extra2d::Application::instance().scenes().replaceScene(
extra2d::makePtr<PlayScene>(g_CurrentLevel),
extra2d::TransitionType::Fade, 0.5f);
}
/**
* @brief 退
*/
void StartScene::exitGame() { extra2d::Application::instance().quit(); }
} // namespace pushbox

View File

@ -1,68 +0,0 @@
// ============================================================================
// StartScene.h - Push Box 开始场景
// ============================================================================
#pragma once
#include "BaseScene.h"
#include <extra2d.h>
namespace pushbox {
/**
* @brief Push Box
*/
class StartScene : public BaseScene {
public:
/**
* @brief
*/
StartScene();
/**
* @brief
*/
void onEnter() override;
/**
* @brief
* @param dt
*/
void onUpdate(float dt) override;
private:
/**
* @brief
*/
void updateMenuColors();
/**
* @brief
*/
void executeMenuItem();
/**
* @brief
*/
void startNewGame();
/**
* @brief
*/
void continueGame();
/**
* @brief 退
*/
void exitGame();
extra2d::Ptr<extra2d::FontAtlas> font_;
extra2d::Ptr<extra2d::Button> startBtn_;
extra2d::Ptr<extra2d::Button> resumeBtn_;
extra2d::Ptr<extra2d::Button> exitBtn_;
extra2d::Ptr<extra2d::Button> soundBtn_;
int selectedIndex_ = 0;
int menuCount_ = 3;
};
} // namespace pushbox

View File

@ -1,81 +0,0 @@
// ============================================================================
// SuccessScene.cpp - Push Box 通关场景实现
// ============================================================================
#include "SuccessScene.h"
#include <extra2d.h>
namespace pushbox {
SuccessScene::SuccessScene() : BaseScene() {
// BaseScene 已处理视口设置
}
/**
* @brief
*/
static extra2d::Ptr<extra2d::FontAtlas> loadMenuFont() {
auto &resources = extra2d::Application::instance().resources();
auto font = resources.loadFont("assets/font.ttf", 28);
return font;
}
void SuccessScene::onEnter() {
BaseScene::onEnter();
auto &app = extra2d::Application::instance();
auto &resources = app.resources();
if (children().empty()) {
// 使用游戏逻辑分辨率
float screenW = GAME_WIDTH;
float screenH = GAME_HEIGHT;
auto bgTex = resources.loadTexture("assets/images/success.jpg");
if (bgTex) {
auto background = extra2d::Sprite::create(bgTex);
float bgWidth = static_cast<float>(bgTex->width());
float bgHeight = static_cast<float>(bgTex->height());
float offsetX = (screenW - bgWidth) / 2.0f;
float offsetY = (screenH - bgHeight) / 2.0f;
background->setAnchor(0.0f, 0.0f);
background->setPosition(offsetX, offsetY);
addChild(background);
float centerX = screenW / 2.0f;
auto font = loadMenuFont();
if (font) {
// 创建按钮文本(仅显示,不响应鼠标)
auto backText = extra2d::Text::create("回主菜单", font);
backText->setPosition(centerX, offsetY + 350.0f);
backText->setTextColor(extra2d::Colors::Black);
addChild(backText);
// 创建选择指示器(箭头)
selectorText_ = extra2d::Text::create(">", font);
selectorText_->setTextColor(extra2d::Colors::Red);
selectorText_->setPosition(centerX - 80.0f, offsetY + 350.0f);
addChild(selectorText_);
}
}
}
}
void SuccessScene::onUpdate(float dt) {
BaseScene::onUpdate(dt);
auto &app = extra2d::Application::instance();
auto &input = app.input();
// A键确认返回主菜单
if (input.isButtonPressed(extra2d::GamepadButton::A)) {
auto &scenes = extra2d::Application::instance().scenes();
scenes.popScene(extra2d::TransitionType::Fade, 0.5f);
scenes.popScene(extra2d::TransitionType::Fade, 0.5f);
}
}
} // namespace pushbox

View File

@ -1,37 +0,0 @@
// ============================================================================
// SuccessScene.h - Push Box 通关场景
// ============================================================================
#pragma once
#include "BaseScene.h"
#include <extra2d.h>
namespace pushbox {
/**
* @brief Push Box
*/
class SuccessScene : public BaseScene {
public:
/**
* @brief
*/
SuccessScene();
/**
* @brief
*/
void onEnter() override;
/**
* @brief
* @param dt
*/
void onUpdate(float dt) override;
private:
extra2d::Ptr<extra2d::Text> selectorText_;
};
} // namespace pushbox

View File

@ -1,112 +0,0 @@
#include "audio_manager.h"
#include "storage.h"
namespace pushbox {
// ============================================================================
// 单例实现
// ============================================================================
AudioManager& AudioManager::instance() {
static AudioManager instance;
return instance;
}
// ============================================================================
// 初始化音频资源
// ============================================================================
void AudioManager::init() {
if (initialized_) {
return;
}
auto& resources = extra2d::Application::instance().resources();
// 加载音效资源
background_ = resources.loadSound("pushbox_bg", "assets/audio/background.wav");
manMove_ = resources.loadSound("pushbox_manmove", "assets/audio/manmove.wav");
boxMove_ = resources.loadSound("pushbox_boxmove", "assets/audio/boxmove.wav");
// 设置背景音乐循环播放
if (background_) {
background_->setLooping(true);
}
// 从存储中读取音效设置
enabled_ = g_SoundOpen;
initialized_ = true;
// 如果音效开启,播放背景音乐
if (enabled_ && background_) {
background_->play();
}
}
// ============================================================================
// 启用/禁用音效
// ============================================================================
void AudioManager::setEnabled(bool enabled) {
enabled_ = enabled;
g_SoundOpen = enabled;
saveSoundOpen(enabled);
if (!background_) {
return;
}
if (enabled_) {
background_->resume();
} else {
background_->pause();
}
}
// ============================================================================
// 播放角色移动音效
// ============================================================================
void AudioManager::playManMove() {
if (!enabled_ || !manMove_) {
return;
}
manMove_->play();
}
// ============================================================================
// 播放箱子移动音效
// ============================================================================
void AudioManager::playBoxMove() {
if (!enabled_ || !boxMove_) {
return;
}
boxMove_->play();
}
// ============================================================================
// 背景音乐控制
// ============================================================================
void AudioManager::playBackground() {
if (background_) {
background_->play();
}
}
void AudioManager::pauseBackground() {
if (background_) {
background_->pause();
}
}
void AudioManager::resumeBackground() {
if (background_) {
background_->resume();
}
}
void AudioManager::stopBackground() {
if (background_) {
background_->stop();
}
}
} // namespace pushbox

View File

@ -1,47 +0,0 @@
#pragma once
#include "data.h"
#include <extra2d.h>
namespace pushbox {
// ============================================================================
// 全局音频管理器 - 单例模式,不依赖场景生命周期
// ============================================================================
class AudioManager {
public:
// 获取单例实例
static AudioManager& instance();
// 初始化音频资源
void init();
// 启用/禁用音效
void setEnabled(bool enabled);
bool isEnabled() const { return enabled_; }
// 播放音效
void playManMove();
void playBoxMove();
// 背景音乐控制
void playBackground();
void pauseBackground();
void resumeBackground();
void stopBackground();
private:
AudioManager() = default;
~AudioManager() = default;
AudioManager(const AudioManager&) = delete;
AudioManager& operator=(const AudioManager&) = delete;
bool initialized_ = false;
bool enabled_ = true;
extra2d::Ptr<extra2d::Sound> background_;
extra2d::Ptr<extra2d::Sound> manMove_;
extra2d::Ptr<extra2d::Sound> boxMove_;
};
} // namespace pushbox

View File

@ -1,117 +0,0 @@
#include "data.h"
namespace pushbox {
int g_CurrentLevel = 1;
bool g_SoundOpen = true;
int g_Direct = 2;
bool g_Pushing = false;
Map g_Maps[MAX_LEVEL] = {
{
8, 8, 4, 4,
{
{{Empty}, {Empty}, {Wall}, {Wall}, {Wall}, {Empty}, {Empty}, {Empty}},
{{Empty}, {Empty}, {Wall}, {Ground, true}, {Wall}, {Empty}, {Empty}, {Empty}},
{{Empty}, {Empty}, {Wall}, {Ground}, {Wall}, {Wall}, {Wall}, {Wall}},
{{Wall}, {Wall}, {Wall}, {Box}, {Ground}, {Box}, {Ground, true}, {Wall}},
{{Wall}, {Ground, true}, {Ground}, {Box}, {Man}, {Wall}, {Wall}, {Wall}},
{{Wall}, {Wall}, {Wall}, {Wall}, {Box}, {Wall}, {Empty}, {Empty}},
{{Empty}, {Empty}, {Empty}, {Wall}, {Ground, true}, {Wall}, {Empty}, {Empty}},
{{Empty}, {Empty}, {Empty}, {Wall}, {Wall}, {Wall}, {Empty}, {Empty}},
},
},
{
9, 9, 1, 1,
{
{{Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Empty}, {Empty}, {Empty}, {Empty}},
{{Wall}, {Man}, {Ground}, {Ground}, {Wall}, {Empty}, {Empty}, {Empty}, {Empty}},
{{Wall}, {Ground}, {Box}, {Box}, {Wall}, {Empty}, {Wall}, {Wall}, {Wall}},
{{Wall}, {Ground}, {Box}, {Ground}, {Wall}, {Empty}, {Wall}, {Ground, true}, {Wall}},
{{Wall}, {Wall}, {Wall}, {Ground}, {Wall}, {Wall}, {Wall}, {Ground, true}, {Wall}},
{{Empty}, {Wall}, {Wall}, {Ground}, {Ground}, {Ground}, {Ground}, {Ground, true}, {Wall}},
{{Empty}, {Wall}, {Ground}, {Ground}, {Ground}, {Wall}, {Ground}, {Ground}, {Wall}},
{{Empty}, {Wall}, {Ground}, {Ground}, {Ground}, {Wall}, {Wall}, {Wall}, {Wall}},
{{Empty}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Empty}, {Empty}, {Empty}},
},
},
{
10, 7, 3, 3,
{
{{Empty}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Empty}, {Empty}},
{{Empty}, {Wall}, {Ground}, {Ground}, {Ground}, {Ground}, {Ground}, {Wall}, {Wall}, {Wall}},
{{Wall}, {Wall}, {Box}, {Wall}, {Wall}, {Wall}, {Ground}, {Ground}, {Ground}, {Wall}},
{{Wall}, {Ground}, {Ground}, {Man}, {Box}, {Ground}, {Ground}, {Box}, {Ground}, {Wall}},
{{Wall}, {Ground}, {Ground, true}, {Ground, true}, {Wall}, {Ground}, {Box}, {Ground}, {Wall}, {Wall}},
{{Wall}, {Wall}, {Ground, true}, {Ground, true}, {Wall}, {Ground}, {Ground}, {Ground}, {Wall}, {Empty}},
{{Empty}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Empty}},
},
},
{
6, 8, 1, 2,
{
{{Empty}, {Wall}, {Wall}, {Wall}, {Wall}, {Empty}},
{{Wall}, {Wall}, {Ground}, {Ground}, {Wall}, {Empty}},
{{Wall}, {Man}, {Box}, {Ground}, {Wall}, {Empty}},
{{Wall}, {Wall}, {Box}, {Ground}, {Wall}, {Wall}},
{{Wall}, {Wall}, {Ground}, {Box}, {Ground}, {Wall}},
{{Wall}, {Ground, true}, {Box}, {Ground}, {Ground}, {Wall}},
{{Wall}, {Ground, true}, {Ground, true}, {Box, true}, {Ground, true}, {Wall}},
{{Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}},
},
},
{
8, 8, 2, 2,
{
{{Empty}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Empty}, {Empty}},
{{Empty}, {Wall}, {Ground}, {Ground}, {Wall}, {Wall}, {Wall}, {Empty}},
{{Empty}, {Wall}, {Man}, {Box}, {Ground}, {Ground}, {Wall}, {Empty}},
{{Wall}, {Wall}, {Wall}, {Ground}, {Wall}, {Ground}, {Wall}, {Wall}},
{{Wall}, {Ground, true}, {Wall}, {Ground}, {Wall}, {Ground}, {Ground}, {Wall}},
{{Wall}, {Ground, true}, {Box}, {Ground}, {Ground}, {Wall}, {Ground}, {Wall}},
{{Wall}, {Ground, true}, {Ground}, {Ground}, {Ground}, {Box}, {Ground}, {Wall}},
{{Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}},
},
},
{
10, 8, 8, 1,
{
{{Empty}, {Empty}, {Empty}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}},
{{Empty}, {Empty}, {Wall}, {Wall}, {Ground}, {Ground}, {Wall}, {Ground}, {Man}, {Wall}},
{{Empty}, {Empty}, {Wall}, {Ground}, {Ground}, {Ground}, {Wall}, {Ground}, {Ground}, {Wall}},
{{Empty}, {Empty}, {Wall}, {Box}, {Ground}, {Box}, {Ground}, {Box}, {Ground}, {Wall}},
{{Empty}, {Empty}, {Wall}, {Ground}, {Box}, {Wall}, {Wall}, {Ground}, {Ground}, {Wall}},
{{Wall}, {Wall}, {Wall}, {Ground}, {Box}, {Ground}, {Wall}, {Ground}, {Wall}, {Wall}},
{{Wall}, {Ground, true}, {Ground, true}, {Ground, true}, {Ground, true}, {Ground, true}, {Ground}, {Ground}, {Wall}, {Empty}},
{{Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Empty}},
},
},
{
10, 7, 8, 3,
{
{{Empty}, {Empty}, {Empty}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Empty}},
{{Empty}, {Wall}, {Wall}, {Wall}, {Ground}, {Ground}, {Ground}, {Ground}, {Wall}, {Empty}},
{{Wall}, {Wall}, {Ground, true}, {Ground}, {Box}, {Wall}, {Wall}, {Ground}, {Wall}, {Wall}},
{{Wall}, {Ground, true}, {Ground, true}, {Box}, {Ground}, {Box}, {Ground}, {Ground}, {Man}, {Wall}},
{{Wall}, {Ground, true}, {Ground, true}, {Ground}, {Box}, {Ground}, {Box}, {Ground}, {Wall}, {Wall}},
{{Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Ground}, {Ground}, {Wall}, {Empty}},
{{Empty}, {Empty}, {Empty}, {Empty}, {Empty}, {Wall}, {Wall}, {Wall}, {Wall}, {Empty}},
},
},
{
11, 9, 8, 7,
{
{{Empty}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Empty}},
{{Empty}, {Wall}, {Ground}, {Ground}, {Wall}, {Wall}, {Ground}, {Ground}, {Ground}, {Wall}, {Empty}},
{{Empty}, {Wall}, {Ground}, {Ground}, {Ground}, {Box}, {Ground}, {Ground}, {Ground}, {Wall}, {Empty}},
{{Empty}, {Wall}, {Box}, {Ground}, {Wall}, {Wall}, {Wall}, {Ground}, {Box}, {Wall}, {Empty}},
{{Empty}, {Wall}, {Ground}, {Wall}, {Ground, true}, {Ground, true}, {Ground, true}, {Wall}, {Ground}, {Wall}, {Empty}},
{{Wall}, {Wall}, {Ground}, {Wall}, {Ground, true}, {Ground, true}, {Ground, true}, {Wall}, {Ground}, {Wall}, {Wall}},
{{Wall}, {Ground}, {Box}, {Ground}, {Ground}, {Box}, {Ground}, {Ground}, {Box}, {Ground}, {Wall}},
{{Wall}, {Ground}, {Ground}, {Ground}, {Ground}, {Ground}, {Wall}, {Ground}, {Man}, {Ground}, {Wall}},
{{Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}},
},
},
};
} // namespace pushbox

View File

@ -1,48 +0,0 @@
#pragma once
#define MAX_LEVEL 8
#define GAME_WIDTH 640.0f
#define GAME_HEIGHT 480.0f
namespace pushbox {
enum TYPE { Empty, Wall, Ground, Box, Man };
struct Piece {
TYPE type;
bool isPoint;
};
struct Map {
int width;
int height;
int roleX;
int roleY;
Piece value[12][12];
};
/**
* @brief -
* 使
*/
struct MoveRecord {
int fromX, fromY;
int toX, toY;
int boxFromX, boxFromY;
int boxToX, boxToY;
bool pushedBox;
MoveRecord() = default;
MoveRecord(int fx, int fy, int tx, int ty, bool pushed = false)
: fromX(fx), fromY(fy), toX(tx), toY(ty)
, boxFromX(-1), boxFromY(-1), boxToX(-1), boxToY(-1)
, pushedBox(pushed) {}
};
extern Map g_Maps[MAX_LEVEL];
extern int g_CurrentLevel;
extern bool g_SoundOpen;
extern int g_Direct;
extern bool g_Pushing;
} // namespace pushbox

View File

@ -1,60 +0,0 @@
#include <extra2d.h>
#include "StartScene.h"
#include "data.h"
#include "storage.h"
#include "audio_manager.h"
using namespace extra2d;
// ============================================================================
// 程序入口
// ============================================================================
int main(int argc, char **argv)
{
Logger::init();
Logger::setLevel(LogLevel::Debug);
E2D_LOG_INFO("========================");
E2D_LOG_INFO("Extra2D push_box");
E2D_LOG_INFO("========================");
auto &app = Application::instance();
AppConfig config;
config.title = "Extra2D - push_box";
config.width = 1280;
config.height = 720;
config.vsync = true;
config.fpsLimit = 60;
if (!app.init(config)) {
E2D_LOG_ERROR("应用初始化失败!");
return -1;
}
// 初始化存储系统
// 在 Windows 上使用当前工作目录,在 Switch 上使用 sdmc:/
#ifdef __SWITCH__
pushbox::initStorage("sdmc:/");
#else
pushbox::initStorage(".");
#endif
pushbox::g_CurrentLevel = pushbox::loadCurrentLevel(1);
if (pushbox::g_CurrentLevel > MAX_LEVEL) {
pushbox::g_CurrentLevel = 1;
}
pushbox::g_SoundOpen = pushbox::loadSoundOpen(true);
// 初始化全局音频管理器
pushbox::AudioManager::instance().init();
// 进入开始场景(主界面)
app.enterScene(makePtr<pushbox::StartScene>());
E2D_LOG_INFO("开始主循环...");
app.run();
E2D_LOG_INFO("应用结束");
return 0;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 477 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 658 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 682 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 641 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 642 B

View File

@ -1,92 +0,0 @@
#include "storage.h"
#include <utils/data.h>
#include <string>
namespace pushbox {
static extra2d::DataStore g_store;
static std::filesystem::path g_filePath;
static bool g_loaded = false;
static void ensureLoaded() {
if (g_loaded) {
return;
}
if (!g_filePath.empty()) {
g_store.load(g_filePath.string());
}
g_loaded = true;
}
void initStorage(const std::filesystem::path& baseDir) {
// Nintendo Switch 标准保存位置:/save/ 或 sdmc:/switch/
// 优先使用 /save/ 目录(官方存档位置)
std::filesystem::path saveDir;
// 检查是否存在 /save/ 目录(这是 NS 官方存档目录)
if (std::filesystem::exists("/save/")) {
saveDir = "/save/";
} else if (std::filesystem::exists("/switch/")) {
// 备用:使用 /switch/ 目录
saveDir = "/switch/push_box/";
std::filesystem::create_directories(saveDir);
} else {
// 开发环境:使用 sdmc:/switch/
saveDir = baseDir / "switch/push_box/";
std::filesystem::create_directories(saveDir);
}
g_filePath = saveDir / "pushbox.ini";
g_store.load(g_filePath.string());
g_loaded = true;
}
int loadCurrentLevel(int defaultValue) {
ensureLoaded();
int level = g_store.getInt("game", "level", defaultValue);
if (level < 1) {
level = 1;
}
return level;
}
void saveCurrentLevel(int level) {
ensureLoaded();
g_store.setInt("game", "level", level);
if (!g_filePath.empty()) {
g_store.save(g_filePath.string());
}
}
bool loadSoundOpen(bool defaultValue) {
ensureLoaded();
return g_store.getBool("game", "sound", defaultValue);
}
void saveSoundOpen(bool open) {
ensureLoaded();
g_store.setBool("game", "sound", open);
if (!g_filePath.empty()) {
g_store.save(g_filePath.string());
}
}
int loadBestStep(int level, int defaultValue) {
ensureLoaded();
std::string key = "level" + std::to_string(level);
return g_store.getInt("best", key, defaultValue);
}
void saveBestStep(int level, int step) {
ensureLoaded();
std::string key = "level" + std::to_string(level);
g_store.setInt("best", key, step);
if (!g_filePath.empty()) {
g_store.save(g_filePath.string());
}
}
std::filesystem::path storageFilePath() { return g_filePath; }
} // namespace pushbox

View File

@ -1,20 +0,0 @@
#pragma once
#include <filesystem>
namespace pushbox {
void initStorage(const std::filesystem::path& baseDir);
int loadCurrentLevel(int defaultValue = 1);
void saveCurrentLevel(int level);
bool loadSoundOpen(bool defaultValue = true);
void saveSoundOpen(bool open);
int loadBestStep(int level, int defaultValue = 0);
void saveBestStep(int level, int step);
std::filesystem::path storageFilePath();
} // namespace pushbox

View File

@ -1,76 +0,0 @@
-- ==============================================
-- Push Box 示例 - Xmake 构建脚本
-- 支持平台: MinGW (Windows), Nintendo Switch
-- ==============================================
-- 获取当前脚本所在目录(示例根目录)
local example_dir = os.scriptdir()
-- 可执行文件目标
target("push_box")
set_kind("binary")
add_files("*.cpp")
add_includedirs("../../include")
add_deps("extra2d")
-- 使用与主项目相同的平台配置
if is_plat("switch") then
set_targetdir("../../build/examples/push_box")
-- 构建后生成 NRO 文件
after_build(function (target)
local devkitPro = os.getenv("DEVKITPRO") or "C:/devkitPro"
local elf_file = target:targetfile()
local output_dir = path.directory(elf_file)
local nacp_file = path.join(output_dir, "push_box.nacp")
local nro_file = path.join(output_dir, "push_box.nro")
local nacptool = path.join(devkitPro, "tools/bin/nacptool.exe")
local elf2nro = path.join(devkitPro, "tools/bin/elf2nro.exe")
if os.isfile(nacptool) and os.isfile(elf2nro) then
os.vrunv(nacptool, {"--create", "Push Box", "Extra2D Team", "1.0.0", nacp_file})
local romfs = path.join(example_dir, "romfs")
if os.isdir(romfs) then
os.vrunv(elf2nro, {elf_file, nro_file, "--nacp=" .. nacp_file, "--romfsdir=" .. romfs})
else
os.vrunv(elf2nro, {elf_file, nro_file, "--nacp=" .. nacp_file})
end
print("Generated NRO: " .. nro_file)
end
end)
-- 打包时将 NRO 文件复制到 package 目录
after_package(function (target)
local nro_file = path.join(target:targetdir(), "push_box.nro")
local package_dir = target:packagedir()
if os.isfile(nro_file) and package_dir then
os.cp(nro_file, package_dir)
print("Copied NRO to package: " .. package_dir)
end
end)
elseif is_plat("mingw") then
set_targetdir("../../build/examples/push_box")
-- add_ldflags("-mwindows", {force = true})
-- 复制资源到输出目录
after_build(function (target)
local romfs = path.join(example_dir, "romfs")
if os.isdir(romfs) then
local target_dir = path.directory(target:targetfile())
local assets_dir = path.join(target_dir, "assets")
-- 创建 assets 目录
if not os.isdir(assets_dir) then
os.mkdir(assets_dir)
end
-- 复制所有资源文件(包括子目录)
os.cp(path.join(romfs, "assets/**"), assets_dir)
print("Copied assets from " .. romfs .. " to " .. assets_dir)
else
print("Warning: romfs directory not found at " .. romfs)
end
end)
end
target_end()

View File

@ -1,538 +0,0 @@
#include <extra2d.h>
#include <random>
using namespace extra2d;
// ============================================================================
// 性能统计
// ============================================================================
struct PerformanceStats {
double updateTime = 0.0;
double collisionTime = 0.0;
double renderTime = 0.0;
size_t collisionCount = 0;
size_t nodeCount = 0;
const char *strategyName = "Unknown";
};
// ============================================================================
// 碰撞节点 - 使用引擎自带的空间索引功能
// ============================================================================
class PhysicsNode : public Node {
public:
PhysicsNode(float size, const Color &color, int id)
: size_(size), color_(color), id_(id), isColliding_(false) {
// 启用引擎自带的空间索引功能
// 这是关键:设置 spatialIndexed_ = true 让节点参与空间索引
setSpatialIndexed(true);
// 随机速度
std::random_device rd;
std::mt19937 gen(rd() + id);
std::uniform_real_distribution<float> velDist(-150.0f, 150.0f);
velocity_ = Vec2(velDist(gen), velDist(gen));
}
void setColliding(bool colliding) { isColliding_ = colliding; }
bool isColliding() const { return isColliding_; }
int getId() const { return id_; }
// 必须实现 boundingBox() 才能参与空间索引碰撞检测
Rect boundingBox() const override {
Vec2 position = pos();
return Rect(position.x - size_ / 2, position.y - size_ / 2, size_, size_);
}
void update(float dt, float screenWidth, float screenHeight) {
Vec2 position = pos();
position = position + velocity_ * dt;
// 边界反弹
if (position.x < size_ / 2 || position.x > screenWidth - size_ / 2) {
velocity_.x = -velocity_.x;
position.x = std::clamp(position.x, size_ / 2, screenWidth - size_ / 2);
}
if (position.y < size_ / 2 || position.y > screenHeight - size_ / 2) {
velocity_.y = -velocity_.y;
position.y = std::clamp(position.y, size_ / 2, screenHeight - size_ / 2);
}
setPosition(position);
}
void onRender(Renderer &renderer) override {
Vec2 position = pos();
// 碰撞时变红色
Color fillColor = isColliding_ ? Color(1.0f, 0.2f, 0.2f, 0.9f) : color_;
renderer.fillRect(
Rect(position.x - size_ / 2, position.y - size_ / 2, size_, size_),
fillColor);
// 绘制边框
Color borderColor = isColliding_ ? Color(1.0f, 0.0f, 0.0f, 1.0f)
: Color(0.3f, 0.3f, 0.3f, 0.5f);
renderer.drawRect(
Rect(position.x - size_ / 2, position.y - size_ / 2, size_, size_),
borderColor, 1.0f);
}
private:
float size_;
Color color_;
int id_;
bool isColliding_;
Vec2 velocity_;
};
// ============================================================================
// 空间索引演示场景
// ============================================================================
class SpatialIndexDemoScene : public Scene {
public:
void onEnter() override {
// 必须先调用父类的 onEnter(),这样才能正确设置 running_ 状态
// 并触发子节点的 onAttachToScene将节点注册到空间索引
Scene::onEnter();
E2D_LOG_INFO("SpatialIndexDemoScene::onEnter - 引擎空间索引演示");
auto &app = Application::instance();
screenWidth_ = static_cast<float>(app.getConfig().width);
screenHeight_ = static_cast<float>(app.getConfig().height);
// 设置背景色
setBackgroundColor(Color(0.05f, 0.05f, 0.1f, 1.0f));
// 创建100个碰撞节点
createPhysicsNodes(100);
// 加载字体
loadFonts();
E2D_LOG_INFO("空间索引已启用: {}", isSpatialIndexingEnabled());
}
void onExit() override {
// 显式移除所有子节点,确保在场景析构前正确清理空间索引
// 这必须在 Scene::onExit() 之前调用,因为 onExit() 会将 running_ 设为 false
removeAllChildren();
Scene::onExit();
}
void onUpdate(float dt) override {
Scene::onUpdate(dt);
auto startTime = std::chrono::high_resolution_clock::now();
// 更新所有物理节点位置
for (const auto &child : children()) {
if (auto node = dynamic_cast<PhysicsNode *>(child.get())) {
node->update(dt, screenWidth_, screenHeight_);
}
}
auto updateEndTime = std::chrono::high_resolution_clock::now();
stats_.updateTime =
std::chrono::duration<double, std::milli>(updateEndTime - startTime)
.count();
// 使用引擎自带的空间索引进行碰撞检测
performCollisionDetection();
auto collisionEndTime = std::chrono::high_resolution_clock::now();
stats_.collisionTime = std::chrono::duration<double, std::milli>(
collisionEndTime - updateEndTime)
.count();
// 统计物理节点数量
stats_.nodeCount = getPhysicsNodeCount();
// 获取当前使用的空间索引策略
stats_.strategyName = getSpatialManager().getStrategyName();
// 检查退出按键
auto &input = Application::instance().input();
if (input.isButtonPressed(GamepadButton::Start)) {
E2D_LOG_INFO("退出应用");
Application::instance().quit();
}
// 按 A 键添加节点
if (input.isButtonPressed(GamepadButton::A)) {
addNodes(100);
}
// 按 B 键减少节点
if (input.isButtonPressed(GamepadButton::B)) {
removeNodes(100);
}
// 按 X 键切换空间索引策略
if (input.isButtonPressed(GamepadButton::X)) {
toggleSpatialStrategy();
}
}
void onRender(Renderer &renderer) override {
auto renderStart = std::chrono::high_resolution_clock::now();
Scene::onRender(renderer);
auto renderEnd = std::chrono::high_resolution_clock::now();
stats_.renderTime =
std::chrono::duration<double, std::milli>(renderEnd - renderStart)
.count();
// 更新UI文本内容
updateUI();
// 绘制图例方块Text组件会自动渲染
drawLegend(renderer);
}
private:
/**
* @brief UI文本组件
*/
void loadFonts() {
auto &resources = Application::instance().resources();
titleFont_ = resources.loadFont("assets/font.ttf", 28, true);
infoFont_ = resources.loadFont("assets/font.ttf", 16, true);
// 创建标题文本
titleText_ = Text::create("引擎空间索引演示", titleFont_);
titleText_->setPosition(30.0f, 20.0f);
titleText_->setTextColor(Color(1.0f, 1.0f, 1.0f, 1.0f));
addChild(titleText_);
float x = 30.0f;
float y = 60.0f;
float lineHeight = 22.0f;
// 创建统计信息文本
nodeCountText_ = Text::create("", infoFont_);
nodeCountText_->setPosition(x, y);
nodeCountText_->setTextColor(Color(0.9f, 0.9f, 0.9f, 1.0f));
addChild(nodeCountText_);
y += lineHeight;
strategyText_ = Text::create("", infoFont_);
strategyText_->setPosition(x, y);
strategyText_->setTextColor(Color(0.5f, 1.0f, 0.5f, 1.0f));
addChild(strategyText_);
y += lineHeight;
collisionText_ = Text::create("", infoFont_);
collisionText_->setPosition(x, y);
collisionText_->setTextColor(Color(1.0f, 0.5f, 0.5f, 1.0f));
addChild(collisionText_);
y += lineHeight;
updateTimeText_ = Text::create("", infoFont_);
updateTimeText_->setPosition(x, y);
updateTimeText_->setTextColor(Color(0.8f, 0.8f, 0.8f, 1.0f));
addChild(updateTimeText_);
y += lineHeight;
collisionTimeText_ = Text::create("", infoFont_);
collisionTimeText_->setPosition(x, y);
collisionTimeText_->setTextColor(Color(0.8f, 0.8f, 0.8f, 1.0f));
addChild(collisionTimeText_);
y += lineHeight;
renderTimeText_ = Text::create("", infoFont_);
renderTimeText_->setPosition(x, y);
renderTimeText_->setTextColor(Color(0.8f, 0.8f, 0.8f, 1.0f));
addChild(renderTimeText_);
y += lineHeight;
fpsText_ = Text::create("", infoFont_);
fpsText_->setPosition(x, y);
fpsText_->setTextColor(Color(0.5f, 1.0f, 0.5f, 1.0f));
addChild(fpsText_);
y += lineHeight * 1.5f;
// 创建操作说明文本
helpTitleText_ = Text::create("操作说明:", infoFont_);
helpTitleText_->setPosition(x, y);
helpTitleText_->setTextColor(Color(1.0f, 1.0f, 0.5f, 1.0f));
addChild(helpTitleText_);
y += lineHeight;
helpAddText_ = Text::create("A键 - 添加100个节点", infoFont_);
helpAddText_->setPosition(x + 10, y);
helpAddText_->setTextColor(Color(0.8f, 0.8f, 0.8f, 1.0f));
addChild(helpAddText_);
y += lineHeight;
helpRemoveText_ = Text::create("B键 - 移除100个节点", infoFont_);
helpRemoveText_->setPosition(x + 10, y);
helpRemoveText_->setTextColor(Color(0.8f, 0.8f, 0.8f, 1.0f));
addChild(helpRemoveText_);
y += lineHeight;
helpToggleText_ = Text::create("X键 - 切换索引策略", infoFont_);
helpToggleText_->setPosition(x + 10, y);
helpToggleText_->setTextColor(Color(0.8f, 0.8f, 0.8f, 1.0f));
addChild(helpToggleText_);
y += lineHeight;
helpExitText_ = Text::create("+键 - 退出程序", infoFont_);
helpExitText_->setPosition(x + 10, y);
helpExitText_->setTextColor(Color(0.8f, 0.8f, 0.8f, 1.0f));
addChild(helpExitText_);
// 创建图例文本
float legendX = screenWidth_ - 200.0f;
float legendY = 20.0f;
legendTitleText_ = Text::create("图例:", infoFont_);
legendTitleText_->setPosition(legendX, legendY);
legendTitleText_->setTextColor(Color(1.0f, 1.0f, 1.0f, 1.0f));
addChild(legendTitleText_);
legendY += 25.0f;
legendNormalText_ = Text::create("- 正常", infoFont_);
legendNormalText_->setPosition(legendX + 20.0f, legendY);
legendNormalText_->setTextColor(Color(0.8f, 0.8f, 0.8f, 1.0f));
addChild(legendNormalText_);
legendY += 25.0f;
legendCollidingText_ = Text::create("- 碰撞中", infoFont_);
legendCollidingText_->setPosition(legendX + 20.0f, legendY);
legendCollidingText_->setTextColor(Color(0.8f, 0.8f, 0.8f, 1.0f));
addChild(legendCollidingText_);
}
/**
* @brief
*/
void createPhysicsNodes(size_t count) {
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_real_distribution<float> posX(50.0f, screenWidth_ - 50.0f);
std::uniform_real_distribution<float> posY(50.0f, screenHeight_ - 50.0f);
std::uniform_real_distribution<float> colorR(0.2f, 0.9f);
std::uniform_real_distribution<float> colorG(0.2f, 0.9f);
std::uniform_real_distribution<float> colorB(0.2f, 0.9f);
for (size_t i = 0; i < count; ++i) {
Color color(colorR(gen), colorG(gen), colorB(gen), 0.7f);
auto node = makePtr<PhysicsNode>(20.0f, color, static_cast<int>(i));
node->setPosition(Vec2(posX(gen), posY(gen)));
addChild(node);
}
}
/**
* @brief
*/
size_t getPhysicsNodeCount() const {
size_t count = 0;
for (const auto &child : children()) {
if (dynamic_cast<PhysicsNode *>(child.get())) {
++count;
}
}
return count;
}
/**
* @brief
*/
std::vector<PhysicsNode *> getPhysicsNodes() const {
std::vector<PhysicsNode *> nodes;
for (const auto &child : children()) {
if (auto node = dynamic_cast<PhysicsNode *>(child.get())) {
nodes.push_back(node);
}
}
return nodes;
}
/**
* @brief
*/
void addNodes(size_t count) {
size_t currentCount = getPhysicsNodeCount();
if (currentCount + count > 5000) {
E2D_LOG_WARN("节点数量已达上限(5000)");
return;
}
createPhysicsNodes(count);
E2D_LOG_INFO("添加 {} 个节点,当前总数: {}", count, getPhysicsNodeCount());
}
/**
* @brief
*/
void removeNodes(size_t count) {
auto physicsNodes = getPhysicsNodes();
if (count >= physicsNodes.size()) {
count = physicsNodes.size();
}
if (count == 0)
return;
// 从后往前移除指定数量的节点
for (size_t i = 0; i < count; ++i) {
// 找到最后一个物理节点对应的子节点并移除
for (auto it = children().rbegin(); it != children().rend(); ++it) {
if (dynamic_cast<PhysicsNode *>(it->get())) {
removeChild(*it);
break;
}
}
}
E2D_LOG_INFO("移除 {} 个节点,当前总数: {}", count, getPhysicsNodeCount());
}
/**
* @brief
*/
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("切换到四叉树策略");
}
}
/**
* @brief 使
*
*
* - Scene::queryCollisions() -
* - SpatialManager::queryCollisions() -
*/
void performCollisionDetection() {
// 清除之前的碰撞状态
for (const auto &child : children()) {
if (auto node = dynamic_cast<PhysicsNode *>(child.get())) {
node->setColliding(false);
}
}
// 使用引擎自带的空间索引进行碰撞检测
// 这是核心Scene::queryCollisions() 会自动使用 SpatialManager
auto collisions = queryCollisions();
stats_.collisionCount = collisions.size();
// 标记碰撞的节点
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);
}
}
}
/**
* @brief UI文本内容
*/
void updateUI() {
if (!nodeCountText_)
return;
auto &app = Application::instance();
// 使用 setFormat 格式化文本
nodeCountText_->setFormat("节点数量: %zu", stats_.nodeCount);
strategyText_->setFormat("索引策略: %s", stats_.strategyName);
collisionText_->setFormat("碰撞对数: %zu", stats_.collisionCount);
updateTimeText_->setFormat("更新时间: %.2f ms", stats_.updateTime);
collisionTimeText_->setFormat("碰撞检测: %.2f ms", stats_.collisionTime);
renderTimeText_->setFormat("渲染时间: %.2f ms", stats_.renderTime);
fpsText_->setFormat("FPS: %u", app.fps());
}
/**
* @brief
*/
void drawLegend(Renderer &renderer) {
float legendX = screenWidth_ - 200.0f;
float legendY = 20.0f + 25.0f; // 在标题下方
// 绘制正常状态方块
renderer.fillRect(Rect(legendX, legendY, 15.0f, 15.0f),
Color(0.5f, 0.5f, 0.9f, 0.7f));
legendY += 25.0f;
// 绘制碰撞状态方块
renderer.fillRect(Rect(legendX, legendY, 15.0f, 15.0f),
Color(1.0f, 0.2f, 0.2f, 0.9f));
}
PerformanceStats stats_;
float screenWidth_ = 1280.0f;
float screenHeight_ = 720.0f;
Ptr<FontAtlas> titleFont_;
Ptr<FontAtlas> infoFont_;
// UI 文本组件
Ptr<Text> titleText_;
Ptr<Text> nodeCountText_;
Ptr<Text> strategyText_;
Ptr<Text> collisionText_;
Ptr<Text> updateTimeText_;
Ptr<Text> collisionTimeText_;
Ptr<Text> renderTimeText_;
Ptr<Text> fpsText_;
Ptr<Text> helpTitleText_;
Ptr<Text> helpAddText_;
Ptr<Text> helpRemoveText_;
Ptr<Text> helpToggleText_;
Ptr<Text> helpExitText_;
Ptr<Text> legendTitleText_;
Ptr<Text> legendNormalText_;
Ptr<Text> legendCollidingText_;
};
// ============================================================================
// 程序入口
// ============================================================================
int main(int argc, char **argv) {
Logger::init();
Logger::setLevel(LogLevel::Debug);
E2D_LOG_INFO("========================");
E2D_LOG_INFO("Easy2D 引擎空间索引演示");
E2D_LOG_INFO("========================");
auto &app = Application::instance();
AppConfig config;
config.title = "Easy2D - 引擎空间索引演示";
config.width = 1280;
config.height = 720;
config.vsync = true;
config.fpsLimit = 60;
if (!app.init(config)) {
E2D_LOG_ERROR("应用初始化失败!");
return -1;
}
app.enterScene(makePtr<SpatialIndexDemoScene>());
E2D_LOG_INFO("开始主循环...");
app.run();
E2D_LOG_INFO("应用结束");
return 0;
}

View File

@ -1,76 +0,0 @@
-- ==============================================
-- Spatial Index Demo 示例 - Xmake 构建脚本
-- 支持平台: MinGW (Windows), Nintendo Switch
-- ==============================================
-- 获取当前脚本所在目录(示例根目录)
local example_dir = os.scriptdir()
-- 可执行文件目标
target("spatial_index_demo")
set_kind("binary")
add_files("main.cpp")
add_includedirs("../../include")
add_deps("extra2d")
-- 使用与主项目相同的平台配置
if is_plat("switch") then
set_targetdir("../../build/examples/spatial_index_demo")
-- 构建后生成 NRO 文件
after_build(function (target)
local devkitPro = os.getenv("DEVKITPRO") or "C:/devkitPro"
local elf_file = target:targetfile()
local output_dir = path.directory(elf_file)
local nacp_file = path.join(output_dir, "spatial_index_demo.nacp")
local nro_file = path.join(output_dir, "spatial_index_demo.nro")
local nacptool = path.join(devkitPro, "tools/bin/nacptool.exe")
local elf2nro = path.join(devkitPro, "tools/bin/elf2nro.exe")
if os.isfile(nacptool) and os.isfile(elf2nro) then
os.vrunv(nacptool, {"--create", "Spatial Index Demo", "Extra2D Team", "1.0.0", nacp_file})
local romfs = path.join(example_dir, "romfs")
if os.isdir(romfs) then
os.vrunv(elf2nro, {elf_file, nro_file, "--nacp=" .. nacp_file, "--romfsdir=" .. romfs})
else
os.vrunv(elf2nro, {elf_file, nro_file, "--nacp=" .. nacp_file})
end
print("Generated NRO: " .. nro_file)
end
end)
-- 打包时将 NRO 文件复制到 package 目录
after_package(function (target)
local nro_file = path.join(target:targetdir(), "spatial_index_demo.nro")
local package_dir = target:packagedir()
if os.isfile(nro_file) and package_dir then
os.cp(nro_file, package_dir)
print("Copied NRO to package: " .. package_dir)
end
end)
elseif is_plat("mingw") then
set_targetdir("../../build/examples/spatial_index_demo")
add_ldflags("-mwindows", {force = true})
-- 复制资源到输出目录
after_build(function (target)
local romfs = path.join(example_dir, "romfs")
if os.isdir(romfs) then
local target_dir = path.directory(target:targetfile())
local assets_dir = path.join(target_dir, "assets")
-- 创建 assets 目录
if not os.isdir(assets_dir) then
os.mkdir(assets_dir)
end
-- 复制所有资源文件(包括子目录)
os.cp(path.join(romfs, "assets/**"), assets_dir)
print("Copied assets from " .. romfs .. " to " .. assets_dir)
else
print("Warning: romfs directory not found at " .. romfs)
end
end)
end
target_end()

View File

@ -71,12 +71,6 @@
#include <utils/random.h> #include <utils/random.h>
#include <utils/timer.h> #include <utils/timer.h>
// Spatial
#include <spatial/quadtree.h>
#include <spatial/spatial_hash.h>
#include <spatial/spatial_index.h>
#include <spatial/spatial_manager.h>
// Application // Application
#include <app/application.h> #include <app/application.h>

View File

@ -146,17 +146,10 @@ public:
virtual void onDetachFromScene(); virtual void onDetachFromScene();
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
// 边界框(用于空间索引) // 边界框
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
virtual Rect boundingBox() const; virtual Rect boundingBox() const;
// 是否需要参与空间索引(默认 true
void setSpatialIndexed(bool indexed) { spatialIndexed_ = indexed; }
bool isSpatialIndexed() const { return spatialIndexed_; }
// 更新空间索引(手动调用,通常在边界框变化后)
void updateSpatialIndex();
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
// 事件系统 // 事件系统
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
@ -300,10 +293,7 @@ private:
Vec2 anchor_ = Vec2(0.5f, 0.5f); // 8 bytes Vec2 anchor_ = Vec2(0.5f, 0.5f); // 8 bytes
Vec2 skew_ = Vec2::Zero(); // 8 bytes Vec2 skew_ = Vec2::Zero(); // 8 bytes
// 8. 边界框(用于空间索引) // 8. 浮点属性
Rect lastSpatialBounds_; // 16 bytes
// 9. 浮点属性
float rotation_ = 0.0f; // 4 bytes float rotation_ = 0.0f; // 4 bytes
float opacity_ = 1.0f; // 4 bytes float opacity_ = 1.0f; // 4 bytes
@ -321,15 +311,14 @@ private:
// 11. 场景指针 // 11. 场景指针
Scene *scene_ = nullptr; // 8 bytes Scene *scene_ = nullptr; // 8 bytes
// 12. 布尔标志(打包在一起) // 9. 布尔标志(打包在一起)
mutable bool transformDirty_ = true; // 1 byte mutable bool transformDirty_ = true; // 1 byte
mutable bool worldTransformDirty_ = true; // 1 byte mutable bool worldTransformDirty_ = true; // 1 byte
bool childrenOrderDirty_ = false; // 1 byte bool childrenOrderDirty_ = false; // 1 byte
bool visible_ = true; // 1 byte bool visible_ = true; // 1 byte
bool running_ = false; // 1 byte bool running_ = false; // 1 byte
bool spatialIndexed_ = true; // 1 byte
// 13. Tween 动画列表 // 10. Tween 动画列表
std::vector<std::shared_ptr<Tween>> tweens_; std::vector<std::shared_ptr<Tween>> tweens_;
}; };

View File

@ -3,7 +3,6 @@
#include <core/color.h> #include <core/color.h>
#include <graphics/camera.h> #include <graphics/camera.h>
#include <scene/node.h> #include <scene/node.h>
#include <spatial/spatial_manager.h>
#include <vector> #include <vector>
namespace extra2d { namespace extra2d {
@ -61,28 +60,6 @@ public:
void collectRenderCommands(std::vector<RenderCommand> &commands, void collectRenderCommands(std::vector<RenderCommand> &commands,
int parentZOrder = 0) override; int parentZOrder = 0) override;
// ------------------------------------------------------------------------
// 空间索引系统
// ------------------------------------------------------------------------
SpatialManager &getSpatialManager() { return spatialManager_; }
const SpatialManager &getSpatialManager() const { return spatialManager_; }
// 启用/禁用空间索引
void setSpatialIndexingEnabled(bool enabled) {
spatialIndexingEnabled_ = enabled;
}
bool isSpatialIndexingEnabled() const { return spatialIndexingEnabled_; }
// 节点空间索引管理(内部使用)
void updateNodeInSpatialIndex(Node *node, const Rect &oldBounds,
const Rect &newBounds);
void removeNodeFromSpatialIndex(Node *node);
// 碰撞检测查询
std::vector<Node *> queryNodesInArea(const Rect &area) const;
std::vector<Node *> queryNodesAtPoint(const Vec2 &point) const;
std::vector<std::pair<Node *, Node *>> queryCollisions() const;
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
// 静态创建方法 // 静态创建方法
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
@ -107,10 +84,6 @@ private:
Ptr<Camera> defaultCamera_; Ptr<Camera> defaultCamera_;
bool paused_ = false; bool paused_ = false;
// 空间索引系统
SpatialManager spatialManager_;
bool spatialIndexingEnabled_ = true;
}; };
} // namespace extra2d } // namespace extra2d

View File

@ -1,70 +0,0 @@
#pragma once
#include <array>
#include <spatial/spatial_index.h>
namespace extra2d {
class QuadTree : public ISpatialIndex {
public:
static constexpr int MAX_OBJECTS = 10;
static constexpr int MAX_LEVELS = 5;
struct QuadTreeNode {
Rect bounds;
int level;
std::vector<std::pair<Node *, Rect>> objects;
std::array<std::unique_ptr<QuadTreeNode>, 4> children;
QuadTreeNode(const Rect &bounds, int level);
bool contains(const Rect &rect) const;
bool intersects(const Rect &rect) const;
};
explicit QuadTree(const Rect &worldBounds);
~QuadTree() override = default;
void insert(Node *node, const Rect &bounds) override;
void remove(Node *node) override;
void update(Node *node, const Rect &newBounds) override;
std::vector<Node *> query(const Rect &area) const override;
std::vector<Node *> query(const Vec2 &point) const override;
std::vector<std::pair<Node *, Node *>> queryCollisions() const override;
void clear() override;
size_t size() const override;
bool empty() const override;
void rebuild() override;
private:
void split(QuadTreeNode *node);
void insertIntoNode(QuadTreeNode *node, Node *object, const Rect &bounds);
void queryNode(const QuadTreeNode *node, const Rect &area,
std::vector<Node *> &results) const;
void queryNode(const QuadTreeNode *node, const Vec2 &point,
std::vector<Node *> &results) const;
void
collectCollisions(const QuadTreeNode *node,
std::vector<std::pair<Node *, Node *>> &collisions) const;
bool removeFromNode(QuadTreeNode *node, Node *object);
/**
* @brief 使线
* @param objects
* @param collisions
*/
void detectCollisionsInNode(
const std::vector<std::pair<Node *, Rect>> &objects,
std::vector<std::pair<Node *, Node *>> &collisions) const;
std::unique_ptr<QuadTreeNode> root_;
Rect worldBounds_;
size_t objectCount_ = 0;
// 碰撞检测用的临时缓冲区,避免重复分配
mutable std::vector<std::pair<Node *, Rect>> collisionBuffer_;
};
} // namespace extra2d

View File

@ -1,75 +0,0 @@
#pragma once
#include <spatial/spatial_index.h>
#include <unordered_map>
#include <vector>
namespace extra2d {
/**
* @brief -
* 使
*/
class SpatialHash : public ISpatialIndex {
public:
using CellKey = std::pair<int64_t, int64_t>;
struct CellKeyHash {
size_t operator()(const CellKey &key) const {
return std::hash<int64_t>()(key.first) ^
(std::hash<int64_t>()(key.second) << 1);
}
};
explicit SpatialHash(float cellSize = 64.0f);
~SpatialHash() override = default;
void insert(Node *node, const Rect &bounds) override;
void remove(Node *node) override;
void update(Node *node, const Rect &newBounds) override;
std::vector<Node *> query(const Rect &area) const override;
std::vector<Node *> query(const Vec2 &point) const override;
std::vector<std::pair<Node *, Node *>> queryCollisions() const override;
void clear() override;
size_t size() const override;
bool empty() const override;
void rebuild() override;
void setCellSize(float cellSize);
float getCellSize() const { return cellSize_; }
private:
/**
* @brief - 使vector代替unordered_set减少内存开销
*/
struct Cell {
std::vector<Node *> objects;
void insert(Node *node);
void remove(Node *node);
bool contains(Node *node) const;
void clear() { objects.clear(); }
size_t size() const { return objects.size(); }
bool empty() const { return objects.empty(); }
};
CellKey getCellKey(float x, float y) const;
void getCellsForRect(const Rect &rect, std::vector<CellKey> &cells) const;
void insertIntoCells(Node *node, const Rect &bounds);
void removeFromCells(Node *node, const Rect &bounds);
float cellSize_;
// 使用vector存储对象列表代替unordered_set内存更紧凑
std::unordered_map<CellKey, Cell, CellKeyHash> grid_;
std::unordered_map<Node *, Rect> objectBounds_;
size_t objectCount_ = 0;
// 查询用的临时缓冲区,避免重复分配
mutable std::vector<Node *> queryBuffer_;
mutable std::vector<std::pair<Node *, Node *>> collisionBuffer_;
};
} // namespace extra2d

View File

@ -1,40 +0,0 @@
#pragma once
#include <core/math_types.h>
#include <core/types.h>
#include <memory>
#include <vector>
namespace extra2d {
class Node;
enum class SpatialStrategy { Auto, QuadTree, SpatialHash };
struct SpatialQueryResult {
Node *node;
Rect bounds;
};
class ISpatialIndex {
public:
virtual ~ISpatialIndex() = default;
virtual void insert(Node *node, const Rect &bounds) = 0;
virtual void remove(Node *node) = 0;
virtual void update(Node *node, const Rect &newBounds) = 0;
virtual std::vector<Node *> query(const Rect &area) const = 0;
virtual std::vector<Node *> query(const Vec2 &point) const = 0;
virtual std::vector<std::pair<Node *, Node *>> queryCollisions() const = 0;
virtual void clear() = 0;
virtual size_t size() const = 0;
virtual bool empty() const = 0;
virtual void rebuild() = 0;
};
using SpatialIndexPtr = std::unique_ptr<ISpatialIndex>;
} // namespace extra2d

View File

@ -1,62 +0,0 @@
#pragma once
#include <spatial/spatial_index.h>
#include <functional>
#include <memory>
namespace extra2d {
class SpatialManager {
public:
using QueryCallback = std::function<bool(Node *)>;
SpatialManager();
explicit SpatialManager(const Rect &worldBounds);
~SpatialManager() = default;
void setStrategy(SpatialStrategy strategy);
void setAutoThresholds(size_t quadTreeThreshold, size_t hashThreshold);
void setWorldBounds(const Rect &bounds);
Rect getWorldBounds() const { return worldBounds_; }
void insert(Node *node, const Rect &bounds);
void remove(Node *node);
void update(Node *node, const Rect &newBounds);
std::vector<Node *> query(const Rect &area) const;
std::vector<Node *> query(const Vec2 &point) const;
std::vector<std::pair<Node *, Node *>> queryCollisions() const;
void query(const Rect &area, const QueryCallback &callback) const;
void query(const Vec2 &point, const QueryCallback &callback) const;
void clear();
size_t size() const;
bool empty() const;
void rebuild();
void optimize();
SpatialStrategy getCurrentStrategy() const;
const char *getStrategyName() const;
static std::unique_ptr<ISpatialIndex> createIndex(SpatialStrategy strategy,
const Rect &bounds);
private:
void selectOptimalStrategy();
SpatialStrategy currentStrategy_ = SpatialStrategy::Auto;
SpatialStrategy activeStrategy_ = SpatialStrategy::QuadTree;
std::unique_ptr<ISpatialIndex> index_;
Rect worldBounds_;
size_t quadTreeThreshold_ = 1000;
size_t hashThreshold_ = 5000;
mutable size_t queryCount_ = 0;
mutable size_t totalQueryTime_ = 0;
};
} // namespace extra2d

View File

@ -62,7 +62,6 @@ public:
va_end(args); va_end(args);
text_ = buffer; text_ = buffer;
sizeDirty_ = true; sizeDirty_ = true;
updateSpatialIndex();
} }
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------

View File

@ -157,7 +157,6 @@ Ptr<Node> Node::childByTag(int tag) const {
void Node::setPosition(const Vec2 &pos) { void Node::setPosition(const Vec2 &pos) {
position_ = pos; position_ = pos;
markTransformDirty(); markTransformDirty();
updateSpatialIndex();
} }
void Node::setPosition(float x, float y) { setPosition(Vec2(x, y)); } void Node::setPosition(float x, float y) { setPosition(Vec2(x, y)); }
@ -165,13 +164,11 @@ void Node::setPosition(float x, float y) { setPosition(Vec2(x, y)); }
void Node::setRotation(float degrees) { void Node::setRotation(float degrees) {
rotation_ = degrees; rotation_ = degrees;
markTransformDirty(); markTransformDirty();
updateSpatialIndex();
} }
void Node::setScale(const Vec2 &scale) { void Node::setScale(const Vec2 &scale) {
scale_ = scale; scale_ = scale;
markTransformDirty(); markTransformDirty();
updateSpatialIndex();
} }
void Node::setScale(float scale) { setScale(Vec2(scale, scale)); } void Node::setScale(float scale) { setScale(Vec2(scale, scale)); }
@ -356,26 +353,12 @@ void Node::onRender(Renderer &renderer) {
void Node::onAttachToScene(Scene *scene) { void Node::onAttachToScene(Scene *scene) {
scene_ = scene; scene_ = scene;
// 添加到场景的空间索引
if (spatialIndexed_ && scene_) {
lastSpatialBounds_ = Rect();
updateSpatialIndex();
}
for (auto &child : children_) { for (auto &child : children_) {
child->onAttachToScene(scene); child->onAttachToScene(scene);
} }
} }
void Node::onDetachFromScene() { void Node::onDetachFromScene() {
// 从场景的空间索引移除
// 注意:即使 lastSpatialBounds_ 为空也要尝试移除,
// 因为节点可能通过其他方式被插入到空间索引中
if (spatialIndexed_ && scene_) {
scene_->removeNodeFromSpatialIndex(this);
lastSpatialBounds_ = Rect();
}
scene_ = nullptr; scene_ = nullptr;
for (auto &child : children_) { for (auto &child : children_) {
child->onDetachFromScene(); child->onDetachFromScene();
@ -387,18 +370,6 @@ Rect Node::boundingBox() const {
return Rect(position_.x, position_.y, 0, 0); return Rect(position_.x, position_.y, 0, 0);
} }
void Node::updateSpatialIndex() {
if (!spatialIndexed_ || !scene_) {
return;
}
Rect newBounds = boundingBox();
if (newBounds != lastSpatialBounds_) {
scene_->updateNodeInSpatialIndex(this, lastSpatialBounds_, newBounds);
lastSpatialBounds_ = newBounds;
}
}
void Node::update(float dt) { onUpdate(dt); } void Node::update(float dt) { onUpdate(dt); }
void Node::render(Renderer &renderer) { void Node::render(Renderer &renderer) {

View File

@ -58,66 +58,12 @@ void Scene::updateScene(float dt) {
void Scene::onEnter() { void Scene::onEnter() {
Node::onEnter(); Node::onEnter();
// 初始化空间索引世界边界
if (spatialIndexingEnabled_) {
spatialManager_.setWorldBounds(
Rect(0, 0, viewportSize_.width, viewportSize_.height));
}
} }
void Scene::onExit() { void Scene::onExit() {
// 清理空间索引
spatialManager_.clear();
Node::onExit(); Node::onExit();
} }
void Scene::updateNodeInSpatialIndex(Node *node, const Rect &oldBounds,
const Rect &newBounds) {
if (!spatialIndexingEnabled_ || !node || !node->isSpatialIndexed()) {
return;
}
// 如果旧边界有效,先移除
if (!oldBounds.empty()) {
spatialManager_.remove(node);
}
// 如果新边界有效,插入
if (!newBounds.empty()) {
spatialManager_.insert(node, newBounds);
}
}
void Scene::removeNodeFromSpatialIndex(Node *node) {
if (!spatialIndexingEnabled_ || !node) {
return;
}
spatialManager_.remove(node);
}
std::vector<Node *> Scene::queryNodesInArea(const Rect &area) const {
if (!spatialIndexingEnabled_) {
return {};
}
return spatialManager_.query(area);
}
std::vector<Node *> Scene::queryNodesAtPoint(const Vec2 &point) const {
if (!spatialIndexingEnabled_) {
return {};
}
return spatialManager_.query(point);
}
std::vector<std::pair<Node *, Node *>> Scene::queryCollisions() const {
if (!spatialIndexingEnabled_) {
return {};
}
return spatialManager_.queryCollisions();
}
void Scene::collectRenderCommands(std::vector<RenderCommand> &commands, void Scene::collectRenderCommands(std::vector<RenderCommand> &commands,
int parentZOrder) { int parentZOrder) {
if (!visible()) if (!visible())

View File

@ -112,17 +112,14 @@ Ptr<ShapeNode> ShapeNode::createFilledPolygon(const std::vector<Vec2> &points,
void ShapeNode::setPoints(const std::vector<Vec2> &points) { void ShapeNode::setPoints(const std::vector<Vec2> &points) {
points_ = points; points_ = points;
updateSpatialIndex();
} }
void ShapeNode::addPoint(const Vec2 &point) { void ShapeNode::addPoint(const Vec2 &point) {
points_.push_back(point); points_.push_back(point);
updateSpatialIndex();
} }
void ShapeNode::clearPoints() { void ShapeNode::clearPoints() {
points_.clear(); points_.clear();
updateSpatialIndex();
} }
Rect ShapeNode::boundingBox() const { Rect ShapeNode::boundingBox() const {

View File

@ -18,12 +18,10 @@ void Sprite::setTexture(Ptr<Texture> texture) {
textureRect_ = Rect(0, 0, static_cast<float>(texture_->width()), textureRect_ = Rect(0, 0, static_cast<float>(texture_->width()),
static_cast<float>(texture_->height())); static_cast<float>(texture_->height()));
} }
updateSpatialIndex();
} }
void Sprite::setTextureRect(const Rect &rect) { void Sprite::setTextureRect(const Rect &rect) {
textureRect_ = rect; textureRect_ = rect;
updateSpatialIndex();
} }
void Sprite::setColor(const Color &color) { color_ = color; } void Sprite::setColor(const Color &color) { color_ = color; }

View File

@ -1,329 +0,0 @@
#include <algorithm>
#include <scene/node.h>
#include <spatial/quadtree.h>
#include <functional>
namespace extra2d {
QuadTree::QuadTreeNode::QuadTreeNode(const Rect &bounds, int level)
: bounds(bounds), level(level) {}
bool QuadTree::QuadTreeNode::contains(const Rect &rect) const {
return bounds.contains(rect);
}
bool QuadTree::QuadTreeNode::intersects(const Rect &rect) const {
return bounds.intersects(rect);
}
QuadTree::QuadTree(const Rect &worldBounds) : worldBounds_(worldBounds) {
root_ = std::make_unique<QuadTreeNode>(worldBounds, 0);
}
void QuadTree::insert(Node *node, const Rect &bounds) {
if (!node || !root_->intersects(bounds))
return;
insertIntoNode(root_.get(), node, bounds);
objectCount_++;
}
void QuadTree::insertIntoNode(QuadTreeNode *node, Node *object,
const Rect &bounds) {
if (node->children[0]) {
int index = -1;
float midX = node->bounds.origin.x + node->bounds.size.width / 2.0f;
float midY = node->bounds.origin.y + node->bounds.size.height / 2.0f;
bool top = bounds.origin.y + bounds.size.height <= midY;
bool bottom = bounds.origin.y >= midY;
bool left = bounds.origin.x + bounds.size.width <= midX;
bool right = bounds.origin.x >= midX;
if (top && left)
index = 0;
else if (top && right)
index = 1;
else if (bottom && left)
index = 2;
else if (bottom && right)
index = 3;
if (index != -1) {
insertIntoNode(node->children[index].get(), object, bounds);
return;
}
}
node->objects.emplace_back(object, bounds);
if (node->objects.size() > MAX_OBJECTS && node->level < MAX_LEVELS) {
if (!node->children[0]) {
split(node);
}
}
}
void QuadTree::split(QuadTreeNode *node) {
float midX = node->bounds.origin.x + node->bounds.size.width / 2.0f;
float midY = node->bounds.origin.y + node->bounds.size.height / 2.0f;
node->children[0] = std::make_unique<QuadTreeNode>(
Rect(node->bounds.origin.x, node->bounds.origin.y,
node->bounds.size.width / 2.0f, node->bounds.size.height / 2.0f),
node->level + 1);
node->children[1] = std::make_unique<QuadTreeNode>(
Rect(midX, node->bounds.origin.y, node->bounds.size.width / 2.0f,
node->bounds.size.height / 2.0f),
node->level + 1);
node->children[2] = std::make_unique<QuadTreeNode>(
Rect(node->bounds.origin.x, midY, node->bounds.size.width / 2.0f,
node->bounds.size.height / 2.0f),
node->level + 1);
node->children[3] = std::make_unique<QuadTreeNode>(
Rect(midX, midY, node->bounds.size.width / 2.0f,
node->bounds.size.height / 2.0f),
node->level + 1);
auto objects = std::move(node->objects);
node->objects.clear();
for (const auto &[obj, bounds] : objects) {
insertIntoNode(node, obj, bounds);
}
}
void QuadTree::remove(Node *node) {
if (!node)
return;
if (removeFromNode(root_.get(), node)) {
objectCount_--;
}
}
bool QuadTree::removeFromNode(QuadTreeNode *node, Node *object) {
auto it =
std::find_if(node->objects.begin(), node->objects.end(),
[object](const auto &pair) { return pair.first == object; });
if (it != node->objects.end()) {
node->objects.erase(it);
return true;
}
if (node->children[0]) {
for (auto &child : node->children) {
if (removeFromNode(child.get(), object)) {
return true;
}
}
}
return false;
}
void QuadTree::update(Node *node, const Rect &newBounds) {
remove(node);
insert(node, newBounds);
}
std::vector<Node *> QuadTree::query(const Rect &area) const {
std::vector<Node *> results;
queryNode(root_.get(), area, results);
return results;
}
void QuadTree::queryNode(const QuadTreeNode *node, const Rect &area,
std::vector<Node *> &results) const {
if (!node || !node->intersects(area))
return;
for (const auto &[obj, bounds] : node->objects) {
if (bounds.intersects(area)) {
results.push_back(obj);
}
}
if (node->children[0]) {
for (const auto &child : node->children) {
queryNode(child.get(), area, results);
}
}
}
std::vector<Node *> QuadTree::query(const Vec2 &point) const {
std::vector<Node *> results;
queryNode(root_.get(), point, results);
return results;
}
void QuadTree::queryNode(const QuadTreeNode *node, const Vec2 &point,
std::vector<Node *> &results) const {
if (!node || !node->bounds.containsPoint(point))
return;
for (const auto &[obj, bounds] : node->objects) {
if (bounds.containsPoint(point)) {
results.push_back(obj);
}
}
if (node->children[0]) {
for (const auto &child : node->children) {
queryNode(child.get(), point, results);
}
}
}
std::vector<std::pair<Node *, Node *>> QuadTree::queryCollisions() const {
std::vector<std::pair<Node *, Node *>> collisions;
collectCollisions(root_.get(), collisions);
return collisions;
}
void QuadTree::detectCollisionsInNode(
const std::vector<std::pair<Node *, Rect>> &objects,
std::vector<std::pair<Node *, Node *>> &collisions) const {
size_t n = objects.size();
if (n < 2)
return;
// 使用扫描线算法优化碰撞检测
// 按 x 坐标排序,只检查可能重叠的对象
collisionBuffer_.clear();
collisionBuffer_.reserve(n);
collisionBuffer_.assign(objects.begin(), objects.end());
// 按左边界排序
std::sort(collisionBuffer_.begin(), collisionBuffer_.end(),
[](const auto &a, const auto &b) {
return a.second.origin.x < b.second.origin.x;
});
// 扫描线检测
for (size_t i = 0; i < n; ++i) {
const auto &[objA, boundsA] = collisionBuffer_[i];
float rightA = boundsA.origin.x + boundsA.size.width;
// 只检查右边界在 objA 右侧的对象
for (size_t j = i + 1; j < n; ++j) {
const auto &[objB, boundsB] = collisionBuffer_[j];
// 如果 objB 的左边界超过 objA 的右边界,后续对象都不会碰撞
if (boundsB.origin.x > rightA)
break;
// 快速 AABB 检测
if (boundsA.intersects(boundsB)) {
collisions.emplace_back(objA, objB);
}
}
}
}
void QuadTree::collectCollisions(
const QuadTreeNode *node,
std::vector<std::pair<Node *, Node *>> &collisions) const {
if (!node)
return;
// 使用迭代而非递归,避免深层树栈溢出
struct StackItem {
const QuadTreeNode *node;
size_t ancestorStart;
size_t ancestorEnd;
};
std::vector<StackItem> stack;
stack.reserve(32);
stack.push_back({node, 0, 0});
// 祖先对象列表,用于检测跨节点碰撞
collisionBuffer_.clear();
while (!stack.empty()) {
StackItem item = stack.back();
stack.pop_back();
const QuadTreeNode *current = item.node;
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 < validAncestorEnd; ++i) {
const auto &[ancestorObj, ancestorBounds] = collisionBuffer_[i];
if (bounds.intersects(ancestorBounds)) {
collisions.emplace_back(ancestorObj, obj);
}
}
}
// 检测当前节点内对象之间的碰撞(使用扫描线算法)
detectCollisionsInNode(current->objects, collisions);
// 记录当前节点的对象作为祖先
size_t oldSize = collisionBuffer_.size();
collisionBuffer_.insert(collisionBuffer_.end(), current->objects.begin(),
current->objects.end());
// 将子节点压入栈(逆序以保持遍历顺序)
if (current->children[0]) {
for (int i = 3; i >= 0; --i) {
if (current->children[i]) {
stack.push_back({current->children[i].get(), oldSize,
collisionBuffer_.size()});
}
}
}
// 恢复祖先列表(模拟递归返回)
// 只有当栈顶元素的祖先范围与当前不同时,才需要恢复
if (stack.empty()) {
collisionBuffer_.resize(oldSize);
} else {
const auto& nextItem = stack.back();
// 如果下一个节点的祖先范围与当前不同,则需要恢复到其祖先起始位置
if (nextItem.ancestorStart != oldSize) {
collisionBuffer_.resize(oldSize);
}
}
}
}
void QuadTree::clear() {
root_ = std::make_unique<QuadTreeNode>(worldBounds_, 0);
objectCount_ = 0;
}
size_t QuadTree::size() const { return objectCount_; }
bool QuadTree::empty() const { return objectCount_ == 0; }
void QuadTree::rebuild() {
std::vector<std::pair<Node *, Rect>> allObjects;
std::function<void(QuadTreeNode *)> collect = [&](QuadTreeNode *node) {
if (!node)
return;
for (const auto &obj : node->objects) {
allObjects.push_back(obj);
}
if (node->children[0]) {
for (auto &child : node->children) {
collect(child.get());
}
}
};
collect(root_.get());
clear();
for (const auto &[obj, bounds] : allObjects) {
insert(obj, bounds);
}
}
} // namespace extra2d

View File

@ -1,256 +0,0 @@
#include <algorithm>
#include <cstdint>
#include <scene/node.h>
#include <spatial/spatial_hash.h>
namespace extra2d {
// Cell 实现
void SpatialHash::Cell::insert(Node *node) {
// 检查是否已存在
if (!contains(node)) {
objects.push_back(node);
}
}
void SpatialHash::Cell::remove(Node *node) {
auto it = std::find(objects.begin(), objects.end(), node);
if (it != objects.end()) {
// 用最后一个元素替换然后pop_backO(1)操作
*it = objects.back();
objects.pop_back();
}
}
bool SpatialHash::Cell::contains(Node *node) const {
return std::find(objects.begin(), objects.end(), node) != objects.end();
}
SpatialHash::SpatialHash(float cellSize) : cellSize_(cellSize) {
// 预分配查询缓冲区,避免重复分配
queryBuffer_.reserve(64);
collisionBuffer_.reserve(128);
}
SpatialHash::CellKey SpatialHash::getCellKey(float x, float y) const {
int64_t cellX = static_cast<int64_t>(std::floor(x / cellSize_));
int64_t cellY = static_cast<int64_t>(std::floor(y / cellSize_));
return {cellX, cellY};
}
void SpatialHash::getCellsForRect(const Rect &rect,
std::vector<CellKey> &cells) const {
cells.clear();
CellKey minCell = getCellKey(rect.origin.x, rect.origin.y);
CellKey maxCell = getCellKey(rect.origin.x + rect.size.width,
rect.origin.y + rect.size.height);
// 预分配空间
size_t cellCount = (maxCell.first - minCell.first + 1) *
(maxCell.second - minCell.second + 1);
cells.reserve(cellCount);
for (int64_t x = minCell.first; x <= maxCell.first; ++x) {
for (int64_t y = minCell.second; y <= maxCell.second; ++y) {
cells.emplace_back(x, y);
}
}
}
void SpatialHash::insertIntoCells(Node *node, const Rect &bounds) {
std::vector<CellKey> cells;
getCellsForRect(bounds, cells);
for (const auto &cell : cells) {
grid_[cell].insert(node);
}
}
void SpatialHash::removeFromCells(Node *node, const Rect &bounds) {
std::vector<CellKey> cells;
getCellsForRect(bounds, cells);
for (const auto &cell : cells) {
auto it = grid_.find(cell);
if (it != grid_.end()) {
it->second.remove(node);
if (it->second.empty()) {
grid_.erase(it);
}
}
}
}
void SpatialHash::insert(Node *node, const Rect &bounds) {
if (!node)
return;
// 检查节点是否已存在,如果存在则先移除
auto it = objectBounds_.find(node);
if (it != objectBounds_.end()) {
removeFromCells(node, it->second);
it->second = bounds;
} else {
objectBounds_[node] = bounds;
objectCount_++;
}
insertIntoCells(node, bounds);
}
void SpatialHash::remove(Node *node) {
if (!node)
return;
auto it = objectBounds_.find(node);
if (it != objectBounds_.end()) {
removeFromCells(node, it->second);
objectBounds_.erase(it);
objectCount_--;
} else {
// 节点不在 objectBounds_ 中,但可能还在 grid_ 的某些单元格中
// 需要遍历所有单元格来移除
for (auto &[cellKey, cell] : grid_) {
cell.remove(node);
}
}
}
void SpatialHash::update(Node *node, const Rect &newBounds) {
auto it = objectBounds_.find(node);
if (it != objectBounds_.end()) {
removeFromCells(node, it->second);
insertIntoCells(node, newBounds);
it->second = newBounds;
}
}
std::vector<Node *> SpatialHash::query(const Rect &area) const {
queryBuffer_.clear();
std::vector<CellKey> cells;
getCellsForRect(area, cells);
// 使用排序+去重代替unordered_set减少内存分配
for (const auto &cell : cells) {
auto it = grid_.find(cell);
if (it != grid_.end()) {
for (Node *node : it->second.objects) {
queryBuffer_.push_back(node);
}
}
}
// 排序并去重
std::sort(queryBuffer_.begin(), queryBuffer_.end());
queryBuffer_.erase(
std::unique(queryBuffer_.begin(), queryBuffer_.end()),
queryBuffer_.end());
// 过滤实际相交的对象
std::vector<Node *> results;
results.reserve(queryBuffer_.size());
for (Node *node : queryBuffer_) {
auto boundsIt = objectBounds_.find(node);
if (boundsIt != objectBounds_.end() &&
boundsIt->second.intersects(area)) {
results.push_back(node);
}
}
return results;
}
std::vector<Node *> SpatialHash::query(const Vec2 &point) const {
std::vector<Node *> results;
CellKey cell = getCellKey(point.x, point.y);
auto it = grid_.find(cell);
if (it != grid_.end()) {
for (Node *node : it->second.objects) {
auto boundsIt = objectBounds_.find(node);
if (boundsIt != objectBounds_.end() &&
boundsIt->second.containsPoint(point)) {
results.push_back(node);
}
}
}
return results;
}
std::vector<std::pair<Node *, Node *>> SpatialHash::queryCollisions() const {
collisionBuffer_.clear();
// 使用排序+唯一性检查代替unordered_set
std::vector<std::pair<Node *, Node *>> tempCollisions;
tempCollisions.reserve(objectCount_ * 2);
for (const auto &[cell, cellData] : grid_) {
const auto &objects = cellData.objects;
size_t count = objects.size();
// 使用扫描线算法优化单元格内碰撞检测
for (size_t i = 0; i < count; ++i) {
Node *nodeA = objects[i];
auto boundsA = objectBounds_.find(nodeA);
if (boundsA == objectBounds_.end())
continue;
for (size_t j = i + 1; j < count; ++j) {
Node *nodeB = objects[j];
auto boundsB = objectBounds_.find(nodeB);
if (boundsB == objectBounds_.end())
continue;
if (boundsA->second.intersects(boundsB->second)) {
// 确保有序对,便于去重
if (nodeA < nodeB) {
tempCollisions.emplace_back(nodeA, nodeB);
} else {
tempCollisions.emplace_back(nodeB, nodeA);
}
}
}
}
}
// 排序并去重
std::sort(tempCollisions.begin(), tempCollisions.end());
tempCollisions.erase(
std::unique(tempCollisions.begin(), tempCollisions.end()),
tempCollisions.end());
return tempCollisions;
}
void SpatialHash::clear() {
grid_.clear();
objectBounds_.clear();
objectCount_ = 0;
}
size_t SpatialHash::size() const { return objectCount_; }
bool SpatialHash::empty() const { return objectCount_ == 0; }
void SpatialHash::rebuild() {
auto bounds = objectBounds_;
clear();
for (const auto &[node, bound] : bounds) {
insert(node, bound);
}
}
void SpatialHash::setCellSize(float cellSize) {
if (cellSize != cellSize_ && cellSize > 0) {
cellSize_ = cellSize;
rebuild();
}
}
} // namespace extra2d

View File

@ -1,198 +0,0 @@
#include <chrono>
#include <scene/node.h>
#include <spatial/quadtree.h>
#include <spatial/spatial_hash.h>
#include <spatial/spatial_manager.h>
namespace extra2d {
SpatialManager::SpatialManager() : worldBounds_(0, 0, 10000, 10000) {
selectOptimalStrategy();
}
SpatialManager::SpatialManager(const Rect &worldBounds)
: worldBounds_(worldBounds) {
selectOptimalStrategy();
}
void SpatialManager::setStrategy(SpatialStrategy strategy) {
if (currentStrategy_ == strategy)
return;
currentStrategy_ = strategy;
rebuild();
}
void SpatialManager::setAutoThresholds(size_t quadTreeThreshold,
size_t hashThreshold) {
quadTreeThreshold_ = quadTreeThreshold;
hashThreshold_ = hashThreshold;
if (currentStrategy_ == SpatialStrategy::Auto) {
selectOptimalStrategy();
}
}
void SpatialManager::setWorldBounds(const Rect &bounds) {
worldBounds_ = bounds;
rebuild();
}
void SpatialManager::insert(Node *node, const Rect &bounds) {
if (!index_) {
selectOptimalStrategy();
}
if (index_) {
index_->insert(node, bounds);
}
}
void SpatialManager::remove(Node *node) {
if (index_) {
index_->remove(node);
}
}
void SpatialManager::update(Node *node, const Rect &newBounds) {
if (index_) {
index_->update(node, newBounds);
}
}
std::vector<Node *> SpatialManager::query(const Rect &area) const {
if (!index_)
return {};
auto start = std::chrono::high_resolution_clock::now();
auto results = index_->query(area);
auto end = std::chrono::high_resolution_clock::now();
queryCount_++;
totalQueryTime_ +=
std::chrono::duration_cast<std::chrono::microseconds>(end - start)
.count();
return results;
}
std::vector<Node *> SpatialManager::query(const Vec2 &point) const {
if (!index_)
return {};
return index_->query(point);
}
std::vector<std::pair<Node *, Node *>> SpatialManager::queryCollisions() const {
if (!index_)
return {};
return index_->queryCollisions();
}
void SpatialManager::query(const Rect &area,
const QueryCallback &callback) const {
auto results = query(area);
for (Node *node : results) {
if (!callback(node))
break;
}
}
void SpatialManager::query(const Vec2 &point,
const QueryCallback &callback) const {
auto results = query(point);
for (Node *node : results) {
if (!callback(node))
break;
}
}
void SpatialManager::clear() {
if (index_) {
index_->clear();
}
}
size_t SpatialManager::size() const { return index_ ? index_->size() : 0; }
bool SpatialManager::empty() const { return index_ ? index_->empty() : true; }
void SpatialManager::rebuild() {
if (!index_) {
selectOptimalStrategy();
return;
}
auto oldIndex = std::move(index_);
selectOptimalStrategy();
if (index_ && oldIndex) {
auto bounds = Rect(worldBounds_);
auto objects = oldIndex->query(bounds);
for (Node *node : objects) {
if (node) {
auto nodeBounds = node->boundingBox();
index_->insert(node, nodeBounds);
}
}
}
}
void SpatialManager::optimize() {
if (currentStrategy_ == SpatialStrategy::Auto) {
selectOptimalStrategy();
}
if (index_) {
index_->rebuild();
}
}
SpatialStrategy SpatialManager::getCurrentStrategy() const {
return activeStrategy_;
}
const char *SpatialManager::getStrategyName() const {
switch (activeStrategy_) {
case SpatialStrategy::QuadTree:
return "QuadTree";
case SpatialStrategy::SpatialHash:
return "SpatialHash";
default:
return "Unknown";
}
}
std::unique_ptr<ISpatialIndex>
SpatialManager::createIndex(SpatialStrategy strategy, const Rect &bounds) {
switch (strategy) {
case SpatialStrategy::QuadTree:
return std::make_unique<QuadTree>(bounds);
case SpatialStrategy::SpatialHash:
return std::make_unique<SpatialHash>(64.0f);
default:
return std::make_unique<QuadTree>(bounds);
}
}
void SpatialManager::selectOptimalStrategy() {
if (currentStrategy_ != SpatialStrategy::Auto) {
activeStrategy_ = currentStrategy_;
} else {
size_t currentSize = index_ ? index_->size() : 0;
if (currentSize < quadTreeThreshold_) {
activeStrategy_ = SpatialStrategy::QuadTree;
} else if (currentSize > hashThreshold_) {
activeStrategy_ = SpatialStrategy::SpatialHash;
} else {
// Keep current strategy in transition zone
if (!index_) {
activeStrategy_ = SpatialStrategy::QuadTree;
}
}
}
index_ = createIndex(activeStrategy_, worldBounds_);
}
} // namespace extra2d

View File

@ -16,7 +16,6 @@ namespace extra2d {
*/ */
Button::Button() { Button::Button() {
setAnchor(0.0f, 0.0f); setAnchor(0.0f, 0.0f);
setSpatialIndexed(false);
auto &dispatcher = getEventDispatcher(); auto &dispatcher = getEventDispatcher();
dispatcher.addListener(EventType::UIHoverEnter, [this](Event &) { dispatcher.addListener(EventType::UIHoverEnter, [this](Event &) {

View File

@ -52,7 +52,6 @@ Ptr<Label> Label::create(const std::string &text, Ptr<FontAtlas> font) {
void Label::setText(const std::string &text) { void Label::setText(const std::string &text) {
text_ = text; text_ = text;
sizeDirty_ = true; sizeDirty_ = true;
updateSpatialIndex();
} }
/** /**
@ -62,7 +61,6 @@ void Label::setText(const std::string &text) {
void Label::setFont(Ptr<FontAtlas> font) { void Label::setFont(Ptr<FontAtlas> font) {
font_ = font; font_ = font;
sizeDirty_ = true; sizeDirty_ = true;
updateSpatialIndex();
} }
/** /**
@ -78,7 +76,6 @@ void Label::setTextColor(const Color &color) { textColor_ = color; }
void Label::setFontSize(int size) { void Label::setFontSize(int size) {
fontSize_ = size; fontSize_ = size;
sizeDirty_ = true; sizeDirty_ = true;
updateSpatialIndex();
} }
/** /**
@ -136,7 +133,6 @@ void Label::setOutlineWidth(float width) { outlineWidth_ = width; }
void Label::setMultiLine(bool multiLine) { void Label::setMultiLine(bool multiLine) {
multiLine_ = multiLine; multiLine_ = multiLine;
sizeDirty_ = true; sizeDirty_ = true;
updateSpatialIndex();
} }
/** /**
@ -146,7 +142,6 @@ void Label::setMultiLine(bool multiLine) {
void Label::setLineSpacing(float spacing) { void Label::setLineSpacing(float spacing) {
lineSpacing_ = spacing; lineSpacing_ = spacing;
sizeDirty_ = true; sizeDirty_ = true;
updateSpatialIndex();
} }
/** /**
@ -156,7 +151,6 @@ void Label::setLineSpacing(float spacing) {
void Label::setMaxWidth(float maxWidth) { void Label::setMaxWidth(float maxWidth) {
maxWidth_ = maxWidth; maxWidth_ = maxWidth;
sizeDirty_ = true; sizeDirty_ = true;
updateSpatialIndex();
} }
/** /**

View File

@ -27,7 +27,6 @@ Text::Text(const std::string &text) : text_(text) {
void Text::setText(const std::string &text) { void Text::setText(const std::string &text) {
text_ = text; text_ = text;
sizeDirty_ = true; sizeDirty_ = true;
updateSpatialIndex();
} }
/** /**
@ -37,7 +36,6 @@ void Text::setText(const std::string &text) {
void Text::setFont(Ptr<FontAtlas> font) { void Text::setFont(Ptr<FontAtlas> font) {
font_ = font; font_ = font;
sizeDirty_ = true; sizeDirty_ = true;
updateSpatialIndex();
} }
/** /**
@ -53,7 +51,6 @@ void Text::setTextColor(const Color &color) { color_ = color; }
void Text::setFontSize(int size) { void Text::setFontSize(int size) {
fontSize_ = size; fontSize_ = size;
sizeDirty_ = true; sizeDirty_ = true;
updateSpatialIndex();
} }
/** /**
@ -62,7 +59,6 @@ void Text::setFontSize(int size) {
*/ */
void Text::setAlignment(Alignment align) { void Text::setAlignment(Alignment align) {
alignment_ = align; alignment_ = align;
updateSpatialIndex();
} }
/** /**
@ -71,7 +67,6 @@ void Text::setAlignment(Alignment align) {
*/ */
void Text::setVerticalAlignment(VerticalAlignment align) { void Text::setVerticalAlignment(VerticalAlignment align) {
verticalAlignment_ = align; verticalAlignment_ = align;
updateSpatialIndex();
} }
/** /**

View File

@ -3,11 +3,10 @@
namespace extra2d { namespace extra2d {
Widget::Widget() { setSpatialIndexed(false); } Widget::Widget() {}
void Widget::setSize(const Size &size) { void Widget::setSize(const Size &size) {
size_ = size; size_ = size;
updateSpatialIndex();
} }
void Widget::setSize(float width, float height) { void Widget::setSize(float width, float height) {

View File

@ -88,10 +88,7 @@ define_extra2d_engine()
-- 示例程序目标(作为子项目) -- 示例程序目标(作为子项目)
if is_config("examples","true") then if is_config("examples","true") then
includes("examples/hello_world", {rootdir = "examples/hello_world"}) includes("examples/hello_world", {rootdir = "examples/hello_world"})
includes("examples/spatial_index_demo", {rootdir = "examples/spatial_index_demo"})
includes("examples/collision_demo", {rootdir = "examples/collision_demo"}) includes("examples/collision_demo", {rootdir = "examples/collision_demo"})
includes("examples/push_box", {rootdir = "examples/push_box"})
includes("examples/flappy_bird", {rootdir = "examples/flappy_bird"})
end end
-- ============================================== -- ==============================================