feat: 添加推箱子游戏示例及相关资源文件
新增推箱子游戏示例,包含完整的游戏逻辑、场景管理、音效控制和存档系统。主要功能包括: - 实现游戏主菜单、关卡选择、游戏场景和通关场景 - 添加音效控制和存档功能 - 支持手柄操作和键盘输入 - 包含8个不同难度的关卡设计 - 添加游戏所需的所有资源文件(图片、音效、字体等) 同时更新了构建脚本,支持在MinGW和Nintendo Switch平台编译运行。
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 477 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 658 B |
|
After Width: | Height: | Size: 682 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 641 B |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 191 KiB |
|
After Width: | Height: | Size: 124 KiB |
|
After Width: | Height: | Size: 642 B |
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -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("========================================")
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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()
|
||||||