feat(平台兼容性): 添加跨平台支持并重构代码结构

refactor(构建系统): 重构xmake配置以支持多平台构建
feat(文件系统): 新增FileSystem类处理跨平台路径
refactor(窗口系统): 重构Window类支持PC平台特性
feat(输入系统): 扩展Input类支持PC输入设备
docs: 添加PC构建指南和Switch构建指南文档
style: 统一平台相关代码风格和命名规范
chore: 删除过时的文档和配置文件
This commit is contained in:
ChestnutYueyue 2026-02-10 05:15:18 +08:00
parent 5880159991
commit 2e08bff567
30 changed files with 3344 additions and 693 deletions

View File

@ -1,80 +0,0 @@
## DataStore 改进计划(基于 Switch FS Save 示例)
### 阶段一Switch 平台存档支持
#### 1.1 添加存档挂载管理
- **目标**: [data.h](file:///c:/Users/soulcoco/Desktop/Easy2D/Extra2D/Extra2D/include/extra2d/utils/data.h) 和 [data.cpp](file:///c:/Users/soulcoco/Desktop/Easy2D/Extra2D/Extra2D/src/utils/data.cpp)
- **内容**:
- 添加 `mountSaveData()` 方法挂载存档
- 添加 `unmountSaveData()` 方法卸载存档
- 添加 `commitSaveData()` 方法提交更改
- 支持自动挂载/卸载 RAII 模式
- **预期收益**: 支持 Switch 官方存档系统
#### 1.2 用户账户集成
- **目标**: [data.h](file:///c:/Users/soulcoco/Desktop/Easy2D/Extra2D/Extra2D/include/extra2d/utils/data.h)
- **内容**:
- 添加 `setUserId()` / `getUserId()` 方法
- 自动获取当前用户ID使用 `accountGetPreselectedUser`
- 支持用户特定的存档路径
- **预期收益**: 支持多用户存档隔离
### 阶段二:事务和缓存优化
#### 2.1 事务支持
- **目标**: [data.cpp](file:///c:/Users/soulcoco/Desktop/Easy2D/Extra2D/Extra2D/src/utils/data.cpp)
- **内容**:
- 添加 `beginTransaction()` 开始事务
- 添加 `commit()` 提交事务
- 添加 `rollback()` 回滚事务
- 批量写入优化减少文件IO
- **预期收益**: 数据一致性和性能提升
#### 2.2 缓存机制
- **目标**: [data.cpp](file:///c:/Users/soulcoco/Desktop/Easy2D/Extra2D/Extra2D/src/utils/data.cpp)
- **内容**:
- 添加内存缓存层
- 延迟写入Lazy Write
- 缓存失效策略
- **预期收益**: 减少文件IO提升读写性能
### 阶段三:数据格式扩展
#### 3.1 JSON 支持
- **目标**: [data.h](file:///c:/Users/soulcoco/Desktop/Easy2D/Extra2D/Extra2D/include/extra2d/utils/data.h)
- **内容**:
- 添加 JSON 格式支持(保留 INI 兼容)
- 嵌套数据结构支持
- 数组类型支持
- **预期收益**: 更灵活的数据结构
#### 3.2 二进制格式支持
- **目标**: [data.cpp](file:///c:/Users/soulcoco/Desktop/Easy2D/Extra2D/Extra2D/src/utils/data.cpp)
- **内容**:
- 添加二进制序列化格式
- 更小的文件体积
- 更快的读写速度
- **预期收益**: 性能和存储优化
### 阶段四:安全和备份
#### 4.1 数据校验
- **目标**: [data.cpp](file:///c:/Users/soulcoco/Desktop/Easy2D/Extra2D/Extra2D/src/utils/data.cpp)
- **内容**:
- 添加 CRC32/MD5 校验
- 数据完整性检查
- 损坏数据恢复机制
- **预期收益**: 数据可靠性
#### 4.2 自动备份
- **目标**: [data.h](file:///c:/Users/soulcoco/Desktop/Easy2D/Extra2D/Extra2D/include/extra2d/utils/data.h)
- **内容**:
- 自动备份机制
- 多版本存档
- 备份恢复接口
- **预期收益**: 数据安全
---
**预计总工作量**: 6-8 小时
**优先级**: 高1.1, 1.2> 中2.1, 3.1> 低2.2, 3.2, 4.1, 4.2

View File

@ -1,105 +0,0 @@
# 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,62 +0,0 @@
## 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

@ -124,9 +124,13 @@ private:
auto &resources = Application::instance().resources(); auto &resources = Application::instance().resources();
// 使用后备字体加载功能 // 使用后备字体加载功能
#ifdef PLATFORM_SWITCH
std::vector<std::string> fontPaths = { std::vector<std::string> fontPaths = {
"romfs:/assets/font.ttf" // 备选字体 "romfs:/assets/font.ttf" // 备选字体
}; };
#else
std::vector<std::string> fontPaths = {FileSystem::resolvePath("font.ttf")};
#endif
titleFont_ = resources.loadFontWithFallbacks(fontPaths, 60, true); titleFont_ = resources.loadFontWithFallbacks(fontPaths, 60, true);
infoFont_ = resources.loadFontWithFallbacks(fontPaths, 28, true); infoFont_ = resources.loadFontWithFallbacks(fontPaths, 28, true);
@ -240,7 +244,12 @@ private:
// 程序入口 // 程序入口
// ============================================================================ // ============================================================================
extern "C" int main(int argc, char *argv[]) { #ifdef _WIN32
int main(int argc, char *argv[])
#else
extern "C" int main(int argc, char *argv[])
#endif
{
(void)argc; (void)argc;
(void)argv; (void)argv;

View File

@ -1,5 +1,9 @@
#include <extra2d/extra2d.h> #include <extra2d/extra2d.h>
#include <extra2d/platform/platform_compat.h>
#ifdef PLATFORM_SWITCH
#include <switch.h> #include <switch.h>
#endif
using namespace extra2d; using namespace extra2d;
@ -13,9 +17,9 @@ using namespace extra2d;
*/ */
static std::vector<std::string> getFontCandidates() { static std::vector<std::string> getFontCandidates() {
return { return {
"romfs:/assets/font.ttf", // 微软雅黑(中文支持) FileSystem::resolvePath("font.ttf"), // 微软雅黑(中文支持)
"romfs:/assets/Gasinamu.ttf", // 备选字体 FileSystem::resolvePath("Gasinamu.ttf"), // 备选字体
"romfs:/assets/default.ttf", // 默认字体 FileSystem::resolvePath("default.ttf"), // 默认字体
}; };
} }
@ -78,12 +82,23 @@ public:
void onUpdate(float dt) override { void onUpdate(float dt) override {
Scene::onUpdate(dt); Scene::onUpdate(dt);
// 检查退出按键START 按钮) // 检查退出按键
auto &input = Application::instance().input(); auto &input = Application::instance().input();
#ifdef PLATFORM_SWITCH
// Switch: 使用手柄 START 按钮
if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_START)) { if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_START)) {
E2D_LOG_INFO("退出应用"); E2D_LOG_INFO("退出应用 (START 按钮)");
Application::instance().quit(); Application::instance().quit();
} }
#else
// PC: 支持 ESC 键或手柄 START 按钮
if (input.isKeyPressed(Key::Escape) ||
input.isButtonPressed(SDL_CONTROLLER_BUTTON_START)) {
E2D_LOG_INFO("退出应用 (ESC 键或 START 按钮)");
Application::instance().quit();
}
#endif
} }
/** /**
@ -107,8 +122,13 @@ public:
// 绘制提示文字(黄色) // 绘制提示文字(黄色)
Color yellow(1.0f, 1.0f, 0.0f, 1.0f); Color yellow(1.0f, 1.0f, 0.0f, 1.0f);
#ifdef PLATFORM_SWITCH
renderer.drawText(*font_, "退出按键START 按钮)", renderer.drawText(*font_, "退出按键START 按钮)",
Vec2(centerX - 80.0f, centerY + 50.0f), yellow); Vec2(centerX - 80.0f, centerY + 50.0f), yellow);
#else
renderer.drawText(*font_, "退出按键ESC 或 START 按钮)",
Vec2(centerX - 80.0f, centerY + 50.0f), yellow);
#endif
} }
private: private:
@ -130,13 +150,25 @@ static AppConfig createAppConfig() {
config.height = 720; config.height = 720;
config.vsync = true; config.vsync = true;
config.fpsLimit = 60; config.fpsLimit = 60;
#ifdef PLATFORM_PC
// PC 端默认窗口模式
config.fullscreen = false;
config.resizable = true;
#endif
return config; return config;
} }
/** /**
* @brief * @brief
*/ */
extern "C" int main(int argc, char *argv[]) { #ifdef _WIN32
int main(int argc, char *argv[])
#else
extern "C" int main(int argc, char *argv[])
#endif
{
(void)argc; (void)argc;
(void)argv; (void)argv;
@ -146,6 +178,7 @@ extern "C" int main(int argc, char *argv[]) {
E2D_LOG_INFO("========================"); E2D_LOG_INFO("========================");
E2D_LOG_INFO("Easy2D Hello World Demo"); E2D_LOG_INFO("Easy2D Hello World Demo");
E2D_LOG_INFO("Platform: {}", platform::getPlatformName());
E2D_LOG_INFO("========================"); E2D_LOG_INFO("========================");
// 获取应用实例 // 获取应用实例

View File

@ -185,11 +185,19 @@ private:
void loadFonts() { void loadFonts() {
auto &resources = Application::instance().resources(); auto &resources = Application::instance().resources();
#ifdef PLATFORM_SWITCH
std::vector<std::string> fontPaths = { std::vector<std::string> fontPaths = {
"romfs:/assets/msjh.ttf", "romfs:/assets/msjh.ttf",
"romfs:/assets/default.ttf", "romfs:/assets/default.ttf",
"romfs:/assets/font.ttf", "romfs:/assets/font.ttf",
}; };
#else
std::vector<std::string> fontPaths = {
FileSystem::resolvePath("msjh.ttf"),
FileSystem::resolvePath("default.ttf"),
FileSystem::resolvePath("font.ttf"),
};
#endif
titleFont_ = resources.loadFontWithFallbacks(fontPaths, 28, true); titleFont_ = resources.loadFontWithFallbacks(fontPaths, 28, true);
infoFont_ = resources.loadFontWithFallbacks(fontPaths, 16, true); infoFont_ = resources.loadFontWithFallbacks(fontPaths, 16, true);
@ -401,7 +409,12 @@ private:
// 程序入口 // 程序入口
// ============================================================================ // ============================================================================
extern "C" int main(int argc, char *argv[]) { #ifdef _WIN32
int main(int argc, char *argv[])
#else
extern "C" int main(int argc, char *argv[])
#endif
{
(void)argc; (void)argc;
(void)argv; (void)argv;

View File

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

View File

@ -12,6 +12,7 @@
// Platform // Platform
#include <extra2d/platform/window.h> #include <extra2d/platform/window.h>
#include <extra2d/platform/input.h> #include <extra2d/platform/input.h>
#include <extra2d/platform/file_system.h>
// Graphics // Graphics
#include <extra2d/graphics/render_backend.h> #include <extra2d/graphics/render_backend.h>

View File

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

View File

@ -0,0 +1,166 @@
#pragma once
/**
* @file file_system.h
* @brief
*
*
* : Nintendo Switch, Windows, Linux, macOS
*/
#include <extra2d/platform/platform_compat.h>
#include <string>
#include <vector>
namespace extra2d {
// ============================================================================
// 文件系统工具类
// ============================================================================
class FileSystem {
public:
/**
* @brief
* @return
*
* Switch: "romfs:/"
* PC: + "/assets/"
*/
static std::string getResourceRoot();
/**
* @brief
* @param relativePath ( "assets/font.ttf")
* @return
*
* Switch: "romfs:/assets/font.ttf"
* PC: "./assets/font.ttf" exe目录/assets/font.ttf
*/
static std::string resolvePath(const std::string& relativePath);
/**
* @brief
* @param path
* @return true
*/
static bool fileExists(const std::string& path);
/**
* @brief
* @param path
* @return true
*/
static bool directoryExists(const std::string& path);
/**
* @brief
* @return
*/
static std::string getExecutableDirectory();
/**
* @brief
* @return
*/
static std::string getCurrentWorkingDirectory();
/**
* @brief
* @param base
* @param relative
* @return
*/
static std::string combinePath(const std::string& base, const std::string& relative);
/**
* @brief
* @param path
* @return
*/
static std::string getFileName(const std::string& path);
/**
* @brief
* @param path
* @return ".ttf"
*/
static std::string getFileExtension(const std::string& path);
/**
* @brief
* @param path
* @return
*/
static std::string getDirectoryName(const std::string& path);
/**
* @brief
* @param path
* @return
*/
static std::string normalizePath(const std::string& path);
/**
* @brief
* @param path
* @return
*/
static std::string readFileText(const std::string& path);
/**
* @brief
* @param path
* @return vector
*/
static std::vector<uint8_t> readFileBytes(const std::string& path);
/**
* @brief
* @param path
* @return -1
*/
static int64_t getFileSize(const std::string& path);
/**
* @brief
* @param path
* @return true
*/
static bool createDirectory(const std::string& path);
/**
* @brief
* @param path
* @return true
*/
static bool createDirectories(const std::string& path);
private:
// 禁止实例化
FileSystem() = delete;
~FileSystem() = delete;
};
// ============================================================================
// 便捷函数
// ============================================================================
/**
* @brief 便
* @param path
* @return
*/
inline std::string resolvePath(const std::string& path) {
return FileSystem::resolvePath(path);
}
/**
* @brief 便
* @param path
* @return true
*/
inline bool fileExists(const std::string& path) {
return FileSystem::fileExists(path);
}
} // namespace extra2d

View File

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

View File

@ -0,0 +1,263 @@
#pragma once
/**
* @file platform_compat.h
* @brief
*
*
* : Nintendo Switch, Windows, Linux, macOS
*/
// ============================================================================
// 平台检测
// ============================================================================
#ifdef __SWITCH__
// Nintendo Switch 平台
#define PLATFORM_SWITCH 1
#define PLATFORM_NAME "Nintendo Switch"
#elif defined(_WIN32)
// Windows 平台
#define PLATFORM_PC 1
#define PLATFORM_WINDOWS 1
#define PLATFORM_NAME "Windows"
#ifndef NOMINMAX
#define NOMINMAX
#endif
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
// 禁用 SDL 的 main 重定义,使用标准 main 函数
#define SDL_MAIN_HANDLED
#elif defined(__linux__)
// Linux 平台
#define PLATFORM_PC 1
#define PLATFORM_LINUX 1
#define PLATFORM_NAME "Linux"
#elif defined(__APPLE__) && defined(__MACH__)
// macOS 平台
#define PLATFORM_PC 1
#define PLATFORM_MACOS 1
#define PLATFORM_NAME "macOS"
#else
#error "Unsupported platform"
#endif
// ============================================================================
// Nintendo Switch 平台包含
// ============================================================================
#ifdef PLATFORM_SWITCH
#include <switch.h>
#include <malloc.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <string>
// RomFS路径前缀
#define ROMFS_PREFIX "romfs:/"
// Switch 特定的编译器属性
#define E2D_LIKELY(x) __builtin_expect(!!(x), 1)
#define E2D_UNLIKELY(x) __builtin_expect(!!(x), 0)
// Switch 调试输出
#ifdef E2D_DEBUG
#define E2D_PLATFORM_LOG(fmt, ...) printf("[Extra2D] " fmt "\n", ##__VA_ARGS__)
#else
#define E2D_PLATFORM_LOG(fmt, ...) ((void)0)
#endif
#endif // PLATFORM_SWITCH
// ============================================================================
// PC 平台包含 (Windows/Linux/macOS)
// ============================================================================
#ifdef PLATFORM_PC
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <string>
#include <cstdint>
// PC 平台使用标准文件路径
#define ROMFS_PREFIX ""
// PC 平台编译器属性
#if defined(__GNUC__) || defined(__clang__)
#define E2D_LIKELY(x) __builtin_expect(!!(x), 1)
#define E2D_UNLIKELY(x) __builtin_expect(!!(x), 0)
#else
#define E2D_LIKELY(x) (x)
#define E2D_UNLIKELY(x) (x)
#endif
// PC 调试输出
#ifdef E2D_DEBUG
#ifdef PLATFORM_WINDOWS
#include <windows.h>
#define E2D_PLATFORM_LOG(fmt, ...) OutputDebugStringA((std::string("[Extra2D] ") + fmt + "\n").c_str())
#else
#define E2D_PLATFORM_LOG(fmt, ...) printf("[Extra2D] " fmt "\n", ##__VA_ARGS__)
#endif
#else
#define E2D_PLATFORM_LOG(fmt, ...) ((void)0)
#endif
#endif // PLATFORM_PC
// ============================================================================
// 跨平台通用定义
// ============================================================================
namespace extra2d {
namespace platform {
/**
* @brief
* @return
*/
inline const char* getPlatformName() {
return PLATFORM_NAME;
}
/**
* @brief Switch
* @return true Switch
*/
inline bool isSwitch() {
#ifdef PLATFORM_SWITCH
return true;
#else
return false;
#endif
}
/**
* @brief PC
* @return true PC (Windows/Linux/macOS)
*/
inline bool isPC() {
#ifdef PLATFORM_PC
return true;
#else
return false;
#endif
}
/**
* @brief Windows
* @return true Windows
*/
inline bool isWindows() {
#ifdef PLATFORM_WINDOWS
return true;
#else
return false;
#endif
}
/**
* @brief Linux
* @return true Linux
*/
inline bool isLinux() {
#ifdef PLATFORM_LINUX
return true;
#else
return false;
#endif
}
/**
* @brief macOS
* @return true macOS
*/
inline bool isMacOS() {
#ifdef PLATFORM_MACOS
return true;
#else
return false;
#endif
}
} // namespace platform
// ============================================================================
// 文件系统路径工具
// ============================================================================
namespace romfs {
/**
* @brief RomFS
*/
#ifdef PLATFORM_SWITCH
static constexpr const char* ROOT = "romfs:/";
#else
static constexpr const char* ROOT = "";
#endif
/**
* @brief
* @param path
* @return true
*/
inline bool fileExists(const char* path) {
struct stat st;
return stat(path, &st) == 0;
}
/**
* @brief romfs (Switch) (PC)
* @param path
* @return true romfs
*/
inline bool isRomfsPath(const char* path) {
return path && (strncmp(path, "romfs:/", 7) == 0 || strncmp(path, "romfs:\\", 7) == 0);
}
/**
* @brief
* @param relativePath
* @return (Switch romfs:/ PC )
*/
inline std::string makePath(const char* relativePath) {
#ifdef PLATFORM_SWITCH
std::string result = ROOT;
result += relativePath;
return result;
#else
return std::string(relativePath);
#endif
}
} // namespace romfs
} // namespace extra2d
// ============================================================================
// 向后兼容:保留 switch_compat.h 的宏定义
// ============================================================================
#ifdef PLATFORM_SWITCH
#define IS_SWITCH_PLATFORM 1
#define SWITCH_ROMFS_PREFIX "romfs:/"
#define SWITCH_LIKELY(x) E2D_LIKELY(x)
#define SWITCH_UNLIKELY(x) E2D_UNLIKELY(x)
#ifdef E2D_DEBUG
#define SWITCH_DEBUG_PRINTF(fmt, ...) E2D_PLATFORM_LOG(fmt, ##__VA_ARGS__)
#else
#define SWITCH_DEBUG_PRINTF(fmt, ...) ((void)0)
#endif
#endif

View File

@ -2,78 +2,16 @@
/** /**
* @file switch_compat.h * @file switch_compat.h
* @brief Nintendo Switch * @brief Nintendo Switch ()
* *
* Switch * @deprecated 使 platform_compat.h
*
*/ */
#ifdef __SWITCH__ // 包含新的跨平台兼容性头文件
#include "platform_compat.h"
// ============================================================================ // 发出弃用警告(仅在非 Switch 平台或调试模式下)
// Switch 平台包含 #if !defined(__SWITCH__) && defined(E2D_DEBUG)
// ============================================================================ #warning "switch_compat.h is deprecated, use platform_compat.h instead"
#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 #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

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

View File

@ -13,18 +13,29 @@
#include <extra2d/utils/timer.h> #include <extra2d/utils/timer.h>
#include <chrono> #include <chrono>
#include <switch.h>
#include <thread> #include <thread>
#include <time.h> #include <time.h>
#ifdef __SWITCH__
#include <switch.h>
#endif
namespace extra2d { namespace extra2d {
// 获取当前时间(秒) // 获取当前时间(秒)
static double getTimeSeconds() { static double getTimeSeconds() {
#ifdef __SWITCH__
struct timespec ts; struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts); clock_gettime(CLOCK_MONOTONIC, &ts);
return static_cast<double>(ts.tv_sec) + return static_cast<double>(ts.tv_sec) +
static_cast<double>(ts.tv_nsec) / 1000000000.0; 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() { Application &Application::instance() {
@ -42,8 +53,9 @@ bool Application::init(const AppConfig &config) {
config_ = config; config_ = config;
#ifdef __SWITCH__
// ======================================== // ========================================
// 1. 初始化 RomFS 文件系统(必须在 SDL_Init 之前 // 1. 初始化 RomFS 文件系统(Switch 平台
// ======================================== // ========================================
Result rc; Result rc;
rc = romfsInit(); rc = romfsInit();
@ -54,13 +66,14 @@ bool Application::init(const AppConfig &config) {
} }
// ======================================== // ========================================
// 2. 初始化 nxlink 调试输出(可选 // 2. 初始化 nxlink 调试输出(Switch 平台
// ======================================== // ========================================
rc = socketInitializeDefault(); rc = socketInitializeDefault();
if (R_FAILED(rc)) { if (R_FAILED(rc)) {
E2D_LOG_WARN( E2D_LOG_WARN(
"socketInitializeDefault failed, nxlink will not be available"); "socketInitializeDefault failed, nxlink will not be available");
} }
#endif
// ======================================== // ========================================
// 3. 创建窗口(包含 SDL_Init + GLES 3.2 上下文创建) // 3. 创建窗口(包含 SDL_Init + GLES 3.2 上下文创建)
@ -70,8 +83,14 @@ bool Application::init(const AppConfig &config) {
winConfig.title = config.title; winConfig.title = config.title;
winConfig.width = 1280; winConfig.width = 1280;
winConfig.height = 720; winConfig.height = 720;
#ifdef __SWITCH__
winConfig.fullscreen = true; winConfig.fullscreen = true;
winConfig.resizable = false; winConfig.resizable = false;
#else
// PC 平台默认窗口模式
winConfig.fullscreen = config.fullscreen;
winConfig.resizable = true;
#endif
winConfig.vsync = config.vsync; winConfig.vsync = config.vsync;
winConfig.msaaSamples = config.msaaSamples; winConfig.msaaSamples = config.msaaSamples;
@ -101,7 +120,7 @@ bool Application::init(const AppConfig &config) {
camera_ = makeUnique<Camera>(0, static_cast<float>(window_->getWidth()), camera_ = makeUnique<Camera>(0, static_cast<float>(window_->getWidth()),
static_cast<float>(window_->getHeight()), 0); static_cast<float>(window_->getHeight()), 0);
// 窗口大小回调Switch 上不会触发,但保留接口) // 窗口大小回调
window_->setResizeCallback([this](int width, int height) { window_->setResizeCallback([this](int width, int height) {
if (camera_) { if (camera_) {
camera_->setViewport(0, static_cast<float>(width), camera_->setViewport(0, static_cast<float>(width),
@ -119,8 +138,13 @@ bool Application::init(const AppConfig &config) {
// 初始化音频引擎 // 初始化音频引擎
AudioEngine::getInstance().initialize(); AudioEngine::getInstance().initialize();
// 添加 romfs:/ 到资源搜索路径 #ifdef __SWITCH__
// 添加 romfs:/ 到资源搜索路径Switch 平台)
resourceManager_->addSearchPath("romfs:/"); resourceManager_->addSearchPath("romfs:/");
#endif
// 添加默认资源路径
resourceManager_->addSearchPath("assets/");
resourceManager_->addSearchPath("./");
initialized_ = true; initialized_ = true;
running_ = true; running_ = true;
@ -161,9 +185,11 @@ void Application::shutdown() {
window_.reset(); window_.reset();
} }
// Switch 清理 #ifdef __SWITCH__
// Switch 平台清理
romfsExit(); romfsExit();
socketExit(); socketExit();
#endif
initialized_ = false; initialized_ = false;
running_ = false; running_ = false;
@ -179,10 +205,17 @@ void Application::run() {
lastFrameTime_ = getTimeSeconds(); lastFrameTime_ = getTimeSeconds();
#ifdef __SWITCH__
// SDL2 on Switch 内部已处理 appletMainLoop // SDL2 on Switch 内部已处理 appletMainLoop
while (running_ && !window_->shouldClose()) { while (running_ && !window_->shouldClose()) {
mainLoop(); mainLoop();
} }
#else
// PC 平台主循环
while (running_ && !window_->shouldClose()) {
mainLoop();
}
#endif
} }
void Application::quit() { void Application::quit() {

View File

@ -0,0 +1,315 @@
#include <extra2d/platform/file_system.h>
#include <extra2d/utils/logger.h>
#include <sys/stat.h>
#include <fstream>
#include <sstream>
#include <algorithm>
#ifdef PLATFORM_WINDOWS
#include <windows.h>
#include <direct.h>
#include <shlwapi.h>
#pragma comment(lib, "shlwapi.lib")
#else
#include <unistd.h>
#include <libgen.h>
#include <sys/types.h>
#include <dirent.h>
#include <limits.h>
#endif
namespace extra2d {
// ============================================================================
// 平台特定辅助函数
// ============================================================================
#ifdef PLATFORM_WINDOWS
static const char PATH_SEPARATOR = '\\';
static const char PATH_SEPARATOR_ALT = '/';
#else
static const char PATH_SEPARATOR = '/';
static const char PATH_SEPARATOR_ALT = '\\';
#endif
static std::string normalizeSeparators(const std::string& path) {
std::string result = path;
std::replace(result.begin(), result.end(), PATH_SEPARATOR_ALT, PATH_SEPARATOR);
return result;
}
// ============================================================================
// 资源根目录
// ============================================================================
std::string FileSystem::getResourceRoot() {
#ifdef PLATFORM_SWITCH
return "romfs:/";
#else
// PC 端:优先使用可执行文件目录下的 assets 文件夹
std::string exeDir = getExecutableDirectory();
std::string assetsDir = combinePath(exeDir, "assets");
if (directoryExists(assetsDir)) {
return assetsDir;
}
// 备选:当前工作目录
std::string cwd = getCurrentWorkingDirectory();
assetsDir = combinePath(cwd, "assets");
if (directoryExists(assetsDir)) {
return assetsDir;
}
// 默认返回当前工作目录
return cwd;
#endif
}
// ============================================================================
// 路径解析
// ============================================================================
std::string FileSystem::resolvePath(const std::string& relativePath) {
std::string normalized = normalizeSeparators(relativePath);
#ifdef PLATFORM_SWITCH
// Switch: 添加 romfs:/ 前缀
if (normalized.find("romfs:/") == 0) {
return normalized;
}
return "romfs:/" + normalized;
#else
// PC: 如果已经是绝对路径,直接返回
#ifdef PLATFORM_WINDOWS
if (normalized.size() >= 2 && normalized[1] == ':') {
return normalized;
}
#else
if (!normalized.empty() && normalized[0] == '/') {
return normalized;
}
#endif
// 组合资源根目录和相对路径
return combinePath(getResourceRoot(), normalized);
#endif
}
// ============================================================================
// 文件/目录检查
// ============================================================================
bool FileSystem::fileExists(const std::string& path) {
struct stat st;
if (stat(path.c_str(), &st) != 0) {
return false;
}
return (st.st_mode & S_IFREG) != 0;
}
bool FileSystem::directoryExists(const std::string& path) {
struct stat st;
if (stat(path.c_str(), &st) != 0) {
return false;
}
return (st.st_mode & S_IFDIR) != 0;
}
// ============================================================================
// 路径操作
// ============================================================================
std::string FileSystem::getExecutableDirectory() {
std::string exePath;
#ifdef PLATFORM_WINDOWS
char buffer[MAX_PATH];
DWORD len = GetModuleFileNameA(NULL, buffer, MAX_PATH);
if (len > 0 && len < MAX_PATH) {
exePath = buffer;
}
#elif defined(PLATFORM_SWITCH)
// Switch: 返回当前工作目录(不支持获取可执行文件路径)
return getCurrentWorkingDirectory();
#else
char buffer[PATH_MAX];
ssize_t len = readlink("/proc/self/exe", buffer, sizeof(buffer) - 1);
if (len != -1) {
buffer[len] = '\0';
exePath = buffer;
}
#endif
if (!exePath.empty()) {
size_t lastSep = exePath.find_last_of("/\\");
if (lastSep != std::string::npos) {
return exePath.substr(0, lastSep);
}
}
return getCurrentWorkingDirectory();
}
std::string FileSystem::getCurrentWorkingDirectory() {
#ifdef PLATFORM_WINDOWS
char buffer[MAX_PATH];
DWORD len = GetCurrentDirectoryA(MAX_PATH, buffer);
if (len > 0 && len < MAX_PATH) {
return std::string(buffer);
}
#else
char buffer[PATH_MAX];
if (getcwd(buffer, sizeof(buffer)) != nullptr) {
return std::string(buffer);
}
#endif
return ".";
}
std::string FileSystem::combinePath(const std::string& base, const std::string& relative) {
if (base.empty()) {
return normalizeSeparators(relative);
}
if (relative.empty()) {
return normalizeSeparators(base);
}
std::string result = normalizeSeparators(base);
std::string rel = normalizeSeparators(relative);
// 移除末尾的分隔符
while (!result.empty() && result.back() == PATH_SEPARATOR) {
result.pop_back();
}
// 移除开头的分隔符
size_t relStart = 0;
while (relStart < rel.size() && rel[relStart] == PATH_SEPARATOR) {
++relStart;
}
if (relStart < rel.size()) {
result += PATH_SEPARATOR;
result += rel.substr(relStart);
}
return result;
}
std::string FileSystem::getFileName(const std::string& path) {
std::string normalized = normalizeSeparators(path);
size_t lastSep = normalized.find_last_of(PATH_SEPARATOR);
if (lastSep != std::string::npos) {
return normalized.substr(lastSep + 1);
}
return normalized;
}
std::string FileSystem::getFileExtension(const std::string& path) {
std::string fileName = getFileName(path);
size_t lastDot = fileName.find_last_of('.');
if (lastDot != std::string::npos && lastDot > 0) {
return fileName.substr(lastDot);
}
return "";
}
std::string FileSystem::getDirectoryName(const std::string& path) {
std::string normalized = normalizeSeparators(path);
size_t lastSep = normalized.find_last_of(PATH_SEPARATOR);
if (lastSep != std::string::npos) {
return normalized.substr(0, lastSep);
}
return ".";
}
std::string FileSystem::normalizePath(const std::string& path) {
return normalizeSeparators(path);
}
// ============================================================================
// 文件读写
// ============================================================================
std::string FileSystem::readFileText(const std::string& path) {
std::ifstream file(path, std::ios::in | std::ios::binary);
if (!file.is_open()) {
E2D_LOG_ERROR("Failed to open file: {}", path);
return "";
}
std::stringstream buffer;
buffer << file.rdbuf();
return buffer.str();
}
std::vector<uint8_t> FileSystem::readFileBytes(const std::string& path) {
std::ifstream file(path, std::ios::in | std::ios::binary | std::ios::ate);
if (!file.is_open()) {
E2D_LOG_ERROR("Failed to open file: {}", path);
return {};
}
auto size = file.tellg();
file.seekg(0, std::ios::beg);
std::vector<uint8_t> buffer(static_cast<size_t>(size));
file.read(reinterpret_cast<char*>(buffer.data()), size);
return buffer;
}
int64_t FileSystem::getFileSize(const std::string& path) {
struct stat st;
if (stat(path.c_str(), &st) != 0) {
return -1;
}
return static_cast<int64_t>(st.st_size);
}
// ============================================================================
// 目录操作
// ============================================================================
bool FileSystem::createDirectory(const std::string& path) {
std::string normalized = normalizeSeparators(path);
#ifdef PLATFORM_WINDOWS
int result = _mkdir(normalized.c_str());
#else
int result = mkdir(normalized.c_str(), 0755);
#endif
return result == 0 || errno == EEXIST;
}
bool FileSystem::createDirectories(const std::string& path) {
std::string normalized = normalizeSeparators(path);
if (normalized.empty()) {
return true;
}
// 逐级创建目录
size_t pos = 0;
while (pos < normalized.size()) {
pos = normalized.find(PATH_SEPARATOR, pos + 1);
if (pos == std::string::npos) {
pos = normalized.size();
}
std::string subPath = normalized.substr(0, pos);
if (!subPath.empty() && !directoryExists(subPath)) {
if (!createDirectory(subPath)) {
return false;
}
}
}
return true;
}
} // namespace extra2d

View File

@ -5,11 +5,19 @@
namespace extra2d { namespace extra2d {
Input::Input() Input::Input()
: controller_(nullptr), leftStickX_(0.0f), leftStickY_(0.0f), : controller_(nullptr),
rightStickX_(0.0f), rightStickY_(0.0f), touching_(false), leftStickX_(0.0f), leftStickY_(0.0f),
prevTouching_(false), touchCount_(0) { rightStickX_(0.0f), rightStickY_(0.0f),
mouseScroll_(0.0f), prevMouseScroll_(0.0f),
touching_(false), prevTouching_(false), touchCount_(0) {
// 初始化所有状态数组
keysDown_.fill(false);
prevKeysDown_.fill(false);
buttonsDown_.fill(false); buttonsDown_.fill(false);
prevButtonsDown_.fill(false); prevButtonsDown_.fill(false);
mouseButtonsDown_.fill(false);
prevMouseButtonsDown_.fill(false);
} }
Input::~Input() { shutdown(); } Input::~Input() { shutdown(); }
@ -28,8 +36,16 @@ void Input::init() {
} }
if (!controller_) { if (!controller_) {
E2D_LOG_WARN("No game controller found, input may be limited"); E2D_LOG_WARN("No game controller found");
} }
// 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() { void Input::shutdown() {
@ -41,10 +57,49 @@ void Input::shutdown() {
void Input::update() { void Input::update() {
// 保存上一帧状态 // 保存上一帧状态
prevKeysDown_ = keysDown_;
prevButtonsDown_ = buttonsDown_; prevButtonsDown_ = buttonsDown_;
prevMouseButtonsDown_ = mouseButtonsDown_;
prevMousePosition_ = mousePosition_;
prevMouseScroll_ = mouseScroll_;
prevTouching_ = touching_; prevTouching_ = touching_;
prevTouchPosition_ = touchPosition_; 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_) { if (controller_) {
// 更新按钮状态 // 更新按钮状态
for (int i = 0; i < MAX_BUTTONS; ++i) { for (int i = 0; i < MAX_BUTTONS; ++i) {
@ -55,23 +110,22 @@ void Input::update() {
// 读取摇杆(归一化到 -1.0 ~ 1.0 // 读取摇杆(归一化到 -1.0 ~ 1.0
leftStickX_ = static_cast<float>(SDL_GameControllerGetAxis( leftStickX_ = static_cast<float>(SDL_GameControllerGetAxis(
controller_, SDL_CONTROLLER_AXIS_LEFTX)) / controller_, SDL_CONTROLLER_AXIS_LEFTX)) / 32767.0f;
32767.0f;
leftStickY_ = static_cast<float>(SDL_GameControllerGetAxis( leftStickY_ = static_cast<float>(SDL_GameControllerGetAxis(
controller_, SDL_CONTROLLER_AXIS_LEFTY)) / controller_, SDL_CONTROLLER_AXIS_LEFTY)) / 32767.0f;
32767.0f;
rightStickX_ = static_cast<float>(SDL_GameControllerGetAxis( rightStickX_ = static_cast<float>(SDL_GameControllerGetAxis(
controller_, SDL_CONTROLLER_AXIS_RIGHTX)) / controller_, SDL_CONTROLLER_AXIS_RIGHTX)) / 32767.0f;
32767.0f;
rightStickY_ = static_cast<float>(SDL_GameControllerGetAxis( rightStickY_ = static_cast<float>(SDL_GameControllerGetAxis(
controller_, SDL_CONTROLLER_AXIS_RIGHTY)) / controller_, SDL_CONTROLLER_AXIS_RIGHTY)) / 32767.0f;
32767.0f;
} else { } else {
buttonsDown_.fill(false); buttonsDown_.fill(false);
leftStickX_ = leftStickY_ = rightStickX_ = rightStickY_ = 0.0f; leftStickX_ = leftStickY_ = rightStickX_ = rightStickY_ = 0.0f;
} }
}
// 更新触摸屏SDL2 Touch API void Input::updateTouch() {
#ifdef PLATFORM_SWITCH
// Switch 原生触摸屏支持
SDL_TouchID touchId = SDL_GetTouchDevice(0); SDL_TouchID touchId = SDL_GetTouchDevice(0);
if (touchId != 0) { if (touchId != 0) {
touchCount_ = SDL_GetNumTouchFingers(touchId); touchCount_ = SDL_GetNumTouchFingers(touchId);
@ -91,10 +145,39 @@ void Input::update() {
touchCount_ = 0; touchCount_ = 0;
touching_ = false; 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 { SDL_GameControllerButton Input::mapKeyToButton(int keyCode) const {
@ -121,19 +204,19 @@ SDL_GameControllerButton Input::mapKeyToButton(int keyCode) const {
// 常用键 → 手柄按钮 // 常用键 → 手柄按钮
case Key::Z: case Key::Z:
return SDL_CONTROLLER_BUTTON_B; // 确认 return SDL_CONTROLLER_BUTTON_B;
case Key::X: case Key::X:
return SDL_CONTROLLER_BUTTON_A; // 取消 return SDL_CONTROLLER_BUTTON_A;
case Key::C: case Key::C:
return SDL_CONTROLLER_BUTTON_Y; return SDL_CONTROLLER_BUTTON_Y;
case Key::V: case Key::V:
return SDL_CONTROLLER_BUTTON_X; return SDL_CONTROLLER_BUTTON_X;
case Key::Space: case Key::Space:
return SDL_CONTROLLER_BUTTON_A; // 空格 = A return SDL_CONTROLLER_BUTTON_A;
case Key::Enter: case Key::Enter:
return SDL_CONTROLLER_BUTTON_A; // 回车 = A return SDL_CONTROLLER_BUTTON_A;
case Key::Escape: case Key::Escape:
return SDL_CONTROLLER_BUTTON_START; // ESC = Start return SDL_CONTROLLER_BUTTON_START;
// 肩键 // 肩键
case Key::Q: case Key::Q:
@ -153,28 +236,54 @@ SDL_GameControllerButton Input::mapKeyToButton(int keyCode) const {
} }
bool Input::isKeyDown(int keyCode) const { bool Input::isKeyDown(int keyCode) const {
#ifdef PLATFORM_SWITCH
// Switch: 映射到手柄按钮
SDL_GameControllerButton button = mapKeyToButton(keyCode); SDL_GameControllerButton button = mapKeyToButton(keyCode);
if (button == SDL_CONTROLLER_BUTTON_INVALID) if (button == SDL_CONTROLLER_BUTTON_INVALID)
return false; return false;
return buttonsDown_[button]; 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 { bool Input::isKeyPressed(int keyCode) const {
#ifdef PLATFORM_SWITCH
SDL_GameControllerButton button = mapKeyToButton(keyCode); SDL_GameControllerButton button = mapKeyToButton(keyCode);
if (button == SDL_CONTROLLER_BUTTON_INVALID) if (button == SDL_CONTROLLER_BUTTON_INVALID)
return false; return false;
return buttonsDown_[button] && !prevButtonsDown_[button]; 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 { bool Input::isKeyReleased(int keyCode) const {
#ifdef PLATFORM_SWITCH
SDL_GameControllerButton button = mapKeyToButton(keyCode); SDL_GameControllerButton button = mapKeyToButton(keyCode);
if (button == SDL_CONTROLLER_BUTTON_INVALID) if (button == SDL_CONTROLLER_BUTTON_INVALID)
return false; return false;
return !buttonsDown_[button] && prevButtonsDown_[button]; 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 { bool Input::isButtonDown(int button) const {
@ -200,21 +309,35 @@ Vec2 Input::getLeftStick() const { return Vec2(leftStickX_, leftStickY_); }
Vec2 Input::getRightStick() const { return Vec2(rightStickX_, rightStickY_); } Vec2 Input::getRightStick() const { return Vec2(rightStickX_, rightStickY_); }
// ============================================================================ // ============================================================================
// 鼠标输入映射到触摸屏 // 鼠标输入
// ============================================================================ // ============================================================================
bool Input::isMouseDown(MouseButton button) const { 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) { if (button == MouseButton::Left) {
return touching_; return touching_;
} }
// A 键映射为右键
if (button == MouseButton::Right) { if (button == MouseButton::Right) {
return buttonsDown_[SDL_CONTROLLER_BUTTON_A]; return buttonsDown_[SDL_CONTROLLER_BUTTON_A];
} }
return false; return false;
#else
// PC: 直接使用鼠标按钮
return mouseButtonsDown_[index];
#endif
} }
bool Input::isMousePressed(MouseButton button) const { 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) { if (button == MouseButton::Left) {
return touching_ && !prevTouching_; return touching_ && !prevTouching_;
} }
@ -223,9 +346,17 @@ bool Input::isMousePressed(MouseButton button) const {
!prevButtonsDown_[SDL_CONTROLLER_BUTTON_A]; !prevButtonsDown_[SDL_CONTROLLER_BUTTON_A];
} }
return false; return false;
#else
return mouseButtonsDown_[index] && !prevMouseButtonsDown_[index];
#endif
} }
bool Input::isMouseReleased(MouseButton button) const { 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) { if (button == MouseButton::Left) {
return !touching_ && prevTouching_; return !touching_ && prevTouching_;
} }
@ -234,37 +365,85 @@ bool Input::isMouseReleased(MouseButton button) const {
prevButtonsDown_[SDL_CONTROLLER_BUTTON_A]; prevButtonsDown_[SDL_CONTROLLER_BUTTON_A];
} }
return false; return false;
#else
return !mouseButtonsDown_[index] && prevMouseButtonsDown_[index];
#endif
} }
Vec2 Input::getMousePosition() const { return touchPosition_; } Vec2 Input::getMousePosition() const {
#ifdef PLATFORM_SWITCH
return touchPosition_;
#else
return mousePosition_;
#endif
}
Vec2 Input::getMouseDelta() const { Vec2 Input::getMouseDelta() const {
#ifdef PLATFORM_SWITCH
if (touching_ && prevTouching_) { if (touching_ && prevTouching_) {
return touchPosition_ - prevTouchPosition_; return touchPosition_ - prevTouchPosition_;
} }
return Vec2::Zero(); return Vec2::Zero();
#else
return mousePosition_ - prevMousePosition_;
#endif
} }
void Input::setMousePosition(const Vec2 & /*position*/) { void Input::setMousePosition(const Vec2 &position) {
// 不支持在 Switch 上设置触摸位置 #ifndef PLATFORM_SWITCH
SDL_WarpMouseInWindow(SDL_GL_GetCurrentWindow(),
static_cast<int>(position.x),
static_cast<int>(position.y));
#else
(void)position;
#endif
} }
void Input::setMouseVisible(bool /*visible*/) { void Input::setMouseVisible(bool visible) {
// Switch 无鼠标光标 #ifndef PLATFORM_SWITCH
SDL_ShowCursor(visible ? SDL_ENABLE : SDL_DISABLE);
#else
(void)visible;
#endif
} }
void Input::setMouseLocked(bool /*locked*/) { void Input::setMouseLocked(bool locked) {
// Switch 无鼠标光标 #ifndef PLATFORM_SWITCH
SDL_SetRelativeMouseMode(locked ? SDL_TRUE : SDL_FALSE);
#else
(void)locked;
#endif
} }
// ============================================================================
// 便捷方法
// ============================================================================
bool Input::isAnyKeyDown() const { bool Input::isAnyKeyDown() const {
#ifdef PLATFORM_SWITCH
for (int i = 0; i < MAX_BUTTONS; ++i) { for (int i = 0; i < MAX_BUTTONS; ++i) {
if (buttonsDown_[i]) if (buttonsDown_[i])
return true; return true;
} }
#else
for (int i = 0; i < MAX_KEYS; ++i) {
if (keysDown_[i])
return true;
}
#endif
return false; return false;
} }
bool Input::isAnyMouseDown() const { return touching_; } 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
}
} // namespace extra2d } // namespace extra2d

View File

@ -10,9 +10,15 @@
namespace extra2d { namespace extra2d {
Window::Window() Window::Window()
: sdlWindow_(nullptr), glContext_(nullptr), width_(1280), height_(720), : sdlWindow_(nullptr), glContext_(nullptr), currentCursor_(nullptr),
vsync_(true), shouldClose_(false), userData_(nullptr), width_(1280), height_(720), vsync_(true), shouldClose_(false),
eventQueue_(nullptr) {} fullscreen_(true), focused_(true), contentScaleX_(1.0f), contentScaleY_(1.0f),
userData_(nullptr), eventQueue_(nullptr) {
// 初始化光标数组
for (int i = 0; i < 9; ++i) {
sdlCursors_[i] = nullptr;
}
}
Window::~Window() { destroy(); } Window::~Window() { destroy(); }
@ -22,12 +28,13 @@ bool Window::create(const WindowConfig &config) {
return false; return false;
} }
width_ = 1280; // Switch 固定分辨率 width_ = config.width;
height_ = 720; height_ = config.height;
vsync_ = config.vsync; vsync_ = config.vsync;
fullscreen_ = config.fullscreen;
// 初始化 SDL2 + 创建窗口 + GL 上下文 // 初始化 SDL2 + 创建窗口 + GL 上下文
if (!initSDL()) { if (!initSDL(config)) {
E2D_LOG_ERROR("Failed to initialize SDL2"); E2D_LOG_ERROR("Failed to initialize SDL2");
return false; return false;
} }
@ -36,14 +43,19 @@ bool Window::create(const WindowConfig &config) {
input_ = makeUnique<Input>(); input_ = makeUnique<Input>();
input_->init(); input_->init();
E2D_LOG_INFO("Window created: {}x{}", width_, height_); // PC 端初始化光标
#ifdef PLATFORM_PC
initCursors();
#endif
E2D_LOG_INFO("Window created: {}x{} (Platform: {})",
width_, height_, platform::getPlatformName());
return true; return true;
} }
bool Window::initSDL() { bool Window::initSDL(const WindowConfig &config) {
// SDL2 全局初始化(视频 + 游戏控制器 + 音频) // SDL2 全局初始化(视频 + 游戏控制器 + 音频)
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER | SDL_INIT_AUDIO) != if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER | SDL_INIT_AUDIO) != 0) {
0) {
E2D_LOG_ERROR("SDL_Init failed: {}", SDL_GetError()); E2D_LOG_ERROR("SDL_Init failed: {}", SDL_GetError());
return false; return false;
} }
@ -61,11 +73,36 @@ bool Window::initSDL() {
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24); SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8); SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
// 创建 SDL2 窗口Switch 全屏) // 双缓冲
Uint32 windowFlags = SDL_WINDOW_OPENGL | SDL_WINDOW_FULLSCREEN; SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
sdlWindow_ =
SDL_CreateWindow("Easy2D", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, // 创建 SDL2 窗口
Uint32 windowFlags = SDL_WINDOW_OPENGL;
#ifdef PLATFORM_SWITCH
// Switch 始终全屏
windowFlags |= SDL_WINDOW_FULLSCREEN;
width_ = 1280;
height_ = 720;
#else
// PC 端根据配置设置
if (config.fullscreen) {
windowFlags |= SDL_WINDOW_FULLSCREEN_DESKTOP;
} else {
if (config.resizable) {
windowFlags |= SDL_WINDOW_RESIZABLE;
}
// 注意SDL_WINDOWPOS_CENTERED 是位置参数,不是窗口标志
// 窗口居中在 SDL_CreateWindow 的位置参数中处理
}
#endif
sdlWindow_ = SDL_CreateWindow(
config.title.c_str(),
config.centerWindow ? SDL_WINDOWPOS_CENTERED : SDL_WINDOWPOS_UNDEFINED,
config.centerWindow ? SDL_WINDOWPOS_CENTERED : SDL_WINDOWPOS_UNDEFINED,
width_, height_, windowFlags); width_, height_, windowFlags);
if (!sdlWindow_) { if (!sdlWindow_) {
E2D_LOG_ERROR("SDL_CreateWindow failed: {}", SDL_GetError()); E2D_LOG_ERROR("SDL_CreateWindow failed: {}", SDL_GetError());
SDL_Quit(); SDL_Quit();
@ -92,8 +129,8 @@ bool Window::initSDL() {
return false; return false;
} }
if (gladLoadGLES2Loader(reinterpret_cast<GLADloadproc>(SDL_GL_GetProcAddress)) == // 加载 OpenGL ES 函数指针
0) { if (gladLoadGLES2Loader(reinterpret_cast<GLADloadproc>(SDL_GL_GetProcAddress)) == 0) {
E2D_LOG_ERROR("gladLoadGLES2Loader failed"); E2D_LOG_ERROR("gladLoadGLES2Loader failed");
SDL_GL_DeleteContext(glContext_); SDL_GL_DeleteContext(glContext_);
glContext_ = nullptr; glContext_ = nullptr;
@ -106,11 +143,23 @@ bool Window::initSDL() {
// 设置 VSync // 设置 VSync
SDL_GL_SetSwapInterval(vsync_ ? 1 : 0); SDL_GL_SetSwapInterval(vsync_ ? 1 : 0);
E2D_LOG_INFO("SDL2 + GLES 3.2 (glad) initialized successfully"); // PC 端更新 DPI 缩放
#ifndef PLATFORM_SWITCH
updateContentScale();
#endif
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)));
return true; return true;
} }
void Window::deinitSDL() { void Window::deinitSDL() {
deinitCursors();
if (glContext_) { if (glContext_) {
SDL_GL_DeleteContext(glContext_); SDL_GL_DeleteContext(glContext_);
glContext_ = nullptr; glContext_ = nullptr;
@ -147,18 +196,24 @@ void Window::pollEvents() {
case SDL_WINDOWEVENT: case SDL_WINDOWEVENT:
switch (event.window.event) { switch (event.window.event) {
case SDL_WINDOWEVENT_RESIZED: case SDL_WINDOWEVENT_RESIZED:
case SDL_WINDOWEVENT_SIZE_CHANGED:
width_ = event.window.data1; width_ = event.window.data1;
height_ = event.window.data2; height_ = event.window.data2;
#ifndef PLATFORM_SWITCH
updateContentScale();
#endif
if (resizeCallback_) { if (resizeCallback_) {
resizeCallback_(width_, height_); resizeCallback_(width_, height_);
} }
break; break;
case SDL_WINDOWEVENT_FOCUS_GAINED: case SDL_WINDOWEVENT_FOCUS_GAINED:
focused_ = true;
if (focusCallback_) { if (focusCallback_) {
focusCallback_(true); focusCallback_(true);
} }
break; break;
case SDL_WINDOWEVENT_FOCUS_LOST: case SDL_WINDOWEVENT_FOCUS_LOST:
focused_ = false;
if (focusCallback_) { if (focusCallback_) {
focusCallback_(false); focusCallback_(false);
} }
@ -184,20 +239,50 @@ bool Window::shouldClose() const { return shouldClose_; }
void Window::setShouldClose(bool close) { shouldClose_ = close; } void Window::setShouldClose(bool close) { shouldClose_ = close; }
void Window::setTitle(const String & /*title*/) { void Window::setTitle(const String &title) {
// Switch 无窗口标题 #ifdef PLATFORM_PC
if (sdlWindow_) {
SDL_SetWindowTitle(sdlWindow_, title.c_str());
}
#else
(void)title;
#endif
} }
void Window::setSize(int /*width*/, int /*height*/) { void Window::setSize(int width, int height) {
// Switch 固定 1280x720 #ifdef PLATFORM_PC
if (sdlWindow_) {
SDL_SetWindowSize(sdlWindow_, width, height);
width_ = width;
height_ = height;
}
#else
(void)width;
(void)height;
#endif
} }
void Window::setPosition(int /*x*/, int /*y*/) { void Window::setPosition(int x, int y) {
// Switch 无窗口位置 #ifdef PLATFORM_PC
if (sdlWindow_) {
SDL_SetWindowPosition(sdlWindow_, x, y);
}
#else
(void)x;
(void)y;
#endif
} }
void Window::setFullscreen(bool /*fullscreen*/) { void Window::setFullscreen(bool fullscreen) {
// Switch 始终全屏 #ifdef PLATFORM_PC
if (sdlWindow_) {
Uint32 flags = fullscreen ? SDL_WINDOW_FULLSCREEN_DESKTOP : 0;
SDL_SetWindowFullscreen(sdlWindow_, flags);
fullscreen_ = fullscreen;
}
#else
(void)fullscreen;
#endif
} }
void Window::setVSync(bool enabled) { void Window::setVSync(bool enabled) {
@ -205,16 +290,135 @@ void Window::setVSync(bool enabled) {
SDL_GL_SetSwapInterval(enabled ? 1 : 0); SDL_GL_SetSwapInterval(enabled ? 1 : 0);
} }
void Window::setResizable(bool /*resizable*/) { void Window::setResizable(bool resizable) {
// Switch 不支持 #ifdef PLATFORM_PC
if (sdlWindow_) {
SDL_SetWindowResizable(sdlWindow_, resizable ? SDL_TRUE : SDL_FALSE);
}
#else
(void)resizable;
#endif
} }
void Window::setCursor(CursorShape /*shape*/) { Vec2 Window::getPosition() const {
// Switch 无鼠标光标 #ifdef PLATFORM_PC
if (sdlWindow_) {
int x, y;
SDL_GetWindowPosition(sdlWindow_, &x, &y);
return Vec2(static_cast<float>(x), static_cast<float>(y));
}
#endif
return Vec2::Zero();
}
float Window::getContentScaleX() const {
#ifdef PLATFORM_SWITCH
return 1.0f;
#else
return contentScaleX_;
#endif
}
float Window::getContentScaleY() const {
#ifdef PLATFORM_SWITCH
return 1.0f;
#else
return contentScaleY_;
#endif
}
Vec2 Window::getContentScale() const {
return Vec2(getContentScaleX(), getContentScaleY());
}
bool Window::isMinimized() const {
#ifdef PLATFORM_PC
if (sdlWindow_) {
Uint32 flags = SDL_GetWindowFlags(sdlWindow_);
return (flags & SDL_WINDOW_MINIMIZED) != 0;
}
#endif
return false;
}
bool Window::isMaximized() const {
#ifdef PLATFORM_PC
if (sdlWindow_) {
Uint32 flags = SDL_GetWindowFlags(sdlWindow_);
return (flags & SDL_WINDOW_MAXIMIZED) != 0;
}
#endif
return true;
}
void Window::initCursors() {
#ifdef PLATFORM_PC
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);
#endif
}
void Window::deinitCursors() {
#ifdef PLATFORM_PC
for (int i = 0; i < 9; ++i) {
if (sdlCursors_[i]) {
SDL_FreeCursor(sdlCursors_[i]);
sdlCursors_[i] = nullptr;
}
}
currentCursor_ = nullptr;
#endif
}
void Window::setCursor(CursorShape shape) {
#ifdef PLATFORM_PC
int index = static_cast<int>(shape);
if (index >= 0 && index < 9 && sdlCursors_[index]) {
SDL_SetCursor(sdlCursors_[index]);
currentCursor_ = sdlCursors_[index];
}
#else
(void)shape;
#endif
} }
void Window::resetCursor() { void Window::resetCursor() {
// Switch 无鼠标光标 #ifdef PLATFORM_PC
SDL_SetCursor(SDL_GetDefaultCursor());
currentCursor_ = nullptr;
#endif
}
void Window::setMouseVisible(bool visible) {
#ifdef PLATFORM_PC
SDL_ShowCursor(visible ? SDL_ENABLE : SDL_DISABLE);
#else
(void)visible;
#endif
}
void Window::updateContentScale() {
#ifndef PLATFORM_SWITCH
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;
}
}
}
#endif
} }
} // namespace extra2d } // namespace extra2d

959
docs/API_REFERENCE.md Normal file
View File

@ -0,0 +1,959 @@
# Extra2D API 参考文档
## 目录
- [核心系统](#核心系统)
- [应用管理](#应用管理)
- [场景系统](#场景系统)
- [节点系统](#节点系统)
- [输入系统](#输入系统)
- [资源管理](#资源管理)
- [动画系统](#动画系统)
- [音频系统](#音频系统)
- [文件系统](#文件系统)
---
## 核心系统
### 类型定义
```cpp
namespace extra2d {
// 基本类型
using Vec2 = glm::vec2;
using Vec3 = glm::vec3;
using Vec4 = glm::vec4;
using Mat3 = glm::mat3;
using Mat4 = glm::mat4;
// 智能指针
template<typename T>
using Ptr = std::shared_ptr<T>;
template<typename T, typename... Args>
Ptr<T> makePtr(Args&&... args) {
return std::make_shared<T>(std::forward<Args>(args)...);
}
} // namespace extra2d
```
### 颜色类
```cpp
struct Color {
float r, g, b, a;
Color(float r = 1.0f, float g = 1.0f, float b = 1.0f, float a = 1.0f);
// 预定义颜色
static Color White;
static Color Black;
static Color Red;
static Color Green;
static Color Blue;
static Color Yellow;
static Color Transparent;
};
```
### 矩形类
```cpp
struct Rect {
float x, y, width, height;
Rect(float x = 0, float y = 0, float w = 0, float h = 0);
bool contains(const Vec2& point) const;
bool intersects(const Rect& other) const;
float left() const { return x; }
float right() const { return x + width; }
float top() const { return y; }
float bottom() const { return y + height; }
Vec2 center() const { return Vec2(x + width/2, y + height/2); }
};
```
---
## 应用管理
### AppConfig
应用配置结构体。
```cpp
struct AppConfig {
String title = "Extra2D Application";
int width = 800;
int height = 600;
bool fullscreen = false;
bool resizable = true;
bool vsync = true;
int fpsLimit = 0; // 0 = 不限制
BackendType renderBackend = BackendType::OpenGL;
int msaaSamples = 0;
};
```
### Application
应用主类,单例模式。
```cpp
class Application {
public:
// 获取单例实例
static Application& instance();
// 初始化应用
bool init(const AppConfig& config);
// 运行主循环
void run();
// 退出应用
void quit();
// 进入场景
void enterScene(Ptr<Scene> scene);
void enterScene(Ptr<Scene> scene, Ptr<Transition> transition);
// 获取子系统
Input& input();
AudioEngine& audio();
ResourceManager& resources();
RenderBackend& renderer();
// 获取配置
const AppConfig& getConfig() const;
// 获取当前 FPS
float fps() const;
// Switch 特定:检测是否连接底座
bool isDocked() const;
};
```
**使用示例:**
```cpp
#include <extra2d/extra2d.h>
using namespace extra2d;
int main() {
// 初始化日志
Logger::init();
Logger::setLevel(LogLevel::Debug);
// 配置应用
AppConfig config;
config.title = "My Game";
config.width = 1280;
config.height = 720;
config.vsync = true;
// 初始化应用
auto& app = Application::instance();
if (!app.init(config)) {
E2D_LOG_ERROR("应用初始化失败!");
return -1;
}
// 进入场景
app.enterScene(makePtr<MyScene>());
// 运行主循环
app.run();
return 0;
}
```
---
## 场景系统
### Scene
场景类,作为游戏内容的容器。
```cpp
class Scene : public Node {
public:
// 构造函数
Scene();
// 场景生命周期回调
virtual void onEnter(); // 进入场景时调用
virtual void onExit(); // 退出场景时调用
virtual void onUpdate(float dt); // 每帧更新
virtual void onRender(RenderBackend& renderer); // 渲染
// 设置背景颜色
void setBackgroundColor(const Color& color);
// 空间索引
void setSpatialIndexingEnabled(bool enabled);
bool isSpatialIndexingEnabled() const;
// 查询碰撞
std::vector<std::pair<Node*, Node*>> queryCollisions();
// 获取空间管理器
SpatialManager& getSpatialManager();
};
```
### Transition
场景过渡动画基类。
```cpp
class Transition {
public:
explicit Transition(float duration);
virtual void update(float dt) = 0;
virtual void render(RenderBackend& renderer,
Ptr<Scene> fromScene,
Ptr<Scene> toScene) = 0;
bool isFinished() const;
};
// 内置过渡效果
class FadeTransition : public Transition {
public:
FadeTransition(float duration, const Color& color = Color::Black);
};
class SlideTransition : public Transition {
public:
SlideTransition(float duration, Direction direction);
};
```
---
## 节点系统
### Node
所有场景对象的基类。
```cpp
class Node {
public:
Node();
virtual ~Node();
// 变换
void setPosition(const Vec2& pos);
Vec2 getPosition() const;
void setRotation(float degrees);
float getRotation() const;
void setScale(const Vec2& scale);
Vec2 getScale() const;
void setAnchor(const Vec2& anchor);
Vec2 getAnchor() const;
// 层级
void addChild(Ptr<Node> child);
void removeChild(Ptr<Node> child);
void removeFromParent();
// 可见性
void setVisible(bool visible);
bool isVisible() const;
// 动作
void runAction(Ptr<Action> action);
void stopAllActions();
// 渲染
virtual void onRender(RenderBackend& renderer);
// 更新
virtual void onUpdate(float dt);
// 边界框(用于碰撞检测)
virtual Rect getBoundingBox() const;
// 空间索引
void setSpatialIndexed(bool indexed);
bool isSpatialIndexed() const;
};
```
### Sprite
精灵节点用于显示2D图像。
```cpp
class Sprite : public Node {
public:
// 创建方法
static Ptr<Sprite> create(Ptr<Texture> texture);
static Ptr<Sprite> create(const std::string& texturePath);
// 设置纹理
void setTexture(Ptr<Texture> texture);
Ptr<Texture> getTexture() const;
// 设置纹理矩形(用于精灵表)
void setTextureRect(const Rect& rect);
// 设置颜色调制
void setColor(const Color& color);
Color getColor() const;
// 翻转
void setFlippedX(bool flipped);
void setFlippedY(bool flipped);
};
```
### Text
文本节点。
```cpp
class Text : public Node {
public:
static Ptr<Text> create(const std::string& text = "");
// 文本内容
void setText(const std::string& text);
std::string getText() const;
// 字体
void setFont(Ptr<FontAtlas> font);
void setFontSize(int size);
// 颜色
void setTextColor(const Color& color);
Color getTextColor() const;
// 对齐
void setHorizontalAlignment(Alignment align);
void setVerticalAlignment(Alignment align);
};
```
---
## 输入系统
### Input
输入管理类。
```cpp
class Input {
public:
// 键盘
bool isKeyDown(int keyCode) const; // 按键是否按下
bool isKeyPressed(int keyCode) const; // 按键是否刚按下
bool isKeyReleased(int keyCode) const; // 按键是否刚释放
// 手柄按钮
bool isButtonDown(int button) const;
bool isButtonPressed(int button) const;
bool isButtonReleased(int button) const;
// 摇杆
Vec2 getLeftStick() const;
Vec2 getRightStick() const;
// 鼠标PC端
Vec2 getMousePosition() const;
bool isMouseDown(int button) const;
bool isMousePressed(int button) const;
// 触摸Switch/移动端)
bool isTouching() const;
Vec2 getTouchPosition() const;
int getTouchCount() const;
};
```
### 按键码 (Key)
```cpp
namespace Key {
// 字母
A, B, C, D, E, F, G, H, I, J, K, L, M,
N, O, P, Q, R, S, T, U, V, W, X, Y, Z,
// 数字
Num0, Num1, Num2, Num3, Num4,
Num5, Num6, Num7, Num8, Num9,
// 功能键
F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12,
// 方向键
Up, Down, Left, Right,
// 特殊键
Escape, Enter, Tab, Backspace, Space,
Insert, Delete, Home, End, PageUp, PageDown,
LeftShift, LeftControl, LeftAlt, LeftSuper,
RightShift, RightControl, RightAlt, RightSuper
}
```
**使用示例:**
```cpp
void MyScene::onUpdate(float dt) {
auto& input = Application::instance().input();
// 键盘输入
if (input.isKeyPressed(Key::Space)) {
// 空格键按下
jump();
}
if (input.isKeyDown(Key::Left)) {
// 左方向键按住
moveLeft();
}
// 手柄输入
if (input.isButtonPressed(GamepadButton::A)) {
// A 按钮按下
jump();
}
// 摇杆输入
Vec2 leftStick = input.getLeftStick();
move(leftStick.x * speed, leftStick.y * speed);
}
```
---
## 资源管理
### ResourceManager
资源管理类,负责加载和管理游戏资源。
```cpp
class ResourceManager {
public:
// 纹理
Ptr<Texture> loadTexture(const std::string& path);
Ptr<Texture> getTexture(const std::string& path);
void unloadTexture(const std::string& path);
// 字体
Ptr<FontAtlas> loadFont(const std::string& path, int size, bool useSDF = false);
Ptr<FontAtlas> loadFontWithFallbacks(const std::vector<std::string>& paths,
int size, bool useSDF = false);
// 音频
Ptr<Sound> loadSound(const std::string& path);
Ptr<Music> loadMusic(const std::string& path);
// 精灵表
Ptr<SpriteFrameCache> loadSpriteSheet(const std::string& path);
// 清理
void unloadAll();
void unloadUnused();
};
```
### Texture
纹理类。
```cpp
class Texture {
public:
// 获取尺寸
int getWidth() const;
int getHeight() const;
Vec2 getSize() const;
// 获取 OpenGL 纹理 ID
GLuint getTextureID() const;
};
```
### FontAtlas
字体图集类。
```cpp
class FontAtlas {
public:
// 获取字体信息
int getFontSize() const;
bool isSDF() const;
// 获取字符信息
const Glyph& getGlyph(char32_t charCode) const;
// 获取行高
float getLineHeight() const;
// 获取纹理
Ptr<Texture> getTexture() const;
};
```
**使用示例:**
```cpp
void MyScene::onEnter() {
auto& resources = Application::instance().resources();
// 加载纹理
auto playerTexture = resources.loadTexture("player.png");
auto enemyTexture = resources.loadTexture("enemies/slime.png");
// 加载字体
auto font = resources.loadFont("font.ttf", 24, true);
// 创建精灵
auto player = Sprite::create(playerTexture);
addChild(player);
// 创建文本
auto text = Text::create("Hello World");
text->setFont(font);
addChild(text);
}
```
---
## 动画系统
### Action
动作基类。
```cpp
class Action {
public:
virtual void update(float dt) = 0;
virtual bool isFinished() const = 0;
void reset();
};
```
### 基础动作
```cpp
// 移动
class MoveTo : public Action {
public:
MoveTo(float duration, const Vec2& position);
};
class MoveBy : public Action {
public:
MoveBy(float duration, const Vec2& delta);
};
// 缩放
class ScaleTo : public Action {
public:
ScaleTo(float duration, const Vec2& scale);
ScaleTo(float duration, float scale);
};
class ScaleBy : public Action {
public:
ScaleBy(float duration, const Vec2& scale);
ScaleBy(float duration, float scale);
};
// 旋转
class RotateTo : public Action {
public:
RotateTo(float duration, float degrees);
};
class RotateBy : public Action {
public:
RotateBy(float duration, float degrees);
};
// 淡入淡出
class FadeTo : public Action {
public:
FadeTo(float duration, float opacity);
};
class FadeIn : public Action {
public:
FadeIn(float duration);
};
class FadeOut : public Action {
public:
FadeOut(float duration);
};
// 延迟
class Delay : public Action {
public:
Delay(float duration);
};
// 回调
class CallFunc : public Action {
public:
CallFunc(std::function<void()> callback);
};
```
### 组合动作
```cpp
// 顺序执行
class Sequence : public Action {
public:
Sequence(std::vector<Ptr<Action>> actions);
template<typename... Args>
static Ptr<Sequence> create(Args&&... args) {
return makePtr<Sequence>(std::vector<Ptr<Action>>{std::forward<Args>(args)...});
}
};
// 同时执行
class Spawn : public Action {
public:
Spawn(std::vector<Ptr<Action>> actions);
};
// 重复
class Repeat : public Action {
public:
Repeat(Ptr<Action> action, int times = -1); // -1 = 无限重复
};
// 反向
class Reverse : public Action {
public:
Reverse(Ptr<Action> action);
};
```
### 缓动函数
```cpp
namespace Ease {
// 线性
float linear(float t);
// 二次
float inQuad(float t);
float outQuad(float t);
float inOutQuad(float t);
// 三次
float inCubic(float t);
float outCubic(float t);
float inOutCubic(float t);
// 弹性
float inElastic(float t);
float outElastic(float t);
// 弹跳
float inBounce(float t);
float outBounce(float t);
// 回退
float inBack(float t);
float outBack(float t);
}
```
**使用示例:**
```cpp
// 创建精灵
auto sprite = Sprite::create("player.png");
sprite->setPosition(Vec2(100, 100));
addChild(sprite);
// 简单动作
sprite->runAction(makePtr<MoveTo>(1.0f, Vec2(300, 200)));
// 组合动作
sprite->runAction(makePtr<Sequence>(
makePtr<ScaleTo>(0.5f, Vec2(1.5f, 1.5f)),
makePtr<Delay>(0.2f),
makePtr<ScaleTo>(0.5f, Vec2(1.0f, 1.0f)),
makePtr<CallFunc>([]() {
E2D_LOG_INFO("动画完成!");
})
));
// 重复动画
sprite->runAction(makePtr<Repeat>(
makePtr<Sequence>(
makePtr<RotateBy>(1.0f, 360.0f),
makePtr<Delay>(0.5f)
)
));
```
---
## 音频系统
### AudioEngine
音频引擎类。
```cpp
class AudioEngine {
public:
// 音量控制 (0.0 - 1.0)
void setMasterVolume(float volume);
float getMasterVolume() const;
void setBGMVolume(float volume);
void setSFXVolume(float volume);
// 播放控制
void pauseAll();
void resumeAll();
void stopAll();
};
```
### Sound
音效类(短音频,适合音效)。
```cpp
class Sound {
public:
void play(int loops = 0); // loops: 0=播放1次, -1=无限循环
void stop();
void pause();
void resume();
void setVolume(float volume);
bool isPlaying() const;
};
```
### Music
音乐类(长音频,适合背景音乐)。
```cpp
class Music {
public:
void play(int loops = -1);
void stop();
void pause();
void resume();
void setVolume(float volume);
bool isPlaying() const;
void setLoopPoints(double start, double end);
};
```
**使用示例:**
```cpp
void MyScene::onEnter() {
auto& audio = Application::instance().audio();
auto& resources = Application::instance().resources();
// 加载并播放背景音乐
auto bgm = resources.loadMusic("bgm/level1.ogg");
bgm->play(-1); // 无限循环
bgm->setVolume(0.7f);
// 加载音效
jumpSound_ = resources.loadSound("sfx/jump.wav");
coinSound_ = resources.loadSound("sfx/coin.wav");
}
void MyScene::jump() {
jumpSound_->play();
}
void MyScene::collectCoin() {
coinSound_->play();
}
```
---
## 文件系统
### FileSystem
跨平台文件系统工具类。
```cpp
class FileSystem {
public:
// 路径解析(自动处理平台差异)
// PC: "assets/font.ttf" -> "C:/.../assets/font.ttf"
// Switch: "assets/font.ttf" -> "romfs:/assets/font.ttf"
static std::string resolvePath(const std::string& relativePath);
// 获取资源根目录
static std::string getResourceRoot();
// 获取可执行文件目录
static std::string getExecutableDirectory();
// 获取当前工作目录
static std::string getCurrentWorkingDirectory();
// 路径组合
static std::string combinePath(const std::string& path1,
const std::string& path2);
// 文件/目录检查
static bool fileExists(const std::string& path);
static bool directoryExists(const std::string& path);
// 读取文件内容
static std::vector<uint8_t> readFile(const std::string& path);
static std::string readTextFile(const std::string& path);
// 写入文件
static bool writeFile(const std::string& path,
const std::vector<uint8_t>& data);
static bool writeTextFile(const std::string& path,
const std::string& content);
};
```
**使用示例:**
```cpp
// 解析资源路径(跨平台)
std::string fontPath = FileSystem::resolvePath("fonts/main.ttf");
// PC: "C:/.../assets/fonts/main.ttf"
// Switch: "romfs:/assets/fonts/main.ttf"
// 检查文件是否存在
if (FileSystem::fileExists(fontPath)) {
auto font = resources.loadFont(fontPath, 24);
}
// 读取配置文件
std::string configPath = FileSystem::resolvePath("config/game.json");
std::string jsonContent = FileSystem::readTextFile(configPath);
```
---
## 日志系统
### Logger
日志系统。
```cpp
enum class LogLevel {
Debug,
Info,
Warning,
Error
};
class Logger {
public:
static void init();
static void shutdown();
static void setLevel(LogLevel level);
// 日志宏
#define E2D_LOG_DEBUG(...) // 调试日志
#define E2D_LOG_INFO(...) // 信息日志
#define E2D_LOG_WARN(...) // 警告日志
#define E2D_LOG_ERROR(...) // 错误日志
};
```
**使用示例:**
```cpp
void MyClass::doSomething() {
E2D_LOG_DEBUG("进入函数 doSomething");
if (!loadData()) {
E2D_LOG_ERROR("加载数据失败!");
return;
}
E2D_LOG_INFO("成功加载 {} 条记录", recordCount);
}
```
---
## 平台兼容性
### 平台检测
```cpp
// 编译时检测
#ifdef PLATFORM_SWITCH
// Switch 代码
#elif defined(PLATFORM_PC)
// PC 代码
#ifdef PLATFORM_WINDOWS
// Windows 代码
#elif defined(PLATFORM_LINUX)
// Linux 代码
#elif defined(PLATFORM_MACOS)
// macOS 代码
#endif
#endif
// 运行时检测
namespace platform {
bool isSwitch();
bool isPC();
bool isWindows();
bool isLinux();
bool isMacOS();
const char* getPlatformName();
}
```
---
## 更多文档
- [Switch 构建指南](./SWITCH_BUILD_GUIDE.md)
- [PC 构建指南](./PC_BUILD_GUIDE.md)
- [迁移完成记录](./SWITCH_MIGRATION_COMPLETE.md)
---
**最后更新**: 2026年2月10日
**Extra2D 版本**: 3.1.0

225
docs/INDEX.md Normal file
View File

@ -0,0 +1,225 @@
# Extra2D 文档索引
欢迎来到 Extra2D 文档中心!这里包含了使用 Extra2D 游戏引擎所需的所有文档。
## 📚 快速导航
### 入门指南
| 文档 | 描述 |
|------|------|
| [README.md](../README.md) | 项目概述和快速开始 |
| [Switch 构建指南](./SWITCH_BUILD_GUIDE.md) | Nintendo Switch 平台构建教程 |
| [PC 构建指南](./PC_BUILD_GUIDE.md) | Windows/Linux/macOS 构建教程 |
### API 参考
| 文档 | 描述 |
|------|------|
| [API 参考文档](./API_REFERENCE.md) | 完整的 API 文档和示例 |
### 开发文档
| 文档 | 描述 |
|------|------|
| [迁移完成记录](./SWITCH_MIGRATION_COMPLETE.md) | 项目迁移历史记录 |
| [数据存储文档](./DataStore.md) | 数据持久化系统文档 |
---
## 🚀 快速开始
### 1. 选择平台
**Nintendo Switch:**
```bash
# 设置环境变量
$env:DEVKITPRO = "C:/devkitPro"
# 配置并构建
xmake f --plat=switch -a arm64
xmake
```
**Windows PC:**
```bash
# 设置环境变量
$env:VCPKG_ROOT = "C:\vcpkg"
# 配置并构建
xmake f --plat=windows -a x64
xmake
```
### 2. 运行示例
```bash
# Switch生成 .nro 文件)
./build/switch/hello_world.nro
# Windows
./build/windows/hello_world.exe
```
### 3. 开始开发
```cpp
#include <extra2d/extra2d.h>
using namespace extra2d;
int main() {
// 初始化
Logger::init();
AppConfig config;
config.title = "My Game";
config.width = 1280;
config.height = 720;
auto& app = Application::instance();
app.init(config);
// 创建场景
auto scene = makePtr<Scene>();
// 添加精灵
auto sprite = Sprite::create("player.png");
scene->addChild(sprite);
// 运行动画
sprite->runAction(makePtr<MoveTo>(1.0f, Vec2(300, 200)));
// 运行
app.enterScene(scene);
app.run();
return 0;
}
```
---
## 📖 核心概念
### 应用生命周期
```
main()
└── Application::init()
└── Scene::onEnter()
└── Node::onUpdate() [每帧]
└── Node::onRender() [每帧]
└── Scene::onExit()
```
### 场景图结构
```
Scene (场景)
├── Node (节点)
│ ├── Sprite (精灵)
│ ├── Text (文本)
│ └── CustomNode (自定义节点)
└── Node
└── ...
```
### 坐标系统
- **原点**: 左上角 (0, 0)
- **X轴**: 向右为正
- **Y轴**: 向下为正
- **单位**: 像素
---
## 🛠️ 平台差异
| 功能 | Switch | PC |
|------|--------|-----|
| 窗口 | 固定全屏 | 可调整大小 |
| 输入 | 手柄/触摸 | 键盘/鼠标/手柄 |
| 资源路径 | `romfs:/` | `./assets/` |
| 渲染 | OpenGL ES | OpenGL ES (Angle) |
---
## 💡 示例代码
### 基础示例
- [Hello World](../Extra2D/examples/hello_world/main.cpp) - 基础窗口和文本
- [Collision Demo](../Extra2D/examples/collision_demo/main.cpp) - 碰撞检测
- [Spatial Index Demo](../Extra2D/examples/spatial_index_demo/main.cpp) - 空间索引
### 常用模式
**场景切换:**
```cpp
auto newScene = makePtr<GameScene>();
auto transition = makePtr<FadeTransition>(0.5f);
app.enterScene(newScene, transition);
```
**输入处理:**
```cpp
void onUpdate(float dt) {
auto& input = app.input();
if (input.isKeyPressed(Key::Space)) {
jump();
}
if (input.isButtonPressed(GamepadButton::A)) {
jump();
}
}
```
**资源加载:**
```cpp
auto& resources = app.resources();
auto texture = resources.loadTexture("player.png");
auto font = resources.loadFont("font.ttf", 24);
auto sound = resources.loadSound("jump.wav");
```
---
## 📦 项目结构
```
Extra2D/
├── docs/ # 文档
├── Extra2D/
│ ├── include/extra2d/ # 头文件
│ ├── src/ # 源文件
│ └── examples/ # 示例程序
├── squirrel/ # Squirrel 脚本引擎
├── xmake/ # xmake 配置
│ ├── toolchains/ # 工具链定义
│ └── targets/ # 构建目标
├── xmake.lua # 主构建配置
└── README.md # 项目说明
```
---
## 🔗 相关链接
- **GitHub**: https://github.com/ChestnutYueyue/extra2d
- **Issues**: https://github.com/ChestnutYueyue/extra2d/issues
- **devkitPro**: https://devkitpro.org/
- **Switch 开发**: https://switchbrew.org/
---
## 📝 许可证
Extra2D 使用 [MIT 许可证](../LICENSE)。
---
**最后更新**: 2026年2月10日
**版本**: 3.1.0

262
docs/PC_BUILD_GUIDE.md Normal file
View File

@ -0,0 +1,262 @@
# Easy2D PC 端编译指南
本文档说明如何在 PC 端Windows/Linux/macOS编译和运行 Easy2D 引擎。
## 概述
Easy2D 现在支持多平台:
- **Nintendo Switch** (ARM64) - 原始平台
- **Windows** (x64) - 新增
- **Linux** (x64) - 新增
- **macOS** (x64/ARM64) - 新增
PC 端使用 OpenGL ES 3.2(通过 Angle 或 Mesa保持与 Switch 的代码兼容性。
## 前置条件
### Windows
1. **Visual Studio 2019/2022** - 安装 C++ 桌面开发工作负载
2. **xmake** - 构建系统 (https://xmake.io)
3. **SDL2** - 可以通过以下方式安装:
- vcpkg: `vcpkg install sdl2 sdl2-mixer`
- 手动下载https://www.libsdl.org/download-2.0.php
4. **Angle** (可选) - Google 的 GLES 实现
- 下载 Angle 二进制文件或从 Chromium 构建
- 放置到 `third_party/angle/` 目录
### Linux (Ubuntu/Debian)
```bash
# 安装依赖
sudo apt-get update
sudo apt-get install -y build-essential
sudo apt-get install -y libsdl2-dev libsdl2-mixer-dev
sudo apt-get install -y libgles2-mesa-dev libegl1-mesa-dev
# 安装 xmake
sudo add-apt-repository ppa:xmake-io/xmake
sudo apt-get update
sudo apt-get install xmake
```
### macOS
```bash
# 安装 Homebrew (如果还没有)
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# 安装依赖
brew install sdl2 sdl2_mixer
brew install angle # 或 mesa
# 安装 xmake
brew install xmake
```
## 编译步骤
### 1. 配置项目
```bash
# 进入项目目录
cd c:\Users\soulcoco\Desktop\Easy2D\Extra2D
# 配置 PC 平台构建
xmake config -p pc -m release
# 或使用环境变量
set E2D_PLATFORM=pc # Windows
export E2D_PLATFORM=pc # Linux/macOS
xmake config
```
### 2. 编译引擎库
```bash
# 编译 Easy2D 静态库
xmake build extra2d
```
### 3. 编译示例程序
```bash
# 编译 Hello World 示例
xmake build hello_world
# 编译所有示例
xmake build -a
```
### 4. 运行示例
```bash
# 运行 Hello World
xmake run hello_world
# 或手动运行
./build/windows/hello_world.exe # Windows
./build/linux/hello_world # Linux
./build/macos/hello_world # macOS
```
## 项目结构
```
Extra2D/
├── xmake.lua # 主构建配置
├── xmake/
│ ├── toolchains/
│ │ ├── switch.lua # Switch 工具链
│ │ └── pc.lua # PC 工具链 (新增)
│ └── targets/
│ ├── extra2d.lua # 引擎库配置
│ └── examples.lua # 示例程序配置
├── Extra2D/
│ ├── include/
│ │ └── extra2d/
│ │ └── platform/
│ │ ├── platform_compat.h # 跨平台兼容性 (新增)
│ │ ├── file_system.h # 文件系统工具 (新增)
│ │ ├── window.h # 窗口系统 (修改)
│ │ └── input.h # 输入系统 (修改)
│ └── src/
│ └── platform/
│ ├── window.cpp # (修改)
│ ├── input.cpp # (修改)
│ └── file_system.cpp # (新增)
└── examples/
└── hello_world/
└── main.cpp # (修改,支持多平台)
```
## 平台差异
### 窗口系统
| 功能 | Switch | PC |
|------|--------|-----|
| 窗口模式 | 始终全屏 (1280x720) | 可配置全屏/窗口 |
| 窗口大小 | 固定 | 可调整 |
| DPI 缩放 | 1.0 | 自动检测 |
| 鼠标光标 | 无 | 支持多种形状 |
### 输入系统
| 功能 | Switch | PC |
|------|--------|-----|
| 键盘 | 映射到手柄 | 原生支持 |
| 鼠标 | 映射到触摸 | 原生支持 |
| 手柄 | SDL GameController | SDL GameController |
| 触摸屏 | 原生 | 可选支持 |
### 文件系统
| 功能 | Switch | PC |
|------|--------|-----|
| 资源路径 | `romfs:/assets/` | `./assets/` 或 exe 目录 |
| 文件访问 | RomFS | 标准文件系统 |
## 开发指南
### 编写跨平台代码
```cpp
#include <extra2d/platform/platform_compat.h>
#include <extra2d/platform/file_system.h>
// 检查平台
if (platform::isSwitch()) {
// Switch 特定代码
} else if (platform::isPC()) {
// PC 特定代码
}
// 解析资源路径(自动处理平台差异)
std::string fontPath = FileSystem::resolvePath("assets/font.ttf");
// Switch: "romfs:/assets/font.ttf"
// PC: "./assets/font.ttf"
// 条件编译
#ifdef PLATFORM_SWITCH
// Switch 代码
#elif defined(PLATFORM_PC)
// PC 代码
#endif
```
### 输入处理
```cpp
// 跨平台输入(推荐)
auto& input = Application::instance().input();
// 键盘PC 原生Switch 映射到手柄)
if (input.isKeyPressed(Key::Space)) {
// 处理空格键
}
// 手柄(所有平台)
if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_A)) {
// 处理 A 按钮
}
// 鼠标PC 原生Switch 映射到触摸)
if (input.isMouseDown(MouseButton::Left)) {
// 处理鼠标左键
}
```
## 故障排除
### 找不到 SDL2
**Windows:**
```bash
# 设置 SDL2 路径
set SDL2_DIR=C:\SDL2-2.28.0
xmake config
```
**Linux:**
```bash
# 安装 SDL2 开发库
sudo apt-get install libsdl2-dev libsdl2-mixer-dev
```
### 找不到 GLES
**Windows (Angle):**
```bash
# 下载 Angle 并设置路径
set ANGLE_DIR=C:\angle
xmake config
```
**Linux (Mesa):**
```bash
# 安装 Mesa GLES
sudo apt-get install libgles2-mesa-dev libegl1-mesa-dev
```
### 编译错误
1. 确保使用 C++17 或更高版本
2. 检查 xmake 版本是否最新:`xmake --version`
3. 清理构建缓存:`xmake clean -a`
## 下一步
1. 尝试编译和运行 `hello_world` 示例
2. 阅读 `platform_compat.h` 了解平台 API
3. 使用 `FileSystem` 类处理跨平台文件路径
4. 参考示例代码编写跨平台应用
## 许可证
Easy2D 采用 MIT 许可证。详见 LICENSE 文件。
---
**最后更新**: 2026年2月10日
**Easy2D 版本**: 3.1.0

View File

@ -1,5 +0,0 @@
*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

View File

@ -1,8 +1,8 @@
-- ============================================== -- ==============================================
-- Extra2D for Nintendo Switch - Xmake Build Script -- Extra2D - Xmake Build Script
-- Purpose: Build Extra2D static library and Switch demo programs -- Purpose: Build Extra2D static library and demo programs
-- Platform: Nintendo Switch (ARM64) -- Platform: Nintendo Switch (ARM64) / PC (Windows/Linux/macOS)
-- Graphics: Desktop OpenGL 3.3+ via Mesa EGL -- Graphics: OpenGL ES 3.2 via Mesa/Angle
-- Audio: SDL2_mixer -- Audio: SDL2_mixer
-- ============================================== -- ==============================================
@ -12,15 +12,66 @@ set_languages("c++17")
set_encodings("utf-8") set_encodings("utf-8")
add_rules("mode.debug", "mode.release") add_rules("mode.debug", "mode.release")
-- ==============================================
-- 平台检测与配置 (必须在 includes 之前)
-- ==============================================
-- 检测目标平台 - 优先级:命令行 > 环境变量 > 默认值
local target_platform = "switch" -- 默认 Switch 平台
-- 方式1: 检查命令行传入的平台配置 (最高优先级)
local plat_config = get_config("plat")
if plat_config and plat_config ~= "" then
if plat_config == "windows" or plat_config == "linux" or plat_config == "macosx" then
target_platform = "pc"
elseif plat_config == "switch" then
target_platform = "switch"
end
-- 方式2: 通过环境变量检测 PC 平台
elseif os.getenv("E2D_PLATFORM") == "pc" then
target_platform = "pc"
-- 方式3: 检查 platform 配置
elseif has_config("platform") then
local platform_val = get_config("platform")
if platform_val == "pc" then
target_platform = "pc"
end
end
-- 设置默认平台和架构 (必须在 includes 之前调用)
if target_platform == "switch" then
set_config("plat", "switch")
set_config("arch", "arm64")
else
-- PC 平台:根据主机自动选择
if is_host("windows") then
set_config("plat", "windows")
set_config("arch", "x64")
elseif is_host("linux") then
set_config("plat", "linux")
elseif is_host("macosx") then
set_config("plat", "macosx")
end
end
print("Extra2D Build Configuration:")
print(" Platform: " .. target_platform)
print(" Mode: " .. (is_mode("debug") and "debug" or "release"))
-- ============================================== -- ==============================================
-- 包含子模块配置 -- 包含子模块配置
-- ============================================== -- ==============================================
-- 包含工具链定义 -- 包含工具链定义
includes("xmake/toolchains/switch.lua") includes("xmake/toolchains/switch.lua")
includes("xmake/toolchains/pc.lua")
-- 定义 Switch 工具链 -- 根据平台定义工具链
if target_platform == "switch" then
define_switch_toolchain() define_switch_toolchain()
else
define_pc_toolchain()
end
-- 包含目标定义 -- 包含目标定义
includes("xmake/targets/extra2d.lua") includes("xmake/targets/extra2d.lua")

View File

@ -1,37 +0,0 @@
-- ==============================================
-- 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,5 +1,6 @@
-- ============================================== -- ==============================================
-- Extra2D 示例程序构建目标 -- Extra2D 示例程序构建目标
-- 支持平台: Nintendo Switch, Windows, Linux, macOS
-- ============================================== -- ==============================================
-- 获取 devkitPro 路径 -- 获取 devkitPro 路径
@ -49,10 +50,10 @@ local function generate_nro_after_build(target_name, app_title, app_author, app_
end) end)
end end
-- 定义示例程序的通用配置 -- 定义 Switch 示例程序的通用配置
-- @param name 目标名称 -- @param name 目标名称
-- @param options 配置选项表 -- @param options 配置选项表
local function define_example_target(name, options) local function define_switch_example_target(name, options)
target(name) target(name)
set_kind("binary") set_kind("binary")
set_plat("switch") set_plat("switch")
@ -69,6 +70,11 @@ local function define_example_target(name, options)
-- 链接 extra2d 库 -- 链接 extra2d 库
add_deps("extra2d") add_deps("extra2d")
-- Windows 控制台应用程序(仅 PC 平台)
if is_plat("windows") then
add_ldflags("-mconsole", {force = true})
end
-- 可选:添加链接器标志 -- 可选:添加链接器标志
if options.ldflags then if options.ldflags then
add_ldflags(options.ldflags, {force = true}) add_ldflags(options.ldflags, {force = true})
@ -85,31 +91,121 @@ local function define_example_target(name, options)
target_end() target_end()
end end
-- 定义 PC 示例程序的通用配置
-- @param name 目标名称
-- @param options 配置选项表
local function define_pc_example_target(name, options)
target(name)
set_kind("binary")
set_toolchains("pc")
-- 设置输出目录
if is_host("windows") then
set_targetdir("build/windows")
elseif is_host("linux") then
set_targetdir("build/linux")
elseif is_host("macosx") then
set_targetdir("build/macos")
else
set_targetdir("build/pc")
end
-- 添加源文件
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
-- PC 端构建后复制资源文件和 DLL
after_build(function (target)
local target_file = target:targetfile()
local output_dir = path.directory(target_file)
local romfs_dir = options.romfs_dir or ("Extra2D/examples/" .. name .. "/romfs")
local romfs_absolute = path.absolute(romfs_dir)
-- 复制 vcpkg DLL 到输出目录
local vcpkg_root = os.getenv("VCPKG_ROOT")
if vcpkg_root then
local triplet = is_arch("x64") and "x64-windows" or "x86-windows"
local vcpkg_bin = path.join(vcpkg_root, "installed", triplet, "bin")
if os.isdir(vcpkg_bin) then
-- 复制 SDL2 相关 DLL
local dlls = {"SDL2.dll", "SDL2_mixer.dll", "ogg.dll", "vorbis.dll", "vorbisfile.dll", "wavpackdll.dll"}
for _, dll in ipairs(dlls) do
local dll_path = path.join(vcpkg_bin, dll)
if os.isfile(dll_path) then
os.cp(dll_path, output_dir)
end
end
print("Copied DLLs from: " .. vcpkg_bin)
end
end
-- 复制资源文件到输出目录
if os.isdir(romfs_absolute) then
local assets_dir = path.join(output_dir, "assets")
os.mkdir(assets_dir)
-- 复制 romfs 内容到 assets 目录
local romfs_assets = path.join(romfs_absolute, "assets")
if os.isdir(romfs_assets) then
print("Copying assets from: " .. romfs_assets .. " to " .. assets_dir)
os.cp(romfs_assets .. "/*", assets_dir)
end
print("Built " .. path.filename(target_file) .. " (with assets)")
else
print("Built " .. path.filename(target_file))
end
end)
target_end()
end
-- 定义所有示例程序目标 -- 定义所有示例程序目标
function define_example_targets() function define_example_targets()
-- 根据平台选择示例程序定义方式
if is_plat("switch") then
-- ============================================ -- ============================================
-- Switch 简单测试程序 -- Switch 示例程序
-- ============================================ -- ============================================
define_example_target("hello_world", { define_switch_example_target("hello_world", {
app_title = "Extra2D hello_world", app_title = "Extra2D hello_world",
app_author = "Extra2D hello_world", app_author = "Extra2D hello_world",
app_version = "1.0.0" app_version = "1.0.0"
}) })
-- ============================================ define_switch_example_target("spatial_index_demo", {
-- 引擎空间索引演示1000个节点
-- ============================================
define_example_target("spatial_index_demo", {
app_title = "Extra2D Spatial Index Demo", app_title = "Extra2D Spatial Index Demo",
app_version = "1.0.0", app_version = "1.0.0",
ldflags = "-Wl,-Map=build/switch/spatial_index_demo.map" ldflags = "-Wl,-Map=build/switch/spatial_index_demo.map"
}) })
-- ============================================ define_switch_example_target("collision_demo", {
-- 碰撞检测演示程序
-- ============================================
define_example_target("collision_demo", {
app_title = "Extra2D Collision Demo", app_title = "Extra2D Collision Demo",
app_version = "1.0.0" app_version = "1.0.0"
}) })
else
-- ============================================
-- PC 示例程序 (Windows/Linux/macOS)
-- ============================================
define_pc_example_target("hello_world", {
romfs_dir = "Extra2D/examples/hello_world/romfs"
})
define_pc_example_target("spatial_index_demo", {
romfs_dir = "Extra2D/examples/spatial_index_demo/romfs"
})
define_pc_example_target("collision_demo", {
romfs_dir = "Extra2D/examples/collision_demo/romfs"
})
end
end end

View File

@ -1,5 +1,6 @@
-- ============================================== -- ==============================================
-- Extra2D 引擎库构建目标 -- Extra2D 引擎库构建目标
-- 支持平台: Nintendo Switch, Windows, Linux, macOS
-- ============================================== -- ==============================================
-- 核心路径定义 -- 核心路径定义
@ -10,9 +11,6 @@ local INC_DIR = "Extra2D/include"
function define_extra2d_target() function define_extra2d_target()
target("extra2d") target("extra2d")
set_kind("static") set_kind("static")
set_plat("switch")
set_arch("arm64")
set_toolchains("switch")
set_basename(is_mode("debug") and "libeasy2dd" or "libeasy2d") set_basename(is_mode("debug") and "libeasy2dd" or "libeasy2d")
-- ============================================== -- ==============================================
@ -37,21 +35,28 @@ function define_extra2d_target()
-- 第三方头文件目录 -- 第三方头文件目录
add_includedirs("squirrel/include", {public = true}) add_includedirs("squirrel/include", {public = true})
-- 平台兼容性头文件路径
add_includedirs(path.join(INC_DIR, "extra2d/platform"), {public = true})
-- ==============================================
-- 平台特定配置
-- ==============================================
if is_plat("switch") then
-- ============================================== -- ==============================================
-- Nintendo Switch 平台配置 -- Nintendo Switch 平台配置
-- ============================================== -- ==============================================
set_plat("switch")
set_arch("arm64")
set_toolchains("switch")
-- devkitPro mesa 路径EGL + 桌面 OpenGL -- devkitPro mesa 路径EGL + OpenGL ES
local devkitPro = "C:/devkitPro" local devkitPro = "C:/devkitPro"
add_includedirs(path.join(devkitPro, "portlibs/switch/include"), {public = true}) add_includedirs(path.join(devkitPro, "portlibs/switch/include"), {public = true})
add_linkdirs(path.join(devkitPro, "portlibs/switch/lib")) add_linkdirs(path.join(devkitPro, "portlibs/switch/lib"))
-- 使用系统 GLES3.2 头文件 (位于 devkitPro/portlibs/switch/include)
-- 链接 EGL、OpenGL ES 3.0mesa和 SDL2 音频 -- 链接 EGL、OpenGL ES 3.0mesa和 SDL2 音频
-- 注意:链接顺序很重要!被依赖的库必须放在后面 -- 注意:链接顺序很重要!被依赖的库必须放在后面
-- 依赖链SDL2 -> EGL -> drm_nouveau
-- GLESv2 -> glapi -> drm_nouveau
add_syslinks("SDL2_mixer", "SDL2", add_syslinks("SDL2_mixer", "SDL2",
"opusfile", "opus", "vorbisidec", "ogg", "opusfile", "opus", "vorbisidec", "ogg",
"modplug", "mpg123", "FLAC", "modplug", "mpg123", "FLAC",
@ -60,41 +65,31 @@ function define_extra2d_target()
"glapi", "glapi",
"drm_nouveau", "drm_nouveau",
{public = true}) {public = true})
else
-- ==============================================
-- PC 平台配置 (Windows/Linux/macOS)
-- ==============================================
set_toolchains("pc")
-- 注意pfd (portable-file-dialogs) 暂时禁用,需要进一步修复 -- PC 平台使用标准 OpenGL (通过 MinGW)
-- add_files(path.join(INC_DIR, "pfd/pfd_switch.cpp")) -- 依赖库在 pc.lua 工具链中配置 (使用 vcpkg)
add_syslinks("SDL2_mixer", "SDL2", "opengl32", {public = true})
-- 添加 Switch 兼容性头文件路径 end
add_includedirs(path.join(INC_DIR, "extra2d/platform"), {public = true})
-- ============================================== -- ==============================================
-- 编译器配置 -- 编译器配置
-- ============================================== -- ==============================================
-- Switch 特定编译标志 -- ==============================================
-- 注意Squirrel 脚本绑定使用 dynamic_cast需要 RTTI 支持 -- 编译器标志 (MinGW GCC)
-- add_cxflags("-fno-rtti", {force = true}) -- ==============================================
add_cxflags("-Wall", "-Wextra", {force = true})
add_cxflags("-Wno-unused-variable", "-Wno-unused-function", {force = true}) add_cxflags("-Wno-unused-variable", "-Wno-unused-function", {force = true})
add_cxflags("-Wno-unused-parameter", {force = true})
-- Squirrel 第三方库警告抑制 -- Squirrel 第三方库警告抑制
add_cxflags("-Wno-deprecated-copy", "-Wno-strict-aliasing", "-Wno-implicit-fallthrough", "-Wno-class-memaccess", {force = true}) 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 if is_mode("debug") then
add_defines("E2D_DEBUG", "_DEBUG", {public = true}) add_defines("E2D_DEBUG", "_DEBUG", {public = true})
@ -103,5 +98,12 @@ function define_extra2d_target()
add_defines("NDEBUG", {public = true}) add_defines("NDEBUG", {public = true})
add_cxxflags("-O2", {force = true}) add_cxxflags("-O2", {force = true})
end end
-- ==============================================
-- 头文件安装配置
-- ==============================================
add_headerfiles(path.join(INC_DIR, "extra2d/**.h"), {prefixdir = "extra2d"})
add_headerfiles(path.join(INC_DIR, "stb/**.h"), {prefixdir = "stb"})
add_headerfiles(path.join(INC_DIR, "simpleini/**.h"), {prefixdir = "simpleini"})
target_end() target_end()
end end

150
xmake/toolchains/pc.lua Normal file
View File

@ -0,0 +1,150 @@
-- ==============================================
-- PC 平台工具链定义 (Windows/Linux/macOS)
-- ==============================================
function define_pc_toolchain()
toolchain("pc")
set_kind("standalone")
set_description("PC Platform Toolchain (Windows/Linux/macOS)")
on_load(function (toolchain)
-- ==============================================
-- 平台检测与配置
-- ==============================================
if is_host("windows") then
-- Windows: 使用 MinGW
toolchain:set("toolset", "cc", "gcc")
toolchain:set("toolset", "cxx", "g++")
toolchain:set("toolset", "ld", "g++")
toolchain:set("toolset", "ar", "ar")
else
-- Linux/macOS: 使用 GCC/Clang
toolchain:set("toolset", "cc", "gcc")
toolchain:set("toolset", "cxx", "g++")
toolchain:set("toolset", "ld", "g++")
toolchain:set("toolset", "ar", "ar")
end
-- ==============================================
-- PC 平台宏定义
-- ==============================================
toolchain:add("defines", "__PC__")
if is_host("windows") then
toolchain:add("defines", "_WIN32", "NOMINMAX", "WIN32_LEAN_AND_MEAN")
elseif is_host("linux") then
toolchain:add("defines", "__linux__")
elseif is_host("macosx") then
toolchain:add("defines", "__APPLE__", "__MACH__")
end
-- SimpleIni 配置
toolchain:add("defines", "SI_NO_CONVERSION")
-- ==============================================
-- OpenGL 配置
-- ==============================================
if is_host("windows") then
-- Windows: 使用标准 OpenGL
toolchain:add("links", "opengl32")
else
-- Linux/macOS: 使用 Mesa OpenGL
toolchain:add("links", "GL")
end
-- ==============================================
-- vcpkg 依赖配置
-- ==============================================
-- 获取 vcpkg 路径
local vcpkg_root = os.getenv("VCPKG_ROOT")
if vcpkg_root then
local triplet = is_arch("x64") and "x64-windows" or "x86-windows"
local vcpkg_installed = path.join(vcpkg_root, "installed", triplet)
if os.isdir(vcpkg_installed) then
-- 添加头文件路径
toolchain:add("includedirs", path.join(vcpkg_installed, "include"))
toolchain:add("includedirs", path.join(vcpkg_installed, "include", "SDL2"))
-- 添加库路径
toolchain:add("linkdirs", path.join(vcpkg_installed, "lib"))
print("vcpkg packages: " .. vcpkg_installed)
end
end
-- ==============================================
-- 链接库
-- ==============================================
-- SDL2 及其扩展
toolchain:add("links", "SDL2_mixer", "SDL2")
-- 音频编解码库 (vcpkg 中可用的)
toolchain:add("links", "ogg")
-- OpenGL (Windows 使用标准 OpenGL)
toolchain:add("links", "opengl32")
-- 系统库
if is_host("windows") then
toolchain:add("syslinks", "gdi32", "user32", "shell32", "winmm", "imm32", "ole32", "oleaut32", "version", "uuid", "advapi32", "setupapi")
else
toolchain:add("syslinks", "m", "dl", "pthread")
if is_host("linux") then
toolchain:add("syslinks", "X11", "Xext")
end
end
-- ==============================================
-- 编译器标志 (MinGW GCC)
-- ==============================================
toolchain:add("cxflags", "-Wall", "-Wextra", {force = true})
toolchain:add("cxflags", "-Wno-unused-parameter", "-Wno-unused-variable", {force = true})
-- Windows 控制台应用程序
toolchain:add("ldflags", "-mconsole", {force = true})
if is_mode("debug") then
toolchain:add("defines", "E2D_DEBUG", "_DEBUG")
toolchain:add("cxflags", "-O0", "-g", {force = true})
else
toolchain:add("defines", "NDEBUG")
toolchain:add("cxflags", "-O2", {force = true})
end
end)
end
-- 获取 PC 平台包含路径
function get_pc_includedirs()
local dirs = {}
-- Angle
local angle_dir = os.getenv("ANGLE_DIR") or "third_party/angle"
if os.isdir(angle_dir) then
table.insert(dirs, path.join(angle_dir, "include"))
end
return dirs
end
-- 获取 PC 平台库路径
function get_pc_linkdirs()
local dirs = {}
-- Angle
local angle_dir = os.getenv("ANGLE_DIR") or "third_party/angle"
if os.isdir(angle_dir) then
table.insert(dirs, path.join(angle_dir, "lib"))
end
return dirs
end
-- 获取 PC 平台系统链接库
function get_pc_syslinks()
return {
"SDL2_mixer", "SDL2",
"opengl32"
}
end

View File

@ -21,8 +21,8 @@ function define_switch_toolchain()
-- 架构标志 -- 架构标志
local arch_flags = "-march=armv8-a+crc+crypto -mtune=cortex-a57 -mtp=soft -fPIE" local arch_flags = "-march=armv8-a+crc+crypto -mtune=cortex-a57 -mtp=soft -fPIE"
add_cxflags(arch_flags) add_cxflags(arch_flags)
-- 使用修复后的 switch_fix.specs 文件(使用 Windows 路径) -- 使用 devkitPro 提供的 switch.specs 文件
add_ldflags("-specs=switch_fix.specs", "-g", arch_flags) add_ldflags("-specs=" .. path.join(devkitPro, "libnx/switch.specs"), "-g", arch_flags)
-- 定义 Switch 平台宏 -- 定义 Switch 平台宏
add_defines("__SWITCH__", "__NX__", "MA_SWITCH", "PFD_SWITCH") add_defines("__SWITCH__", "__NX__", "MA_SWITCH", "PFD_SWITCH")