refactor: 移除空间索引系统及相关示例代码
移除空间索引系统(QuadTree/SpatialHash)及相关实现代码 删除push_box和flappy_bird示例游戏及其资源文件 清理不再需要的空间索引相关函数调用和头文件引用
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
// ============================================================================
|
||||
// SplashScene.h - 启动场景
|
||||
// 描述: 显示游戏 Logo,2秒后自动跳转到开始场景
|
||||
// ============================================================================
|
||||
|
||||
#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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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}
|
||||
]
|
||||
}
|
||||
|
Before Width: | Height: | Size: 74 KiB |
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 477 B |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 658 B |
|
Before Width: | Height: | Size: 682 B |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 641 B |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 191 KiB |
|
Before Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 642 B |
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
@ -71,12 +71,6 @@
|
|||
#include <utils/random.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
|
||||
#include <app/application.h>
|
||||
|
||||
|
|
|
|||
|
|
@ -146,17 +146,10 @@ public:
|
|||
virtual void onDetachFromScene();
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// 边界框(用于空间索引)
|
||||
// 边界框
|
||||
// ------------------------------------------------------------------------
|
||||
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 skew_ = Vec2::Zero(); // 8 bytes
|
||||
|
||||
// 8. 边界框(用于空间索引)
|
||||
Rect lastSpatialBounds_; // 16 bytes
|
||||
|
||||
// 9. 浮点属性
|
||||
// 8. 浮点属性
|
||||
float rotation_ = 0.0f; // 4 bytes
|
||||
float opacity_ = 1.0f; // 4 bytes
|
||||
|
||||
|
|
@ -321,15 +311,14 @@ private:
|
|||
// 11. 场景指针
|
||||
Scene *scene_ = nullptr; // 8 bytes
|
||||
|
||||
// 12. 布尔标志(打包在一起)
|
||||
// 9. 布尔标志(打包在一起)
|
||||
mutable bool transformDirty_ = true; // 1 byte
|
||||
mutable bool worldTransformDirty_ = true; // 1 byte
|
||||
bool childrenOrderDirty_ = false; // 1 byte
|
||||
bool visible_ = true; // 1 byte
|
||||
bool running_ = false; // 1 byte
|
||||
bool spatialIndexed_ = true; // 1 byte
|
||||
|
||||
// 13. Tween 动画列表
|
||||
// 10. Tween 动画列表
|
||||
std::vector<std::shared_ptr<Tween>> tweens_;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
#include <core/color.h>
|
||||
#include <graphics/camera.h>
|
||||
#include <scene/node.h>
|
||||
#include <spatial/spatial_manager.h>
|
||||
#include <vector>
|
||||
|
||||
namespace extra2d {
|
||||
|
|
@ -61,28 +60,6 @@ public:
|
|||
void collectRenderCommands(std::vector<RenderCommand> &commands,
|
||||
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_;
|
||||
|
||||
bool paused_ = false;
|
||||
|
||||
// 空间索引系统
|
||||
SpatialManager spatialManager_;
|
||||
bool spatialIndexingEnabled_ = true;
|
||||
};
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -62,7 +62,6 @@ public:
|
|||
va_end(args);
|
||||
text_ = buffer;
|
||||
sizeDirty_ = true;
|
||||
updateSpatialIndex();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -157,7 +157,6 @@ Ptr<Node> Node::childByTag(int tag) const {
|
|||
void Node::setPosition(const Vec2 &pos) {
|
||||
position_ = pos;
|
||||
markTransformDirty();
|
||||
updateSpatialIndex();
|
||||
}
|
||||
|
||||
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) {
|
||||
rotation_ = degrees;
|
||||
markTransformDirty();
|
||||
updateSpatialIndex();
|
||||
}
|
||||
|
||||
void Node::setScale(const Vec2 &scale) {
|
||||
scale_ = scale;
|
||||
markTransformDirty();
|
||||
updateSpatialIndex();
|
||||
}
|
||||
|
||||
void Node::setScale(float scale) { setScale(Vec2(scale, scale)); }
|
||||
|
|
@ -356,26 +353,12 @@ void Node::onRender(Renderer &renderer) {
|
|||
void Node::onAttachToScene(Scene *scene) {
|
||||
scene_ = scene;
|
||||
|
||||
// 添加到场景的空间索引
|
||||
if (spatialIndexed_ && scene_) {
|
||||
lastSpatialBounds_ = Rect();
|
||||
updateSpatialIndex();
|
||||
}
|
||||
|
||||
for (auto &child : children_) {
|
||||
child->onAttachToScene(scene);
|
||||
}
|
||||
}
|
||||
|
||||
void Node::onDetachFromScene() {
|
||||
// 从场景的空间索引移除
|
||||
// 注意:即使 lastSpatialBounds_ 为空也要尝试移除,
|
||||
// 因为节点可能通过其他方式被插入到空间索引中
|
||||
if (spatialIndexed_ && scene_) {
|
||||
scene_->removeNodeFromSpatialIndex(this);
|
||||
lastSpatialBounds_ = Rect();
|
||||
}
|
||||
|
||||
scene_ = nullptr;
|
||||
for (auto &child : children_) {
|
||||
child->onDetachFromScene();
|
||||
|
|
@ -387,18 +370,6 @@ Rect Node::boundingBox() const {
|
|||
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::render(Renderer &renderer) {
|
||||
|
|
|
|||
|
|
@ -58,66 +58,12 @@ void Scene::updateScene(float dt) {
|
|||
|
||||
void Scene::onEnter() {
|
||||
Node::onEnter();
|
||||
|
||||
// 初始化空间索引世界边界
|
||||
if (spatialIndexingEnabled_) {
|
||||
spatialManager_.setWorldBounds(
|
||||
Rect(0, 0, viewportSize_.width, viewportSize_.height));
|
||||
}
|
||||
}
|
||||
|
||||
void Scene::onExit() {
|
||||
// 清理空间索引
|
||||
spatialManager_.clear();
|
||||
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,
|
||||
int parentZOrder) {
|
||||
if (!visible())
|
||||
|
|
|
|||
|
|
@ -112,17 +112,14 @@ Ptr<ShapeNode> ShapeNode::createFilledPolygon(const std::vector<Vec2> &points,
|
|||
|
||||
void ShapeNode::setPoints(const std::vector<Vec2> &points) {
|
||||
points_ = points;
|
||||
updateSpatialIndex();
|
||||
}
|
||||
|
||||
void ShapeNode::addPoint(const Vec2 &point) {
|
||||
points_.push_back(point);
|
||||
updateSpatialIndex();
|
||||
}
|
||||
|
||||
void ShapeNode::clearPoints() {
|
||||
points_.clear();
|
||||
updateSpatialIndex();
|
||||
}
|
||||
|
||||
Rect ShapeNode::boundingBox() const {
|
||||
|
|
|
|||
|
|
@ -18,12 +18,10 @@ void Sprite::setTexture(Ptr<Texture> texture) {
|
|||
textureRect_ = Rect(0, 0, static_cast<float>(texture_->width()),
|
||||
static_cast<float>(texture_->height()));
|
||||
}
|
||||
updateSpatialIndex();
|
||||
}
|
||||
|
||||
void Sprite::setTextureRect(const Rect &rect) {
|
||||
textureRect_ = rect;
|
||||
updateSpatialIndex();
|
||||
}
|
||||
|
||||
void Sprite::setColor(const Color &color) { color_ = color; }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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_back,O(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
|
||||
|
|
@ -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
|
||||
|
|
@ -16,7 +16,6 @@ namespace extra2d {
|
|||
*/
|
||||
Button::Button() {
|
||||
setAnchor(0.0f, 0.0f);
|
||||
setSpatialIndexed(false);
|
||||
|
||||
auto &dispatcher = getEventDispatcher();
|
||||
dispatcher.addListener(EventType::UIHoverEnter, [this](Event &) {
|
||||
|
|
|
|||
|
|
@ -52,7 +52,6 @@ Ptr<Label> Label::create(const std::string &text, Ptr<FontAtlas> font) {
|
|||
void Label::setText(const std::string &text) {
|
||||
text_ = text;
|
||||
sizeDirty_ = true;
|
||||
updateSpatialIndex();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -62,7 +61,6 @@ void Label::setText(const std::string &text) {
|
|||
void Label::setFont(Ptr<FontAtlas> font) {
|
||||
font_ = font;
|
||||
sizeDirty_ = true;
|
||||
updateSpatialIndex();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -78,7 +76,6 @@ void Label::setTextColor(const Color &color) { textColor_ = color; }
|
|||
void Label::setFontSize(int size) {
|
||||
fontSize_ = size;
|
||||
sizeDirty_ = true;
|
||||
updateSpatialIndex();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -136,7 +133,6 @@ void Label::setOutlineWidth(float width) { outlineWidth_ = width; }
|
|||
void Label::setMultiLine(bool multiLine) {
|
||||
multiLine_ = multiLine;
|
||||
sizeDirty_ = true;
|
||||
updateSpatialIndex();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -146,7 +142,6 @@ void Label::setMultiLine(bool multiLine) {
|
|||
void Label::setLineSpacing(float spacing) {
|
||||
lineSpacing_ = spacing;
|
||||
sizeDirty_ = true;
|
||||
updateSpatialIndex();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -156,7 +151,6 @@ void Label::setLineSpacing(float spacing) {
|
|||
void Label::setMaxWidth(float maxWidth) {
|
||||
maxWidth_ = maxWidth;
|
||||
sizeDirty_ = true;
|
||||
updateSpatialIndex();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ Text::Text(const std::string &text) : text_(text) {
|
|||
void Text::setText(const std::string &text) {
|
||||
text_ = text;
|
||||
sizeDirty_ = true;
|
||||
updateSpatialIndex();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -37,7 +36,6 @@ void Text::setText(const std::string &text) {
|
|||
void Text::setFont(Ptr<FontAtlas> font) {
|
||||
font_ = font;
|
||||
sizeDirty_ = true;
|
||||
updateSpatialIndex();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -53,7 +51,6 @@ void Text::setTextColor(const Color &color) { color_ = color; }
|
|||
void Text::setFontSize(int size) {
|
||||
fontSize_ = size;
|
||||
sizeDirty_ = true;
|
||||
updateSpatialIndex();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -62,7 +59,6 @@ void Text::setFontSize(int size) {
|
|||
*/
|
||||
void Text::setAlignment(Alignment align) {
|
||||
alignment_ = align;
|
||||
updateSpatialIndex();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -71,7 +67,6 @@ void Text::setAlignment(Alignment align) {
|
|||
*/
|
||||
void Text::setVerticalAlignment(VerticalAlignment align) {
|
||||
verticalAlignment_ = align;
|
||||
updateSpatialIndex();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -3,11 +3,10 @@
|
|||
|
||||
namespace extra2d {
|
||||
|
||||
Widget::Widget() { setSpatialIndexed(false); }
|
||||
Widget::Widget() {}
|
||||
|
||||
void Widget::setSize(const Size &size) {
|
||||
size_ = size;
|
||||
updateSpatialIndex();
|
||||
}
|
||||
|
||||
void Widget::setSize(float width, float height) {
|
||||
|
|
|
|||
|
|
@ -88,10 +88,7 @@ define_extra2d_engine()
|
|||
-- 示例程序目标(作为子项目)
|
||||
if is_config("examples","true") then
|
||||
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/push_box", {rootdir = "examples/push_box"})
|
||||
includes("examples/flappy_bird", {rootdir = "examples/flappy_bird"})
|
||||
end
|
||||
|
||||
-- ==============================================
|
||||
|
|
|
|||