feat: 重构资源管理和日志系统,添加多平台支持

refactor: 替换GLES为glad库以提升跨平台兼容性
style: 更新logo和品牌资源文件
docs: 添加新字体加载API文档
chore: 移除旧的推箱子示例代码
test: 更新测试用例以适配新API
This commit is contained in:
ChestnutYueyue 2026-02-09 19:34:22 +08:00
parent 730dfd9e61
commit 2a7372f7be
72 changed files with 13030 additions and 2213 deletions

View File

@ -1,9 +0,0 @@
{
"permissions": {
"allow": [
"Bash(xargs:*)",
"Bash(wc -l \"C:\\\\Users\\\\soulcoco\\\\Desktop\\\\Easy2D\\\\Easy2D-dev\\\\Easy2D\\\\src\"/**/*.cpp)",
"Bash(wc -l:*)"
]
}
}

View File

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

Binary file not shown.

View File

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

Binary file not shown.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 477 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 658 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 682 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 641 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 642 B

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,13 +0,0 @@
#pragma once
#include <extra2d/extra2d.h>
namespace pushbox {
class SuccessScene : public extra2d::Scene {
public:
SuccessScene();
void onEnter() override;
};
} // namespace pushbox

View File

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

View File

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

View File

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

View File

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

View File

@ -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_ */

View File

@ -4,8 +4,7 @@
#include <extra2d/graphics/opengl/gl_shader.h> #include <extra2d/graphics/opengl/gl_shader.h>
#include <extra2d/graphics/opengl/gl_sprite_batch.h> #include <extra2d/graphics/opengl/gl_sprite_batch.h>
// 使用标准 GLES3.2 #include <glad/glad.h>
#include <GLES3/gl32.h>
namespace extra2d { namespace extra2d {

View File

@ -1,7 +1,6 @@
#pragma once #pragma once
// 使用标准 GLES3.2 #include <glad/glad.h>
#include <GLES3/gl32.h>
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>

View File

@ -8,8 +8,7 @@
#include <glm/mat4x4.hpp> #include <glm/mat4x4.hpp>
#include <vector> #include <vector>
// 使用标准 GLES3.2 #include <glad/glad.h>
#include <GLES3/gl32.h>
namespace extra2d { namespace extra2d {

View File

@ -3,8 +3,7 @@
#include <extra2d/graphics/texture.h> #include <extra2d/graphics/texture.h>
#include <extra2d/graphics/alpha_mask.h> #include <extra2d/graphics/alpha_mask.h>
// 使用标准 GLES3.2 #include <glad/glad.h>
#include <GLES3/gl32.h>
#include <memory> #include <memory>

View File

@ -83,6 +83,14 @@ public:
Ptr<FontAtlas> loadFont(const std::string &filepath, int fontSize, Ptr<FontAtlas> loadFont(const std::string &filepath, int fontSize,
bool useSDF = false); 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获取已缓存的字体图集 /// 通过key获取已缓存的字体图集
Ptr<FontAtlas> getFont(const std::string &key) const; Ptr<FontAtlas> getFont(const std::string &key) const;

View File

@ -7,19 +7,22 @@
#include <string> #include <string>
#include <type_traits> #include <type_traits>
// SDL2 日志头文件
#include <SDL.h>
namespace extra2d { namespace extra2d {
// ============================================================================ // ============================================================================
// 日志级别枚举 // 日志级别枚举 - 映射到 SDL_LogPriority
// ============================================================================ // ============================================================================
enum class LogLevel { enum class LogLevel {
Trace = 0, Trace = SDL_LOG_PRIORITY_VERBOSE, // SDL 详细日志
Debug = 1, Debug = SDL_LOG_PRIORITY_DEBUG, // SDL 调试日志
Info = 2, Info = SDL_LOG_PRIORITY_INFO, // SDL 信息日志
Warn = 3, Warn = SDL_LOG_PRIORITY_WARN, // SDL 警告日志
Error = 4, Error = SDL_LOG_PRIORITY_ERROR, // SDL 错误日志
Fatal = 5, Fatal = SDL_LOG_PRIORITY_CRITICAL, // SDL 严重日志
Off = 6 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); } inline std::string e2d_format(const char *fmt) { return std::string(fmt); }
// ============================================================================ // ============================================================================
// Logger 类 - 简单 printf 日志 // Logger 类 - 使用 SDL2 日志系统
// ============================================================================ // ============================================================================
class Logger { class Logger {
public: public:
/**
* @brief
*/
static void init(); static void init();
/**
* @brief
*/
static void shutdown(); static void shutdown();
/**
* @brief
* @param level
*/
static void setLevel(LogLevel level); static void setLevel(LogLevel level);
/**
* @brief
* @param enable
*/
static void setConsoleOutput(bool enable); static void setConsoleOutput(bool enable);
/**
* @brief
* @param filename
*/
static void setFileOutput(const std::string &filename); static void setFileOutput(const std::string &filename);
/**
* @brief
* @return
*/
static LogLevel getLevel() { return level_; } static LogLevel getLevel() { return level_; }
/**
* @brief
* @param level
* @param fmt
* @param args
*/
template <typename... Args> template <typename... Args>
static void log(LogLevel level, const char *fmt, const Args &...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; return;
std::string msg = e2d_format(fmt, args...); std::string msg = e2d_format(fmt, args...);
const char *levelStr = ""; SDL_LogMessage(SDL_LOG_CATEGORY_APPLICATION,
switch (level) { static_cast<SDL_LogPriority>(level), "[%s] %s",
case LogLevel::Trace: getLevelString(level), msg.c_str());
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());
} }
// 无参数版本 /**
* @brief
* @param level
* @param msg
*/
static void log(LogLevel level, const char *msg) { static void log(LogLevel level, const char *msg) {
if (level < level_) if (static_cast<int>(level) < static_cast<int>(level_))
return; return;
const char *levelStr = ""; SDL_LogMessage(SDL_LOG_CATEGORY_APPLICATION,
switch (level) { static_cast<SDL_LogPriority>(level), "[%s] %s",
case LogLevel::Trace: getLevelString(level), msg);
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);
} }
private: private:
static LogLevel level_; static LogLevel level_; // 当前日志级别
static bool initialized_; static bool initialized_; // 是否已初始化
static bool consoleOutput_; // 是否输出到控制台
static bool fileOutput_; // 是否输出到文件
static std::string logFile_; // 日志文件路径
/**
* @brief
* @param level
* @return
*/
static const char *getLevelString(LogLevel level);
}; };
// ============================================================================ // ============================================================================

7028
Extra2D/include/glad/glad.h Normal file

File diff suppressed because one or more lines are too long

View File

@ -3,8 +3,7 @@
#include <extra2d/graphics/render_target.h> #include <extra2d/graphics/render_target.h>
#include <extra2d/utils/logger.h> #include <extra2d/utils/logger.h>
// 使用标准 GLES3.2 #include <glad/glad.h>
#include <GLES3/gl32.h>
namespace extra2d { namespace extra2d {

3602
Extra2D/src/glad/glad.c Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,4 @@
// Switch: 使用 GLES 3.2 #include <glad/glad.h>
#include <GLES3/gl32.h>
#include <extra2d/graphics/opengl/gl_texture.h> #include <extra2d/graphics/opengl/gl_texture.h>
#include <extra2d/graphics/render_target.h> #include <extra2d/graphics/render_target.h>
#include <extra2d/utils/logger.h> #include <extra2d/utils/logger.h>

View File

@ -5,8 +5,7 @@
#include <SDL.h> #include <SDL.h>
// 使用标准 GLES3.2 #include <glad/glad.h>
#include <GLES3/gl32.h>
namespace extra2d { namespace extra2d {
@ -83,10 +82,31 @@ bool Window::initSDL() {
return false; 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 // 设置 VSync
SDL_GL_SetSwapInterval(vsync_ ? 1 : 0); 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; return true;
} }

View File

@ -313,6 +313,92 @@ void ResourceManager::unloadFont(const std::string &key) {
E2D_LOG_DEBUG("ResourceManager: unloaded font: {}", 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;
}
// ============================================================================ // ============================================================================
// 音效资源 // 音效资源
// ============================================================================ // ============================================================================

View File

@ -3,18 +3,54 @@
namespace extra2d { namespace extra2d {
// 静态成员定义 // 静态成员定义
LogLevel Logger::level_ = LogLevel::Trace; LogLevel Logger::level_ = LogLevel::Info;
bool Logger::initialized_ = false; 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() { void Logger::init() {
if (initialized_) { if (initialized_) {
return; return;
} }
// 设置 SDL 日志级别为详细模式(允许所有级别的日志)
SDL_LogSetPriority(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_VERBOSE);
initialized_ = true; initialized_ = true;
log(LogLevel::Info, "Logger initialized"); log(LogLevel::Info, "Logger initialized with SDL2");
} }
/**
* @brief
*/
void Logger::shutdown() { void Logger::shutdown() {
if (initialized_) { if (initialized_) {
log(LogLevel::Info, "Logger shutting down"); log(LogLevel::Info, "Logger shutting down");
@ -22,16 +58,47 @@ void Logger::shutdown() {
initialized_ = false; initialized_ = false;
} }
void Logger::setLevel(LogLevel level) { level_ = level; } /**
* @brief
void Logger::setConsoleOutput(bool /*enable*/) { * @param level
// On Switch, console output always goes to nxlink stdout */
// Nothing to configure 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 * @brief
// Could potentially write to sdmc:/ in the future * @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 } // namespace extra2d

783
README.md
View File

@ -1,13 +1,13 @@
<div align="center"> <div align="center">
![Easy2D Logo](./logo/logo_text_dark.png) ![Extra2D Logo](./logo/logo_text_dark.svg)
<p align="center"> <p align="center">
<a href="https://github.com/Easy2D/Easy2D/releases/latest"> <a href="https://github.com/ChestnutYueyue/extra2d/releases/latest">
<img src="https://img.shields.io/github/release/easy2d/easy2d?style=for-the-badge&color=blue&logo=github" alt="Release"> <img src="https://img.shields.io/github/release/ChestnutYueyue/extra2d?style=for-the-badge&color=blue&logo=github" alt="Release">
</a> </a>
<a href="https://github.com/Easy2D/Easy2D/blob/master/LICENSE"> <a href="https://github.com/ChestnutYueyue/extra2d/blob/master/LICENSE">
<img src="https://img.shields.io/github/license/easy2d/easy2d?style=for-the-badge&color=green&logo=opensourceinitiative" alt="License"> <img src="https://img.shields.io/github/license/ChestnutYueyue/extra2d?style=for-the-badge&color=green&logo=opensourceinitiative" alt="License">
</a> </a>
<a href="#"> <a href="#">
<img src="https://img.shields.io/badge/build-passing-brightgreen?style=for-the-badge&logo=appveyor" alt="Build Status"> <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"> <img src="https://img.shields.io/badge/C++-17-00599C?style=for-the-badge&logo=c%2B%2B" alt="C++17">
</a> </a>
<a href="#"> <a href="#">
<img src="https://img.shields.io/badge/Windows-0078D6?style=for-the-badge&logo=windows&logoColor=white" alt="Windows"> <img src="https://img.shields.io/badge/Nintendo%20Switch-E60012?style=for-the-badge&logo=nintendo-switch&logoColor=white" alt="Nintendo Switch">
</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">
</a> </a>
</p> </p>
<p align="center"> <p align="center">
<b>🎮 为 C++ 打造的轻量级 2D 游戏引擎</b><br> <b>🎮 专为 Nintendo Switch 打造的轻量级 2D 游戏引擎</b><br>
<i>简单、高效、跨平台</i> <i>高性能、易用、原生支持 Switch 平台</i>
</p> </p>
[📖 官方文档](https://easy2d.cn) | [🚀 快速开始](#快速开始) | [📦 构建安装](#构建安装) | [💬 QQ群: 608406540](#联系方式) [📖 构建指南](./SWITCH_BUILD_GUIDE.md) | [🚀 快速开始](#快速开始) | [📦 项目结构](#项目结构) | [💬 问题反馈](https://github.com/ChestnutYueyue/extra2d/issues)
</div> </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` 提供完整的动画控制,包括播放、暂停、帧范围限制、动画字典管理等功能。 - **🎯 Switch 原生支持**:专为 Nintendo Switch 硬件优化,支持掌机/主机双模式
- **🎬 高级动画系统**:支持骨骼动画、精灵动画、补间动画,提供 ALS 动画格式支持
- **📜 脚本系统**:集成 Squirrel 脚本引擎,支持使用类 JavaScript 语法编写游戏逻辑。通过 `ScriptComponent` 将脚本附加到节点,实现数据驱动的游戏开发。提供完整的引擎 API 绑定,包括节点操作、输入处理、动画控制等。 - **📜 脚本系统**:集成 Squirrel 脚本引擎,支持热更新和快速迭代开发
- **🎵 音频系统**:基于 miniaudio 的高质量音频播放,支持 BGM 和音效
- **🎮 跨平台**:一套代码,多平台运行。支持 Windows、Linux 和 macOS。 - **🎨 特效系统**:粒子系统、后处理效果、自定义着色器支持
- **💾 数据持久化**:游戏存档、配置文件的便捷读写
--- ---
@ -57,7 +52,7 @@
```mermaid ```mermaid
mindmap mindmap
root((Easy2D v3.1.0 引擎架构)) root((Extra2D 引擎架构))
核心系统 核心系统
应用管理 Application 应用管理 Application
渲染后端 RenderBackend 渲染后端 RenderBackend
@ -66,50 +61,32 @@ mindmap
音频引擎 AudioEngine 音频引擎 AudioEngine
资源管理 ResourceManager 资源管理 ResourceManager
事件系统 EventDispatcher 事件系统 EventDispatcher
日志系统 Logger
场景管理 场景管理
场景 Scene 场景 Scene
场景管理器 SceneManager 场景管理器 SceneManager
过渡动画 Transition 过渡动画 Transition
空间索引 SpatialManager 空间索引 SpatialManager
四叉树 QuadTree
空间哈希 SpatialHash
节点系统 节点系统
基础节点 Node 基础节点 Node
精灵 Sprite 精灵 Sprite
文本 Text 文本 Text
形状 ShapeNode 形状 ShapeNode
摄像机 Camera 摄像机 Camera
动画节点 AnimationNode
动画系统 动画系统
动作系统 Action 动作系统 Action
位移动作 MoveBy/MoveTo 精灵动画 AnimatedSprite
缩放动作 ScaleBy/ScaleTo 骨骼动画支持
旋转动作 RotateBy/RotateTo 动画缓存 AnimationCache
淡入淡出 FadeIn/FadeOut 动画事件 AnimationEvent
跳跃动作 JumpBy/JumpTo
组合动作 Sequence/Spawn/Repeat
缓动函数 Ease
精灵动画系统
动画精灵 AnimatedSprite
动画片段 AnimationClip
动画控制器 AnimationController
精灵帧 SpriteFrame
脚本系统 脚本系统
Squirrel 脚本引擎 ScriptEngine Squirrel 脚本引擎
VM 虚拟机管理 脚本节点 ScriptNode
脚本加载与执行 完整 API 绑定
错误处理与调试 特效系统
脚本组件 ScriptComponent 粒子系统 ParticleSystem
生命周期回调 onEnter/onUpdate/onExit 后处理 PostProcess
节点访问与操作 自定义效果管理器
脚本绑定 API
节点绑定 Node/Sprite/AnimatedSprite
输入绑定 Input/Key
数学绑定 Vec2/Rect/Color
事件系统
事件队列 EventQueue
事件分发 EventDispatcher
输入码 InputCodes
UI 系统 UI 系统
基础控件 Widget 基础控件 Widget
按钮 Button 按钮 Button
@ -124,136 +101,6 @@ mindmap
矩形 Rect 矩形 Rect
大小 Size 大小 Size
颜色 Color 颜色 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 | | 开发环境 | devkitPro + devkitA64 |
| Linux | Ubuntu 18.04 | Ubuntu 22.04 | | C++ 标准 | C++17 |
| macOS | 10.14 | 最新版 | | 构建工具 | xmake |
| C++ 标准 | C++17 | C++17 | | 目标平台 | Nintendo Switch |
| OpenGL | 3.3 | 4.0+ |
### 使用 xmake 构建(推荐) ### 安装 devkitPro
#### 步骤 1: 安装 xmake
```bash ```bash
# Windows (PowerShell) # Windows (以管理员身份运行 PowerShell)
Invoke-Expression (Invoke-Webrequest 'https://xmake.io/psget.text' -UseBasicParsing).Content Invoke-WebRequest -Uri "https://github.com/devkitPro/pacman/releases/latest/download/devkitpro-pacman.amd64.exe" -OutFile "devkitpro-pacman.exe"
.\devkitpro-pacman.exe
# Linux/macOS # 安装 Switch 开发工具链
curl -fsSL https://xmake.io/shget.text | bash pacman -S switch-dev switch-portlibs
``` ```
#### 步骤 2: 克隆并构建 ### 构建项目
```bash ```bash
# 克隆仓库 # 克隆仓库
git clone https://github.com/nomango/easy2d.git git clone https://github.com/ChestnutYueyue/extra2d.git
cd easy2d cd extra2d
# 配置并构建 # 配置 Switch 平台构建
xmake f --mode=release xmake f -p switch --mode=release
# 构建引擎
xmake xmake
# 运行示例 # 构建示例游戏
xmake run hello_world xmake -g examples
xmake run push_box
``` ```
#### 平台特定配置 ### 生成 NSP 可运行文件
```bash ```bash
# Windows (MSVC - 默认) # 打包推箱子游戏示例
xmake f --mode=release xmake package push_box
# Windows (MinGW) # 生成的文件位于
xmake f --toolchain=mingw --mode=release # build/switch/release/push_box/push_box.nsp
# Linux
xmake f --mode=release
# macOS
xmake f --mode=release
# 调试模式
xmake f --mode=debug
``` ```
--- ---
@ -322,9 +159,9 @@ xmake f --mode=debug
## 📝 Hello World 示例 ## 📝 Hello World 示例
```cpp ```cpp
#include <easy2d/easy2d.h> #include <extra2d/extra2d.h>
using namespace easy2d; using namespace extra2d;
int main() int main()
{ {
@ -334,9 +171,9 @@ int main()
// 配置应用 // 配置应用
AppConfig config; AppConfig config;
config.title = "Hello Easy2D"; config.title = "Hello Extra2D";
config.width = 800; config.width = 1280;
config.height = 600; config.height = 720;
config.vsync = true; config.vsync = true;
// 初始化应用 // 初始化应用
@ -351,11 +188,11 @@ int main()
scene->setBackgroundColor(Color(0.1f, 0.1f, 0.15f, 1.0f)); scene->setBackgroundColor(Color(0.1f, 0.1f, 0.15f, 1.0f));
// 创建文本节点 // 创建文本节点
auto text = Text::create("Hello, Easy2D v3.0!"); auto text = Text::create("Hello, Extra2D on Switch!");
text->setPosition(Vec2(400, 300)); text->setPosition(Vec2(640, 360));
text->setAnchor(Vec2(0.5f, 0.5f)); text->setAnchor(Vec2(0.5f, 0.5f));
text->setTextColor(Color(1.0f, 0.5f, 0.2f, 1.0f)); text->setTextColor(Color(1.0f, 0.5f, 0.2f, 1.0f));
text->setFontSize(32); text->setFontSize(48);
// 添加动画效果 // 添加动画效果
text->runAction(makePtr<Repeat>( 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/ Extra2D/
├── 📁 Easy2D/ # 引擎核心代码 ├── 📁 Extra2D/ # 引擎核心代码
│ ├── 📁 include/ # 头文件 │ ├── 📁 include/ # 头文件
│ │ ├── 📁 easy2d/ # 引擎头文件 │ │ └── 📁 extra2d/ # 引擎头文件
│ │ │ ├── easy2d.h # 主头文件 │ │ ├── extra2d.h # 主头文件
│ │ │ ├── app/ # 应用管理 │ │ ├── app/ # 应用管理
│ │ │ │ └── application.h │ │ ├── action/ # 动作系统
│ │ │ ├── action/ # 动作系统 │ │ ├── animation/ # 动画系统
│ │ │ │ ├── action.h │ │ ├── audio/ # 音频系统
│ │ │ │ ├── actions.h │ │ ├── core/ # 核心类型
│ │ │ │ └── ease.h │ │ ├── effects/ # 特效系统
│ │ │ ├── audio/ # 音频系统 │ │ ├── event/ # 事件系统
│ │ │ │ ├── audio_engine.h │ │ ├── graphics/ # 图形渲染
│ │ │ │ └── sound.h │ │ ├── platform/ # 平台抽象
│ │ │ ├── core/ # 核心类型 │ │ ├── resource/ # 资源管理
│ │ │ │ ├── types.h │ │ ├── scene/ # 场景系统
│ │ │ │ ├── math_types.h │ │ ├── script/ # 脚本系统
│ │ │ │ ├── color.h │ │ ├── spatial/ # 空间索引
│ │ │ │ └── string.h │ │ ├── ui/ # UI 系统
│ │ │ ├── event/ # 事件系统 │ │ └── utils/ # 工具库
│ │ │ │ ├── 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 配置库
│ ├── 📁 src/ # 源文件 │ ├── 📁 src/ # 源文件
│ │ ├── App/ # 应用实现
│ │ ├── Action/ # 动作系统实现
│ │ ├── Animation/ # 动画系统实现
│ │ ├── Audio/ # 音频系统实现
│ │ ├── Core/ # 核心实现
│ │ ├── Event/ # 事件系统实现
│ │ ├── Graphics/ # 图形渲染实现
│ │ ├── Platform/ # 平台实现
│ │ ├── Resource/ # 资源管理实现
│ │ ├── Scene/ # 场景系统实现
│ │ ├── Script/ # 脚本系统实现
│ │ ├── Spatial/ # 空间索引实现
│ │ ├── UI/ # UI 系统实现
│ │ └── Utils/ # 工具库实现
│ └── 📁 examples/ # 示例程序 │ └── 📁 examples/ # 示例程序
│ ├── hello_world/ # Hello World 示例 │ ├── push_box/ # 推箱子游戏
│ ├── animation_demo/ # 精灵动画示例 │ └── switch_simple_test/ # 简单测试
│ ├── script_demo/ # 脚本系统示例 ├── 📁 squirrel/ # Squirrel 脚本引擎
│ ├── font_test/ # 字体测试示例
│ └── push_box/ # 推箱子游戏示例
├── 📁 logo/ # Logo 资源 ├── 📁 logo/ # Logo 资源
├── 📄 xmake.lua # xmake 构建配置 ├── 📄 xmake.lua # xmake 构建配置
├── 📄 SWITCH_BUILD_GUIDE.md # Switch 构建详细指南
├── 📄 LICENSE # MIT 许可证 ├── 📄 LICENSE # MIT 许可证
└── 📄 README.md # 本文件 └── 📄 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 速查 ## 📋 API 速查
### 应用控制 ### 应用控制
```cpp ```cpp
// 获取应用实例
auto& app = Application::instance(); auto& app = Application::instance();
// 初始化
AppConfig config;
config.title = "My Game";
config.width = 800;
config.height = 600;
config.vsync = true;
app.init(config); app.init(config);
// 运行主循环
app.run(); app.run();
// 状态控制
app.pause();
app.resume();
app.quit(); 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 ```cpp
// 创建场景
auto scene = makePtr<Scene>(); auto scene = makePtr<Scene>();
scene->setBackgroundColor(Color(0.1f, 0.1f, 0.2f, 1.0f)); app.enterScene(scene);
app.enterScene(scene, makePtr<FadeTransition>(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();
``` ```
### 节点操作 ### 节点操作
```cpp ```cpp
// 创建节点
auto node = makePtr<Node>();
auto sprite = Sprite::create(texture); auto sprite = Sprite::create(texture);
auto text = Text::create("Hello"); sprite->setPosition(Vec2(100, 200));
sprite->setRotation(45.0f);
// 变换属性 sprite->runAction(makePtr<MoveTo>(1.0f, Vec2(200, 300)));
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();
``` ```
### 动系统 ### 动画系统
```cpp ```cpp
// 创建动作 // 精灵动画
auto move = makePtr<MoveTo>(1.0f, Vec2(100, 200)); auto anim = AnimatedSprite::createFromGrid(
auto scale = makePtr<ScaleTo>(0.5f, Vec2(2.0f, 2.0f)); "player.png", 96, 96, 125.0f, 16);
auto rotate = makePtr<RotateBy>(1.0f, 90.0f); anim->setFrameRange(0, 3);
auto fade = makePtr<FadeIn>(0.5f); anim->play();
auto jump = makePtr<JumpBy>(1.0f, Vec2(100, 0), 50.0f, 3);
// 组合动作 // 动作动画
auto sequence = makePtr<Sequence>(std::vector<Ptr<Action>>{ node->runAction(makePtr<Sequence>(
move, scale, rotate makePtr<MoveTo>(1.0f, Vec2(100, 200)),
}); makePtr<ScaleTo>(0.5f, Vec2(2.0f, 2.0f))
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);
``` ```
### 输入处理 ### 输入处理
```cpp ```cpp
auto& input = app.input(); auto& input = app.input();
if (input.isKeyDown(KeyCode::ButtonA)) {}
// 键盘 if (input.isKeyPressed(KeyCode::ButtonB)) {}
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)) {}
auto pos = input.getMousePosition(); auto pos = input.getMousePosition();
auto delta = input.getMouseDelta();
auto scroll = input.getMouseScrollDelta();
// 鼠标控制
input.setMousePosition(Vec2(400, 300));
input.setMouseVisible(false);
input.setMouseLocked(true);
``` ```
### 音频播放 ### 音频播放
```cpp ```cpp
auto& audio = app.audio(); auto& audio = app.audio();
// 加载音效
auto sound = audio.loadSound("jump.wav"); auto sound = audio.loadSound("jump.wav");
auto namedSound = audio.loadSound("jump", "jump.wav");
// 播放控制
sound->play(); sound->play();
sound->pause();
sound->resume();
sound->stop();
// 属性
sound->setVolume(0.8f); 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+ | | GLFW | 窗口和输入管理 | 3.3+ |
| GLEW | OpenGL 扩展加载 | 2.1+ |
| GLM | 数学库 | 0.9.9+ | | GLM | 数学库 | 0.9.9+ |
| miniaudio | 音频播放 | 最新版 | | miniaudio | 音频播放 | 最新版 |
| spdlog | 日志系统 | 最新版 | | spdlog | 日志系统 | 最新版 |
| stb_image | 图像加载 | 最新版 | | stb_image | 图像加载 | 最新版 |
| Squirrel | 脚本引擎 | 3.2+ |
| xmake | 构建系统 | 2.5+ | | xmake | 构建系统 | 2.5+ |
--- ---
## 📦 使用 xmake 集成 Easy2D ## 📖 相关文档
在你的项目中使用 Easy2D - [Switch 构建指南](./SWITCH_BUILD_GUIDE.md) - 详细的 Switch 平台构建教程
- [迁移完成记录](./SWITCH_MIGRATION_COMPLETE.md) - 项目迁移历史记录
```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()
```
--- ---
@ -866,11 +393,11 @@ target_end()
## 📄 许可证 ## 📄 许可证
Easy2D 使用 [MIT](LICENSE) 许可证。 Extra2D 使用 [MIT](LICENSE) 许可证。
--- ---
## 联系方式 ## 联系方式
- QQ群: 608406540 - GitHub Issues: https://github.com/ChestnutYueyue/extra2d/issues
- GitHub: https://github.com/Easy2D/Easy2D - 作者: [ChestnutYueyue](https://github.com/ChestnutYueyue)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

99
logo/logo.svg Normal file
View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

88
logo/logo_text_dark.svg Normal file
View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

88
logo/logo_text_light.svg Normal file
View File

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

362
logo/preview.html Normal file
View File

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

@ -42,9 +42,6 @@ toolchain("switch")
-- SimpleIni 配置:不使用 Windows API -- SimpleIni 配置:不使用 Windows API
add_defines("SI_NO_CONVERSION") add_defines("SI_NO_CONVERSION")
-- OpenGL 配置:使用标准 GLES3.2
add_defines("GL_GLES_PROTOTYPES")
-- libnx 路径 - 必须在工具链级别添加 -- libnx 路径 - 必须在工具链级别添加
add_includedirs(path.join(devkitPro, "libnx/include")) add_includedirs(path.join(devkitPro, "libnx/include"))
add_linkdirs(path.join(devkitPro, "libnx/lib")) 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, "**.cpp"))
add_files(path.join(SRC_DIR, "glad/glad.c"))
-- Squirrel 3.2 源文件 -- Squirrel 3.2 源文件
add_files("squirrel/squirrel/*.cpp") add_files("squirrel/squirrel/*.cpp")
@ -145,86 +143,23 @@ target("extra2d")
end end
target_end() target_end()
-- ============================================== -- ============================================
-- 2. Nintendo Switch 音频演示 -- Switch 简单测试程序
-- ============================================== -- ============================================
target("switch_audio_demo") target("hello_world")
set_kind("binary") set_kind("binary")
set_plat("switch") set_plat("switch")
set_arch("arm64") set_arch("arm64")
set_toolchains("switch") 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") set_targetdir("build/switch")
-- 应用信息 -- 应用信息
local appTitle = "Extra2D Simple Test" local appTitle = "Extra2D hello_world"
local appAuthor = "Extra2D Team" local appAuthor = "Extra2D hello_world"
local appVersion = "1.0.0" 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") add_includedirs("Extra2D/include")
@ -238,9 +173,9 @@ target("switch_simple_test")
local devkitPro = "C:/devkitPro" local devkitPro = "C:/devkitPro"
local elf_file = target:targetfile() local elf_file = target:targetfile()
local output_dir = path.directory(elf_file) local output_dir = path.directory(elf_file)
local nacp_file = path.join(output_dir, "switch_simple_test.nacp") local nacp_file = path.join(output_dir, "hello_world.nacp")
local nro_file = path.join(output_dir, "switch_simple_test.nro") local nro_file = path.join(output_dir, "hello_world.nro")
local romfs_dir = "Extra2D/examples/switch_simple_test/romfs" local romfs_dir = "Extra2D/examples/hello_world/romfs"
local nacptool = path.join(devkitPro, "tools/bin/nacptool.exe") local nacptool = path.join(devkitPro, "tools/bin/nacptool.exe")
local elf2nro = path.join(devkitPro, "tools/bin/elf2nro.exe") local elf2nro = path.join(devkitPro, "tools/bin/elf2nro.exe")
@ -270,3 +205,125 @@ target("switch_simple_test")
end) end)
target_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()