refactor(input): 统一使用 GamepadButton 枚举替代 SDL_CONTROLLER_BUTTON 常量

docs: 添加 API 教程文档和构建系统文档

新增完整的 API 教程文档,涵盖快速开始、场景系统、节点系统、资源管理、输入处理、碰撞检测、UI 系统和音频系统。同时添加详细的构建系统文档,说明 MinGW 和 Switch 平台的构建配置。

feat(build): 支持 MinGW 平台构建

新增 MinGW 平台支持,更新 README 添加 MinGW 构建说明,完善 xmake 构建脚本以支持多平台构建。

chore: 更新项目结构和文档链接

调整项目目录结构,更新 README 中的文档链接,添加构建系统文档和 API 教程的快速访问链接
This commit is contained in:
ChestnutYueyue 2026-02-10 19:32:22 +08:00
parent 0f89262498
commit 23647b6458
15 changed files with 2460 additions and 43 deletions

View File

@ -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` 中添加平台配置

114
README.md
View File

@ -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 脚本引擎
├── <20> 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) - 项目迁移历史记录
---

View File

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

View File

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

View File

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

View File

@ -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<FontAtlas> titleFont_;
Ptr<FontAtlas> infoFont_;
Ptr<Texture> playerTexture_;
Ptr<Texture> enemyTexture_;
Ptr<Sprite> player_;
};
```
## 下一步
- [05. 输入处理](05_Input_Handling.md)
- [06. 碰撞检测](06_Collision_Detection.md)

View File

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

View File

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

View File

@ -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<FontAtlas> font_;
std::vector<Ptr<Button>> buttons_;
int selectedIndex_ = 0;
int menuCount_ = 0;
void createMenuButton(const std::string &text, float x, float y, int index) {
auto button = Button::create();
button->setFont(font_);
button->setText(text);
button->setTextColor(Colors::Black);
button->setBackgroundColor(
Colors::Transparent,
Colors::Transparent,
Colors::Transparent
);
button->setBorder(Colors::Transparent, 0.0f);
button->setPadding(Vec2(0.0f, 0.0f));
button->setCustomSize(200.0f, 40.0f);
button->setAnchor(0.5f, 0.5f);
button->setPosition(x, y);
addChild(button);
buttons_.push_back(button);
}
void updateMenuColors() {
for (int i = 0; i < buttons_.size(); ++i) {
if (buttons_[i]) {
buttons_[i]->setTextColor(
i == selectedIndex_ ? Colors::Red : Colors::Black
);
}
}
}
void executeMenuItem() {
switch (selectedIndex_) {
case 0: startGame(); break;
case 1: continueGame(); break;
case 2: exitGame(); break;
}
}
void startGame() {
// 切换到游戏场景
}
void continueGame() {
// 继续游戏
}
void exitGame() {
Application::instance().quit();
}
};
```
## 绘制文字
### 基本文字绘制
```cpp
void onRender(RenderBackend &renderer) override {
Scene::onRender(renderer);
if (font_) {
// 绘制文字
renderer.drawText(*font_, "Hello World",
Vec2(100.0f, 100.0f), Colors::White);
// 绘制带颜色的文字
renderer.drawText(*font_, "红色文字",
Vec2(100.0f, 150.0f), Colors::Red);
}
}
```
### 格式化文字
```cpp
void onRender(RenderBackend &renderer) override {
Scene::onRender(renderer);
if (infoFont_) {
auto &app = Application::instance();
// 绘制 FPS
std::stringstream ss;
ss << "FPS: " << app.fps();
renderer.drawText(*infoFont_, ss.str(),
Vec2(50.0f, 50.0f), Colors::Yellow);
// 绘制节点数量
ss.str("");
ss << "Nodes: " << nodes_.size();
renderer.drawText(*infoFont_, ss.str(),
Vec2(50.0f, 80.0f), Colors::White);
}
}
```
## 完整示例
```cpp
class StartScene : public Scene {
public:
void onEnter() override {
Scene::onEnter();
auto &app = Application::instance();
auto &resources = app.resources();
// 加载背景
auto bgTex = resources.loadTexture("assets/background.jpg");
if (bgTex) {
auto bg = Sprite::create(bgTex);
bg->setAnchor(0.0f, 0.0f);
addChild(bg);
}
// 加载字体
font_ = resources.loadFont("assets/font.ttf", 32, true);
float centerX = app.getConfig().width / 2.0f;
// 创建标题
titleBtn_ = Button::create();
titleBtn_->setFont(font_);
titleBtn_->setText("游戏标题");
titleBtn_->setTextColor(Colors::Gold);
titleBtn_->setBackgroundColor(
Colors::Transparent,
Colors::Transparent,
Colors::Transparent
);
titleBtn_->setBorder(Colors::Transparent, 0.0f);
titleBtn_->setAnchor(0.5f, 0.5f);
titleBtn_->setPosition(centerX, 200.0f);
addChild(titleBtn_);
// 创建菜单按钮
createMenuButton("新游戏", centerX, 350.0f, 0);
createMenuButton("继续", centerX, 400.0f, 1);
createMenuButton("退出", centerX, 450.0f, 2);
menuCount_ = 3;
updateMenuColors();
}
void onUpdate(float dt) override {
Scene::onUpdate(dt);
auto &input = Application::instance().input();
if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_DPAD_UP)) {
selectedIndex_ = (selectedIndex_ - 1 + menuCount_) % menuCount_;
updateMenuColors();
} else if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_DPAD_DOWN)) {
selectedIndex_ = (selectedIndex_ + 1) % menuCount_;
updateMenuColors();
}
if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_A)) {
executeMenuItem();
}
}
private:
Ptr<FontAtlas> font_;
Ptr<Button> titleBtn_;
std::vector<Ptr<Button>> menuButtons_;
int selectedIndex_ = 0;
int menuCount_ = 0;
void createMenuButton(const std::string &text, float x, float y, int index) {
auto btn = Button::create();
btn->setFont(font_);
btn->setText(text);
btn->setTextColor(Colors::White);
btn->setBackgroundColor(
Colors::Transparent,
Colors::Transparent,
Colors::Transparent
);
btn->setBorder(Colors::Transparent, 0.0f);
btn->setAnchor(0.5f, 0.5f);
btn->setPosition(x, y);
addChild(btn);
menuButtons_.push_back(btn);
}
void updateMenuColors() {
for (int i = 0; i < menuButtons_.size(); ++i) {
if (menuButtons_[i]) {
menuButtons_[i]->setTextColor(
i == selectedIndex_ ? Colors::Red : Colors::White
);
}
}
}
void executeMenuItem() {
switch (selectedIndex_) {
case 0: /* 新游戏 */ break;
case 1: /* 继续 */ break;
case 2: Application::instance().quit(); break;
}
}
};
```
## 下一步
- [08. 音频系统](08_Audio_System.md)

View File

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

View File

@ -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` 中添加平台配置

View File

@ -104,7 +104,7 @@ public:
// 检查退出按键
auto &input = Application::instance().input();
if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_START)) {
if (input.isButtonPressed(GamepadButton::Start)) {
E2D_LOG_INFO("退出应用");
Application::instance().quit();
}

View File

@ -41,8 +41,8 @@ public:
// 检查退出按键
auto &input = Application::instance().input();
// Switch: 使用手柄 START 按钮
if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_START)) {
// 使用手柄 START 按钮退出 (GamepadButton::Start)
if (input.isButtonPressed(GamepadButton::Start)) {
E2D_LOG_INFO("退出应用 (START 按钮)");
Application::instance().quit();
}

View File

@ -122,21 +122,21 @@ void StartScene::onUpdate(float dt) {
auto& input = app.input();
// 方向键上下切换选择
if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_DPAD_UP)) {
if (input.isButtonPressed(extra2d::GamepadButton::DPadUp)) {
selectedIndex_ = (selectedIndex_ - 1 + menuCount_) % menuCount_;
updateMenuColors();
} else if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_DPAD_DOWN)) {
} else if (input.isButtonPressed(extra2d::GamepadButton::DPadDown)) {
selectedIndex_ = (selectedIndex_ + 1) % menuCount_;
updateMenuColors();
}
// A键确认
if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_A)) {
if (input.isButtonPressed(extra2d::GamepadButton::A)) {
executeMenuItem();
}
// Y键切换音效
if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_X)) {
// X键切换音效
if (input.isButtonPressed(extra2d::GamepadButton::X)) {
g_SoundOpen = !g_SoundOpen;
if (auto audio = getAudioController()) {
audio->setEnabled(g_SoundOpen);

View File

@ -156,23 +156,23 @@ public:
// 检查退出按键
auto &input = Application::instance().input();
if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_START)) {
if (input.isButtonPressed(GamepadButton::Start)) {
E2D_LOG_INFO("退出应用");
Application::instance().quit();
}
// 按A键添加节点
if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_A)) {
// 按 A 键添加节点
if (input.isButtonPressed(GamepadButton::A)) {
addNodes(100);
}
// 按B键减少节点
if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_B)) {
// 按 B 键减少节点
if (input.isButtonPressed(GamepadButton::B)) {
removeNodes(100);
}
// 按X键切换空间索引策略
if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_X)) {
// 按 X 键切换空间索引策略
if (input.isButtonPressed(GamepadButton::X)) {
toggleSpatialStrategy();
}
}