diff --git a/examples/collision_demo/main.cpp b/examples/collision_demo/main.cpp new file mode 100644 index 0000000..109cd5a --- /dev/null +++ b/examples/collision_demo/main.cpp @@ -0,0 +1,281 @@ +#include +#include +#include + + +using namespace extra2d; + +// ============================================================================ +// 碰撞测试节点 - 有实际边界框 +// ============================================================================ +class CollisionBox : public Node { +public: + CollisionBox(float width, float height, const Color &color) + : width_(width), height_(height), color_(color), isColliding_(false) { + // 启用空间索引,这是碰撞检测的关键 + setSpatialIndexed(true); + } + + void setColliding(bool colliding) { isColliding_ = colliding; } + + Rect getBoundingBox() const override { + // 返回实际的矩形边界 + Vec2 pos = getPosition(); + return Rect(pos.x - width_ / 2, pos.y - height_ / 2, width_, height_); + } + + void onRender(RenderBackend &renderer) override { + Vec2 pos = getPosition(); + + // 绘制填充矩形 + Color fillColor = isColliding_ ? Color(1.0f, 0.2f, 0.2f, 0.8f) : color_; + renderer.fillRect( + Rect(pos.x - width_ / 2, pos.y - height_ / 2, width_, height_), + fillColor); + + // 绘制边框 + Color borderColor = isColliding_ ? Color(1.0f, 0.0f, 0.0f, 1.0f) + : Color(1.0f, 1.0f, 1.0f, 0.5f); + float borderWidth = isColliding_ ? 3.0f : 2.0f; + renderer.drawRect( + Rect(pos.x - width_ / 2, pos.y - height_ / 2, width_, height_), + borderColor, borderWidth); + } + +private: + float width_, height_; + Color color_; + bool isColliding_; +}; + +// ============================================================================ +// 碰撞检测场景 +// ============================================================================ +class CollisionDemoScene : public Scene { +public: + void onEnter() override { + E2D_LOG_INFO("CollisionDemoScene::onEnter - 碰撞检测演示"); + + // 设置背景色 + setBackgroundColor(Color(0.05f, 0.05f, 0.1f, 1.0f)); + + // 获取屏幕中心 + auto &app = Application::instance(); + float centerX = app.getConfig().width / 2.0f; + float centerY = app.getConfig().height / 2.0f; + + // 创建静态碰撞框 + createStaticBoxes(centerX, centerY); + + // 创建移动的中心方块 + centerBox_ = + makePtr(80.0f, 80.0f, Color(0.2f, 0.6f, 1.0f, 0.8f)); + centerBox_->setPosition(Vec2(centerX, centerY)); + addChild(centerBox_); + + // 加载字体 + loadFonts(); + + E2D_LOG_INFO("创建了 {} 个碰撞框", boxes_.size() + 1); + } + + void onUpdate(float dt) override { + Scene::onUpdate(dt); + + // 旋转中心方块 + rotationAngle_ += rotationSpeed_ * dt; + if (rotationAngle_ >= 360.0f) + rotationAngle_ -= 360.0f; + + // 让中心方块沿圆形路径移动 + float radius = 150.0f; + float rad = rotationAngle_ * 3.14159f / 180.0f; + + auto &app = Application::instance(); + Vec2 center = + Vec2(app.getConfig().width / 2.0f, app.getConfig().height / 2.0f); + + centerBox_->setPosition(Vec2(center.x + std::cos(rad) * radius, + center.y + std::sin(rad) * radius)); + centerBox_->setRotation(rotationAngle_); + + // 执行碰撞检测 + performCollisionDetection(); + + // 检查退出按键 + auto &input = Application::instance().input(); + if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_START)) { + E2D_LOG_INFO("退出应用"); + Application::instance().quit(); + } + } + + void onRender(RenderBackend &renderer) override { + Scene::onRender(renderer); + + // 绘制说明文字 + drawUI(renderer); + } + +private: + /** + * @brief 加载字体资源 + */ + void loadFonts() { + auto &resources = Application::instance().resources(); + + std::vector fontPaths = { + "romfs:/assets/font.ttf" // 备选字体 + }; + + titleFont_ = resources.loadFontWithFallbacks(fontPaths, 60, true); + infoFont_ = resources.loadFontWithFallbacks(fontPaths, 28, true); + + if (!titleFont_) { + E2D_LOG_WARN("无法加载标题字体"); + } + if (!infoFont_) { + E2D_LOG_WARN("无法加载信息字体"); + } + } + + /** + * @brief 创建静态碰撞框 + */ + void createStaticBoxes(float centerX, float centerY) { + // 创建围绕中心的静态碰撞框 + std::vector> positions = { + {Vec2(centerX - 200, centerY - 150), Color(0.3f, 1.0f, 0.3f, 0.7f)}, + {Vec2(centerX + 200, centerY - 150), Color(1.0f, 0.3f, 0.3f, 0.7f)}, + {Vec2(centerX - 200, centerY + 150), Color(0.3f, 0.3f, 1.0f, 0.7f)}, + {Vec2(centerX + 200, centerY + 150), Color(1.0f, 1.0f, 0.3f, 0.7f)}, + {Vec2(centerX, centerY - 220), Color(1.0f, 0.3f, 1.0f, 0.7f)}, + {Vec2(centerX, centerY + 220), Color(0.3f, 1.0f, 1.0f, 0.7f)}, + }; + + for (const auto &[pos, color] : positions) { + auto box = makePtr(70.0f, 70.0f, color); + box->setPosition(pos); + addChild(box); + boxes_.push_back(box); + } + } + + /** + * @brief 执行碰撞检测 + */ + void performCollisionDetection() { + // 清除之前的碰撞状态 + centerBox_->setColliding(false); + for (auto &box : boxes_) { + box->setColliding(false); + } + + // 使用空间索引进行碰撞检测 + auto collisions = queryCollisions(); + + collisionCount_ = collisions.size(); + + // 标记碰撞的节点 + for (const auto &[nodeA, nodeB] : collisions) { + if (auto boxA = dynamic_cast(nodeA)) { + boxA->setColliding(true); + } + if (auto boxB = dynamic_cast(nodeB)) { + boxB->setColliding(true); + } + } + } + + /** + * @brief 绘制UI界面 + */ + void drawUI(RenderBackend &renderer) { + if (!titleFont_ || !infoFont_) + return; + + auto &app = Application::instance(); + + // 绘制标题 + renderer.drawText(*titleFont_, "碰撞检测演示", Vec2(50.0f, 30.0f), + Color(1.0f, 1.0f, 1.0f, 1.0f)); + + // 绘制说明文字 + renderer.drawText(*infoFont_, "蓝色方块旋转并检测碰撞", Vec2(50.0f, 80.0f), + Color(0.8f, 0.8f, 0.8f, 1.0f)); + renderer.drawText(*infoFont_, "红色 = 检测到碰撞", Vec2(50.0f, 105.0f), + Color(1.0f, 0.5f, 0.5f, 1.0f)); + + // 绘制碰撞统计 + std::stringstream ss; + ss << "碰撞数: " << collisionCount_; + renderer.drawText(*infoFont_, ss.str(), Vec2(50.0f, 150.0f), + Color(1.0f, 1.0f, 0.5f, 1.0f)); + + // 绘制 FPS + ss.str(""); + ss << "FPS: " << app.fps(); + renderer.drawText(*infoFont_, ss.str(), Vec2(50.0f, 175.0f), + Color(0.8f, 1.0f, 0.8f, 1.0f)); + + // 绘制操作提示 + float screenHeight = static_cast(app.getConfig().height); + renderer.drawText(*infoFont_, "按 + 键退出", + Vec2(50.0f, screenHeight - 50.0f), + Color(0.8f, 0.8f, 0.8f, 1.0f)); + } + + Ptr centerBox_; + std::vector> boxes_; + float rotationAngle_ = 0.0f; + float rotationSpeed_ = 60.0f; // 旋转速度(度/秒) + size_t collisionCount_ = 0; + + // 字体资源 + Ptr titleFont_; + Ptr infoFont_; +}; + +// ============================================================================ +// 程序入口 +// ============================================================================ + +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()); + + E2D_LOG_INFO("开始主循环..."); + + // 运行应用 + app.run(); + + E2D_LOG_INFO("应用结束"); + + return 0; +} diff --git a/examples/collision_demo/romfs/assets/font.ttf b/examples/collision_demo/romfs/assets/font.ttf new file mode 100644 index 0000000..8997148 Binary files /dev/null and b/examples/collision_demo/romfs/assets/font.ttf differ diff --git a/examples/collision_demo/xmake.lua b/examples/collision_demo/xmake.lua new file mode 100644 index 0000000..1e10e59 --- /dev/null +++ b/examples/collision_demo/xmake.lua @@ -0,0 +1,61 @@ +-- ============================================== +-- Collision Demo 示例 - Xmake 构建脚本 +-- 支持平台: MinGW (Windows), Nintendo Switch +-- ============================================== + +-- 获取当前平台 +local host_plat = os.host() +local target_plat = get_config("plat") or host_plat + +-- 可执行文件目标 +target("collision_demo") + set_kind("binary") + add_files("main.cpp") + add_includedirs("../../Extra2D/include") + add_deps("extra2d") + + if target_plat == "switch" then + set_plat("switch") + set_arch("arm64") + set_toolchains("switch") + set_targetdir("build/switch") + + 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, "collision_demo.nacp") + local nro_file = path.join(output_dir, "collision_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", "Collision Demo", "Extra2D Team", "1.0.0", nacp_file}) + local romfs = path.absolute("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 + end + end) + + elseif target_plat == "mingw" then + set_plat("mingw") + set_arch("x86_64") + set_targetdir("build/mingw") + add_ldflags("-mwindows", {force = true}) + + after_build(function (target) + local romfs = path.absolute("romfs") + if os.isdir(romfs) then + local target_dir = path.directory(target:targetfile()) + local assets_dir = path.join(target_dir, "assets") + if not os.isdir(assets_dir) then + os.mkdir(assets_dir) + end + os.cp(path.join(romfs, "assets/*"), assets_dir) + end + end) + end +target_end() diff --git a/examples/hello_world/main.cpp b/examples/hello_world/main.cpp new file mode 100644 index 0000000..88ef188 --- /dev/null +++ b/examples/hello_world/main.cpp @@ -0,0 +1,119 @@ +#include + +using namespace extra2d; + + +// ============================================================================ +// Hello World 场景 +// ============================================================================ + +/** + * @brief Hello World 场景类 + * 显示简单的 "Hello World" 文字 + */ +class HelloWorldScene : public Scene { +public: + /** + * @brief 场景进入时调用 + */ + void onEnter() override { + E2D_LOG_INFO("HelloWorldScene::onEnter - 进入场景"); + + // 设置背景颜色为深蓝色 + setBackgroundColor(Color(0.1f, 0.1f, 0.3f, 1.0f)); + + // 加载字体(支持多种字体后备) + auto &resources = Application::instance().resources(); + font_ = resources.loadFont("assets/font.ttf", 48, true); + + if (!font_) { + E2D_LOG_ERROR("字体加载失败,文字渲染将不可用!"); + } + } + + /** + * @brief 每帧更新时调用 + * @param dt 时间间隔(秒) + */ + void onUpdate(float dt) override { + Scene::onUpdate(dt); + + // 检查退出按键 + auto &input = Application::instance().input(); + + // Switch: 使用手柄 START 按钮 + if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_START)) { + E2D_LOG_INFO("退出应用 (START 按钮)"); + Application::instance().quit(); + } + } + + /** + * @brief 渲染时调用 + * @param renderer 渲染后端 + */ + void onRender(RenderBackend &renderer) override { + Scene::onRender(renderer); + + if (!font_) + return; + + // 屏幕中心位置 + float centerX = 640.0f; // 1280 / 2 + float centerY = 360.0f; // 720 / 2 + + // 绘制 "你好世界" 文字(白色,居中) + renderer.drawText(*font_, "你好世界", Vec2(centerX - 100.0f, centerY),Color(1.0f, 1.0f, 1.0f, 1.0f)); + + // 绘制提示文字(黄色) + renderer.drawText(*font_, "退出按键(START 按钮)",Vec2(centerX - 80.0f, centerY + 50.0f), Color(1.0f, 1.0f, 0.0f, 1.0f)); + } + +private: + Ptr font_; // 字体图集 +}; + +// ============================================================================ +// 程序入口 +// ============================================================================ + +int main(int argc, char **argv) +{ + // 初始化日志系统 + Logger::init(); + Logger::setLevel(LogLevel::Debug); + + E2D_LOG_INFO("========================"); + E2D_LOG_INFO("Easy2D Hello World Demo"); + E2D_LOG_INFO("Platform: {}", platform::getPlatformName()); + E2D_LOG_INFO("========================"); + + // 获取应用实例 + auto &app = Application::instance(); + + // 配置应用 + AppConfig config; + config.title = "Easy2D - Hello World"; + config.width = 1280; + config.height = 720; + config.vsync = true; + config.fpsLimit = 60; + + // 初始化应用 + if (!app.init(config)) { + E2D_LOG_ERROR("应用初始化失败!"); + return -1; + } + + // 进入 Hello World 场景 + app.enterScene(makePtr()); + + E2D_LOG_INFO("开始主循环..."); + + // 运行应用 + app.run(); + + E2D_LOG_INFO("应用结束"); + + return 0; +} diff --git a/examples/hello_world/romfs/assets/font.ttf b/examples/hello_world/romfs/assets/font.ttf new file mode 100644 index 0000000..8997148 Binary files /dev/null and b/examples/hello_world/romfs/assets/font.ttf differ diff --git a/examples/hello_world/xmake.lua b/examples/hello_world/xmake.lua new file mode 100644 index 0000000..4b84725 --- /dev/null +++ b/examples/hello_world/xmake.lua @@ -0,0 +1,63 @@ +-- ============================================== +-- Hello World 示例 - Xmake 构建脚本 +-- 支持平台: MinGW (Windows), Nintendo Switch +-- ============================================== + +-- 获取当前平台 +local host_plat = os.host() +local target_plat = get_config("plat") or host_plat + +-- 可执行文件目标 +target("hello_world") + set_kind("binary") + add_files("main.cpp") + add_includedirs("../../Extra2D/include") + add_deps("extra2d") + + if target_plat == "switch" then + set_plat("switch") + set_arch("arm64") + set_toolchains("switch") + set_targetdir("build/switch") + + -- 生成 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, "hello_world.nacp") + local nro_file = path.join(output_dir, "hello_world.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", "Hello World", "Extra2D Team", "1.0.0", nacp_file}) + local romfs = path.absolute("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 + end + end) + + elseif target_plat == "mingw" then + set_plat("mingw") + set_arch("x86_64") + set_targetdir("build/mingw") + add_ldflags("-mwindows", {force = true}) + + -- 复制资源 + after_build(function (target) + local romfs = path.absolute("romfs") + if os.isdir(romfs) then + local target_dir = path.directory(target:targetfile()) + local assets_dir = path.join(target_dir, "assets") + if not os.isdir(assets_dir) then + os.mkdir(assets_dir) + end + os.cp(path.join(romfs, "assets/*"), assets_dir) + end + end) + end +target_end() diff --git a/examples/push_box/PlayScene.cpp b/examples/push_box/PlayScene.cpp new file mode 100644 index 0000000..fcd3af7 --- /dev/null +++ b/examples/push_box/PlayScene.cpp @@ -0,0 +1,400 @@ +#include "PlayScene.h" + +#include "audio_context.h" +#include "audio_controller.h" +#include "storage.h" +#include "StartScene.h" +#include "SuccessScene.h" +#include + +namespace pushbox { + +static extra2d::Ptr loadFont(int size) { + auto& resources = extra2d::Application::instance().resources(); + auto font = resources.loadFont("assets/font.ttf", size); + return font; +} + +PlayScene::PlayScene(int level) { + setBackgroundColor(extra2d::Colors::Black); + + auto& app = extra2d::Application::instance(); + auto& config = app.getConfig(); + setViewportSize(static_cast(config.width), static_cast(config.height)); + + 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 = static_cast(app.getConfig().width); + float screenH = static_cast(app.getConfig().height); + + // 计算游戏区域居中偏移(假设游戏区域是 640x480) + float gameWidth = 640.0f; + float gameHeight = 480.0f; + float offsetX = (screenW - gameWidth) / 2.0f; + float offsetY = (screenH - gameHeight) / 2.0f; + + // 音效图标(左上角,与主界面一致) + auto soundOn = resources.loadTexture("assets/images/soundon.png"); + auto soundOff = resources.loadTexture("assets/images/soundoff.png"); + if (soundOn && soundOff) { + soundIcon_ = extra2d::Sprite::create(g_SoundOpen ? soundOn : soundOff); + soundIcon_->setPosition(offsetX + 50.0f, offsetY + 50.0f); + addChild(soundIcon_); + } + + 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_); + + mapLayer_ = extra2d::makePtr(); + mapLayer_->setAnchor(0.0f, 0.0f); + mapLayer_->setPosition(0.0f, 0.0f); + addChild(mapLayer_); + + auto audioNode = AudioController::create(); + audioNode->setName("AudioController"); + addChild(audioNode); + setAudioController(audioNode); + + setLevel(level); +} + +void PlayScene::onEnter() { + Scene::onEnter(); + updateSoundIcon(); + updateMenuColors(); +} + +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); + } +} + +void PlayScene::onUpdate(float dt) { + Scene::onUpdate(dt); + + auto& app = extra2d::Application::instance(); + auto& input = app.input(); + + // B 键返回主菜单 + if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_B)) { + app.scenes().replaceScene( + extra2d::makePtr(), extra2d::TransitionType::Fade, 0.2f); + return; + } + + // Y 键重开 + if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_Y)) { + setLevel(g_CurrentLevel); + return; + } + + // X 键直接切换音效 + if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_X)) { + g_SoundOpen = !g_SoundOpen; + if (auto audio = getAudioController()) { + audio->setEnabled(g_SoundOpen); + } + updateSoundIcon(); + return; + } + + // A 键执行选中的菜单项 + if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_A)) { + executeMenuItem(); + return; + } + + // 方向键移动 + if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_DPAD_UP)) { + move(0, -1, 1); + flush(); + } else if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_DPAD_DOWN)) { + move(0, 1, 2); + flush(); + } else if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_DPAD_LEFT)) { + move(-1, 0, 3); + flush(); + } else if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_DPAD_RIGHT)) { + 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(); +} + +void PlayScene::executeMenuItem() { + switch (menuIndex_) { + case 0: // 重开 + setLevel(g_CurrentLevel); + break; + case 1: // 切换音效 + g_SoundOpen = !g_SoundOpen; + if (auto audio = getAudioController()) { + audio->setEnabled(g_SoundOpen); + } + updateSoundIcon(); + break; + } +} + +void PlayScene::updateSoundIcon() { + if (!soundIcon_) return; + + auto& app = extra2d::Application::instance(); + auto& resources = app.resources(); + auto soundOn = resources.loadTexture("assets/images/soundon.png"); + auto soundOff = resources.loadTexture("assets/images/soundoff.png"); + + if (soundOn && soundOff) { + soundIcon_->setTexture(g_SoundOpen ? soundOn : soundOff); + } +} + +void PlayScene::flush() { + mapLayer_->removeAllChildren(); + + int tileW = texFloor_ ? texFloor_->getWidth() : 32; + int tileH = texFloor_ ? texFloor_->getHeight() : 32; + + // 获取窗口尺寸,计算游戏区域居中偏移 + auto& app = extra2d::Application::instance(); + float screenW = static_cast(app.getConfig().width); + float screenH = static_cast(app.getConfig().height); + float gameWidth = 640.0f; + float gameHeight = 480.0f; + float baseOffsetX = (screenW - gameWidth) / 2.0f; + float baseOffsetY = (screenH - gameHeight) / 2.0f; + + // 在 12x12 网格中居中地图 + float mapOffsetX = static_cast((12 - map_.width) / 2) * tileW; + float mapOffsetY = static_cast((12 - map_.height) / 2) * 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 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(i * tileW), + offsetY + static_cast(j * tileH)); + mapLayer_->addChild(sprite); + } + } +} + +void PlayScene::setLevel(int level) { + g_CurrentLevel = level; + saveCurrentLevel(g_CurrentLevel); + + 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(); +} + +void PlayScene::setStep(int step) { + step_ = step; + if (stepText_) { + stepText_->setText("当前" + std::to_string(step) + "步"); + } +} + +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; + } + + 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; + if (auto audio = getAudioController()) { + audio->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; + } + + map_.value[boxY][boxX].type = TYPE::Box; + map_.value[targetY][targetX].type = TYPE::Man; + map_.value[map_.roleY][map_.roleX].type = TYPE::Ground; + + if (auto audio = getAudioController()) { + audio->playBoxMove(); + } + } else { + return; + } + + map_.roleX = targetX; + map_.roleY = targetY; + setStep(step_ + 1); +} + +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(), + extra2d::TransitionType::Fade, 0.25f); + return; + } + + setLevel(g_CurrentLevel + 1); +} + +} // namespace pushbox diff --git a/examples/push_box/PlayScene.h b/examples/push_box/PlayScene.h new file mode 100644 index 0000000..2f6b81b --- /dev/null +++ b/examples/push_box/PlayScene.h @@ -0,0 +1,51 @@ +#pragma once + +#include "data.h" +#include + +namespace pushbox { + +class PlayScene : public extra2d::Scene { +public: + explicit PlayScene(int level); + + void onEnter() override; + void onUpdate(float dt) override; + +private: + void updateMenuColors(); + void executeMenuItem(); + void updateSoundIcon(); + void flush(); + void setLevel(int level); + void setStep(int step); + void move(int dx, int dy, int direct); + void gameOver(); + + int step_ = 0; + int menuIndex_ = 0; + Map map_{}; + + extra2d::Ptr font28_; + extra2d::Ptr font20_; + + extra2d::Ptr levelText_; + extra2d::Ptr stepText_; + extra2d::Ptr bestText_; + extra2d::Ptr restartText_; + extra2d::Ptr soundToggleText_; + extra2d::Ptr mapLayer_; + + extra2d::Ptr soundIcon_; + + extra2d::Ptr texWall_; + extra2d::Ptr texPoint_; + extra2d::Ptr texFloor_; + extra2d::Ptr texBox_; + extra2d::Ptr texBoxInPoint_; + + extra2d::Ptr texMan_[5]; + extra2d::Ptr texManPush_[5]; +}; + +} // namespace pushbox diff --git a/examples/push_box/StartScene.cpp b/examples/push_box/StartScene.cpp new file mode 100644 index 0000000..8d20786 --- /dev/null +++ b/examples/push_box/StartScene.cpp @@ -0,0 +1,217 @@ +#include "StartScene.h" + +#include "audio_context.h" +#include "audio_controller.h" +#include "data.h" +#include "PlayScene.h" +#include + +namespace pushbox { + +StartScene::StartScene() { + auto& app = extra2d::Application::instance(); + auto& config = app.getConfig(); + setViewportSize(static_cast(config.width), static_cast(config.height)); +} + +static extra2d::Ptr loadMenuFont() { + auto& resources = extra2d::Application::instance().resources(); + auto font = resources.loadFont("assets/font.ttf", 28); + return font; +} + +void StartScene::onEnter() { + Scene::onEnter(); + + auto& app = extra2d::Application::instance(); + auto& resources = app.resources(); + setBackgroundColor(extra2d::Colors::Black); + + if (getChildren().empty()) { + auto audioNode = AudioController::create(); + audioNode->setName("audio_controller"); + addChild(audioNode); + setAudioController(audioNode); + + float screenW = static_cast(app.getConfig().width); + float screenH = static_cast(app.getConfig().height); + + auto bgTex = resources.loadTexture("assets/images/start.jpg"); + if (bgTex) { + auto background = extra2d::Sprite::create(bgTex); + float bgWidth = static_cast(bgTex->getWidth()); + float bgHeight = static_cast(bgTex->getHeight()); + 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_); + + // 音效开关图标(相对于背景图左上角) + auto soundOn = resources.loadTexture("assets/images/soundon.png"); + auto soundOff = resources.loadTexture("assets/images/soundoff.png"); + if (soundOn && soundOff) { + soundIcon_ = extra2d::Sprite::create(g_SoundOpen ? soundOn : soundOff); + soundIcon_->setPosition(offsetX + 50.0f, offsetY + 50.0f); + addChild(soundIcon_); + } + } + } + + // 始终有3个菜单项 + menuCount_ = 3; + updateMenuColors(); +} + +void StartScene::onUpdate(float dt) { + Scene::onUpdate(dt); + + auto& app = extra2d::Application::instance(); + auto& input = app.input(); + + // 方向键上下切换选择 + if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_DPAD_UP)) { + selectedIndex_ = (selectedIndex_ - 1 + menuCount_) % menuCount_; + updateMenuColors(); + } else if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_DPAD_DOWN)) { + selectedIndex_ = (selectedIndex_ + 1) % menuCount_; + updateMenuColors(); + } + + // A键确认 + if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_A)) { + executeMenuItem(); + } + + // Y键切换音效 + if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_X)) { + g_SoundOpen = !g_SoundOpen; + if (auto audio = getAudioController()) { + audio->setEnabled(g_SoundOpen); + } + updateSoundIcon(); + } +} + +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); + } +} + +void StartScene::updateSoundIcon() { + if (!soundIcon_) return; + + auto& app = extra2d::Application::instance(); + auto& resources = app.resources(); + auto soundOn = resources.loadTexture("assets/images/soundon.png"); + auto soundOff = resources.loadTexture("assets/images/soundoff.png"); + + if (soundOn && soundOff) { + soundIcon_->setTexture(g_SoundOpen ? soundOn : soundOff); + } +} + +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; + } +} + +void StartScene::startNewGame() { + extra2d::Application::instance().scenes().replaceScene( + extra2d::makePtr(1), extra2d::TransitionType::Fade, 0.25f); +} + +void StartScene::continueGame() { + extra2d::Application::instance().scenes().replaceScene( + extra2d::makePtr(g_CurrentLevel), extra2d::TransitionType::Fade, 0.25f); +} + +void StartScene::exitGame() { + extra2d::Application::instance().quit(); +} + +} // namespace pushbox diff --git a/examples/push_box/StartScene.h b/examples/push_box/StartScene.h new file mode 100644 index 0000000..3c627a4 --- /dev/null +++ b/examples/push_box/StartScene.h @@ -0,0 +1,30 @@ +#pragma once + +#include + +namespace pushbox { + +class StartScene : public extra2d::Scene { +public: + StartScene(); + void onEnter() override; + void onUpdate(float dt) override; + +private: + void updateMenuColors(); + void updateSoundIcon(); + void executeMenuItem(); + void startNewGame(); + void continueGame(); + void exitGame(); + + extra2d::Ptr font_; + extra2d::Ptr startBtn_; + extra2d::Ptr resumeBtn_; + extra2d::Ptr exitBtn_; + extra2d::Ptr soundIcon_; + int selectedIndex_ = 0; + int menuCount_ = 3; +}; + +} // namespace pushbox diff --git a/examples/push_box/SuccessScene.cpp b/examples/push_box/SuccessScene.cpp new file mode 100644 index 0000000..be2c1ab --- /dev/null +++ b/examples/push_box/SuccessScene.cpp @@ -0,0 +1,77 @@ +#include "SuccessScene.h" + +#include + +namespace pushbox { + +SuccessScene::SuccessScene() { + auto& app = extra2d::Application::instance(); + auto& config = app.getConfig(); + setViewportSize(static_cast(config.width), static_cast(config.height)); +} + +static extra2d::Ptr loadMenuFont() { + auto& resources = extra2d::Application::instance().resources(); + auto font = resources.loadFont("assets/font.ttf", 28); + return font; +} + +void SuccessScene::onEnter() { + Scene::onEnter(); + + auto& app = extra2d::Application::instance(); + auto& resources = app.resources(); + setBackgroundColor(extra2d::Colors::Black); + + if (getChildren().empty()) { + // 获取窗口尺寸 + float screenW = static_cast(app.getConfig().width); + float screenH = static_cast(app.getConfig().height); + + auto bgTex = resources.loadTexture("assets/images/success.jpg"); + if (bgTex) { + auto background = extra2d::Sprite::create(bgTex); + float bgWidth = static_cast(bgTex->getWidth()); + float bgHeight = static_cast(bgTex->getHeight()); + 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) { + Scene::onUpdate(dt); + + auto& app = extra2d::Application::instance(); + auto& input = app.input(); + + // A键确认返回主菜单 + if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_A)) { + auto& scenes = extra2d::Application::instance().scenes(); + scenes.popScene(extra2d::TransitionType::Fade, 0.2f); + scenes.popScene(extra2d::TransitionType::Fade, 0.2f); + } +} + +} // namespace pushbox diff --git a/examples/push_box/SuccessScene.h b/examples/push_box/SuccessScene.h new file mode 100644 index 0000000..76ce1a5 --- /dev/null +++ b/examples/push_box/SuccessScene.h @@ -0,0 +1,17 @@ +#pragma once + +#include + +namespace pushbox { + +class SuccessScene : public extra2d::Scene { +public: + SuccessScene(); + void onEnter() override; + void onUpdate(float dt) override; + +private: + extra2d::Ptr selectorText_; +}; + +} // namespace pushbox diff --git a/examples/push_box/audio_context.cpp b/examples/push_box/audio_context.cpp new file mode 100644 index 0000000..0b16dab --- /dev/null +++ b/examples/push_box/audio_context.cpp @@ -0,0 +1,17 @@ +#include "audio_context.h" + +#include "audio_controller.h" + +namespace pushbox { + +static extra2d::WeakPtr g_audioController; + +void setAudioController(const extra2d::Ptr& controller) { + g_audioController = controller; +} + +extra2d::Ptr getAudioController() { + return g_audioController.lock(); +} + +} // namespace pushbox diff --git a/examples/push_box/audio_context.h b/examples/push_box/audio_context.h new file mode 100644 index 0000000..fab8de8 --- /dev/null +++ b/examples/push_box/audio_context.h @@ -0,0 +1,12 @@ +#pragma once + +#include + +namespace pushbox { + +class AudioController; + +void setAudioController(const extra2d::Ptr& controller); +extra2d::Ptr getAudioController(); + +} // namespace pushbox diff --git a/examples/push_box/audio_controller.cpp b/examples/push_box/audio_controller.cpp new file mode 100644 index 0000000..6564a8e --- /dev/null +++ b/examples/push_box/audio_controller.cpp @@ -0,0 +1,62 @@ +#include "audio_controller.h" + +#include "storage.h" + +namespace pushbox { + +extra2d::Ptr AudioController::create() { + return extra2d::makePtr(); +} + +void AudioController::onEnter() { + Node::onEnter(); + + if (!loaded_) { + 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); + background_->play(); + } + + loaded_ = true; + } + + setEnabled(g_SoundOpen); +} + +void AudioController::setEnabled(bool enabled) { + enabled_ = enabled; + g_SoundOpen = enabled; + saveSoundOpen(enabled); + + if (!background_) { + return; + } + + if (enabled_) { + background_->resume(); + } else { + background_->pause(); + } +} + +void AudioController::playManMove() { + if (!enabled_ || !manMove_) { + return; + } + manMove_->play(); +} + +void AudioController::playBoxMove() { + if (!enabled_ || !boxMove_) { + return; + } + boxMove_->play(); +} + +} // namespace pushbox diff --git a/examples/push_box/audio_controller.h b/examples/push_box/audio_controller.h new file mode 100644 index 0000000..c73ee3e --- /dev/null +++ b/examples/push_box/audio_controller.h @@ -0,0 +1,29 @@ +#pragma once + +#include "data.h" +#include + +namespace pushbox { + +class AudioController : public extra2d::Node { +public: + static extra2d::Ptr create(); + + void onEnter() override; + + void setEnabled(bool enabled); + bool isEnabled() const { return enabled_; } + + void playManMove(); + void playBoxMove(); + +private: + bool loaded_ = false; + bool enabled_ = true; + + extra2d::Ptr background_; + extra2d::Ptr manMove_; + extra2d::Ptr boxMove_; +}; + +} // namespace pushbox diff --git a/examples/push_box/data.cpp b/examples/push_box/data.cpp new file mode 100644 index 0000000..33902f1 --- /dev/null +++ b/examples/push_box/data.cpp @@ -0,0 +1,117 @@ +#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 diff --git a/examples/push_box/data.h b/examples/push_box/data.h new file mode 100644 index 0000000..5caaa72 --- /dev/null +++ b/examples/push_box/data.h @@ -0,0 +1,28 @@ +#pragma once + +#define MAX_LEVEL 8 + +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]; +}; + +extern Map g_Maps[MAX_LEVEL]; +extern int g_CurrentLevel; +extern bool g_SoundOpen; +extern int g_Direct; +extern bool g_Pushing; + +} // namespace pushbox diff --git a/examples/push_box/main.cpp b/examples/push_box/main.cpp new file mode 100644 index 0000000..00e10e8 --- /dev/null +++ b/examples/push_box/main.cpp @@ -0,0 +1,52 @@ +#include +#include "StartScene.h" +#include "data.h" +#include "storage.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("Platform: {}", platform::getPlatformName()); + 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; + } + + // 初始化存储系统 + pushbox::initStorage("sdmc:/"); + pushbox::g_CurrentLevel = pushbox::loadCurrentLevel(1); + if (pushbox::g_CurrentLevel > MAX_LEVEL) { + pushbox::g_CurrentLevel = 1; + } + pushbox::g_SoundOpen = pushbox::loadSoundOpen(true); + + // 进入开始场景(主界面) + app.enterScene(makePtr()); + + E2D_LOG_INFO("开始主循环..."); + app.run(); + + E2D_LOG_INFO("应用结束"); + return 0; +} diff --git a/examples/push_box/menu_button.cpp b/examples/push_box/menu_button.cpp new file mode 100644 index 0000000..101c76f --- /dev/null +++ b/examples/push_box/menu_button.cpp @@ -0,0 +1,40 @@ +#include "menu_button.h" + +#include + +namespace pushbox { + +extra2d::Ptr MenuButton::create(extra2d::Ptr font, + const extra2d::String& text, + extra2d::Function onClick) { + auto btn = extra2d::makePtr(); + btn->setFont(font); + btn->setText(text); + btn->setPadding(extra2d::Vec2(0.0f, 0.0f)); + btn->setBackgroundColor(extra2d::Colors::Transparent, extra2d::Colors::Transparent, + extra2d::Colors::Transparent); + btn->setBorder(extra2d::Colors::Transparent, 0.0f); + btn->setTextColor(extra2d::Colors::Black); + + btn->onClick_ = std::move(onClick); + btn->setOnClick([wbtn = extra2d::WeakPtr(btn)]() { + if (auto self = wbtn.lock()) { + if (self->enabled_ && self->onClick_) { + self->onClick_(); + } + } + }); + + // 使用事件监听来处理悬停效果 + // Note: Extra2D 的 Button 类可能有不同的悬停检测机制 + // 这里简化处理,仅保留基本功能 + + return btn; +} + +void MenuButton::setEnabled(bool enabled) { + enabled_ = enabled; + setTextColor(enabled ? extra2d::Colors::Black : extra2d::Colors::LightGray); +} + +} // namespace pushbox diff --git a/examples/push_box/menu_button.h b/examples/push_box/menu_button.h new file mode 100644 index 0000000..119393b --- /dev/null +++ b/examples/push_box/menu_button.h @@ -0,0 +1,21 @@ +#pragma once + +#include + +namespace pushbox { + +class MenuButton : public extra2d::Button { +public: + static extra2d::Ptr create(extra2d::Ptr font, + const extra2d::String& text, + extra2d::Function onClick); + + void setEnabled(bool enabled); + bool isEnabled() const { return enabled_; } + +private: + bool enabled_ = true; + extra2d::Function onClick_; +}; + +} // namespace pushbox diff --git a/examples/push_box/romfs/assets/audio/background.wav b/examples/push_box/romfs/assets/audio/background.wav new file mode 100644 index 0000000..8b669c6 Binary files /dev/null and b/examples/push_box/romfs/assets/audio/background.wav differ diff --git a/examples/push_box/romfs/assets/audio/boxmove.wav b/examples/push_box/romfs/assets/audio/boxmove.wav new file mode 100644 index 0000000..cda7cc0 Binary files /dev/null and b/examples/push_box/romfs/assets/audio/boxmove.wav differ diff --git a/examples/push_box/romfs/assets/audio/manmove.wav b/examples/push_box/romfs/assets/audio/manmove.wav new file mode 100644 index 0000000..c13f1bd Binary files /dev/null and b/examples/push_box/romfs/assets/audio/manmove.wav differ diff --git a/examples/push_box/romfs/assets/font.ttf b/examples/push_box/romfs/assets/font.ttf new file mode 100644 index 0000000..8997148 Binary files /dev/null and b/examples/push_box/romfs/assets/font.ttf differ diff --git a/examples/push_box/romfs/assets/images/box.gif b/examples/push_box/romfs/assets/images/box.gif new file mode 100644 index 0000000..4c835fd Binary files /dev/null and b/examples/push_box/romfs/assets/images/box.gif differ diff --git a/examples/push_box/romfs/assets/images/boxinpoint.gif b/examples/push_box/romfs/assets/images/boxinpoint.gif new file mode 100644 index 0000000..a12c967 Binary files /dev/null and b/examples/push_box/romfs/assets/images/boxinpoint.gif differ diff --git a/examples/push_box/romfs/assets/images/floor.gif b/examples/push_box/romfs/assets/images/floor.gif new file mode 100644 index 0000000..321d058 Binary files /dev/null and b/examples/push_box/romfs/assets/images/floor.gif differ diff --git a/examples/push_box/romfs/assets/images/player/mandown.gif b/examples/push_box/romfs/assets/images/player/mandown.gif new file mode 100644 index 0000000..98ccb72 Binary files /dev/null and b/examples/push_box/romfs/assets/images/player/mandown.gif differ diff --git a/examples/push_box/romfs/assets/images/player/manhanddown.gif b/examples/push_box/romfs/assets/images/player/manhanddown.gif new file mode 100644 index 0000000..6e1ba0f Binary files /dev/null and b/examples/push_box/romfs/assets/images/player/manhanddown.gif differ diff --git a/examples/push_box/romfs/assets/images/player/manhandleft.gif b/examples/push_box/romfs/assets/images/player/manhandleft.gif new file mode 100644 index 0000000..18326bf Binary files /dev/null and b/examples/push_box/romfs/assets/images/player/manhandleft.gif differ diff --git a/examples/push_box/romfs/assets/images/player/manhandright.gif b/examples/push_box/romfs/assets/images/player/manhandright.gif new file mode 100644 index 0000000..e6e3618 Binary files /dev/null and b/examples/push_box/romfs/assets/images/player/manhandright.gif differ diff --git a/examples/push_box/romfs/assets/images/player/manhandup.gif b/examples/push_box/romfs/assets/images/player/manhandup.gif new file mode 100644 index 0000000..33e4a79 Binary files /dev/null and b/examples/push_box/romfs/assets/images/player/manhandup.gif differ diff --git a/examples/push_box/romfs/assets/images/player/manleft.gif b/examples/push_box/romfs/assets/images/player/manleft.gif new file mode 100644 index 0000000..e875c28 Binary files /dev/null and b/examples/push_box/romfs/assets/images/player/manleft.gif differ diff --git a/examples/push_box/romfs/assets/images/player/manright.gif b/examples/push_box/romfs/assets/images/player/manright.gif new file mode 100644 index 0000000..bf94f12 Binary files /dev/null and b/examples/push_box/romfs/assets/images/player/manright.gif differ diff --git a/examples/push_box/romfs/assets/images/player/manup.gif b/examples/push_box/romfs/assets/images/player/manup.gif new file mode 100644 index 0000000..7211b42 Binary files /dev/null and b/examples/push_box/romfs/assets/images/player/manup.gif differ diff --git a/examples/push_box/romfs/assets/images/point.gif b/examples/push_box/romfs/assets/images/point.gif new file mode 100644 index 0000000..b81cbe4 Binary files /dev/null and b/examples/push_box/romfs/assets/images/point.gif differ diff --git a/examples/push_box/romfs/assets/images/soundoff.png b/examples/push_box/romfs/assets/images/soundoff.png new file mode 100644 index 0000000..246abd1 Binary files /dev/null and b/examples/push_box/romfs/assets/images/soundoff.png differ diff --git a/examples/push_box/romfs/assets/images/soundon.png b/examples/push_box/romfs/assets/images/soundon.png new file mode 100644 index 0000000..592edb4 Binary files /dev/null and b/examples/push_box/romfs/assets/images/soundon.png differ diff --git a/examples/push_box/romfs/assets/images/start.jpg b/examples/push_box/romfs/assets/images/start.jpg new file mode 100644 index 0000000..0a2e7e8 Binary files /dev/null and b/examples/push_box/romfs/assets/images/start.jpg differ diff --git a/examples/push_box/romfs/assets/images/success.jpg b/examples/push_box/romfs/assets/images/success.jpg new file mode 100644 index 0000000..f12b5ac Binary files /dev/null and b/examples/push_box/romfs/assets/images/success.jpg differ diff --git a/examples/push_box/romfs/assets/images/wall.gif b/examples/push_box/romfs/assets/images/wall.gif new file mode 100644 index 0000000..448dc5e Binary files /dev/null and b/examples/push_box/romfs/assets/images/wall.gif differ diff --git a/examples/push_box/storage.cpp b/examples/push_box/storage.cpp new file mode 100644 index 0000000..cf153aa --- /dev/null +++ b/examples/push_box/storage.cpp @@ -0,0 +1,92 @@ +#include "storage.h" + +#include +#include + +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 diff --git a/examples/push_box/storage.h b/examples/push_box/storage.h new file mode 100644 index 0000000..d766a8a --- /dev/null +++ b/examples/push_box/storage.h @@ -0,0 +1,20 @@ +#pragma once + +#include + +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 diff --git a/examples/push_box/xmake.lua b/examples/push_box/xmake.lua new file mode 100644 index 0000000..1a69ffc --- /dev/null +++ b/examples/push_box/xmake.lua @@ -0,0 +1,61 @@ +-- ============================================== +-- Push Box 示例 - Xmake 构建脚本 +-- 支持平台: MinGW (Windows), Nintendo Switch +-- ============================================== + +-- 获取当前平台 +local host_plat = os.host() +local target_plat = get_config("plat") or host_plat + +-- 可执行文件目标 +target("push_box") + set_kind("binary") + add_files("*.cpp") + add_includedirs("../../Extra2D/include") + add_deps("extra2d") + + if target_plat == "switch" then + set_plat("switch") + set_arch("arm64") + set_toolchains("switch") + set_targetdir("build/switch") + + 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.absolute("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 + end + end) + + elseif target_plat == "mingw" then + set_plat("mingw") + set_arch("x86_64") + set_targetdir("build/mingw") + add_ldflags("-mwindows", {force = true}) + + after_build(function (target) + local romfs = path.absolute("romfs") + if os.isdir(romfs) then + local target_dir = path.directory(target:targetfile()) + local assets_dir = path.join(target_dir, "assets") + if not os.isdir(assets_dir) then + os.mkdir(assets_dir) + end + os.cp(path.join(romfs, "assets/*"), assets_dir) + end + end) + end +target_end() diff --git a/examples/spatial_index_demo/main.cpp b/examples/spatial_index_demo/main.cpp new file mode 100644 index 0000000..99fc4db --- /dev/null +++ b/examples/spatial_index_demo/main.cpp @@ -0,0 +1,436 @@ +#include +#include +#include +#include +#include + +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 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_; } + + // 必须实现 getBoundingBox() 才能参与空间索引碰撞检测 + Rect getBoundingBox() const override { + Vec2 pos = getPosition(); + return Rect(pos.x - size_ / 2, pos.y - size_ / 2, size_, size_); + } + + void update(float dt, float screenWidth, float screenHeight) { + Vec2 pos = getPosition(); + pos = pos + velocity_ * dt; + + // 边界反弹 + if (pos.x < size_ / 2 || pos.x > screenWidth - size_ / 2) { + velocity_.x = -velocity_.x; + pos.x = std::clamp(pos.x, size_ / 2, screenWidth - size_ / 2); + } + if (pos.y < size_ / 2 || pos.y > screenHeight - size_ / 2) { + velocity_.y = -velocity_.y; + pos.y = std::clamp(pos.y, size_ / 2, screenHeight - size_ / 2); + } + + setPosition(pos); + } + + void onRender(RenderBackend &renderer) override { + Vec2 pos = getPosition(); + + // 碰撞时变红色 + Color fillColor = isColliding_ ? Color(1.0f, 0.2f, 0.2f, 0.9f) : color_; + renderer.fillRect(Rect(pos.x - size_ / 2, pos.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(pos.x - size_ / 2, pos.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 { + E2D_LOG_INFO("SpatialIndexDemoScene::onEnter - 引擎空间索引演示"); + + auto &app = Application::instance(); + screenWidth_ = static_cast(app.getConfig().width); + screenHeight_ = static_cast(app.getConfig().height); + + // 设置背景色 + setBackgroundColor(Color(0.05f, 0.05f, 0.1f, 1.0f)); + + // 创建100个碰撞节点 + createNodes(100); + + // 加载字体 + loadFonts(); + + E2D_LOG_INFO("创建了 {} 个碰撞节点", nodes_.size()); + E2D_LOG_INFO("空间索引已启用: {}", isSpatialIndexingEnabled()); + } + + void onUpdate(float dt) override { + Scene::onUpdate(dt); + + auto startTime = std::chrono::high_resolution_clock::now(); + + // 更新所有节点位置 + for (auto &node : nodes_) { + node->update(dt, screenWidth_, screenHeight_); + } + + auto updateEndTime = std::chrono::high_resolution_clock::now(); + stats_.updateTime = + std::chrono::duration(updateEndTime - startTime) + .count(); + + // 使用引擎自带的空间索引进行碰撞检测 + performCollisionDetection(); + + auto collisionEndTime = std::chrono::high_resolution_clock::now(); + stats_.collisionTime = std::chrono::duration( + collisionEndTime - updateEndTime) + .count(); + + stats_.nodeCount = nodes_.size(); + + // 获取当前使用的空间索引策略 + stats_.strategyName = getSpatialManager().getStrategyName(); + + // 检查退出按键 + auto &input = Application::instance().input(); + if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_START)) { + E2D_LOG_INFO("退出应用"); + Application::instance().quit(); + } + + // 按A键添加节点 + if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_A)) { + addNodes(100); + } + + // 按B键减少节点 + if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_B)) { + removeNodes(100); + } + + // 按X键切换空间索引策略 + if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_X)) { + toggleSpatialStrategy(); + } + } + + void onRender(RenderBackend &renderer) override { + Scene::onRender(renderer); + + auto renderStart = std::chrono::high_resolution_clock::now(); + + // 节点渲染由Scene自动处理 + + auto renderEnd = std::chrono::high_resolution_clock::now(); + stats_.renderTime = + std::chrono::duration(renderEnd - renderStart) + .count(); + + // 绘制UI + drawUI(renderer); + } + +private: + /** + * @brief 加载字体资源 + */ + void loadFonts() { + auto &resources = Application::instance().resources(); + + std::vector fontPaths = { + "romfs:/assets/msjh.ttf", + "romfs:/assets/default.ttf", + "romfs:/assets/font.ttf", + }; + + titleFont_ = resources.loadFontWithFallbacks(fontPaths, 28, true); + infoFont_ = resources.loadFontWithFallbacks(fontPaths, 16, true); + } + + /** + * @brief 创建指定数量的节点 + */ + void createNodes(size_t count) { + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_real_distribution posX(50.0f, screenWidth_ - 50.0f); + std::uniform_real_distribution posY(50.0f, screenHeight_ - 50.0f); + std::uniform_real_distribution colorR(0.2f, 0.9f); + std::uniform_real_distribution colorG(0.2f, 0.9f); + std::uniform_real_distribution 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(20.0f, color, static_cast(i)); + node->setPosition(Vec2(posX(gen), posY(gen))); + addChild(node); + nodes_.push_back(node); + } + } + + /** + * @brief 添加节点 + */ + void addNodes(size_t count) { + size_t currentCount = nodes_.size(); + if (currentCount + count > 5000) { + E2D_LOG_WARN("节点数量已达上限(5000)"); + return; + } + createNodes(count); + E2D_LOG_INFO("添加 {} 个节点,当前总数: {}", count, nodes_.size()); + } + + /** + * @brief 移除节点 + */ + void removeNodes(size_t count) { + if (count >= nodes_.size()) { + count = nodes_.size(); + } + if (count == 0) + return; + + for (size_t i = 0; i < count; ++i) { + removeChild(nodes_.back()); + nodes_.pop_back(); + } + E2D_LOG_INFO("移除 {} 个节点,当前总数: {}", count, nodes_.size()); + } + + /** + * @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 (auto &node : nodes_) { + node->setColliding(false); + } + + // 使用引擎自带的空间索引进行碰撞检测 + // 这是核心:Scene::queryCollisions() 会自动使用 SpatialManager + auto collisions = queryCollisions(); + + stats_.collisionCount = collisions.size(); + + // 标记碰撞的节点 + for (const auto &[nodeA, nodeB] : collisions) { + if (auto boxA = dynamic_cast(nodeA)) { + boxA->setColliding(true); + } + if (auto boxB = dynamic_cast(nodeB)) { + boxB->setColliding(true); + } + } + } + + /** + * @brief 绘制UI界面 + */ + void drawUI(RenderBackend &renderer) { + if (!titleFont_ || !infoFont_) + return; + + auto &app = Application::instance(); + + // 绘制标题 + renderer.drawText(*titleFont_, "引擎空间索引演示", Vec2(30.0f, 20.0f), + Color(1.0f, 1.0f, 1.0f, 1.0f)); + + // 绘制性能统计 + std::stringstream ss; + float x = 30.0f; + float y = 60.0f; + float lineHeight = 22.0f; + + ss << "节点数量: " << stats_.nodeCount; + renderer.drawText(*infoFont_, ss.str(), Vec2(x, y), + Color(0.9f, 0.9f, 0.9f, 1.0f)); + y += lineHeight; + + ss.str(""); + ss << "索引策略: " << stats_.strategyName; + renderer.drawText(*infoFont_, ss.str(), Vec2(x, y), + Color(0.5f, 1.0f, 0.5f, 1.0f)); + y += lineHeight; + + ss.str(""); + ss << "碰撞对数: " << stats_.collisionCount; + renderer.drawText(*infoFont_, ss.str(), Vec2(x, y), + Color(1.0f, 0.5f, 0.5f, 1.0f)); + y += lineHeight; + + ss.str(""); + ss << std::fixed << std::setprecision(2); + ss << "更新时间: " << stats_.updateTime << " ms"; + renderer.drawText(*infoFont_, ss.str(), Vec2(x, y), + Color(0.8f, 0.8f, 0.8f, 1.0f)); + y += lineHeight; + + ss.str(""); + ss << "碰撞检测: " << stats_.collisionTime << " ms"; + renderer.drawText(*infoFont_, ss.str(), Vec2(x, y), + Color(0.8f, 0.8f, 0.8f, 1.0f)); + y += lineHeight; + + ss.str(""); + ss << "渲染时间: " << stats_.renderTime << " ms"; + renderer.drawText(*infoFont_, ss.str(), Vec2(x, y), + Color(0.8f, 0.8f, 0.8f, 1.0f)); + y += lineHeight; + + ss.str(""); + ss << "FPS: " << app.fps(); + renderer.drawText(*infoFont_, ss.str(), Vec2(x, y), + Color(0.5f, 1.0f, 0.5f, 1.0f)); + y += lineHeight * 1.5f; + + // 绘制操作说明 + renderer.drawText(*infoFont_, "操作说明:", Vec2(x, y), + Color(1.0f, 1.0f, 0.5f, 1.0f)); + y += lineHeight; + renderer.drawText(*infoFont_, "A键 - 添加100个节点", Vec2(x + 10, y), + Color(0.8f, 0.8f, 0.8f, 1.0f)); + y += lineHeight; + renderer.drawText(*infoFont_, "B键 - 移除100个节点", Vec2(x + 10, y), + Color(0.8f, 0.8f, 0.8f, 1.0f)); + y += lineHeight; + renderer.drawText(*infoFont_, "X键 - 切换索引策略", Vec2(x + 10, y), + Color(0.8f, 0.8f, 0.8f, 1.0f)); + y += lineHeight; + renderer.drawText(*infoFont_, "+键 - 退出程序", Vec2(x + 10, y), + Color(0.8f, 0.8f, 0.8f, 1.0f)); + + // 绘制图例 + float legendX = screenWidth_ - 200.0f; + float legendY = 20.0f; + renderer.drawText(*infoFont_, "图例:", Vec2(legendX, legendY), + Color(1.0f, 1.0f, 1.0f, 1.0f)); + legendY += 25.0f; + + renderer.fillRect(Rect(legendX, legendY, 15.0f, 15.0f), + Color(0.5f, 0.5f, 0.9f, 0.7f)); + renderer.drawText(*infoFont_, "- 正常", Vec2(legendX + 20.0f, legendY), + Color(0.8f, 0.8f, 0.8f, 1.0f)); + legendY += 25.0f; + + renderer.fillRect(Rect(legendX, legendY, 15.0f, 15.0f), + Color(1.0f, 0.2f, 0.2f, 0.9f)); + renderer.drawText(*infoFont_, "- 碰撞中", Vec2(legendX + 20.0f, legendY), + Color(0.8f, 0.8f, 0.8f, 1.0f)); + } + + std::vector> nodes_; + PerformanceStats stats_; + float screenWidth_ = 1280.0f; + float screenHeight_ = 720.0f; + + Ptr titleFont_; + Ptr infoFont_; +}; + +// ============================================================================ +// 程序入口 +// ============================================================================ + +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()); + + E2D_LOG_INFO("开始主循环..."); + + app.run(); + + E2D_LOG_INFO("应用结束"); + + return 0; +} diff --git a/examples/spatial_index_demo/romfs/assets/font.ttf b/examples/spatial_index_demo/romfs/assets/font.ttf new file mode 100644 index 0000000..8997148 Binary files /dev/null and b/examples/spatial_index_demo/romfs/assets/font.ttf differ diff --git a/examples/spatial_index_demo/xmake.lua b/examples/spatial_index_demo/xmake.lua new file mode 100644 index 0000000..d6d796c --- /dev/null +++ b/examples/spatial_index_demo/xmake.lua @@ -0,0 +1,61 @@ +-- ============================================== +-- Spatial Index Demo 示例 - Xmake 构建脚本 +-- 支持平台: MinGW (Windows), Nintendo Switch +-- ============================================== + +-- 获取当前平台 +local host_plat = os.host() +local target_plat = get_config("plat") or host_plat + +-- 可执行文件目标 +target("spatial_index_demo") + set_kind("binary") + add_files("main.cpp") + add_includedirs("../../Extra2D/include") + add_deps("extra2d") + + if target_plat == "switch" then + set_plat("switch") + set_arch("arm64") + set_toolchains("switch") + set_targetdir("build/switch") + + 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.absolute("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 + end + end) + + elseif target_plat == "mingw" then + set_plat("mingw") + set_arch("x86_64") + set_targetdir("build/mingw") + add_ldflags("-mwindows", {force = true}) + + after_build(function (target) + local romfs = path.absolute("romfs") + if os.isdir(romfs) then + local target_dir = path.directory(target:targetfile()) + local assets_dir = path.join(target_dir, "assets") + if not os.isdir(assets_dir) then + os.mkdir(assets_dir) + end + os.cp(path.join(romfs, "assets/*"), assets_dir) + end + end) + end +target_end() diff --git a/xmake.lua b/xmake.lua new file mode 100644 index 0000000..2c6c820 --- /dev/null +++ b/xmake.lua @@ -0,0 +1,105 @@ +-- ============================================== +-- Extra2D - 2D Game Engine +-- Build System: Xmake +-- Platforms: MinGW (Windows), Nintendo Switch +-- ============================================== + +-- 项目元信息 +set_project("Extra2D") +set_version("3.1.0") +set_license("MIT") + +-- 语言和编码设置 +set_languages("c++17") +set_encodings("utf-8") + +-- 构建模式 +add_rules("mode.debug", "mode.release") + +-- ============================================== +-- 构建选项 +-- ============================================== + +option("examples") + set_default(true) + set_showmenu(true) + set_description("Build example programs") +option_end() + +option("debug_logs") + set_default(false) + set_showmenu(true) + set_description("Enable debug logging") +option_end() + +-- ============================================== +-- 平台检测与配置 +-- ============================================== + +local host_plat = os.host() +local target_plat = get_config("plat") or host_plat +local supported_plats = {mingw = true, switch = true} + +if not supported_plats[target_plat] then + if host_plat == "windows" then + target_plat = "mingw" + else + error("Unsupported platform: " .. target_plat .. ". Supported platforms: mingw, switch") + end +end + +set_plat(target_plat) + +if target_plat == "switch" then + set_arch("arm64") +elseif target_plat == "mingw" then + set_arch("x86_64") +end + +-- ============================================== +-- 加载工具链配置 +-- ============================================== + +if target_plat == "switch" then + includes("xmake/toolchains/switch.lua") + set_toolchains("switch") +end + +-- ============================================== +-- 添加依赖包 (MinGW) +-- ============================================== + +if target_plat == "mingw" then + add_requires("glm", "libsdl2", "libsdl2_mixer") +end + +-- ============================================== +-- 加载构建目标 +-- ============================================== + +-- 加载引擎库定义 +includes("xmake/engine.lua") + +-- 定义引擎库 +define_extra2d_engine() + +-- 示例程序目标(作为子项目) +if has_config("examples") 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"}) +end + +-- ============================================== +-- 项目信息输出 +-- ============================================== + +print("========================================") +print("Extra2D Build Configuration") +print("========================================") +print("Platform: " .. target_plat) +print("Architecture: " .. (get_config("arch") or "auto")) +print("Mode: " .. (is_mode("debug") and "debug" or "release")) +print("Examples: " .. (has_config("examples") and "enabled" or "disabled")) +print("========================================") diff --git a/xmake/engine.lua b/xmake/engine.lua new file mode 100644 index 0000000..5757079 --- /dev/null +++ b/xmake/engine.lua @@ -0,0 +1,54 @@ +-- ============================================== +-- Extra2D 引擎库共享配置 +-- 被主项目和示例共享使用 +-- ============================================== + +-- 获取当前平台 +local function get_current_plat() + return get_config("plat") or os.host() +end + +-- 定义 Extra2D 引擎库目标 +function define_extra2d_engine() + target("extra2d") + set_kind("static") + + -- 引擎源文件 + add_files("Extra2D/src/**.cpp") + add_files("Extra2D/src/glad/glad.c") + add_files("squirrel/squirrel/*.cpp") + add_files("squirrel/sqstdlib/*.cpp") + + -- 头文件路径 + add_includedirs("Extra2D/include", {public = true}) + add_includedirs("squirrel/include", {public = true}) + add_includedirs("Extra2D/include/extra2d/platform", {public = true}) + + -- 平台配置 + local plat = get_current_plat() + if plat == "switch" then + local devkitPro = os.getenv("DEVKITPRO") or "C:/devkitPro" + add_includedirs(devkitPro .. "/portlibs/switch/include", {public = true}) + add_linkdirs(devkitPro .. "/portlibs/switch/lib") + add_syslinks("SDL2_mixer", "SDL2", "opusfile", "opus", "vorbisidec", "ogg", + "modplug", "mpg123", "FLAC", "GLESv2", "EGL", "glapi", "drm_nouveau", + {public = true}) + elseif plat == "mingw" then + add_packages("glm", "libsdl2", "libsdl2_mixer", {public = true}) + add_syslinks("opengl32", "glu32", "winmm", "imm32", "version", "setupapi", {public = true}) + end + + -- 编译器标志 + add_cxflags("-Wall", "-Wextra", {force = true}) + add_cxflags("-Wno-unused-variable", "-Wno-unused-function", "-Wno-unused-parameter", {force = true}) + add_cxflags("-Wno-deprecated-copy", "-Wno-strict-aliasing", "-Wno-implicit-fallthrough", "-Wno-class-memaccess", {force = true}) + + if is_mode("debug") then + add_defines("E2D_DEBUG", "_DEBUG", {public = true}) + add_cxxflags("-O0", "-g", {force = true}) + else + add_defines("NDEBUG", {public = true}) + add_cxxflags("-O2", {force = true}) + end + target_end() +end diff --git a/xmake/toolchains/switch.lua b/xmake/toolchains/switch.lua new file mode 100644 index 0000000..6371d7b --- /dev/null +++ b/xmake/toolchains/switch.lua @@ -0,0 +1,47 @@ +-- ============================================== +-- Nintendo Switch 工具链定义 +-- ============================================== + +function define_switch_toolchain() + toolchain("switch") + set_kind("standalone") + set_description("Nintendo Switch devkitA64 toolchain") + + -- 检查 DEVKITPRO 环境变量(Windows 上使用 C:/devkitPro) + local devkitPro = os.getenv("DEVKITPRO") or "C:/devkitPro" + local devkitA64 = path.join(devkitPro, "devkitA64") + + -- 设置工具链路径 + set_toolset("cc", path.join(devkitA64, "bin/aarch64-none-elf-gcc.exe")) + set_toolset("cxx", path.join(devkitA64, "bin/aarch64-none-elf-g++.exe")) + set_toolset("ld", path.join(devkitA64, "bin/aarch64-none-elf-g++.exe")) + set_toolset("ar", path.join(devkitA64, "bin/aarch64-none-elf-gcc-ar.exe")) + set_toolset("strip", path.join(devkitA64, "bin/aarch64-none-elf-strip.exe")) + + -- 架构标志 + local arch_flags = "-march=armv8-a+crc+crypto -mtune=cortex-a57 -mtp=soft -fPIE" + add_cxflags(arch_flags) + -- 使用 devkitPro 提供的 switch.specs 文件 + add_ldflags("-specs=" .. path.join(devkitPro, "libnx/switch.specs"), "-g", arch_flags) + + -- 定义 Switch 平台宏 + add_defines("__SWITCH__", "__NX__", "MA_SWITCH", "PFD_SWITCH") + + -- SimpleIni 配置:不使用 Windows API + add_defines("SI_NO_CONVERSION") + + -- libnx 路径 - 必须在工具链级别添加 + add_includedirs(path.join(devkitPro, "libnx/include")) + add_linkdirs(path.join(devkitPro, "libnx/lib")) + + -- portlibs 路径(EGL + 桌面 OpenGL + SDL2) + add_includedirs(path.join(devkitPro, "portlibs/switch/include")) + add_includedirs(path.join(devkitPro, "portlibs/switch/include/SDL2")) + add_linkdirs(path.join(devkitPro, "portlibs/switch/lib")) + + add_syslinks("nx", "m") + toolchain_end() +end + +-- 定义工具链 +define_switch_toolchain()