feat: 添加推箱子游戏示例及相关资源文件

新增推箱子游戏示例,包含完整的游戏逻辑、场景管理、音效控制和存档系统。主要功能包括:
- 实现游戏主菜单、关卡选择、游戏场景和通关场景
- 添加音效控制和存档功能
- 支持手柄操作和键盘输入
- 包含8个不同难度的关卡设计
- 添加游戏所需的所有资源文件(图片、音效、字体等)

同时更新了构建脚本,支持在MinGW和Nintendo Switch平台编译运行。
This commit is contained in:
ChestnutYueyue 2026-02-10 16:15:11 +08:00
parent 4066abf40f
commit 120b272abf
51 changed files with 2570 additions and 0 deletions

View File

@ -0,0 +1,281 @@
#include <cmath>
#include <extra2d/extra2d.h>
#include <sstream>
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<CollisionBox>(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<std::string> 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<std::pair<Vec2, Color>> 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<CollisionBox>(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<CollisionBox *>(nodeA)) {
boxA->setColliding(true);
}
if (auto boxB = dynamic_cast<CollisionBox *>(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<float>(app.getConfig().height);
renderer.drawText(*infoFont_, "按 + 键退出",
Vec2(50.0f, screenHeight - 50.0f),
Color(0.8f, 0.8f, 0.8f, 1.0f));
}
Ptr<CollisionBox> centerBox_;
std::vector<Ptr<CollisionBox>> boxes_;
float rotationAngle_ = 0.0f;
float rotationSpeed_ = 60.0f; // 旋转速度(度/秒)
size_t collisionCount_ = 0;
// 字体资源
Ptr<FontAtlas> titleFont_;
Ptr<FontAtlas> 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<CollisionDemoScene>());
E2D_LOG_INFO("开始主循环...");
// 运行应用
app.run();
E2D_LOG_INFO("应用结束");
return 0;
}

Binary file not shown.

View File

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

View File

@ -0,0 +1,119 @@
#include <extra2d/extra2d.h>
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<FontAtlas> 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<HelloWorldScene>());
E2D_LOG_INFO("开始主循环...");
// 运行应用
app.run();
E2D_LOG_INFO("应用结束");
return 0;
}

Binary file not shown.

View File

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

View File

@ -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 <extra2d/extra2d.h>
namespace pushbox {
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) {
setBackgroundColor(extra2d::Colors::Black);
auto& app = extra2d::Application::instance();
auto& config = app.getConfig();
setViewportSize(static_cast<float>(config.width), static_cast<float>(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<float>(app.getConfig().width);
float screenH = static_cast<float>(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<extra2d::Node>();
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<StartScene>(), 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<float>(app.getConfig().width);
float screenH = static_cast<float>(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<float>((12 - map_.width) / 2) * tileW;
float mapOffsetY = static_cast<float>((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<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);
}
}
}
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<SuccessScene>(),
extra2d::TransitionType::Fade, 0.25f);
return;
}
setLevel(g_CurrentLevel + 1);
}
} // namespace pushbox

View File

@ -0,0 +1,51 @@
#pragma once
#include "data.h"
#include <extra2d/extra2d.h>
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<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::Node> mapLayer_;
extra2d::Ptr<extra2d::Sprite> soundIcon_;
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];
};
} // namespace pushbox

View File

@ -0,0 +1,217 @@
#include "StartScene.h"
#include "audio_context.h"
#include "audio_controller.h"
#include "data.h"
#include "PlayScene.h"
#include <extra2d/extra2d.h>
namespace pushbox {
StartScene::StartScene() {
auto& app = extra2d::Application::instance();
auto& config = app.getConfig();
setViewportSize(static_cast<float>(config.width), static_cast<float>(config.height));
}
static extra2d::Ptr<extra2d::FontAtlas> 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<float>(app.getConfig().width);
float screenH = static_cast<float>(app.getConfig().height);
auto bgTex = resources.loadTexture("assets/images/start.jpg");
if (bgTex) {
auto background = extra2d::Sprite::create(bgTex);
float bgWidth = static_cast<float>(bgTex->getWidth());
float bgHeight = static_cast<float>(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<PlayScene>(1), extra2d::TransitionType::Fade, 0.25f);
}
void StartScene::continueGame() {
extra2d::Application::instance().scenes().replaceScene(
extra2d::makePtr<PlayScene>(g_CurrentLevel), extra2d::TransitionType::Fade, 0.25f);
}
void StartScene::exitGame() {
extra2d::Application::instance().quit();
}
} // namespace pushbox

View File

@ -0,0 +1,30 @@
#pragma once
#include <extra2d/extra2d.h>
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<extra2d::FontAtlas> font_;
extra2d::Ptr<extra2d::Button> startBtn_;
extra2d::Ptr<extra2d::Button> resumeBtn_;
extra2d::Ptr<extra2d::Button> exitBtn_;
extra2d::Ptr<extra2d::Sprite> soundIcon_;
int selectedIndex_ = 0;
int menuCount_ = 3;
};
} // namespace pushbox

View File

@ -0,0 +1,77 @@
#include "SuccessScene.h"
#include <extra2d/extra2d.h>
namespace pushbox {
SuccessScene::SuccessScene() {
auto& app = extra2d::Application::instance();
auto& config = app.getConfig();
setViewportSize(static_cast<float>(config.width), static_cast<float>(config.height));
}
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() {
Scene::onEnter();
auto& app = extra2d::Application::instance();
auto& resources = app.resources();
setBackgroundColor(extra2d::Colors::Black);
if (getChildren().empty()) {
// 获取窗口尺寸
float screenW = static_cast<float>(app.getConfig().width);
float screenH = static_cast<float>(app.getConfig().height);
auto bgTex = resources.loadTexture("assets/images/success.jpg");
if (bgTex) {
auto background = extra2d::Sprite::create(bgTex);
float bgWidth = static_cast<float>(bgTex->getWidth());
float bgHeight = static_cast<float>(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

View File

@ -0,0 +1,17 @@
#pragma once
#include <extra2d/extra2d.h>
namespace pushbox {
class SuccessScene : public extra2d::Scene {
public:
SuccessScene();
void onEnter() override;
void onUpdate(float dt) override;
private:
extra2d::Ptr<extra2d::Text> selectorText_;
};
} // namespace pushbox

View File

@ -0,0 +1,17 @@
#include "audio_context.h"
#include "audio_controller.h"
namespace pushbox {
static extra2d::WeakPtr<AudioController> g_audioController;
void setAudioController(const extra2d::Ptr<AudioController>& controller) {
g_audioController = controller;
}
extra2d::Ptr<AudioController> getAudioController() {
return g_audioController.lock();
}
} // namespace pushbox

View File

@ -0,0 +1,12 @@
#pragma once
#include <extra2d/extra2d.h>
namespace pushbox {
class AudioController;
void setAudioController(const extra2d::Ptr<AudioController>& controller);
extra2d::Ptr<AudioController> getAudioController();
} // namespace pushbox

View File

@ -0,0 +1,62 @@
#include "audio_controller.h"
#include "storage.h"
namespace pushbox {
extra2d::Ptr<AudioController> AudioController::create() {
return extra2d::makePtr<AudioController>();
}
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

View File

@ -0,0 +1,29 @@
#pragma once
#include "data.h"
#include <extra2d/extra2d.h>
namespace pushbox {
class AudioController : public extra2d::Node {
public:
static extra2d::Ptr<AudioController> 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<extra2d::Sound> background_;
extra2d::Ptr<extra2d::Sound> manMove_;
extra2d::Ptr<extra2d::Sound> boxMove_;
};
} // namespace pushbox

117
examples/push_box/data.cpp Normal file
View File

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

28
examples/push_box/data.h Normal file
View File

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

View File

@ -0,0 +1,52 @@
#include <extra2d/extra2d.h>
#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<pushbox::StartScene>());
E2D_LOG_INFO("开始主循环...");
app.run();
E2D_LOG_INFO("应用结束");
return 0;
}

View File

@ -0,0 +1,40 @@
#include "menu_button.h"
#include <extra2d/extra2d.h>
namespace pushbox {
extra2d::Ptr<MenuButton> MenuButton::create(extra2d::Ptr<extra2d::FontAtlas> font,
const extra2d::String& text,
extra2d::Function<void()> onClick) {
auto btn = extra2d::makePtr<MenuButton>();
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<MenuButton>(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

View File

@ -0,0 +1,21 @@
#pragma once
#include <extra2d/extra2d.h>
namespace pushbox {
class MenuButton : public extra2d::Button {
public:
static extra2d::Ptr<MenuButton> create(extra2d::Ptr<extra2d::FontAtlas> font,
const extra2d::String& text,
extra2d::Function<void()> onClick);
void setEnabled(bool enabled);
bool isEnabled() const { return enabled_; }
private:
bool enabled_ = true;
extra2d::Function<void()> onClick_;
};
} // namespace pushbox

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 477 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 658 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 682 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 641 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 642 B

View File

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

View File

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

View File

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

View File

@ -0,0 +1,436 @@
#include <cmath>
#include <extra2d/extra2d.h>
#include <iomanip>
#include <random>
#include <sstream>
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_; }
// 必须实现 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<float>(app.getConfig().width);
screenHeight_ = static_cast<float>(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<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 = 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<double, std::milli>(renderEnd - renderStart)
.count();
// 绘制UI
drawUI(renderer);
}
private:
/**
* @brief
*/
void loadFonts() {
auto &resources = Application::instance().resources();
std::vector<std::string> 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<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);
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<PhysicsNode *>(nodeA)) {
boxA->setColliding(true);
}
if (auto boxB = dynamic_cast<PhysicsNode *>(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<Ptr<PhysicsNode>> nodes_;
PerformanceStats stats_;
float screenWidth_ = 1280.0f;
float screenHeight_ = 720.0f;
Ptr<FontAtlas> titleFont_;
Ptr<FontAtlas> 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<SpatialIndexDemoScene>());
E2D_LOG_INFO("开始主循环...");
app.run();
E2D_LOG_INFO("应用结束");
return 0;
}

Binary file not shown.

View File

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

105
xmake.lua Normal file
View File

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

54
xmake/engine.lua Normal file
View File

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

View File

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