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