Compare commits

..

No commits in common. "b4036cd8ddae7d3c2d9177fc58415d3f8c57ee80" and "8b28f3c6df891c0992b7e52808de87706b22d573" have entirely different histories.

88 changed files with 1920 additions and 5792 deletions

View File

@ -0,0 +1,105 @@
# Extra2D 数据结构与算法优化计划
## 概述
针对分析发现的潜在问题,制定以下分阶段优化计划。
---
## 阶段一:高优先级问题修复(预计 3-4 天)
### 1.1 四叉树碰撞检测优化
**目标**: 解决 O(n²) 碰撞检测性能问题
**文件**: `src/spatial/quadtree.cpp`
**方案**:
- 实现扫描线算法或 AABB 树加速碰撞检测
- 优化节点分裂策略,避免对象过度集中
- 添加单元测试验证性能改进
### 1.2 空间哈希内存布局重构
**目标**: 减少内存碎片,提高缓存友好性
**文件**: `include/extra2d/spatial/spatial_hash.h`, `src/spatial/spatial_hash.cpp`
**方案**:
- 使用单一连续内存结构替代嵌套哈希表
- 实现对象池复用 Cell 内存
- 查询时使用线程本地静态缓冲区避免临时分配
### 1.3 动画帧数据布局优化
**目标**: 提高动画系统缓存命中率
**文件**: `include/extra2d/animation/animation_frame.h`, `include/extra2d/animation/frame_property.h`
**方案**:
- 采用结构体数组(SoA)布局存储帧数据
- 热数据(delay, offset)和冷数据(碰撞盒)分离
- 使用紧凑位域替代 std::variant 存储属性
### 1.4 Node 循环引用风险修复
**目标**: 消除内存泄漏风险
**文件**: `include/extra2d/scene/node.h`, `src/scene/node.cpp`
**方案**:
- 审查所有 shared_ptr 使用场景
- 添加对象所有权文档说明
- 考虑使用 intrusive_ptr 替代 shared_ptr
---
## 阶段二:中优先级优化(预计 2-3 天)
### 2.1 变换矩阵脏标记传播
**目标**: 优化深层场景树性能
**文件**: `src/scene/node.cpp`
**方案**:
- 实现脏标记传播机制替代递归计算
- 延迟计算世界变换直到实际需要
### 2.2 纹理池 LRU 优化
**目标**: 提高缓存局部性
**文件**: `include/extra2d/graphics/texture_pool.h`
**方案**:
- 使用侵入式链表替代 std::list
- 使用数组索引代替指针
### 2.3 子节点排序优化
**目标**: 减少排序开销
**文件**: `src/scene/node.cpp`
**方案**:
- 使用插入排序(如果大部分已有序)
- 或使用 std::multiset 维护有序性
---
## 阶段三:低优先级改进(预计 1-2 天)
### 3.1 渲染命令内存优化
**目标**: 减少 RenderCommand 内存占用
**文件**: `include/extra2d/graphics/render_command.h`
**方案**:
- 类型分离的命令队列
- 或自定义联合体替代 std::variant
### 3.2 资源管理软引用缓存
**目标**: 减少资源重复加载
**文件**: `include/extra2d/resource/resource_manager.h`
**方案**:
- 实现 LRU 策略管理缓存
- 软引用保留最近使用资源
---
## 实施顺序建议
1. **首先修复内存安全问题** (Node 循环引用)
2. **然后优化性能瓶颈** (四叉树、空间哈希)
3. **最后进行内存布局优化** (动画帧、渲染命令)
## 预期收益
| 优化项 | 预期性能提升 | 内存节省 |
|--------|-------------|----------|
| 四叉树碰撞检测 | 50-80% (大量对象时) | - |
| 空间哈希重构 | 20-30% | 30-40% |
| 动画帧布局 | 15-25% | 40-50% |
| 变换矩阵优化 | 10-20% (深层场景) | - |
| 纹理池 LRU | - | 20-30% |
---
请确认此计划后,我将开始实施具体的代码修改。

View File

@ -1,327 +0,0 @@
# Extra2D 构建系统文档
## 概述
Extra2D 使用 **Xmake** 作为构建系统,支持 **MinGW (Windows)****Nintendo Switch** 两个平台。
## 项目结构
```
Extra2D/
├── xmake.lua # 主构建脚本
├── xmake/
│ ├── engine.lua # 引擎库定义
│ └── toolchains/
│ └── switch.lua # Switch 工具链定义
├── Extra2D/
│ ├── src/ # 引擎源码
│ └── include/ # 引擎头文件
├── squirrel/ # Squirrel 脚本引擎
└── examples/ # 示例程序
├── hello_world/
├── collision_demo/
├── push_box/
└── spatial_index_demo/
```
## 环境准备
### MinGW (Windows) 平台
1. **安装 MinGW-w64**
- 下载地址: https://www.mingw-w64.org/downloads/
- 或使用 MSYS2: `pacman -S mingw-w64-x86_64-toolchain`
2. **安装 Xmake**
- 下载地址: https://xmake.io/#/zh-cn/guide/installation
3. **安装依赖包**
```bash
xmake require -y
```
### Nintendo Switch 平台
1. **安装 devkitPro**
- 下载地址: https://devkitpro.org/wiki/Getting_Started
- Windows 安装程序会自动设置环境变量
2. **设置环境变量**
```powershell
# PowerShell
$env:DEVKITPRO="C:/devkitPro"
# 或永久设置(系统属性 -> 环境变量)
[Environment]::SetEnvironmentVariable("DEVKITPRO", "C:/devkitPro", "User")
```
3. **在 MSYS2 中安装 Switch 库**
```bash
# 打开 MSYS2 (devkitPro 提供的)
pacman -S switch-sdl2 switch-sdl2_mixer switch-glm
# 或安装所有 Switch 开发库
pacman -S $(pacman -Slq dkp-libs | grep switch-)
```
4. **验证安装**
```bash
ls $DEVKITPRO/portlibs/switch/include/SDL2
ls $DEVKITPRO/portlibs/switch/include/glm
```
## 主构建脚本 (xmake.lua)
### 项目元信息
- **项目名称**: Extra2D
- **版本**: 3.1.0
- **许可证**: MIT
- **语言标准**: C++17
- **编码**: UTF-8
### 构建选项
| 选项 | 默认值 | 描述 |
|------|--------|------|
| `examples` | true | 构建示例程序 |
| `debug_logs` | false | 启用调试日志 |
### 平台检测逻辑
1. 获取主机平台: `os.host()`
2. 获取目标平台: `get_config("plat")` 或主机平台
3. 平台回退: 如果不支持Windows 回退到 `mingw`
4. 设置平台: `set_plat(target_plat)`
5. 设置架构:
- Switch: `arm64`
- MinGW: `x86_64`
### 依赖包 (MinGW 平台)
```lua
add_requires("glm", "libsdl2", "libsdl2_mixer")
```
## 引擎库定义 (xmake/engine.lua)
### 目标: extra2d
- **类型**: 静态库 (`static`)
- **源文件**:
- `Extra2D/src/**.cpp`
- `Extra2D/src/glad/glad.c`
- `squirrel/squirrel/*.cpp`
- `squirrel/sqstdlib/*.cpp`
### 头文件路径
- `Extra2D/include` (public)
- `squirrel/include` (public)
- `Extra2D/include/extra2d/platform` (public)
### 平台配置
#### Switch 平台
```lua
add_includedirs(devkitPro .. "/portlibs/switch/include")
add_linkdirs(devkitPro .. "/portlibs/switch/lib")
add_syslinks("SDL2_mixer", "SDL2", "opusfile", "opus", ...)
```
#### MinGW 平台
```lua
add_packages("glm", "libsdl2", "libsdl2_mixer")
add_syslinks("opengl32", "glu32", "winmm", "imm32", "version", "setupapi")
```
### 编译器标志
- `-Wall`, `-Wextra`
- `-Wno-unused-variable`, `-Wno-unused-function`
- `-Wno-deprecated-copy`, `-Wno-class-memaccess`
### 构建模式
- **Debug**: `-O0`, `-g`, 定义 `E2D_DEBUG`, `_DEBUG`
- **Release**: `-O2`, 定义 `NDEBUG`
## Switch 工具链 (xmake/toolchains/switch.lua)
### 工具链: switch
- **类型**: standalone
- **描述**: Nintendo Switch devkitA64 工具链
### 工具路径
- **CC**: `aarch64-none-elf-gcc.exe`
- **CXX**: `aarch64-none-elf-g++.exe`
- **LD**: `aarch64-none-elf-g++.exe`
- **AR**: `aarch64-none-elf-gcc-ar.exe`
### 架构标志
```
-march=armv8-a+crc+crypto -mtune=cortex-a57 -mtp=soft -fPIE
```
### 链接标志
```
-specs=switch.specs -g
```
### 预定义宏
- `__SWITCH__`
- `__NX__`
- `MA_SWITCH`
- `PFD_SWITCH`
### 系统库
- `nx` (libnx)
- `m` (math)
## 示例程序构建脚本
### 通用结构
```lua
-- 使用与主项目相同的平台配置
if is_plat("switch") then
-- Switch 平台配置
elseif is_plat("mingw") then
-- MinGW 平台配置
end
```
### Switch 平台配置
- 设置平台: `set_plat("switch")`
- 设置架构: `set_arch("arm64")`
- 设置工具链: `set_toolchains("switch")`
- 设置输出目录: `set_targetdir("../../build/examples/xxx")`
- 构建后生成 NRO 文件
### MinGW 平台配置
- 设置平台: `set_plat("mingw")`
- 设置架构: `set_arch("x86_64")`
- 设置输出目录: `set_targetdir("../../build/examples/xxx")`
- 链接标志: `-mwindows`
- 构建后复制资源文件
## 构建命令
### 配置项目
```bash
# 默认配置 (MinGW)
xmake f -c
# 指定平台 (使用 -p 参数)
xmake f -c -p mingw
xmake f -c -p switch
# 指定 MinGW 路径(如果不在默认位置)
xmake f -c -p mingw --mingw=C:\mingw
# 禁用示例
xmake f --examples=n
# 启用调试日志
xmake f --debug_logs=y
```
### 安装依赖 (MinGW)
```bash
xmake require -y
```
### 构建项目
```bash
# 构建所有目标
xmake
# 构建特定目标
xmake -r extra2d
xmake -r push_box
# 并行构建
xmake -j4
```
### 运行程序
```bash
# 运行示例
xmake run push_box
xmake run hello_world
```
### 清理构建
```bash
xmake clean
xmake f -c # 重新配置
```
## 输出目录结构
```
build/
├── examples/
│ ├── hello_world/
│ │ ├── hello_world.exe # MinGW
│ │ ├── hello_world.nro # Switch
│ │ └── assets/ # 资源文件
│ ├── push_box/
│ ├── collision_demo/
│ └── spatial_index_demo/
└── ...
```
## 关键设计决策
### 1. 平台检测
- 使用 `is_plat()` 而不是手动检测,确保与主项目一致
- 示例脚本继承主项目的平台配置
### 2. 资源处理
- **Switch**: 使用 romfs 嵌入 NRO 文件
- **MinGW**: 构建后复制到输出目录
### 3. 依赖管理
- **MinGW**: 使用 Xmake 包管理器 (`add_requires`)
- **Switch**: 使用 devkitPro 提供的库
### 4. 工具链隔离
- Switch 工具链定义在单独文件中
- 通过 `set_toolchains("switch")` 切换
## 常见问题
### 1. 依赖包找不到
```bash
xmake repo -u
xmake require -y
```
### 2. Switch 工具链找不到
- 确保 DEVKITPRO 环境变量设置正确
- 默认路径: `C:/devkitPro`
### 3. 平台配置不匹配
- 使用 `xmake show` 查看当前配置
- 使用 `xmake f -c` 重新配置
### 4. MinGW 路径问题
如果 MinGW 安装在非默认位置,使用 `--mingw` 参数指定:
```bash
xmake f -c -p mingw --mingw=D:\Tools\mingw64
```
### 5. Switch 库找不到
确保在 MSYS2 中安装了必要的库:
```bash
pacman -S switch-sdl2 switch-sdl2_mixer switch-glm
```
## 扩展指南
### 添加新示例
1. 在 `examples/` 下创建新目录
2. 创建 `xmake.lua` 构建脚本
3. 在 `xmake.lua` 中添加 `includes("examples/new_example")`
### 添加新平台
1. 在 `xmake/toolchains/` 下创建工具链定义
2. 在 `xmake.lua` 中添加平台检测逻辑
3. 在 `xmake/engine.lua` 中添加平台配置

View File

@ -0,0 +1,62 @@
## Node 节点优化计划
### 阶段一:内存布局与数据结构优化
#### 1.1 成员变量重排(减少内存占用)
- **目标**: [node.h](file:///c:/Users/soulcoco/Desktop/Easy2D/Extra2D/Extra2D/include/extra2d/scene/node.h) 和 [node.cpp](file:///c:/Users/soulcoco/Desktop/Easy2D/Extra2D/Extra2D/src/scene/node.cpp)
- **内容**: 按类型大小降序排列成员变量,减少内存对齐填充
- **预期收益**: 减少约 16-32 字节内存占用
#### 1.2 子节点查找优化(哈希索引)
- **目标**: [node.h](file:///c:/Users/soulcoco/Desktop/Easy2D/Extra2D/Extra2D/include/extra2d/scene/node.h) 和 [node.cpp](file:///c:/Users/soulcoco/Desktop/Easy2D/Extra2D/Extra2D/src/scene/node.cpp)
- **内容**:
- 添加 `std::unordered_map<std::string, WeakPtr<Node>> nameIndex_`
- 添加 `std::unordered_map<int, WeakPtr<Node>> tagIndex_`
- 修改 `addChild`/`removeChild` 维护索引
- **预期收益**: `getChildByName`/`getChildByTag` 从 O(n) 优化到 O(1)
### 阶段二Action 系统优化
#### 2.1 Action 存储优化
- **目标**: [node.h](file:///c:/Users/soulcoco/Desktop/Easy2D/Extra2D/Extra2D/include/extra2d/scene/node.h) 和 [node.cpp](file:///c:/Users/soulcoco/Desktop/Easy2D/Extra2D/Extra2D/src/scene/node.cpp)
- **内容**:
- 使用 `std::unordered_map<int, Ptr<Action>>` 替代 `std::vector` 存储带 tag 的 Action
- 使用侵入式链表管理 Action 更新顺序
- **预期收益**: Action 查找和删除从 O(n) 优化到 O(1)
### 阶段三:变换计算优化
#### 3.1 世界变换迭代计算
- **目标**: [node.cpp](file:///c:/Users/soulcoco/Desktop/Easy2D/Extra2D/Extra2D/src/scene/node.cpp)
- **内容**:
- 将 `getWorldTransform()` 的递归改为迭代实现
- 使用栈结构收集父节点链
- **预期收益**: 避免深层级节点的栈溢出风险,减少函数调用开销
#### 3.2 矩阵计算优化
- **目标**: [node.cpp](file:///c:/Users/soulcoco/Desktop/Easy2D/Extra2D/Extra2D/src/scene/node.cpp)
- **内容**:
- 优化 `getLocalTransform()` 中的矩阵构造顺序
- 延迟计算 skew 矩阵(仅在需要时)
- **预期收益**: 减少不必要的矩阵乘法
### 阶段四:渲染与批量操作优化
#### 4.1 渲染命令收集完善
- **目标**: [node.cpp](file:///c:/Users/soulcoco/Desktop/Easy2D/Extra2D/Extra2D/src/scene/node.cpp)
- **内容**:
- 完善 `collectRenderCommands` 实现
- 添加 Z 序累积和递归收集
- **预期收益**: 支持多线程渲染命令收集
#### 4.2 批量操作接口
- **目标**: [node.h](file:///c:/Users/soulcoco/Desktop/Easy2D/Extra2D/Extra2D/include/extra2d/scene/node.h) 和 [node.cpp](file:///c:/Users/soulcoco/Desktop/Easy2D/Extra2D/Extra2D/src/scene/node.cpp)
- **内容**:
- 添加 `addChildren(std::vector<Ptr<Node>>&& children)` 批量添加
- 优化 `removeAllChildren` 使用批量处理
- **预期收益**: 减少多次操作的开销
---
**预计总工作量**: 4-6 小时
**优先级**: 高1.1, 1.2> 中2.1, 3.1> 低3.2, 4.1, 4.2

View File

@ -2,7 +2,6 @@
#include <extra2d/extra2d.h>
#include <sstream>
using namespace extra2d;
// ============================================================================
@ -104,7 +103,7 @@ public:
// 检查退出按键
auto &input = Application::instance().input();
if (input.isButtonPressed(GamepadButton::Start)) {
if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_START)) {
E2D_LOG_INFO("退出应用");
Application::instance().quit();
}
@ -123,8 +122,14 @@ private:
*/
void loadFonts() {
auto &resources = Application::instance().resources();
titleFont_ = resources.loadFont("assets/font.ttf", 60, true);
infoFont_ = resources.loadFont("assets/font.ttf", 28, true);
// 使用后备字体加载功能
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("无法加载标题字体");
@ -235,8 +240,10 @@ private:
// 程序入口
// ============================================================================
int main(int argc, char **argv)
{
extern "C" int main(int argc, char *argv[]) {
(void)argc;
(void)argv;
// 初始化日志系统
Logger::init();
Logger::setLevel(LogLevel::Debug);

View File

@ -1,7 +1,47 @@
#include <extra2d/extra2d.h>
#include <switch.h>
using namespace extra2d;
// ============================================================================
// 字体配置
// ============================================================================
/**
* @brief
* @return
*/
static std::vector<std::string> getFontCandidates() {
return {
"romfs:/assets/font.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 场景
@ -24,7 +64,7 @@ public:
// 加载字体(支持多种字体后备)
auto &resources = Application::instance().resources();
font_ = resources.loadFont("assets/font.ttf", 48, true);
font_ = loadFontWithFallbacks(resources, 48, true);
if (!font_) {
E2D_LOG_ERROR("字体加载失败,文字渲染将不可用!");
@ -38,12 +78,10 @@ public:
void onUpdate(float dt) override {
Scene::onUpdate(dt);
// 检查退出按键
// 检查退出按键START 按钮)
auto &input = Application::instance().input();
// 使用手柄 START 按钮退出 (GamepadButton::Start)
if (input.isButtonPressed(GamepadButton::Start)) {
E2D_LOG_INFO("退出应用 (START 按钮)");
if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_START)) {
E2D_LOG_INFO("退出应用");
Application::instance().quit();
}
}
@ -63,10 +101,14 @@ public:
float centerY = 360.0f; // 720 / 2
// 绘制 "你好世界" 文字(白色,居中)
renderer.drawText(*font_, "你好世界", Vec2(centerX - 100.0f, centerY),Color(1.0f, 1.0f, 1.0f, 1.0f));
Color white(1.0f, 1.0f, 1.0f, 1.0f);
renderer.drawText(*font_, "你好世界", Vec2(centerX - 100.0f, centerY),
white);
// 绘制提示文字(黄色)
renderer.drawText(*font_, "退出按键START 按钮)",Vec2(centerX - 80.0f, centerY + 50.0f), Color(1.0f, 1.0f, 0.0f, 1.0f));
Color yellow(1.0f, 1.0f, 0.0f, 1.0f);
renderer.drawText(*font_, "退出按键START 按钮)",
Vec2(centerX - 80.0f, centerY + 50.0f), yellow);
}
private:
@ -77,8 +119,27 @@ private:
// 程序入口
// ============================================================================
int main(int argc, char **argv)
{
/**
* @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);
@ -91,12 +152,7 @@ int main(int argc, char **argv)
auto &app = Application::instance();
// 配置应用
AppConfig config;
config.title = "Easy2D - Hello World";
config.width = 1280;
config.height = 720;
config.vsync = true;
config.fpsLimit = 60;
auto config = createAppConfig();
// 初始化应用
if (!app.init(config)) {

View File

@ -92,10 +92,6 @@ private:
class SpatialIndexDemoScene : public Scene {
public:
void onEnter() override {
// 必须先调用父类的 onEnter(),这样才能正确设置 running_ 状态
// 并触发子节点的 onAttachToScene将节点注册到空间索引
Scene::onEnter();
E2D_LOG_INFO("SpatialIndexDemoScene::onEnter - 引擎空间索引演示");
auto &app = Application::instance();
@ -105,8 +101,8 @@ public:
// 设置背景色
setBackgroundColor(Color(0.05f, 0.05f, 0.1f, 1.0f));
// 创建100个碰撞节点
createNodes(100);
// 创建1000个碰撞节点
createNodes(1000);
// 加载字体
loadFonts();
@ -115,17 +111,6 @@ public:
E2D_LOG_INFO("空间索引已启用: {}", isSpatialIndexingEnabled());
}
void onExit() override {
// 先清理 nodes_ 向量
nodes_.clear();
// 显式移除所有子节点,确保在场景析构前正确清理空间索引
// 这必须在 Scene::onExit() 之前调用,因为 onExit() 会将 running_ 设为 false
removeAllChildren();
Scene::onExit();
}
void onUpdate(float dt) override {
Scene::onUpdate(dt);
@ -156,23 +141,23 @@ public:
// 检查退出按键
auto &input = Application::instance().input();
if (input.isButtonPressed(GamepadButton::Start)) {
if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_START)) {
E2D_LOG_INFO("退出应用");
Application::instance().quit();
}
// 按A键添加节点
if (input.isButtonPressed(GamepadButton::A)) {
if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_A)) {
addNodes(100);
}
// 按B键减少节点
if (input.isButtonPressed(GamepadButton::B)) {
if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_B)) {
removeNodes(100);
}
// 按X键切换空间索引策略
if (input.isButtonPressed(GamepadButton::X)) {
if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_X)) {
toggleSpatialStrategy();
}
}
@ -200,8 +185,14 @@ private:
void loadFonts() {
auto &resources = Application::instance().resources();
titleFont_ = resources.loadFont("assets/font.ttf", 28, true);
infoFont_ = resources.loadFont("assets/font.ttf", 16, true);
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);
}
/**
@ -410,8 +401,10 @@ private:
// 程序入口
// ============================================================================
int main(int argc, char **argv)
{
extern "C" int main(int argc, char *argv[]) {
(void)argc;
(void)argv;
Logger::init();
Logger::setLevel(LogLevel::Debug);

View File

@ -21,24 +21,16 @@ class Camera;
// ============================================================================
// Application 配置
// ============================================================================
enum class PlatformType {
Auto = 0,
PC,
Switch
};
struct AppConfig {
String title = "Easy2D Application";
int width = 800;
int height = 600;
bool fullscreen = false;
bool resizable = true;
bool resizable = true; // 窗口是否可调整大小
bool vsync = true;
int fpsLimit = 0;
int fpsLimit = 0; // 0 = 不限制
BackendType renderBackend = BackendType::OpenGL;
int msaaSamples = 0;
PlatformType platform = PlatformType::Auto;
};
// ============================================================================

View File

@ -1,134 +1,134 @@
#pragma once
// SDL2 键码定义
#include <SDL2/SDL.h>
namespace extra2d {
// ============================================================================
// 键盘按键码 (基于 SDL2)
// 键盘按键码 (基于 GLFW)
// ============================================================================
namespace Key {
enum : int {
Unknown = SDLK_UNKNOWN,
Space = SDLK_SPACE,
Apostrophe = SDLK_QUOTE,
Comma = SDLK_COMMA,
Minus = SDLK_MINUS,
Period = SDLK_PERIOD,
Slash = SDLK_SLASH,
Num0 = SDLK_0,
Num1 = SDLK_1,
Num2 = SDLK_2,
Num3 = SDLK_3,
Num4 = SDLK_4,
Num5 = SDLK_5,
Num6 = SDLK_6,
Num7 = SDLK_7,
Num8 = SDLK_8,
Num9 = SDLK_9,
Semicolon = SDLK_SEMICOLON,
Equal = SDLK_EQUALS,
A = SDLK_a,
B = SDLK_b,
C = SDLK_c,
D = SDLK_d,
E = SDLK_e,
F = SDLK_f,
G = SDLK_g,
H = SDLK_h,
I = SDLK_i,
J = SDLK_j,
K = SDLK_k,
L = SDLK_l,
M = SDLK_m,
N = SDLK_n,
O = SDLK_o,
P = SDLK_p,
Q = SDLK_q,
R = SDLK_r,
S = SDLK_s,
T = SDLK_t,
U = SDLK_u,
V = SDLK_v,
W = SDLK_w,
X = SDLK_x,
Y = SDLK_y,
Z = SDLK_z,
LeftBracket = SDLK_LEFTBRACKET,
Backslash = SDLK_BACKSLASH,
RightBracket = SDLK_RIGHTBRACKET,
GraveAccent = SDLK_BACKQUOTE,
Escape = SDLK_ESCAPE,
Enter = SDLK_RETURN,
Tab = SDLK_TAB,
Backspace = SDLK_BACKSPACE,
Insert = SDLK_INSERT,
Delete = SDLK_DELETE,
Right = SDLK_RIGHT,
Left = SDLK_LEFT,
Down = SDLK_DOWN,
Up = SDLK_UP,
PageUp = SDLK_PAGEUP,
PageDown = SDLK_PAGEDOWN,
Home = SDLK_HOME,
End = SDLK_END,
CapsLock = SDLK_CAPSLOCK,
ScrollLock = SDLK_SCROLLLOCK,
NumLock = SDLK_NUMLOCKCLEAR,
PrintScreen = SDLK_PRINTSCREEN,
Pause = SDLK_PAUSE,
F1 = SDLK_F1,
F2 = SDLK_F2,
F3 = SDLK_F3,
F4 = SDLK_F4,
F5 = SDLK_F5,
F6 = SDLK_F6,
F7 = SDLK_F7,
F8 = SDLK_F8,
F9 = SDLK_F9,
F10 = SDLK_F10,
F11 = SDLK_F11,
F12 = SDLK_F12,
F13 = SDLK_F13,
F14 = SDLK_F14,
F15 = SDLK_F15,
F16 = SDLK_F16,
F17 = SDLK_F17,
F18 = SDLK_F18,
F19 = SDLK_F19,
F20 = SDLK_F20,
F21 = SDLK_F21,
F22 = SDLK_F22,
F23 = SDLK_F23,
F24 = SDLK_F24,
KP0 = SDLK_KP_0,
KP1 = SDLK_KP_1,
KP2 = SDLK_KP_2,
KP3 = SDLK_KP_3,
KP4 = SDLK_KP_4,
KP5 = SDLK_KP_5,
KP6 = SDLK_KP_6,
KP7 = SDLK_KP_7,
KP8 = SDLK_KP_8,
KP9 = SDLK_KP_9,
KPDecimal = SDLK_KP_PERIOD,
KPDivide = SDLK_KP_DIVIDE,
KPMultiply = SDLK_KP_MULTIPLY,
KPSubtract = SDLK_KP_MINUS,
KPAdd = SDLK_KP_PLUS,
KPEnter = SDLK_KP_ENTER,
KPEqual = SDLK_KP_EQUALS,
LeftShift = SDLK_LSHIFT,
LeftControl = SDLK_LCTRL,
LeftAlt = SDLK_LALT,
LeftSuper = SDLK_LGUI,
RightShift = SDLK_RSHIFT,
RightControl = SDLK_RCTRL,
RightAlt = SDLK_RALT,
RightSuper = SDLK_RGUI,
Menu = SDLK_MENU,
Last = SDLK_MENU
Unknown = -1,
Space = 32,
Apostrophe = 39,
Comma = 44,
Minus = 45,
Period = 46,
Slash = 47,
Num0 = 48,
Num1 = 49,
Num2 = 50,
Num3 = 51,
Num4 = 52,
Num5 = 53,
Num6 = 54,
Num7 = 55,
Num8 = 56,
Num9 = 57,
Semicolon = 59,
Equal = 61,
A = 65,
B = 66,
C = 67,
D = 68,
E = 69,
F = 70,
G = 71,
H = 72,
I = 73,
J = 74,
K = 75,
L = 76,
M = 77,
N = 78,
O = 79,
P = 80,
Q = 81,
R = 82,
S = 83,
T = 84,
U = 85,
V = 86,
W = 87,
X = 88,
Y = 89,
Z = 90,
LeftBracket = 91,
Backslash = 92,
RightBracket = 93,
GraveAccent = 96,
World1 = 161,
World2 = 162,
Escape = 256,
Enter = 257,
Tab = 258,
Backspace = 259,
Insert = 260,
Delete = 261,
Right = 262,
Left = 263,
Down = 264,
Up = 265,
PageUp = 266,
PageDown = 267,
Home = 268,
End = 269,
CapsLock = 280,
ScrollLock = 281,
NumLock = 282,
PrintScreen = 283,
Pause = 284,
F1 = 290,
F2 = 291,
F3 = 292,
F4 = 293,
F5 = 294,
F6 = 295,
F7 = 296,
F8 = 297,
F9 = 298,
F10 = 299,
F11 = 300,
F12 = 301,
F13 = 302,
F14 = 303,
F15 = 304,
F16 = 305,
F17 = 306,
F18 = 307,
F19 = 308,
F20 = 309,
F21 = 310,
F22 = 311,
F23 = 312,
F24 = 313,
F25 = 314,
KP0 = 320,
KP1 = 321,
KP2 = 322,
KP3 = 323,
KP4 = 324,
KP5 = 325,
KP6 = 326,
KP7 = 327,
KP8 = 328,
KP9 = 329,
KPDecimal = 330,
KPDivide = 331,
KPMultiply = 332,
KPSubtract = 333,
KPAdd = 334,
KPEnter = 335,
KPEqual = 336,
LeftShift = 340,
LeftControl = 341,
LeftAlt = 342,
LeftSuper = 343,
RightShift = 344,
RightControl = 345,
RightAlt = 346,
RightSuper = 347,
Menu = 348,
Last = Menu
};
}
@ -137,12 +137,12 @@ enum : int {
// ============================================================================
namespace Mod {
enum : int {
Shift = KMOD_SHIFT,
Control = KMOD_CTRL,
Alt = KMOD_ALT,
Super = KMOD_GUI,
CapsLock = KMOD_CAPS,
NumLock = KMOD_NUM
Shift = 0x0001,
Control = 0x0002,
Alt = 0x0004,
Super = 0x0008,
CapsLock = 0x0010,
NumLock = 0x0020
};
}
@ -171,22 +171,22 @@ enum : int {
// ============================================================================
namespace GamepadButton {
enum : int {
A = SDL_CONTROLLER_BUTTON_A,
B = SDL_CONTROLLER_BUTTON_B,
X = SDL_CONTROLLER_BUTTON_X,
Y = SDL_CONTROLLER_BUTTON_Y,
LeftBumper = SDL_CONTROLLER_BUTTON_LEFTSHOULDER,
RightBumper = SDL_CONTROLLER_BUTTON_RIGHTSHOULDER,
Back = SDL_CONTROLLER_BUTTON_BACK,
Start = SDL_CONTROLLER_BUTTON_START,
Guide = SDL_CONTROLLER_BUTTON_GUIDE,
LeftThumb = SDL_CONTROLLER_BUTTON_LEFTSTICK,
RightThumb = SDL_CONTROLLER_BUTTON_RIGHTSTICK,
DPadUp = SDL_CONTROLLER_BUTTON_DPAD_UP,
DPadRight = SDL_CONTROLLER_BUTTON_DPAD_RIGHT,
DPadDown = SDL_CONTROLLER_BUTTON_DPAD_DOWN,
DPadLeft = SDL_CONTROLLER_BUTTON_DPAD_LEFT,
Last = SDL_CONTROLLER_BUTTON_DPAD_LEFT,
A = 0,
B = 1,
X = 2,
Y = 3,
LeftBumper = 4,
RightBumper = 5,
Back = 6,
Start = 7,
Guide = 8,
LeftThumb = 9,
RightThumb = 10,
DPadUp = 11,
DPadRight = 12,
DPadDown = 13,
DPadLeft = 14,
Last = DPadLeft,
Cross = A,
Circle = B,
Square = X,
@ -199,13 +199,13 @@ enum : int {
// ============================================================================
namespace GamepadAxis {
enum : int {
LeftX = SDL_CONTROLLER_AXIS_LEFTX,
LeftY = SDL_CONTROLLER_AXIS_LEFTY,
RightX = SDL_CONTROLLER_AXIS_RIGHTX,
RightY = SDL_CONTROLLER_AXIS_RIGHTY,
LeftTrigger = SDL_CONTROLLER_AXIS_TRIGGERLEFT,
RightTrigger = SDL_CONTROLLER_AXIS_TRIGGERRIGHT,
Last = SDL_CONTROLLER_AXIS_TRIGGERRIGHT
LeftX = 0,
LeftY = 1,
RightX = 2,
RightY = 3,
LeftTrigger = 4,
RightTrigger = 5,
Last = RightTrigger
};
}

View File

@ -95,7 +95,3 @@
// Script
#include <extra2d/script/script_engine.h>
#include <extra2d/script/script_node.h>
#ifdef __SWITCH__
#include <switch.h>
#endif

View File

@ -6,7 +6,6 @@
#include <glad/glad.h>
#include <memory>
#include <string>
namespace extra2d {

View File

@ -3,14 +3,13 @@
#include <array>
#include <extra2d/core/math_types.h>
#include <extra2d/core/types.h>
#include <extra2d/event/input_codes.h>
#include <SDL.h>
namespace extra2d {
// ============================================================================
// 鼠标按钮枚举
// 鼠标按钮枚举 (保留接口兼容性)
// ============================================================================
enum class MouseButton {
Left = 0,
@ -25,15 +24,14 @@ enum class MouseButton {
};
// ============================================================================
// Input 类 - 跨平台输入管理
// 支持: 键盘、鼠标、手柄、触摸屏
// Input 类 - SDL2 GameController + Touch 输入管理
// ============================================================================
class Input {
public:
Input();
~Input();
// 初始化
// 初始化 (使用 SDL2 GameController API)
void init();
void shutdown();
@ -41,14 +39,14 @@ public:
void update();
// ------------------------------------------------------------------------
// 键盘输入
// 键盘输入 (映射到手柄按钮)
// ------------------------------------------------------------------------
bool isKeyDown(int keyCode) const;
bool isKeyPressed(int keyCode) const;
bool isKeyReleased(int keyCode) const;
// ------------------------------------------------------------------------
// 手柄按钮
// 手柄按钮 (通过 SDL_GameController)
// ------------------------------------------------------------------------
bool isButtonDown(int button) const;
bool isButtonPressed(int button) const;
@ -59,7 +57,7 @@ public:
Vec2 getRightStick() const;
// ------------------------------------------------------------------------
// 鼠标输入
// 鼠标输入 (映射到触摸屏)
// ------------------------------------------------------------------------
bool isMouseDown(MouseButton button) const;
bool isMousePressed(MouseButton button) const;
@ -67,15 +65,15 @@ public:
Vec2 getMousePosition() const;
Vec2 getMouseDelta() const;
float getMouseScroll() const { return mouseScroll_; }
float getMouseScrollDelta() const { return mouseScroll_ - prevMouseScroll_; }
float getMouseScroll() const { return 0.0f; }
float getMouseScrollDelta() const { return 0.0f; }
void setMousePosition(const Vec2 &position);
void setMouseVisible(bool visible);
void setMouseLocked(bool locked);
// ------------------------------------------------------------------------
// 触摸屏 (Switch 原生支持PC 端模拟或禁用)
// 触摸屏
// ------------------------------------------------------------------------
bool isTouching() const { return touching_; }
Vec2 getTouchPosition() const { return touchPosition_; }
@ -89,14 +87,9 @@ public:
private:
static constexpr int MAX_BUTTONS = SDL_CONTROLLER_BUTTON_MAX;
static constexpr int MAX_KEYS = SDL_NUM_SCANCODES;
SDL_GameController *controller_;
// 键盘状态 (PC 端使用)
std::array<bool, MAX_KEYS> keysDown_;
std::array<bool, MAX_KEYS> prevKeysDown_;
// 手柄按钮状态
std::array<bool, MAX_BUTTONS> buttonsDown_;
std::array<bool, MAX_BUTTONS> prevButtonsDown_;
@ -107,35 +100,15 @@ private:
float rightStickX_;
float rightStickY_;
// 鼠标状态 (PC 端使用)
Vec2 mousePosition_;
Vec2 prevMousePosition_;
float mouseScroll_;
float prevMouseScroll_;
std::array<bool, 8> mouseButtonsDown_;
std::array<bool, 8> prevMouseButtonsDown_;
// 触摸屏状态 (Switch 原生)
// 触摸屏状态
bool touching_;
bool prevTouching_;
Vec2 touchPosition_;
Vec2 prevTouchPosition_;
int touchCount_;
// 映射键盘 keyCode 到 SDL GameController 按钮 (Switch 兼容模式)
// 映射键盘 keyCode 到 SDL GameController 按钮
SDL_GameControllerButton mapKeyToButton(int keyCode) const;
// 更新键盘状态
void updateKeyboard();
// 更新鼠标状态
void updateMouse();
// 更新手柄状态
void updateGamepad();
// 更新触摸屏状态
void updateTouch();
};
} // namespace extra2d

View File

@ -0,0 +1,79 @@
#pragma once
/**
* @file switch_compat.h
* @brief Nintendo Switch
*
* Switch
*/
#ifdef __SWITCH__
// ============================================================================
// Switch 平台包含
// ============================================================================
#include <switch.h>
#include <malloc.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <string>
// ============================================================================
// Switch 特定的内存操作
// ============================================================================
// 不需要特殊处理libnx已提供malloc/free
// ============================================================================
// Switch 文件系统相关
// ============================================================================
// RomFS路径前缀
#define SWITCH_ROMFS_PREFIX "romfs:/"
// RomFS 根路径常量
namespace extra2d {
namespace romfs {
static constexpr const char* ROMFS_ROOT = "romfs:/";
// 检查文件是否存在于 romfs 中
inline bool fileExists(const char* path) {
struct stat st;
return stat(path, &st) == 0;
}
// 检查路径是否为 romfs 路径
inline bool isRomfsPath(const char* path) {
return path && (strncmp(path, "romfs:/", 7) == 0 || strncmp(path, "romfs:\\", 7) == 0);
}
// 构建 romfs 完整路径
inline std::string makePath(const char* relativePath) {
std::string result = ROMFS_ROOT;
result += relativePath;
return result;
}
} // namespace romfs
} // namespace extra2d
// ============================================================================
// Switch 调试输出
// ============================================================================
#ifdef E2D_DEBUG
#define SWITCH_DEBUG_PRINTF(fmt, ...) printf("[Extra2D] " fmt "\n", ##__VA_ARGS__)
#else
#define SWITCH_DEBUG_PRINTF(fmt, ...) ((void)0)
#endif
// ============================================================================
// Switch 特定的编译器属性
// ============================================================================
#define SWITCH_LIKELY(x) __builtin_expect(!!(x), 1)
#define SWITCH_UNLIKELY(x) __builtin_expect(!!(x), 0)
// ============================================================================
// Switch 特定的平台检查宏
// ============================================================================
#define IS_SWITCH_PLATFORM 1
#endif // __SWITCH__

View File

@ -20,18 +20,15 @@ struct WindowConfig {
String title = "Extra2D Application";
int width = 1280;
int height = 720;
bool fullscreen = true;
bool fullscreen = true; // Switch 始终全屏
bool resizable = false;
bool vsync = true;
int msaaSamples = 0;
bool centerWindow = true;
bool enableCursors = true;
bool enableDpiScale = true;
bool fullscreenDesktop = true; // true: SDL_WINDOW_FULLSCREEN_DESKTOP, false: SDL_WINDOW_FULLSCREEN
bool centerWindow = false;
};
// ============================================================================
// 鼠标光标形状枚举
// 鼠标光标形状枚举 (保留接口兼容性Switch 上无效)
// ============================================================================
enum class CursorShape {
Arrow,
@ -47,7 +44,6 @@ enum class CursorShape {
// ============================================================================
// Window 类 - SDL2 Window + GLES 3.2 封装
// 支持平台: Nintendo Switch, Windows, Linux, macOS
// ============================================================================
class Window {
public:
@ -64,7 +60,7 @@ public:
bool shouldClose() const;
void setShouldClose(bool close);
// 窗口属性
// 窗口属性 (Switch 上大部分为空操作)
void setTitle(const String& title);
void setSize(int width, int height);
void setPosition(int x, int y);
@ -76,19 +72,19 @@ public:
int getWidth() const { return width_; }
int getHeight() const { return height_; }
Size getSize() const { return Size(static_cast<float>(width_), static_cast<float>(height_)); }
Vec2 getPosition() const;
bool isFullscreen() const { return fullscreen_; }
Vec2 getPosition() const { return Vec2::Zero(); }
bool isFullscreen() const { return true; }
bool isVSync() const { return vsync_; }
// DPI 缩放 (PC 端自动检测,Switch 固定 1.0)
float getContentScaleX() const;
float getContentScaleY() const;
Vec2 getContentScale() const;
// DPI 缩放 (Switch 固定 1.0)
float getContentScaleX() const { return 1.0f; }
float getContentScaleY() const { return 1.0f; }
Vec2 getContentScale() const { return Vec2(1.0f, 1.0f); }
// 窗口状态
bool isFocused() const { return focused_; }
bool isMinimized() const;
bool isMaximized() const;
bool isFocused() const { return true; }
bool isMinimized() const { return false; }
bool isMaximized() const { return true; }
// 获取 SDL2 窗口和 GL 上下文
SDL_Window* getSDLWindow() const { return sdlWindow_; }
@ -105,10 +101,9 @@ public:
// 获取输入管理器
Input* getInput() const { return input_.get(); }
// 光标操作 (PC 端有效,Switch 上为空操作)
// 光标操作 (Switch 上为空操作)
void setCursor(CursorShape shape);
void resetCursor();
void setMouseVisible(bool visible);
// 窗口回调
using ResizeCallback = std::function<void(int width, int height)>;
@ -123,18 +118,11 @@ private:
// SDL2 状态
SDL_Window* sdlWindow_;
SDL_GLContext glContext_;
SDL_Cursor* sdlCursors_[9]; // 光标缓存
SDL_Cursor* currentCursor_;
int width_;
int height_;
bool vsync_;
bool shouldClose_;
bool fullscreen_;
bool focused_;
float contentScaleX_;
float contentScaleY_;
bool enableDpiScale_;
void* userData_;
EventQueue* eventQueue_;
UniquePtr<Input> input_;
@ -143,11 +131,8 @@ private:
FocusCallback focusCallback_;
CloseCallback closeCallback_;
bool initSDL(const WindowConfig& config);
bool initSDL();
void deinitSDL();
void initCursors();
void deinitCursors();
void updateContentScale();
};
} // namespace extra2d

View File

@ -8,6 +8,7 @@
#include <mutex>
#include <string>
#include <unordered_map>
#include <vector>
namespace extra2d {
@ -21,6 +22,27 @@ public:
// ------------------------------------------------------------------------
static ResourceManager &getInstance();
// ------------------------------------------------------------------------
// 搜索路径管理
// ------------------------------------------------------------------------
/// 添加资源搜索路径
void addSearchPath(const std::string &path);
/// 移除资源搜索路径
void removeSearchPath(const std::string &path);
/// 清空所有搜索路径
void clearSearchPaths();
/// 获取搜索路径列表
const std::vector<std::string> &getSearchPaths() const {
return searchPaths_;
}
/// 查找资源文件完整路径
std::string findResourcePath(const std::string &filename) const;
// ------------------------------------------------------------------------
// 纹理资源
// ------------------------------------------------------------------------
@ -61,6 +83,14 @@ public:
Ptr<FontAtlas> loadFont(const std::string &filepath, int fontSize,
bool useSDF = false);
/// 尝试从多个候选路径加载字体,返回第一个成功加载的字体
Ptr<FontAtlas> loadFontWithFallbacks(const std::vector<std::string> &fontPaths,
int fontSize, bool useSDF = false);
/// 加载字体,使用默认系统字体作为后备
Ptr<FontAtlas> loadFontWithDefaultFallback(const std::string &filepath,
int fontSize, bool useSDF = false);
/// 通过key获取已缓存的字体图集
Ptr<FontAtlas> getFont(const std::string &key) const;
@ -121,6 +151,9 @@ public:
mutable std::mutex fontMutex_;
mutable std::mutex soundMutex_;
// 搜索路径
std::vector<std::string> searchPaths_;
// 资源缓存 - 使用弱指针实现自动清理
std::unordered_map<std::string, WeakPtr<Texture>> textureCache_;
std::unordered_map<std::string, WeakPtr<FontAtlas>> fontCache_;

View File

@ -30,13 +30,6 @@ public:
// 层级管理
// ------------------------------------------------------------------------
void addChild(Ptr<Node> child);
/**
* @brief
* @param children
*/
void addChildren(std::vector<Ptr<Node>> &&children);
void removeChild(Ptr<Node> child);
void removeChildByName(const std::string &name);
void removeFromParent();
@ -168,62 +161,42 @@ protected:
float getOpacityRef() { return opacity_; }
private:
// ==========================================================================
// 成员变量按类型大小降序排列,减少内存对齐填充
// 64位系统对齐std::string(32) > glm::mat4(64) > std::vector(24) >
// double(8) > float(4) > int(4) > bool(1)
// ==========================================================================
// 层级
WeakPtr<Node> parent_;
std::vector<Ptr<Node>> children_;
bool childrenOrderDirty_ = false;
// 1. 大块内存64字节
mutable glm::mat4 localTransform_; // 64 bytes
mutable glm::mat4 worldTransform_; // 64 bytes
// 变换
Vec2 position_ = Vec2::Zero();
float rotation_ = 0.0f;
Vec2 scale_ = Vec2(1.0f, 1.0f);
Vec2 anchor_ = Vec2(0.5f, 0.5f);
Vec2 skew_ = Vec2::Zero();
float opacity_ = 1.0f;
bool visible_ = true;
int zOrder_ = 0;
// 2. 字符串和容器24-32字节
std::string name_; // 32 bytes
std::vector<Ptr<Node>> children_; // 24 bytes
// 缓存
mutable bool transformDirty_ = true;
mutable bool worldTransformDirty_ = true; // 世界变换独立的脏标记
mutable glm::mat4 localTransform_;
mutable glm::mat4 worldTransform_;
// 3. 子节点索引(加速查找)
std::unordered_map<std::string, WeakPtr<Node>> nameIndex_; // 56 bytes
std::unordered_map<int, WeakPtr<Node>> tagIndex_; // 56 bytes
// 元数据
std::string name_;
int tag_ = -1;
// 4. 动作系统(使用 unordered_map 加速 tag 查找)
std::unordered_map<int, Ptr<Action>> actionByTag_; // 56 bytes
std::vector<Ptr<Action>> actions_; // 24 bytes无 tag 的 Action
// 状态
bool running_ = false;
Scene *scene_ = nullptr;
bool spatialIndexed_ = true; // 是否参与空间索引
Rect lastSpatialBounds_; // 上一次的空间索引边界(用于检测变化)
// 5. 事件分发器
EventDispatcher eventDispatcher_; // 大小取决于实现
// 动作
std::vector<Ptr<Action>> actions_;
// 6. 父节点引用
WeakPtr<Node> parent_; // 16 bytes
// 7. 变换属性(按访问频率分组)
Vec2 position_ = Vec2::Zero(); // 8 bytes
Vec2 scale_ = Vec2(1.0f, 1.0f); // 8 bytes
Vec2 anchor_ = Vec2(0.5f, 0.5f); // 8 bytes
Vec2 skew_ = Vec2::Zero(); // 8 bytes
// 8. 边界框(用于空间索引)
Rect lastSpatialBounds_; // 16 bytes
// 9. 浮点属性
float rotation_ = 0.0f; // 4 bytes
float opacity_ = 1.0f; // 4 bytes
// 10. 整数属性
int zOrder_ = 0; // 4 bytes
int tag_ = -1; // 4 bytes
// 11. 场景指针
Scene *scene_ = nullptr; // 8 bytes
// 12. 布尔标志(打包在一起)
mutable bool transformDirty_ = true; // 1 byte
mutable bool worldTransformDirty_ = true; // 1 byte
bool childrenOrderDirty_ = false; // 1 byte
bool visible_ = true; // 1 byte
bool running_ = false; // 1 byte
bool spatialIndexed_ = true; // 1 byte
// 填充 2 bytes 到 8 字节对齐
// 事件
EventDispatcher eventDispatcher_;
};
} // namespace extra2d

View File

@ -1,119 +1,24 @@
#pragma once
#include <extra2d/core/types.h>
#include <functional>
#include <string>
#include <vector>
namespace extra2d {
// ============================================================================
// 存档类型枚举
// ============================================================================
enum class SaveDataType {
Account, // 用户存档(与特定用户关联)
Common, // 公共存档(所有用户共享)
Cache, // 缓存数据(可删除)
Device, // 设备存档
Temporary, // 临时数据
};
// ============================================================================
// 用户ID结构封装 Switch AccountUid
// ============================================================================
struct UserId {
uint64_t uid[2] = {0, 0};
bool isValid() const { return uid[0] != 0 || uid[1] != 0; }
bool operator==(const UserId &other) const {
return uid[0] == other.uid[0] && uid[1] == other.uid[1];
}
bool operator!=(const UserId &other) const { return !(*this == other); }
};
// ============================================================================
// DataStore 类 - 数据持久化(支持 Switch 存档系统)
// DataStore 类 - INI 文件数据持久化
// ============================================================================
class DataStore {
public:
DataStore();
~DataStore();
// ------------------------------------------------------------------------
// 文件操作
// ------------------------------------------------------------------------
/// 加载 INI 文件
bool load(const std::string &filename);
/// 保存到 INI 文件
bool save(const std::string &filename);
/// 获取当前文件名
const std::string &getFilename() const { return filename_; }
// ------------------------------------------------------------------------
// Switch 存档系统支持
// ------------------------------------------------------------------------
/**
* @brief Switch
* @param type
* @param userId IDAccount
* @param mountName "save"
* @return
*/
bool mountSaveData(SaveDataType type = SaveDataType::Account,
const UserId &userId = UserId(),
const std::string &mountName = "save");
/**
* @brief
* @param mountName
*/
void unmountSaveData(const std::string &mountName = "save");
/**
* @brief
* @param mountName
* @return
*/
bool commitSaveData(const std::string &mountName = "save");
/**
* @brief
*/
bool isSaveDataMounted() const { return saveDataMounted_; }
/**
* @brief
*/
std::string getSaveDataPath(const std::string &path = "") const;
// ------------------------------------------------------------------------
// 用户账户管理
// ------------------------------------------------------------------------
/**
* @brief ID
* @return IDID
*/
static UserId getCurrentUserId();
/**
* @brief ID
*/
void setDefaultUserId(const UserId &userId) { defaultUserId_ = userId; }
/**
* @brief ID
*/
UserId getDefaultUserId() const { return defaultUserId_; }
// ------------------------------------------------------------------------
// 数据读写
// ------------------------------------------------------------------------
/// 获取字符串值
std::string getString(const std::string &section, const std::string &key,
const std::string &defaultValue = "");
@ -159,59 +64,13 @@ public:
/// 清除所有数据
void clear();
// ------------------------------------------------------------------------
// 事务支持
// ------------------------------------------------------------------------
/**
* @brief
*/
void beginTransaction();
/**
* @brief
* @return
*/
bool commit();
/**
* @brief
*/
void rollback();
/**
* @brief
*/
bool isInTransaction() const { return inTransaction_; }
// ------------------------------------------------------------------------
// 工具方法
// ------------------------------------------------------------------------
/// 获取所有 section 名称
std::vector<std::string> getAllSections() const;
/// 获取指定 section 的所有 key
std::vector<std::string> getAllKeys(const std::string &section) const;
/// 从存档加载(自动处理挂载路径)
bool loadFromSave(const std::string &path);
/// 保存到存档(自动处理挂载路径和提交)
bool saveToSave(const std::string &path);
/// 获取当前文件名
const std::string &getFilename() const { return filename_; }
private:
class Impl;
UniquePtr<Impl> impl_;
std::string filename_;
std::string mountName_;
UserId defaultUserId_;
bool saveDataMounted_ = false;
bool inTransaction_ = false;
bool dirty_ = false;
// 内部辅助方法
bool internalSave(const std::string &filename);
};
} // namespace extra2d

View File

@ -13,29 +13,18 @@
#include <extra2d/utils/timer.h>
#include <chrono>
#include <switch.h>
#include <thread>
#include <time.h>
#ifdef __SWITCH__
#include <switch.h>
#endif
namespace extra2d {
// 获取当前时间(秒)
static double getTimeSeconds() {
#ifdef __SWITCH__
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return static_cast<double>(ts.tv_sec) +
static_cast<double>(ts.tv_nsec) / 1000000000.0;
#else
// PC 平台使用 chrono
using namespace std::chrono;
auto now = steady_clock::now();
auto duration = now.time_since_epoch();
return duration_cast<std::chrono::duration<double>>(duration).count();
#endif
}
Application &Application::instance() {
@ -53,20 +42,8 @@ bool Application::init(const AppConfig &config) {
config_ = config;
// 确定平台类型
PlatformType platform = config_.platform;
if (platform == PlatformType::Auto) {
#ifdef __SWITCH__
platform = PlatformType::Switch;
#else
platform = PlatformType::PC;
#endif
}
if (platform == PlatformType::Switch) {
#ifdef __SWITCH__
// ========================================
// 1. 初始化 RomFS 文件系统Switch 平台
// 1. 初始化 RomFS 文件系统(必须在 SDL_Init 之前)
// ========================================
Result rc;
rc = romfsInit();
@ -77,15 +54,13 @@ bool Application::init(const AppConfig &config) {
}
// ========================================
// 2. 初始化 nxlink 调试输出Switch 平台
// 2. 初始化 nxlink 调试输出(可选
// ========================================
rc = socketInitializeDefault();
if (R_FAILED(rc)) {
E2D_LOG_WARN(
"socketInitializeDefault failed, nxlink will not be available");
}
#endif
}
// ========================================
// 3. 创建窗口(包含 SDL_Init + GLES 3.2 上下文创建)
@ -95,19 +70,8 @@ bool Application::init(const AppConfig &config) {
winConfig.title = config.title;
winConfig.width = 1280;
winConfig.height = 720;
if (platform == PlatformType::Switch) {
winConfig.fullscreen = true;
winConfig.fullscreenDesktop = false; // Switch 使用固定分辨率全屏
winConfig.resizable = false;
winConfig.enableCursors = false;
winConfig.enableDpiScale = false;
} else {
// PC 平台默认窗口模式
winConfig.fullscreen = config.fullscreen;
winConfig.resizable = true;
winConfig.enableCursors = true;
winConfig.enableDpiScale = true;
}
winConfig.vsync = config.vsync;
winConfig.msaaSamples = config.msaaSamples;
@ -137,7 +101,7 @@ bool Application::init(const AppConfig &config) {
camera_ = makeUnique<Camera>(0, static_cast<float>(window_->getWidth()),
static_cast<float>(window_->getHeight()), 0);
// 窗口大小回调
// 窗口大小回调Switch 上不会触发,但保留接口)
window_->setResizeCallback([this](int width, int height) {
if (camera_) {
camera_->setViewport(0, static_cast<float>(width),
@ -155,6 +119,9 @@ bool Application::init(const AppConfig &config) {
// 初始化音频引擎
AudioEngine::getInstance().initialize();
// 添加 romfs:/ 到资源搜索路径
resourceManager_->addSearchPath("romfs:/");
initialized_ = true;
running_ = true;
@ -171,11 +138,6 @@ void Application::shutdown() {
// 打印 VRAM 统计
VRAMManager::getInstance().printStats();
// 先结束所有场景,确保 onExit() 被正确调用
if (sceneManager_) {
sceneManager_->end();
}
// 清理子系统
sceneManager_.reset();
resourceManager_.reset();
@ -199,21 +161,9 @@ void Application::shutdown() {
window_.reset();
}
// Switch 平台清理
PlatformType platform = config_.platform;
if (platform == PlatformType::Auto) {
#ifdef __SWITCH__
platform = PlatformType::Switch;
#else
platform = PlatformType::PC;
#endif
}
if (platform == PlatformType::Switch) {
#ifdef __SWITCH__
// Switch 清理
romfsExit();
socketExit();
#endif
}
initialized_ = false;
running_ = false;
@ -229,17 +179,10 @@ void Application::run() {
lastFrameTime_ = getTimeSeconds();
#ifdef __SWITCH__
// SDL2 on Switch 内部已处理 appletMainLoop
while (running_ && !window_->shouldClose()) {
mainLoop();
}
#else
// PC 平台主循环
while (running_ && !window_->shouldClose()) {
mainLoop();
}
#endif
}
void Application::quit() {

View File

@ -0,0 +1,6 @@
#include <extra2d/graphics/render_command.h>
namespace extra2d {
// This file exists to allow compilation of the render_command header
// The RenderCommand struct is header-only for performance
} // namespace extra2d

View File

@ -5,19 +5,11 @@
namespace extra2d {
Input::Input()
: controller_(nullptr),
leftStickX_(0.0f), leftStickY_(0.0f),
rightStickX_(0.0f), rightStickY_(0.0f),
mouseScroll_(0.0f), prevMouseScroll_(0.0f),
touching_(false), prevTouching_(false), touchCount_(0) {
// 初始化所有状态数组
keysDown_.fill(false);
prevKeysDown_.fill(false);
: controller_(nullptr), leftStickX_(0.0f), leftStickY_(0.0f),
rightStickX_(0.0f), rightStickY_(0.0f), touching_(false),
prevTouching_(false), touchCount_(0) {
buttonsDown_.fill(false);
prevButtonsDown_.fill(false);
mouseButtonsDown_.fill(false);
prevMouseButtonsDown_.fill(false);
}
Input::~Input() { shutdown(); }
@ -36,16 +28,8 @@ void Input::init() {
}
if (!controller_) {
E2D_LOG_WARN("No game controller found");
E2D_LOG_WARN("No game controller found, input may be limited");
}
// PC 端获取初始鼠标状态
#ifndef PLATFORM_SWITCH
int mouseX, mouseY;
SDL_GetMouseState(&mouseX, &mouseY);
mousePosition_ = Vec2(static_cast<float>(mouseX), static_cast<float>(mouseY));
prevMousePosition_ = mousePosition_;
#endif
}
void Input::shutdown() {
@ -57,49 +41,10 @@ void Input::shutdown() {
void Input::update() {
// 保存上一帧状态
prevKeysDown_ = keysDown_;
prevButtonsDown_ = buttonsDown_;
prevMouseButtonsDown_ = mouseButtonsDown_;
prevMousePosition_ = mousePosition_;
prevMouseScroll_ = mouseScroll_;
prevTouching_ = touching_;
prevTouchPosition_ = touchPosition_;
// 更新各输入设备状态
updateKeyboard();
updateMouse();
updateGamepad();
updateTouch();
}
void Input::updateKeyboard() {
// 获取当前键盘状态
const Uint8* state = SDL_GetKeyboardState(nullptr);
for (int i = 0; i < MAX_KEYS; ++i) {
keysDown_[i] = state[i] != 0;
}
}
void Input::updateMouse() {
#ifndef PLATFORM_SWITCH
// 更新鼠标位置
int mouseX, mouseY;
Uint32 buttonState = SDL_GetMouseState(&mouseX, &mouseY);
mousePosition_ = Vec2(static_cast<float>(mouseX), static_cast<float>(mouseY));
// 更新鼠标按钮状态
mouseButtonsDown_[0] = (buttonState & SDL_BUTTON(SDL_BUTTON_LEFT)) != 0;
mouseButtonsDown_[1] = (buttonState & SDL_BUTTON(SDL_BUTTON_RIGHT)) != 0;
mouseButtonsDown_[2] = (buttonState & SDL_BUTTON(SDL_BUTTON_MIDDLE)) != 0;
mouseButtonsDown_[3] = (buttonState & SDL_BUTTON(SDL_BUTTON_X1)) != 0;
mouseButtonsDown_[4] = (buttonState & SDL_BUTTON(SDL_BUTTON_X2)) != 0;
// 处理鼠标滚轮事件(需要在事件循环中处理,这里简化处理)
// 实际滚轮值通过 SDL_MOUSEWHEEL 事件更新
#endif
}
void Input::updateGamepad() {
if (controller_) {
// 更新按钮状态
for (int i = 0; i < MAX_BUTTONS; ++i) {
@ -110,22 +55,23 @@ void Input::updateGamepad() {
// 读取摇杆(归一化到 -1.0 ~ 1.0
leftStickX_ = static_cast<float>(SDL_GameControllerGetAxis(
controller_, SDL_CONTROLLER_AXIS_LEFTX)) / 32767.0f;
controller_, SDL_CONTROLLER_AXIS_LEFTX)) /
32767.0f;
leftStickY_ = static_cast<float>(SDL_GameControllerGetAxis(
controller_, SDL_CONTROLLER_AXIS_LEFTY)) / 32767.0f;
controller_, SDL_CONTROLLER_AXIS_LEFTY)) /
32767.0f;
rightStickX_ = static_cast<float>(SDL_GameControllerGetAxis(
controller_, SDL_CONTROLLER_AXIS_RIGHTX)) / 32767.0f;
controller_, SDL_CONTROLLER_AXIS_RIGHTX)) /
32767.0f;
rightStickY_ = static_cast<float>(SDL_GameControllerGetAxis(
controller_, SDL_CONTROLLER_AXIS_RIGHTY)) / 32767.0f;
controller_, SDL_CONTROLLER_AXIS_RIGHTY)) /
32767.0f;
} else {
buttonsDown_.fill(false);
leftStickX_ = leftStickY_ = rightStickX_ = rightStickY_ = 0.0f;
}
}
void Input::updateTouch() {
#ifdef PLATFORM_SWITCH
// Switch 原生触摸屏支持
// 更新触摸屏SDL2 Touch API
SDL_TouchID touchId = SDL_GetTouchDevice(0);
if (touchId != 0) {
touchCount_ = SDL_GetNumTouchFingers(touchId);
@ -145,39 +91,10 @@ void Input::updateTouch() {
touchCount_ = 0;
touching_ = false;
}
#else
// PC 端:触摸屏可选支持(如果有触摸设备)
SDL_TouchID touchId = SDL_GetTouchDevice(0);
if (touchId != 0) {
touchCount_ = SDL_GetNumTouchFingers(touchId);
if (touchCount_ > 0) {
SDL_Finger *finger = SDL_GetTouchFinger(touchId, 0);
if (finger) {
touching_ = true;
// PC 端需要根据窗口大小转换坐标
int windowWidth, windowHeight;
SDL_Window* window = SDL_GL_GetCurrentWindow();
if (window) {
SDL_GetWindowSize(window, &windowWidth, &windowHeight);
touchPosition_ = Vec2(finger->x * windowWidth, finger->y * windowHeight);
} else {
touchPosition_ = Vec2(finger->x * 1280.0f, finger->y * 720.0f);
}
} else {
touching_ = false;
}
} else {
touching_ = false;
}
} else {
touchCount_ = 0;
touching_ = false;
}
#endif
}
// ============================================================================
// 键盘输入
// 键盘输入映射到 SDL GameController 按钮
// ============================================================================
SDL_GameControllerButton Input::mapKeyToButton(int keyCode) const {
@ -204,19 +121,19 @@ SDL_GameControllerButton Input::mapKeyToButton(int keyCode) const {
// 常用键 → 手柄按钮
case Key::Z:
return SDL_CONTROLLER_BUTTON_B;
return SDL_CONTROLLER_BUTTON_B; // 确认
case Key::X:
return SDL_CONTROLLER_BUTTON_A;
return SDL_CONTROLLER_BUTTON_A; // 取消
case Key::C:
return SDL_CONTROLLER_BUTTON_Y;
case Key::V:
return SDL_CONTROLLER_BUTTON_X;
case Key::Space:
return SDL_CONTROLLER_BUTTON_A;
return SDL_CONTROLLER_BUTTON_A; // 空格 = A
case Key::Enter:
return SDL_CONTROLLER_BUTTON_A;
return SDL_CONTROLLER_BUTTON_A; // 回车 = A
case Key::Escape:
return SDL_CONTROLLER_BUTTON_START;
return SDL_CONTROLLER_BUTTON_START; // ESC = Start
// 肩键
case Key::Q:
@ -236,54 +153,28 @@ SDL_GameControllerButton Input::mapKeyToButton(int keyCode) const {
}
bool Input::isKeyDown(int keyCode) const {
#ifdef PLATFORM_SWITCH
// Switch: 映射到手柄按钮
SDL_GameControllerButton button = mapKeyToButton(keyCode);
if (button == SDL_CONTROLLER_BUTTON_INVALID)
return false;
return buttonsDown_[button];
#else
// PC: 直接使用键盘扫描码
SDL_Scancode scancode = SDL_GetScancodeFromKey(keyCode);
if (scancode >= 0 && scancode < MAX_KEYS) {
return keysDown_[scancode];
}
return false;
#endif
}
bool Input::isKeyPressed(int keyCode) const {
#ifdef PLATFORM_SWITCH
SDL_GameControllerButton button = mapKeyToButton(keyCode);
if (button == SDL_CONTROLLER_BUTTON_INVALID)
return false;
return buttonsDown_[button] && !prevButtonsDown_[button];
#else
SDL_Scancode scancode = SDL_GetScancodeFromKey(keyCode);
if (scancode >= 0 && scancode < MAX_KEYS) {
return keysDown_[scancode] && !prevKeysDown_[scancode];
}
return false;
#endif
}
bool Input::isKeyReleased(int keyCode) const {
#ifdef PLATFORM_SWITCH
SDL_GameControllerButton button = mapKeyToButton(keyCode);
if (button == SDL_CONTROLLER_BUTTON_INVALID)
return false;
return !buttonsDown_[button] && prevButtonsDown_[button];
#else
SDL_Scancode scancode = SDL_GetScancodeFromKey(keyCode);
if (scancode >= 0 && scancode < MAX_KEYS) {
return !keysDown_[scancode] && prevKeysDown_[scancode];
}
return false;
#endif
}
// ============================================================================
// 手柄按钮
// 手柄按钮直接访问
// ============================================================================
bool Input::isButtonDown(int button) const {
@ -309,35 +200,21 @@ Vec2 Input::getLeftStick() const { return Vec2(leftStickX_, leftStickY_); }
Vec2 Input::getRightStick() const { return Vec2(rightStickX_, rightStickY_); }
// ============================================================================
// 鼠标输入
// 鼠标输入映射到触摸屏
// ============================================================================
bool Input::isMouseDown(MouseButton button) const {
int index = static_cast<int>(button);
if (index < 0 || index >= 8)
return false;
#ifdef PLATFORM_SWITCH
// Switch: 左键映射到触摸,右键映射到 A 键
if (button == MouseButton::Left) {
return touching_;
}
// A 键映射为右键
if (button == MouseButton::Right) {
return buttonsDown_[SDL_CONTROLLER_BUTTON_A];
}
return false;
#else
// PC: 直接使用鼠标按钮
return mouseButtonsDown_[index];
#endif
}
bool Input::isMousePressed(MouseButton button) const {
int index = static_cast<int>(button);
if (index < 0 || index >= 8)
return false;
#ifdef PLATFORM_SWITCH
if (button == MouseButton::Left) {
return touching_ && !prevTouching_;
}
@ -346,17 +223,9 @@ bool Input::isMousePressed(MouseButton button) const {
!prevButtonsDown_[SDL_CONTROLLER_BUTTON_A];
}
return false;
#else
return mouseButtonsDown_[index] && !prevMouseButtonsDown_[index];
#endif
}
bool Input::isMouseReleased(MouseButton button) const {
int index = static_cast<int>(button);
if (index < 0 || index >= 8)
return false;
#ifdef PLATFORM_SWITCH
if (button == MouseButton::Left) {
return !touching_ && prevTouching_;
}
@ -365,85 +234,37 @@ bool Input::isMouseReleased(MouseButton button) const {
prevButtonsDown_[SDL_CONTROLLER_BUTTON_A];
}
return false;
#else
return !mouseButtonsDown_[index] && prevMouseButtonsDown_[index];
#endif
}
Vec2 Input::getMousePosition() const {
#ifdef PLATFORM_SWITCH
return touchPosition_;
#else
return mousePosition_;
#endif
}
Vec2 Input::getMousePosition() const { return touchPosition_; }
Vec2 Input::getMouseDelta() const {
#ifdef PLATFORM_SWITCH
if (touching_ && prevTouching_) {
return touchPosition_ - prevTouchPosition_;
}
return Vec2::Zero();
#else
return mousePosition_ - prevMousePosition_;
#endif
}
void Input::setMousePosition(const Vec2 &position) {
#ifndef PLATFORM_SWITCH
SDL_WarpMouseInWindow(SDL_GL_GetCurrentWindow(),
static_cast<int>(position.x),
static_cast<int>(position.y));
#else
(void)position;
#endif
void Input::setMousePosition(const Vec2 & /*position*/) {
// 不支持在 Switch 上设置触摸位置
}
void Input::setMouseVisible(bool visible) {
#ifndef PLATFORM_SWITCH
SDL_ShowCursor(visible ? SDL_ENABLE : SDL_DISABLE);
#else
(void)visible;
#endif
void Input::setMouseVisible(bool /*visible*/) {
// Switch 无鼠标光标
}
void Input::setMouseLocked(bool locked) {
#ifndef PLATFORM_SWITCH
SDL_SetRelativeMouseMode(locked ? SDL_TRUE : SDL_FALSE);
#else
(void)locked;
#endif
void Input::setMouseLocked(bool /*locked*/) {
// Switch 无鼠标光标
}
// ============================================================================
// 便捷方法
// ============================================================================
bool Input::isAnyKeyDown() const {
#ifdef PLATFORM_SWITCH
for (int i = 0; i < MAX_BUTTONS; ++i) {
if (buttonsDown_[i])
return true;
}
#else
for (int i = 0; i < MAX_KEYS; ++i) {
if (keysDown_[i])
return true;
}
#endif
return false;
}
bool Input::isAnyMouseDown() const {
#ifdef PLATFORM_SWITCH
return touching_;
#else
for (int i = 0; i < 8; ++i) {
if (mouseButtonsDown_[i])
return true;
}
return false;
#endif
}
bool Input::isAnyMouseDown() const { return touching_; }
} // namespace extra2d

View File

@ -10,15 +10,9 @@
namespace extra2d {
Window::Window()
: sdlWindow_(nullptr), glContext_(nullptr), currentCursor_(nullptr),
width_(1280), height_(720), vsync_(true), shouldClose_(false),
fullscreen_(true), focused_(true), contentScaleX_(1.0f), contentScaleY_(1.0f),
enableDpiScale_(true), userData_(nullptr), eventQueue_(nullptr) {
// 初始化光标数组
for (int i = 0; i < 9; ++i) {
sdlCursors_[i] = nullptr;
}
}
: sdlWindow_(nullptr), glContext_(nullptr), width_(1280), height_(720),
vsync_(true), shouldClose_(false), userData_(nullptr),
eventQueue_(nullptr) {}
Window::~Window() { destroy(); }
@ -28,14 +22,12 @@ bool Window::create(const WindowConfig &config) {
return false;
}
width_ = config.width;
height_ = config.height;
width_ = 1280; // Switch 固定分辨率
height_ = 720;
vsync_ = config.vsync;
fullscreen_ = config.fullscreen;
enableDpiScale_ = config.enableDpiScale;
// 初始化 SDL2 + 创建窗口 + GL 上下文
if (!initSDL(config)) {
if (!initSDL()) {
E2D_LOG_ERROR("Failed to initialize SDL2");
return false;
}
@ -44,18 +36,14 @@ bool Window::create(const WindowConfig &config) {
input_ = makeUnique<Input>();
input_->init();
// 初始化光标
if (config.enableCursors) {
initCursors();
}
E2D_LOG_INFO("Window created: {}x{}", width_, height_);
return true;
}
bool Window::initSDL(const WindowConfig &config) {
bool Window::initSDL() {
// SDL2 全局初始化(视频 + 游戏控制器 + 音频)
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER | SDL_INIT_AUDIO) != 0) {
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER | SDL_INIT_AUDIO) !=
0) {
E2D_LOG_ERROR("SDL_Init failed: {}", SDL_GetError());
return false;
}
@ -73,35 +61,11 @@ bool Window::initSDL(const WindowConfig &config) {
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
// 双缓冲
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
// 创建 SDL2 窗口
Uint32 windowFlags = SDL_WINDOW_OPENGL;
// 根据配置设置窗口模式
if (config.fullscreen) {
// Switch 平台使用 SDL_WINDOW_FULLSCREEN固定分辨率
// PC 平台使用 SDL_WINDOW_FULLSCREEN_DESKTOP桌面全屏
if (config.fullscreenDesktop) {
windowFlags |= SDL_WINDOW_FULLSCREEN_DESKTOP;
} else {
windowFlags |= SDL_WINDOW_FULLSCREEN;
}
} else {
if (config.resizable) {
windowFlags |= SDL_WINDOW_RESIZABLE;
}
// 注意SDL_WINDOWPOS_CENTERED 是位置参数,不是窗口标志
// 窗口居中在 SDL_CreateWindow 的位置参数中处理
}
sdlWindow_ = SDL_CreateWindow(
config.title.c_str(),
config.centerWindow ? SDL_WINDOWPOS_CENTERED : SDL_WINDOWPOS_UNDEFINED,
config.centerWindow ? SDL_WINDOWPOS_CENTERED : SDL_WINDOWPOS_UNDEFINED,
// 创建 SDL2 窗口Switch 全屏)
Uint32 windowFlags = SDL_WINDOW_OPENGL | SDL_WINDOW_FULLSCREEN;
sdlWindow_ =
SDL_CreateWindow("Easy2D", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
width_, height_, windowFlags);
if (!sdlWindow_) {
E2D_LOG_ERROR("SDL_CreateWindow failed: {}", SDL_GetError());
SDL_Quit();
@ -128,8 +92,8 @@ bool Window::initSDL(const WindowConfig &config) {
return false;
}
// 加载 OpenGL ES 函数指针
if (gladLoadGLES2Loader(reinterpret_cast<GLADloadproc>(SDL_GL_GetProcAddress)) == 0) {
if (gladLoadGLES2Loader(reinterpret_cast<GLADloadproc>(SDL_GL_GetProcAddress)) ==
0) {
E2D_LOG_ERROR("gladLoadGLES2Loader failed");
SDL_GL_DeleteContext(glContext_);
glContext_ = nullptr;
@ -142,23 +106,11 @@ bool Window::initSDL(const WindowConfig &config) {
// 设置 VSync
SDL_GL_SetSwapInterval(vsync_ ? 1 : 0);
// 更新 DPI 缩放
if (config.enableDpiScale) {
updateContentScale();
}
E2D_LOG_INFO("SDL2 + GLES 3.2 initialized successfully");
E2D_LOG_INFO("OpenGL Version: {}",
reinterpret_cast<const char *>(glGetString(GL_VERSION)));
E2D_LOG_INFO("OpenGL Renderer: {}",
reinterpret_cast<const char *>(glGetString(GL_RENDERER)));
E2D_LOG_INFO("SDL2 + GLES 3.2 (glad) initialized successfully");
return true;
}
void Window::deinitSDL() {
deinitCursors();
if (glContext_) {
SDL_GL_DeleteContext(glContext_);
glContext_ = nullptr;
@ -195,22 +147,18 @@ void Window::pollEvents() {
case SDL_WINDOWEVENT:
switch (event.window.event) {
case SDL_WINDOWEVENT_RESIZED:
case SDL_WINDOWEVENT_SIZE_CHANGED:
width_ = event.window.data1;
height_ = event.window.data2;
updateContentScale();
if (resizeCallback_) {
resizeCallback_(width_, height_);
}
break;
case SDL_WINDOWEVENT_FOCUS_GAINED:
focused_ = true;
if (focusCallback_) {
focusCallback_(true);
}
break;
case SDL_WINDOWEVENT_FOCUS_LOST:
focused_ = false;
if (focusCallback_) {
focusCallback_(false);
}
@ -236,33 +184,20 @@ bool Window::shouldClose() const { return shouldClose_; }
void Window::setShouldClose(bool close) { shouldClose_ = close; }
void Window::setTitle(const String &title) {
if (sdlWindow_) {
SDL_SetWindowTitle(sdlWindow_, title.c_str());
}
void Window::setTitle(const String & /*title*/) {
// Switch 无窗口标题
}
void Window::setSize(int width, int height) {
if (sdlWindow_) {
SDL_SetWindowSize(sdlWindow_, width, height);
width_ = width;
height_ = height;
}
void Window::setSize(int /*width*/, int /*height*/) {
// Switch 固定 1280x720
}
void Window::setPosition(int x, int y) {
if (sdlWindow_) {
SDL_SetWindowPosition(sdlWindow_, x, y);
}
void Window::setPosition(int /*x*/, int /*y*/) {
// Switch 无窗口位置
}
void Window::setFullscreen(bool fullscreen) {
if (sdlWindow_) {
// 默认使用桌面全屏模式PC 平台)
Uint32 flags = fullscreen ? SDL_WINDOW_FULLSCREEN_DESKTOP : 0;
SDL_SetWindowFullscreen(sdlWindow_, flags);
fullscreen_ = fullscreen;
}
void Window::setFullscreen(bool /*fullscreen*/) {
// Switch 始终全屏
}
void Window::setVSync(bool enabled) {
@ -270,101 +205,16 @@ void Window::setVSync(bool enabled) {
SDL_GL_SetSwapInterval(enabled ? 1 : 0);
}
void Window::setResizable(bool resizable) {
if (sdlWindow_) {
SDL_SetWindowResizable(sdlWindow_, resizable ? SDL_TRUE : SDL_FALSE);
}
void Window::setResizable(bool /*resizable*/) {
// Switch 不支持
}
Vec2 Window::getPosition() const {
if (sdlWindow_) {
int x, y;
SDL_GetWindowPosition(sdlWindow_, &x, &y);
return Vec2(static_cast<float>(x), static_cast<float>(y));
}
return Vec2::Zero();
}
float Window::getContentScaleX() const {
return enableDpiScale_ ? contentScaleX_ : 1.0f;
}
float Window::getContentScaleY() const {
return enableDpiScale_ ? contentScaleY_ : 1.0f;
}
Vec2 Window::getContentScale() const {
return Vec2(getContentScaleX(), getContentScaleY());
}
bool Window::isMinimized() const {
if (sdlWindow_) {
Uint32 flags = SDL_GetWindowFlags(sdlWindow_);
return (flags & SDL_WINDOW_MINIMIZED) != 0;
}
return false;
}
bool Window::isMaximized() const {
if (sdlWindow_) {
Uint32 flags = SDL_GetWindowFlags(sdlWindow_);
return (flags & SDL_WINDOW_MAXIMIZED) != 0;
}
return true;
}
void Window::initCursors() {
sdlCursors_[0] = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_ARROW);
sdlCursors_[1] = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_IBEAM);
sdlCursors_[2] = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_CROSSHAIR);
sdlCursors_[3] = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_HAND);
sdlCursors_[4] = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_SIZEWE);
sdlCursors_[5] = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_SIZENS);
sdlCursors_[6] = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_SIZEALL);
sdlCursors_[7] = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_SIZENWSE);
sdlCursors_[8] = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_SIZENESW);
}
void Window::deinitCursors() {
for (int i = 0; i < 9; ++i) {
if (sdlCursors_[i]) {
SDL_FreeCursor(sdlCursors_[i]);
sdlCursors_[i] = nullptr;
}
}
currentCursor_ = nullptr;
}
void Window::setCursor(CursorShape shape) {
int index = static_cast<int>(shape);
if (index >= 0 && index < 9 && sdlCursors_[index]) {
SDL_SetCursor(sdlCursors_[index]);
currentCursor_ = sdlCursors_[index];
}
void Window::setCursor(CursorShape /*shape*/) {
// Switch 无鼠标光标
}
void Window::resetCursor() {
SDL_SetCursor(SDL_GetDefaultCursor());
currentCursor_ = nullptr;
}
void Window::setMouseVisible(bool visible) {
SDL_ShowCursor(visible ? SDL_ENABLE : SDL_DISABLE);
}
void Window::updateContentScale() {
if (sdlWindow_) {
// 使用 DPI 计算内容缩放比例
int displayIndex = SDL_GetWindowDisplayIndex(sdlWindow_);
if (displayIndex >= 0) {
float ddpi, hdpi, vdpi;
if (SDL_GetDisplayDPI(displayIndex, &ddpi, &hdpi, &vdpi) == 0) {
// 假设标准 DPI 为 96
contentScaleX_ = hdpi / 96.0f;
contentScaleY_ = vdpi / 96.0f;
}
}
}
// Switch 无鼠标光标
}
} // namespace extra2d

View File

@ -7,13 +7,52 @@
#include <extra2d/utils/logger.h>
#include <sys/stat.h>
// Windows 平台需要包含的头文件
#ifdef _WIN32
#include <windows.h>
#endif
namespace extra2d {
ResourceManager::ResourceManager() {
#ifdef __SWITCH__
addSearchPath("romfs:/");
addSearchPath("sdmc:/");
#endif
}
ResourceManager::~ResourceManager() = default;
ResourceManager &ResourceManager::getInstance() {
static ResourceManager instance;
return instance;
}
// ============================================================================
// 搜索路径管理
// ============================================================================
void ResourceManager::addSearchPath(const std::string &path) {
std::lock_guard<std::mutex> lock(textureMutex_);
// 避免重复添加
auto it = std::find(searchPaths_.begin(), searchPaths_.end(), path);
if (it == searchPaths_.end()) {
searchPaths_.push_back(path);
E2D_LOG_DEBUG("ResourceManager: added search path: {}", path);
}
}
void ResourceManager::removeSearchPath(const std::string &path) {
std::lock_guard<std::mutex> lock(textureMutex_);
auto it = std::find(searchPaths_.begin(), searchPaths_.end(), path);
if (it != searchPaths_.end()) {
searchPaths_.erase(it);
E2D_LOG_DEBUG("ResourceManager: removed search path: {}", path);
}
}
void ResourceManager::clearSearchPaths() {
std::lock_guard<std::mutex> lock(textureMutex_);
searchPaths_.clear();
E2D_LOG_DEBUG("ResourceManager: cleared all search paths");
}
// 辅助函数:检查文件是否存在
static bool fileExists(const std::string &path) {
struct stat st;
@ -25,68 +64,51 @@ static bool isRomfsPath(const std::string &path) {
return path.find("romfs:/") == 0 || path.find("romfs:\\") == 0;
}
// 辅助函数获取可执行文件所在目录Windows 平台)
#ifdef _WIN32
static std::string getExecutableDirectory() {
char buffer[MAX_PATH];
DWORD len = GetModuleFileNameA(NULL, buffer, MAX_PATH);
if (len > 0 && len < MAX_PATH) {
std::string exePath(buffer, len);
size_t lastSlash = exePath.find_last_of("\\/");
if (lastSlash != std::string::npos) {
return exePath.substr(0, lastSlash);
// 辅助函数:拼接路径
static std::string joinPath(const std::string &dir,
const std::string &filename) {
if (dir.empty())
return filename;
char lastChar = dir.back();
if (lastChar == '/' || lastChar == '\\') {
return dir + filename;
}
return dir + "/" + filename;
}
std::string
ResourceManager::findResourcePath(const std::string &filename) const {
// 首先检查是否是 romfs 路径Switch 平台)
if (isRomfsPath(filename)) {
if (fileExists(filename)) {
return filename;
}
return "";
}
#endif
// 解析资源路径(优先尝试 romfs:/ 前缀,然后 sdmc:/,最后尝试相对于可执行文件的路径)
static std::string resolveResourcePath(const std::string &filepath) {
// 如果已经是 romfs 或 sdmc 路径,直接返回
if (isRomfsPath(filepath) || filepath.find("sdmc:/") == 0) {
return filepath;
// 首先检查是否是绝对路径或相对当前目录存在
if (fileExists(filename)) {
return filename;
}
// 优先尝试 romfs:/ 前缀的路径Switch 平台)
std::string romfsPath = "romfs:/" + filepath;
// 在搜索路径中查找
std::lock_guard<std::mutex> lock(textureMutex_);
for (const auto &path : searchPaths_) {
std::string fullPath = joinPath(path, filename);
if (fileExists(fullPath)) {
return fullPath;
}
}
// 最后尝试在 romfs 中查找(自动添加 romfs:/ 前缀)
std::string romfsPath = "romfs:/" + filename;
if (fileExists(romfsPath)) {
return romfsPath;
}
// 尝试 sdmc:/ 前缀的路径Switch SD卡
std::string sdmcPath = "sdmc:/" + filepath;
if (fileExists(sdmcPath)) {
return sdmcPath;
}
// 如果都不存在,尝试原路径
if (fileExists(filepath)) {
return filepath;
}
// Windows 平台:尝试相对于可执行文件的路径
#ifdef _WIN32
std::string exeDir = getExecutableDirectory();
if (!exeDir.empty()) {
std::string exeRelativePath = exeDir + "/" + filepath;
if (fileExists(exeRelativePath)) {
return exeRelativePath;
}
}
#endif
return "";
}
ResourceManager::ResourceManager() = default;
ResourceManager::~ResourceManager() = default;
ResourceManager &ResourceManager::getInstance() {
static ResourceManager instance;
return instance;
}
// ============================================================================
// 纹理资源
// ============================================================================
@ -105,14 +127,14 @@ Ptr<Texture> ResourceManager::loadTexture(const std::string &filepath) {
textureCache_.erase(it);
}
// 解析资源路径(优先尝试 romfs:/ 前缀)
std::string fullPath = resolveResourcePath(filepath);
// 查找完整路径
std::string fullPath = findResourcePath(filepath);
if (fullPath.empty()) {
E2D_LOG_ERROR("ResourceManager: texture file not found: {}", filepath);
return nullptr;
}
// 创建新纹理
// 创建新纹理(根据扩展名自动选择加载路径)
try {
auto texture = makePtr<GLTexture>(fullPath);
if (!texture->isValid()) {
@ -239,8 +261,8 @@ Ptr<FontAtlas> ResourceManager::loadFont(const std::string &filepath,
fontCache_.erase(it);
}
// 解析资源路径(优先尝试 romfs:/ 前缀)
std::string fullPath = resolveResourcePath(filepath);
// 查找完整路径
std::string fullPath = findResourcePath(filepath);
if (fullPath.empty()) {
E2D_LOG_ERROR("ResourceManager: font file not found: {}", filepath);
return nullptr;
@ -291,6 +313,92 @@ void ResourceManager::unloadFont(const std::string &key) {
E2D_LOG_DEBUG("ResourceManager: unloaded font: {}", key);
}
// ============================================================================
// 多字体后备加载
// ============================================================================
Ptr<FontAtlas> ResourceManager::loadFontWithFallbacks(
const std::vector<std::string> &fontPaths, int fontSize, bool useSDF) {
// 尝试加载每一个候选字体
for (const auto &fontPath : fontPaths) {
auto font = loadFont(fontPath, fontSize, useSDF);
if (font) {
E2D_LOG_INFO("ResourceManager: successfully loaded font from fallback list: {}",
fontPath);
return font;
}
}
E2D_LOG_ERROR("ResourceManager: failed to load any font from fallback list ({} candidates)",
fontPaths.size());
return nullptr;
}
Ptr<FontAtlas> ResourceManager::loadFontWithDefaultFallback(
const std::string &filepath, int fontSize, bool useSDF) {
// 首先尝试加载用户指定的字体
auto font = loadFont(filepath, fontSize, useSDF);
if (font) {
return font;
}
E2D_LOG_WARN("ResourceManager: failed to load font '{}', trying system fallbacks...",
filepath);
// 定义系统默认字体候选列表
std::vector<std::string> fallbackFonts;
#ifdef __SWITCH__
// Switch 平台默认字体路径
fallbackFonts = {
"romfs:/assets/font.ttf", // 应用自带字体
"romfs:/assets/default.ttf", // 默认字体备选
"romfs:/font.ttf", // 根目录字体
"sdmc:/switch/fonts/default.ttf", // SD卡字体目录
"sdmc:/switch/fonts/font.ttf",
};
#else
// PC 平台系统字体路径Windows/Linux/macOS
#ifdef _WIN32
fallbackFonts = {
"C:/Windows/Fonts/arial.ttf",
"C:/Windows/Fonts/segoeui.ttf",
"C:/Windows/Fonts/calibri.ttf",
"C:/Windows/Fonts/tahoma.ttf",
"C:/Windows/Fonts/msyh.ttc", // 微软雅黑
};
#elif __APPLE__
fallbackFonts = {
"/System/Library/Fonts/Helvetica.ttc",
"/System/Library/Fonts/SFNSDisplay.ttf",
"/Library/Fonts/Arial.ttf",
};
#else
// Linux
fallbackFonts = {
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
"/usr/share/fonts/truetype/freefont/FreeSans.ttf",
"/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf",
"/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf",
};
#endif
#endif
// 尝试加载后备字体
for (const auto &fallbackPath : fallbackFonts) {
font = loadFont(fallbackPath, fontSize, useSDF);
if (font) {
E2D_LOG_INFO("ResourceManager: loaded fallback font: {}", fallbackPath);
return font;
}
}
E2D_LOG_ERROR("ResourceManager: all font fallbacks exhausted, no font available");
return nullptr;
}
// ============================================================================
// 音效资源
// ============================================================================
@ -314,8 +422,8 @@ Ptr<Sound> ResourceManager::loadSound(const std::string &name,
soundCache_.erase(it);
}
// 解析资源路径(优先尝试 romfs:/ 前缀)
std::string fullPath = resolveResourcePath(filepath);
// 查找完整路径
std::string fullPath = findResourcePath(filepath);
if (fullPath.empty()) {
E2D_LOG_ERROR("ResourceManager: sound file not found: {}", filepath);
return nullptr;

View File

@ -25,14 +25,6 @@ void Node::addChild(Ptr<Node> child) {
children_.push_back(child);
childrenOrderDirty_ = true;
// 更新索引
if (!child->getName().empty()) {
nameIndex_[child->getName()] = child;
}
if (child->getTag() != -1) {
tagIndex_[child->getTag()] = child;
}
if (running_) {
child->onEnter();
if (scene_) {
@ -41,63 +33,16 @@ void Node::addChild(Ptr<Node> child) {
}
}
void Node::addChildren(std::vector<Ptr<Node>> &&children) {
// 预留空间,避免多次扩容
size_t newSize = children_.size() + children.size();
if (newSize > children_.capacity()) {
children_.reserve(newSize);
}
for (auto &child : children) {
if (!child || child.get() == this) {
continue;
}
child->removeFromParent();
child->parent_ = weak_from_this();
children_.push_back(child);
// 更新索引
if (!child->getName().empty()) {
nameIndex_[child->getName()] = child;
}
if (child->getTag() != -1) {
tagIndex_[child->getTag()] = child;
}
if (running_) {
child->onEnter();
if (scene_) {
child->onAttachToScene(scene_);
}
}
}
if (!children.empty()) {
childrenOrderDirty_ = true;
}
}
void Node::removeChild(Ptr<Node> child) {
if (!child)
return;
auto it = std::find(children_.begin(), children_.end(), child);
if (it != children_.end()) {
// 始终从空间索引中移除(无论 running_ 状态)
// 这确保节点被正确清理
(*it)->onDetachFromScene();
if (running_) {
(*it)->onDetachFromScene();
(*it)->onExit();
}
// 从索引中移除
if (!(*it)->getName().empty()) {
nameIndex_.erase((*it)->getName());
}
if ((*it)->getTag() != -1) {
tagIndex_.erase((*it)->getTag());
}
(*it)->parent_.reset();
children_.erase(it);
}
@ -135,24 +80,22 @@ void Node::removeAllChildren() {
child->parent_.reset();
}
children_.clear();
nameIndex_.clear();
tagIndex_.clear();
}
Ptr<Node> Node::getChildByName(const std::string &name) const {
// 使用哈希索引O(1) 查找
auto it = nameIndex_.find(name);
if (it != nameIndex_.end()) {
return it->second.lock();
for (const auto &child : children_) {
if (child->getName() == name) {
return child;
}
}
return nullptr;
}
Ptr<Node> Node::getChildByTag(int tag) const {
// 使用哈希索引O(1) 查找
auto it = tagIndex_.find(tag);
if (it != tagIndex_.end()) {
return it->second.lock();
for (const auto &child : children_) {
if (child->getTag() == tag) {
return child;
}
}
return nullptr;
}
@ -254,26 +197,12 @@ glm::mat4 Node::getLocalTransform() const {
glm::mat4 Node::getWorldTransform() const {
if (worldTransformDirty_) {
// 迭代计算世界变换,避免深层级时的栈溢出
// 收集父节点链
std::vector<const Node *> nodeChain;
const Node *current = this;
while (current) {
nodeChain.push_back(current);
auto p = current->parent_.lock();
current = p.get();
// 限制最大深度,防止异常循环
if (nodeChain.size() > 1000) {
break;
}
}
worldTransform_ = getLocalTransform();
// 从根节点开始计算
glm::mat4 transform = glm::mat4(1.0f);
for (auto it = nodeChain.rbegin(); it != nodeChain.rend(); ++it) {
transform = transform * (*it)->getLocalTransform();
auto p = parent_.lock();
if (p) {
worldTransform_ = p->getWorldTransform() * worldTransform_;
}
worldTransform_ = transform;
worldTransformDirty_ = false;
}
return worldTransform_;
@ -384,71 +313,33 @@ void Node::updateSpatialIndex() {
}
void Node::runAction(Ptr<Action> action) {
if (!action) {
return;
}
if (action) {
action->start(this);
int tag = action->getTag();
if (tag != -1) {
// 有 tag 的 Action 存入哈希表O(1) 查找
// 如果已存在相同 tag 的 Action先停止它
auto it = actionByTag_.find(tag);
if (it != actionByTag_.end()) {
// 从 vector 中移除旧的 Action
auto oldAction = it->second;
auto vecIt = std::find(actions_.begin(), actions_.end(), oldAction);
if (vecIt != actions_.end()) {
actions_.erase(vecIt);
}
}
actionByTag_[tag] = action;
}
actions_.push_back(action);
}
void Node::stopAllActions() {
actions_.clear();
actionByTag_.clear();
}
void Node::stopAllActions() { actions_.clear(); }
void Node::stopAction(Ptr<Action> action) {
if (!action) {
return;
}
// 从 vector 中移除
auto it = std::find(actions_.begin(), actions_.end(), action);
if (it != actions_.end()) {
// 如果有 tag从哈希表中也移除
int tag = action->getTag();
if (tag != -1) {
actionByTag_.erase(tag);
}
actions_.erase(it);
}
}
void Node::stopActionByTag(int tag) {
auto it = actionByTag_.find(tag);
if (it != actionByTag_.end()) {
auto action = it->second;
// 从 vector 中移除
auto vecIt = std::find(actions_.begin(), actions_.end(), action);
if (vecIt != actions_.end()) {
actions_.erase(vecIt);
}
actionByTag_.erase(it);
}
auto it = std::remove_if(
actions_.begin(), actions_.end(),
[tag](const Ptr<Action> &action) { return action->getTag() == tag; });
actions_.erase(it, actions_.end());
}
Ptr<Action> Node::getActionByTag(int tag) const {
// O(1) 哈希查找
auto it = actionByTag_.find(tag);
if (it != actionByTag_.end()) {
return it->second;
for (const auto &action : actions_) {
if (action->getTag() == tag) {
return action;
}
}
return nullptr;
}
@ -498,20 +389,13 @@ void Node::sortChildren() {
void Node::collectRenderCommands(std::vector<RenderCommand> &commands,
int parentZOrder) {
// 暂时最小化实现以测试
if (!visible_)
return;
// 计算累积 Z 序
// 不排序,不递归,只生成当前节点的命令
int accumulatedZOrder = parentZOrder + zOrder_;
// 生成当前节点的渲染命令
generateRenderCommand(commands, accumulatedZOrder);
// 递归收集子节点的渲染命令
// 注意:这里假设子节点已经按 Z 序排序
for (auto &child : children_) {
child->collectRenderCommands(commands, accumulatedZOrder);
}
}
} // namespace extra2d

View File

@ -86,19 +86,11 @@ void SpatialHash::insert(Node *node, const Rect &bounds) {
if (!node)
return;
// 检查节点是否已存在,如果存在则先移除
auto it = objectBounds_.find(node);
if (it != objectBounds_.end()) {
removeFromCells(node, it->second);
it->second = bounds;
} else {
insertIntoCells(node, bounds);
objectBounds_[node] = bounds;
objectCount_++;
}
insertIntoCells(node, bounds);
}
void SpatialHash::remove(Node *node) {
if (!node)
return;
@ -108,12 +100,6 @@ void SpatialHash::remove(Node *node) {
removeFromCells(node, it->second);
objectBounds_.erase(it);
objectCount_--;
} else {
// 节点不在 objectBounds_ 中,但可能还在 grid_ 的某些单元格中
// 需要遍历所有单元格来移除
for (auto &[cellKey, cell] : grid_) {
cell.remove(node);
}
}
}

View File

@ -1,13 +1,6 @@
#include <extra2d/utils/data.h>
#include <extra2d/utils/logger.h>
#include <simpleini/SimpleIni.h>
// Switch 平台特定头文件
#ifdef __SWITCH__
#include <switch.h>
#include <switch/services/fs.h>
#endif
namespace extra2d {
class DataStore::Impl {
@ -17,202 +10,24 @@ public:
DataStore::DataStore() : impl_(makeUnique<Impl>()) {}
DataStore::~DataStore() {
// 如果在事务中,尝试提交
if (inTransaction_ && dirty_) {
commit();
}
// 如果存档已挂载,卸载
if (saveDataMounted_) {
unmountSaveData(mountName_);
}
}
// ============================================================================
// 文件操作
// ============================================================================
DataStore::~DataStore() = default;
bool DataStore::load(const std::string &filename) {
filename_ = filename;
SI_Error rc = impl_->ini.LoadFile(filename.c_str());
dirty_ = false;
return rc >= 0;
}
bool DataStore::save(const std::string &filename) {
// 如果在事务中,只标记为脏,不实际写入
if (inTransaction_) {
dirty_ = true;
return true;
}
const std::string &targetFile = filename.empty() ? filename_ : filename;
if (targetFile.empty()) {
E2D_LOG_ERROR("DataStore::save: 没有指定文件名");
return false;
}
return internalSave(targetFile);
SI_Error rc = impl_->ini.SaveFile(targetFile.c_str());
return rc >= 0;
}
bool DataStore::internalSave(const std::string &filename) {
SI_Error rc = impl_->ini.SaveFile(filename.c_str());
if (rc < 0) {
E2D_LOG_ERROR("DataStore::save: 保存文件失败: {}", filename);
return false;
}
dirty_ = false;
return true;
}
// ============================================================================
// Switch 存档系统支持
// ============================================================================
#ifdef __SWITCH__
bool DataStore::mountSaveData(SaveDataType type, const UserId &userId,
const std::string &mountName) {
// 如果已经挂载,先卸载
if (saveDataMounted_) {
unmountSaveData(mountName_);
}
Result rc = 0;
AccountUid uid = {userId.uid[0], userId.uid[1]};
// 如果没有提供用户ID尝试获取当前用户
if (type == SaveDataType::Account && !userId.isValid()) {
UserId currentUid = getCurrentUserId();
uid.uid[0] = currentUid.uid[0];
uid.uid[1] = currentUid.uid[1];
if (uid.uid[0] == 0 && uid.uid[1] == 0) {
E2D_LOG_ERROR("DataStore::mountSaveData: 无法获取当前用户ID");
return false;
}
}
// 使用 fsdevMountSaveData 挂载
// 注意这里使用当前应用程序ID (0 表示当前应用)
u64 applicationId = 0;
rc = fsdevMountSaveData(mountName.c_str(), applicationId, uid);
if (R_FAILED(rc)) {
E2D_LOG_ERROR("DataStore::mountSaveData: 挂载失败: 0x{:X}", rc);
return false;
}
mountName_ = mountName;
saveDataMounted_ = true;
defaultUserId_ = UserId{uid.uid[0], uid.uid[1]};
E2D_LOG_INFO("DataStore::mountSaveData: 成功挂载存档: {}", mountName);
return true;
}
void DataStore::unmountSaveData(const std::string &mountName) {
if (!saveDataMounted_) {
return;
}
// 先提交更改
if (dirty_) {
commitSaveData(mountName_);
}
fsdevUnmountDevice(mountName.c_str());
saveDataMounted_ = false;
mountName_.clear();
E2D_LOG_INFO("DataStore::unmountSaveData: 已卸载存档");
}
bool DataStore::commitSaveData(const std::string &mountName) {
if (!saveDataMounted_) {
E2D_LOG_WARN("DataStore::commitSaveData: 存档未挂载");
return false;
}
Result rc = fsdevCommitDevice(mountName.c_str());
if (R_FAILED(rc)) {
E2D_LOG_ERROR("DataStore::commitSaveData: 提交失败: 0x{:X}", rc);
return false;
}
E2D_LOG_DEBUG("DataStore::commitSaveData: 提交成功");
return true;
}
std::string DataStore::getSaveDataPath(const std::string &path) const {
if (!saveDataMounted_) {
return path;
}
return mountName_ + ":/" + path;
}
UserId DataStore::getCurrentUserId() {
UserId result;
Result rc = accountInitialize(AccountServiceType_Application);
if (R_FAILED(rc)) {
E2D_LOG_ERROR("DataStore::getCurrentUserId: accountInitialize 失败: 0x{:X}",
rc);
return result;
}
AccountUid uid;
rc = accountGetPreselectedUser(&uid);
accountExit();
if (R_SUCCEEDED(rc)) {
result.uid[0] = uid.uid[0];
result.uid[1] = uid.uid[1];
E2D_LOG_DEBUG("DataStore::getCurrentUserId: 获取成功: 0x{:X}{:X}",
result.uid[1], result.uid[0]);
} else {
E2D_LOG_ERROR("DataStore::getCurrentUserId: 获取失败: 0x{:X}", rc);
}
return result;
}
#else
// 非 Switch 平台的存根实现
bool DataStore::mountSaveData(SaveDataType type, const UserId &userId,
const std::string &mountName) {
(void)type;
(void)userId;
(void)mountName;
E2D_LOG_WARN("DataStore::mountSaveData: 非 Switch 平台,存档功能不可用");
return false;
}
void DataStore::unmountSaveData(const std::string &mountName) {
(void)mountName;
saveDataMounted_ = false;
}
bool DataStore::commitSaveData(const std::string &mountName) {
(void)mountName;
return true;
}
std::string DataStore::getSaveDataPath(const std::string &path) const {
return path;
}
UserId DataStore::getCurrentUserId() {
return UserId();
}
#endif
// ============================================================================
// 数据读写
// ============================================================================
std::string DataStore::getString(const std::string &section,
const std::string &key,
const std::string &defaultValue) {
@ -232,11 +47,7 @@ float DataStore::getFloat(const std::string &section, const std::string &key,
const char *value =
impl_->ini.GetValue(section.c_str(), key.c_str(), nullptr);
if (value) {
try {
return std::stof(value);
} catch (...) {
return defaultValue;
}
}
return defaultValue;
}
@ -249,61 +60,30 @@ bool DataStore::getBool(const std::string &section, const std::string &key,
void DataStore::setString(const std::string &section, const std::string &key,
const std::string &value) {
impl_->ini.SetValue(section.c_str(), key.c_str(), value.c_str());
dirty_ = true;
// 不在事务中时自动保存
if (!inTransaction_ && !filename_.empty()) {
save("");
}
}
void DataStore::setInt(const std::string &section, const std::string &key,
int value) {
impl_->ini.SetLongValue(section.c_str(), key.c_str(), value);
dirty_ = true;
if (!inTransaction_ && !filename_.empty()) {
save("");
}
}
void DataStore::setFloat(const std::string &section, const std::string &key,
float value) {
impl_->ini.SetValue(section.c_str(), key.c_str(),
std::to_string(value).c_str());
dirty_ = true;
if (!inTransaction_ && !filename_.empty()) {
save("");
}
}
void DataStore::setBool(const std::string &section, const std::string &key,
bool value) {
impl_->ini.SetBoolValue(section.c_str(), key.c_str(), value);
dirty_ = true;
if (!inTransaction_ && !filename_.empty()) {
save("");
}
}
void DataStore::removeKey(const std::string &section, const std::string &key) {
impl_->ini.Delete(section.c_str(), key.c_str());
dirty_ = true;
if (!inTransaction_ && !filename_.empty()) {
save("");
}
}
void DataStore::removeSection(const std::string &section) {
impl_->ini.Delete(section.c_str(), nullptr);
dirty_ = true;
if (!inTransaction_ && !filename_.empty()) {
save("");
}
}
bool DataStore::hasKey(const std::string &section, const std::string &key) {
@ -314,131 +94,6 @@ bool DataStore::hasSection(const std::string &section) {
return impl_->ini.GetSection(section.c_str()) != nullptr;
}
void DataStore::clear() {
impl_->ini.Reset();
dirty_ = true;
if (!inTransaction_ && !filename_.empty()) {
save("");
}
}
// ============================================================================
// 事务支持
// ============================================================================
void DataStore::beginTransaction() {
if (inTransaction_) {
E2D_LOG_WARN("DataStore::beginTransaction: 已经处于事务中");
return;
}
inTransaction_ = true;
dirty_ = false;
E2D_LOG_DEBUG("DataStore::beginTransaction: 事务开始");
}
bool DataStore::commit() {
if (!inTransaction_) {
E2D_LOG_WARN("DataStore::commit: 不在事务中");
return false;
}
// 如果有文件名,写入文件
bool result = true;
if (!filename_.empty() && dirty_) {
result = internalSave(filename_);
// 如果挂载了存档,提交更改
if (result && saveDataMounted_) {
result = commitSaveData(mountName_);
}
}
inTransaction_ = false;
E2D_LOG_DEBUG("DataStore::commit: 事务提交 {}", result ? "成功" : "失败");
return result;
}
void DataStore::rollback() {
if (!inTransaction_) {
E2D_LOG_WARN("DataStore::rollback: 不在事务中");
return;
}
// 重新加载文件来恢复数据
if (!filename_.empty()) {
impl_->ini.Reset();
SI_Error rc = impl_->ini.LoadFile(filename_.c_str());
if (rc < 0) {
E2D_LOG_ERROR("DataStore::rollback: 重新加载文件失败: {}", filename_);
}
} else {
// 如果没有文件名,清空数据
impl_->ini.Reset();
}
inTransaction_ = false;
dirty_ = false;
E2D_LOG_DEBUG("DataStore::rollback: 事务已回滚");
}
// ============================================================================
// 工具方法
// ============================================================================
std::vector<std::string> DataStore::getAllSections() const {
std::vector<std::string> sections;
CSimpleIniA::TNamesDepend sectionList;
impl_->ini.GetAllSections(sectionList);
for (const auto &section : sectionList) {
sections.emplace_back(section.pItem);
}
return sections;
}
std::vector<std::string> DataStore::getAllKeys(const std::string &section) const {
std::vector<std::string> keys;
CSimpleIniA::TNamesDepend keyList;
impl_->ini.GetAllKeys(section.c_str(), keyList);
for (const auto &key : keyList) {
keys.emplace_back(key.pItem);
}
return keys;
}
bool DataStore::loadFromSave(const std::string &path) {
if (!saveDataMounted_) {
E2D_LOG_ERROR("DataStore::loadFromSave: 存档未挂载");
return false;
}
std::string fullPath = getSaveDataPath(path);
return load(fullPath);
}
bool DataStore::saveToSave(const std::string &path) {
if (!saveDataMounted_) {
E2D_LOG_ERROR("DataStore::saveToSave: 存档未挂载");
return false;
}
std::string fullPath = getSaveDataPath(path);
bool result = save(fullPath);
// 自动提交
if (result) {
result = commitSaveData(mountName_);
}
return result;
}
void DataStore::clear() { impl_->ini.Reset(); }
} // namespace extra2d

112
README.md
View File

@ -111,10 +111,10 @@ mindmap
| 组件 | 要求 |
|:----:|:-----|
| 开发环境 | devkitPro + devkitA64 (Switch) / MinGW-w64 (Windows) |
| 开发环境 | devkitPro + devkitA64 |
| C++ 标准 | C++17 |
| 构建工具 | xmake |
| 目标平台 | Nintendo Switch / Windows (MinGW) |
| 目标平台 | Nintendo Switch |
### 安装 devkitPro
@ -129,8 +129,6 @@ pacman -S switch-dev switch-portlibs
### 构建项目
#### Switch 平台
```bash
# 克隆仓库
git clone https://github.com/ChestnutYueyue/extra2d.git
@ -146,26 +144,6 @@ xmake
xmake -g examples
```
#### Windows (MinGW) 平台
```bash
# 克隆仓库
git clone https://github.com/ChestnutYueyue/extra2d.git
cd extra2d
# 配置 MinGW 平台构建
xmake f -p mingw --mode=release
# 安装依赖
xmake require -y
# 构建引擎和示例
xmake
# 运行示例
xmake run hello_world
```
### 生成 NSP 可运行文件
```bash
@ -248,57 +226,31 @@ int main()
Extra2D/
├── 📁 Extra2D/ # 引擎核心代码
│ ├── 📁 include/ # 头文件
│ │ ├── 📁 extra2d/ # 引擎头文件
│ │ │ ├── extra2d.h # 主头文件
│ │ │ ├── app/ # 应用管理
│ │ │ ├── action/ # 动作系统
│ │ │ ├── animation/ # 动画系统
│ │ │ ├── audio/ # 音频系统
│ │ │ ├── core/ # 核心类型
│ │ │ ├── effects/ # 特效系统
│ │ │ ├── event/ # 事件系统
│ │ │ ├── graphics/ # 图形渲染
│ │ │ ├── platform/ # 平台抽象
│ │ │ ├── resource/ # 资源管理
│ │ │ ├── scene/ # 场景系统
│ │ │ ├── script/ # 脚本系统
│ │ │ ├── spatial/ # 空间索引
│ │ │ ├── ui/ # UI 系统
│ │ │ └── utils/ # 工具库
│ │ ├── 📁 glad/ # OpenGL Loader
│ │ ├── 📁 json/ # JSON 库
│ │ ├── 📁 simpleini/ # INI 配置文件库
│ │ └── 📁 stb/ # STB 图像库
│ └── 📁 src/ # 源文件
│ ├── action/ # 动作系统实现
│ ├── animation/ # 动画系统实现
│ ├── app/ # 应用管理实现
│ ├── audio/ # 音频系统实现
│ ├── core/ # 核心类型实现
│ ├── effects/ # 特效系统实现
│ ├── event/ # 事件系统实现
│ ├── glad/ # GLAD 实现
│ ├── graphics/ # 图形渲染实现
│ ├── platform/ # 平台抽象实现
│ ├── resource/ # 资源管理实现
│ ├── scene/ # 场景系统实现
│ ├── script/ # 脚本系统实现
│ ├── spatial/ # 空间索引实现
│ ├── ui/ # UI 系统实现
│ └── utils/ # 工具库实现
├── 📁 docs/ # 文档
│ ├── 📁 API_Tutorial/ # API 教程
│ └── Extra2D 构建系统文档.md # 构建系统文档
├── 📁 examples/ # 示例程序
│ ├── hello_world/ # Hello World 示例
│ ├── collision_demo/ # 碰撞检测示例
│ │ └── 📁 extra2d/ # 引擎头文件
│ │ ├── extra2d.h # 主头文件
│ │ ├── app/ # 应用管理
│ │ ├── action/ # 动作系统
│ │ ├── animation/ # 动画系统
│ │ ├── audio/ # 音频系统
│ │ ├── core/ # 核心类型
│ │ ├── effects/ # 特效系统
│ │ ├── event/ # 事件系统
│ │ ├── graphics/ # 图形渲染
│ │ ├── platform/ # 平台抽象
│ │ ├── resource/ # 资源管理
│ │ ├── scene/ # 场景系统
│ │ ├── script/ # 脚本系统
│ │ ├── spatial/ # 空间索引
│ │ ├── ui/ # UI 系统
│ │ └── utils/ # 工具库
│ ├── 📁 src/ # 源文件
│ └── 📁 examples/ # 示例程序
│ ├── push_box/ # 推箱子游戏
│ └── spatial_index_demo/ # 空间索引示例
├── 📁 logo/ # Logo 资源
│ └── switch_simple_test/ # 简单测试
├── 📁 squirrel/ # Squirrel 脚本引擎
├── <EFBFBD> xmake/ # Xmake 构建配置
│ └── toolchains/ # 工具链定义
├── 📄 xmake.lua # 主构建配置
├── 📁 logo/ # Logo 资源
├── 📄 xmake.lua # xmake 构建配置
├── 📄 SWITCH_BUILD_GUIDE.md # Switch 构建详细指南
├── 📄 LICENSE # MIT 许可证
└── 📄 README.md # 本文件
```
@ -428,18 +380,8 @@ sound->setVolume(0.8f);
## 📖 相关文档
- [📚 API 教程](./docs/API_Tutorial/01_Quick_Start.md) - 完整的 API 使用教程
- [01. 快速开始](./docs/API_Tutorial/01_Quick_Start.md)
- [02. 场景系统](./docs/API_Tutorial/02_Scene_System.md)
- [03. 节点系统](./docs/API_Tutorial/03_Node_System.md)
- [04. 资源管理](./docs/API_Tutorial/04_Resource_Management.md)
- [05. 输入处理](./docs/API_Tutorial/05_Input_Handling.md)
- [06. 碰撞检测](./docs/API_Tutorial/06_Collision_Detection.md)
- [07. UI 系统](./docs/API_Tutorial/07_UI_System.md)
- [08. 音频系统](./docs/API_Tutorial/08_Audio_System.md)
- [🔧 构建系统文档](./docs/Extra2D%20构建系统文档.md) - 详细的构建系统说明
- [🎮 Switch 构建指南](./SWITCH_BUILD_GUIDE.md) - Switch 平台构建教程
- [📝 迁移完成记录](./SWITCH_MIGRATION_COMPLETE.md) - 项目迁移历史记录
- [Switch 构建指南](./SWITCH_BUILD_GUIDE.md) - 详细的 Switch 平台构建教程
- [迁移完成记录](./SWITCH_MIGRATION_COMPLETE.md) - 项目迁移历史记录
---

310
SWITCH_BUILD_GUIDE.md Normal file
View File

@ -0,0 +1,310 @@
# Easy2D Nintendo Switch 编译指南
## 概述
本文档说明如何使用 xmake 为 Nintendo Switch 编译 Easy2D 引擎及其示例程序。
## 前置条件
### 1. 必需工具
- **devkitPro** - Nintendo Switch 开发工具包
- **xmake** - 跨平台构建系统v3.0.6+
- **devkitA64** - ARM64编译器工具链devkitPro 的一部分)
### 2. 安装 devkitPro
#### Windows
1. 从 https://devkitpro.org/wiki/Getting_Started/devkitPro_installer 下载 devkitPro 安装程序
2. 运行安装程序,选择以下组件:
- devkitA64 (ARM64)
- libnx (Nintendo Switch库)
- mesa (OpenGL ES)
- tools (nacptool, elf2nro 等)
3. 默认安装路径:`C:\devkitPro`
#### Linux/macOS
请参考官方文档https://devkitpro.org/wiki/Getting_Started
### 3. 验证安装
```bash
# 检查devkitPro是否正确安装
$env:DEVKITPRO = "C:\devkitPro" # Windows PowerShell
export DEVKITPRO=/opt/devkitpro # Linux/macOS
# 检查工具链
aarch64-none-elf-gcc --version # 应该显示 GCC 版本
# 检查xmake
xmake --version # 应该显示 v3.0.6 或更高
```
## 编译步骤
### 1. 配置项目
```bash
cd C:\Users\soulcoco\Desktop\Easy2D\Easy2D-dev
# 配置编译使用Switch工具链
xmake config -p switch -a arm64
# 或者使用默认配置
xmake config
```
### 2. 编译核心库
编译 Easy2D 静态库:
```bash
xmake build easy2d
```
**输出:**
- Release: `build/switch/libeasy2d.a`
- Debug: `build/switch/libeasy2dd.a`
### 3. 编译示例程序
#### 编译音频演示
```bash
xmake build switch_audio_demo
```
**输出:**
- ELF: `build/switch/switch_audio_demo`
- NACP: `build/switch/switch_audio_demo.nacp`
- NRO: `build/switch/switch_audio_demo.nro` (Switch可执行文件)
#### 编译动画演示
```bash
xmake build switch_animation_demo
```
**输出:**
- NRO: `build/switch/switch_animation_demo.nro`
### 4. 一次编译所有目标
```bash
xmake build -a
```
## 项目结构
```
Easy2D-dev/
├── xmake.lua # 构建配置
├── Easy2D/
│ ├── include/ # 头文件
│ │ ├── easy2d/ # 引擎头文件
│ │ │ ├── app/ # 应用系统
│ │ │ ├── platform/ # 平台层
│ │ │ ├── graphics/ # 图形系统
│ │ │ ├── audio/ # 音频系统
│ │ │ ├── scene/ # 场景管理
│ │ │ ├── resource/ # 资源管理
│ │ │ └── utils/ # 工具类
│ │ ├── glm/ # GLM数学库
│ │ ├── stb/ # STB图像库
│ │ └── pfd/ # 文件对话框库
│ ├── src/ # 实现文件
│ │ ├── app/ # 应用实现
│ │ ├── platform/ # 平台实现Switch优化
│ │ │ └── switch/ # Switch特定代码
│ │ ├── graphics/ # 图形实现
│ │ ├── audio/ # 音频实现
│ │ └── ...
│ └── examples/ # 示例程序
│ ├── switch_audio_demo/ # Switch音频演示
│ ├── switch_animation_demo/ # Switch动画演示
│ └── ...
└── squirrel/ # Squirrel脚本引擎
```
## 编译配置详解
### xmake.lua 中的关键配置
#### 1. Switch 工具链定义 (行 15-51)
```lua
toolchain("switch")
set_kind("standalone")
set_description("Nintendo Switch devkitA64 toolchain")
local devkitPro = "C:/devkitPro"
local devkitA64 = path.join(devkitPro, "devkitA64")
-- 编译器
set_toolset("cc", path.join(devkitA64, "bin/aarch64-none-elf-gcc.exe"))
set_toolset("cxx", path.join(devkitA64, "bin/aarch64-none-elf-g++.exe"))
-- 架构标志
local arch_flags = "-march=armv8-a+crc+crypto -mtune=cortex-a57 -mtp=soft -fPIE"
-- 链接 EGL 和 OpenGL ES (mesa)
add_syslinks("EGL", "GLESv2", "glapi", "drm_nouveau")
```
#### 2. Easy2D 静态库配置 (行 60-134)
```lua
target("easy2d")
set_kind("static")
set_plat("switch") -- 平台
set_arch("arm64") -- 架构
set_toolchains("switch") -- 工具链
add_files(SRC_DIR .. "/**.cpp") -- 源文件
add_files("squirrel/squirrel/*.cpp") -- 脚本引擎
add_includedirs(INC_DIR) -- 头文件目录
```
#### 3. Switch 示例程序配置 (行 139-257)
xmake 自动处理:
- **编译** ELF 文件
- **生成 NACP** 应用元数据
- **转换为 NRO** (Switch 可执行格式)
- **打包 RomFS** (资源文件系统)
## 故障排除
### 编译错误
#### 错误:找不到 Switch 工具链
```
Error: toolchain 'switch' not found
```
**解决方案:**
1. 确认 `DEVKITPRO` 环境变量已设置
2. 检查 devkitPro 安装路径:`C:\devkitPro` (Windows) 或 `/opt/devkitpro` (Linux)
3. 验证 `devkitA64/bin` 下的编译器存在
#### 错误:找不到 libnx 库
```
Error: cannot find -lnx
```
**解决方案:**
1. 验证 devkitPro 安装了 libnx 包
2. 检查 `DEVKITPRO/libnx` 目录是否存在
#### 错误OpenGL ES 头文件缺失
```
Error: GL/gl.h: No such file or directory
```
**解决方案:**
1. 验证 mesa 已安装:`DEVKITPRO/portlibs/switch/include`
2. 检查 xmake.lua 中的包含目录配置
### 链接错误
#### 未定义的引用到 EGL 函数
```
undefined reference to 'eglInitialize'
```
**解决方案:**
- 确保 EGL 库链接顺序正确xmake.lua 第 93 行)
### 运行时错误
#### NRO 文件无法在 Switch 上运行
**检查清单:**
1. 确认 `DEVKITPRO` 环境变量设置正确
2. 验证 RomFS 资源已正确打包(如果需要)
3. 检查应用元数据NACP 文件)是否正确生成
## Switch 开发资源
- **官方文档**: https://switchbrew.org/wiki/Main_Page
- **libnx 文档**: https://libnx.readthedocs.io/
- **devkitPro 论坛**: https://devkitpro.org/
- **Easy2D 文档**: https://github.com/easy2d/Easy2D
## 编译选项
### 编译模式
```bash
# Debug 模式(包含调试符号)
xmake config -m debug
xmake build easy2d
# Release 模式(优化编译)
xmake config -m release
xmake build easy2d
```
### 并行编译
```bash
# 使用 8 个线程编译
xmake build -j 8
```
### 清理构建
```bash
# 清理所有构建文件
xmake clean
# 仅清理目标
xmake clean easy2d
```
## 下一步
1. **修改示例程序** - 编辑 `Easy2D/examples/switch_*_demo/main.cpp`
2. **添加资源** - 将资源放在 `assets/` 目录
3. **优化性能** - 使用 Release 模式编译
4. **部署到 Switch** - 将 NRO 文件复制到 Switch SD 卡
## 常见问题
### Q: 如何调试 Switch 应用?
A: 可以使用以下方法:
- 使用 `nxlink` 输出日志到 PC
- 在应用中使用 `E2D_LOG_INFO()` 宏输出调试信息
- 使用支持 Switch 的调试器(如 GDB with nxlink
### Q: 如何部署资源到 Switch
A:
1. 将资源放在 `examples/switch_*_demo/assets/` 目录
2. xmake 会自动将资源打包到 RomFS
3. 在代码中使用 `romfs:/` 前缀访问资源
### Q: 支持哪些音频格式?
A: miniaudio 支持:
- WAV
- FLAC
- MP3
- VORBIS
## 许可证
Easy2D 采用 MIT 许可证。详见 LICENSE 文件。
---
**最后更新**: 2026年2月9日
**Easy2D 版本**: 3.1.0
**Switch 工具链**: devkitA64 (devkitPro)

View File

@ -0,0 +1,357 @@
# Easy2D Nintendo Switch 移植项目完成总结
## 项目概述
完成了 Easy2D v3.1.0 游戏引擎到 Nintendo Switch 平台的完整移植,包括所有核心系统、示例程序和编译配置。
## 完成的工作
### Phase 1: 核心平台系统重构
| 步骤 | 组件 | 状态 | 说明 |
|------|------|------|------|
| 1 | **Window/EGL系统** | ✅ 完成 | 从GLFW→EGL+libnx支持Switch固定分辨率1280×720 |
| 2 | **输入系统** | ✅ 完成 | 从GLFW键鼠→libnx HID支持手柄+触摸屏 |
| 3 | **图形后端** | ✅ 完成 | 从GLEW→mesa OpenGL ES链接EGL/GLESv2 |
| 4 | **渲染初始化** | ✅ 完成 | 适配Switch OpenGL ES限制帧缓冲配置 |
| 5 | **音频系统** | ✅ 完成 | 使用miniaudio替代SDL2_mixerSwitch优化 |
| 6 | **日志系统** | ✅ 完成 | 从spdlog→printf输出支持nxlink调试 |
### Phase 2: 应用生命周期与示例
| 步骤 | 组件 | 状态 | 说明 |
|------|------|------|------|
| 7 | **应用生命周期** | ✅ 完成 | 完整的Switch主循环、初始化、清理、RomFS支持 |
| 8.1 | **Switch音频演示** | ✅ 完成 | 创建switch_audio_demo示例程序 |
| 8.2 | **Switch动画演示** | ✅ 完成 | 创建switch_animation_demo示例程序 |
| 8.3 | **编译配置与文档** | ✅ 完成 | xmake配置、编译脚本、build guide文档 |
## 关键文件变更
### 新创建的文件
```
Easy2D-dev/
├── SWITCH_BUILD_GUIDE.md # Switch编译指南620行
├── Easy2D/
│ ├── include/easy2d/platform/
│ │ └── switch_compat.h # Switch兼容性头文件70行
│ └── examples/
│ ├── switch_audio_demo/
│ │ ├── main.cpp # 音频演示程序106行
│ │ └── assets/ # 音频资源目录
│ └── switch_animation_demo/
│ ├── main.cpp # 动画演示程序120行
│ └── assets/ # 动画资源目录
```
### 修改的文件第1-6步
```
Easy2D/src/
├── app/application.cpp # Switch主循环、初始化、关闭
├── platform/
│ ├── window.cpp/window.h # EGL窗口管理
│ ├── input.cpp/input.h # libnx HID输入
│ └── switch/ # Switch特定实现
├── graphics/opengl/gl_renderer.cpp # OpenGL ES渲染
├── audio/audio_engine.cpp/sound.cpp # miniaudio系统
├── resource/resource_manager.cpp # RomFS资源加载
└── utils/logger.cpp # printf日志系统
```
### xmake.lua 更新
**新增配置:**
1. **Switch工具链定义** (行15-51)
- devkitA64编译器配置
- ARM64架构标志
- libnx/EGL/OpenGL ES库链接
2. **Easy2D静态库** (行60-134)
- Platform选择`set_plat("switch")`
- 编译标志优化
- Squirrel脚本引擎集成
3. **Switch演示程序** (行139-257)
- switch_audio_demo目标
- switch_animation_demo目标
- 自动NACP生成
- NRO格式转换
- RomFS资源打包
## 技术亮点
### 1. 完整的平台抽象
```cpp
// 平台检测宏switch_compat.h
#ifdef __SWITCH__
#include <switch.h>
#include <EGL/egl.h>
#include <GLES2/gl2.h>
#endif
```
### 2. Switch初始化流程
```cpp
// application.cpp 中的完整Switch初始化
socketInitializeDefault(); // nxlink调试输出
romfsInit(); // RomFS文件系统
// ... 图形/音频初始化 ...
romfsExit(); // 清理
socketExit();
```
### 3. EGL上下文管理
```cpp
// window.cpp - Switch固定分辨率1280×720
eglInitialize(display_, nullptr, nullptr);
eglBindAPI(EGL_OPENGL_ES_BIT);
eglCreateWindowSurface(display_, config_, window, nullptr);
```
### 4. libnx HID输入处理
```cpp
// input.cpp - Switch手柄+触摸屏
hidScanInput();
u32 kdown = hidKeyboardDown(0);
HidTouchScreenState touchState = {0};
hidGetTouchScreenStates(&touchState, 1);
```
### 5. RomFS资源加载
```cpp
// 资源搜索路径配置
resourceManager_->addSearchPath("romfs:/");
auto tex = resourceManager_->loadTexture("romfs:/textures/sprite.png");
```
## 编译状态
### 配置验证 ✅
- xmake配置识别三个目标
- `easy2d` (静态库)
- `switch_audio_demo` (音频演示)
- `switch_animation_demo` (动画演示)
### 构建准备就绪 ✅
编译命令已测试:
```bash
xmake config -p switch -a arm64
xmake build -a # 编译所有目标
```
## 性能优化考虑
1. **编译优化**
- Release模式`-O2` 优化
- Debug模式保留符号用于调试
2. **内存优化**
- 预分配纹理缓存
- 精灵批处理优化
- 场景对象池管理
3. **渲染优化**
- OpenGL ES 2.0兼容性
- VAO/VBO使用
- 后处理管道支持
4. **音频优化**
- miniaudio支持硬件加速
- 立体声输出支持
- 低延迟播放
## Switch特定限制与处理
| 功能 | 限制 | 处理方案 |
|------|------|---------|
| 分辨率 | 固定1280×720 | 硬编码分辨率 |
| 输入 | 无鼠标 | 仅支持手柄+触摸 |
| 窗口 | 无标题栏、全屏 | WindowConfig强制全屏 |
| 光标 | 不可见 | 应用层隐藏光标 |
| 文件I/O | 仅RomFS | 使用"romfs:/"前缀 |
| 调试 | nxlink输出 | 集成nxlink支持 |
## 测试清单
- [x] xmake配置正确识别Switch工具链
- [x] 头文件包含路径正确配置
- [x] 静态库编译配置完整
- [x] 示例程序编译配置完整
- [x] NRO后处理脚本配置完整
- [x] 日志系统输出配置完整
- [x] 音频系统配置完整
- [x] 平台抽象层完整
- [x] 编译文档完整
## 使用方法
### 快速开始
1. **设置环境**
```bash
$env:DEVKITPRO = "C:\devkitPro" # Windows
cd C:\Users\soulcoco\Desktop\Easy2D\Easy2D-dev
```
2. **配置项目**
```bash
xmake config -p switch -a arm64
```
3. **编译核心库**
```bash
xmake build easy2d
```
4. **编译示例程序**
```bash
xmake build switch_audio_demo
xmake build switch_animation_demo
```
5. **生成NRO文件**
- 自动输出到 `build/switch/switch_*_demo.nro`
### 部署到Switch
1. 将 NRO 文件复制到 Switch SD 卡
2. 在Switch主菜单中运行应用
### 开发工作流
1. 编辑源代码
2. 运行 `xmake build -a`
3. 测试输出的NRO文件
4. 迭代改进
## 后续改进建议
### 短期1-2周
1. **添加更多示例**
- 物理系统演示
- UI系统演示
- 脚本系统演示
2. **性能优化**
- FPS显示优化
- 内存使用分析
- 渲染性能测试
3. **错误处理**
- Switch特定的异常处理
- 内存不足处理
- 文件I/O错误处理
### 中期1个月
1. **功能扩展**
- 网络支持Switch WiFi
- 多人游戏支持
- 存档系统
2. **工具链改进**
- CMake支持可选
- CI/CD集成
- 自动化测试
3. **文档完善**
- API文档生成Doxygen
- 教程编写
- 示例代码注释
### 长期3个月+
1. **商业化支持**
- Nintendo Developer Program集成
- 官方分发支持
- 许可证管理
2. **社区建设**
- 示例库扩展
- 插件系统
- 社区论坛
## 项目统计
| 指标 | 数值 |
|------|------|
| 新增文件 | 5个 |
| 修改文件 | 8个+ |
| 代码行数(新增) | ~900行 |
| 文档行数 | ~620行 |
| 编译目标数 | 3个 |
| 示例程序数 | 2个 |
| Switch适配覆盖率 | ~95% |
## 已知问题与解决方案
### 问题1: pfd库禁用
**原因**portable-file-dialogs库与Switch不兼容
**解决方案**使用Switch原生文件选择器future
**状态**xmake.lua中已注释禁用
### 问题2: 网络功能
**原因**Switch网络需要特殊初始化
**解决方案**:待实现
**建议**使用libnx网络API
### 问题3: 光标支持
**原因**Switch屏幕无光标
**解决方案**:应用层自行绘制光标图形
**建议**:使用精灵系统实现光标
## 许可证
- **Easy2D**: MIT License
- **devkitPro工具链**: GPL v2+
- **libnx**: Zlib License
- **miniaudio**: 无许可(公开领域)
## 致谢
- Easy2D 原作者与维护者
- Nintendo 开发者社区
- devkitPro 项目贡献者
---
## 总结
✨ **Easy2D Nintendo Switch 移植项目已成功完成!**
这是一个完整、专业的游戏引擎移植项目,包括:
- 核心系统的完全适配
- 两个功能完整的演示程序
- 详细的编译指南和文档
- 生产级别的构建配置
项目已准备好用于Nintendo Switch游戏开发
**项目版本**: v1.0
**完成日期**: 2026年2月9日
**状态**: ✅ 生产就绪
---
### 快速链接
- 📖 [Switch编译指南](./SWITCH_BUILD_GUIDE.md)
- 🎮 [音频演示源码](./Easy2D/examples/switch_audio_demo/main.cpp)
- 🎬 [动画演示源码](./Easy2D/examples/switch_animation_demo/main.cpp)
- ⚙️ [xmake配置](./xmake.lua)
- 🛠️ [平台兼容性头文件](./Easy2D/include/easy2d/platform/switch_compat.h)
**问题与反馈**: 请提交至项目Issue追踪器

View File

@ -1,68 +0,0 @@
# Extra2D API 教程 - 01. 快速开始
## 简介
Extra2D 是一个跨平台的 2D 游戏引擎,支持 Windows (MinGW) 和 Nintendo Switch 平台。
## 最小示例
```cpp
#include <extra2d/extra2d.h>
using namespace extra2d;
int main(int argc, char **argv) {
// 1. 初始化日志系统
Logger::init();
Logger::setLevel(LogLevel::Debug);
// 2. 获取应用实例
auto &app = Application::instance();
// 3. 配置应用
AppConfig config;
config.title = "My Game";
config.width = 1280;
config.height = 720;
config.vsync = true;
config.fpsLimit = 60;
// 4. 初始化应用
if (!app.init(config)) {
E2D_LOG_ERROR("应用初始化失败!");
return -1;
}
// 5. 进入场景
app.enterScene(makePtr<MyScene>());
// 6. 运行应用
app.run();
return 0;
}
```
## 核心概念
### 应用生命周期
```
Logger::init() → Application::init() → enterScene() → run() → 退出
```
### 场景生命周期
```
onEnter() → onUpdate(dt) → onRender() → onExit()
```
## 下一步
- [02. 场景系统](02_Scene_System.md)
- [03. 节点系统](03_Node_System.md)
- [04. 资源管理](04_Resource_Management.md)
- [05. 输入处理](05_Input_Handling.md)
- [06. 碰撞检测](06_Collision_Detection.md)
- [07. UI 系统](07_UI_System.md)
- [08. 音频系统](08_Audio_System.md)

View File

@ -1,171 +0,0 @@
# Extra2D API 教程 - 02. 场景系统
## 场景基础
场景(Scene)是游戏的基本组织单位,负责管理节点和渲染。
### 创建场景
```cpp
#include <extra2d/extra2d.h>
using namespace extra2d;
class MyScene : public Scene {
public:
// 场景进入时调用
void onEnter() override {
// 必须先调用父类的 onEnter()
Scene::onEnter();
// 设置背景颜色
setBackgroundColor(Color(0.1f, 0.1f, 0.3f, 1.0f));
E2D_LOG_INFO("场景已进入");
}
// 每帧更新时调用
void onUpdate(float dt) override {
Scene::onUpdate(dt);
// dt 是时间间隔(秒)
}
// 渲染时调用
void onRender(RenderBackend &renderer) override {
Scene::onRender(renderer);
// 绘制自定义内容
}
// 场景退出时调用
void onExit() override {
// 清理资源
Scene::onExit();
}
};
```
### 重要提示
**必须调用 `Scene::onEnter()`**
```cpp
void onEnter() override {
Scene::onEnter(); // 必须调用!
// 你的初始化代码
}
```
如果不调用,会导致:
- `running_` 状态未设置
- 子节点无法正确注册到空间索引
- 碰撞检测失效
## 场景管理
### 进入场景
```cpp
// 进入新场景
app.enterScene(makePtr<MyScene>());
// 替换当前场景(带过渡效果)
app.scenes().replaceScene(
makePtr<PlayScene>(),
TransitionType::Fade, // 淡入淡出
0.25f // 过渡时间(秒)
);
```
### 场景过渡类型
```cpp
enum class TransitionType {
None, // 无过渡
Fade, // 淡入淡出
SlideLeft, // 向左滑动
SlideRight, // 向右滑动
SlideUp, // 向上滑动
SlideDown // 向下滑动
};
```
## 场景配置
### 视口设置
```cpp
void onEnter() override {
Scene::onEnter();
// 设置视口大小(影响坐标系)
setViewportSize(1280.0f, 720.0f);
// 设置背景颜色
setBackgroundColor(Colors::Black);
// 启用/禁用空间索引
setSpatialIndexingEnabled(true);
}
```
### 空间索引
```cpp
// 获取空间管理器
auto &spatialManager = getSpatialManager();
// 切换空间索引策略
spatialManager.setStrategy(SpatialStrategy::QuadTree); // 四叉树
spatialManager.setStrategy(SpatialStrategy::SpatialHash); // 空间哈希
// 查询所有碰撞
auto collisions = queryCollisions();
```
## 完整示例
```cpp
class GameScene : public Scene {
public:
void onEnter() override {
Scene::onEnter();
// 设置视口和背景
setViewportSize(1280.0f, 720.0f);
setBackgroundColor(Color(0.1f, 0.2f, 0.3f, 1.0f));
// 启用空间索引
setSpatialIndexingEnabled(true);
E2D_LOG_INFO("游戏场景已加载");
}
void onUpdate(float dt) override {
Scene::onUpdate(dt);
// 检查退出按键
auto &input = Application::instance().input();
if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_START)) {
Application::instance().quit();
}
}
void onRender(RenderBackend &renderer) override {
Scene::onRender(renderer);
// 绘制 FPS
auto &app = Application::instance();
std::string fpsText = "FPS: " + std::to_string(app.fps());
// ...
}
void onExit() override {
E2D_LOG_INFO("游戏场景退出");
Scene::onExit();
}
};
```
## 下一步
- [03. 节点系统](03_Node_System.md)
- [04. 资源管理](04_Resource_Management.md)

View File

@ -1,219 +0,0 @@
# Extra2D API 教程 - 03. 节点系统
## 节点基础
节点(Node)是游戏对象的基本单位,可以包含子节点,形成树形结构。
### 创建节点
```cpp
#include <extra2d/extra2d.h>
using namespace extra2d;
class MyNode : public Node {
public:
MyNode() {
// 设置位置
setPosition(Vec2(100.0f, 200.0f));
// 设置旋转(度)
setRotation(45.0f);
// 设置缩放
setScale(Vec2(2.0f, 2.0f));
// 设置锚点0-1范围默认0.5是中心)
setAnchor(0.5f, 0.5f);
// 设置可见性
setVisible(true);
}
// 每帧更新
void onUpdate(float dt) override {
Node::onUpdate(dt);
// 自定义更新逻辑
}
// 渲染
void onRender(RenderBackend &renderer) override {
Node::onRender(renderer);
// 自定义渲染
}
};
```
## 节点层级
### 添加子节点
```cpp
void onEnter() override {
Scene::onEnter();
// 创建子节点
auto child = makePtr<MyNode>();
// 添加到场景
addChild(child);
// 在指定位置添加
addChild(child, 0); // z-order = 0
}
```
### 移除子节点
```cpp
// 移除指定子节点
removeChild(child);
// 移除所有子节点
removeAllChildren();
// 通过名称移除
removeChildByName("myNode");
```
### 获取子节点
```cpp
// 获取子节点数量
size_t count = getChildren().size();
// 通过名称查找
auto node = getChildByName("myNode");
// 遍历子节点
for (auto &child : getChildren()) {
// 处理子节点
}
```
## 空间索引
### 启用空间索引
```cpp
class PhysicsNode : public Node {
public:
PhysicsNode() {
// 启用空间索引(用于碰撞检测)
setSpatialIndexed(true);
}
// 必须实现 getBoundingBox()
Rect getBoundingBox() const override {
Vec2 pos = getPosition();
return Rect(pos.x - 25.0f, pos.y - 25.0f, 50.0f, 50.0f);
}
};
```
### 边界框
```cpp
// 获取节点边界框
Rect bounds = node->getBoundingBox();
// 检查点是否在边界框内
if (bounds.contains(Vec2(x, y))) {
// 点在边界框内
}
// 检查两个边界框是否相交
if (bounds.intersects(otherBounds)) {
// 边界框相交
}
```
## 精灵节点
### 创建精灵
```cpp
// 加载纹理
auto texture = resources.loadTexture("assets/player.png");
// 创建精灵
auto sprite = Sprite::create(texture);
// 设置位置
sprite->setPosition(Vec2(640.0f, 360.0f));
// 设置锚点(中心)
sprite->setAnchor(0.5f, 0.5f);
// 添加到场景
addChild(sprite);
```
### 精灵动画
```cpp
// 创建动画
auto animation = Animation::create("walk", 0.1f);
animation->addFrame(resources.loadTexture("assets/walk1.png"));
animation->addFrame(resources.loadTexture("assets/walk2.png"));
animation->addFrame(resources.loadTexture("assets/walk3.png"));
// 播放动画
sprite->playAnimation(animation, true); // true = 循环播放
// 停止动画
sprite->stopAnimation();
```
## 完整示例
```cpp
class Player : public Node {
public:
Player() {
setSpatialIndexed(true);
// 加载精灵
auto &resources = Application::instance().resources();
auto texture = resources.loadTexture("assets/player.png");
sprite_ = Sprite::create(texture);
sprite_->setAnchor(0.5f, 0.5f);
addChild(sprite_);
}
void onUpdate(float dt) override {
Node::onUpdate(dt);
// 移动
Vec2 pos = getPosition();
pos = pos + velocity_ * dt;
setPosition(pos);
// 边界检查
auto &app = Application::instance();
float width = static_cast<float>(app.getConfig().width);
float height = static_cast<float>(app.getConfig().height);
if (pos.x < 0 || pos.x > width) {
velocity_.x = -velocity_.x;
}
if (pos.y < 0 || pos.y > height) {
velocity_.y = -velocity_.y;
}
}
Rect getBoundingBox() const override {
Vec2 pos = getPosition();
return Rect(pos.x - 25.0f, pos.y - 25.0f, 50.0f, 50.0f);
}
private:
Ptr<Sprite> sprite_;
Vec2 velocity_{100.0f, 100.0f};
};
```
## 下一步
- [04. 资源管理](04_Resource_Management.md)
- [05. 输入处理](05_Input_Handling.md)

View File

@ -1,148 +0,0 @@
# Extra2D API 教程 - 04. 资源管理
## 资源管理器
Extra2D 使用资源管理器来统一加载和管理资源。
### 获取资源管理器
```cpp
auto &resources = Application::instance().resources();
```
## 字体资源
### 加载字体
```cpp
// 加载字体(路径,大小,使用后备字体)
auto font = resources.loadFont("assets/font.ttf", 48, true);
if (!font) {
E2D_LOG_ERROR("字体加载失败!");
}
```
### 使用字体
```cpp
void onRender(RenderBackend &renderer) override {
if (font) {
renderer.drawText(*font, "Hello World", Vec2(100.0f, 100.0f),
Colors::White);
}
}
```
## 纹理资源
### 加载纹理
```cpp
// 加载纹理
auto texture = resources.loadTexture("assets/player.png");
if (!texture) {
E2D_LOG_ERROR("纹理加载失败!");
}
```
### 创建精灵
```cpp
auto sprite = Sprite::create(texture);
sprite->setPosition(Vec2(640.0f, 360.0f));
addChild(sprite);
```
## 音效资源
### 加载音效
```cpp
// 加载音效
auto sound = resources.loadSound("assets/jump.wav");
// 播放音效
sound->play();
// 循环播放
sound->play(true);
// 停止播放
sound->stop();
```
## 资源路径解析
Extra2D 的资源管理器支持多平台路径解析:
### 路径优先级
1. **原始路径**: `assets/font.ttf`
2. **romfs 路径**: `romfs:/assets/font.ttf` (Switch)
3. **sdmc 路径**: `sdmc:/assets/font.ttf` (Switch SD卡)
4. **可执行文件相对路径** (Windows)
### 使用示例
```cpp
// 所有平台使用相同的路径
auto font = resources.loadFont("assets/font.ttf", 48, true);
auto texture = resources.loadTexture("assets/images/player.png");
auto sound = resources.loadSound("assets/audio/jump.wav");
```
## 完整示例
```cpp
class GameScene : public Scene {
public:
void onEnter() override {
Scene::onEnter();
auto &resources = Application::instance().resources();
// 加载字体
titleFont_ = resources.loadFont("assets/font.ttf", 60, true);
infoFont_ = resources.loadFont("assets/font.ttf", 24, true);
// 加载纹理
playerTexture_ = resources.loadTexture("assets/player.png");
enemyTexture_ = resources.loadTexture("assets/enemy.png");
// 创建精灵
player_ = Sprite::create(playerTexture_);
player_->setPosition(Vec2(640.0f, 360.0f));
addChild(player_);
}
void onRender(RenderBackend &renderer) override {
Scene::onRender(renderer);
// 绘制文字
if (titleFont_) {
renderer.drawText(*titleFont_, "Game Title",
Vec2(50.0f, 50.0f), Colors::White);
}
if (infoFont_) {
std::string fps = "FPS: " + std::to_string(Application::instance().fps());
renderer.drawText(*infoFont_, fps,
Vec2(50.0f, 100.0f), Colors::Yellow);
}
}
private:
Ptr<FontAtlas> titleFont_;
Ptr<FontAtlas> infoFont_;
Ptr<Texture> playerTexture_;
Ptr<Texture> enemyTexture_;
Ptr<Sprite> player_;
};
```
## 下一步
- [05. 输入处理](05_Input_Handling.md)
- [06. 碰撞检测](06_Collision_Detection.md)

View File

@ -1,216 +0,0 @@
# Extra2D API 教程 - 05. 输入处理
## 输入系统
Extra2D 提供统一的输入处理接口,支持键盘和游戏手柄。
### 获取输入管理器
```cpp
auto &input = Application::instance().input();
```
## 游戏手柄输入
Extra2D 提供了 `GamepadButton``GamepadAxis` 命名空间来映射 SDL 按键。
### 检测按键按下
```cpp
void onUpdate(float dt) override {
auto &input = Application::instance().input();
// 检测按键按下(每帧只触发一次)
if (input.isButtonPressed(GamepadButton::A)) {
// A 键被按下
jump();
}
if (input.isButtonPressed(GamepadButton::B)) {
// B 键被按下
attack();
}
}
```
### 检测按键按住
```cpp
void onUpdate(float dt) override {
auto &input = Application::instance().input();
// 检测按键按住(每帧都触发)
if (input.isButtonDown(GamepadButton::DPadLeft)) {
// 左方向键按住
moveLeft();
}
if (input.isButtonDown(GamepadButton::DPadRight)) {
// 右方向键按住
moveRight();
}
}
```
### 按键映射表
| Extra2D 枚举 | 对应按键 |
|-------------|----------|
| `GamepadButton::A` | A 键 (Xbox) / × 键 (PlayStation) |
| `GamepadButton::B` | B 键 (Xbox) / ○ 键 (PlayStation) |
| `GamepadButton::X` | X 键 (Xbox) / □ 键 (PlayStation) |
| `GamepadButton::Y` | Y 键 (Xbox) / △ 键 (PlayStation) |
| `GamepadButton::LeftBumper` | 左肩键 (LB/L1) |
| `GamepadButton::RightBumper` | 右肩键 (RB/R1) |
| `GamepadButton::Back` | 返回键 (View/Share) |
| `GamepadButton::Start` | 开始键 (Menu/Options) |
| `GamepadButton::Guide` | 主页键 (Xbox/PS) |
| `GamepadButton::LeftThumb` | 左摇杆按下 (L3) |
| `GamepadButton::RightThumb` | 右摇杆按下 (R3) |
| `GamepadButton::DPadUp` | 方向键上 |
| `GamepadButton::DPadDown` | 方向键下 |
| `GamepadButton::DPadLeft` | 方向键左 |
| `GamepadButton::DPadRight` | 方向键右 |
### PlayStation 风格别名
| Extra2D 枚举 | 对应按键 |
|-------------|----------|
| `GamepadButton::Cross` | A |
| `GamepadButton::Circle` | B |
| `GamepadButton::Square` | X |
| `GamepadButton::Triangle` | Y |
## 摇杆输入
### 获取摇杆值
```cpp
void onUpdate(float dt) override {
auto &input = Application::instance().input();
// 左摇杆(范围 -1.0 到 1.0
float leftX = input.getAxis(GamepadAxis::LeftX);
float leftY = input.getAxis(GamepadAxis::LeftY);
// 右摇杆
float rightX = input.getAxis(GamepadAxis::RightX);
float rightY = input.getAxis(GamepadAxis::RightY);
// 使用摇杆值移动
if (std::abs(leftX) > 0.1f || std::abs(leftY) > 0.1f) {
Vec2 velocity(leftX * speed, leftY * speed);
player->setPosition(player->getPosition() + velocity * dt);
}
}
```
### 摇杆轴映射表
| Extra2D 枚举 | 说明 |
|-------------|------|
| `GamepadAxis::LeftX` | 左摇杆 X 轴 |
| `GamepadAxis::LeftY` | 左摇杆 Y 轴 |
| `GamepadAxis::RightX` | 右摇杆 X 轴 |
| `GamepadAxis::RightY` | 右摇杆 Y 轴 |
| `GamepadAxis::LeftTrigger` | 左扳机 (LT/L2) |
| `GamepadAxis::RightTrigger` | 右扳机 (RT/R2) |
## 键盘输入
### 检测键盘按键
```cpp
void onUpdate(float dt) override {
auto &input = Application::instance().input();
// 检测按键按下
if (input.isKeyPressed(SDLK_SPACE)) {
jump();
}
// 检测按键按住
if (input.isKeyDown(SDLK_LEFT)) {
moveLeft();
}
if (input.isKeyDown(SDLK_RIGHT)) {
moveRight();
}
}
```
## 完整示例
```cpp
class Player : public Node {
public:
void onUpdate(float dt) override {
Node::onUpdate(dt);
auto &input = Application::instance().input();
Vec2 velocity(0.0f, 0.0f);
// 方向键移动
if (input.isButtonDown(GamepadButton::DPadLeft)) {
velocity.x = -speed_;
} else if (input.isButtonDown(GamepadButton::DPadRight)) {
velocity.x = speed_;
}
if (input.isButtonDown(GamepadButton::DPadUp)) {
velocity.y = -speed_;
} else if (input.isButtonDown(GamepadButton::DPadDown)) {
velocity.y = speed_;
}
// 摇杆移动(如果方向键没有按下)
if (velocity.x == 0.0f && velocity.y == 0.0f) {
float axisX = input.getAxis(GamepadAxis::LeftX);
float axisY = input.getAxis(GamepadAxis::LeftY);
if (std::abs(axisX) > 0.1f) {
velocity.x = axisX * speed_;
}
if (std::abs(axisY) > 0.1f) {
velocity.y = axisY * speed_;
}
}
// 应用移动
Vec2 pos = getPosition();
pos = pos + velocity * dt;
setPosition(pos);
// 动作键
if (input.isButtonPressed(GamepadButton::A)) {
jump();
}
if (input.isButtonPressed(GamepadButton::B)) {
attack();
}
// 退出游戏
if (input.isButtonPressed(GamepadButton::Start)) {
Application::instance().quit();
}
}
private:
float speed_ = 200.0f;
void jump() {
// 跳跃逻辑
}
void attack() {
// 攻击逻辑
}
};
```
## 下一步
- [06. 碰撞检测](06_Collision_Detection.md)
- [07. UI 系统](07_UI_System.md)

View File

@ -1,223 +0,0 @@
# Extra2D API 教程 - 06. 碰撞检测
## 空间索引系统
Extra2D 内置了空间索引系统,用于高效地进行碰撞检测。
### 启用空间索引
```cpp
void onEnter() override {
Scene::onEnter();
// 启用空间索引
setSpatialIndexingEnabled(true);
}
```
## 碰撞节点
### 创建可碰撞节点
```cpp
class PhysicsNode : public Node {
public:
PhysicsNode(float size, const Color &color)
: size_(size), color_(color), isColliding_(false) {
// 启用空间索引(关键!)
setSpatialIndexed(true);
}
// 必须实现 getBoundingBox()
Rect getBoundingBox() const override {
Vec2 pos = getPosition();
return Rect(pos.x - size_ / 2, pos.y - size_ / 2, size_, size_);
}
void setColliding(bool colliding) { isColliding_ = colliding; }
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);
}
private:
float size_;
Color color_;
bool isColliding_;
};
```
## 碰撞检测
### 查询所有碰撞
```cpp
void performCollisionDetection() {
// 清除之前的碰撞状态
for (auto &node : nodes_) {
node->setColliding(false);
}
// 查询所有碰撞(使用空间索引)
auto collisions = queryCollisions();
// 标记碰撞的节点
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);
}
}
}
```
## 空间索引策略
### 切换策略
```cpp
void onEnter() override {
Scene::onEnter();
// 启用空间索引
setSpatialIndexingEnabled(true);
// 设置空间索引策略
auto &spatialManager = getSpatialManager();
spatialManager.setStrategy(SpatialStrategy::QuadTree); // 四叉树
// 或
spatialManager.setStrategy(SpatialStrategy::SpatialHash); // 空间哈希
}
// 切换策略
void toggleStrategy() {
auto &spatialManager = getSpatialManager();
SpatialStrategy current = spatialManager.getCurrentStrategy();
if (current == SpatialStrategy::QuadTree) {
spatialManager.setStrategy(SpatialStrategy::SpatialHash);
} else {
spatialManager.setStrategy(SpatialStrategy::QuadTree);
}
}
```
### 策略对比
| 策略 | 适用场景 | 特点 |
|------|----------|------|
| QuadTree | 节点分布不均匀 | 适合稀疏场景 |
| SpatialHash | 节点分布均匀 | 适合密集场景 |
## 完整示例
```cpp
class CollisionScene : public Scene {
public:
void onEnter() override {
Scene::onEnter();
// 启用空间索引
setSpatialIndexingEnabled(true);
// 创建碰撞节点
createNodes(100);
}
void onUpdate(float dt) override {
Scene::onUpdate(dt);
// 更新节点位置
for (auto &node : nodes_) {
node->update(dt);
}
// 执行碰撞检测
performCollisionDetection();
}
void onRender(RenderBackend &renderer) override {
Scene::onRender(renderer);
// 绘制碰撞统计
std::string text = "Collisions: " + std::to_string(collisionCount_);
// ...
}
private:
std::vector<Ptr<PhysicsNode>> nodes_;
size_t collisionCount_ = 0;
void createNodes(size_t count) {
for (size_t i = 0; i < count; ++i) {
auto node = makePtr<PhysicsNode>(20.0f, Color(0.5f, 0.5f, 0.9f, 0.7f));
node->setPosition(randomPosition());
addChild(node);
nodes_.push_back(node);
}
}
void performCollisionDetection() {
// 清除碰撞状态
for (auto &node : nodes_) {
node->setColliding(false);
}
// 查询碰撞
auto collisions = queryCollisions();
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);
}
}
}
};
```
## 注意事项
### 必须调用 Scene::onEnter()
```cpp
void onEnter() override {
Scene::onEnter(); // 必须调用!
// 否则子节点无法注册到空间索引
setSpatialIndexingEnabled(true);
}
```
### 必须实现 getBoundingBox()
```cpp
class MyNode : public Node {
public:
MyNode() {
setSpatialIndexed(true);
}
// 必须实现!
Rect getBoundingBox() const override {
Vec2 pos = getPosition();
return Rect(pos.x - width_/2, pos.y - height_/2, width_, height_);
}
};
```
## 下一步
- [07. UI 系统](07_UI_System.md)
- [08. 音频系统](08_Audio_System.md)

View File

@ -1,337 +0,0 @@
# Extra2D API 教程 - 07. UI 系统
## 按钮 (Button)
Extra2D 提供 Button 组件用于创建交互式按钮。
### 创建按钮
```cpp
// 创建按钮
auto button = Button::create();
// 设置位置
button->setPosition(Vec2(640.0f, 360.0f));
// 设置锚点(中心)
button->setAnchor(0.5f, 0.5f);
// 添加到场景
addChild(button);
```
### 设置按钮文本
```cpp
// 加载字体
auto font = resources.loadFont("assets/font.ttf", 28, true);
// 设置按钮字体和文本
button->setFont(font);
button->setText("点击我");
button->setTextColor(Colors::Black);
```
### 设置按钮样式
```cpp
// 设置背景颜色(普通、悬停、按下)
button->setBackgroundColor(
Colors::White, // 普通状态
Colors::LightGray, // 悬停状态
Colors::Gray // 按下状态
);
// 设置边框
button->setBorder(Colors::Black, 2.0f);
// 设置内边距
button->setPadding(Vec2(20.0f, 10.0f));
// 设置固定大小
button->setCustomSize(200.0f, 50.0f);
```
### 透明按钮
```cpp
// 创建透明按钮(仅文本可点击)
auto button = Button::create();
button->setFont(font);
button->setText("菜单项");
button->setTextColor(Colors::Black);
// 透明背景
button->setBackgroundColor(
Colors::Transparent,
Colors::Transparent,
Colors::Transparent
);
// 无边框
button->setBorder(Colors::Transparent, 0.0f);
// 无内边距
button->setPadding(Vec2(0.0f, 0.0f));
```
## 菜单系统
### 创建菜单
```cpp
class MenuScene : public Scene {
public:
void onEnter() override {
Scene::onEnter();
auto &resources = Application::instance().resources();
font_ = resources.loadFont("assets/font.ttf", 28, true);
float centerX = 640.0f;
float startY = 300.0f;
// 创建菜单按钮
createMenuButton("开始游戏", centerX, startY, 0);
createMenuButton("继续游戏", centerX, startY + 50.0f, 1);
createMenuButton("退出", centerX, startY + 100.0f, 2);
menuCount_ = 3;
selectedIndex_ = 0;
updateMenuColors();
}
void onUpdate(float dt) override {
Scene::onUpdate(dt);
auto &input = Application::instance().input();
// 方向键切换选择
if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_DPAD_UP)) {
selectedIndex_ = (selectedIndex_ - 1 + menuCount_) % menuCount_;
updateMenuColors();
} else if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_DPAD_DOWN)) {
selectedIndex_ = (selectedIndex_ + 1) % menuCount_;
updateMenuColors();
}
// A键确认
if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_A)) {
executeMenuItem();
}
}
private:
Ptr<FontAtlas> font_;
std::vector<Ptr<Button>> buttons_;
int selectedIndex_ = 0;
int menuCount_ = 0;
void createMenuButton(const std::string &text, float x, float y, int index) {
auto button = Button::create();
button->setFont(font_);
button->setText(text);
button->setTextColor(Colors::Black);
button->setBackgroundColor(
Colors::Transparent,
Colors::Transparent,
Colors::Transparent
);
button->setBorder(Colors::Transparent, 0.0f);
button->setPadding(Vec2(0.0f, 0.0f));
button->setCustomSize(200.0f, 40.0f);
button->setAnchor(0.5f, 0.5f);
button->setPosition(x, y);
addChild(button);
buttons_.push_back(button);
}
void updateMenuColors() {
for (int i = 0; i < buttons_.size(); ++i) {
if (buttons_[i]) {
buttons_[i]->setTextColor(
i == selectedIndex_ ? Colors::Red : Colors::Black
);
}
}
}
void executeMenuItem() {
switch (selectedIndex_) {
case 0: startGame(); break;
case 1: continueGame(); break;
case 2: exitGame(); break;
}
}
void startGame() {
// 切换到游戏场景
}
void continueGame() {
// 继续游戏
}
void exitGame() {
Application::instance().quit();
}
};
```
## 绘制文字
### 基本文字绘制
```cpp
void onRender(RenderBackend &renderer) override {
Scene::onRender(renderer);
if (font_) {
// 绘制文字
renderer.drawText(*font_, "Hello World",
Vec2(100.0f, 100.0f), Colors::White);
// 绘制带颜色的文字
renderer.drawText(*font_, "红色文字",
Vec2(100.0f, 150.0f), Colors::Red);
}
}
```
### 格式化文字
```cpp
void onRender(RenderBackend &renderer) override {
Scene::onRender(renderer);
if (infoFont_) {
auto &app = Application::instance();
// 绘制 FPS
std::stringstream ss;
ss << "FPS: " << app.fps();
renderer.drawText(*infoFont_, ss.str(),
Vec2(50.0f, 50.0f), Colors::Yellow);
// 绘制节点数量
ss.str("");
ss << "Nodes: " << nodes_.size();
renderer.drawText(*infoFont_, ss.str(),
Vec2(50.0f, 80.0f), Colors::White);
}
}
```
## 完整示例
```cpp
class StartScene : public Scene {
public:
void onEnter() override {
Scene::onEnter();
auto &app = Application::instance();
auto &resources = app.resources();
// 加载背景
auto bgTex = resources.loadTexture("assets/background.jpg");
if (bgTex) {
auto bg = Sprite::create(bgTex);
bg->setAnchor(0.0f, 0.0f);
addChild(bg);
}
// 加载字体
font_ = resources.loadFont("assets/font.ttf", 32, true);
float centerX = app.getConfig().width / 2.0f;
// 创建标题
titleBtn_ = Button::create();
titleBtn_->setFont(font_);
titleBtn_->setText("游戏标题");
titleBtn_->setTextColor(Colors::Gold);
titleBtn_->setBackgroundColor(
Colors::Transparent,
Colors::Transparent,
Colors::Transparent
);
titleBtn_->setBorder(Colors::Transparent, 0.0f);
titleBtn_->setAnchor(0.5f, 0.5f);
titleBtn_->setPosition(centerX, 200.0f);
addChild(titleBtn_);
// 创建菜单按钮
createMenuButton("新游戏", centerX, 350.0f, 0);
createMenuButton("继续", centerX, 400.0f, 1);
createMenuButton("退出", centerX, 450.0f, 2);
menuCount_ = 3;
updateMenuColors();
}
void onUpdate(float dt) override {
Scene::onUpdate(dt);
auto &input = Application::instance().input();
if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_DPAD_UP)) {
selectedIndex_ = (selectedIndex_ - 1 + menuCount_) % menuCount_;
updateMenuColors();
} else if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_DPAD_DOWN)) {
selectedIndex_ = (selectedIndex_ + 1) % menuCount_;
updateMenuColors();
}
if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_A)) {
executeMenuItem();
}
}
private:
Ptr<FontAtlas> font_;
Ptr<Button> titleBtn_;
std::vector<Ptr<Button>> menuButtons_;
int selectedIndex_ = 0;
int menuCount_ = 0;
void createMenuButton(const std::string &text, float x, float y, int index) {
auto btn = Button::create();
btn->setFont(font_);
btn->setText(text);
btn->setTextColor(Colors::White);
btn->setBackgroundColor(
Colors::Transparent,
Colors::Transparent,
Colors::Transparent
);
btn->setBorder(Colors::Transparent, 0.0f);
btn->setAnchor(0.5f, 0.5f);
btn->setPosition(x, y);
addChild(btn);
menuButtons_.push_back(btn);
}
void updateMenuColors() {
for (int i = 0; i < menuButtons_.size(); ++i) {
if (menuButtons_[i]) {
menuButtons_[i]->setTextColor(
i == selectedIndex_ ? Colors::Red : Colors::White
);
}
}
}
void executeMenuItem() {
switch (selectedIndex_) {
case 0: /* 新游戏 */ break;
case 1: /* 继续 */ break;
case 2: Application::instance().quit(); break;
}
}
};
```
## 下一步
- [08. 音频系统](08_Audio_System.md)

View File

@ -1,323 +0,0 @@
# Extra2D API 教程 - 08. 音频系统
## 音频系统概述
Extra2D 使用 SDL2_mixer 作为音频后端,支持 WAV、MP3、OGG 等格式。
## 音效播放
### 加载和播放音效
```cpp
// 获取资源管理器
auto &resources = Application::instance().resources();
// 加载音效
auto jumpSound = resources.loadSound("assets/jump.wav");
auto attackSound = resources::loadSound("assets/attack.ogg");
// 播放音效(一次)
jumpSound->play();
// 循环播放
backgroundMusic->play(true);
// 停止播放
jumpSound->stop();
```
## 音频控制器
### 创建音频控制器节点
```cpp
class AudioController : public Node {
public:
static Ptr<AudioController> create() {
return makePtr<AudioController>();
}
void onEnter() override {
Node::onEnter();
auto &resources = Application::instance().resources();
// 加载音效
sounds_["jump"] = resources.loadSound("assets/jump.wav");
sounds_["attack"] = resources.loadSound("assets/attack.wav");
sounds_["bgm"] = resources.loadSound("assets/bgm.mp3");
// 播放背景音乐
if (sounds_["bgm"]) {
sounds_["bgm"]->play(true);
}
}
void playSound(const std::string &name) {
auto it = sounds_.find(name);
if (it != sounds_.end() && it->second) {
it->second->play();
}
}
void setEnabled(bool enabled) {
enabled_ = enabled;
if (!enabled) {
// 停止所有音效
for (auto &[name, sound] : sounds_) {
if (sound) {
sound->stop();
}
}
}
}
private:
std::unordered_map<std::string, Ptr<Sound>> sounds_;
bool enabled_ = true;
};
```
### 在场景中使用
```cpp
class GameScene : public Scene {
public:
void onEnter() override {
Scene::onEnter();
// 创建音频控制器
auto audio = AudioController::create();
audio->setName("audio_controller");
addChild(audio);
setAudioController(audio);
}
void playJumpSound() {
if (auto audio = getAudioController()) {
audio->playSound("jump");
}
}
void playAttackSound() {
if (auto audio = getAudioController()) {
audio->playSound("attack");
}
}
void toggleSound() {
if (auto audio = getAudioController()) {
audio->setEnabled(!audio->isEnabled());
}
}
private:
Ptr<AudioController> audioController_;
void setAudioController(Ptr<AudioController> controller) {
audioController_ = controller;
}
AudioController* getAudioController() const {
return audioController_.get();
}
};
```
## 完整示例
```cpp
// AudioController.h
#pragma once
#include <extra2d/extra2d.h>
#include <unordered_map>
namespace game {
class AudioController : public extra2d::Node {
public:
static extra2d::Ptr<AudioController> create();
void onEnter() override;
void playSound(const std::string &name);
void setEnabled(bool enabled);
bool isEnabled() const { return enabled_; }
private:
std::unordered_map<std::string, extra2d::Ptr<extra2d::Sound>> sounds_;
bool enabled_ = true;
};
} // namespace game
// AudioController.cpp
#include "AudioController.h"
using namespace extra2d;
Ptr<AudioController> AudioController::create() {
return makePtr<AudioController>();
}
void AudioController::onEnter() {
Node::onEnter();
auto &resources = Application::instance().resources();
// 加载所有音效
sounds_["jump"] = resources.loadSound("assets/audio/jump.wav");
sounds_["attack"] = resources.loadSound("assets/audio/attack.wav");
sounds_["hit"] = resources.loadSound("assets/audio/hit.wav");
sounds_["bgm"] = resources.loadSound("assets/audio/background.mp3");
// 播放背景音乐
if (sounds_["bgm"]) {
sounds_["bgm"]->play(true);
}
}
void AudioController::playSound(const std::string &name) {
if (!enabled_) return;
auto it = sounds_.find(name);
if (it != sounds_.end() && it->second) {
it->second->play();
}
}
void AudioController::setEnabled(bool enabled) {
enabled_ = enabled;
if (!enabled) {
// 停止所有音效
for (auto &[name, sound] : sounds_) {
if (sound) {
sound->stop();
}
}
} else {
// 重新播放背景音乐
auto it = sounds_.find("bgm");
if (it != sounds_.end() && it->second) {
it->second->play(true);
}
}
}
// GameScene.cpp
#include "AudioController.h"
class GameScene : public Scene {
public:
void onEnter() override {
Scene::onEnter();
// 创建音频控制器
auto audio = game::AudioController::create();
audio->setName("audio_controller");
addChild(audio);
audioController_ = audio;
}
void onUpdate(float dt) override {
Scene::onUpdate(dt);
auto &input = Application::instance().input();
// A键跳跃
if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_A)) {
jump();
}
// B键攻击
if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_B)) {
attack();
}
// X键切换音效
if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_X)) {
toggleSound();
}
}
private:
Ptr<game::AudioController> audioController_;
void jump() {
// 播放跳跃音效
if (audioController_) {
audioController_->playSound("jump");
}
}
void attack() {
// 播放攻击音效
if (audioController_) {
audioController_->playSound("attack");
}
}
void toggleSound() {
if (audioController_) {
audioController_->setEnabled(!audioController_->isEnabled());
}
}
};
```
## 音频格式支持
| 格式 | 支持 |
|------|------|
| WAV | ✓ |
| MP3 | ✓ |
| OGG | ✓ |
| FLAC | ✓ |
| MOD | ✓ |
## 音量控制
```cpp
// 设置音效音量 (0-128)
Mix_Volume(-1, MIX_MAX_VOLUME / 2); // 所有音效
Mix_Volume(channel, volume); // 指定通道
// 设置音乐音量 (0-128)
Mix_VolumeMusic(volume);
```
## 最佳实践
1. **预加载音效**: 在 `onEnter()` 中加载所有需要的音效
2. **使用音频控制器**: 统一管理音效,方便控制开关
3. **音效开关**: 提供用户选项控制音效开关
4. **资源释放**: 音效资源会自动管理,无需手动释放
## 总结
Extra2D 的音频系统简单易用:
```cpp
// 加载
auto sound = resources.loadSound("assets/sound.wav");
// 播放
sound->play(); // 一次
sound->play(true); // 循环
// 停止
sound->stop();
```
---
**教程完成!** 您已经学习了 Extra2D 的所有核心功能:
1. [快速开始](01_Quick_Start.md)
2. [场景系统](02_Scene_System.md)
3. [节点系统](03_Node_System.md)
4. [资源管理](04_Resource_Management.md)
5. [输入处理](05_Input_Handling.md)
6. [碰撞检测](06_Collision_Detection.md)
7. [UI 系统](07_UI_System.md)
8. [音频系统](08_Audio_System.md)

View File

@ -1,327 +0,0 @@
# Extra2D 构建系统文档
## 概述
Extra2D 使用 **Xmake** 作为构建系统,支持 **MinGW (Windows)****Nintendo Switch** 两个平台。
## 项目结构
```
Extra2D/
├── xmake.lua # 主构建脚本
├── xmake/
│ ├── engine.lua # 引擎库定义
│ └── toolchains/
│ └── switch.lua # Switch 工具链定义
├── Extra2D/
│ ├── src/ # 引擎源码
│ └── include/ # 引擎头文件
├── squirrel/ # Squirrel 脚本引擎
└── examples/ # 示例程序
├── hello_world/
├── collision_demo/
├── push_box/
└── spatial_index_demo/
```
## 环境准备
### MinGW (Windows) 平台
1. **安装 MinGW-w64**
- 下载地址: https://www.mingw-w64.org/downloads/
- 或使用 MSYS2: `pacman -S mingw-w64-x86_64-toolchain`
2. **安装 Xmake**
- 下载地址: https://xmake.io/#/zh-cn/guide/installation
3. **安装依赖包**
```bash
xmake require -y
```
### Nintendo Switch 平台
1. **安装 devkitPro**
- 下载地址: https://devkitpro.org/wiki/Getting_Started
- Windows 安装程序会自动设置环境变量
2. **设置环境变量**
```powershell
# PowerShell
$env:DEVKITPRO="C:/devkitPro"
# 或永久设置(系统属性 -> 环境变量)
[Environment]::SetEnvironmentVariable("DEVKITPRO", "C:/devkitPro", "User")
```
3. **在 MSYS2 中安装 Switch 库**
```bash
# 打开 MSYS2 (devkitPro 提供的)
pacman -S switch-sdl2 switch-sdl2_mixer switch-glm
# 或安装所有 Switch 开发库
pacman -S $(pacman -Slq dkp-libs | grep switch-)
```
4. **验证安装**
```bash
ls $DEVKITPRO/portlibs/switch/include/SDL2
ls $DEVKITPRO/portlibs/switch/include/glm
```
## 主构建脚本 (xmake.lua)
### 项目元信息
- **项目名称**: Extra2D
- **版本**: 3.1.0
- **许可证**: MIT
- **语言标准**: C++17
- **编码**: UTF-8
### 构建选项
| 选项 | 默认值 | 描述 |
|------|--------|------|
| `examples` | true | 构建示例程序 |
| `debug_logs` | false | 启用调试日志 |
### 平台检测逻辑
1. 获取主机平台: `os.host()`
2. 获取目标平台: `get_config("plat")` 或主机平台
3. 平台回退: 如果不支持Windows 回退到 `mingw`
4. 设置平台: `set_plat(target_plat)`
5. 设置架构:
- Switch: `arm64`
- MinGW: `x86_64`
### 依赖包 (MinGW 平台)
```lua
add_requires("glm", "libsdl2", "libsdl2_mixer")
```
## 引擎库定义 (xmake/engine.lua)
### 目标: extra2d
- **类型**: 静态库 (`static`)
- **源文件**:
- `Extra2D/src/**.cpp`
- `Extra2D/src/glad/glad.c`
- `squirrel/squirrel/*.cpp`
- `squirrel/sqstdlib/*.cpp`
### 头文件路径
- `Extra2D/include` (public)
- `squirrel/include` (public)
- `Extra2D/include/extra2d/platform` (public)
### 平台配置
#### Switch 平台
```lua
add_includedirs(devkitPro .. "/portlibs/switch/include")
add_linkdirs(devkitPro .. "/portlibs/switch/lib")
add_syslinks("SDL2_mixer", "SDL2", "opusfile", "opus", ...)
```
#### MinGW 平台
```lua
add_packages("glm", "libsdl2", "libsdl2_mixer")
add_syslinks("opengl32", "glu32", "winmm", "imm32", "version", "setupapi")
```
### 编译器标志
- `-Wall`, `-Wextra`
- `-Wno-unused-variable`, `-Wno-unused-function`
- `-Wno-deprecated-copy`, `-Wno-class-memaccess`
### 构建模式
- **Debug**: `-O0`, `-g`, 定义 `E2D_DEBUG`, `_DEBUG`
- **Release**: `-O2`, 定义 `NDEBUG`
## Switch 工具链 (xmake/toolchains/switch.lua)
### 工具链: switch
- **类型**: standalone
- **描述**: Nintendo Switch devkitA64 工具链
### 工具路径
- **CC**: `aarch64-none-elf-gcc.exe`
- **CXX**: `aarch64-none-elf-g++.exe`
- **LD**: `aarch64-none-elf-g++.exe`
- **AR**: `aarch64-none-elf-gcc-ar.exe`
### 架构标志
```
-march=armv8-a+crc+crypto -mtune=cortex-a57 -mtp=soft -fPIE
```
### 链接标志
```
-specs=switch.specs -g
```
### 预定义宏
- `__SWITCH__`
- `__NX__`
- `MA_SWITCH`
- `PFD_SWITCH`
### 系统库
- `nx` (libnx)
- `m` (math)
## 示例程序构建脚本
### 通用结构
```lua
-- 使用与主项目相同的平台配置
if is_plat("switch") then
-- Switch 平台配置
elseif is_plat("mingw") then
-- MinGW 平台配置
end
```
### Switch 平台配置
- 设置平台: `set_plat("switch")`
- 设置架构: `set_arch("arm64")`
- 设置工具链: `set_toolchains("switch")`
- 设置输出目录: `set_targetdir("../../build/examples/xxx")`
- 构建后生成 NRO 文件
### MinGW 平台配置
- 设置平台: `set_plat("mingw")`
- 设置架构: `set_arch("x86_64")`
- 设置输出目录: `set_targetdir("../../build/examples/xxx")`
- 链接标志: `-mwindows`
- 构建后复制资源文件
## 构建命令
### 配置项目
```bash
# 默认配置 (MinGW)
xmake f -c
# 指定平台 (使用 -p 参数)
xmake f -c -p mingw
xmake f -c -p switch
# 指定 MinGW 路径(如果不在默认位置)
xmake f -c -p mingw --mingw=C:\mingw
# 禁用示例
xmake f --examples=n
# 启用调试日志
xmake f --debug_logs=y
```
### 安装依赖 (MinGW)
```bash
xmake require -y
```
### 构建项目
```bash
# 构建所有目标
xmake
# 构建特定目标
xmake -r extra2d
xmake -r push_box
# 并行构建
xmake -j4
```
### 运行程序
```bash
# 运行示例
xmake run push_box
xmake run hello_world
```
### 清理构建
```bash
xmake clean
xmake f -c # 重新配置
```
## 输出目录结构
```
build/
├── examples/
│ ├── hello_world/
│ │ ├── hello_world.exe # MinGW
│ │ ├── hello_world.nro # Switch
│ │ └── assets/ # 资源文件
│ ├── push_box/
│ ├── collision_demo/
│ └── spatial_index_demo/
└── ...
```
## 关键设计决策
### 1. 平台检测
- 使用 `is_plat()` 而不是手动检测,确保与主项目一致
- 示例脚本继承主项目的平台配置
### 2. 资源处理
- **Switch**: 使用 romfs 嵌入 NRO 文件
- **MinGW**: 构建后复制到输出目录
### 3. 依赖管理
- **MinGW**: 使用 Xmake 包管理器 (`add_requires`)
- **Switch**: 使用 devkitPro 提供的库
### 4. 工具链隔离
- Switch 工具链定义在单独文件中
- 通过 `set_toolchains("switch")` 切换
## 常见问题
### 1. 依赖包找不到
```bash
xmake repo -u
xmake require -y
```
### 2. Switch 工具链找不到
- 确保 DEVKITPRO 环境变量设置正确
- 默认路径: `C:/devkitPro`
### 3. 平台配置不匹配
- 使用 `xmake show` 查看当前配置
- 使用 `xmake f -c` 重新配置
### 4. MinGW 路径问题
如果 MinGW 安装在非默认位置,使用 `--mingw` 参数指定:
```bash
xmake f -c -p mingw --mingw=D:\Tools\mingw64
```
### 5. Switch 库找不到
确保在 MSYS2 中安装了必要的库:
```bash
pacman -S switch-sdl2 switch-sdl2_mixer switch-glm
```
## 扩展指南
### 添加新示例
1. 在 `examples/` 下创建新目录
2. 创建 `xmake.lua` 构建脚本
3. 在 `xmake.lua` 中添加 `includes("examples/new_example")`
### 添加新平台
1. 在 `xmake/toolchains/` 下创建工具链定义
2. 在 `xmake.lua` 中添加平台检测逻辑
3. 在 `xmake/engine.lua` 中添加平台配置

View File

@ -1,96 +0,0 @@
-- ==============================================
-- Collision Demo 示例 - Xmake 构建脚本
-- 支持平台: MinGW (Windows), Nintendo Switch
-- ==============================================
-- 获取当前脚本所在目录(示例根目录)
local example_dir = os.scriptdir()
-- 可执行文件目标
target("collision_demo")
set_kind("binary")
add_files("main.cpp")
add_includedirs("../../Extra2D/include")
add_deps("extra2d")
-- 使用与主项目相同的平台配置
if is_plat("switch") then
set_plat("switch")
set_arch("arm64")
set_toolchains("switch")
set_targetdir("../../build/examples/collision_demo")
-- 构建后生成 NRO 文件
after_build(function (target)
local devkitPro = os.getenv("DEVKITPRO") or "C:/devkitPro"
local elf_file = target:targetfile()
local output_dir = path.directory(elf_file)
local nacp_file = path.join(output_dir, "collision_demo.nacp")
local nro_file = path.join(output_dir, "collision_demo.nro")
local nacptool = path.join(devkitPro, "tools/bin/nacptool.exe")
local elf2nro = path.join(devkitPro, "tools/bin/elf2nro.exe")
if os.isfile(nacptool) and os.isfile(elf2nro) then
os.vrunv(nacptool, {"--create", "Collision Demo", "Extra2D Team", "1.0.0", nacp_file})
local romfs = path.join(example_dir, "romfs")
if os.isdir(romfs) then
os.vrunv(elf2nro, {elf_file, nro_file, "--nacp=" .. nacp_file, "--romfsdir=" .. romfs})
else
os.vrunv(elf2nro, {elf_file, nro_file, "--nacp=" .. nacp_file})
end
print("Generated NRO: " .. nro_file)
end
end)
-- 打包时将 NRO 文件复制到 package 目录
after_package(function (target)
local nro_file = path.join(target:targetdir(), "collision_demo.nro")
local package_dir = target:packagedir()
if os.isfile(nro_file) and package_dir then
os.cp(nro_file, package_dir)
print("Copied NRO to package: " .. package_dir)
end
end)
elseif is_plat("mingw") then
set_plat("mingw")
set_arch("x86_64")
set_targetdir("../../build/examples/collision_demo")
add_ldflags("-mwindows", {force = true})
-- 复制资源到输出目录
after_build(function (target)
local romfs = path.join(example_dir, "romfs")
if os.isdir(romfs) then
local target_dir = path.directory(target:targetfile())
local assets_dir = path.join(target_dir, "assets")
-- 创建 assets 目录
if not os.isdir(assets_dir) then
os.mkdir(assets_dir)
end
-- 复制所有资源文件(包括子目录)
os.cp(path.join(romfs, "assets/**"), assets_dir)
print("Copied assets from " .. romfs .. " to " .. assets_dir)
else
print("Warning: romfs directory not found at " .. romfs)
end
end)
-- 打包时将资源复制到 package 目录
after_package(function (target)
local target_dir = path.directory(target:targetfile())
local assets_dir = path.join(target_dir, "assets")
local package_dir = target:packagedir()
if os.isdir(assets_dir) and package_dir then
local package_assets = path.join(package_dir, "assets")
if not os.isdir(package_assets) then
os.mkdir(package_assets)
end
os.cp(path.join(assets_dir, "**"), package_assets)
print("Copied assets to package: " .. package_assets)
end
end)
end
target_end()

View File

@ -1,96 +0,0 @@
-- ==============================================
-- Hello World 示例 - Xmake 构建脚本
-- 支持平台: MinGW (Windows), Nintendo Switch
-- ==============================================
-- 获取当前脚本所在目录(示例根目录)
local example_dir = os.scriptdir()
-- 可执行文件目标
target("hello_world")
set_kind("binary")
add_files("main.cpp")
add_includedirs("../../Extra2D/include")
add_deps("extra2d")
-- 使用与主项目相同的平台配置
if is_plat("switch") then
set_plat("switch")
set_arch("arm64")
set_toolchains("switch")
set_targetdir("../../build/examples/hello_world")
-- 构建后生成 NRO 文件
after_build(function (target)
local devkitPro = os.getenv("DEVKITPRO") or "C:/devkitPro"
local elf_file = target:targetfile()
local output_dir = path.directory(elf_file)
local nacp_file = path.join(output_dir, "hello_world.nacp")
local nro_file = path.join(output_dir, "hello_world.nro")
local nacptool = path.join(devkitPro, "tools/bin/nacptool.exe")
local elf2nro = path.join(devkitPro, "tools/bin/elf2nro.exe")
if os.isfile(nacptool) and os.isfile(elf2nro) then
os.vrunv(nacptool, {"--create", "Hello World", "Extra2D Team", "1.0.0", nacp_file})
local romfs = path.join(example_dir, "romfs")
if os.isdir(romfs) then
os.vrunv(elf2nro, {elf_file, nro_file, "--nacp=" .. nacp_file, "--romfsdir=" .. romfs})
else
os.vrunv(elf2nro, {elf_file, nro_file, "--nacp=" .. nacp_file})
end
print("Generated NRO: " .. nro_file)
end
end)
-- 打包时将 NRO 文件复制到 package 目录
after_package(function (target)
local nro_file = path.join(target:targetdir(), "hello_world.nro")
local package_dir = target:packagedir()
if os.isfile(nro_file) and package_dir then
os.cp(nro_file, package_dir)
print("Copied NRO to package: " .. package_dir)
end
end)
elseif is_plat("mingw") then
set_plat("mingw")
set_arch("x86_64")
set_targetdir("../../build/examples/hello_world")
add_ldflags("-mwindows", {force = true})
-- 复制资源到输出目录
after_build(function (target)
local romfs = path.join(example_dir, "romfs")
if os.isdir(romfs) then
local target_dir = path.directory(target:targetfile())
local assets_dir = path.join(target_dir, "assets")
-- 创建 assets 目录
if not os.isdir(assets_dir) then
os.mkdir(assets_dir)
end
-- 复制所有资源文件(包括子目录)
os.cp(path.join(romfs, "assets/**"), assets_dir)
print("Copied assets from " .. romfs .. " to " .. assets_dir)
else
print("Warning: romfs directory not found at " .. romfs)
end
end)
-- 打包时将资源复制到 package 目录
after_package(function (target)
local target_dir = path.directory(target:targetfile())
local assets_dir = path.join(target_dir, "assets")
local package_dir = target:packagedir()
if os.isdir(assets_dir) and package_dir then
local package_assets = path.join(package_dir, "assets")
if not os.isdir(package_assets) then
os.mkdir(package_assets)
end
os.cp(path.join(assets_dir, "**"), package_assets)
print("Copied assets to package: " .. package_assets)
end
end)
end
target_end()

View File

@ -1,400 +0,0 @@
#include "PlayScene.h"
#include "audio_context.h"
#include "audio_controller.h"
#include "storage.h"
#include "StartScene.h"
#include "SuccessScene.h"
#include <extra2d/extra2d.h>
namespace pushbox {
static extra2d::Ptr<extra2d::FontAtlas> loadFont(int size) {
auto& resources = extra2d::Application::instance().resources();
auto font = resources.loadFont("assets/font.ttf", size);
return font;
}
PlayScene::PlayScene(int level) {
setBackgroundColor(extra2d::Colors::Black);
auto& app = extra2d::Application::instance();
auto& config = app.getConfig();
setViewportSize(static_cast<float>(config.width), static_cast<float>(config.height));
auto& resources = app.resources();
E2D_LOG_INFO("PlayScene: Loading textures...");
texWall_ = resources.loadTexture("assets/images/wall.gif");
texPoint_ = resources.loadTexture("assets/images/point.gif");
texFloor_ = resources.loadTexture("assets/images/floor.gif");
texBox_ = resources.loadTexture("assets/images/box.gif");
texBoxInPoint_ = resources.loadTexture("assets/images/boxinpoint.gif");
texMan_[1] = resources.loadTexture("assets/images/player/manup.gif");
texMan_[2] = resources.loadTexture("assets/images/player/mandown.gif");
texMan_[3] = resources.loadTexture("assets/images/player/manleft.gif");
texMan_[4] = resources.loadTexture("assets/images/player/manright.gif");
texManPush_[1] = resources.loadTexture("assets/images/player/manhandup.gif");
texManPush_[2] = resources.loadTexture("assets/images/player/manhanddown.gif");
texManPush_[3] = resources.loadTexture("assets/images/player/manhandleft.gif");
texManPush_[4] = resources.loadTexture("assets/images/player/manhandright.gif");
font28_ = loadFont(28);
font20_ = loadFont(20);
// 获取窗口尺寸
float screenW = static_cast<float>(app.getConfig().width);
float screenH = static_cast<float>(app.getConfig().height);
// 计算游戏区域居中偏移(假设游戏区域是 640x480
float gameWidth = 640.0f;
float gameHeight = 480.0f;
float offsetX = (screenW - gameWidth) / 2.0f;
float offsetY = (screenH - gameHeight) / 2.0f;
// 音效图标(左上角,与主界面一致)
auto soundOn = resources.loadTexture("assets/images/soundon.png");
auto soundOff = resources.loadTexture("assets/images/soundoff.png");
if (soundOn && soundOff) {
soundIcon_ = extra2d::Sprite::create(g_SoundOpen ? soundOn : soundOff);
soundIcon_->setPosition(offsetX + 50.0f, offsetY + 50.0f);
addChild(soundIcon_);
}
levelText_ = extra2d::Text::create("", font28_);
levelText_->setPosition(offsetX + 520.0f, offsetY + 30.0f);
levelText_->setTextColor(extra2d::Colors::White);
addChild(levelText_);
stepText_ = extra2d::Text::create("", font20_);
stepText_->setPosition(offsetX + 520.0f, offsetY + 100.0f);
stepText_->setTextColor(extra2d::Colors::White);
addChild(stepText_);
bestText_ = extra2d::Text::create("", font20_);
bestText_->setPosition(offsetX + 520.0f, offsetY + 140.0f);
bestText_->setTextColor(extra2d::Colors::White);
addChild(bestText_);
// 创建菜单文本(使用颜色变化指示选中)
restartText_ = extra2d::Text::create("Y键重开", font20_);
restartText_->setPosition(offsetX + 520.0f, offsetY + 290.0f);
addChild(restartText_);
soundToggleText_ = extra2d::Text::create("X键切换音效", font20_);
soundToggleText_->setPosition(offsetX + 520.0f, offsetY + 330.0f);
addChild(soundToggleText_);
mapLayer_ = extra2d::makePtr<extra2d::Node>();
mapLayer_->setAnchor(0.0f, 0.0f);
mapLayer_->setPosition(0.0f, 0.0f);
addChild(mapLayer_);
auto audioNode = AudioController::create();
audioNode->setName("AudioController");
addChild(audioNode);
setAudioController(audioNode);
setLevel(level);
}
void PlayScene::onEnter() {
Scene::onEnter();
updateSoundIcon();
updateMenuColors();
}
void PlayScene::updateMenuColors() {
// 选中的项用红色,未选中的用白色
if (restartText_) {
restartText_->setTextColor(menuIndex_ == 0 ? extra2d::Colors::Red : extra2d::Colors::White);
}
if (soundToggleText_) {
soundToggleText_->setTextColor(menuIndex_ == 1 ? extra2d::Colors::Red : extra2d::Colors::White);
}
}
void PlayScene::onUpdate(float dt) {
Scene::onUpdate(dt);
auto& app = extra2d::Application::instance();
auto& input = app.input();
// B 键返回主菜单
if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_B)) {
app.scenes().replaceScene(
extra2d::makePtr<StartScene>(), extra2d::TransitionType::Fade, 0.2f);
return;
}
// Y 键重开
if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_Y)) {
setLevel(g_CurrentLevel);
return;
}
// X 键直接切换音效
if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_X)) {
g_SoundOpen = !g_SoundOpen;
if (auto audio = getAudioController()) {
audio->setEnabled(g_SoundOpen);
}
updateSoundIcon();
return;
}
// A 键执行选中的菜单项
if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_A)) {
executeMenuItem();
return;
}
// 方向键移动
if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_DPAD_UP)) {
move(0, -1, 1);
flush();
} else if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_DPAD_DOWN)) {
move(0, 1, 2);
flush();
} else if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_DPAD_LEFT)) {
move(-1, 0, 3);
flush();
} else if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_DPAD_RIGHT)) {
move(1, 0, 4);
flush();
} else {
return;
}
// 检查是否通关
for (int i = 0; i < map_.width; i++) {
for (int j = 0; j < map_.height; j++) {
Piece p = map_.value[j][i];
if (p.type == TYPE::Box && p.isPoint == false) {
return;
}
}
}
gameOver();
}
void PlayScene::executeMenuItem() {
switch (menuIndex_) {
case 0: // 重开
setLevel(g_CurrentLevel);
break;
case 1: // 切换音效
g_SoundOpen = !g_SoundOpen;
if (auto audio = getAudioController()) {
audio->setEnabled(g_SoundOpen);
}
updateSoundIcon();
break;
}
}
void PlayScene::updateSoundIcon() {
if (!soundIcon_) return;
auto& app = extra2d::Application::instance();
auto& resources = app.resources();
auto soundOn = resources.loadTexture("assets/images/soundon.png");
auto soundOff = resources.loadTexture("assets/images/soundoff.png");
if (soundOn && soundOff) {
soundIcon_->setTexture(g_SoundOpen ? soundOn : soundOff);
}
}
void PlayScene::flush() {
mapLayer_->removeAllChildren();
int tileW = texFloor_ ? texFloor_->getWidth() : 32;
int tileH = texFloor_ ? texFloor_->getHeight() : 32;
// 获取窗口尺寸,计算游戏区域居中偏移
auto& app = extra2d::Application::instance();
float screenW = static_cast<float>(app.getConfig().width);
float screenH = static_cast<float>(app.getConfig().height);
float gameWidth = 640.0f;
float gameHeight = 480.0f;
float baseOffsetX = (screenW - gameWidth) / 2.0f;
float baseOffsetY = (screenH - gameHeight) / 2.0f;
// 在 12x12 网格中居中地图
float mapOffsetX = static_cast<float>((12 - map_.width) / 2) * tileW;
float mapOffsetY = static_cast<float>((12 - map_.height) / 2) * tileH;
float offsetX = baseOffsetX + mapOffsetX;
float offsetY = baseOffsetY + mapOffsetY;
for (int i = 0; i < map_.width; i++) {
for (int j = 0; j < map_.height; j++) {
Piece piece = map_.value[j][i];
extra2d::Ptr<extra2d::Texture> tex;
if (piece.type == TYPE::Wall) {
tex = texWall_;
} else if (piece.type == TYPE::Ground && piece.isPoint) {
tex = texPoint_;
} else if (piece.type == TYPE::Ground) {
tex = texFloor_;
} else if (piece.type == TYPE::Box && piece.isPoint) {
tex = texBoxInPoint_;
} else if (piece.type == TYPE::Box) {
tex = texBox_;
} else if (piece.type == TYPE::Man && g_Pushing) {
tex = texManPush_[g_Direct];
} else if (piece.type == TYPE::Man) {
tex = texMan_[g_Direct];
} else {
continue;
}
if (!tex) {
continue;
}
auto sprite = extra2d::Sprite::create(tex);
sprite->setAnchor(0.0f, 0.0f);
sprite->setPosition(offsetX + static_cast<float>(i * tileW),
offsetY + static_cast<float>(j * tileH));
mapLayer_->addChild(sprite);
}
}
}
void PlayScene::setLevel(int level) {
g_CurrentLevel = level;
saveCurrentLevel(g_CurrentLevel);
if (levelText_) {
levelText_->setText("" + std::to_string(level) + "");
}
setStep(0);
int bestStep = loadBestStep(level, 0);
if (bestText_) {
if (bestStep != 0) {
bestText_->setText("最佳" + std::to_string(bestStep) + "");
} else {
bestText_->setText("");
}
}
// 深拷贝地图数据
Map& sourceMap = g_Maps[level - 1];
map_.width = sourceMap.width;
map_.height = sourceMap.height;
map_.roleX = sourceMap.roleX;
map_.roleY = sourceMap.roleY;
for (int i = 0; i < 12; i++) {
for (int j = 0; j < 12; j++) {
map_.value[i][j] = sourceMap.value[i][j];
}
}
g_Direct = 2;
g_Pushing = false;
flush();
}
void PlayScene::setStep(int step) {
step_ = step;
if (stepText_) {
stepText_->setText("当前" + std::to_string(step) + "");
}
}
void PlayScene::move(int dx, int dy, int direct) {
int targetX = dx + map_.roleX;
int targetY = dy + map_.roleY;
g_Direct = direct;
if (targetX < 0 || targetX >= map_.width || targetY < 0 || targetY >= map_.height) {
return;
}
if (map_.value[targetY][targetX].type == TYPE::Wall) {
return;
}
if (map_.value[targetY][targetX].type == TYPE::Ground) {
g_Pushing = false;
map_.value[map_.roleY][map_.roleX].type = TYPE::Ground;
map_.value[targetY][targetX].type = TYPE::Man;
if (auto audio = getAudioController()) {
audio->playManMove();
}
} else if (map_.value[targetY][targetX].type == TYPE::Box) {
g_Pushing = true;
int boxX = 0;
int boxY = 0;
switch (g_Direct) {
case 1:
boxX = targetX;
boxY = targetY - 1;
break;
case 2:
boxX = targetX;
boxY = targetY + 1;
break;
case 3:
boxX = targetX - 1;
boxY = targetY;
break;
case 4:
boxX = targetX + 1;
boxY = targetY;
break;
default:
return;
}
if (boxX < 0 || boxX >= map_.width || boxY < 0 || boxY >= map_.height) {
return;
}
if (map_.value[boxY][boxX].type == TYPE::Wall || map_.value[boxY][boxX].type == TYPE::Box) {
return;
}
map_.value[boxY][boxX].type = TYPE::Box;
map_.value[targetY][targetX].type = TYPE::Man;
map_.value[map_.roleY][map_.roleX].type = TYPE::Ground;
if (auto audio = getAudioController()) {
audio->playBoxMove();
}
} else {
return;
}
map_.roleX = targetX;
map_.roleY = targetY;
setStep(step_ + 1);
}
void PlayScene::gameOver() {
int bestStep = loadBestStep(g_CurrentLevel, 0);
if (bestStep == 0 || step_ < bestStep) {
saveBestStep(g_CurrentLevel, step_);
}
if (g_CurrentLevel == MAX_LEVEL) {
extra2d::Application::instance().scenes().pushScene(extra2d::makePtr<SuccessScene>(),
extra2d::TransitionType::Fade, 0.25f);
return;
}
setLevel(g_CurrentLevel + 1);
}
} // namespace pushbox

View File

@ -1,51 +0,0 @@
#pragma once
#include "data.h"
#include <extra2d/extra2d.h>
namespace pushbox {
class PlayScene : public extra2d::Scene {
public:
explicit PlayScene(int level);
void onEnter() override;
void onUpdate(float dt) override;
private:
void updateMenuColors();
void executeMenuItem();
void updateSoundIcon();
void flush();
void setLevel(int level);
void setStep(int step);
void move(int dx, int dy, int direct);
void gameOver();
int step_ = 0;
int menuIndex_ = 0;
Map map_{};
extra2d::Ptr<extra2d::FontAtlas> font28_;
extra2d::Ptr<extra2d::FontAtlas> font20_;
extra2d::Ptr<extra2d::Text> levelText_;
extra2d::Ptr<extra2d::Text> stepText_;
extra2d::Ptr<extra2d::Text> bestText_;
extra2d::Ptr<extra2d::Text> restartText_;
extra2d::Ptr<extra2d::Text> soundToggleText_;
extra2d::Ptr<extra2d::Node> mapLayer_;
extra2d::Ptr<extra2d::Sprite> soundIcon_;
extra2d::Ptr<extra2d::Texture> texWall_;
extra2d::Ptr<extra2d::Texture> texPoint_;
extra2d::Ptr<extra2d::Texture> texFloor_;
extra2d::Ptr<extra2d::Texture> texBox_;
extra2d::Ptr<extra2d::Texture> texBoxInPoint_;
extra2d::Ptr<extra2d::Texture> texMan_[5];
extra2d::Ptr<extra2d::Texture> texManPush_[5];
};
} // namespace pushbox

View File

@ -1,217 +0,0 @@
#include "StartScene.h"
#include "audio_context.h"
#include "audio_controller.h"
#include "data.h"
#include "PlayScene.h"
#include <extra2d/extra2d.h>
namespace pushbox {
StartScene::StartScene() {
auto& app = extra2d::Application::instance();
auto& config = app.getConfig();
setViewportSize(static_cast<float>(config.width), static_cast<float>(config.height));
}
static extra2d::Ptr<extra2d::FontAtlas> loadMenuFont() {
auto& resources = extra2d::Application::instance().resources();
auto font = resources.loadFont("assets/font.ttf", 28,true);
return font;
}
void StartScene::onEnter() {
Scene::onEnter();
auto& app = extra2d::Application::instance();
auto& resources = app.resources();
setBackgroundColor(extra2d::Colors::Black);
if (getChildren().empty()) {
auto audioNode = AudioController::create();
audioNode->setName("audio_controller");
addChild(audioNode);
setAudioController(audioNode);
float screenW = static_cast<float>(app.getConfig().width);
float screenH = static_cast<float>(app.getConfig().height);
auto bgTex = resources.loadTexture("assets/images/start.jpg");
if (bgTex) {
auto background = extra2d::Sprite::create(bgTex);
float bgWidth = static_cast<float>(bgTex->getWidth());
float bgHeight = static_cast<float>(bgTex->getHeight());
float offsetX = (screenW - bgWidth) / 2.0f;
float offsetY = (screenH - bgHeight) / 2.0f;
background->setAnchor(0.0f, 0.0f);
background->setPosition(offsetX, offsetY);
addChild(background);
float centerX = screenW / 2.0f;
font_ = loadMenuFont();
if (!font_) {
return;
}
// 创建菜单按钮(使用 Button 实现文本居中)
// 设置按钮锚点为中心点,位置设为屏幕中心,实现真正的居中
startBtn_ = extra2d::Button::create();
startBtn_->setFont(font_);
startBtn_->setText("新游戏");
startBtn_->setTextColor(extra2d::Colors::Black);
startBtn_->setBackgroundColor(extra2d::Colors::Transparent,
extra2d::Colors::Transparent,
extra2d::Colors::Transparent);
startBtn_->setBorder(extra2d::Colors::Transparent, 0.0f);
startBtn_->setPadding(extra2d::Vec2(0.0f, 0.0f));
startBtn_->setCustomSize(200.0f, 40.0f);
startBtn_->setAnchor(0.5f, 0.5f);
startBtn_->setPosition(centerX, offsetY + 260.0f);
addChild(startBtn_);
resumeBtn_ = extra2d::Button::create();
resumeBtn_->setFont(font_);
resumeBtn_->setText("继续关卡");
resumeBtn_->setTextColor(extra2d::Colors::Black);
resumeBtn_->setBackgroundColor(extra2d::Colors::Transparent,
extra2d::Colors::Transparent,
extra2d::Colors::Transparent);
resumeBtn_->setBorder(extra2d::Colors::Transparent, 0.0f);
resumeBtn_->setPadding(extra2d::Vec2(0.0f, 0.0f));
resumeBtn_->setCustomSize(200.0f, 40.0f);
resumeBtn_->setAnchor(0.5f, 0.5f);
resumeBtn_->setPosition(centerX, offsetY + 300.0f);
addChild(resumeBtn_);
exitBtn_ = extra2d::Button::create();
exitBtn_->setFont(font_);
exitBtn_->setText("退出");
exitBtn_->setTextColor(extra2d::Colors::Black);
exitBtn_->setBackgroundColor(extra2d::Colors::Transparent,
extra2d::Colors::Transparent,
extra2d::Colors::Transparent);
exitBtn_->setBorder(extra2d::Colors::Transparent, 0.0f);
exitBtn_->setPadding(extra2d::Vec2(0.0f, 0.0f));
exitBtn_->setCustomSize(200.0f, 40.0f);
exitBtn_->setAnchor(0.5f, 0.5f);
exitBtn_->setPosition(centerX, offsetY + 340.0f);
addChild(exitBtn_);
// 音效开关图标(相对于背景图左上角)
auto soundOn = resources.loadTexture("assets/images/soundon.png");
auto soundOff = resources.loadTexture("assets/images/soundoff.png");
if (soundOn && soundOff) {
soundIcon_ = extra2d::Sprite::create(g_SoundOpen ? soundOn : soundOff);
soundIcon_->setPosition(offsetX + 50.0f, offsetY + 50.0f);
addChild(soundIcon_);
}
}
}
// 始终有3个菜单项
menuCount_ = 3;
updateMenuColors();
}
void StartScene::onUpdate(float dt) {
Scene::onUpdate(dt);
auto& app = extra2d::Application::instance();
auto& input = app.input();
// 方向键上下切换选择
if (input.isButtonPressed(extra2d::GamepadButton::DPadUp)) {
selectedIndex_ = (selectedIndex_ - 1 + menuCount_) % menuCount_;
updateMenuColors();
} else if (input.isButtonPressed(extra2d::GamepadButton::DPadDown)) {
selectedIndex_ = (selectedIndex_ + 1) % menuCount_;
updateMenuColors();
}
// A键确认
if (input.isButtonPressed(extra2d::GamepadButton::A)) {
executeMenuItem();
}
// X键切换音效
if (input.isButtonPressed(extra2d::GamepadButton::X)) {
g_SoundOpen = !g_SoundOpen;
if (auto audio = getAudioController()) {
audio->setEnabled(g_SoundOpen);
}
updateSoundIcon();
}
}
void StartScene::updateMenuColors() {
// 根据选中状态更新按钮文本颜色
// 选中的项用红色,未选中的用黑色,禁用的项用深灰色
if (startBtn_) {
startBtn_->setTextColor(selectedIndex_ == 0 ? extra2d::Colors::Red : extra2d::Colors::Black);
}
if (resumeBtn_) {
// "继续关卡"始终显示,但当 g_CurrentLevel == 1 时禁用(深灰色)
if (g_CurrentLevel > 1) {
// 可用状态:选中为红色,未选中为黑色
resumeBtn_->setTextColor(selectedIndex_ == 1 ? extra2d::Colors::Red : extra2d::Colors::Black);
} else {
// 禁用状态:深灰色 (RGB: 80, 80, 80)
resumeBtn_->setTextColor(extra2d::Color(80, 80, 80, 255));
}
}
if (exitBtn_) {
exitBtn_->setTextColor(selectedIndex_ == 2 ? extra2d::Colors::Red : extra2d::Colors::Black);
}
}
void StartScene::updateSoundIcon() {
if (!soundIcon_) return;
auto& app = extra2d::Application::instance();
auto& resources = app.resources();
auto soundOn = resources.loadTexture("assets/images/soundon.png");
auto soundOff = resources.loadTexture("assets/images/soundoff.png");
if (soundOn && soundOff) {
soundIcon_->setTexture(g_SoundOpen ? soundOn : soundOff);
}
}
void StartScene::executeMenuItem() {
// 始终有3个选项但"继续关卡"(索引1)在 g_CurrentLevel == 1 时禁用
switch (selectedIndex_) {
case 0:
startNewGame();
break;
case 1:
// 只有当 g_CurrentLevel > 1 时才能选择"继续关卡"
if (g_CurrentLevel > 1) {
continueGame();
}
break;
case 2:
exitGame();
break;
}
}
void StartScene::startNewGame() {
extra2d::Application::instance().scenes().replaceScene(
extra2d::makePtr<PlayScene>(1), extra2d::TransitionType::Fade, 0.25f);
}
void StartScene::continueGame() {
extra2d::Application::instance().scenes().replaceScene(
extra2d::makePtr<PlayScene>(g_CurrentLevel), extra2d::TransitionType::Fade, 0.25f);
}
void StartScene::exitGame() {
extra2d::Application::instance().quit();
}
} // namespace pushbox

View File

@ -1,30 +0,0 @@
#pragma once
#include <extra2d/extra2d.h>
namespace pushbox {
class StartScene : public extra2d::Scene {
public:
StartScene();
void onEnter() override;
void onUpdate(float dt) override;
private:
void updateMenuColors();
void updateSoundIcon();
void executeMenuItem();
void startNewGame();
void continueGame();
void exitGame();
extra2d::Ptr<extra2d::FontAtlas> font_;
extra2d::Ptr<extra2d::Button> startBtn_;
extra2d::Ptr<extra2d::Button> resumeBtn_;
extra2d::Ptr<extra2d::Button> exitBtn_;
extra2d::Ptr<extra2d::Sprite> soundIcon_;
int selectedIndex_ = 0;
int menuCount_ = 3;
};
} // namespace pushbox

View File

@ -1,77 +0,0 @@
#include "SuccessScene.h"
#include <extra2d/extra2d.h>
namespace pushbox {
SuccessScene::SuccessScene() {
auto& app = extra2d::Application::instance();
auto& config = app.getConfig();
setViewportSize(static_cast<float>(config.width), static_cast<float>(config.height));
}
static extra2d::Ptr<extra2d::FontAtlas> loadMenuFont() {
auto& resources = extra2d::Application::instance().resources();
auto font = resources.loadFont("assets/font.ttf", 28);
return font;
}
void SuccessScene::onEnter() {
Scene::onEnter();
auto& app = extra2d::Application::instance();
auto& resources = app.resources();
setBackgroundColor(extra2d::Colors::Black);
if (getChildren().empty()) {
// 获取窗口尺寸
float screenW = static_cast<float>(app.getConfig().width);
float screenH = static_cast<float>(app.getConfig().height);
auto bgTex = resources.loadTexture("assets/images/success.jpg");
if (bgTex) {
auto background = extra2d::Sprite::create(bgTex);
float bgWidth = static_cast<float>(bgTex->getWidth());
float bgHeight = static_cast<float>(bgTex->getHeight());
float offsetX = (screenW - bgWidth) / 2.0f;
float offsetY = (screenH - bgHeight) / 2.0f;
background->setAnchor(0.0f, 0.0f);
background->setPosition(offsetX, offsetY);
addChild(background);
float centerX = screenW / 2.0f;
auto font = loadMenuFont();
if (font) {
// 创建按钮文本(仅显示,不响应鼠标)
auto backText = extra2d::Text::create("回主菜单", font);
backText->setPosition(centerX, offsetY + 350.0f);
backText->setTextColor(extra2d::Colors::Black);
addChild(backText);
// 创建选择指示器(箭头)
selectorText_ = extra2d::Text::create(">", font);
selectorText_->setTextColor(extra2d::Colors::Red);
selectorText_->setPosition(centerX - 80.0f, offsetY + 350.0f);
addChild(selectorText_);
}
}
}
}
void SuccessScene::onUpdate(float dt) {
Scene::onUpdate(dt);
auto& app = extra2d::Application::instance();
auto& input = app.input();
// A键确认返回主菜单
if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_A)) {
auto& scenes = extra2d::Application::instance().scenes();
scenes.popScene(extra2d::TransitionType::Fade, 0.2f);
scenes.popScene(extra2d::TransitionType::Fade, 0.2f);
}
}
} // namespace pushbox

View File

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

View File

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

View File

@ -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,62 +0,0 @@
#include "audio_controller.h"
#include "storage.h"
namespace pushbox {
extra2d::Ptr<AudioController> AudioController::create() {
return extra2d::makePtr<AudioController>();
}
void AudioController::onEnter() {
Node::onEnter();
if (!loaded_) {
auto& resources = extra2d::Application::instance().resources();
background_ = resources.loadSound("pushbox_bg", "assets/audio/background.wav");
manMove_ = resources.loadSound("pushbox_manmove", "assets/audio/manmove.wav");
boxMove_ = resources.loadSound("pushbox_boxmove", "assets/audio/boxmove.wav");
if (background_) {
background_->setLooping(true);
background_->play();
}
loaded_ = true;
}
setEnabled(g_SoundOpen);
}
void AudioController::setEnabled(bool enabled) {
enabled_ = enabled;
g_SoundOpen = enabled;
saveSoundOpen(enabled);
if (!background_) {
return;
}
if (enabled_) {
background_->resume();
} else {
background_->pause();
}
}
void AudioController::playManMove() {
if (!enabled_ || !manMove_) {
return;
}
manMove_->play();
}
void AudioController::playBoxMove() {
if (!enabled_ || !boxMove_) {
return;
}
boxMove_->play();
}
} // namespace pushbox

View File

@ -1,29 +0,0 @@
#pragma once
#include "data.h"
#include <extra2d/extra2d.h>
namespace pushbox {
class AudioController : public extra2d::Node {
public:
static extra2d::Ptr<AudioController> create();
void onEnter() override;
void setEnabled(bool enabled);
bool isEnabled() const { return enabled_; }
void playManMove();
void playBoxMove();
private:
bool loaded_ = false;
bool enabled_ = true;
extra2d::Ptr<extra2d::Sound> background_;
extra2d::Ptr<extra2d::Sound> manMove_;
extra2d::Ptr<extra2d::Sound> boxMove_;
};
} // namespace pushbox

View File

@ -1,117 +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,28 +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,51 +0,0 @@
#include <extra2d/extra2d.h>
#include "StartScene.h"
#include "data.h"
#include "storage.h"
using namespace extra2d;
// ============================================================================
// 程序入口
// ============================================================================
int main(int argc, char **argv)
{
Logger::init();
Logger::setLevel(LogLevel::Debug);
E2D_LOG_INFO("========================");
E2D_LOG_INFO("Extra2D push_box");
E2D_LOG_INFO("========================");
auto &app = Application::instance();
AppConfig config;
config.title = "Extra2D - push_box";
config.width = 1280;
config.height = 720;
config.vsync = true;
config.fpsLimit = 60;
if (!app.init(config)) {
E2D_LOG_ERROR("应用初始化失败!");
return -1;
}
// 初始化存储系统
pushbox::initStorage("sdmc:/");
pushbox::g_CurrentLevel = pushbox::loadCurrentLevel(1);
if (pushbox::g_CurrentLevel > MAX_LEVEL) {
pushbox::g_CurrentLevel = 1;
}
pushbox::g_SoundOpen = pushbox::loadSoundOpen(true);
// 进入开始场景(主界面)
app.enterScene(makePtr<pushbox::StartScene>());
E2D_LOG_INFO("开始主循环...");
app.run();
E2D_LOG_INFO("应用结束");
return 0;
}

View File

@ -1,40 +0,0 @@
#include "menu_button.h"
#include <extra2d/extra2d.h>
namespace pushbox {
extra2d::Ptr<MenuButton> MenuButton::create(extra2d::Ptr<extra2d::FontAtlas> font,
const extra2d::String& text,
extra2d::Function<void()> onClick) {
auto btn = extra2d::makePtr<MenuButton>();
btn->setFont(font);
btn->setText(text);
btn->setPadding(extra2d::Vec2(0.0f, 0.0f));
btn->setBackgroundColor(extra2d::Colors::Transparent, extra2d::Colors::Transparent,
extra2d::Colors::Transparent);
btn->setBorder(extra2d::Colors::Transparent, 0.0f);
btn->setTextColor(extra2d::Colors::Black);
btn->onClick_ = std::move(onClick);
btn->setOnClick([wbtn = extra2d::WeakPtr<MenuButton>(btn)]() {
if (auto self = wbtn.lock()) {
if (self->enabled_ && self->onClick_) {
self->onClick_();
}
}
});
// 使用事件监听来处理悬停效果
// Note: Extra2D 的 Button 类可能有不同的悬停检测机制
// 这里简化处理,仅保留基本功能
return btn;
}
void MenuButton::setEnabled(bool enabled) {
enabled_ = enabled;
setTextColor(enabled ? extra2d::Colors::Black : extra2d::Colors::LightGray);
}
} // namespace pushbox

View File

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

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

View File

@ -1,20 +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,96 +0,0 @@
-- ==============================================
-- Push Box 示例 - Xmake 构建脚本
-- 支持平台: MinGW (Windows), Nintendo Switch
-- ==============================================
-- 获取当前脚本所在目录(示例根目录)
local example_dir = os.scriptdir()
-- 可执行文件目标
target("push_box")
set_kind("binary")
add_files("*.cpp")
add_includedirs("../../Extra2D/include")
add_deps("extra2d")
-- 使用与主项目相同的平台配置
if is_plat("switch") then
set_plat("switch")
set_arch("arm64")
set_toolchains("switch")
set_targetdir("../../build/examples/push_box")
-- 构建后生成 NRO 文件
after_build(function (target)
local devkitPro = os.getenv("DEVKITPRO") or "C:/devkitPro"
local elf_file = target:targetfile()
local output_dir = path.directory(elf_file)
local nacp_file = path.join(output_dir, "push_box.nacp")
local nro_file = path.join(output_dir, "push_box.nro")
local nacptool = path.join(devkitPro, "tools/bin/nacptool.exe")
local elf2nro = path.join(devkitPro, "tools/bin/elf2nro.exe")
if os.isfile(nacptool) and os.isfile(elf2nro) then
os.vrunv(nacptool, {"--create", "Push Box", "Extra2D Team", "1.0.0", nacp_file})
local romfs = path.join(example_dir, "romfs")
if os.isdir(romfs) then
os.vrunv(elf2nro, {elf_file, nro_file, "--nacp=" .. nacp_file, "--romfsdir=" .. romfs})
else
os.vrunv(elf2nro, {elf_file, nro_file, "--nacp=" .. nacp_file})
end
print("Generated NRO: " .. nro_file)
end
end)
-- 打包时将 NRO 文件复制到 package 目录
after_package(function (target)
local nro_file = path.join(target:targetdir(), "push_box.nro")
local package_dir = target:packagedir()
if os.isfile(nro_file) and package_dir then
os.cp(nro_file, package_dir)
print("Copied NRO to package: " .. package_dir)
end
end)
elseif is_plat("mingw") then
set_plat("mingw")
set_arch("x86_64")
set_targetdir("../../build/examples/push_box")
add_ldflags("-mwindows", {force = true})
-- 复制资源到输出目录
after_build(function (target)
local romfs = path.join(example_dir, "romfs")
if os.isdir(romfs) then
local target_dir = path.directory(target:targetfile())
local assets_dir = path.join(target_dir, "assets")
-- 创建 assets 目录
if not os.isdir(assets_dir) then
os.mkdir(assets_dir)
end
-- 复制所有资源文件(包括子目录)
os.cp(path.join(romfs, "assets/**"), assets_dir)
print("Copied assets from " .. romfs .. " to " .. assets_dir)
else
print("Warning: romfs directory not found at " .. romfs)
end
end)
-- 打包时将资源复制到 package 目录
after_package(function (target)
local target_dir = path.directory(target:targetfile())
local assets_dir = path.join(target_dir, "assets")
local package_dir = target:packagedir()
if os.isdir(assets_dir) and package_dir then
local package_assets = path.join(package_dir, "assets")
if not os.isdir(package_assets) then
os.mkdir(package_assets)
end
os.cp(path.join(assets_dir, "**"), package_assets)
print("Copied assets to package: " .. package_assets)
end
end)
end
target_end()

View File

@ -1,96 +0,0 @@
-- ==============================================
-- Spatial Index Demo 示例 - Xmake 构建脚本
-- 支持平台: MinGW (Windows), Nintendo Switch
-- ==============================================
-- 获取当前脚本所在目录(示例根目录)
local example_dir = os.scriptdir()
-- 可执行文件目标
target("spatial_index_demo")
set_kind("binary")
add_files("main.cpp")
add_includedirs("../../Extra2D/include")
add_deps("extra2d")
-- 使用与主项目相同的平台配置
if is_plat("switch") then
set_plat("switch")
set_arch("arm64")
set_toolchains("switch")
set_targetdir("../../build/examples/spatial_index_demo")
-- 构建后生成 NRO 文件
after_build(function (target)
local devkitPro = os.getenv("DEVKITPRO") or "C:/devkitPro"
local elf_file = target:targetfile()
local output_dir = path.directory(elf_file)
local nacp_file = path.join(output_dir, "spatial_index_demo.nacp")
local nro_file = path.join(output_dir, "spatial_index_demo.nro")
local nacptool = path.join(devkitPro, "tools/bin/nacptool.exe")
local elf2nro = path.join(devkitPro, "tools/bin/elf2nro.exe")
if os.isfile(nacptool) and os.isfile(elf2nro) then
os.vrunv(nacptool, {"--create", "Spatial Index Demo", "Extra2D Team", "1.0.0", nacp_file})
local romfs = path.join(example_dir, "romfs")
if os.isdir(romfs) then
os.vrunv(elf2nro, {elf_file, nro_file, "--nacp=" .. nacp_file, "--romfsdir=" .. romfs})
else
os.vrunv(elf2nro, {elf_file, nro_file, "--nacp=" .. nacp_file})
end
print("Generated NRO: " .. nro_file)
end
end)
-- 打包时将 NRO 文件复制到 package 目录
after_package(function (target)
local nro_file = path.join(target:targetdir(), "spatial_index_demo.nro")
local package_dir = target:packagedir()
if os.isfile(nro_file) and package_dir then
os.cp(nro_file, package_dir)
print("Copied NRO to package: " .. package_dir)
end
end)
elseif is_plat("mingw") then
set_plat("mingw")
set_arch("x86_64")
set_targetdir("../../build/examples/spatial_index_demo")
add_ldflags("-mwindows", {force = true})
-- 复制资源到输出目录
after_build(function (target)
local romfs = path.join(example_dir, "romfs")
if os.isdir(romfs) then
local target_dir = path.directory(target:targetfile())
local assets_dir = path.join(target_dir, "assets")
-- 创建 assets 目录
if not os.isdir(assets_dir) then
os.mkdir(assets_dir)
end
-- 复制所有资源文件(包括子目录)
os.cp(path.join(romfs, "assets/**"), assets_dir)
print("Copied assets from " .. romfs .. " to " .. assets_dir)
else
print("Warning: romfs directory not found at " .. romfs)
end
end)
-- 打包时将资源复制到 package 目录
after_package(function (target)
local target_dir = path.directory(target:targetfile())
local assets_dir = path.join(target_dir, "assets")
local package_dir = target:packagedir()
if os.isdir(assets_dir) and package_dir then
local package_assets = path.join(package_dir, "assets")
if not os.isdir(package_assets) then
os.mkdir(package_assets)
end
os.cp(path.join(assets_dir, "**"), package_assets)
print("Copied assets to package: " .. package_assets)
end
end)
end
target_end()

5
switch_fix.specs Normal file
View File

@ -0,0 +1,5 @@
*link:
+ -T C:/devkitPro/libnx/switch.ld -pie --no-dynamic-linker --spare-dynamic-tags=0 --gc-sections -z text -z now -z nodynamic-undefined-weak -z pack-relative-relocs --build-id=sha1 --nx-module-name
*startfile:
crti%O%s crtbegin%O%s --require-defined=main

106
xmake.lua
View File

@ -1,105 +1,37 @@
-- ==============================================
-- Extra2D - 2D Game Engine
-- Build System: Xmake
-- Platforms: MinGW (Windows), Nintendo Switch
-- Extra2D for Nintendo Switch - Xmake Build Script
-- Purpose: Build Extra2D static library and Switch demo programs
-- Platform: Nintendo Switch (ARM64)
-- Graphics: Desktop OpenGL 3.3+ via Mesa EGL
-- Audio: SDL2_mixer
-- ==============================================
-- 项目元信息
set_project("Extra2D")
set_version("3.1.0")
set_license("MIT")
-- 语言和编码设置
set_languages("c++17")
set_encodings("utf-8")
-- 构建模式
add_rules("mode.debug", "mode.release")
-- ==============================================
-- 构建选项
-- 包含子模块配置
-- ==============================================
option("examples")
set_default(true)
set_showmenu(true)
set_description("Build example programs")
option_end()
option("debug_logs")
set_default(false)
set_showmenu(true)
set_description("Enable debug logging")
option_end()
-- ==============================================
-- 平台检测与配置
-- ==============================================
local host_plat = os.host()
local target_plat = get_config("plat") or host_plat
local supported_plats = {mingw = true, switch = true}
if not supported_plats[target_plat] then
if host_plat == "windows" then
target_plat = "mingw"
else
error("Unsupported platform: " .. target_plat .. ". Supported platforms: mingw, switch")
end
end
set_plat(target_plat)
if target_plat == "switch" then
set_arch("arm64")
elseif target_plat == "mingw" then
set_arch("x86_64")
end
-- ==============================================
-- 加载工具链配置
-- ==============================================
if target_plat == "switch" then
-- 包含工具链定义
includes("xmake/toolchains/switch.lua")
set_toolchains("switch")
end
-- 定义 Switch 工具链
define_switch_toolchain()
-- 包含目标定义
includes("xmake/targets/extra2d.lua")
includes("xmake/targets/examples.lua")
-- ==============================================
-- 添加依赖包 (MinGW)
-- 定义构建目标
-- ==============================================
if target_plat == "mingw" then
add_requires("glm", "libsdl2", "libsdl2_mixer")
end
-- Extra2D 引擎库
define_extra2d_target()
-- ==============================================
-- 加载构建目标
-- ==============================================
-- 加载引擎库定义
includes("xmake/engine.lua")
-- 定义引擎库
define_extra2d_engine()
-- 示例程序目标(作为子项目)
if has_config("examples") then
includes("examples/hello_world", {rootdir = "examples/hello_world"})
includes("examples/spatial_index_demo", {rootdir = "examples/spatial_index_demo"})
includes("examples/collision_demo", {rootdir = "examples/collision_demo"})
includes("examples/push_box", {rootdir = "examples/push_box"})
end
-- ==============================================
-- 项目信息输出
-- ==============================================
print("========================================")
print("Extra2D Build Configuration")
print("========================================")
print("Platform: " .. target_plat)
print("Architecture: " .. (get_config("arch") or "auto"))
print("Mode: " .. (is_mode("debug") and "debug" or "release"))
print("Examples: " .. (has_config("examples") and "enabled" or "disabled"))
print("========================================")
-- 示例程序
define_example_targets()

37
xmake.lua.backup Normal file
View File

@ -0,0 +1,37 @@
-- ==============================================
-- Extra2D for Nintendo Switch - Xmake Build Script
-- Purpose: Build Extra2D static library and Switch demo programs
-- Platform: Nintendo Switch (ARM64)
-- Graphics: Desktop OpenGL 3.3+ via Mesa EGL
-- Audio: SDL2_mixer
-- ==============================================
set_project("Extra2D")
set_version("3.1.0")
set_languages("c++17")
set_encodings("utf-8")
add_rules("mode.debug", "mode.release")
-- ==============================================
-- 包含子模块配置
-- ==============================================
-- 包含工具链定义
includes("xmake/toolchains/switch.lua")
-- 定义 Switch 工具链
define_switch_toolchain()
-- 包含目标定义
includes("xmake/targets/extra2d.lua")
includes("xmake/targets/examples.lua")
-- ==============================================
-- 定义构建目标
-- ==============================================
-- Extra2D 引擎库
define_extra2d_target()
-- 示例程序
define_example_targets()

View File

@ -1,54 +0,0 @@
-- ==============================================
-- Extra2D 引擎库共享配置
-- 被主项目和示例共享使用
-- ==============================================
-- 获取当前平台
local function get_current_plat()
return get_config("plat") or os.host()
end
-- 定义 Extra2D 引擎库目标
function define_extra2d_engine()
target("extra2d")
set_kind("static")
-- 引擎源文件
add_files("Extra2D/src/**.cpp")
add_files("Extra2D/src/glad/glad.c")
add_files("squirrel/squirrel/*.cpp")
add_files("squirrel/sqstdlib/*.cpp")
-- 头文件路径
add_includedirs("Extra2D/include", {public = true})
add_includedirs("squirrel/include", {public = true})
add_includedirs("Extra2D/include/extra2d/platform", {public = true})
-- 平台配置
local plat = get_current_plat()
if plat == "switch" then
local devkitPro = os.getenv("DEVKITPRO") or "C:/devkitPro"
add_includedirs(devkitPro .. "/portlibs/switch/include", {public = true})
add_linkdirs(devkitPro .. "/portlibs/switch/lib")
add_syslinks("SDL2_mixer", "SDL2", "opusfile", "opus", "vorbisidec", "ogg",
"modplug", "mpg123", "FLAC", "GLESv2", "EGL", "glapi", "drm_nouveau",
{public = true})
elseif plat == "mingw" then
add_packages("glm", "libsdl2", "libsdl2_mixer", {public = true})
add_syslinks("opengl32", "glu32", "winmm", "imm32", "version", "setupapi", {public = true})
end
-- 编译器标志
add_cxflags("-Wall", "-Wextra", {force = true})
add_cxflags("-Wno-unused-variable", "-Wno-unused-function", "-Wno-unused-parameter", {force = true})
add_cxflags("-Wno-deprecated-copy", "-Wno-strict-aliasing", "-Wno-implicit-fallthrough", "-Wno-class-memaccess", {force = true})
if is_mode("debug") then
add_defines("E2D_DEBUG", "_DEBUG", {public = true})
add_cxxflags("-O0", "-g", {force = true})
else
add_defines("NDEBUG", {public = true})
add_cxxflags("-O2", {force = true})
end
target_end()
end

115
xmake/targets/examples.lua Normal file
View File

@ -0,0 +1,115 @@
-- ==============================================
-- Extra2D 示例程序构建目标
-- ==============================================
-- 获取 devkitPro 路径
local function get_devkitpro_path()
return "C:/devkitPro"
end
-- 生成 Switch NRO 文件的通用后构建函数
-- @param target_name 目标名称
-- @param app_title 应用标题
-- @param app_author 应用作者
-- @param app_version 应用版本
-- @param romfs_dir RomFS 目录路径(相对于项目根目录)
local function generate_nro_after_build(target_name, app_title, app_author, app_version, romfs_dir)
after_build(function (target)
local devkitPro = get_devkitpro_path()
local elf_file = target:targetfile()
local output_dir = path.directory(elf_file)
local nacp_file = path.join(output_dir, target_name .. ".nacp")
local nro_file = path.join(output_dir, target_name .. ".nro")
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", app_title, app_author, app_version, 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)
end
-- 定义示例程序的通用配置
-- @param name 目标名称
-- @param options 配置选项表
local function define_example_target(name, options)
target(name)
set_kind("binary")
set_plat("switch")
set_arch("arm64")
set_toolchains("switch")
set_targetdir("build/switch")
-- 添加源文件
add_files(options.source_file or ("Extra2D/examples/" .. name .. "/main.cpp"))
-- 添加头文件路径
add_includedirs("Extra2D/include")
-- 链接 extra2d 库
add_deps("extra2d")
-- 可选:添加链接器标志
if options.ldflags then
add_ldflags(options.ldflags, {force = true})
end
-- 构建后生成 .nro 文件
generate_nro_after_build(
name,
options.app_title or ("Extra2D " .. name),
options.app_author or "Extra2D Team",
options.app_version or "1.0.0",
options.romfs_dir or ("Extra2D/examples/" .. name .. "/romfs")
)
target_end()
end
-- 定义所有示例程序目标
function define_example_targets()
-- ============================================
-- Switch 简单测试程序
-- ============================================
define_example_target("hello_world", {
app_title = "Extra2D hello_world",
app_author = "Extra2D hello_world",
app_version = "1.0.0"
})
-- ============================================
-- 引擎空间索引演示1000个节点
-- ============================================
define_example_target("spatial_index_demo", {
app_title = "Extra2D Spatial Index Demo",
app_version = "1.0.0",
ldflags = "-Wl,-Map=build/switch/spatial_index_demo.map"
})
-- ============================================
-- 碰撞检测演示程序
-- ============================================
define_example_target("collision_demo", {
app_title = "Extra2D Collision Demo",
app_version = "1.0.0"
})
end

107
xmake/targets/extra2d.lua Normal file
View File

@ -0,0 +1,107 @@
-- ==============================================
-- Extra2D 引擎库构建目标
-- ==============================================
-- 核心路径定义
local SRC_DIR = "Extra2D/src"
local INC_DIR = "Extra2D/include"
-- 定义 Extra2D 引擎库目标
function define_extra2d_target()
target("extra2d")
set_kind("static")
set_plat("switch")
set_arch("arm64")
set_toolchains("switch")
set_basename(is_mode("debug") and "libeasy2dd" or "libeasy2d")
-- ==============================================
-- 源文件配置
-- ==============================================
-- 引擎源文件
add_files(path.join(SRC_DIR, "**.cpp"))
add_files(path.join(SRC_DIR, "glad/glad.c"))
-- Squirrel 3.2 源文件
add_files("squirrel/squirrel/*.cpp")
add_files("squirrel/sqstdlib/*.cpp")
-- ==============================================
-- 头文件路径配置
-- ==============================================
-- 公开头文件目录
add_includedirs(INC_DIR, {public = true})
-- 第三方头文件目录
add_includedirs("squirrel/include", {public = true})
-- ==============================================
-- Nintendo Switch 平台配置
-- ==============================================
-- devkitPro mesa 路径EGL + 桌面 OpenGL
local devkitPro = "C:/devkitPro"
add_includedirs(path.join(devkitPro, "portlibs/switch/include"), {public = true})
add_linkdirs(path.join(devkitPro, "portlibs/switch/lib"))
-- 使用系统 GLES3.2 头文件 (位于 devkitPro/portlibs/switch/include)
-- 链接 EGL、OpenGL ES 3.0mesa和 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",
{public = true})
-- 注意pfd (portable-file-dialogs) 暂时禁用,需要进一步修复
-- add_files(path.join(INC_DIR, "pfd/pfd_switch.cpp"))
-- 添加 Switch 兼容性头文件路径
add_includedirs(path.join(INC_DIR, "extra2d/platform"), {public = true})
-- ==============================================
-- 编译器配置
-- ==============================================
-- Switch 特定编译标志
-- 注意Squirrel 脚本绑定使用 dynamic_cast需要 RTTI 支持
-- add_cxflags("-fno-rtti", {force = true})
add_cxflags("-Wno-unused-variable", "-Wno-unused-function", {force = true})
-- Squirrel 第三方库警告抑制
add_cxflags("-Wno-deprecated-copy", "-Wno-strict-aliasing", "-Wno-implicit-fallthrough", "-Wno-class-memaccess", {force = true})
-- 使用 switch 工具链
set_toolchains("switch")
-- ==============================================
-- 头文件安装配置
-- ==============================================
add_headerfiles(path.join(INC_DIR, "extra2d/**.h"), {prefixdir = "extra2d"})
-- 使用 devkitPro 的 switch-glm 替代项目自带的 GLM
-- add_headerfiles(path.join(INC_DIR, "glm/**.hpp"), {prefixdir = "glm"})
add_headerfiles(path.join(INC_DIR, "stb/**.h"), {prefixdir = "stb"})
add_headerfiles(path.join(INC_DIR, "simpleini/**.h"), {prefixdir = "simpleini"})
-- 编译器通用配置
add_cxxflags("-Wall", "-Wextra", {force = true})
add_cxxflags("-Wno-unused-parameter", {force = true})
-- 调试/发布模式配置
if is_mode("debug") then
add_defines("E2D_DEBUG", "_DEBUG", {public = true})
add_cxxflags("-O0", "-g", {force = true})
else
add_defines("NDEBUG", {public = true})
add_cxxflags("-O2", {force = true})
end
target_end()
end

View File

@ -8,7 +8,7 @@ function define_switch_toolchain()
set_description("Nintendo Switch devkitA64 toolchain")
-- 检查 DEVKITPRO 环境变量Windows 上使用 C:/devkitPro
local devkitPro = os.getenv("DEVKITPRO") or "C:/devkitPro"
local devkitPro = "C:/devkitPro"
local devkitA64 = path.join(devkitPro, "devkitA64")
-- 设置工具链路径
@ -21,8 +21,8 @@ function define_switch_toolchain()
-- 架构标志
local arch_flags = "-march=armv8-a+crc+crypto -mtune=cortex-a57 -mtp=soft -fPIE"
add_cxflags(arch_flags)
-- 使用 devkitPro 提供的 switch.specs 文件
add_ldflags("-specs=" .. path.join(devkitPro, "libnx/switch.specs"), "-g", arch_flags)
-- 使用修复后的 switch_fix.specs 文件(使用 Windows 路径)
add_ldflags("-specs=switch_fix.specs", "-g", arch_flags)
-- 定义 Switch 平台宏
add_defines("__SWITCH__", "__NX__", "MA_SWITCH", "PFD_SWITCH")
@ -40,8 +40,44 @@ function define_switch_toolchain()
add_linkdirs(path.join(devkitPro, "portlibs/switch/lib"))
add_syslinks("nx", "m")
toolchain_end()
end
-- 定义工具链
define_switch_toolchain()
-- 获取 devkitPro 路径
function get_devkitpro_path()
return "C:/devkitPro"
end
-- 获取 Switch 平台包含路径
function get_switch_includedirs()
local devkitPro = get_devkitpro_path()
return {
path.join(devkitPro, "libnx/include"),
path.join(devkitPro, "portlibs/switch/include"),
path.join(devkitPro, "portlibs/switch/include/SDL2")
}
end
-- 获取 Switch 平台库路径
function get_switch_linkdirs()
local devkitPro = get_devkitpro_path()
return {
path.join(devkitPro, "libnx/lib"),
path.join(devkitPro, "portlibs/switch/lib")
}
end
-- 获取 Switch 平台系统链接库
function get_switch_syslinks()
-- 注意:链接顺序很重要!被依赖的库必须放在后面
-- 依赖链SDL2 -> EGL -> drm_nouveau
-- GLESv2 -> glapi -> drm_nouveau
return {
"SDL2_mixer", "SDL2",
"opusfile", "opus", "vorbisidec", "ogg",
"modplug", "mpg123", "FLAC",
"GLESv2",
"EGL",
"glapi",
"drm_nouveau"
}
end