feat: 重构资源管理和日志系统,添加多平台支持
refactor: 替换GLES为glad库以提升跨平台兼容性 style: 更新logo和品牌资源文件 docs: 添加新字体加载API文档 chore: 移除旧的推箱子示例代码 test: 更新测试用例以适配新API
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(xargs:*)",
|
||||
"Bash(wc -l \"C:\\\\Users\\\\soulcoco\\\\Desktop\\\\Easy2D\\\\Easy2D-dev\\\\Easy2D\\\\src\"/**/*.cpp)",
|
||||
"Bash(wc -l:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,283 @@
|
|||
#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_;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 程序入口
|
||||
// ============================================================================
|
||||
|
||||
extern "C" int main(int argc, char *argv[]) {
|
||||
(void)argc;
|
||||
(void)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,174 @@
|
|||
#include <extra2d/extra2d.h>
|
||||
#include <switch.h>
|
||||
|
||||
using namespace extra2d;
|
||||
|
||||
// ============================================================================
|
||||
// 字体配置
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief 获取字体候选列表
|
||||
* @return 按优先级排序的字体路径列表
|
||||
*/
|
||||
static std::vector<std::string> getFontCandidates() {
|
||||
return {
|
||||
"romfs:/assets/msjh.ttf", // 微软雅黑(中文支持)
|
||||
"romfs:/assets/Gasinamu.ttf", // 备选字体
|
||||
"romfs:/assets/default.ttf", // 默认字体
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 加载字体,支持多种字体后备
|
||||
* @param resources 资源管理器引用
|
||||
* @param fontSize 字体大小
|
||||
* @param useSDF 是否使用SDF渲染
|
||||
* @return 成功加载的字体图集,失败返回nullptr
|
||||
*/
|
||||
static Ptr<FontAtlas> loadFontWithFallbacks(ResourceManager &resources,
|
||||
int fontSize, bool useSDF) {
|
||||
auto candidates = getFontCandidates();
|
||||
|
||||
for (const auto &fontPath : candidates) {
|
||||
auto font = resources.loadFont(fontPath, fontSize, useSDF);
|
||||
if (font) {
|
||||
E2D_LOG_INFO("成功加载字体: {}", fontPath);
|
||||
return font;
|
||||
}
|
||||
E2D_LOG_WARN("字体加载失败,尝试下一个: {}", fontPath);
|
||||
}
|
||||
|
||||
E2D_LOG_ERROR("所有字体候选都加载失败!");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 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_ = loadFontWithFallbacks(resources, 48, true);
|
||||
|
||||
if (!font_) {
|
||||
E2D_LOG_ERROR("字体加载失败,文字渲染将不可用!");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 每帧更新时调用
|
||||
* @param dt 时间间隔(秒)
|
||||
*/
|
||||
void onUpdate(float dt) override {
|
||||
Scene::onUpdate(dt);
|
||||
|
||||
// 检查退出按键(START 按钮)
|
||||
auto &input = Application::instance().input();
|
||||
if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_START)) {
|
||||
E2D_LOG_INFO("退出应用");
|
||||
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
|
||||
|
||||
// 绘制 "你好世界" 文字(白色,居中)
|
||||
Color white(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
renderer.drawText(*font_, "你好世界", Vec2(centerX - 100.0f, centerY),
|
||||
white);
|
||||
|
||||
// 绘制提示文字(黄色)
|
||||
Color yellow(1.0f, 1.0f, 0.0f, 1.0f);
|
||||
renderer.drawText(*font_, "退出按键(START 按钮)",
|
||||
Vec2(centerX - 80.0f, centerY + 50.0f), yellow);
|
||||
}
|
||||
|
||||
private:
|
||||
Ptr<FontAtlas> font_; // 字体图集
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 程序入口
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief 初始化应用配置
|
||||
* @return 应用配置结构体
|
||||
*/
|
||||
static AppConfig createAppConfig() {
|
||||
AppConfig config;
|
||||
config.title = "Easy2D - Hello World";
|
||||
config.width = 1280;
|
||||
config.height = 720;
|
||||
config.vsync = true;
|
||||
config.fpsLimit = 60;
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 程序入口
|
||||
*/
|
||||
extern "C" int main(int argc, char *argv[]) {
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
|
||||
// 初始化日志系统
|
||||
Logger::init();
|
||||
Logger::setLevel(LogLevel::Debug);
|
||||
|
||||
E2D_LOG_INFO("========================");
|
||||
E2D_LOG_INFO("Easy2D Hello World Demo");
|
||||
E2D_LOG_INFO("========================");
|
||||
|
||||
// 获取应用实例
|
||||
auto &app = Application::instance();
|
||||
|
||||
// 配置应用
|
||||
auto config = createAppConfig();
|
||||
|
||||
// 初始化应用
|
||||
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;
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
#include "audio_context.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
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <extra2d/extra2d.h>
|
||||
|
||||
namespace pushbox {
|
||||
|
||||
class AudioController;
|
||||
|
||||
void setAudioController(const extra2d::Ptr<AudioController> &controller);
|
||||
extra2d::Ptr<AudioController> getAudioController();
|
||||
|
||||
} // namespace pushbox
|
||||
|
|
@ -1,118 +0,0 @@
|
|||
#include "data.h"
|
||||
|
||||
namespace pushbox {
|
||||
|
||||
int g_CurrentLevel = 1;
|
||||
bool g_SoundOpen = true;
|
||||
int g_Direct = 2;
|
||||
bool g_Pushing = false;
|
||||
|
||||
Map g_Maps[MAX_LEVEL] = {
|
||||
{
|
||||
8, 8, 4, 4,
|
||||
{
|
||||
{{Empty}, {Empty}, {Wall}, {Wall}, {Wall}, {Empty}, {Empty}, {Empty}},
|
||||
{{Empty}, {Empty}, {Wall}, {Ground, true}, {Wall}, {Empty}, {Empty}, {Empty}},
|
||||
{{Empty}, {Empty}, {Wall}, {Ground}, {Wall}, {Wall}, {Wall}, {Wall}},
|
||||
{{Wall}, {Wall}, {Wall}, {Box}, {Ground}, {Box}, {Ground, true}, {Wall}},
|
||||
{{Wall}, {Ground, true}, {Ground}, {Box}, {Man}, {Wall}, {Wall}, {Wall}},
|
||||
{{Wall}, {Wall}, {Wall}, {Wall}, {Box}, {Wall}, {Empty}, {Empty}},
|
||||
{{Empty}, {Empty}, {Empty}, {Wall}, {Ground, true}, {Wall}, {Empty}, {Empty}},
|
||||
{{Empty}, {Empty}, {Empty}, {Wall}, {Wall}, {Wall}, {Empty}, {Empty}},
|
||||
},
|
||||
},
|
||||
{
|
||||
9, 9, 1, 1,
|
||||
{
|
||||
{{Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Empty}, {Empty}, {Empty}, {Empty}},
|
||||
{{Wall}, {Man}, {Ground}, {Ground}, {Wall}, {Empty}, {Empty}, {Empty}, {Empty}},
|
||||
{{Wall}, {Ground}, {Box}, {Box}, {Wall}, {Empty}, {Wall}, {Wall}, {Wall}},
|
||||
{{Wall}, {Ground}, {Box}, {Ground}, {Wall}, {Empty}, {Wall}, {Ground, true}, {Wall}},
|
||||
{{Wall}, {Wall}, {Wall}, {Ground}, {Wall}, {Wall}, {Wall}, {Ground, true}, {Wall}},
|
||||
{{Empty}, {Wall}, {Wall}, {Ground}, {Ground}, {Ground}, {Ground}, {Ground, true}, {Wall}},
|
||||
{{Empty}, {Wall}, {Ground}, {Ground}, {Ground}, {Wall}, {Ground}, {Ground}, {Wall}},
|
||||
{{Empty}, {Wall}, {Ground}, {Ground}, {Ground}, {Wall}, {Wall}, {Wall}, {Wall}},
|
||||
{{Empty}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Empty}, {Empty}, {Empty}},
|
||||
},
|
||||
},
|
||||
{
|
||||
10, 7, 3, 3,
|
||||
{
|
||||
{{Empty}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Empty}, {Empty}},
|
||||
{{Empty}, {Wall}, {Ground}, {Ground}, {Ground}, {Ground}, {Ground}, {Wall}, {Wall}, {Wall}},
|
||||
{{Wall}, {Wall}, {Box}, {Wall}, {Wall}, {Wall}, {Ground}, {Ground}, {Ground}, {Wall}},
|
||||
{{Wall}, {Ground}, {Ground}, {Man}, {Box}, {Ground}, {Ground}, {Box}, {Ground}, {Wall}},
|
||||
{{Wall}, {Ground}, {Ground, true}, {Ground, true}, {Wall}, {Ground}, {Box}, {Ground}, {Wall}, {Wall}},
|
||||
{{Wall}, {Wall}, {Ground, true}, {Ground, true}, {Wall}, {Ground}, {Ground}, {Ground}, {Wall}, {Empty}},
|
||||
{{Empty}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Empty}},
|
||||
},
|
||||
},
|
||||
{
|
||||
6, 8, 1, 2,
|
||||
{
|
||||
{{Empty}, {Wall}, {Wall}, {Wall}, {Wall}, {Empty}},
|
||||
{{Wall}, {Wall}, {Ground}, {Ground}, {Wall}, {Empty}},
|
||||
{{Wall}, {Man}, {Box}, {Ground}, {Wall}, {Empty}},
|
||||
{{Wall}, {Wall}, {Box}, {Ground}, {Wall}, {Wall}},
|
||||
{{Wall}, {Wall}, {Ground}, {Box}, {Ground}, {Wall}},
|
||||
{{Wall}, {Ground, true}, {Box}, {Ground}, {Ground}, {Wall}},
|
||||
{{Wall}, {Ground, true}, {Ground, true}, {Box, true}, {Ground, true}, {Wall}},
|
||||
{{Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}},
|
||||
},
|
||||
},
|
||||
{
|
||||
8, 8, 2, 2,
|
||||
{
|
||||
{{Empty}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Empty}, {Empty}},
|
||||
{{Empty}, {Wall}, {Ground}, {Ground}, {Wall}, {Wall}, {Wall}, {Empty}},
|
||||
{{Empty}, {Wall}, {Man}, {Box}, {Ground}, {Ground}, {Wall}, {Empty}},
|
||||
{{Wall}, {Wall}, {Wall}, {Ground}, {Wall}, {Ground}, {Wall}, {Wall}},
|
||||
{{Wall}, {Ground, true}, {Wall}, {Ground}, {Wall}, {Ground}, {Ground}, {Wall}},
|
||||
{{Wall}, {Ground, true}, {Box}, {Ground}, {Ground}, {Wall}, {Ground}, {Wall}},
|
||||
{{Wall}, {Ground, true}, {Ground}, {Ground}, {Ground}, {Box}, {Ground}, {Wall}},
|
||||
{{Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}},
|
||||
},
|
||||
},
|
||||
{
|
||||
10, 8, 8, 1,
|
||||
{
|
||||
{{Empty}, {Empty}, {Empty}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}},
|
||||
{{Empty}, {Empty}, {Wall}, {Wall}, {Ground}, {Ground}, {Wall}, {Ground}, {Man}, {Wall}},
|
||||
{{Empty}, {Empty}, {Wall}, {Ground}, {Ground}, {Ground}, {Wall}, {Ground}, {Ground}, {Wall}},
|
||||
{{Empty}, {Empty}, {Wall}, {Box}, {Ground}, {Box}, {Ground}, {Box}, {Ground}, {Wall}},
|
||||
{{Empty}, {Empty}, {Wall}, {Ground}, {Box}, {Wall}, {Wall}, {Ground}, {Ground}, {Wall}},
|
||||
{{Wall}, {Wall}, {Wall}, {Ground}, {Box}, {Ground}, {Wall}, {Ground}, {Wall}, {Wall}},
|
||||
{{Wall}, {Ground, true}, {Ground, true}, {Ground, true}, {Ground, true}, {Ground, true}, {Ground}, {Ground}, {Wall}, {Empty}},
|
||||
{{Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Empty}},
|
||||
},
|
||||
},
|
||||
{
|
||||
10, 7, 8, 3,
|
||||
{
|
||||
{{Empty}, {Empty}, {Empty}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Empty}},
|
||||
{{Empty}, {Wall}, {Wall}, {Wall}, {Ground}, {Ground}, {Ground}, {Ground}, {Wall}, {Empty}},
|
||||
{{Wall}, {Wall}, {Ground, true}, {Ground}, {Box}, {Wall}, {Wall}, {Ground}, {Wall}, {Wall}},
|
||||
{{Wall}, {Ground, true}, {Ground, true}, {Box}, {Ground}, {Box}, {Ground}, {Ground}, {Man}, {Wall}},
|
||||
{{Wall}, {Ground, true}, {Ground, true}, {Ground}, {Box}, {Ground}, {Box}, {Ground}, {Wall}, {Wall}},
|
||||
{{Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Ground}, {Ground}, {Wall}, {Empty}},
|
||||
{{Empty}, {Empty}, {Empty}, {Empty}, {Empty}, {Wall}, {Wall}, {Wall}, {Wall}, {Empty}},
|
||||
},
|
||||
},
|
||||
{
|
||||
11, 9, 8, 7,
|
||||
{
|
||||
{{Empty}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Empty}},
|
||||
{{Empty}, {Wall}, {Ground}, {Ground}, {Wall}, {Wall}, {Ground}, {Ground}, {Ground}, {Wall}, {Empty}},
|
||||
{{Empty}, {Wall}, {Ground}, {Ground}, {Ground}, {Box}, {Ground}, {Ground}, {Ground}, {Wall}, {Empty}},
|
||||
{{Empty}, {Wall}, {Box}, {Ground}, {Wall}, {Wall}, {Wall}, {Ground}, {Box}, {Wall}, {Empty}},
|
||||
{{Empty}, {Wall}, {Ground}, {Wall}, {Ground, true}, {Ground, true}, {Ground, true}, {Wall}, {Ground}, {Wall}, {Empty}},
|
||||
{{Wall}, {Wall}, {Ground}, {Wall}, {Ground, true}, {Ground, true}, {Ground, true}, {Wall}, {Ground}, {Wall}, {Wall}},
|
||||
{{Wall}, {Ground}, {Box}, {Ground}, {Ground}, {Box}, {Ground}, {Ground}, {Box}, {Ground}, {Wall}},
|
||||
{{Wall}, {Ground}, {Ground}, {Ground}, {Ground}, {Ground}, {Wall}, {Ground}, {Man}, {Ground}, {Wall}},
|
||||
{{Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}, {Wall}},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
} // namespace pushbox
|
||||
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
#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
|
||||
|
||||
|
|
@ -1,149 +0,0 @@
|
|||
#include "storage.h"
|
||||
|
||||
#include <extra2d/utils/data.h>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
namespace pushbox {
|
||||
|
||||
static extra2d::DataStore g_store;
|
||||
static std::filesystem::path g_filePath;
|
||||
static bool g_loaded = false;
|
||||
|
||||
// 默认配置内容
|
||||
static const char *DEFAULT_CONFIG = R"([game]
|
||||
level = 1
|
||||
sound = true
|
||||
|
||||
[best]
|
||||
)";
|
||||
|
||||
/**
|
||||
* @brief 从 romfs 加载默认配置文件
|
||||
* @return 配置文件内容,如果失败则返回空字符串
|
||||
* @note Switch 平台上 romfs 路径格式为 romfs:/pushbox.ini
|
||||
*/
|
||||
static std::string loadDefaultConfigFromRomfs() {
|
||||
// 尝试多个可能的路径(按优先级排序)
|
||||
const char *paths[] = {
|
||||
"romfs:/pushbox.ini", // Switch romfs 正确路径格式
|
||||
"romfs/pushbox.ini", // 开发环境相对路径
|
||||
"pushbox.ini", // 当前目录
|
||||
};
|
||||
|
||||
for (const char *path : paths) {
|
||||
if (std::filesystem::exists(path)) {
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
if (file) {
|
||||
std::ostringstream buffer;
|
||||
buffer << file.rdbuf();
|
||||
return buffer.str();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果找不到文件,返回内置的默认配置
|
||||
return DEFAULT_CONFIG;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 将配置字符串写入临时文件并加载
|
||||
* @param content 配置内容
|
||||
* @return 临时文件路径
|
||||
*/
|
||||
static std::filesystem::path writeConfigToTempFile(const std::string &content) {
|
||||
auto tempPath =
|
||||
std::filesystem::temp_directory_path() / "pushbox_default.ini";
|
||||
std::ofstream file(tempPath, std::ios::binary);
|
||||
if (file) {
|
||||
file << content;
|
||||
file.close();
|
||||
}
|
||||
return tempPath;
|
||||
}
|
||||
|
||||
static void ensureLoaded() {
|
||||
if (g_loaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 首先尝试从可执行目录加载用户配置
|
||||
if (!g_filePath.empty() && std::filesystem::exists(g_filePath)) {
|
||||
g_store.load(g_filePath.string());
|
||||
} else {
|
||||
// 从 romfs 加载默认配置
|
||||
std::string defaultConfig = loadDefaultConfigFromRomfs();
|
||||
if (!defaultConfig.empty()) {
|
||||
auto tempPath = writeConfigToTempFile(defaultConfig);
|
||||
g_store.load(tempPath.string());
|
||||
}
|
||||
}
|
||||
g_loaded = true;
|
||||
}
|
||||
|
||||
void initStorage(const std::filesystem::path &baseDir) {
|
||||
g_filePath = baseDir / "pushbox.ini";
|
||||
|
||||
// 首先尝试从可执行目录加载用户配置
|
||||
if (std::filesystem::exists(g_filePath)) {
|
||||
g_store.load(g_filePath.string());
|
||||
} else {
|
||||
// 从 romfs 加载默认配置
|
||||
std::string defaultConfig = loadDefaultConfigFromRomfs();
|
||||
if (!defaultConfig.empty()) {
|
||||
auto tempPath = writeConfigToTempFile(defaultConfig);
|
||||
g_store.load(tempPath.string());
|
||||
}
|
||||
}
|
||||
g_loaded = true;
|
||||
}
|
||||
|
||||
int loadCurrentLevel(int defaultValue) {
|
||||
ensureLoaded();
|
||||
int level = g_store.getInt("game", "level", defaultValue);
|
||||
if (level < 1) {
|
||||
level = 1;
|
||||
}
|
||||
return level;
|
||||
}
|
||||
|
||||
void saveCurrentLevel(int level) {
|
||||
ensureLoaded();
|
||||
g_store.setInt("game", "level", level);
|
||||
if (!g_filePath.empty()) {
|
||||
g_store.save(g_filePath.string());
|
||||
}
|
||||
}
|
||||
|
||||
bool loadSoundOpen(bool defaultValue) {
|
||||
ensureLoaded();
|
||||
return g_store.getBool("game", "sound", defaultValue);
|
||||
}
|
||||
|
||||
void saveSoundOpen(bool open) {
|
||||
ensureLoaded();
|
||||
g_store.setBool("game", "sound", open);
|
||||
if (!g_filePath.empty()) {
|
||||
g_store.save(g_filePath.string());
|
||||
}
|
||||
}
|
||||
|
||||
int loadBestStep(int level, int defaultValue) {
|
||||
ensureLoaded();
|
||||
std::string key = "level" + std::to_string(level);
|
||||
return g_store.getInt("best", key, defaultValue);
|
||||
}
|
||||
|
||||
void saveBestStep(int level, int step) {
|
||||
ensureLoaded();
|
||||
std::string key = "level" + std::to_string(level);
|
||||
g_store.setInt("best", key, step);
|
||||
if (!g_filePath.empty()) {
|
||||
g_store.save(g_filePath.string());
|
||||
}
|
||||
}
|
||||
|
||||
std::filesystem::path storageFilePath() { return g_filePath; }
|
||||
|
||||
} // namespace pushbox
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
namespace pushbox {
|
||||
|
||||
void initStorage(const std::filesystem::path& baseDir);
|
||||
|
||||
int loadCurrentLevel(int defaultValue = 1);
|
||||
void saveCurrentLevel(int level);
|
||||
|
||||
bool loadSoundOpen(bool defaultValue = true);
|
||||
void saveSoundOpen(bool open);
|
||||
|
||||
int loadBestStep(int level, int defaultValue = 0);
|
||||
void saveBestStep(int level, int step);
|
||||
|
||||
std::filesystem::path storageFilePath();
|
||||
|
||||
} // namespace pushbox
|
||||
|
||||
|
|
@ -1,103 +0,0 @@
|
|||
#include <extra2d/extra2d.h>
|
||||
#include <filesystem>
|
||||
|
||||
// Pushbox 游戏核心头文件
|
||||
#include "core/data.h"
|
||||
#include "core/storage.h"
|
||||
#include "scenes/start_scene.h"
|
||||
|
||||
// Nintendo Switch 平台支持
|
||||
#ifdef __SWITCH__
|
||||
#include <switch.h>
|
||||
#endif
|
||||
|
||||
// Switch 上的存储路径
|
||||
#ifdef __SWITCH__
|
||||
static const char *SWITCH_STORAGE_PATH = "sdmc:/switch/pushbox";
|
||||
#endif
|
||||
|
||||
static std::filesystem::path getExecutableDir(int argc, char **argv) {
|
||||
// Nintendo Switch 上使用 SD 卡路径
|
||||
#ifdef __SWITCH__
|
||||
// 创建目录(如果不存在)
|
||||
std::filesystem::create_directories(SWITCH_STORAGE_PATH);
|
||||
return SWITCH_STORAGE_PATH;
|
||||
#else
|
||||
if (argc <= 0 || argv == nullptr || argv[0] == nullptr) {
|
||||
return std::filesystem::current_path();
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
auto exePath = std::filesystem::absolute(argv[0], ec);
|
||||
if (ec) {
|
||||
return std::filesystem::current_path();
|
||||
}
|
||||
return exePath.parent_path();
|
||||
#endif
|
||||
}
|
||||
|
||||
static float parseAutoQuitSeconds(int argc, char **argv) {
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (!argv[i]) {
|
||||
continue;
|
||||
}
|
||||
const std::string arg = argv[i];
|
||||
const std::string prefix = "--autoquit=";
|
||||
if (arg.rfind(prefix, 0) == 0) {
|
||||
try {
|
||||
return std::stof(arg.substr(prefix.size()));
|
||||
} catch (...) {
|
||||
return 0.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
extra2d::Logger::init();
|
||||
extra2d::Logger::setLevel(extra2d::LogLevel::Info);
|
||||
|
||||
auto &app = extra2d::Application::instance();
|
||||
|
||||
extra2d::AppConfig config;
|
||||
config.title = "推箱子";
|
||||
config.width = 640;
|
||||
config.height = 480;
|
||||
config.vsync = true;
|
||||
config.fpsLimit = 0;
|
||||
|
||||
if (!app.init(config)) {
|
||||
extra2d::Logger::shutdown();
|
||||
return -1;
|
||||
}
|
||||
|
||||
const auto exeDir = getExecutableDir(argc, argv);
|
||||
auto &resources = app.resources();
|
||||
resources.addSearchPath(exeDir.string());
|
||||
resources.addSearchPath((exeDir / "assets").string());
|
||||
resources.addSearchPath((exeDir.parent_path() / "assets").string());
|
||||
resources.addSearchPath((exeDir.parent_path() / "src").string());
|
||||
resources.addSearchPath("assets");
|
||||
resources.addSearchPath("src");
|
||||
|
||||
pushbox::initStorage(exeDir);
|
||||
pushbox::g_CurrentLevel = pushbox::loadCurrentLevel(1);
|
||||
if (pushbox::g_CurrentLevel > MAX_LEVEL) {
|
||||
pushbox::g_CurrentLevel = 1;
|
||||
}
|
||||
pushbox::g_SoundOpen = pushbox::loadSoundOpen(true);
|
||||
|
||||
// 进入开始场景(主界面)
|
||||
app.enterScene(extra2d::makePtr<pushbox::StartScene>());
|
||||
|
||||
const float autoQuitSeconds = parseAutoQuitSeconds(argc, argv);
|
||||
if (autoQuitSeconds > 0.0f) {
|
||||
app.timers().addTimer(autoQuitSeconds, [&app]() { app.quit(); });
|
||||
}
|
||||
app.run();
|
||||
|
||||
app.shutdown();
|
||||
extra2d::Logger::shutdown();
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
#include "audio_controller.h"
|
||||
|
||||
#include "../core/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
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include "../core/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
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 477 B |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 658 B |
|
Before Width: | Height: | Size: 682 B |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 641 B |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 191 KiB |
|
Before Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 642 B |
|
|
@ -1,27 +0,0 @@
|
|||
; Pushbox Game Configuration File
|
||||
; 推箱子游戏配置文件
|
||||
|
||||
[game]
|
||||
; 当前关卡 (1-15)
|
||||
level = 1
|
||||
|
||||
; 声音开关 (true/false)
|
||||
sound = true
|
||||
|
||||
[best]
|
||||
; 各关卡最佳步数记录
|
||||
; level1 = 0
|
||||
; level2 = 0
|
||||
; level3 = 0
|
||||
; level4 = 0
|
||||
; level5 = 0
|
||||
; level6 = 0
|
||||
; level7 = 0
|
||||
; level8 = 0
|
||||
; level9 = 0
|
||||
; level10 = 0
|
||||
; level11 = 0
|
||||
; level12 = 0
|
||||
; level13 = 0
|
||||
; level14 = 0
|
||||
; level15 = 0
|
||||
|
|
@ -1,339 +0,0 @@
|
|||
#include "play_scene.h"
|
||||
|
||||
#include "../core/audio_context.h"
|
||||
#include "../core/storage.h"
|
||||
#include "../nodes/audio_controller.h"
|
||||
#include "start_scene.h"
|
||||
#include "success_scene.h"
|
||||
#include <extra2d/extra2d.h>
|
||||
|
||||
namespace pushbox {
|
||||
|
||||
static extra2d::Ptr<extra2d::FontAtlas> loadFont(int size) {
|
||||
auto &resources = extra2d::Application::instance().resources();
|
||||
return resources.loadFont("assets/font.ttf", size);
|
||||
}
|
||||
|
||||
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");
|
||||
E2D_LOG_INFO("wall texture: {}", texWall_ ? "OK" : "FAILED");
|
||||
|
||||
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");
|
||||
|
||||
if (!texWall_ || !texFloor_ || !texBox_ || !texBoxInPoint_) {
|
||||
E2D_LOG_ERROR("PlayScene: Failed to load basic textures!");
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if (!font28_ || !font20_) {
|
||||
E2D_LOG_ERROR("PlayScene: Failed to load fonts!");
|
||||
}
|
||||
|
||||
if (font28_) {
|
||||
levelText_ = extra2d::Text::create("", font28_);
|
||||
levelText_->setPosition(520.0f, 30.0f);
|
||||
levelText_->setTextColor(extra2d::Colors::White);
|
||||
addChild(levelText_);
|
||||
}
|
||||
|
||||
if (font20_) {
|
||||
stepText_ = extra2d::Text::create("", font20_);
|
||||
stepText_->setPosition(520.0f, 100.0f);
|
||||
stepText_->setTextColor(extra2d::Colors::White);
|
||||
addChild(stepText_);
|
||||
|
||||
bestText_ = extra2d::Text::create("", font20_);
|
||||
bestText_->setPosition(520.0f, 140.0f);
|
||||
bestText_->setTextColor(extra2d::Colors::White);
|
||||
addChild(bestText_);
|
||||
|
||||
auto exitText = extra2d::Text::create("按ESC返回", font20_);
|
||||
exitText->setPosition(520.0f, 250.0f);
|
||||
exitText->setTextColor(extra2d::Colors::White);
|
||||
addChild(exitText);
|
||||
|
||||
auto restartText = extra2d::Text::create("按回车重开", font20_);
|
||||
restartText->setPosition(520.0f, 290.0f);
|
||||
restartText->setTextColor(extra2d::Colors::White);
|
||||
addChild(restartText);
|
||||
}
|
||||
|
||||
auto soundOn = resources.loadTexture("assets/images/soundon.png");
|
||||
auto soundOff = resources.loadTexture("assets/images/soundoff.png");
|
||||
if (soundOn && soundOff) {
|
||||
soundBtn_ = extra2d::ToggleImageButton::create();
|
||||
soundBtn_->setStateImages(soundOff, soundOn);
|
||||
soundBtn_->setCustomSize(static_cast<float>(soundOn->getWidth()),
|
||||
static_cast<float>(soundOn->getHeight()));
|
||||
soundBtn_->setBorder(extra2d::Colors::Transparent, 0.0f);
|
||||
soundBtn_->setPosition(560.0f, 360.0f);
|
||||
soundBtn_->setOnStateChange([](bool on) {
|
||||
if (auto audio = getAudioController()) {
|
||||
audio->setEnabled(on);
|
||||
}
|
||||
});
|
||||
addChild(soundBtn_);
|
||||
}
|
||||
|
||||
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();
|
||||
if (soundBtn_) {
|
||||
soundBtn_->setOn(g_SoundOpen);
|
||||
}
|
||||
}
|
||||
|
||||
void PlayScene::onUpdate(float dt) {
|
||||
Scene::onUpdate(dt);
|
||||
|
||||
auto &app = extra2d::Application::instance();
|
||||
auto &input = app.input();
|
||||
|
||||
if (input.isKeyPressed(extra2d::Key::Escape)) {
|
||||
app.scenes().replaceScene(extra2d::makePtr<StartScene>(),
|
||||
extra2d::TransitionType::Fade, 0.2f);
|
||||
return;
|
||||
}
|
||||
|
||||
if (input.isKeyPressed(extra2d::Key::Enter)) {
|
||||
setLevel(g_CurrentLevel);
|
||||
return;
|
||||
}
|
||||
|
||||
if (input.isKeyPressed(extra2d::Key::Up)) {
|
||||
move(0, -1, 1);
|
||||
flush();
|
||||
} else if (input.isKeyPressed(extra2d::Key::Down)) {
|
||||
move(0, 1, 2);
|
||||
flush();
|
||||
} else if (input.isKeyPressed(extra2d::Key::Left)) {
|
||||
move(-1, 0, 3);
|
||||
flush();
|
||||
} else if (input.isKeyPressed(extra2d::Key::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::flush() {
|
||||
mapLayer_->removeAllChildren();
|
||||
|
||||
int tileW = texFloor_ ? texFloor_->getWidth() : 32;
|
||||
int tileH = texFloor_ ? texFloor_->getHeight() : 32;
|
||||
|
||||
float offsetX = static_cast<float>((12 - map_.width) / 2) * tileW;
|
||||
float offsetY = static_cast<float>((12 - map_.height) / 2) * tileH;
|
||||
|
||||
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_ = g_Maps[level - 1];
|
||||
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
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include "../core/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 flush();
|
||||
void setLevel(int level);
|
||||
void setStep(int step);
|
||||
void move(int dx, int dy, int direct);
|
||||
void gameOver();
|
||||
|
||||
int step_ = 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::Node> mapLayer_;
|
||||
|
||||
extra2d::Ptr<extra2d::ToggleImageButton> soundBtn_;
|
||||
|
||||
extra2d::Ptr<extra2d::Texture> texWall_;
|
||||
extra2d::Ptr<extra2d::Texture> texPoint_;
|
||||
extra2d::Ptr<extra2d::Texture> texFloor_;
|
||||
extra2d::Ptr<extra2d::Texture> texBox_;
|
||||
extra2d::Ptr<extra2d::Texture> texBoxInPoint_;
|
||||
|
||||
extra2d::Ptr<extra2d::Texture> texMan_[5];
|
||||
extra2d::Ptr<extra2d::Texture> texManPush_[5];
|
||||
};
|
||||
|
||||
} // namespace pushbox
|
||||
|
|
@ -1,134 +0,0 @@
|
|||
#include "start_scene.h"
|
||||
|
||||
#include "../core/audio_context.h"
|
||||
#include "../core/data.h"
|
||||
#include "../nodes/audio_controller.h"
|
||||
#include "../scenes/play_scene.h"
|
||||
#include "../ui/menu_button.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();
|
||||
return resources.loadFont("assets/font.ttf", 28);
|
||||
}
|
||||
|
||||
void StartScene::onEnter() {
|
||||
Scene::onEnter();
|
||||
|
||||
E2D_LOG_INFO("StartScene::onEnter() - BEGIN");
|
||||
|
||||
auto &app = extra2d::Application::instance();
|
||||
auto &resources = app.resources();
|
||||
// 设置红色背景用于测试渲染
|
||||
setBackgroundColor(extra2d::Color(1.0f, 0.0f, 0.0f, 1.0f));
|
||||
E2D_LOG_INFO("StartScene: Background color set to RED for testing");
|
||||
|
||||
if (getChildren().empty()) {
|
||||
E2D_LOG_INFO("StartScene: Creating audio controller...");
|
||||
auto audioNode = AudioController::create();
|
||||
audioNode->setName("audio_controller");
|
||||
addChild(audioNode);
|
||||
setAudioController(audioNode);
|
||||
E2D_LOG_INFO("StartScene: Audio controller created");
|
||||
|
||||
E2D_LOG_INFO("StartScene: Loading background texture...");
|
||||
auto bgTex = resources.loadTexture("assets/images/start.jpg");
|
||||
if (bgTex) {
|
||||
E2D_LOG_INFO("StartScene: Background texture loaded successfully");
|
||||
auto background = extra2d::Sprite::create(bgTex);
|
||||
background->setAnchor(0.0f, 0.0f);
|
||||
background->setPosition(0.0f, 0.0f);
|
||||
float sx = static_cast<float>(app.getConfig().width) /
|
||||
static_cast<float>(bgTex->getWidth());
|
||||
float sy = static_cast<float>(app.getConfig().height) /
|
||||
static_cast<float>(bgTex->getHeight());
|
||||
background->setScale(sx, sy);
|
||||
addChild(background);
|
||||
E2D_LOG_INFO("StartScene: Background sprite added");
|
||||
} else {
|
||||
E2D_LOG_ERROR("StartScene: Failed to load background texture");
|
||||
}
|
||||
|
||||
E2D_LOG_INFO("StartScene: Loading font...");
|
||||
font_ = loadMenuFont();
|
||||
if (font_) {
|
||||
E2D_LOG_INFO(
|
||||
"StartScene: Font loaded successfully, creating menu buttons");
|
||||
// 字体加载成功,创建菜单按钮
|
||||
auto startBtn =
|
||||
MenuButton::create(font_, "新游戏", [this]() { startNewGame(); });
|
||||
startBtn->setPosition(app.getConfig().width / 2.0f, 260.0f);
|
||||
addChild(startBtn);
|
||||
|
||||
resumeBtn_ =
|
||||
MenuButton::create(font_, "继续关卡", [this]() { continueGame(); });
|
||||
resumeBtn_->setPosition(app.getConfig().width / 2.0f, 300.0f);
|
||||
addChild(resumeBtn_);
|
||||
|
||||
auto exitBtn =
|
||||
MenuButton::create(font_, "退出", [this]() { exitGame(); });
|
||||
exitBtn->setPosition(app.getConfig().width / 2.0f, 340.0f);
|
||||
addChild(exitBtn);
|
||||
E2D_LOG_INFO("StartScene: Menu buttons created");
|
||||
} else {
|
||||
E2D_LOG_ERROR("StartScene: Failed to load font, menu buttons will not be "
|
||||
"displayed");
|
||||
}
|
||||
|
||||
E2D_LOG_INFO("StartScene: Loading sound icons...");
|
||||
auto soundOn = resources.loadTexture("assets/images/soundon.png");
|
||||
auto soundOff = resources.loadTexture("assets/images/soundoff.png");
|
||||
if (soundOn && soundOff) {
|
||||
E2D_LOG_INFO("StartScene: Sound icons loaded successfully");
|
||||
soundBtn_ = extra2d::ToggleImageButton::create();
|
||||
soundBtn_->setStateImages(soundOff, soundOn);
|
||||
soundBtn_->setCustomSize(static_cast<float>(soundOn->getWidth()),
|
||||
static_cast<float>(soundOn->getHeight()));
|
||||
soundBtn_->setBorder(extra2d::Colors::Transparent, 0.0f);
|
||||
soundBtn_->setPosition(50.0f, 50.0f);
|
||||
soundBtn_->setOnStateChange([](bool on) {
|
||||
if (auto audio = getAudioController()) {
|
||||
audio->setEnabled(on);
|
||||
}
|
||||
});
|
||||
addChild(soundBtn_);
|
||||
} else {
|
||||
E2D_LOG_WARN("StartScene: Failed to load sound icons");
|
||||
}
|
||||
}
|
||||
|
||||
if (resumeBtn_) {
|
||||
resumeBtn_->setEnabled(g_CurrentLevel != 1);
|
||||
}
|
||||
|
||||
if (soundBtn_) {
|
||||
soundBtn_->setOn(g_SoundOpen);
|
||||
}
|
||||
|
||||
E2D_LOG_INFO("StartScene::onEnter() - END");
|
||||
}
|
||||
|
||||
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
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <extra2d/extra2d.h>
|
||||
|
||||
namespace pushbox {
|
||||
|
||||
class MenuButton;
|
||||
|
||||
class StartScene : public extra2d::Scene {
|
||||
public:
|
||||
StartScene();
|
||||
void onEnter() override;
|
||||
|
||||
private:
|
||||
void startNewGame();
|
||||
void continueGame();
|
||||
void exitGame();
|
||||
|
||||
extra2d::Ptr<MenuButton> resumeBtn_;
|
||||
extra2d::Ptr<extra2d::ToggleImageButton> soundBtn_;
|
||||
extra2d::Ptr<extra2d::FontAtlas> font_;
|
||||
};
|
||||
|
||||
} // namespace pushbox
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
#include "success_scene.h"
|
||||
|
||||
#include "../ui/menu_button.h"
|
||||
#include <extra2d/app/application.h>
|
||||
#include <extra2d/resource/resource_manager.h>
|
||||
#include <extra2d/scene/scene_manager.h>
|
||||
#include <extra2d/scene/sprite.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();
|
||||
return resources.loadFont("assets/font.ttf", 28);
|
||||
}
|
||||
|
||||
void SuccessScene::onEnter() {
|
||||
Scene::onEnter();
|
||||
|
||||
auto &app = extra2d::Application::instance();
|
||||
auto &resources = app.resources();
|
||||
setBackgroundColor(extra2d::Colors::Black);
|
||||
|
||||
if (getChildren().empty()) {
|
||||
auto bgTex = resources.loadTexture("assets/images/success.jpg");
|
||||
if (bgTex) {
|
||||
auto background = extra2d::Sprite::create(bgTex);
|
||||
background->setAnchor(0.0f, 0.0f);
|
||||
background->setPosition(0.0f, 0.0f);
|
||||
float sx = static_cast<float>(app.getConfig().width) /
|
||||
static_cast<float>(bgTex->getWidth());
|
||||
float sy = static_cast<float>(app.getConfig().height) /
|
||||
static_cast<float>(bgTex->getHeight());
|
||||
background->setScale(sx, sy);
|
||||
addChild(background);
|
||||
}
|
||||
|
||||
auto font = loadMenuFont();
|
||||
if (font) {
|
||||
auto backBtn = MenuButton::create(font, "回主菜单", []() {
|
||||
auto &scenes = extra2d::Application::instance().scenes();
|
||||
scenes.popScene(extra2d::TransitionType::Fade, 0.2f);
|
||||
scenes.popScene(extra2d::TransitionType::Fade, 0.2f);
|
||||
});
|
||||
backBtn->setPosition(app.getConfig().width / 2.0f, 350.0f);
|
||||
addChild(backBtn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace pushbox
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <extra2d/extra2d.h>
|
||||
|
||||
namespace pushbox {
|
||||
|
||||
class SuccessScene : public extra2d::Scene {
|
||||
public:
|
||||
SuccessScene();
|
||||
void onEnter() override;
|
||||
};
|
||||
|
||||
} // namespace pushbox
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
#include "menu_button.h"
|
||||
|
||||
#include <extra2d/core/color.h>
|
||||
#include <extra2d/event/event.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_();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
btn->getEventDispatcher().addListener(
|
||||
extra2d::EventType::UIHoverEnter,
|
||||
[wbtn = extra2d::WeakPtr<MenuButton>(btn)](extra2d::Event &) {
|
||||
if (auto self = wbtn.lock()) {
|
||||
if (self->enabled_) {
|
||||
self->setTextColor(extra2d::Colors::Blue);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
btn->getEventDispatcher().addListener(
|
||||
extra2d::EventType::UIHoverExit,
|
||||
[wbtn = extra2d::WeakPtr<MenuButton>(btn)](extra2d::Event &) {
|
||||
if (auto self = wbtn.lock()) {
|
||||
if (self->enabled_) {
|
||||
self->setTextColor(extra2d::Colors::Black);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return btn;
|
||||
}
|
||||
|
||||
void MenuButton::setEnabled(bool enabled) {
|
||||
enabled_ = enabled;
|
||||
setTextColor(enabled ? extra2d::Colors::Black : extra2d::Colors::LightGray);
|
||||
}
|
||||
|
||||
} // namespace pushbox
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
#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
|
||||
|
|
@ -0,0 +1,438 @@
|
|||
#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));
|
||||
|
||||
// 创建1000个碰撞节点
|
||||
createNodes(1000);
|
||||
|
||||
// 加载字体
|
||||
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_;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// 程序入口
|
||||
// ============================================================================
|
||||
|
||||
extern "C" int main(int argc, char *argv[]) {
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
|
||||
Logger::init();
|
||||
Logger::setLevel(LogLevel::Debug);
|
||||
|
||||
E2D_LOG_INFO("========================");
|
||||
E2D_LOG_INFO("Easy2D 引擎空间索引演示");
|
||||
E2D_LOG_INFO("========================");
|
||||
|
||||
auto &app = Application::instance();
|
||||
|
||||
AppConfig config;
|
||||
config.title = "Easy2D - 引擎空间索引演示";
|
||||
config.width = 1280;
|
||||
config.height = 720;
|
||||
config.vsync = true;
|
||||
config.fpsLimit = 60;
|
||||
|
||||
if (!app.init(config)) {
|
||||
E2D_LOG_ERROR("应用初始化失败!");
|
||||
return -1;
|
||||
}
|
||||
|
||||
app.enterScene(makePtr<SpatialIndexDemoScene>());
|
||||
|
||||
E2D_LOG_INFO("开始主循环...");
|
||||
|
||||
app.run();
|
||||
|
||||
E2D_LOG_INFO("应用结束");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -1,152 +0,0 @@
|
|||
#include <extra2d/extra2d.h>
|
||||
#include <iostream>
|
||||
|
||||
// Nintendo Switch 平台支持
|
||||
#ifdef __SWITCH__
|
||||
#include <switch.h>
|
||||
#endif
|
||||
|
||||
using namespace extra2d;
|
||||
|
||||
// 加载系统字体的辅助函数
|
||||
extra2d::Ptr<extra2d::FontAtlas> loadSystemFont(int size) {
|
||||
auto &resources = Application::instance().resources();
|
||||
extra2d::Ptr<extra2d::FontAtlas> font = nullptr;
|
||||
|
||||
#ifdef __SWITCH__
|
||||
// Nintendo Switch 系统字体路径
|
||||
const char *switchFontPaths[] = {
|
||||
"romfs:/font.TTF", // RomFS 中的字体(注意大小写)
|
||||
"romfs:/font.ttf", // 小写备选
|
||||
"sdmc:/switch/pushbox/font.ttf", // SD 卡字体
|
||||
"/switch/pushbox/font.ttf", // 绝对路径
|
||||
};
|
||||
|
||||
for (auto *path : switchFontPaths) {
|
||||
font = resources.loadFont(path, size);
|
||||
if (font) {
|
||||
E2D_LOG_INFO("Loaded Switch font: %s", path);
|
||||
return font;
|
||||
}
|
||||
}
|
||||
#else
|
||||
// Windows 系统字体
|
||||
const char *winFontPaths[] = {
|
||||
"C:/Windows/Fonts/arial.ttf",
|
||||
"C:/Windows/Fonts/segoeui.ttf",
|
||||
"C:/Windows/Fonts/simsun.ttc",
|
||||
"C:/Windows/Fonts/simhei.ttf",
|
||||
};
|
||||
|
||||
for (auto *path : winFontPaths) {
|
||||
font = resources.loadFont(path, size);
|
||||
if (font) {
|
||||
E2D_LOG_INFO("Loaded Windows font: %s", path);
|
||||
return font;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
E2D_LOG_WARN("Failed to load any system font!");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
class SimpleScene : public Scene {
|
||||
public:
|
||||
SimpleScene() {
|
||||
// 设置背景颜色为深蓝色 (使用 RGB 值)
|
||||
setBackgroundColor(Color(0.0f, 0.0f, 0.5f, 1.0f));
|
||||
|
||||
// 创建一个红色填充矩形(用于测试渲染)
|
||||
// 矩形在屏幕左上角,大小 200x200
|
||||
Rect rectBounds(50, 50, 200, 200); // x, y, width, height
|
||||
auto rect = ShapeNode::createFilledRect(rectBounds, Colors::Red);
|
||||
addChild(rect);
|
||||
|
||||
// 创建一个黄色圆形
|
||||
auto circle =
|
||||
ShapeNode::createFilledCircle(Vec2(400, 300), 100, Colors::Yellow);
|
||||
addChild(circle);
|
||||
|
||||
// 创建一个绿色三角形
|
||||
auto triangle = ShapeNode::createFilledTriangle(
|
||||
Vec2(700, 200), Vec2(600, 400), Vec2(800, 400), Colors::Green);
|
||||
addChild(triangle);
|
||||
|
||||
// 创建一个简单的标签
|
||||
auto label = Text::create("Hello Switch!");
|
||||
|
||||
// 加载系统字体
|
||||
auto font = loadSystemFont(48);
|
||||
if (font) {
|
||||
label->setFont(font);
|
||||
E2D_LOG_INFO("Font loaded successfully!");
|
||||
} else {
|
||||
E2D_LOG_WARN("Font loading failed!");
|
||||
}
|
||||
|
||||
label->setTextColor(Colors::White);
|
||||
label->setPosition(640, 100); // 屏幕上方居中
|
||||
label->setAnchor(0.5f, 0.5f);
|
||||
addChild(label);
|
||||
|
||||
E2D_LOG_INFO("SimpleScene created successfully!");
|
||||
}
|
||||
};
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
// Nintendo Switch 初始化
|
||||
#ifdef __SWITCH__
|
||||
Result rc;
|
||||
|
||||
// 初始化 nxlink 调试输出(可选)
|
||||
rc = socketInitializeDefault();
|
||||
if (R_FAILED(rc)) {
|
||||
std::cout << "socketInitializeDefault failed" << std::endl;
|
||||
} else {
|
||||
nxlinkStdio();
|
||||
std::cout << "nxlink initialized!" << std::endl;
|
||||
}
|
||||
|
||||
// 初始化 RomFS(可选)
|
||||
rc = romfsInit();
|
||||
if (R_FAILED(rc)) {
|
||||
std::cout << "romfsInit failed" << std::endl;
|
||||
}
|
||||
#endif
|
||||
|
||||
std::cout << "Starting Easy2D Simple Test..." << std::endl;
|
||||
|
||||
// 配置应用
|
||||
AppConfig config;
|
||||
config.title = "Switch Simple Test";
|
||||
config.width = 1280;
|
||||
config.height = 720;
|
||||
|
||||
// 初始化 Easy2D
|
||||
if (!Application::instance().init(config)) {
|
||||
std::cerr << "Failed to initialize Easy2D!" << std::endl;
|
||||
return -1;
|
||||
}
|
||||
|
||||
std::cout << "Easy2D initialized successfully!" << std::endl;
|
||||
|
||||
// 创建场景并设置到场景管理器
|
||||
auto scene = std::make_shared<SimpleScene>();
|
||||
Application::instance().scenes().pushScene(scene);
|
||||
|
||||
std::cout << "Scene started!" << std::endl;
|
||||
|
||||
// 运行主循环
|
||||
Application::instance().run();
|
||||
|
||||
// 清理
|
||||
Application::instance().shutdown();
|
||||
|
||||
#ifdef __SWITCH__
|
||||
romfsExit();
|
||||
socketExit();
|
||||
#endif
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -0,0 +1,311 @@
|
|||
#ifndef __khrplatform_h_
|
||||
#define __khrplatform_h_
|
||||
|
||||
/*
|
||||
** Copyright (c) 2008-2018 The Khronos Group Inc.
|
||||
**
|
||||
** Permission is hereby granted, free of charge, to any person obtaining a
|
||||
** copy of this software and/or associated documentation files (the
|
||||
** "Materials"), to deal in the Materials without restriction, including
|
||||
** without limitation the rights to use, copy, modify, merge, publish,
|
||||
** distribute, sublicense, and/or sell copies of the Materials, and to
|
||||
** permit persons to whom the Materials are furnished to do so, subject to
|
||||
** the following conditions:
|
||||
**
|
||||
** The above copyright notice and this permission notice shall be included
|
||||
** in all copies or substantial portions of the Materials.
|
||||
**
|
||||
** THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
** MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS.
|
||||
*/
|
||||
|
||||
/* Khronos platform-specific types and definitions.
|
||||
*
|
||||
* The master copy of khrplatform.h is maintained in the Khronos EGL
|
||||
* Registry repository at https://github.com/KhronosGroup/EGL-Registry
|
||||
* The last semantic modification to khrplatform.h was at commit ID:
|
||||
* 67a3e0864c2d75ea5287b9f3d2eb74a745936692
|
||||
*
|
||||
* Adopters may modify this file to suit their platform. Adopters are
|
||||
* encouraged to submit platform specific modifications to the Khronos
|
||||
* group so that they can be included in future versions of this file.
|
||||
* Please submit changes by filing pull requests or issues on
|
||||
* the EGL Registry repository linked above.
|
||||
*
|
||||
*
|
||||
* See the Implementer's Guidelines for information about where this file
|
||||
* should be located on your system and for more details of its use:
|
||||
* http://www.khronos.org/registry/implementers_guide.pdf
|
||||
*
|
||||
* This file should be included as
|
||||
* #include <KHR/khrplatform.h>
|
||||
* by Khronos client API header files that use its types and defines.
|
||||
*
|
||||
* The types in khrplatform.h should only be used to define API-specific types.
|
||||
*
|
||||
* Types defined in khrplatform.h:
|
||||
* khronos_int8_t signed 8 bit
|
||||
* khronos_uint8_t unsigned 8 bit
|
||||
* khronos_int16_t signed 16 bit
|
||||
* khronos_uint16_t unsigned 16 bit
|
||||
* khronos_int32_t signed 32 bit
|
||||
* khronos_uint32_t unsigned 32 bit
|
||||
* khronos_int64_t signed 64 bit
|
||||
* khronos_uint64_t unsigned 64 bit
|
||||
* khronos_intptr_t signed same number of bits as a pointer
|
||||
* khronos_uintptr_t unsigned same number of bits as a pointer
|
||||
* khronos_ssize_t signed size
|
||||
* khronos_usize_t unsigned size
|
||||
* khronos_float_t signed 32 bit floating point
|
||||
* khronos_time_ns_t unsigned 64 bit time in nanoseconds
|
||||
* khronos_utime_nanoseconds_t unsigned time interval or absolute time in
|
||||
* nanoseconds
|
||||
* khronos_stime_nanoseconds_t signed time interval in nanoseconds
|
||||
* khronos_boolean_enum_t enumerated boolean type. This should
|
||||
* only be used as a base type when a client API's boolean type is
|
||||
* an enum. Client APIs which use an integer or other type for
|
||||
* booleans cannot use this as the base type for their boolean.
|
||||
*
|
||||
* Tokens defined in khrplatform.h:
|
||||
*
|
||||
* KHRONOS_FALSE, KHRONOS_TRUE Enumerated boolean false/true values.
|
||||
*
|
||||
* KHRONOS_SUPPORT_INT64 is 1 if 64 bit integers are supported; otherwise 0.
|
||||
* KHRONOS_SUPPORT_FLOAT is 1 if floats are supported; otherwise 0.
|
||||
*
|
||||
* Calling convention macros defined in this file:
|
||||
* KHRONOS_APICALL
|
||||
* KHRONOS_APIENTRY
|
||||
* KHRONOS_APIATTRIBUTES
|
||||
*
|
||||
* These may be used in function prototypes as:
|
||||
*
|
||||
* KHRONOS_APICALL void KHRONOS_APIENTRY funcname(
|
||||
* int arg1,
|
||||
* int arg2) KHRONOS_APIATTRIBUTES;
|
||||
*/
|
||||
|
||||
#if defined(__SCITECH_SNAP__) && !defined(KHRONOS_STATIC)
|
||||
# define KHRONOS_STATIC 1
|
||||
#endif
|
||||
|
||||
/*-------------------------------------------------------------------------
|
||||
* Definition of KHRONOS_APICALL
|
||||
*-------------------------------------------------------------------------
|
||||
* This precedes the return type of the function in the function prototype.
|
||||
*/
|
||||
#if defined(KHRONOS_STATIC)
|
||||
/* If the preprocessor constant KHRONOS_STATIC is defined, make the
|
||||
* header compatible with static linking. */
|
||||
# define KHRONOS_APICALL
|
||||
#elif defined(_WIN32)
|
||||
# define KHRONOS_APICALL __declspec(dllimport)
|
||||
#elif defined (__SYMBIAN32__)
|
||||
# define KHRONOS_APICALL IMPORT_C
|
||||
#elif defined(__ANDROID__)
|
||||
# define KHRONOS_APICALL __attribute__((visibility("default")))
|
||||
#else
|
||||
# define KHRONOS_APICALL
|
||||
#endif
|
||||
|
||||
/*-------------------------------------------------------------------------
|
||||
* Definition of KHRONOS_APIENTRY
|
||||
*-------------------------------------------------------------------------
|
||||
* This follows the return type of the function and precedes the function
|
||||
* name in the function prototype.
|
||||
*/
|
||||
#if defined(_WIN32) && !defined(_WIN32_WCE) && !defined(__SCITECH_SNAP__)
|
||||
/* Win32 but not WinCE */
|
||||
# define KHRONOS_APIENTRY __stdcall
|
||||
#else
|
||||
# define KHRONOS_APIENTRY
|
||||
#endif
|
||||
|
||||
/*-------------------------------------------------------------------------
|
||||
* Definition of KHRONOS_APIATTRIBUTES
|
||||
*-------------------------------------------------------------------------
|
||||
* This follows the closing parenthesis of the function prototype arguments.
|
||||
*/
|
||||
#if defined (__ARMCC_2__)
|
||||
#define KHRONOS_APIATTRIBUTES __softfp
|
||||
#else
|
||||
#define KHRONOS_APIATTRIBUTES
|
||||
#endif
|
||||
|
||||
/*-------------------------------------------------------------------------
|
||||
* basic type definitions
|
||||
*-----------------------------------------------------------------------*/
|
||||
#if (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L) || defined(__GNUC__) || defined(__SCO__) || defined(__USLC__)
|
||||
|
||||
|
||||
/*
|
||||
* Using <stdint.h>
|
||||
*/
|
||||
#include <stdint.h>
|
||||
typedef int32_t khronos_int32_t;
|
||||
typedef uint32_t khronos_uint32_t;
|
||||
typedef int64_t khronos_int64_t;
|
||||
typedef uint64_t khronos_uint64_t;
|
||||
#define KHRONOS_SUPPORT_INT64 1
|
||||
#define KHRONOS_SUPPORT_FLOAT 1
|
||||
/*
|
||||
* To support platform where unsigned long cannot be used interchangeably with
|
||||
* inptr_t (e.g. CHERI-extended ISAs), we can use the stdint.h intptr_t.
|
||||
* Ideally, we could just use (u)intptr_t everywhere, but this could result in
|
||||
* ABI breakage if khronos_uintptr_t is changed from unsigned long to
|
||||
* unsigned long long or similar (this results in different C++ name mangling).
|
||||
* To avoid changes for existing platforms, we restrict usage of intptr_t to
|
||||
* platforms where the size of a pointer is larger than the size of long.
|
||||
*/
|
||||
#if defined(__SIZEOF_LONG__) && defined(__SIZEOF_POINTER__)
|
||||
#if __SIZEOF_POINTER__ > __SIZEOF_LONG__
|
||||
#define KHRONOS_USE_INTPTR_T
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#elif defined(__VMS ) || defined(__sgi)
|
||||
|
||||
/*
|
||||
* Using <inttypes.h>
|
||||
*/
|
||||
#include <inttypes.h>
|
||||
typedef int32_t khronos_int32_t;
|
||||
typedef uint32_t khronos_uint32_t;
|
||||
typedef int64_t khronos_int64_t;
|
||||
typedef uint64_t khronos_uint64_t;
|
||||
#define KHRONOS_SUPPORT_INT64 1
|
||||
#define KHRONOS_SUPPORT_FLOAT 1
|
||||
|
||||
#elif defined(_WIN32) && !defined(__SCITECH_SNAP__)
|
||||
|
||||
/*
|
||||
* Win32
|
||||
*/
|
||||
typedef __int32 khronos_int32_t;
|
||||
typedef unsigned __int32 khronos_uint32_t;
|
||||
typedef __int64 khronos_int64_t;
|
||||
typedef unsigned __int64 khronos_uint64_t;
|
||||
#define KHRONOS_SUPPORT_INT64 1
|
||||
#define KHRONOS_SUPPORT_FLOAT 1
|
||||
|
||||
#elif defined(__sun__) || defined(__digital__)
|
||||
|
||||
/*
|
||||
* Sun or Digital
|
||||
*/
|
||||
typedef int khronos_int32_t;
|
||||
typedef unsigned int khronos_uint32_t;
|
||||
#if defined(__arch64__) || defined(_LP64)
|
||||
typedef long int khronos_int64_t;
|
||||
typedef unsigned long int khronos_uint64_t;
|
||||
#else
|
||||
typedef long long int khronos_int64_t;
|
||||
typedef unsigned long long int khronos_uint64_t;
|
||||
#endif /* __arch64__ */
|
||||
#define KHRONOS_SUPPORT_INT64 1
|
||||
#define KHRONOS_SUPPORT_FLOAT 1
|
||||
|
||||
#elif 0
|
||||
|
||||
/*
|
||||
* Hypothetical platform with no float or int64 support
|
||||
*/
|
||||
typedef int khronos_int32_t;
|
||||
typedef unsigned int khronos_uint32_t;
|
||||
#define KHRONOS_SUPPORT_INT64 0
|
||||
#define KHRONOS_SUPPORT_FLOAT 0
|
||||
|
||||
#else
|
||||
|
||||
/*
|
||||
* Generic fallback
|
||||
*/
|
||||
#include <stdint.h>
|
||||
typedef int32_t khronos_int32_t;
|
||||
typedef uint32_t khronos_uint32_t;
|
||||
typedef int64_t khronos_int64_t;
|
||||
typedef uint64_t khronos_uint64_t;
|
||||
#define KHRONOS_SUPPORT_INT64 1
|
||||
#define KHRONOS_SUPPORT_FLOAT 1
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
/*
|
||||
* Types that are (so far) the same on all platforms
|
||||
*/
|
||||
typedef signed char khronos_int8_t;
|
||||
typedef unsigned char khronos_uint8_t;
|
||||
typedef signed short int khronos_int16_t;
|
||||
typedef unsigned short int khronos_uint16_t;
|
||||
|
||||
/*
|
||||
* Types that differ between LLP64 and LP64 architectures - in LLP64,
|
||||
* pointers are 64 bits, but 'long' is still 32 bits. Win64 appears
|
||||
* to be the only LLP64 architecture in current use.
|
||||
*/
|
||||
#ifdef KHRONOS_USE_INTPTR_T
|
||||
typedef intptr_t khronos_intptr_t;
|
||||
typedef uintptr_t khronos_uintptr_t;
|
||||
#elif defined(_WIN64)
|
||||
typedef signed long long int khronos_intptr_t;
|
||||
typedef unsigned long long int khronos_uintptr_t;
|
||||
#else
|
||||
typedef signed long int khronos_intptr_t;
|
||||
typedef unsigned long int khronos_uintptr_t;
|
||||
#endif
|
||||
|
||||
#if defined(_WIN64)
|
||||
typedef signed long long int khronos_ssize_t;
|
||||
typedef unsigned long long int khronos_usize_t;
|
||||
#else
|
||||
typedef signed long int khronos_ssize_t;
|
||||
typedef unsigned long int khronos_usize_t;
|
||||
#endif
|
||||
|
||||
#if KHRONOS_SUPPORT_FLOAT
|
||||
/*
|
||||
* Float type
|
||||
*/
|
||||
typedef float khronos_float_t;
|
||||
#endif
|
||||
|
||||
#if KHRONOS_SUPPORT_INT64
|
||||
/* Time types
|
||||
*
|
||||
* These types can be used to represent a time interval in nanoseconds or
|
||||
* an absolute Unadjusted System Time. Unadjusted System Time is the number
|
||||
* of nanoseconds since some arbitrary system event (e.g. since the last
|
||||
* time the system booted). The Unadjusted System Time is an unsigned
|
||||
* 64 bit value that wraps back to 0 every 584 years. Time intervals
|
||||
* may be either signed or unsigned.
|
||||
*/
|
||||
typedef khronos_uint64_t khronos_utime_nanoseconds_t;
|
||||
typedef khronos_int64_t khronos_stime_nanoseconds_t;
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Dummy value used to pad enum types to 32 bits.
|
||||
*/
|
||||
#ifndef KHRONOS_MAX_ENUM
|
||||
#define KHRONOS_MAX_ENUM 0x7FFFFFFF
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Enumerated boolean type
|
||||
*
|
||||
* Values other than zero should be considered to be true. Therefore
|
||||
* comparisons should not be made against KHRONOS_TRUE.
|
||||
*/
|
||||
typedef enum {
|
||||
KHRONOS_FALSE = 0,
|
||||
KHRONOS_TRUE = 1,
|
||||
KHRONOS_BOOLEAN_ENUM_FORCE_SIZE = KHRONOS_MAX_ENUM
|
||||
} khronos_boolean_enum_t;
|
||||
|
||||
#endif /* __khrplatform_h_ */
|
||||
|
|
@ -4,8 +4,7 @@
|
|||
#include <extra2d/graphics/opengl/gl_shader.h>
|
||||
#include <extra2d/graphics/opengl/gl_sprite_batch.h>
|
||||
|
||||
// 使用标准 GLES3.2
|
||||
#include <GLES3/gl32.h>
|
||||
#include <glad/glad.h>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
#pragma once
|
||||
|
||||
// 使用标准 GLES3.2
|
||||
#include <GLES3/gl32.h>
|
||||
#include <glad/glad.h>
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
|
|
|||
|
|
@ -8,8 +8,7 @@
|
|||
#include <glm/mat4x4.hpp>
|
||||
#include <vector>
|
||||
|
||||
// 使用标准 GLES3.2
|
||||
#include <GLES3/gl32.h>
|
||||
#include <glad/glad.h>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,7 @@
|
|||
#include <extra2d/graphics/texture.h>
|
||||
#include <extra2d/graphics/alpha_mask.h>
|
||||
|
||||
// 使用标准 GLES3.2
|
||||
#include <GLES3/gl32.h>
|
||||
#include <glad/glad.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
|
|
|
|||
|
|
@ -83,6 +83,14 @@ public:
|
|||
Ptr<FontAtlas> loadFont(const std::string &filepath, int fontSize,
|
||||
bool useSDF = false);
|
||||
|
||||
/// 尝试从多个候选路径加载字体,返回第一个成功加载的字体
|
||||
Ptr<FontAtlas> loadFontWithFallbacks(const std::vector<std::string> &fontPaths,
|
||||
int fontSize, bool useSDF = false);
|
||||
|
||||
/// 加载字体,使用默认系统字体作为后备
|
||||
Ptr<FontAtlas> loadFontWithDefaultFallback(const std::string &filepath,
|
||||
int fontSize, bool useSDF = false);
|
||||
|
||||
/// 通过key获取已缓存的字体图集
|
||||
Ptr<FontAtlas> getFont(const std::string &key) const;
|
||||
|
||||
|
|
|
|||
|
|
@ -7,19 +7,22 @@
|
|||
#include <string>
|
||||
#include <type_traits>
|
||||
|
||||
// SDL2 日志头文件
|
||||
#include <SDL.h>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
// ============================================================================
|
||||
// 日志级别枚举
|
||||
// 日志级别枚举 - 映射到 SDL_LogPriority
|
||||
// ============================================================================
|
||||
enum class LogLevel {
|
||||
Trace = 0,
|
||||
Debug = 1,
|
||||
Info = 2,
|
||||
Warn = 3,
|
||||
Error = 4,
|
||||
Fatal = 5,
|
||||
Off = 6
|
||||
Trace = SDL_LOG_PRIORITY_VERBOSE, // SDL 详细日志
|
||||
Debug = SDL_LOG_PRIORITY_DEBUG, // SDL 调试日志
|
||||
Info = SDL_LOG_PRIORITY_INFO, // SDL 信息日志
|
||||
Warn = SDL_LOG_PRIORITY_WARN, // SDL 警告日志
|
||||
Error = SDL_LOG_PRIORITY_ERROR, // SDL 错误日志
|
||||
Fatal = SDL_LOG_PRIORITY_CRITICAL, // SDL 严重日志
|
||||
Off = SDL_LOG_PRIORITY_CRITICAL + 1 // 关闭日志 (使用 Critical+1 作为关闭标记)
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
|
|
@ -146,82 +149,86 @@ inline std::string e2d_format(const char *fmt, const Args &...args) {
|
|||
inline std::string e2d_format(const char *fmt) { return std::string(fmt); }
|
||||
|
||||
// ============================================================================
|
||||
// Logger 类 - 简单 printf 日志
|
||||
// Logger 类 - 使用 SDL2 日志系统
|
||||
// ============================================================================
|
||||
class Logger {
|
||||
public:
|
||||
/**
|
||||
* @brief 初始化日志系统
|
||||
*/
|
||||
static void init();
|
||||
|
||||
/**
|
||||
* @brief 关闭日志系统
|
||||
*/
|
||||
static void shutdown();
|
||||
|
||||
/**
|
||||
* @brief 设置日志级别
|
||||
* @param level 日志级别
|
||||
*/
|
||||
static void setLevel(LogLevel level);
|
||||
|
||||
/**
|
||||
* @brief 设置是否输出到控制台
|
||||
* @param enable 是否启用
|
||||
*/
|
||||
static void setConsoleOutput(bool enable);
|
||||
|
||||
/**
|
||||
* @brief 设置日志输出到文件
|
||||
* @param filename 日志文件名
|
||||
*/
|
||||
static void setFileOutput(const std::string &filename);
|
||||
|
||||
/**
|
||||
* @brief 获取当前日志级别
|
||||
* @return 当前日志级别
|
||||
*/
|
||||
static LogLevel getLevel() { return level_; }
|
||||
|
||||
/**
|
||||
* @brief 日志记录模板函数
|
||||
* @param level 日志级别
|
||||
* @param fmt 格式化字符串
|
||||
* @param args 可变参数
|
||||
*/
|
||||
template <typename... Args>
|
||||
static void log(LogLevel level, const char *fmt, const Args &...args) {
|
||||
if (level < level_)
|
||||
if (static_cast<int>(level) < static_cast<int>(level_))
|
||||
return;
|
||||
std::string msg = e2d_format(fmt, args...);
|
||||
const char *levelStr = "";
|
||||
switch (level) {
|
||||
case LogLevel::Trace:
|
||||
levelStr = "TRACE";
|
||||
break;
|
||||
case LogLevel::Debug:
|
||||
levelStr = "DEBUG";
|
||||
break;
|
||||
case LogLevel::Info:
|
||||
levelStr = "INFO ";
|
||||
break;
|
||||
case LogLevel::Warn:
|
||||
levelStr = "WARN ";
|
||||
break;
|
||||
case LogLevel::Error:
|
||||
levelStr = "ERROR";
|
||||
break;
|
||||
case LogLevel::Fatal:
|
||||
levelStr = "FATAL";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
printf("[%s] %s\n", levelStr, msg.c_str());
|
||||
SDL_LogMessage(SDL_LOG_CATEGORY_APPLICATION,
|
||||
static_cast<SDL_LogPriority>(level), "[%s] %s",
|
||||
getLevelString(level), msg.c_str());
|
||||
}
|
||||
|
||||
// 无参数版本
|
||||
/**
|
||||
* @brief 日志记录无参数版本
|
||||
* @param level 日志级别
|
||||
* @param msg 日志消息
|
||||
*/
|
||||
static void log(LogLevel level, const char *msg) {
|
||||
if (level < level_)
|
||||
if (static_cast<int>(level) < static_cast<int>(level_))
|
||||
return;
|
||||
const char *levelStr = "";
|
||||
switch (level) {
|
||||
case LogLevel::Trace:
|
||||
levelStr = "TRACE";
|
||||
break;
|
||||
case LogLevel::Debug:
|
||||
levelStr = "DEBUG";
|
||||
break;
|
||||
case LogLevel::Info:
|
||||
levelStr = "INFO ";
|
||||
break;
|
||||
case LogLevel::Warn:
|
||||
levelStr = "WARN ";
|
||||
break;
|
||||
case LogLevel::Error:
|
||||
levelStr = "ERROR";
|
||||
break;
|
||||
case LogLevel::Fatal:
|
||||
levelStr = "FATAL";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
printf("[%s] %s\n", levelStr, msg);
|
||||
SDL_LogMessage(SDL_LOG_CATEGORY_APPLICATION,
|
||||
static_cast<SDL_LogPriority>(level), "[%s] %s",
|
||||
getLevelString(level), msg);
|
||||
}
|
||||
|
||||
private:
|
||||
static LogLevel level_;
|
||||
static bool initialized_;
|
||||
static LogLevel level_; // 当前日志级别
|
||||
static bool initialized_; // 是否已初始化
|
||||
static bool consoleOutput_; // 是否输出到控制台
|
||||
static bool fileOutput_; // 是否输出到文件
|
||||
static std::string logFile_; // 日志文件路径
|
||||
|
||||
/**
|
||||
* @brief 获取日志级别字符串
|
||||
* @param level 日志级别
|
||||
* @return 级别字符串
|
||||
*/
|
||||
static const char *getLevelString(LogLevel level);
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
|
|
|
|||
|
|
@ -3,8 +3,7 @@
|
|||
#include <extra2d/graphics/render_target.h>
|
||||
#include <extra2d/utils/logger.h>
|
||||
|
||||
// 使用标准 GLES3.2
|
||||
#include <GLES3/gl32.h>
|
||||
#include <glad/glad.h>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
// Switch: 使用 GLES 3.2
|
||||
#include <GLES3/gl32.h>
|
||||
#include <glad/glad.h>
|
||||
#include <extra2d/graphics/opengl/gl_texture.h>
|
||||
#include <extra2d/graphics/render_target.h>
|
||||
#include <extra2d/utils/logger.h>
|
||||
|
|
|
|||
|
|
@ -5,8 +5,7 @@
|
|||
|
||||
#include <SDL.h>
|
||||
|
||||
// 使用标准 GLES3.2
|
||||
#include <GLES3/gl32.h>
|
||||
#include <glad/glad.h>
|
||||
|
||||
namespace extra2d {
|
||||
|
||||
|
|
@ -83,10 +82,31 @@ bool Window::initSDL() {
|
|||
return false;
|
||||
}
|
||||
|
||||
if (SDL_GL_MakeCurrent(sdlWindow_, glContext_) != 0) {
|
||||
E2D_LOG_ERROR("SDL_GL_MakeCurrent failed: {}", SDL_GetError());
|
||||
SDL_GL_DeleteContext(glContext_);
|
||||
glContext_ = nullptr;
|
||||
SDL_DestroyWindow(sdlWindow_);
|
||||
sdlWindow_ = nullptr;
|
||||
SDL_Quit();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (gladLoadGLES2Loader(reinterpret_cast<GLADloadproc>(SDL_GL_GetProcAddress)) ==
|
||||
0) {
|
||||
E2D_LOG_ERROR("gladLoadGLES2Loader failed");
|
||||
SDL_GL_DeleteContext(glContext_);
|
||||
glContext_ = nullptr;
|
||||
SDL_DestroyWindow(sdlWindow_);
|
||||
sdlWindow_ = nullptr;
|
||||
SDL_Quit();
|
||||
return false;
|
||||
}
|
||||
|
||||
// 设置 VSync
|
||||
SDL_GL_SetSwapInterval(vsync_ ? 1 : 0);
|
||||
|
||||
E2D_LOG_INFO("SDL2 + GLES 3.2 initialized successfully");
|
||||
E2D_LOG_INFO("SDL2 + GLES 3.2 (glad) initialized successfully");
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -313,6 +313,92 @@ void ResourceManager::unloadFont(const std::string &key) {
|
|||
E2D_LOG_DEBUG("ResourceManager: unloaded font: {}", key);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 多字体后备加载
|
||||
// ============================================================================
|
||||
|
||||
Ptr<FontAtlas> ResourceManager::loadFontWithFallbacks(
|
||||
const std::vector<std::string> &fontPaths, int fontSize, bool useSDF) {
|
||||
|
||||
// 尝试加载每一个候选字体
|
||||
for (const auto &fontPath : fontPaths) {
|
||||
auto font = loadFont(fontPath, fontSize, useSDF);
|
||||
if (font) {
|
||||
E2D_LOG_INFO("ResourceManager: successfully loaded font from fallback list: {}",
|
||||
fontPath);
|
||||
return font;
|
||||
}
|
||||
}
|
||||
|
||||
E2D_LOG_ERROR("ResourceManager: failed to load any font from fallback list ({} candidates)",
|
||||
fontPaths.size());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Ptr<FontAtlas> ResourceManager::loadFontWithDefaultFallback(
|
||||
const std::string &filepath, int fontSize, bool useSDF) {
|
||||
|
||||
// 首先尝试加载用户指定的字体
|
||||
auto font = loadFont(filepath, fontSize, useSDF);
|
||||
if (font) {
|
||||
return font;
|
||||
}
|
||||
|
||||
E2D_LOG_WARN("ResourceManager: failed to load font '{}', trying system fallbacks...",
|
||||
filepath);
|
||||
|
||||
// 定义系统默认字体候选列表
|
||||
std::vector<std::string> fallbackFonts;
|
||||
|
||||
#ifdef __SWITCH__
|
||||
// Switch 平台默认字体路径
|
||||
fallbackFonts = {
|
||||
"romfs:/assets/font.ttf", // 应用自带字体
|
||||
"romfs:/assets/default.ttf", // 默认字体备选
|
||||
"romfs:/font.ttf", // 根目录字体
|
||||
"sdmc:/switch/fonts/default.ttf", // SD卡字体目录
|
||||
"sdmc:/switch/fonts/font.ttf",
|
||||
};
|
||||
#else
|
||||
// PC 平台系统字体路径(Windows/Linux/macOS)
|
||||
#ifdef _WIN32
|
||||
fallbackFonts = {
|
||||
"C:/Windows/Fonts/arial.ttf",
|
||||
"C:/Windows/Fonts/segoeui.ttf",
|
||||
"C:/Windows/Fonts/calibri.ttf",
|
||||
"C:/Windows/Fonts/tahoma.ttf",
|
||||
"C:/Windows/Fonts/msyh.ttc", // 微软雅黑
|
||||
};
|
||||
#elif __APPLE__
|
||||
fallbackFonts = {
|
||||
"/System/Library/Fonts/Helvetica.ttc",
|
||||
"/System/Library/Fonts/SFNSDisplay.ttf",
|
||||
"/Library/Fonts/Arial.ttf",
|
||||
};
|
||||
#else
|
||||
// Linux
|
||||
fallbackFonts = {
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
||||
"/usr/share/fonts/truetype/freefont/FreeSans.ttf",
|
||||
"/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
|
||||
"/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf",
|
||||
};
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// 尝试加载后备字体
|
||||
for (const auto &fallbackPath : fallbackFonts) {
|
||||
font = loadFont(fallbackPath, fontSize, useSDF);
|
||||
if (font) {
|
||||
E2D_LOG_INFO("ResourceManager: loaded fallback font: {}", fallbackPath);
|
||||
return font;
|
||||
}
|
||||
}
|
||||
|
||||
E2D_LOG_ERROR("ResourceManager: all font fallbacks exhausted, no font available");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 音效资源
|
||||
// ============================================================================
|
||||
|
|
|
|||
|
|
@ -3,18 +3,54 @@
|
|||
namespace extra2d {
|
||||
|
||||
// 静态成员定义
|
||||
LogLevel Logger::level_ = LogLevel::Trace;
|
||||
LogLevel Logger::level_ = LogLevel::Info;
|
||||
bool Logger::initialized_ = false;
|
||||
bool Logger::consoleOutput_ = true;
|
||||
bool Logger::fileOutput_ = false;
|
||||
std::string Logger::logFile_;
|
||||
|
||||
/**
|
||||
* @brief 获取日志级别字符串
|
||||
* @param level 日志级别
|
||||
* @return 级别字符串
|
||||
*/
|
||||
const char *Logger::getLevelString(LogLevel level) {
|
||||
switch (level) {
|
||||
case LogLevel::Trace:
|
||||
return "TRACE";
|
||||
case LogLevel::Debug:
|
||||
return "DEBUG";
|
||||
case LogLevel::Info:
|
||||
return "INFO ";
|
||||
case LogLevel::Warn:
|
||||
return "WARN ";
|
||||
case LogLevel::Error:
|
||||
return "ERROR";
|
||||
case LogLevel::Fatal:
|
||||
return "FATAL";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 初始化日志系统
|
||||
*/
|
||||
void Logger::init() {
|
||||
if (initialized_) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置 SDL 日志级别为详细模式(允许所有级别的日志)
|
||||
SDL_LogSetPriority(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_VERBOSE);
|
||||
|
||||
initialized_ = true;
|
||||
log(LogLevel::Info, "Logger initialized");
|
||||
log(LogLevel::Info, "Logger initialized with SDL2");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 关闭日志系统
|
||||
*/
|
||||
void Logger::shutdown() {
|
||||
if (initialized_) {
|
||||
log(LogLevel::Info, "Logger shutting down");
|
||||
|
|
@ -22,16 +58,47 @@ void Logger::shutdown() {
|
|||
initialized_ = false;
|
||||
}
|
||||
|
||||
void Logger::setLevel(LogLevel level) { level_ = level; }
|
||||
|
||||
void Logger::setConsoleOutput(bool /*enable*/) {
|
||||
// On Switch, console output always goes to nxlink stdout
|
||||
// Nothing to configure
|
||||
/**
|
||||
* @brief 设置日志级别
|
||||
* @param level 日志级别
|
||||
*/
|
||||
void Logger::setLevel(LogLevel level) {
|
||||
level_ = level;
|
||||
// 同时设置 SDL 的日志级别
|
||||
if (level != LogLevel::Off) {
|
||||
SDL_LogSetPriority(SDL_LOG_CATEGORY_APPLICATION,
|
||||
static_cast<SDL_LogPriority>(level));
|
||||
}
|
||||
}
|
||||
|
||||
void Logger::setFileOutput(const std::string & /*filename*/) {
|
||||
// File output not supported on Switch
|
||||
// Could potentially write to sdmc:/ in the future
|
||||
/**
|
||||
* @brief 设置是否输出到控制台
|
||||
* @param enable 是否启用
|
||||
*/
|
||||
void Logger::setConsoleOutput(bool enable) {
|
||||
consoleOutput_ = enable;
|
||||
// SDL2 日志默认输出到控制台,通过设置日志优先级控制
|
||||
if (!enable) {
|
||||
SDL_LogSetPriority(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_CRITICAL);
|
||||
} else {
|
||||
SDL_LogSetPriority(SDL_LOG_CATEGORY_APPLICATION,
|
||||
static_cast<SDL_LogPriority>(level_));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 设置日志输出到文件
|
||||
* @param filename 日志文件名
|
||||
*/
|
||||
void Logger::setFileOutput(const std::string &filename) {
|
||||
logFile_ = filename;
|
||||
fileOutput_ = !filename.empty();
|
||||
|
||||
if (fileOutput_) {
|
||||
// SDL2 使用 SDL_LogSetOutputFunction 可以重定向日志输出
|
||||
// 这里我们记录文件路径,实际文件输出可以通过自定义回调实现
|
||||
log(LogLevel::Info, "File output configured: {}", filename);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace extra2d
|
||||
|
|
|
|||
783
README.md
|
|
@ -1,13 +1,13 @@
|
|||
<div align="center">
|
||||
|
||||

|
||||

|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Easy2D/Easy2D/releases/latest">
|
||||
<img src="https://img.shields.io/github/release/easy2d/easy2d?style=for-the-badge&color=blue&logo=github" alt="Release">
|
||||
<a href="https://github.com/ChestnutYueyue/extra2d/releases/latest">
|
||||
<img src="https://img.shields.io/github/release/ChestnutYueyue/extra2d?style=for-the-badge&color=blue&logo=github" alt="Release">
|
||||
</a>
|
||||
<a href="https://github.com/Easy2D/Easy2D/blob/master/LICENSE">
|
||||
<img src="https://img.shields.io/github/license/easy2d/easy2d?style=for-the-badge&color=green&logo=opensourceinitiative" alt="License">
|
||||
<a href="https://github.com/ChestnutYueyue/extra2d/blob/master/LICENSE">
|
||||
<img src="https://img.shields.io/github/license/ChestnutYueyue/extra2d?style=for-the-badge&color=green&logo=opensourceinitiative" alt="License">
|
||||
</a>
|
||||
<a href="#">
|
||||
<img src="https://img.shields.io/badge/build-passing-brightgreen?style=for-the-badge&logo=appveyor" alt="Build Status">
|
||||
|
|
@ -16,22 +16,16 @@
|
|||
<img src="https://img.shields.io/badge/C++-17-00599C?style=for-the-badge&logo=c%2B%2B" alt="C++17">
|
||||
</a>
|
||||
<a href="#">
|
||||
<img src="https://img.shields.io/badge/Windows-0078D6?style=for-the-badge&logo=windows&logoColor=white" alt="Windows">
|
||||
</a>
|
||||
<a href="#">
|
||||
<img src="https://img.shields.io/badge/Linux-FCC624?style=for-the-badge&logo=linux&logoColor=black" alt="Linux">
|
||||
</a>
|
||||
<a href="#">
|
||||
<img src="https://img.shields.io/badge/macOS-000000?style=for-the-badge&logo=apple&logoColor=white" alt="macOS">
|
||||
<img src="https://img.shields.io/badge/Nintendo%20Switch-E60012?style=for-the-badge&logo=nintendo-switch&logoColor=white" alt="Nintendo Switch">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<b>🎮 为 C++ 打造的轻量级 2D 游戏引擎</b><br>
|
||||
<i>简单、高效、跨平台</i>
|
||||
<b>🎮 专为 Nintendo Switch 打造的轻量级 2D 游戏引擎</b><br>
|
||||
<i>高性能、易用、原生支持 Switch 平台</i>
|
||||
</p>
|
||||
|
||||
[📖 官方文档](https://easy2d.cn) | [🚀 快速开始](#快速开始) | [📦 构建安装](#构建安装) | [💬 QQ群: 608406540](#联系方式)
|
||||
[📖 构建指南](./SWITCH_BUILD_GUIDE.md) | [🚀 快速开始](#快速开始) | [📦 项目结构](#项目结构) | [💬 问题反馈](https://github.com/ChestnutYueyue/extra2d/issues)
|
||||
|
||||
</div>
|
||||
|
||||
|
|
@ -39,17 +33,18 @@
|
|||
|
||||
## 🌟 简介
|
||||
|
||||
**Easy2D v3.1.0** 是一个专为 C++ 设计的轻量级 2D 游戏引擎,采用全新架构设计,支持 Windows、Linux 和 macOS 三大平台。
|
||||
**Extra2D** 是一个专为 **Nintendo Switch** 平台设计的轻量级 2D 游戏引擎,采用现代 C++17 架构,充分利用 Switch 硬件特性,为开发者提供流畅的游戏开发体验。
|
||||
|
||||
> 💡 创建这个引擎的初衷是学习游戏引擎技术,并开发一些有趣的小游戏。Easy2D 提供了丰富的工具和轮子,让游戏开发变得简单而愉快。
|
||||
> 💡 Extra2D 的诞生是为了让 Switch 独立游戏开发变得更加简单高效。无论是复古风格的像素游戏,还是现代化的 2D 作品,Extra2D 都能提供强大的支持。
|
||||
|
||||
### ✨ 核心特性
|
||||
|
||||
- **🎬 动画系统**:支持基于动作(Action)的补间动画和基于精灵图(Sprite Sheet)的帧动画。`AnimatedSprite` 提供完整的动画控制,包括播放、暂停、帧范围限制、动画字典管理等功能。
|
||||
|
||||
- **📜 脚本系统**:集成 Squirrel 脚本引擎,支持使用类 JavaScript 语法编写游戏逻辑。通过 `ScriptComponent` 将脚本附加到节点,实现数据驱动的游戏开发。提供完整的引擎 API 绑定,包括节点操作、输入处理、动画控制等。
|
||||
|
||||
- **🎮 跨平台**:一套代码,多平台运行。支持 Windows、Linux 和 macOS。
|
||||
- **🎯 Switch 原生支持**:专为 Nintendo Switch 硬件优化,支持掌机/主机双模式
|
||||
- **🎬 高级动画系统**:支持骨骼动画、精灵动画、补间动画,提供 ALS 动画格式支持
|
||||
- **📜 脚本系统**:集成 Squirrel 脚本引擎,支持热更新和快速迭代开发
|
||||
- **🎵 音频系统**:基于 miniaudio 的高质量音频播放,支持 BGM 和音效
|
||||
- **🎨 特效系统**:粒子系统、后处理效果、自定义着色器支持
|
||||
- **💾 数据持久化**:游戏存档、配置文件的便捷读写
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -57,7 +52,7 @@
|
|||
|
||||
```mermaid
|
||||
mindmap
|
||||
root((Easy2D v3.1.0 引擎架构))
|
||||
root((Extra2D 引擎架构))
|
||||
核心系统
|
||||
应用管理 Application
|
||||
渲染后端 RenderBackend
|
||||
|
|
@ -66,50 +61,32 @@ mindmap
|
|||
音频引擎 AudioEngine
|
||||
资源管理 ResourceManager
|
||||
事件系统 EventDispatcher
|
||||
日志系统 Logger
|
||||
场景管理
|
||||
场景 Scene
|
||||
场景管理器 SceneManager
|
||||
过渡动画 Transition
|
||||
空间索引 SpatialManager
|
||||
四叉树 QuadTree
|
||||
空间哈希 SpatialHash
|
||||
节点系统
|
||||
基础节点 Node
|
||||
精灵 Sprite
|
||||
文本 Text
|
||||
形状 ShapeNode
|
||||
摄像机 Camera
|
||||
动画节点 AnimationNode
|
||||
动画系统
|
||||
动作系统 Action
|
||||
位移动作 MoveBy/MoveTo
|
||||
缩放动作 ScaleBy/ScaleTo
|
||||
旋转动作 RotateBy/RotateTo
|
||||
淡入淡出 FadeIn/FadeOut
|
||||
跳跃动作 JumpBy/JumpTo
|
||||
组合动作 Sequence/Spawn/Repeat
|
||||
缓动函数 Ease
|
||||
精灵动画系统
|
||||
动画精灵 AnimatedSprite
|
||||
动画片段 AnimationClip
|
||||
动画控制器 AnimationController
|
||||
精灵帧 SpriteFrame
|
||||
精灵动画 AnimatedSprite
|
||||
骨骼动画支持
|
||||
动画缓存 AnimationCache
|
||||
动画事件 AnimationEvent
|
||||
脚本系统
|
||||
Squirrel 脚本引擎 ScriptEngine
|
||||
VM 虚拟机管理
|
||||
脚本加载与执行
|
||||
错误处理与调试
|
||||
脚本组件 ScriptComponent
|
||||
生命周期回调 onEnter/onUpdate/onExit
|
||||
节点访问与操作
|
||||
脚本绑定 API
|
||||
节点绑定 Node/Sprite/AnimatedSprite
|
||||
输入绑定 Input/Key
|
||||
数学绑定 Vec2/Rect/Color
|
||||
事件系统
|
||||
事件队列 EventQueue
|
||||
事件分发 EventDispatcher
|
||||
输入码 InputCodes
|
||||
Squirrel 脚本引擎
|
||||
脚本节点 ScriptNode
|
||||
完整 API 绑定
|
||||
特效系统
|
||||
粒子系统 ParticleSystem
|
||||
后处理 PostProcess
|
||||
自定义效果管理器
|
||||
UI 系统
|
||||
基础控件 Widget
|
||||
按钮 Button
|
||||
|
|
@ -124,136 +101,6 @@ mindmap
|
|||
矩形 Rect
|
||||
大小 Size
|
||||
颜色 Color
|
||||
矩阵 glm::mat4
|
||||
```
|
||||
|
||||
### 🎬 动画系统详解
|
||||
|
||||
Easy2D 提供两套动画系统,满足不同场景需求:
|
||||
|
||||
**1. 动作系统(Action)**
|
||||
- 基于补间动画的节点变换系统
|
||||
- 支持位移、缩放、旋转、淡入淡出等基础动作
|
||||
- 支持组合动作(Sequence/Spawn/Repeat)和缓动函数
|
||||
- 适用于 UI 动画、特效动画等场景
|
||||
|
||||
**2. 精灵动画系统(AnimatedSprite)**
|
||||
- 基于精灵图的帧动画系统
|
||||
- 支持从网格创建动画(`createFromGrid`)
|
||||
- 支持帧范围限制,实现多方向动画管理
|
||||
- 支持动画字典,动态切换不同动画
|
||||
- 提供完整的播放控制(play/pause/stop/reset)
|
||||
- 适用于角色行走、攻击等游戏动画
|
||||
|
||||
### 📜 脚本系统详解
|
||||
|
||||
Easy2D v3.1.0 引入 Squirrel 脚本引擎,支持数据驱动的游戏开发:
|
||||
|
||||
**1. 脚本引擎(ScriptEngine)**
|
||||
- 基于 Squirrel 3.2 稳定版
|
||||
- 类 JavaScript 语法,易于学习
|
||||
- 支持面向对象编程
|
||||
- 提供完整的错误处理和调试信息
|
||||
|
||||
**2. 脚本组件(ScriptComponent)**
|
||||
- 将脚本附加到场景节点
|
||||
- 生命周期回调:`onEnter`、`onUpdate`、`onExit`
|
||||
- 通过 `node` 参数访问和操附加的节点
|
||||
- 支持自定义属性和方法
|
||||
|
||||
**3. 脚本绑定 API**
|
||||
- **节点操作**:`Node`、`Sprite`、`AnimatedSprite` 等
|
||||
- **输入处理**:`Input.isKeyDown()`、`Input.isKeyPressed()`
|
||||
- **数学类型**:`Vec2`、`Rect`、`Color` 等
|
||||
- **全局函数**:`log()` 日志输出
|
||||
|
||||
**示例脚本结构**:
|
||||
```nut
|
||||
return {
|
||||
function onEnter(node) {
|
||||
// 初始化:创建精灵、设置位置等
|
||||
}
|
||||
|
||||
function onUpdate(node, dt) {
|
||||
// 每帧更新:处理输入、更新状态等
|
||||
}
|
||||
|
||||
function onExit(node) {
|
||||
// 清理:释放资源等
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✨ 功能特性
|
||||
|
||||
### 🎬 核心功能
|
||||
|
||||
| 功能模块 | 描述 | 状态 |
|
||||
|:--------:|:-----|:----:|
|
||||
| 🎭 场景管理 | 灵活的场景切换与管理 | ✅ |
|
||||
| 🎨 过渡动画 | 淡入淡出、移动、盒子等多种过渡效果 | ✅ |
|
||||
| 🎬 动画系统 | 丰富的动作和帧动画支持 | ✅ |
|
||||
| 📜 脚本系统 | Squirrel 脚本支持,可编写游戏逻辑 | ✅ |
|
||||
| 🔘 GUI 系统 | 简单易用的按钮组件 | ✅ |
|
||||
| 🎵 音频支持 | 基于 miniaudio 的音频播放 | ✅ |
|
||||
| 💾 数据持久化 | 游戏数据保存与读取 | ✅ |
|
||||
| 📝 日志系统 | 基于 spdlog 的高性能日志 | ✅ |
|
||||
| 🌐 跨平台 | 支持 Windows/Linux/macOS | ✅ |
|
||||
| 🚀 OpenGL 渲染 | 现代 OpenGL 渲染后端 | ✅ |
|
||||
| 🎯 空间索引 | 四叉树/空间哈希碰撞检测 | ✅ |
|
||||
|
||||
### 🎯 动作系统详解
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph 基础动作
|
||||
A[Action 基类]
|
||||
B[IntervalAction 持续动作]
|
||||
C[InstantAction 瞬时动作]
|
||||
end
|
||||
|
||||
subgraph 变换动作
|
||||
D[MoveBy/MoveTo 位移]
|
||||
E[ScaleBy/ScaleTo 缩放]
|
||||
F[RotateBy/RotateTo 旋转]
|
||||
G[FadeIn/FadeOut 淡入淡出]
|
||||
H[JumpBy/JumpTo 跳跃]
|
||||
end
|
||||
|
||||
subgraph 复合动作
|
||||
I[Sequence 顺序执行]
|
||||
J[Spawn 同步执行]
|
||||
K[Repeat 循环执行]
|
||||
L[Delay 延时]
|
||||
M[CallFunc 回调]
|
||||
end
|
||||
|
||||
subgraph 缓动函数
|
||||
N[EaseIn/EaseOut]
|
||||
O[EaseInOut]
|
||||
P[EaseBack/EaseBounce]
|
||||
Q[EaseElastic]
|
||||
end
|
||||
|
||||
A --> B & C
|
||||
B --> D & E & F & G & H
|
||||
A --> I & J & K & L & M
|
||||
B --> N & O & P & Q
|
||||
```
|
||||
|
||||
### 🖼️ 渲染流程
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[Application] --> B[Update Scene]
|
||||
B --> C[Collect RenderCommands]
|
||||
C --> D[OpenGL Backend]
|
||||
D --> E[GPU Rendering]
|
||||
|
||||
style A fill:#ff6b6b,color:#fff
|
||||
style E fill:#4ecdc4,color:#fff
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -262,59 +109,49 @@ flowchart LR
|
|||
|
||||
### 环境要求
|
||||
|
||||
| 组件 | 最低版本 | 推荐版本 |
|
||||
|:----:|:--------:|:--------:|
|
||||
| Windows | Windows 7 | Windows 10/11 |
|
||||
| Linux | Ubuntu 18.04 | Ubuntu 22.04 |
|
||||
| macOS | 10.14 | 最新版 |
|
||||
| C++ 标准 | C++17 | C++17 |
|
||||
| OpenGL | 3.3 | 4.0+ |
|
||||
| 组件 | 要求 |
|
||||
|:----:|:-----|
|
||||
| 开发环境 | devkitPro + devkitA64 |
|
||||
| C++ 标准 | C++17 |
|
||||
| 构建工具 | xmake |
|
||||
| 目标平台 | Nintendo Switch |
|
||||
|
||||
### 使用 xmake 构建(推荐)
|
||||
|
||||
#### 步骤 1: 安装 xmake
|
||||
### 安装 devkitPro
|
||||
|
||||
```bash
|
||||
# Windows (PowerShell)
|
||||
Invoke-Expression (Invoke-Webrequest 'https://xmake.io/psget.text' -UseBasicParsing).Content
|
||||
# Windows (以管理员身份运行 PowerShell)
|
||||
Invoke-WebRequest -Uri "https://github.com/devkitPro/pacman/releases/latest/download/devkitpro-pacman.amd64.exe" -OutFile "devkitpro-pacman.exe"
|
||||
.\devkitpro-pacman.exe
|
||||
|
||||
# Linux/macOS
|
||||
curl -fsSL https://xmake.io/shget.text | bash
|
||||
# 安装 Switch 开发工具链
|
||||
pacman -S switch-dev switch-portlibs
|
||||
```
|
||||
|
||||
#### 步骤 2: 克隆并构建
|
||||
### 构建项目
|
||||
|
||||
```bash
|
||||
# 克隆仓库
|
||||
git clone https://github.com/nomango/easy2d.git
|
||||
cd easy2d
|
||||
git clone https://github.com/ChestnutYueyue/extra2d.git
|
||||
cd extra2d
|
||||
|
||||
# 配置并构建
|
||||
xmake f --mode=release
|
||||
# 配置 Switch 平台构建
|
||||
xmake f -p switch --mode=release
|
||||
|
||||
# 构建引擎
|
||||
xmake
|
||||
|
||||
# 运行示例
|
||||
xmake run hello_world
|
||||
xmake run push_box
|
||||
# 构建示例游戏
|
||||
xmake -g examples
|
||||
```
|
||||
|
||||
#### 平台特定配置
|
||||
### 生成 NSP 可运行文件
|
||||
|
||||
```bash
|
||||
# Windows (MSVC - 默认)
|
||||
xmake f --mode=release
|
||||
# 打包推箱子游戏示例
|
||||
xmake package push_box
|
||||
|
||||
# Windows (MinGW)
|
||||
xmake f --toolchain=mingw --mode=release
|
||||
|
||||
# Linux
|
||||
xmake f --mode=release
|
||||
|
||||
# macOS
|
||||
xmake f --mode=release
|
||||
|
||||
# 调试模式
|
||||
xmake f --mode=debug
|
||||
# 生成的文件位于
|
||||
# build/switch/release/push_box/push_box.nsp
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -322,9 +159,9 @@ xmake f --mode=debug
|
|||
## 📝 Hello World 示例
|
||||
|
||||
```cpp
|
||||
#include <easy2d/easy2d.h>
|
||||
#include <extra2d/extra2d.h>
|
||||
|
||||
using namespace easy2d;
|
||||
using namespace extra2d;
|
||||
|
||||
int main()
|
||||
{
|
||||
|
|
@ -334,9 +171,9 @@ int main()
|
|||
|
||||
// 配置应用
|
||||
AppConfig config;
|
||||
config.title = "Hello Easy2D";
|
||||
config.width = 800;
|
||||
config.height = 600;
|
||||
config.title = "Hello Extra2D";
|
||||
config.width = 1280;
|
||||
config.height = 720;
|
||||
config.vsync = true;
|
||||
|
||||
// 初始化应用
|
||||
|
|
@ -351,11 +188,11 @@ int main()
|
|||
scene->setBackgroundColor(Color(0.1f, 0.1f, 0.15f, 1.0f));
|
||||
|
||||
// 创建文本节点
|
||||
auto text = Text::create("Hello, Easy2D v3.0!");
|
||||
text->setPosition(Vec2(400, 300));
|
||||
auto text = Text::create("Hello, Extra2D on Switch!");
|
||||
text->setPosition(Vec2(640, 360));
|
||||
text->setAnchor(Vec2(0.5f, 0.5f));
|
||||
text->setTextColor(Color(1.0f, 0.5f, 0.2f, 1.0f));
|
||||
text->setFontSize(32);
|
||||
text->setFontSize(48);
|
||||
|
||||
// 添加动画效果
|
||||
text->runAction(makePtr<Repeat>(
|
||||
|
|
@ -381,440 +218,147 @@ int main()
|
|||
}
|
||||
```
|
||||
|
||||
### 脚本系统示例
|
||||
|
||||
```nut
|
||||
// player_controller.nut - 角色控制器脚本
|
||||
// 使用 WASD 控制角色移动和动画
|
||||
|
||||
local Direction = {
|
||||
Down = 0, // 向下走 - 帧 0-3
|
||||
Left = 1, // 向左走 - 帧 4-7
|
||||
Right = 2, // 向右走 - 帧 8-11
|
||||
Up = 3 // 向上走 - 帧 12-15
|
||||
}
|
||||
|
||||
return {
|
||||
character = null
|
||||
currentDir = Direction.Down
|
||||
isMoving = false
|
||||
moveSpeed = 150.0
|
||||
|
||||
function onEnter(node) {
|
||||
// 创建动画精灵
|
||||
character = AnimatedSprite.createFromGrid(
|
||||
"player.png", 96, 96, 125.0, 16)
|
||||
|
||||
// 设置初始帧范围(向下走:帧 0-3)
|
||||
character.setFrameRange(0, 3)
|
||||
character.setPosition(450.0, 300.0)
|
||||
|
||||
node.addChild(character)
|
||||
}
|
||||
|
||||
function onUpdate(node, dt) {
|
||||
isMoving = false
|
||||
|
||||
// 处理输入
|
||||
if (Input.isKeyDown(Key.W)) {
|
||||
moveCharacter(Direction.Up, dt)
|
||||
} else if (Input.isKeyDown(Key.S)) {
|
||||
moveCharacter(Direction.Down, dt)
|
||||
} else if (Input.isKeyDown(Key.A)) {
|
||||
moveCharacter(Direction.Left, dt)
|
||||
} else if (Input.isKeyDown(Key.D)) {
|
||||
moveCharacter(Direction.Right, dt)
|
||||
}
|
||||
|
||||
// 停止移动时暂停动画
|
||||
if (!isMoving && character.isPlaying()) {
|
||||
character.pause()
|
||||
}
|
||||
}
|
||||
|
||||
function moveCharacter(dir, dt) {
|
||||
local frameStart = dir * 4
|
||||
local frameEnd = frameStart + 3
|
||||
|
||||
// 方向改变时切换帧范围
|
||||
if (currentDir != dir) {
|
||||
character.setFrameRange(frameStart, frameEnd)
|
||||
character.setFrameIndex(frameStart)
|
||||
}
|
||||
|
||||
if (!character.isPlaying()) {
|
||||
character.play()
|
||||
}
|
||||
|
||||
currentDir = dir
|
||||
isMoving = true
|
||||
|
||||
// 移动角色
|
||||
local pos = character.getPosition()
|
||||
switch (dir) {
|
||||
case Direction.Down: pos.setY(pos.getY() + moveSpeed * dt); break
|
||||
case Direction.Up: pos.setY(pos.getY() - moveSpeed * dt); break
|
||||
case Direction.Left: pos.setX(pos.getX() - moveSpeed * dt); break
|
||||
case Direction.Right: pos.setX(pos.getX() + moveSpeed * dt); break
|
||||
}
|
||||
character.setPosition(pos.getX(), pos.getY())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ 项目结构
|
||||
|
||||
```
|
||||
Easy2D/
|
||||
├── 📁 Easy2D/ # 引擎核心代码
|
||||
Extra2D/
|
||||
├── 📁 Extra2D/ # 引擎核心代码
|
||||
│ ├── 📁 include/ # 头文件
|
||||
│ │ ├── 📁 easy2d/ # 引擎头文件
|
||||
│ │ │ ├── easy2d.h # 主头文件
|
||||
│ │ │ ├── app/ # 应用管理
|
||||
│ │ │ │ └── application.h
|
||||
│ │ │ ├── action/ # 动作系统
|
||||
│ │ │ │ ├── action.h
|
||||
│ │ │ │ ├── actions.h
|
||||
│ │ │ │ └── ease.h
|
||||
│ │ │ ├── audio/ # 音频系统
|
||||
│ │ │ │ ├── audio_engine.h
|
||||
│ │ │ │ └── sound.h
|
||||
│ │ │ ├── core/ # 核心类型
|
||||
│ │ │ │ ├── types.h
|
||||
│ │ │ │ ├── math_types.h
|
||||
│ │ │ │ ├── color.h
|
||||
│ │ │ │ └── string.h
|
||||
│ │ │ ├── event/ # 事件系统
|
||||
│ │ │ │ ├── event.h
|
||||
│ │ │ │ ├── event_dispatcher.h
|
||||
│ │ │ │ └── input_codes.h
|
||||
│ │ │ ├── graphics/ # 图形渲染
|
||||
│ │ │ │ ├── render_backend.h
|
||||
│ │ │ │ ├── texture.h
|
||||
│ │ │ │ ├── font.h
|
||||
│ │ │ │ ├── camera.h
|
||||
│ │ │ │ └── opengl/ # OpenGL 实现
|
||||
│ │ │ ├── platform/ # 平台抽象
|
||||
│ │ │ │ ├── window.h
|
||||
│ │ │ │ └── input.h
|
||||
│ │ │ ├── resource/ # 资源管理
|
||||
│ │ │ │ └── resource_manager.h
|
||||
│ │ │ ├── scene/ # 场景系统
|
||||
│ │ │ │ ├── node.h
|
||||
│ │ │ │ ├── scene.h
|
||||
│ │ │ │ ├── sprite.h
|
||||
│ │ │ │ ├── text.h
|
||||
│ │ │ │ ├── shape_node.h
|
||||
│ │ │ │ ├── scene_manager.h
|
||||
│ │ │ │ └── transition.h
|
||||
│ │ │ ├── spatial/ # 空间索引
|
||||
│ │ │ │ ├── spatial_manager.h
|
||||
│ │ │ │ ├── quadtree.h
|
||||
│ │ │ │ └── spatial_hash.h
|
||||
│ │ │ ├── script/ # 脚本系统
|
||||
│ │ │ │ ├── script_engine.h
|
||||
│ │ │ │ ├── script_component.h
|
||||
│ │ │ │ └── sq_binding.h
|
||||
│ │ │ ├── ui/ # UI 系统
|
||||
│ │ │ │ ├── widget.h
|
||||
│ │ │ │ └── button.h
|
||||
│ │ │ └── utils/ # 工具库
|
||||
│ │ │ ├── logger.h
|
||||
│ │ │ ├── timer.h
|
||||
│ │ │ ├── data.h
|
||||
│ │ │ └── random.h
|
||||
│ │ ├── 📁 glew/ # GLEW 库
|
||||
│ │ ├── 📁 glfw/ # GLFW 库
|
||||
│ │ ├── 📁 glm/ # GLM 数学库
|
||||
│ │ ├── 📁 spdlog/ # spdlog 日志库
|
||||
│ │ ├── 📁 stb/ # stb 图像库
|
||||
│ │ ├── 📁 miniaudio/ # miniaudio 音频库
|
||||
│ │ └── 📁 simpleini/ # simpleini 配置库
|
||||
│ │ └── 📁 extra2d/ # 引擎头文件
|
||||
│ │ ├── extra2d.h # 主头文件
|
||||
│ │ ├── app/ # 应用管理
|
||||
│ │ ├── action/ # 动作系统
|
||||
│ │ ├── animation/ # 动画系统
|
||||
│ │ ├── audio/ # 音频系统
|
||||
│ │ ├── core/ # 核心类型
|
||||
│ │ ├── effects/ # 特效系统
|
||||
│ │ ├── event/ # 事件系统
|
||||
│ │ ├── graphics/ # 图形渲染
|
||||
│ │ ├── platform/ # 平台抽象
|
||||
│ │ ├── resource/ # 资源管理
|
||||
│ │ ├── scene/ # 场景系统
|
||||
│ │ ├── script/ # 脚本系统
|
||||
│ │ ├── spatial/ # 空间索引
|
||||
│ │ ├── ui/ # UI 系统
|
||||
│ │ └── utils/ # 工具库
|
||||
│ ├── 📁 src/ # 源文件
|
||||
│ │ ├── App/ # 应用实现
|
||||
│ │ ├── Action/ # 动作系统实现
|
||||
│ │ ├── Animation/ # 动画系统实现
|
||||
│ │ ├── Audio/ # 音频系统实现
|
||||
│ │ ├── Core/ # 核心实现
|
||||
│ │ ├── Event/ # 事件系统实现
|
||||
│ │ ├── Graphics/ # 图形渲染实现
|
||||
│ │ ├── Platform/ # 平台实现
|
||||
│ │ ├── Resource/ # 资源管理实现
|
||||
│ │ ├── Scene/ # 场景系统实现
|
||||
│ │ ├── Script/ # 脚本系统实现
|
||||
│ │ ├── Spatial/ # 空间索引实现
|
||||
│ │ ├── UI/ # UI 系统实现
|
||||
│ │ └── Utils/ # 工具库实现
|
||||
│ └── 📁 examples/ # 示例程序
|
||||
│ ├── hello_world/ # Hello World 示例
|
||||
│ ├── animation_demo/ # 精灵动画示例
|
||||
│ ├── script_demo/ # 脚本系统示例
|
||||
│ ├── font_test/ # 字体测试示例
|
||||
│ └── push_box/ # 推箱子游戏示例
|
||||
│ ├── push_box/ # 推箱子游戏
|
||||
│ └── switch_simple_test/ # 简单测试
|
||||
├── 📁 squirrel/ # Squirrel 脚本引擎
|
||||
├── 📁 logo/ # Logo 资源
|
||||
├── 📄 xmake.lua # xmake 构建配置
|
||||
├── 📄 SWITCH_BUILD_GUIDE.md # Switch 构建详细指南
|
||||
├── 📄 LICENSE # MIT 许可证
|
||||
└── 📄 README.md # 本文件
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎮 Switch 特定功能
|
||||
|
||||
### 双模式支持
|
||||
|
||||
```cpp
|
||||
// 检测当前模式
|
||||
if (app.isDocked()) {
|
||||
// 主机模式:可使用更高分辨率
|
||||
config.width = 1920;
|
||||
config.height = 1080;
|
||||
} else {
|
||||
// 掌机模式
|
||||
config.width = 1280;
|
||||
config.height = 720;
|
||||
}
|
||||
```
|
||||
|
||||
### 控制器输入
|
||||
|
||||
```cpp
|
||||
auto& input = app.input();
|
||||
|
||||
// Joy-Con 支持
|
||||
if (input.isKeyDown(KeyCode::ButtonA)) {
|
||||
// A 键按下
|
||||
}
|
||||
|
||||
if (input.isKeyDown(KeyCode::ButtonLeft)) {
|
||||
// 左摇杆向左
|
||||
}
|
||||
```
|
||||
|
||||
### ROMFS 资源加载
|
||||
|
||||
```cpp
|
||||
// 自动从 ROMFS 加载资源
|
||||
auto texture = resources.loadTexture("romfs:/images/player.png");
|
||||
auto sound = audio.loadSound("romfs:/audio/jump.wav");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 API 速查
|
||||
|
||||
### 应用控制
|
||||
|
||||
```cpp
|
||||
// 获取应用实例
|
||||
auto& app = Application::instance();
|
||||
|
||||
// 初始化
|
||||
AppConfig config;
|
||||
config.title = "My Game";
|
||||
config.width = 800;
|
||||
config.height = 600;
|
||||
config.vsync = true;
|
||||
app.init(config);
|
||||
|
||||
// 运行主循环
|
||||
app.run();
|
||||
|
||||
// 状态控制
|
||||
app.pause();
|
||||
app.resume();
|
||||
app.quit();
|
||||
|
||||
// 进入场景
|
||||
app.enterScene(makePtr<MyScene>());
|
||||
app.enterScene(makePtr<MyScene>(), makePtr<FadeTransition>(1.0f));
|
||||
|
||||
// 获取子系统
|
||||
auto& input = app.input();
|
||||
auto& audio = app.audio();
|
||||
auto& resources = app.resources();
|
||||
auto& timers = app.timers();
|
||||
```
|
||||
|
||||
### 场景管理
|
||||
|
||||
```cpp
|
||||
// 创建场景
|
||||
auto scene = makePtr<Scene>();
|
||||
scene->setBackgroundColor(Color(0.1f, 0.1f, 0.2f, 1.0f));
|
||||
|
||||
// 场景属性
|
||||
scene->setViewportSize(800, 600);
|
||||
scene->pause();
|
||||
scene->resume();
|
||||
|
||||
// 空间索引
|
||||
scene->setSpatialIndexingEnabled(true);
|
||||
auto nodes = scene->queryNodesInArea(Rect(0, 0, 100, 100));
|
||||
auto collisions = scene->queryCollisions();
|
||||
app.enterScene(scene);
|
||||
app.enterScene(scene, makePtr<FadeTransition>(1.0f));
|
||||
```
|
||||
|
||||
### 节点操作
|
||||
|
||||
```cpp
|
||||
// 创建节点
|
||||
auto node = makePtr<Node>();
|
||||
auto sprite = Sprite::create(texture);
|
||||
auto text = Text::create("Hello");
|
||||
|
||||
// 变换属性
|
||||
node->setPosition(Vec2(100, 200));
|
||||
node->setPosition(100, 200);
|
||||
node->setRotation(45.0f);
|
||||
node->setScale(Vec2(2.0f, 2.0f));
|
||||
node->setScale(2.0f);
|
||||
node->setAnchor(Vec2(0.5f, 0.5f));
|
||||
node->setOpacity(0.8f);
|
||||
node->setVisible(true);
|
||||
node->setZOrder(10);
|
||||
|
||||
// 层级管理
|
||||
parent->addChild(child);
|
||||
parent->removeChild(child);
|
||||
child->removeFromParent();
|
||||
auto found = parent->getChildByName("player");
|
||||
auto found = parent->getChildByTag(100);
|
||||
|
||||
// 世界变换
|
||||
auto worldPos = node->convertToWorldSpace(Vec2(0, 0));
|
||||
auto localPos = node->convertToNodeSpace(worldPos);
|
||||
auto transform = node->getWorldTransform();
|
||||
|
||||
// 空间索引
|
||||
node->setSpatialIndexed(true);
|
||||
node->updateSpatialIndex();
|
||||
auto bounds = node->getBoundingBox();
|
||||
sprite->setPosition(Vec2(100, 200));
|
||||
sprite->setRotation(45.0f);
|
||||
sprite->runAction(makePtr<MoveTo>(1.0f, Vec2(200, 300)));
|
||||
```
|
||||
|
||||
### 动作系统
|
||||
### 动画系统
|
||||
|
||||
```cpp
|
||||
// 创建动作
|
||||
auto move = makePtr<MoveTo>(1.0f, Vec2(100, 200));
|
||||
auto scale = makePtr<ScaleTo>(0.5f, Vec2(2.0f, 2.0f));
|
||||
auto rotate = makePtr<RotateBy>(1.0f, 90.0f);
|
||||
auto fade = makePtr<FadeIn>(0.5f);
|
||||
auto jump = makePtr<JumpBy>(1.0f, Vec2(100, 0), 50.0f, 3);
|
||||
// 精灵动画
|
||||
auto anim = AnimatedSprite::createFromGrid(
|
||||
"player.png", 96, 96, 125.0f, 16);
|
||||
anim->setFrameRange(0, 3);
|
||||
anim->play();
|
||||
|
||||
// 组合动作
|
||||
auto sequence = makePtr<Sequence>(std::vector<Ptr<Action>>{
|
||||
move, scale, rotate
|
||||
});
|
||||
auto spawn = makePtr<Spawn>(std::vector<Ptr<Action>>{
|
||||
move, fade
|
||||
});
|
||||
auto repeat = makePtr<Repeat>(sequence);
|
||||
auto repeatForever = makePtr<RepeatForever>(rotate);
|
||||
|
||||
// 缓动
|
||||
auto easeMove = makePtr<EaseInOut>(move, 2.0f);
|
||||
|
||||
// 运行动作
|
||||
node->runAction(action);
|
||||
node->stopAllActions();
|
||||
node->stopAction(action);
|
||||
node->stopActionByTag(1);
|
||||
// 动作动画
|
||||
node->runAction(makePtr<Sequence>(
|
||||
makePtr<MoveTo>(1.0f, Vec2(100, 200)),
|
||||
makePtr<ScaleTo>(0.5f, Vec2(2.0f, 2.0f))
|
||||
));
|
||||
```
|
||||
|
||||
### 输入处理
|
||||
|
||||
```cpp
|
||||
auto& input = app.input();
|
||||
|
||||
// 键盘
|
||||
if (input.isKeyDown(KeyCode::Space)) {}
|
||||
if (input.isKeyPressed(KeyCode::Enter)) {}
|
||||
if (input.isKeyReleased(KeyCode::Escape)) {}
|
||||
|
||||
// 鼠标
|
||||
if (input.isMouseDown(MouseButton::Left)) {}
|
||||
if (input.isMousePressed(MouseButton::Right)) {}
|
||||
if (input.isKeyDown(KeyCode::ButtonA)) {}
|
||||
if (input.isKeyPressed(KeyCode::ButtonB)) {}
|
||||
auto pos = input.getMousePosition();
|
||||
auto delta = input.getMouseDelta();
|
||||
auto scroll = input.getMouseScrollDelta();
|
||||
|
||||
// 鼠标控制
|
||||
input.setMousePosition(Vec2(400, 300));
|
||||
input.setMouseVisible(false);
|
||||
input.setMouseLocked(true);
|
||||
```
|
||||
|
||||
### 音频播放
|
||||
|
||||
```cpp
|
||||
auto& audio = app.audio();
|
||||
|
||||
// 加载音效
|
||||
auto sound = audio.loadSound("jump.wav");
|
||||
auto namedSound = audio.loadSound("jump", "jump.wav");
|
||||
|
||||
// 播放控制
|
||||
sound->play();
|
||||
sound->pause();
|
||||
sound->resume();
|
||||
sound->stop();
|
||||
|
||||
// 属性
|
||||
sound->setVolume(0.8f);
|
||||
sound->setLooping(true);
|
||||
sound->setPitch(1.2f);
|
||||
|
||||
// 全局控制
|
||||
audio.setMasterVolume(0.5f);
|
||||
audio.pauseAll();
|
||||
audio.resumeAll();
|
||||
audio.stopAll();
|
||||
```
|
||||
|
||||
### 资源管理
|
||||
|
||||
```cpp
|
||||
auto& resources = app.resources();
|
||||
|
||||
// 添加搜索路径
|
||||
resources.addSearchPath("assets");
|
||||
resources.addSearchPath("assets/images");
|
||||
|
||||
// 加载纹理
|
||||
auto texture = resources.loadTexture("player.png");
|
||||
auto texture = resources.loadTexture("atlas.png", Rect(0, 0, 32, 32));
|
||||
|
||||
// 加载字体
|
||||
auto font = resources.loadFont("arial.ttf", 16);
|
||||
|
||||
// 创建精灵
|
||||
auto sprite = Sprite::create(texture);
|
||||
```
|
||||
|
||||
### UI 系统
|
||||
|
||||
```cpp
|
||||
// 创建按钮
|
||||
auto button = Button::create();
|
||||
button->setText("Click Me");
|
||||
button->setPosition(Vec2(400, 300));
|
||||
button->setTextColor(Color::White);
|
||||
button->setFontSize(24);
|
||||
|
||||
// 背景设置
|
||||
button->setBackgroundColor(
|
||||
Color(0.2f, 0.4f, 0.8f, 1.0f), // normal
|
||||
Color(0.3f, 0.5f, 0.9f, 1.0f), // hover
|
||||
Color(0.1f, 0.3f, 0.7f, 1.0f) // pressed
|
||||
);
|
||||
button->setCornerRadius(8.0f);
|
||||
|
||||
// 点击回调
|
||||
button->setOnClick([]() {
|
||||
Logger::info("Button clicked!");
|
||||
});
|
||||
|
||||
scene->addChild(button);
|
||||
```
|
||||
|
||||
### 定时器
|
||||
|
||||
```cpp
|
||||
auto& timers = app.timers();
|
||||
|
||||
// 单次定时器
|
||||
auto id = timers.addTimer(2.0f, []() {
|
||||
Logger::info("Timer fired!");
|
||||
});
|
||||
|
||||
// 重复定时器
|
||||
auto id = timers.addRepeatingTimer(1.0f, []() {
|
||||
Logger::info("Every second!");
|
||||
});
|
||||
|
||||
// 控制
|
||||
timers.pauseTimer(id);
|
||||
timers.resumeTimer(id);
|
||||
timers.cancelTimer(id);
|
||||
```
|
||||
|
||||
### 数据持久化
|
||||
|
||||
```cpp
|
||||
// 保存数据
|
||||
Data data;
|
||||
data.setInt("score", 1000);
|
||||
data.setFloat("volume", 0.8f);
|
||||
data.setBool("fullscreen", true);
|
||||
data.setString("player", "Alice");
|
||||
data.save("savegame.dat");
|
||||
|
||||
// 加载数据
|
||||
Data data;
|
||||
if (data.load("savegame.dat")) {
|
||||
int score = data.getInt("score", 0);
|
||||
float volume = data.getFloat("volume", 1.0f);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -823,38 +367,21 @@ if (data.load("savegame.dat")) {
|
|||
|
||||
| 技术 | 用途 | 版本 |
|
||||
|:----:|:-----|:----:|
|
||||
| OpenGL | 2D 图形渲染 | 3.3+ |
|
||||
| OpenGL | 2D 图形渲染 | ES 3.0+ |
|
||||
| GLFW | 窗口和输入管理 | 3.3+ |
|
||||
| GLEW | OpenGL 扩展加载 | 2.1+ |
|
||||
| GLM | 数学库 | 0.9.9+ |
|
||||
| miniaudio | 音频播放 | 最新版 |
|
||||
| spdlog | 日志系统 | 最新版 |
|
||||
| stb_image | 图像加载 | 最新版 |
|
||||
| Squirrel | 脚本引擎 | 3.2+ |
|
||||
| xmake | 构建系统 | 2.5+ |
|
||||
|
||||
---
|
||||
|
||||
## 📦 使用 xmake 集成 Easy2D
|
||||
## 📖 相关文档
|
||||
|
||||
在你的项目中使用 Easy2D:
|
||||
|
||||
```lua
|
||||
-- xmake.lua
|
||||
add_rules("mode.debug", "mode.release")
|
||||
|
||||
-- 添加 Easy2D 仓库
|
||||
add_repositories("easy2d https://github.com/ChestnutYueyue/xmake-repo")
|
||||
|
||||
-- 添加依赖
|
||||
add_requires("easy2d")
|
||||
|
||||
target("mygame")
|
||||
set_kind("binary")
|
||||
set_languages("c++17")
|
||||
add_files("src/*.cpp")
|
||||
add_packages("easy2d")
|
||||
target_end()
|
||||
```
|
||||
- [Switch 构建指南](./SWITCH_BUILD_GUIDE.md) - 详细的 Switch 平台构建教程
|
||||
- [迁移完成记录](./SWITCH_MIGRATION_COMPLETE.md) - 项目迁移历史记录
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -866,11 +393,11 @@ target_end()
|
|||
|
||||
## 📄 许可证
|
||||
|
||||
Easy2D 使用 [MIT](LICENSE) 许可证。
|
||||
Extra2D 使用 [MIT](LICENSE) 许可证。
|
||||
|
||||
---
|
||||
|
||||
## 联系方式
|
||||
|
||||
- QQ群: 608406540
|
||||
- GitHub: https://github.com/Easy2D/Easy2D
|
||||
- GitHub Issues: https://github.com/ChestnutYueyue/extra2d/issues
|
||||
- 作者: [ChestnutYueyue](https://github.com/ChestnutYueyue)
|
||||
|
|
|
|||
BIN
logo/logo.ico
|
Before Width: | Height: | Size: 32 KiB |
BIN
logo/logo.png
|
Before Width: | Height: | Size: 8.3 KiB |
|
|
@ -0,0 +1,99 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<defs>
|
||||
<!-- 主渐变 -->
|
||||
<linearGradient id="mainGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#E60012;stop-opacity:1" />
|
||||
<stop offset="50%" style="stop-color:#FF4757;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#00C3E3;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
|
||||
<!-- 蓝色渐变 -->
|
||||
<linearGradient id="blueGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#00C3E3;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#0099CC;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
|
||||
<!-- 阴影 -->
|
||||
<filter id="shadow" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feDropShadow dx="4" dy="8" stdDeviation="12" flood-color="rgba(0,0,0,0.25)"/>
|
||||
</filter>
|
||||
|
||||
<!-- 内阴影 -->
|
||||
<filter id="innerShadow">
|
||||
<feOffset dx="0" dy="2"/>
|
||||
<feGaussianBlur stdDeviation="3" result="offset-blur"/>
|
||||
<feComposite operator="out" in="SourceGraphic" in2="offset-blur" result="inverse"/>
|
||||
<feFlood flood-color="rgba(0,0,0,0.2)" result="color"/>
|
||||
<feComposite operator="in" in="color" in2="inverse" result="shadow"/>
|
||||
<feComposite operator="over" in="shadow" in2="SourceGraphic"/>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<!-- 背景 - 六边形风格 -->
|
||||
<g transform="translate(256, 256)" filter="url(#shadow)">
|
||||
<!-- 主体六边形 -->
|
||||
<path d="M-200 -100 L-200 100 L0 200 L200 100 L200 -100 L0 -200 Z"
|
||||
fill="url(#mainGradient)" />
|
||||
|
||||
<!-- 内部装饰线条 - 左侧红 -->
|
||||
<path d="M-180 -90 L-180 90 L-60 150 L-60 -150 Z"
|
||||
fill="#E60012" opacity="0.3"/>
|
||||
|
||||
<!-- 内部装饰线条 - 右侧蓝 -->
|
||||
<path d="M60 -150 L60 150 L180 90 L180 -90 Z"
|
||||
fill="#00C3E3" opacity="0.3"/>
|
||||
|
||||
<!-- 中心白色区域 -->
|
||||
<path d="M-120 -60 L-120 60 L0 120 L120 60 L120 -60 L0 -120 Z"
|
||||
fill="white" opacity="0.95"/>
|
||||
|
||||
<!-- 边框线条 -->
|
||||
<path d="M-200 -100 L-200 100 L0 200 L200 100 L200 -100 L0 -200 Z"
|
||||
fill="none" stroke="rgba(255,255,255,0.3)" stroke-width="4"/>
|
||||
</g>
|
||||
|
||||
<!-- 文字 E2D -->
|
||||
<g transform="translate(256, 270)">
|
||||
<!-- E -->
|
||||
<text x="-85" y="25"
|
||||
font-family="Arial Black, Arial, sans-serif"
|
||||
font-size="130"
|
||||
font-weight="900"
|
||||
fill="#2D3436"
|
||||
style="letter-spacing: -5px;">E</text>
|
||||
|
||||
<!-- 2 -->
|
||||
<text x="5" y="25"
|
||||
font-family="Arial Black, Arial, sans-serif"
|
||||
font-size="110"
|
||||
font-weight="900"
|
||||
fill="#E60012">2</text>
|
||||
|
||||
<!-- D -->
|
||||
<text x="75" y="25"
|
||||
font-family="Arial Black, Arial, sans-serif"
|
||||
font-size="110"
|
||||
font-weight="900"
|
||||
fill="#00C3E3">D</text>
|
||||
</g>
|
||||
|
||||
<!-- 顶部高光 -->
|
||||
<g transform="translate(256, 256)" opacity="0.4">
|
||||
<path d="M-180 -90 L0 -180 L180 -90 L160 -80 L0 -160 L-160 -80 Z"
|
||||
fill="white"/>
|
||||
</g>
|
||||
|
||||
<!-- 像素装饰 - 左下角 -->
|
||||
<g transform="translate(80, 400)" opacity="0.6">
|
||||
<rect x="0" y="0" width="16" height="16" fill="#E60012" rx="2"/>
|
||||
<rect x="20" y="0" width="16" height="16" fill="#E60012" rx="2"/>
|
||||
<rect x="40" y="0" width="16" height="16" fill="#E60012" rx="2"/>
|
||||
</g>
|
||||
|
||||
<!-- 像素装饰 - 右下角 -->
|
||||
<g transform="translate(376, 400)" opacity="0.6">
|
||||
<rect x="0" y="0" width="16" height="16" fill="#00C3E3" rx="2"/>
|
||||
<rect x="20" y="0" width="16" height="16" fill="#00C3E3" rx="2"/>
|
||||
<rect x="40" y="0" width="16" height="16" fill="#00C3E3" rx="2"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 7.3 KiB |
|
|
@ -0,0 +1,88 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 300">
|
||||
<defs>
|
||||
<!-- 主渐变 -->
|
||||
<linearGradient id="mainGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#E60012;stop-opacity:1" />
|
||||
<stop offset="50%" style="stop-color:#FF4757;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#00C3E3;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
|
||||
<!-- 阴影 -->
|
||||
<filter id="shadow" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feDropShadow dx="3" dy="6" stdDeviation="8" flood-color="rgba(0,0,0,0.3)"/>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<!-- Logo 图标 - 六边形 -->
|
||||
<g transform="translate(100, 150)" filter="url(#shadow)">
|
||||
<!-- 主体六边形 -->
|
||||
<path d="M-60 -35 L-60 35 L0 70 L60 35 L60 -35 L0 -70 Z"
|
||||
fill="url(#mainGradient)" />
|
||||
|
||||
<!-- 内部白色区域 -->
|
||||
<path d="M-40 -23 L-40 23 L0 46 L40 23 L40 -23 L0 -46 Z"
|
||||
fill="white" opacity="0.95"/>
|
||||
|
||||
<!-- 边框 -->
|
||||
<path d="M-60 -35 L-60 35 L0 70 L60 35 L60 -35 L0 -70 Z"
|
||||
fill="none" stroke="rgba(255,255,255,0.4)" stroke-width="2"/>
|
||||
|
||||
<!-- 文字 E -->
|
||||
<text x="-22" y="18"
|
||||
font-family="Arial Black, Arial, sans-serif"
|
||||
font-size="45"
|
||||
font-weight="900"
|
||||
fill="#2D3436">E</text>
|
||||
|
||||
<!-- 小 2 -->
|
||||
<text x="8" y="12"
|
||||
font-family="Arial Black, Arial, sans-serif"
|
||||
font-size="24"
|
||||
font-weight="900"
|
||||
fill="#E60012">2</text>
|
||||
|
||||
<!-- 小 D -->
|
||||
<text x="8" y="32"
|
||||
font-family="Arial Black, Arial, sans-serif"
|
||||
font-size="20"
|
||||
font-weight="900"
|
||||
fill="#00C3E3">D</text>
|
||||
</g>
|
||||
|
||||
<!-- Extra2D 文字 -->
|
||||
<g transform="translate(200, 145)">
|
||||
<!-- Extra -->
|
||||
<text x="0" y="20"
|
||||
font-family="Arial, sans-serif"
|
||||
font-size="70"
|
||||
font-weight="800"
|
||||
fill="white">Extra</text>
|
||||
|
||||
<!-- 2 - 红色 -->
|
||||
<text x="215" y="20"
|
||||
font-family="Arial, sans-serif"
|
||||
font-size="70"
|
||||
font-weight="800"
|
||||
fill="#E60012">2</text>
|
||||
|
||||
<!-- D - 蓝色 -->
|
||||
<text x="265" y="20"
|
||||
font-family="Arial, sans-serif"
|
||||
font-size="70"
|
||||
font-weight="800"
|
||||
fill="#00C3E3">D</text>
|
||||
</g>
|
||||
|
||||
<!-- 副标题 -->
|
||||
<g transform="translate(200, 190)">
|
||||
<text x="0" y="0"
|
||||
font-family="Arial, sans-serif"
|
||||
font-size="18"
|
||||
font-weight="500"
|
||||
fill="#888"
|
||||
letter-spacing="4">SWITCH GAME ENGINE</text>
|
||||
</g>
|
||||
|
||||
<!-- 装饰线 -->
|
||||
<line x1="200" y1="165" x2="720" y2="165" stroke="rgba(255,255,255,0.1)" stroke-width="1"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
|
@ -0,0 +1,88 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 300">
|
||||
<defs>
|
||||
<!-- 主渐变 -->
|
||||
<linearGradient id="mainGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#E60012;stop-opacity:1" />
|
||||
<stop offset="50%" style="stop-color:#FF4757;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#00C3E3;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
|
||||
<!-- 阴影 -->
|
||||
<filter id="shadow" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feDropShadow dx="3" dy="6" stdDeviation="8" flood-color="rgba(0,0,0,0.2)"/>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<!-- Logo 图标 - 六边形 -->
|
||||
<g transform="translate(100, 150)" filter="url(#shadow)">
|
||||
<!-- 主体六边形 -->
|
||||
<path d="M-60 -35 L-60 35 L0 70 L60 35 L60 -35 L0 -70 Z"
|
||||
fill="url(#mainGradient)" />
|
||||
|
||||
<!-- 内部白色区域 -->
|
||||
<path d="M-40 -23 L-40 23 L0 46 L40 23 L40 -23 L0 -46 Z"
|
||||
fill="white" opacity="0.95"/>
|
||||
|
||||
<!-- 边框 -->
|
||||
<path d="M-60 -35 L-60 35 L0 70 L60 35 L60 -35 L0 -70 Z"
|
||||
fill="none" stroke="rgba(255,255,255,0.4)" stroke-width="2"/>
|
||||
|
||||
<!-- 文字 E -->
|
||||
<text x="-22" y="18"
|
||||
font-family="Arial Black, Arial, sans-serif"
|
||||
font-size="45"
|
||||
font-weight="900"
|
||||
fill="#2D3436">E</text>
|
||||
|
||||
<!-- 小 2 -->
|
||||
<text x="8" y="12"
|
||||
font-family="Arial Black, Arial, sans-serif"
|
||||
font-size="24"
|
||||
font-weight="900"
|
||||
fill="#E60012">2</text>
|
||||
|
||||
<!-- 小 D -->
|
||||
<text x="8" y="32"
|
||||
font-family="Arial Black, Arial, sans-serif"
|
||||
font-size="20"
|
||||
font-weight="900"
|
||||
fill="#00C3E3">D</text>
|
||||
</g>
|
||||
|
||||
<!-- Extra2D 文字 -->
|
||||
<g transform="translate(200, 145)">
|
||||
<!-- Extra - 深色 -->
|
||||
<text x="0" y="20"
|
||||
font-family="Arial, sans-serif"
|
||||
font-size="70"
|
||||
font-weight="800"
|
||||
fill="#2D3436">Extra</text>
|
||||
|
||||
<!-- 2 - 红色 -->
|
||||
<text x="215" y="20"
|
||||
font-family="Arial, sans-serif"
|
||||
font-size="70"
|
||||
font-weight="800"
|
||||
fill="#E60012">2</text>
|
||||
|
||||
<!-- D - 蓝色 -->
|
||||
<text x="265" y="20"
|
||||
font-family="Arial, sans-serif"
|
||||
font-size="70"
|
||||
font-weight="800"
|
||||
fill="#00C3E3">D</text>
|
||||
</g>
|
||||
|
||||
<!-- 副标题 -->
|
||||
<g transform="translate(200, 190)">
|
||||
<text x="0" y="0"
|
||||
font-family="Arial, sans-serif"
|
||||
font-size="18"
|
||||
font-weight="500"
|
||||
fill="#636E72"
|
||||
letter-spacing="4">SWITCH GAME ENGINE</text>
|
||||
</g>
|
||||
|
||||
<!-- 装饰线 -->
|
||||
<line x1="200" y1="165" x2="720" y2="165" stroke="rgba(0,0,0,0.1)" stroke-width="1"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
|
|
@ -0,0 +1,362 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Extra2D Logo Preview - Geometric Design</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif;
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
|
||||
min-height: 100vh;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: white;
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 10px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
text-align: center;
|
||||
color: #888;
|
||||
margin-bottom: 50px;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.preview-section {
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
color: white;
|
||||
font-size: 1.3rem;
|
||||
margin-bottom: 20px;
|
||||
padding-left: 15px;
|
||||
border-left: 4px solid #E60012;
|
||||
}
|
||||
|
||||
.preview-box {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 20px;
|
||||
padding: 40px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 300px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.preview-box.light {
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #e4e8ec 100%);
|
||||
}
|
||||
|
||||
.preview-box.dark {
|
||||
background: linear-gradient(135deg, #2d3436 0%, #1a1a2e 100%);
|
||||
}
|
||||
|
||||
.icon-preview {
|
||||
width: 220px;
|
||||
height: 220px;
|
||||
}
|
||||
|
||||
.text-preview {
|
||||
width: 100%;
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.color-info {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 30px;
|
||||
margin-top: 30px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.color-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.color-dot {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid rgba(255,255,255,0.3);
|
||||
}
|
||||
|
||||
.features {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 20px;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 15px;
|
||||
padding: 25px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.feature-card h3 {
|
||||
color: white;
|
||||
margin-bottom: 10px;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.feature-card p {
|
||||
color: #888;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.size-preview {
|
||||
display: flex;
|
||||
gap: 40px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.size-item {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.size-label {
|
||||
color: #888;
|
||||
font-size: 0.9rem;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.reference-logos {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 40px;
|
||||
margin-top: 30px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.ref-item {
|
||||
text-align: center;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.ref-item img {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
margin-bottom: 10px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Extra2D Logo Design</h1>
|
||||
<p class="subtitle">Geometric Style - Nintendo Switch Game Engine</p>
|
||||
|
||||
<!-- 主图标预览 -->
|
||||
<div class="preview-section">
|
||||
<h2 class="section-title">主图标 (App Icon) - 六边形设计</h2>
|
||||
<div class="preview-box dark">
|
||||
<svg class="icon-preview" viewBox="0 0 512 512">
|
||||
<defs>
|
||||
<linearGradient id="mainGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#E60012;stop-opacity:1" />
|
||||
<stop offset="50%" style="stop-color:#FF4757;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#00C3E3;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
<filter id="shadow" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feDropShadow dx="4" dy="8" stdDeviation="12" flood-color="rgba(0,0,0,0.25)"/>
|
||||
</filter>
|
||||
</defs>
|
||||
<g transform="translate(256, 256)" filter="url(#shadow)">
|
||||
<path d="M-200 -100 L-200 100 L0 200 L200 100 L200 -100 L0 -200 Z" fill="url(#mainGradient)" />
|
||||
<path d="M-180 -90 L-180 90 L-60 150 L-60 -150 Z" fill="#E60012" opacity="0.3"/>
|
||||
<path d="M60 -150 L60 150 L180 90 L180 -90 Z" fill="#00C3E3" opacity="0.3"/>
|
||||
<path d="M-120 -60 L-120 60 L0 120 L120 60 L120 -60 L0 -120 Z" fill="white" opacity="0.95"/>
|
||||
<path d="M-200 -100 L-200 100 L0 200 L200 100 L200 -100 L0 -200 Z" fill="none" stroke="rgba(255,255,255,0.3)" stroke-width="4"/>
|
||||
</g>
|
||||
<g transform="translate(256, 270)">
|
||||
<text x="-85" y="25" font-family="Arial Black, Arial, sans-serif" font-size="130" font-weight="900" fill="#2D3436" style="letter-spacing: -5px;">E</text>
|
||||
<text x="5" y="25" font-family="Arial Black, Arial, sans-serif" font-size="110" font-weight="900" fill="#E60012">2</text>
|
||||
<text x="75" y="25" font-family="Arial Black, Arial, sans-serif" font-size="110" font-weight="900" fill="#00C3E3">D</text>
|
||||
</g>
|
||||
<g transform="translate(256, 256)" opacity="0.4">
|
||||
<path d="M-180 -90 L0 -180 L180 -90 L160 -80 L0 -160 L-160 -80 Z" fill="white"/>
|
||||
</g>
|
||||
<g transform="translate(80, 400)" opacity="0.6">
|
||||
<rect x="0" y="0" width="16" height="16" fill="#E60012" rx="2"/>
|
||||
<rect x="20" y="0" width="16" height="16" fill="#E60012" rx="2"/>
|
||||
<rect x="40" y="0" width="16" height="16" fill="#E60012" rx="2"/>
|
||||
</g>
|
||||
<g transform="translate(376, 400)" opacity="0.6">
|
||||
<rect x="0" y="0" width="16" height="16" fill="#00C3E3" rx="2"/>
|
||||
<rect x="20" y="0" width="16" height="16" fill="#00C3E3" rx="2"/>
|
||||
<rect x="40" y="0" width="16" height="16" fill="#00C3E3" rx="2"/>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="size-preview" style="margin-top: 30px;">
|
||||
<div class="size-item">
|
||||
<svg width="64" height="64" viewBox="0 0 512 512">
|
||||
<g transform="translate(256, 256)">
|
||||
<path d="M-200 -100 L-200 100 L0 200 L200 100 L200 -100 L0 -200 Z" fill="url(#mainGradient)" />
|
||||
<path d="M-120 -60 L-120 60 L0 120 L120 60 L120 -60 L0 -120 Z" fill="white" opacity="0.95"/>
|
||||
</g>
|
||||
<text x="256" y="280" text-anchor="middle" font-family="Arial Black, Arial, sans-serif" font-size="130" font-weight="900" fill="#2D3436">E</text>
|
||||
</svg>
|
||||
<div class="size-label">64x64</div>
|
||||
</div>
|
||||
<div class="size-item">
|
||||
<svg width="128" height="128" viewBox="0 0 512 512">
|
||||
<g transform="translate(256, 256)">
|
||||
<path d="M-200 -100 L-200 100 L0 200 L200 100 L200 -100 L0 -200 Z" fill="url(#mainGradient)" />
|
||||
<path d="M-120 -60 L-120 60 L0 120 L120 60 L120 -60 L0 -120 Z" fill="white" opacity="0.95"/>
|
||||
</g>
|
||||
<text x="256" y="280" text-anchor="middle" font-family="Arial Black, Arial, sans-serif" font-size="130" font-weight="900" fill="#2D3436">E</text>
|
||||
</svg>
|
||||
<div class="size-label">128x128</div>
|
||||
</div>
|
||||
<div class="size-item">
|
||||
<svg width="256" height="256" viewBox="0 0 512 512">
|
||||
<g transform="translate(256, 256)">
|
||||
<path d="M-200 -100 L-200 100 L0 200 L200 100 L200 -100 L0 -200 Z" fill="url(#mainGradient)" />
|
||||
<path d="M-120 -60 L-120 60 L0 120 L120 60 L120 -60 L0 -120 Z" fill="white" opacity="0.95"/>
|
||||
</g>
|
||||
<text x="256" y="280" text-anchor="middle" font-family="Arial Black, Arial, sans-serif" font-size="130" font-weight="900" fill="#2D3436">E</text>
|
||||
</svg>
|
||||
<div class="size-label">256x256</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文字 Logo 预览 -->
|
||||
<div class="preview-section">
|
||||
<h2 class="section-title">文字 Logo - 深色背景</h2>
|
||||
<div class="preview-box dark">
|
||||
<svg class="text-preview" viewBox="0 0 800 300">
|
||||
<defs>
|
||||
<linearGradient id="textGradient1" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#E60012;stop-opacity:1" />
|
||||
<stop offset="50%" style="stop-color:#FF4757;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#00C3E3;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
<filter id="textShadow" x="-50%" y="-50%" width="200%" height="200%">
|
||||
<feDropShadow dx="3" dy="6" stdDeviation="8" flood-color="rgba(0,0,0,0.3)"/>
|
||||
</filter>
|
||||
</defs>
|
||||
<g transform="translate(100, 150)" filter="url(#textShadow)">
|
||||
<path d="M-60 -35 L-60 35 L0 70 L60 35 L60 -35 L0 -70 Z" fill="url(#textGradient1)" />
|
||||
<path d="M-40 -23 L-40 23 L0 46 L40 23 L40 -23 L0 -46 Z" fill="white" opacity="0.95"/>
|
||||
<path d="M-60 -35 L-60 35 L0 70 L60 35 L60 -35 L0 -70 Z" fill="none" stroke="rgba(255,255,255,0.4)" stroke-width="2"/>
|
||||
<text x="-22" y="18" font-family="Arial Black, Arial, sans-serif" font-size="45" font-weight="900" fill="#2D3436">E</text>
|
||||
<text x="8" y="12" font-family="Arial Black, Arial, sans-serif" font-size="24" font-weight="900" fill="#E60012">2</text>
|
||||
<text x="8" y="32" font-family="Arial Black, Arial, sans-serif" font-size="20" font-weight="900" fill="#00C3E3">D</text>
|
||||
</g>
|
||||
<g transform="translate(200, 145)">
|
||||
<text x="0" y="20" font-family="Arial, sans-serif" font-size="70" font-weight="800" fill="white">Extra</text>
|
||||
<text x="215" y="20" font-family="Arial, sans-serif" font-size="70" font-weight="800" fill="#E60012">2</text>
|
||||
<text x="265" y="20" font-family="Arial, sans-serif" font-size="70" font-weight="800" fill="#00C3E3">D</text>
|
||||
</g>
|
||||
<g transform="translate(200, 190)">
|
||||
<text x="0" y="0" font-family="Arial, sans-serif" font-size="18" font-weight="500" fill="#888" letter-spacing="4">SWITCH GAME ENGINE</text>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="preview-section">
|
||||
<h2 class="section-title">文字 Logo - 浅色背景</h2>
|
||||
<div class="preview-box light">
|
||||
<svg class="text-preview" viewBox="0 0 800 300">
|
||||
<defs>
|
||||
<linearGradient id="textGradient2" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#E60012;stop-opacity:1" />
|
||||
<stop offset="50%" style="stop-color:#FF4757;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#00C3E3;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g transform="translate(100, 150)" filter="url(#shadow)">
|
||||
<path d="M-60 -35 L-60 35 L0 70 L60 35 L60 -35 L0 -70 Z" fill="url(#textGradient2)" />
|
||||
<path d="M-40 -23 L-40 23 L0 46 L40 23 L40 -23 L0 -46 Z" fill="white" opacity="0.95"/>
|
||||
<path d="M-60 -35 L-60 35 L0 70 L60 35 L60 -35 L0 -70 Z" fill="none" stroke="rgba(255,255,255,0.4)" stroke-width="2"/>
|
||||
<text x="-22" y="18" font-family="Arial Black, Arial, sans-serif" font-size="45" font-weight="900" fill="#2D3436">E</text>
|
||||
<text x="8" y="12" font-family="Arial Black, Arial, sans-serif" font-size="24" font-weight="900" fill="#E60012">2</text>
|
||||
<text x="8" y="32" font-family="Arial Black, Arial, sans-serif" font-size="20" font-weight="900" fill="#00C3E3">D</text>
|
||||
</g>
|
||||
<g transform="translate(200, 145)">
|
||||
<text x="0" y="20" font-family="Arial, sans-serif" font-size="70" font-weight="800" fill="#2D3436">Extra</text>
|
||||
<text x="215" y="20" font-family="Arial, sans-serif" font-size="70" font-weight="800" fill="#E60012">2</text>
|
||||
<text x="265" y="20" font-family="Arial, sans-serif" font-size="70" font-weight="800" fill="#00C3E3">D</text>
|
||||
</g>
|
||||
<g transform="translate(200, 190)">
|
||||
<text x="0" y="0" font-family="Arial, sans-serif" font-size="18" font-weight="500" fill="#636E72" letter-spacing="4">SWITCH GAME ENGINE</text>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 配色方案 -->
|
||||
<div class="preview-section">
|
||||
<h2 class="section-title">配色方案</h2>
|
||||
<div class="color-info">
|
||||
<div class="color-item">
|
||||
<div class="color-dot" style="background: #E60012;"></div>
|
||||
<span>Nintendo Red #E60012</span>
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<div class="color-dot" style="background: #FF4757;"></div>
|
||||
<span>Coral #FF4757</span>
|
||||
</div>
|
||||
<div class="color-item">
|
||||
<div class="color-dot" style="background: #00C3E3;"></div>
|
||||
<span>Nintendo Blue #00C3E3</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 设计特点 -->
|
||||
<div class="features">
|
||||
<div class="feature-card">
|
||||
<h3>🔷 几何风格</h3>
|
||||
<p>采用六边形设计,类似 Godot、Cocos2d 等流行引擎的几何 logo 风格</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<h3>🎮 Switch 配色</h3>
|
||||
<p>红蓝渐变配色致敬 Nintendo Switch,左红右蓝的视觉分割</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<h3>✨ 现代扁平</h3>
|
||||
<p>扁平化设计配合微妙阴影,符合现代游戏引擎的审美趋势</p>
|
||||
</div>
|
||||
<div class="feature-card">
|
||||
<h3>🎯 清晰识别</h3>
|
||||
<p>E2D 字母组合清晰醒目,在各种尺寸下都能保持辨识度</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
209
xmake.lua
|
|
@ -42,9 +42,6 @@ toolchain("switch")
|
|||
-- SimpleIni 配置:不使用 Windows API
|
||||
add_defines("SI_NO_CONVERSION")
|
||||
|
||||
-- OpenGL 配置:使用标准 GLES3.2
|
||||
add_defines("GL_GLES_PROTOTYPES")
|
||||
|
||||
-- libnx 路径 - 必须在工具链级别添加
|
||||
add_includedirs(path.join(devkitPro, "libnx/include"))
|
||||
add_linkdirs(path.join(devkitPro, "libnx/lib"))
|
||||
|
|
@ -72,6 +69,7 @@ target("extra2d")
|
|||
|
||||
-- 引擎源文件
|
||||
add_files(path.join(SRC_DIR, "**.cpp"))
|
||||
add_files(path.join(SRC_DIR, "glad/glad.c"))
|
||||
|
||||
-- Squirrel 3.2 源文件
|
||||
add_files("squirrel/squirrel/*.cpp")
|
||||
|
|
@ -145,86 +143,23 @@ target("extra2d")
|
|||
end
|
||||
target_end()
|
||||
|
||||
-- ==============================================
|
||||
-- 2. Nintendo Switch 音频演示
|
||||
-- ==============================================
|
||||
target("switch_audio_demo")
|
||||
-- ============================================
|
||||
-- Switch 简单测试程序
|
||||
-- ============================================
|
||||
target("hello_world")
|
||||
set_kind("binary")
|
||||
set_plat("switch")
|
||||
set_arch("arm64")
|
||||
set_toolchains("switch")
|
||||
|
||||
add_files("Extra2D/examples/push_box/src/**.cpp")
|
||||
add_deps("extra2d")
|
||||
set_targetdir("$(builddir)/switch")
|
||||
|
||||
-- 链接 EGL、OpenGL ES 3.0 和 SDL2 音频库
|
||||
-- 注意:链接顺序很重要!被依赖的库必须放在后面
|
||||
-- 依赖链:SDL2 -> EGL -> drm_nouveau
|
||||
-- GLESv2 -> glapi -> drm_nouveau
|
||||
add_syslinks("SDL2_mixer", "SDL2",
|
||||
"opusfile", "opus", "vorbisidec", "ogg",
|
||||
"modplug", "mpg123", "FLAC",
|
||||
"GLESv2",
|
||||
"EGL",
|
||||
"glapi",
|
||||
"drm_nouveau")
|
||||
|
||||
local appTitle = "Extra2D Switch Audio Demo"
|
||||
local appAuthor = "Extra2D Switch Audio Demo"
|
||||
local appVersion = "1.0.0"
|
||||
|
||||
after_build(function (target)
|
||||
-- 强制使用 Windows 路径
|
||||
local devkitPro = "C:/devkitPro"
|
||||
local elf_file = target:targetfile()
|
||||
local output_dir = path.directory(elf_file)
|
||||
local nacp_file = path.join(output_dir, "switch_audio_demo.nacp")
|
||||
local nro_file = path.join(output_dir, "switch_audio_demo.nro")
|
||||
local romfs_dir = "Extra2D/examples/push_box/src/romfs"
|
||||
local nacptool = path.join(devkitPro, "tools/bin/nacptool.exe")
|
||||
local elf2nro = path.join(devkitPro, "tools/bin/elf2nro.exe")
|
||||
|
||||
if not os.isfile(nacptool) then
|
||||
print("Warning: nacptool not found at " .. nacptool)
|
||||
return
|
||||
end
|
||||
if not os.isfile(elf2nro) then
|
||||
print("Warning: elf2nro not found at " .. elf2nro)
|
||||
return
|
||||
end
|
||||
|
||||
-- 生成 .nacp 文件
|
||||
os.vrunv(nacptool, {"--create", appTitle, appAuthor, appVersion, nacp_file})
|
||||
print("Built " .. path.filename(nacp_file))
|
||||
|
||||
-- 生成 .nro 文件(包含 RomFS)
|
||||
local romfs_absolute = path.absolute(romfs_dir)
|
||||
if os.isdir(romfs_absolute) then
|
||||
print("Packing RomFS from: " .. romfs_absolute)
|
||||
os.vrunv(elf2nro, {elf_file, nro_file, "--nacp=" .. nacp_file, "--romfsdir=" .. romfs_absolute})
|
||||
print("Built " .. path.filename(nro_file) .. " (with RomFS)")
|
||||
else
|
||||
os.vrunv(elf2nro, {elf_file, nro_file, "--nacp=" .. nacp_file})
|
||||
print("Built " .. path.filename(nro_file))
|
||||
end
|
||||
end)
|
||||
target_end()
|
||||
|
||||
-- ============================================
|
||||
-- Switch 简单测试程序
|
||||
-- ============================================
|
||||
target("switch_simple_test")
|
||||
set_kind("binary")
|
||||
set_targetdir("build/switch")
|
||||
|
||||
-- 应用信息
|
||||
local appTitle = "Extra2D Simple Test"
|
||||
local appAuthor = "Extra2D Team"
|
||||
local appTitle = "Extra2D hello_world"
|
||||
local appAuthor = "Extra2D hello_world"
|
||||
local appVersion = "1.0.0"
|
||||
|
||||
-- 添加源文件
|
||||
add_files("Extra2D/examples/switch_simple_test/main.cpp")
|
||||
add_files("Extra2D/examples/hello_world/main.cpp")
|
||||
|
||||
-- 添加头文件路径
|
||||
add_includedirs("Extra2D/include")
|
||||
|
|
@ -238,9 +173,9 @@ target("switch_simple_test")
|
|||
local devkitPro = "C:/devkitPro"
|
||||
local elf_file = target:targetfile()
|
||||
local output_dir = path.directory(elf_file)
|
||||
local nacp_file = path.join(output_dir, "switch_simple_test.nacp")
|
||||
local nro_file = path.join(output_dir, "switch_simple_test.nro")
|
||||
local romfs_dir = "Extra2D/examples/switch_simple_test/romfs"
|
||||
local nacp_file = path.join(output_dir, "hello_world.nacp")
|
||||
local nro_file = path.join(output_dir, "hello_world.nro")
|
||||
local romfs_dir = "Extra2D/examples/hello_world/romfs"
|
||||
local nacptool = path.join(devkitPro, "tools/bin/nacptool.exe")
|
||||
local elf2nro = path.join(devkitPro, "tools/bin/elf2nro.exe")
|
||||
|
||||
|
|
@ -270,3 +205,125 @@ target("switch_simple_test")
|
|||
end)
|
||||
target_end()
|
||||
|
||||
-- ============================================
|
||||
-- 引擎空间索引演示(1000个节点)
|
||||
-- ============================================
|
||||
target("spatial_index_demo")
|
||||
set_kind("binary")
|
||||
set_plat("switch")
|
||||
set_arch("arm64")
|
||||
set_toolchains("switch")
|
||||
set_targetdir("build/switch")
|
||||
|
||||
-- 应用信息
|
||||
local appTitle = "Extra2D Spatial Index Demo"
|
||||
local appAuthor = "Extra2D Team"
|
||||
local appVersion = "1.0.0"
|
||||
|
||||
-- 添加源文件
|
||||
add_files("Extra2D/examples/spatial_index_demo/main.cpp")
|
||||
|
||||
-- 添加头文件路径
|
||||
add_includedirs("Extra2D/include")
|
||||
|
||||
-- 链接 extra2d 库
|
||||
add_deps("extra2d")
|
||||
|
||||
-- 构建后生成 .nro 文件(包含 RomFS)
|
||||
after_build(function (target)
|
||||
local devkitPro = "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 romfs_dir = "Extra2D/examples/spatial_index_demo/romfs"
|
||||
local nacptool = path.join(devkitPro, "tools/bin/nacptool.exe")
|
||||
local elf2nro = path.join(devkitPro, "tools/bin/elf2nro.exe")
|
||||
|
||||
if not os.isfile(nacptool) then
|
||||
print("Warning: nacptool not found at " .. nacptool)
|
||||
return
|
||||
end
|
||||
if not os.isfile(elf2nro) then
|
||||
print("Warning: elf2nro not found at " .. elf2nro)
|
||||
return
|
||||
end
|
||||
|
||||
-- 生成 .nacp 文件
|
||||
os.vrunv(nacptool, {"--create", appTitle, appAuthor, appVersion, nacp_file})
|
||||
print("Built " .. path.filename(nacp_file))
|
||||
|
||||
-- 生成 .nro 文件(包含 RomFS)
|
||||
local romfs_absolute = path.absolute(romfs_dir)
|
||||
if os.isdir(romfs_absolute) then
|
||||
print("Packing RomFS from: " .. romfs_absolute)
|
||||
os.vrunv(elf2nro, {elf_file, nro_file, "--nacp=" .. nacp_file, "--romfsdir=" .. romfs_absolute})
|
||||
print("Built " .. path.filename(nro_file) .. " (with RomFS)")
|
||||
else
|
||||
os.vrunv(elf2nro, {elf_file, nro_file, "--nacp=" .. nacp_file})
|
||||
print("Built " .. path.filename(nro_file))
|
||||
end
|
||||
end)
|
||||
target_end()
|
||||
|
||||
|
||||
-- ============================================
|
||||
-- 碰撞检测演示程序
|
||||
-- ============================================
|
||||
target("collision_demo")
|
||||
set_kind("binary")
|
||||
set_plat("switch")
|
||||
set_arch("arm64")
|
||||
set_toolchains("switch")
|
||||
set_targetdir("build/switch")
|
||||
|
||||
-- 应用信息
|
||||
local appTitle = "Extra2D Collision Demo"
|
||||
local appAuthor = "Extra2D Team"
|
||||
local appVersion = "1.0.0"
|
||||
|
||||
-- 添加源文件
|
||||
add_files("Extra2D/examples/collision_demo/main.cpp")
|
||||
|
||||
-- 添加头文件路径
|
||||
add_includedirs("Extra2D/include")
|
||||
|
||||
-- 链接 extra2d 库
|
||||
add_deps("extra2d")
|
||||
|
||||
-- 构建后生成 .nro 文件(包含 RomFS)
|
||||
after_build(function (target)
|
||||
local devkitPro = "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 romfs_dir = "Extra2D/examples/collision_demo/romfs"
|
||||
local nacptool = path.join(devkitPro, "tools/bin/nacptool.exe")
|
||||
local elf2nro = path.join(devkitPro, "tools/bin/elf2nro.exe")
|
||||
|
||||
if not os.isfile(nacptool) then
|
||||
print("Warning: nacptool not found at " .. nacptool)
|
||||
return
|
||||
end
|
||||
if not os.isfile(elf2nro) then
|
||||
print("Warning: elf2nro not found at " .. elf2nro)
|
||||
return
|
||||
end
|
||||
|
||||
-- 生成 .nacp 文件
|
||||
os.vrunv(nacptool, {"--create", appTitle, appAuthor, appVersion, nacp_file})
|
||||
print("Built " .. path.filename(nacp_file))
|
||||
|
||||
-- 生成 .nro 文件(包含 RomFS)
|
||||
local romfs_absolute = path.absolute(romfs_dir)
|
||||
if os.isdir(romfs_absolute) then
|
||||
print("Packing RomFS from: " .. romfs_absolute)
|
||||
os.vrunv(elf2nro, {elf_file, nro_file, "--nacp=" .. nacp_file, "--romfsdir=" .. romfs_absolute})
|
||||
print("Built " .. path.filename(nro_file) .. " (with RomFS)")
|
||||
else
|
||||
os.vrunv(elf2nro, {elf_file, nro_file, "--nacp=" .. nacp_file})
|
||||
print("Built " .. path.filename(nro_file))
|
||||
end
|
||||
end)
|
||||
target_end()
|
||||
|
|
|
|||