diff --git a/.trae/documents/Extra2D 构建系统文档.md b/.trae/documents/Extra2D 构建系统文档.md new file mode 100644 index 0000000..3e3a801 --- /dev/null +++ b/.trae/documents/Extra2D 构建系统文档.md @@ -0,0 +1,327 @@ +# Extra2D 构建系统文档 + +## 概述 + +Extra2D 使用 **Xmake** 作为构建系统,支持 **MinGW (Windows)** 和 **Nintendo Switch** 两个平台。 + +## 项目结构 + +``` +Extra2D/ +├── xmake.lua # 主构建脚本 +├── xmake/ +│ ├── engine.lua # 引擎库定义 +│ └── toolchains/ +│ └── switch.lua # Switch 工具链定义 +├── Extra2D/ +│ ├── src/ # 引擎源码 +│ └── include/ # 引擎头文件 +├── squirrel/ # Squirrel 脚本引擎 +└── examples/ # 示例程序 + ├── hello_world/ + ├── collision_demo/ + ├── push_box/ + └── spatial_index_demo/ +``` + +## 环境准备 + +### MinGW (Windows) 平台 + +1. **安装 MinGW-w64** + - 下载地址: https://www.mingw-w64.org/downloads/ + - 或使用 MSYS2: `pacman -S mingw-w64-x86_64-toolchain` + +2. **安装 Xmake** + - 下载地址: https://xmake.io/#/zh-cn/guide/installation + +3. **安装依赖包** + ```bash + xmake require -y + ``` + +### Nintendo Switch 平台 + +1. **安装 devkitPro** + - 下载地址: https://devkitpro.org/wiki/Getting_Started + - Windows 安装程序会自动设置环境变量 + +2. **设置环境变量** + ```powershell + # PowerShell + $env:DEVKITPRO="C:/devkitPro" + + # 或永久设置(系统属性 -> 环境变量) + [Environment]::SetEnvironmentVariable("DEVKITPRO", "C:/devkitPro", "User") + ``` + +3. **在 MSYS2 中安装 Switch 库** + ```bash + # 打开 MSYS2 (devkitPro 提供的) + pacman -S switch-sdl2 switch-sdl2_mixer switch-glm + + # 或安装所有 Switch 开发库 + pacman -S $(pacman -Slq dkp-libs | grep switch-) + ``` + +4. **验证安装** + ```bash + ls $DEVKITPRO/portlibs/switch/include/SDL2 + ls $DEVKITPRO/portlibs/switch/include/glm + ``` + +## 主构建脚本 (xmake.lua) + +### 项目元信息 +- **项目名称**: Extra2D +- **版本**: 3.1.0 +- **许可证**: MIT +- **语言标准**: C++17 +- **编码**: UTF-8 + +### 构建选项 + +| 选项 | 默认值 | 描述 | +|------|--------|------| +| `examples` | true | 构建示例程序 | +| `debug_logs` | false | 启用调试日志 | + +### 平台检测逻辑 + +1. 获取主机平台: `os.host()` +2. 获取目标平台: `get_config("plat")` 或主机平台 +3. 平台回退: 如果不支持,Windows 回退到 `mingw` +4. 设置平台: `set_plat(target_plat)` +5. 设置架构: + - Switch: `arm64` + - MinGW: `x86_64` + +### 依赖包 (MinGW 平台) + +```lua +add_requires("glm", "libsdl2", "libsdl2_mixer") +``` + +## 引擎库定义 (xmake/engine.lua) + +### 目标: extra2d +- **类型**: 静态库 (`static`) +- **源文件**: + - `Extra2D/src/**.cpp` + - `Extra2D/src/glad/glad.c` + - `squirrel/squirrel/*.cpp` + - `squirrel/sqstdlib/*.cpp` + +### 头文件路径 +- `Extra2D/include` (public) +- `squirrel/include` (public) +- `Extra2D/include/extra2d/platform` (public) + +### 平台配置 + +#### Switch 平台 +```lua +add_includedirs(devkitPro .. "/portlibs/switch/include") +add_linkdirs(devkitPro .. "/portlibs/switch/lib") +add_syslinks("SDL2_mixer", "SDL2", "opusfile", "opus", ...) +``` + +#### MinGW 平台 +```lua +add_packages("glm", "libsdl2", "libsdl2_mixer") +add_syslinks("opengl32", "glu32", "winmm", "imm32", "version", "setupapi") +``` + +### 编译器标志 +- `-Wall`, `-Wextra` +- `-Wno-unused-variable`, `-Wno-unused-function` +- `-Wno-deprecated-copy`, `-Wno-class-memaccess` + +### 构建模式 +- **Debug**: `-O0`, `-g`, 定义 `E2D_DEBUG`, `_DEBUG` +- **Release**: `-O2`, 定义 `NDEBUG` + +## Switch 工具链 (xmake/toolchains/switch.lua) + +### 工具链: switch +- **类型**: standalone +- **描述**: Nintendo Switch devkitA64 工具链 + +### 工具路径 +- **CC**: `aarch64-none-elf-gcc.exe` +- **CXX**: `aarch64-none-elf-g++.exe` +- **LD**: `aarch64-none-elf-g++.exe` +- **AR**: `aarch64-none-elf-gcc-ar.exe` + +### 架构标志 +``` +-march=armv8-a+crc+crypto -mtune=cortex-a57 -mtp=soft -fPIE +``` + +### 链接标志 +``` +-specs=switch.specs -g +``` + +### 预定义宏 +- `__SWITCH__` +- `__NX__` +- `MA_SWITCH` +- `PFD_SWITCH` + +### 系统库 +- `nx` (libnx) +- `m` (math) + +## 示例程序构建脚本 + +### 通用结构 + +```lua +-- 使用与主项目相同的平台配置 +if is_plat("switch") then + -- Switch 平台配置 +elseif is_plat("mingw") then + -- MinGW 平台配置 +end +``` + +### Switch 平台配置 +- 设置平台: `set_plat("switch")` +- 设置架构: `set_arch("arm64")` +- 设置工具链: `set_toolchains("switch")` +- 设置输出目录: `set_targetdir("../../build/examples/xxx")` +- 构建后生成 NRO 文件 + +### MinGW 平台配置 +- 设置平台: `set_plat("mingw")` +- 设置架构: `set_arch("x86_64")` +- 设置输出目录: `set_targetdir("../../build/examples/xxx")` +- 链接标志: `-mwindows` +- 构建后复制资源文件 + +## 构建命令 + +### 配置项目 + +```bash +# 默认配置 (MinGW) +xmake f -c + +# 指定平台 (使用 -p 参数) +xmake f -c -p mingw +xmake f -c -p switch + +# 指定 MinGW 路径(如果不在默认位置) +xmake f -c -p mingw --mingw=C:\mingw + +# 禁用示例 +xmake f --examples=n + +# 启用调试日志 +xmake f --debug_logs=y +``` + +### 安装依赖 (MinGW) +```bash +xmake require -y +``` + +### 构建项目 +```bash +# 构建所有目标 +xmake + +# 构建特定目标 +xmake -r extra2d +xmake -r push_box + +# 并行构建 +xmake -j4 +``` + +### 运行程序 +```bash +# 运行示例 +xmake run push_box +xmake run hello_world +``` + +### 清理构建 +```bash +xmake clean +xmake f -c # 重新配置 +``` + +## 输出目录结构 + +``` +build/ +├── examples/ +│ ├── hello_world/ +│ │ ├── hello_world.exe # MinGW +│ │ ├── hello_world.nro # Switch +│ │ └── assets/ # 资源文件 +│ ├── push_box/ +│ ├── collision_demo/ +│ └── spatial_index_demo/ +└── ... +``` + +## 关键设计决策 + +### 1. 平台检测 +- 使用 `is_plat()` 而不是手动检测,确保与主项目一致 +- 示例脚本继承主项目的平台配置 + +### 2. 资源处理 +- **Switch**: 使用 romfs 嵌入 NRO 文件 +- **MinGW**: 构建后复制到输出目录 + +### 3. 依赖管理 +- **MinGW**: 使用 Xmake 包管理器 (`add_requires`) +- **Switch**: 使用 devkitPro 提供的库 + +### 4. 工具链隔离 +- Switch 工具链定义在单独文件中 +- 通过 `set_toolchains("switch")` 切换 + +## 常见问题 + +### 1. 依赖包找不到 +```bash +xmake repo -u +xmake require -y +``` + +### 2. Switch 工具链找不到 +- 确保 DEVKITPRO 环境变量设置正确 +- 默认路径: `C:/devkitPro` + +### 3. 平台配置不匹配 +- 使用 `xmake show` 查看当前配置 +- 使用 `xmake f -c` 重新配置 + +### 4. MinGW 路径问题 +如果 MinGW 安装在非默认位置,使用 `--mingw` 参数指定: +```bash +xmake f -c -p mingw --mingw=D:\Tools\mingw64 +``` + +### 5. Switch 库找不到 +确保在 MSYS2 中安装了必要的库: +```bash +pacman -S switch-sdl2 switch-sdl2_mixer switch-glm +``` + +## 扩展指南 + +### 添加新示例 +1. 在 `examples/` 下创建新目录 +2. 创建 `xmake.lua` 构建脚本 +3. 在 `xmake.lua` 中添加 `includes("examples/new_example")` + +### 添加新平台 +1. 在 `xmake/toolchains/` 下创建工具链定义 +2. 在 `xmake.lua` 中添加平台检测逻辑 +3. 在 `xmake/engine.lua` 中添加平台配置 \ No newline at end of file diff --git a/README.md b/README.md index 4828d46..c3285b0 100644 --- a/README.md +++ b/README.md @@ -111,10 +111,10 @@ mindmap | 组件 | 要求 | |:----:|:-----| -| 开发环境 | devkitPro + devkitA64 | +| 开发环境 | devkitPro + devkitA64 (Switch) / MinGW-w64 (Windows) | | C++ 标准 | C++17 | | 构建工具 | xmake | -| 目标平台 | Nintendo Switch | +| 目标平台 | Nintendo Switch / Windows (MinGW) | ### 安装 devkitPro @@ -129,6 +129,8 @@ pacman -S switch-dev switch-portlibs ### 构建项目 +#### Switch 平台 + ```bash # 克隆仓库 git clone https://github.com/ChestnutYueyue/extra2d.git @@ -144,6 +146,26 @@ xmake xmake -g examples ``` +#### Windows (MinGW) 平台 + +```bash +# 克隆仓库 +git clone https://github.com/ChestnutYueyue/extra2d.git +cd extra2d + +# 配置 MinGW 平台构建 +xmake f -p mingw --mode=release + +# 安装依赖 +xmake require -y + +# 构建引擎和示例 +xmake + +# 运行示例 +xmake run hello_world +``` + ### 生成 NSP 可运行文件 ```bash @@ -226,31 +248,57 @@ int main() Extra2D/ ├── 📁 Extra2D/ # 引擎核心代码 │ ├── 📁 include/ # 头文件 -│ │ └── 📁 extra2d/ # 引擎头文件 -│ │ ├── extra2d.h # 主头文件 -│ │ ├── app/ # 应用管理 -│ │ ├── action/ # 动作系统 -│ │ ├── animation/ # 动画系统 -│ │ ├── audio/ # 音频系统 -│ │ ├── core/ # 核心类型 -│ │ ├── effects/ # 特效系统 -│ │ ├── event/ # 事件系统 -│ │ ├── graphics/ # 图形渲染 -│ │ ├── platform/ # 平台抽象 -│ │ ├── resource/ # 资源管理 -│ │ ├── scene/ # 场景系统 -│ │ ├── script/ # 脚本系统 -│ │ ├── spatial/ # 空间索引 -│ │ ├── ui/ # UI 系统 -│ │ └── utils/ # 工具库 -│ ├── 📁 src/ # 源文件 -│ └── 📁 examples/ # 示例程序 -│ ├── push_box/ # 推箱子游戏 -│ └── switch_simple_test/ # 简单测试 -├── 📁 squirrel/ # Squirrel 脚本引擎 +│ │ ├── 📁 extra2d/ # 引擎头文件 +│ │ │ ├── extra2d.h # 主头文件 +│ │ │ ├── app/ # 应用管理 +│ │ │ ├── action/ # 动作系统 +│ │ │ ├── animation/ # 动画系统 +│ │ │ ├── audio/ # 音频系统 +│ │ │ ├── core/ # 核心类型 +│ │ │ ├── effects/ # 特效系统 +│ │ │ ├── event/ # 事件系统 +│ │ │ ├── graphics/ # 图形渲染 +│ │ │ ├── platform/ # 平台抽象 +│ │ │ ├── resource/ # 资源管理 +│ │ │ ├── scene/ # 场景系统 +│ │ │ ├── script/ # 脚本系统 +│ │ │ ├── spatial/ # 空间索引 +│ │ │ ├── ui/ # UI 系统 +│ │ │ └── utils/ # 工具库 +│ │ ├── 📁 glad/ # OpenGL Loader +│ │ ├── 📁 json/ # JSON 库 +│ │ ├── 📁 simpleini/ # INI 配置文件库 +│ │ └── 📁 stb/ # STB 图像库 +│ └── 📁 src/ # 源文件 +│ ├── action/ # 动作系统实现 +│ ├── animation/ # 动画系统实现 +│ ├── app/ # 应用管理实现 +│ ├── audio/ # 音频系统实现 +│ ├── core/ # 核心类型实现 +│ ├── effects/ # 特效系统实现 +│ ├── event/ # 事件系统实现 +│ ├── glad/ # GLAD 实现 +│ ├── graphics/ # 图形渲染实现 +│ ├── platform/ # 平台抽象实现 +│ ├── resource/ # 资源管理实现 +│ ├── scene/ # 场景系统实现 +│ ├── script/ # 脚本系统实现 +│ ├── spatial/ # 空间索引实现 +│ ├── ui/ # UI 系统实现 +│ └── utils/ # 工具库实现 +├── 📁 docs/ # 文档 +│ ├── 📁 API_Tutorial/ # API 教程 +│ └── Extra2D 构建系统文档.md # 构建系统文档 +├── 📁 examples/ # 示例程序 +│ ├── hello_world/ # Hello World 示例 +│ ├── collision_demo/ # 碰撞检测示例 +│ ├── push_box/ # 推箱子游戏 +│ └── spatial_index_demo/ # 空间索引示例 ├── 📁 logo/ # Logo 资源 -├── 📄 xmake.lua # xmake 构建配置 -├── 📄 SWITCH_BUILD_GUIDE.md # Switch 构建详细指南 +├── 📁 squirrel/ # Squirrel 脚本引擎 +├── � xmake/ # Xmake 构建配置 +│ └── toolchains/ # 工具链定义 +├── 📄 xmake.lua # 主构建配置 ├── 📄 LICENSE # MIT 许可证 └── 📄 README.md # 本文件 ``` @@ -380,8 +428,18 @@ sound->setVolume(0.8f); ## 📖 相关文档 -- [Switch 构建指南](./SWITCH_BUILD_GUIDE.md) - 详细的 Switch 平台构建教程 -- [迁移完成记录](./SWITCH_MIGRATION_COMPLETE.md) - 项目迁移历史记录 +- [📚 API 教程](./docs/API_Tutorial/01_Quick_Start.md) - 完整的 API 使用教程 + - [01. 快速开始](./docs/API_Tutorial/01_Quick_Start.md) + - [02. 场景系统](./docs/API_Tutorial/02_Scene_System.md) + - [03. 节点系统](./docs/API_Tutorial/03_Node_System.md) + - [04. 资源管理](./docs/API_Tutorial/04_Resource_Management.md) + - [05. 输入处理](./docs/API_Tutorial/05_Input_Handling.md) + - [06. 碰撞检测](./docs/API_Tutorial/06_Collision_Detection.md) + - [07. UI 系统](./docs/API_Tutorial/07_UI_System.md) + - [08. 音频系统](./docs/API_Tutorial/08_Audio_System.md) +- [🔧 构建系统文档](./docs/Extra2D%20构建系统文档.md) - 详细的构建系统说明 +- [🎮 Switch 构建指南](./SWITCH_BUILD_GUIDE.md) - Switch 平台构建教程 +- [📝 迁移完成记录](./SWITCH_MIGRATION_COMPLETE.md) - 项目迁移历史记录 --- diff --git a/docs/API_Tutorial/01_Quick_Start.md b/docs/API_Tutorial/01_Quick_Start.md new file mode 100644 index 0000000..9490a63 --- /dev/null +++ b/docs/API_Tutorial/01_Quick_Start.md @@ -0,0 +1,68 @@ +# Extra2D API 教程 - 01. 快速开始 + +## 简介 + +Extra2D 是一个跨平台的 2D 游戏引擎,支持 Windows (MinGW) 和 Nintendo Switch 平台。 + +## 最小示例 + +```cpp +#include + +using namespace extra2d; + +int main(int argc, char **argv) { + // 1. 初始化日志系统 + Logger::init(); + Logger::setLevel(LogLevel::Debug); + + // 2. 获取应用实例 + auto &app = Application::instance(); + + // 3. 配置应用 + AppConfig config; + config.title = "My Game"; + config.width = 1280; + config.height = 720; + config.vsync = true; + config.fpsLimit = 60; + + // 4. 初始化应用 + if (!app.init(config)) { + E2D_LOG_ERROR("应用初始化失败!"); + return -1; + } + + // 5. 进入场景 + app.enterScene(makePtr()); + + // 6. 运行应用 + app.run(); + + return 0; +} +``` + +## 核心概念 + +### 应用生命周期 + +``` +Logger::init() → Application::init() → enterScene() → run() → 退出 +``` + +### 场景生命周期 + +``` +onEnter() → onUpdate(dt) → onRender() → onExit() +``` + +## 下一步 + +- [02. 场景系统](02_Scene_System.md) +- [03. 节点系统](03_Node_System.md) +- [04. 资源管理](04_Resource_Management.md) +- [05. 输入处理](05_Input_Handling.md) +- [06. 碰撞检测](06_Collision_Detection.md) +- [07. UI 系统](07_UI_System.md) +- [08. 音频系统](08_Audio_System.md) diff --git a/docs/API_Tutorial/02_Scene_System.md b/docs/API_Tutorial/02_Scene_System.md new file mode 100644 index 0000000..d885e14 --- /dev/null +++ b/docs/API_Tutorial/02_Scene_System.md @@ -0,0 +1,171 @@ +# Extra2D API 教程 - 02. 场景系统 + +## 场景基础 + +场景(Scene)是游戏的基本组织单位,负责管理节点和渲染。 + +### 创建场景 + +```cpp +#include + +using namespace extra2d; + +class MyScene : public Scene { +public: + // 场景进入时调用 + void onEnter() override { + // 必须先调用父类的 onEnter() + Scene::onEnter(); + + // 设置背景颜色 + setBackgroundColor(Color(0.1f, 0.1f, 0.3f, 1.0f)); + + E2D_LOG_INFO("场景已进入"); + } + + // 每帧更新时调用 + void onUpdate(float dt) override { + Scene::onUpdate(dt); + // dt 是时间间隔(秒) + } + + // 渲染时调用 + void onRender(RenderBackend &renderer) override { + Scene::onRender(renderer); + // 绘制自定义内容 + } + + // 场景退出时调用 + void onExit() override { + // 清理资源 + Scene::onExit(); + } +}; +``` + +### 重要提示 + +**必须调用 `Scene::onEnter()`**: +```cpp +void onEnter() override { + Scene::onEnter(); // 必须调用! + // 你的初始化代码 +} +``` + +如果不调用,会导致: +- `running_` 状态未设置 +- 子节点无法正确注册到空间索引 +- 碰撞检测失效 + +## 场景管理 + +### 进入场景 + +```cpp +// 进入新场景 +app.enterScene(makePtr()); + +// 替换当前场景(带过渡效果) +app.scenes().replaceScene( + makePtr(), + TransitionType::Fade, // 淡入淡出 + 0.25f // 过渡时间(秒) +); +``` + +### 场景过渡类型 + +```cpp +enum class TransitionType { + None, // 无过渡 + Fade, // 淡入淡出 + SlideLeft, // 向左滑动 + SlideRight, // 向右滑动 + SlideUp, // 向上滑动 + SlideDown // 向下滑动 +}; +``` + +## 场景配置 + +### 视口设置 + +```cpp +void onEnter() override { + Scene::onEnter(); + + // 设置视口大小(影响坐标系) + setViewportSize(1280.0f, 720.0f); + + // 设置背景颜色 + setBackgroundColor(Colors::Black); + + // 启用/禁用空间索引 + setSpatialIndexingEnabled(true); +} +``` + +### 空间索引 + +```cpp +// 获取空间管理器 +auto &spatialManager = getSpatialManager(); + +// 切换空间索引策略 +spatialManager.setStrategy(SpatialStrategy::QuadTree); // 四叉树 +spatialManager.setStrategy(SpatialStrategy::SpatialHash); // 空间哈希 + +// 查询所有碰撞 +auto collisions = queryCollisions(); +``` + +## 完整示例 + +```cpp +class GameScene : public Scene { +public: + void onEnter() override { + Scene::onEnter(); + + // 设置视口和背景 + setViewportSize(1280.0f, 720.0f); + setBackgroundColor(Color(0.1f, 0.2f, 0.3f, 1.0f)); + + // 启用空间索引 + setSpatialIndexingEnabled(true); + + E2D_LOG_INFO("游戏场景已加载"); + } + + void onUpdate(float dt) override { + Scene::onUpdate(dt); + + // 检查退出按键 + auto &input = Application::instance().input(); + if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_START)) { + Application::instance().quit(); + } + } + + void onRender(RenderBackend &renderer) override { + Scene::onRender(renderer); + + // 绘制 FPS + auto &app = Application::instance(); + std::string fpsText = "FPS: " + std::to_string(app.fps()); + // ... + } + + void onExit() override { + E2D_LOG_INFO("游戏场景退出"); + Scene::onExit(); + } +}; +``` + +## 下一步 + +- [03. 节点系统](03_Node_System.md) +- [04. 资源管理](04_Resource_Management.md) diff --git a/docs/API_Tutorial/03_Node_System.md b/docs/API_Tutorial/03_Node_System.md new file mode 100644 index 0000000..63a7b08 --- /dev/null +++ b/docs/API_Tutorial/03_Node_System.md @@ -0,0 +1,219 @@ +# Extra2D API 教程 - 03. 节点系统 + +## 节点基础 + +节点(Node)是游戏对象的基本单位,可以包含子节点,形成树形结构。 + +### 创建节点 + +```cpp +#include + +using namespace extra2d; + +class MyNode : public Node { +public: + MyNode() { + // 设置位置 + setPosition(Vec2(100.0f, 200.0f)); + + // 设置旋转(度) + setRotation(45.0f); + + // 设置缩放 + setScale(Vec2(2.0f, 2.0f)); + + // 设置锚点(0-1范围,默认0.5是中心) + setAnchor(0.5f, 0.5f); + + // 设置可见性 + setVisible(true); + } + + // 每帧更新 + void onUpdate(float dt) override { + Node::onUpdate(dt); + // 自定义更新逻辑 + } + + // 渲染 + void onRender(RenderBackend &renderer) override { + Node::onRender(renderer); + // 自定义渲染 + } +}; +``` + +## 节点层级 + +### 添加子节点 + +```cpp +void onEnter() override { + Scene::onEnter(); + + // 创建子节点 + auto child = makePtr(); + + // 添加到场景 + addChild(child); + + // 在指定位置添加 + addChild(child, 0); // z-order = 0 +} +``` + +### 移除子节点 + +```cpp +// 移除指定子节点 +removeChild(child); + +// 移除所有子节点 +removeAllChildren(); + +// 通过名称移除 +removeChildByName("myNode"); +``` + +### 获取子节点 + +```cpp +// 获取子节点数量 +size_t count = getChildren().size(); + +// 通过名称查找 +auto node = getChildByName("myNode"); + +// 遍历子节点 +for (auto &child : getChildren()) { + // 处理子节点 +} +``` + +## 空间索引 + +### 启用空间索引 + +```cpp +class PhysicsNode : public Node { +public: + PhysicsNode() { + // 启用空间索引(用于碰撞检测) + setSpatialIndexed(true); + } + + // 必须实现 getBoundingBox() + Rect getBoundingBox() const override { + Vec2 pos = getPosition(); + return Rect(pos.x - 25.0f, pos.y - 25.0f, 50.0f, 50.0f); + } +}; +``` + +### 边界框 + +```cpp +// 获取节点边界框 +Rect bounds = node->getBoundingBox(); + +// 检查点是否在边界框内 +if (bounds.contains(Vec2(x, y))) { + // 点在边界框内 +} + +// 检查两个边界框是否相交 +if (bounds.intersects(otherBounds)) { + // 边界框相交 +} +``` + +## 精灵节点 + +### 创建精灵 + +```cpp +// 加载纹理 +auto texture = resources.loadTexture("assets/player.png"); + +// 创建精灵 +auto sprite = Sprite::create(texture); + +// 设置位置 +sprite->setPosition(Vec2(640.0f, 360.0f)); + +// 设置锚点(中心) +sprite->setAnchor(0.5f, 0.5f); + +// 添加到场景 +addChild(sprite); +``` + +### 精灵动画 + +```cpp +// 创建动画 +auto animation = Animation::create("walk", 0.1f); +animation->addFrame(resources.loadTexture("assets/walk1.png")); +animation->addFrame(resources.loadTexture("assets/walk2.png")); +animation->addFrame(resources.loadTexture("assets/walk3.png")); + +// 播放动画 +sprite->playAnimation(animation, true); // true = 循环播放 + +// 停止动画 +sprite->stopAnimation(); +``` + +## 完整示例 + +```cpp +class Player : public Node { +public: + Player() { + setSpatialIndexed(true); + + // 加载精灵 + auto &resources = Application::instance().resources(); + auto texture = resources.loadTexture("assets/player.png"); + sprite_ = Sprite::create(texture); + sprite_->setAnchor(0.5f, 0.5f); + addChild(sprite_); + } + + void onUpdate(float dt) override { + Node::onUpdate(dt); + + // 移动 + Vec2 pos = getPosition(); + pos = pos + velocity_ * dt; + setPosition(pos); + + // 边界检查 + auto &app = Application::instance(); + float width = static_cast(app.getConfig().width); + float height = static_cast(app.getConfig().height); + + if (pos.x < 0 || pos.x > width) { + velocity_.x = -velocity_.x; + } + if (pos.y < 0 || pos.y > height) { + velocity_.y = -velocity_.y; + } + } + + Rect getBoundingBox() const override { + Vec2 pos = getPosition(); + return Rect(pos.x - 25.0f, pos.y - 25.0f, 50.0f, 50.0f); + } + +private: + Ptr sprite_; + Vec2 velocity_{100.0f, 100.0f}; +}; +``` + +## 下一步 + +- [04. 资源管理](04_Resource_Management.md) +- [05. 输入处理](05_Input_Handling.md) diff --git a/docs/API_Tutorial/04_Resource_Management.md b/docs/API_Tutorial/04_Resource_Management.md new file mode 100644 index 0000000..18d8108 --- /dev/null +++ b/docs/API_Tutorial/04_Resource_Management.md @@ -0,0 +1,148 @@ +# Extra2D API 教程 - 04. 资源管理 + +## 资源管理器 + +Extra2D 使用资源管理器来统一加载和管理资源。 + +### 获取资源管理器 + +```cpp +auto &resources = Application::instance().resources(); +``` + +## 字体资源 + +### 加载字体 + +```cpp +// 加载字体(路径,大小,使用后备字体) +auto font = resources.loadFont("assets/font.ttf", 48, true); + +if (!font) { + E2D_LOG_ERROR("字体加载失败!"); +} +``` + +### 使用字体 + +```cpp +void onRender(RenderBackend &renderer) override { + if (font) { + renderer.drawText(*font, "Hello World", Vec2(100.0f, 100.0f), + Colors::White); + } +} +``` + +## 纹理资源 + +### 加载纹理 + +```cpp +// 加载纹理 +auto texture = resources.loadTexture("assets/player.png"); + +if (!texture) { + E2D_LOG_ERROR("纹理加载失败!"); +} +``` + +### 创建精灵 + +```cpp +auto sprite = Sprite::create(texture); +sprite->setPosition(Vec2(640.0f, 360.0f)); +addChild(sprite); +``` + +## 音效资源 + +### 加载音效 + +```cpp +// 加载音效 +auto sound = resources.loadSound("assets/jump.wav"); + +// 播放音效 +sound->play(); + +// 循环播放 +sound->play(true); + +// 停止播放 +sound->stop(); +``` + +## 资源路径解析 + +Extra2D 的资源管理器支持多平台路径解析: + +### 路径优先级 + +1. **原始路径**: `assets/font.ttf` +2. **romfs 路径**: `romfs:/assets/font.ttf` (Switch) +3. **sdmc 路径**: `sdmc:/assets/font.ttf` (Switch SD卡) +4. **可执行文件相对路径** (Windows) + +### 使用示例 + +```cpp +// 所有平台使用相同的路径 +auto font = resources.loadFont("assets/font.ttf", 48, true); +auto texture = resources.loadTexture("assets/images/player.png"); +auto sound = resources.loadSound("assets/audio/jump.wav"); +``` + +## 完整示例 + +```cpp +class GameScene : public Scene { +public: + void onEnter() override { + Scene::onEnter(); + + auto &resources = Application::instance().resources(); + + // 加载字体 + titleFont_ = resources.loadFont("assets/font.ttf", 60, true); + infoFont_ = resources.loadFont("assets/font.ttf", 24, true); + + // 加载纹理 + playerTexture_ = resources.loadTexture("assets/player.png"); + enemyTexture_ = resources.loadTexture("assets/enemy.png"); + + // 创建精灵 + player_ = Sprite::create(playerTexture_); + player_->setPosition(Vec2(640.0f, 360.0f)); + addChild(player_); + } + + void onRender(RenderBackend &renderer) override { + Scene::onRender(renderer); + + // 绘制文字 + if (titleFont_) { + renderer.drawText(*titleFont_, "Game Title", + Vec2(50.0f, 50.0f), Colors::White); + } + + if (infoFont_) { + std::string fps = "FPS: " + std::to_string(Application::instance().fps()); + renderer.drawText(*infoFont_, fps, + Vec2(50.0f, 100.0f), Colors::Yellow); + } + } + +private: + Ptr titleFont_; + Ptr infoFont_; + Ptr playerTexture_; + Ptr enemyTexture_; + Ptr player_; +}; +``` + +## 下一步 + +- [05. 输入处理](05_Input_Handling.md) +- [06. 碰撞检测](06_Collision_Detection.md) diff --git a/docs/API_Tutorial/05_Input_Handling.md b/docs/API_Tutorial/05_Input_Handling.md new file mode 100644 index 0000000..24ed607 --- /dev/null +++ b/docs/API_Tutorial/05_Input_Handling.md @@ -0,0 +1,216 @@ +# Extra2D API 教程 - 05. 输入处理 + +## 输入系统 + +Extra2D 提供统一的输入处理接口,支持键盘和游戏手柄。 + +### 获取输入管理器 + +```cpp +auto &input = Application::instance().input(); +``` + +## 游戏手柄输入 + +Extra2D 提供了 `GamepadButton` 和 `GamepadAxis` 命名空间来映射 SDL 按键。 + +### 检测按键按下 + +```cpp +void onUpdate(float dt) override { + auto &input = Application::instance().input(); + + // 检测按键按下(每帧只触发一次) + if (input.isButtonPressed(GamepadButton::A)) { + // A 键被按下 + jump(); + } + + if (input.isButtonPressed(GamepadButton::B)) { + // B 键被按下 + attack(); + } +} +``` + +### 检测按键按住 + +```cpp +void onUpdate(float dt) override { + auto &input = Application::instance().input(); + + // 检测按键按住(每帧都触发) + if (input.isButtonDown(GamepadButton::DPadLeft)) { + // 左方向键按住 + moveLeft(); + } + + if (input.isButtonDown(GamepadButton::DPadRight)) { + // 右方向键按住 + moveRight(); + } +} +``` + +### 按键映射表 + +| Extra2D 枚举 | 对应按键 | +|-------------|----------| +| `GamepadButton::A` | A 键 (Xbox) / × 键 (PlayStation) | +| `GamepadButton::B` | B 键 (Xbox) / ○ 键 (PlayStation) | +| `GamepadButton::X` | X 键 (Xbox) / □ 键 (PlayStation) | +| `GamepadButton::Y` | Y 键 (Xbox) / △ 键 (PlayStation) | +| `GamepadButton::LeftBumper` | 左肩键 (LB/L1) | +| `GamepadButton::RightBumper` | 右肩键 (RB/R1) | +| `GamepadButton::Back` | 返回键 (View/Share) | +| `GamepadButton::Start` | 开始键 (Menu/Options) | +| `GamepadButton::Guide` | 主页键 (Xbox/PS) | +| `GamepadButton::LeftThumb` | 左摇杆按下 (L3) | +| `GamepadButton::RightThumb` | 右摇杆按下 (R3) | +| `GamepadButton::DPadUp` | 方向键上 | +| `GamepadButton::DPadDown` | 方向键下 | +| `GamepadButton::DPadLeft` | 方向键左 | +| `GamepadButton::DPadRight` | 方向键右 | + +### PlayStation 风格别名 + +| Extra2D 枚举 | 对应按键 | +|-------------|----------| +| `GamepadButton::Cross` | A | +| `GamepadButton::Circle` | B | +| `GamepadButton::Square` | X | +| `GamepadButton::Triangle` | Y | + +## 摇杆输入 + +### 获取摇杆值 + +```cpp +void onUpdate(float dt) override { + auto &input = Application::instance().input(); + + // 左摇杆(范围 -1.0 到 1.0) + float leftX = input.getAxis(GamepadAxis::LeftX); + float leftY = input.getAxis(GamepadAxis::LeftY); + + // 右摇杆 + float rightX = input.getAxis(GamepadAxis::RightX); + float rightY = input.getAxis(GamepadAxis::RightY); + + // 使用摇杆值移动 + if (std::abs(leftX) > 0.1f || std::abs(leftY) > 0.1f) { + Vec2 velocity(leftX * speed, leftY * speed); + player->setPosition(player->getPosition() + velocity * dt); + } +} +``` + +### 摇杆轴映射表 + +| Extra2D 枚举 | 说明 | +|-------------|------| +| `GamepadAxis::LeftX` | 左摇杆 X 轴 | +| `GamepadAxis::LeftY` | 左摇杆 Y 轴 | +| `GamepadAxis::RightX` | 右摇杆 X 轴 | +| `GamepadAxis::RightY` | 右摇杆 Y 轴 | +| `GamepadAxis::LeftTrigger` | 左扳机 (LT/L2) | +| `GamepadAxis::RightTrigger` | 右扳机 (RT/R2) | + +## 键盘输入 + +### 检测键盘按键 + +```cpp +void onUpdate(float dt) override { + auto &input = Application::instance().input(); + + // 检测按键按下 + if (input.isKeyPressed(SDLK_SPACE)) { + jump(); + } + + // 检测按键按住 + if (input.isKeyDown(SDLK_LEFT)) { + moveLeft(); + } + + if (input.isKeyDown(SDLK_RIGHT)) { + moveRight(); + } +} +``` + +## 完整示例 + +```cpp +class Player : public Node { +public: + void onUpdate(float dt) override { + Node::onUpdate(dt); + + auto &input = Application::instance().input(); + Vec2 velocity(0.0f, 0.0f); + + // 方向键移动 + if (input.isButtonDown(GamepadButton::DPadLeft)) { + velocity.x = -speed_; + } else if (input.isButtonDown(GamepadButton::DPadRight)) { + velocity.x = speed_; + } + + if (input.isButtonDown(GamepadButton::DPadUp)) { + velocity.y = -speed_; + } else if (input.isButtonDown(GamepadButton::DPadDown)) { + velocity.y = speed_; + } + + // 摇杆移动(如果方向键没有按下) + if (velocity.x == 0.0f && velocity.y == 0.0f) { + float axisX = input.getAxis(GamepadAxis::LeftX); + float axisY = input.getAxis(GamepadAxis::LeftY); + + if (std::abs(axisX) > 0.1f) { + velocity.x = axisX * speed_; + } + if (std::abs(axisY) > 0.1f) { + velocity.y = axisY * speed_; + } + } + + // 应用移动 + Vec2 pos = getPosition(); + pos = pos + velocity * dt; + setPosition(pos); + + // 动作键 + if (input.isButtonPressed(GamepadButton::A)) { + jump(); + } + + if (input.isButtonPressed(GamepadButton::B)) { + attack(); + } + + // 退出游戏 + if (input.isButtonPressed(GamepadButton::Start)) { + Application::instance().quit(); + } + } + +private: + float speed_ = 200.0f; + + void jump() { + // 跳跃逻辑 + } + + void attack() { + // 攻击逻辑 + } +}; +``` + +## 下一步 + +- [06. 碰撞检测](06_Collision_Detection.md) +- [07. UI 系统](07_UI_System.md) diff --git a/docs/API_Tutorial/06_Collision_Detection.md b/docs/API_Tutorial/06_Collision_Detection.md new file mode 100644 index 0000000..8c213bd --- /dev/null +++ b/docs/API_Tutorial/06_Collision_Detection.md @@ -0,0 +1,223 @@ +# Extra2D API 教程 - 06. 碰撞检测 + +## 空间索引系统 + +Extra2D 内置了空间索引系统,用于高效地进行碰撞检测。 + +### 启用空间索引 + +```cpp +void onEnter() override { + Scene::onEnter(); + + // 启用空间索引 + setSpatialIndexingEnabled(true); +} +``` + +## 碰撞节点 + +### 创建可碰撞节点 + +```cpp +class PhysicsNode : public Node { +public: + PhysicsNode(float size, const Color &color) + : size_(size), color_(color), isColliding_(false) { + // 启用空间索引(关键!) + setSpatialIndexed(true); + } + + // 必须实现 getBoundingBox() + Rect getBoundingBox() const override { + Vec2 pos = getPosition(); + return Rect(pos.x - size_ / 2, pos.y - size_ / 2, size_, size_); + } + + void setColliding(bool colliding) { isColliding_ = colliding; } + + void onRender(RenderBackend &renderer) override { + Vec2 pos = getPosition(); + + // 碰撞时变红色 + Color fillColor = isColliding_ ? Color(1.0f, 0.2f, 0.2f, 0.9f) : color_; + renderer.fillRect(Rect(pos.x - size_ / 2, pos.y - size_ / 2, size_, size_), + fillColor); + } + +private: + float size_; + Color color_; + bool isColliding_; +}; +``` + +## 碰撞检测 + +### 查询所有碰撞 + +```cpp +void performCollisionDetection() { + // 清除之前的碰撞状态 + for (auto &node : nodes_) { + node->setColliding(false); + } + + // 查询所有碰撞(使用空间索引) + auto collisions = queryCollisions(); + + // 标记碰撞的节点 + for (const auto &[nodeA, nodeB] : collisions) { + if (auto boxA = dynamic_cast(nodeA)) { + boxA->setColliding(true); + } + if (auto boxB = dynamic_cast(nodeB)) { + boxB->setColliding(true); + } + } +} +``` + +## 空间索引策略 + +### 切换策略 + +```cpp +void onEnter() override { + Scene::onEnter(); + + // 启用空间索引 + setSpatialIndexingEnabled(true); + + // 设置空间索引策略 + auto &spatialManager = getSpatialManager(); + spatialManager.setStrategy(SpatialStrategy::QuadTree); // 四叉树 + // 或 + spatialManager.setStrategy(SpatialStrategy::SpatialHash); // 空间哈希 +} + +// 切换策略 +void toggleStrategy() { + auto &spatialManager = getSpatialManager(); + SpatialStrategy current = spatialManager.getCurrentStrategy(); + + if (current == SpatialStrategy::QuadTree) { + spatialManager.setStrategy(SpatialStrategy::SpatialHash); + } else { + spatialManager.setStrategy(SpatialStrategy::QuadTree); + } +} +``` + +### 策略对比 + +| 策略 | 适用场景 | 特点 | +|------|----------|------| +| QuadTree | 节点分布不均匀 | 适合稀疏场景 | +| SpatialHash | 节点分布均匀 | 适合密集场景 | + +## 完整示例 + +```cpp +class CollisionScene : public Scene { +public: + void onEnter() override { + Scene::onEnter(); + + // 启用空间索引 + setSpatialIndexingEnabled(true); + + // 创建碰撞节点 + createNodes(100); + } + + void onUpdate(float dt) override { + Scene::onUpdate(dt); + + // 更新节点位置 + for (auto &node : nodes_) { + node->update(dt); + } + + // 执行碰撞检测 + performCollisionDetection(); + } + + void onRender(RenderBackend &renderer) override { + Scene::onRender(renderer); + + // 绘制碰撞统计 + std::string text = "Collisions: " + std::to_string(collisionCount_); + // ... + } + +private: + std::vector> nodes_; + size_t collisionCount_ = 0; + + void createNodes(size_t count) { + for (size_t i = 0; i < count; ++i) { + auto node = makePtr(20.0f, Color(0.5f, 0.5f, 0.9f, 0.7f)); + node->setPosition(randomPosition()); + addChild(node); + nodes_.push_back(node); + } + } + + void performCollisionDetection() { + // 清除碰撞状态 + for (auto &node : nodes_) { + node->setColliding(false); + } + + // 查询碰撞 + auto collisions = queryCollisions(); + collisionCount_ = collisions.size(); + + // 标记碰撞节点 + for (const auto &[nodeA, nodeB] : collisions) { + if (auto boxA = dynamic_cast(nodeA)) { + boxA->setColliding(true); + } + if (auto boxB = dynamic_cast(nodeB)) { + boxB->setColliding(true); + } + } + } +}; +``` + +## 注意事项 + +### 必须调用 Scene::onEnter() + +```cpp +void onEnter() override { + Scene::onEnter(); // 必须调用! + + // 否则子节点无法注册到空间索引 + setSpatialIndexingEnabled(true); +} +``` + +### 必须实现 getBoundingBox() + +```cpp +class MyNode : public Node { +public: + MyNode() { + setSpatialIndexed(true); + } + + // 必须实现! + Rect getBoundingBox() const override { + Vec2 pos = getPosition(); + return Rect(pos.x - width_/2, pos.y - height_/2, width_, height_); + } +}; +``` + +## 下一步 + +- [07. UI 系统](07_UI_System.md) +- [08. 音频系统](08_Audio_System.md) diff --git a/docs/API_Tutorial/07_UI_System.md b/docs/API_Tutorial/07_UI_System.md new file mode 100644 index 0000000..1b82ad2 --- /dev/null +++ b/docs/API_Tutorial/07_UI_System.md @@ -0,0 +1,337 @@ +# Extra2D API 教程 - 07. UI 系统 + +## 按钮 (Button) + +Extra2D 提供 Button 组件用于创建交互式按钮。 + +### 创建按钮 + +```cpp +// 创建按钮 +auto button = Button::create(); + +// 设置位置 +button->setPosition(Vec2(640.0f, 360.0f)); + +// 设置锚点(中心) +button->setAnchor(0.5f, 0.5f); + +// 添加到场景 +addChild(button); +``` + +### 设置按钮文本 + +```cpp +// 加载字体 +auto font = resources.loadFont("assets/font.ttf", 28, true); + +// 设置按钮字体和文本 +button->setFont(font); +button->setText("点击我"); +button->setTextColor(Colors::Black); +``` + +### 设置按钮样式 + +```cpp +// 设置背景颜色(普通、悬停、按下) +button->setBackgroundColor( + Colors::White, // 普通状态 + Colors::LightGray, // 悬停状态 + Colors::Gray // 按下状态 +); + +// 设置边框 +button->setBorder(Colors::Black, 2.0f); + +// 设置内边距 +button->setPadding(Vec2(20.0f, 10.0f)); + +// 设置固定大小 +button->setCustomSize(200.0f, 50.0f); +``` + +### 透明按钮 + +```cpp +// 创建透明按钮(仅文本可点击) +auto button = Button::create(); +button->setFont(font); +button->setText("菜单项"); +button->setTextColor(Colors::Black); + +// 透明背景 +button->setBackgroundColor( + Colors::Transparent, + Colors::Transparent, + Colors::Transparent +); + +// 无边框 +button->setBorder(Colors::Transparent, 0.0f); + +// 无内边距 +button->setPadding(Vec2(0.0f, 0.0f)); +``` + +## 菜单系统 + +### 创建菜单 + +```cpp +class MenuScene : public Scene { +public: + void onEnter() override { + Scene::onEnter(); + + auto &resources = Application::instance().resources(); + font_ = resources.loadFont("assets/font.ttf", 28, true); + + float centerX = 640.0f; + float startY = 300.0f; + + // 创建菜单按钮 + createMenuButton("开始游戏", centerX, startY, 0); + createMenuButton("继续游戏", centerX, startY + 50.0f, 1); + createMenuButton("退出", centerX, startY + 100.0f, 2); + + menuCount_ = 3; + selectedIndex_ = 0; + updateMenuColors(); + } + + void onUpdate(float dt) override { + Scene::onUpdate(dt); + + auto &input = Application::instance().input(); + + // 方向键切换选择 + if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_DPAD_UP)) { + selectedIndex_ = (selectedIndex_ - 1 + menuCount_) % menuCount_; + updateMenuColors(); + } else if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_DPAD_DOWN)) { + selectedIndex_ = (selectedIndex_ + 1) % menuCount_; + updateMenuColors(); + } + + // A键确认 + if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_A)) { + executeMenuItem(); + } + } + +private: + Ptr font_; + std::vector> buttons_; + int selectedIndex_ = 0; + int menuCount_ = 0; + + void createMenuButton(const std::string &text, float x, float y, int index) { + auto button = Button::create(); + button->setFont(font_); + button->setText(text); + button->setTextColor(Colors::Black); + button->setBackgroundColor( + Colors::Transparent, + Colors::Transparent, + Colors::Transparent + ); + button->setBorder(Colors::Transparent, 0.0f); + button->setPadding(Vec2(0.0f, 0.0f)); + button->setCustomSize(200.0f, 40.0f); + button->setAnchor(0.5f, 0.5f); + button->setPosition(x, y); + addChild(button); + buttons_.push_back(button); + } + + void updateMenuColors() { + for (int i = 0; i < buttons_.size(); ++i) { + if (buttons_[i]) { + buttons_[i]->setTextColor( + i == selectedIndex_ ? Colors::Red : Colors::Black + ); + } + } + } + + void executeMenuItem() { + switch (selectedIndex_) { + case 0: startGame(); break; + case 1: continueGame(); break; + case 2: exitGame(); break; + } + } + + void startGame() { + // 切换到游戏场景 + } + + void continueGame() { + // 继续游戏 + } + + void exitGame() { + Application::instance().quit(); + } +}; +``` + +## 绘制文字 + +### 基本文字绘制 + +```cpp +void onRender(RenderBackend &renderer) override { + Scene::onRender(renderer); + + if (font_) { + // 绘制文字 + renderer.drawText(*font_, "Hello World", + Vec2(100.0f, 100.0f), Colors::White); + + // 绘制带颜色的文字 + renderer.drawText(*font_, "红色文字", + Vec2(100.0f, 150.0f), Colors::Red); + } +} +``` + +### 格式化文字 + +```cpp +void onRender(RenderBackend &renderer) override { + Scene::onRender(renderer); + + if (infoFont_) { + auto &app = Application::instance(); + + // 绘制 FPS + std::stringstream ss; + ss << "FPS: " << app.fps(); + renderer.drawText(*infoFont_, ss.str(), + Vec2(50.0f, 50.0f), Colors::Yellow); + + // 绘制节点数量 + ss.str(""); + ss << "Nodes: " << nodes_.size(); + renderer.drawText(*infoFont_, ss.str(), + Vec2(50.0f, 80.0f), Colors::White); + } +} +``` + +## 完整示例 + +```cpp +class StartScene : public Scene { +public: + void onEnter() override { + Scene::onEnter(); + + auto &app = Application::instance(); + auto &resources = app.resources(); + + // 加载背景 + auto bgTex = resources.loadTexture("assets/background.jpg"); + if (bgTex) { + auto bg = Sprite::create(bgTex); + bg->setAnchor(0.0f, 0.0f); + addChild(bg); + } + + // 加载字体 + font_ = resources.loadFont("assets/font.ttf", 32, true); + + float centerX = app.getConfig().width / 2.0f; + + // 创建标题 + titleBtn_ = Button::create(); + titleBtn_->setFont(font_); + titleBtn_->setText("游戏标题"); + titleBtn_->setTextColor(Colors::Gold); + titleBtn_->setBackgroundColor( + Colors::Transparent, + Colors::Transparent, + Colors::Transparent + ); + titleBtn_->setBorder(Colors::Transparent, 0.0f); + titleBtn_->setAnchor(0.5f, 0.5f); + titleBtn_->setPosition(centerX, 200.0f); + addChild(titleBtn_); + + // 创建菜单按钮 + createMenuButton("新游戏", centerX, 350.0f, 0); + createMenuButton("继续", centerX, 400.0f, 1); + createMenuButton("退出", centerX, 450.0f, 2); + + menuCount_ = 3; + updateMenuColors(); + } + + void onUpdate(float dt) override { + Scene::onUpdate(dt); + + auto &input = Application::instance().input(); + + if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_DPAD_UP)) { + selectedIndex_ = (selectedIndex_ - 1 + menuCount_) % menuCount_; + updateMenuColors(); + } else if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_DPAD_DOWN)) { + selectedIndex_ = (selectedIndex_ + 1) % menuCount_; + updateMenuColors(); + } + + if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_A)) { + executeMenuItem(); + } + } + +private: + Ptr font_; + Ptr