docs: 更新API教程文档和构建系统文档

refactor: 重构xmake.lua中的示例构建逻辑
style: 统一文档格式和代码风格
This commit is contained in:
ChestnutYueyue 2026-02-11 16:42:20 +08:00
parent 6975f69d64
commit 39a0ab7124
11 changed files with 2044 additions and 1929 deletions

404
README.md
View File

@ -25,7 +25,7 @@
<i>高性能、易用、原生支持 Switch 平台</i>
</p>
[📖 构建指南](./SWITCH_BUILD_GUIDE.md) | [🚀 快速开始](#快速开始) | [📦 项目结构](#项目结构) | [💬 问题反馈](https://github.com/ChestnutYueyue/extra2d/issues)
[📖 构建指南](./docs/Extra2D%20构建系统文档.md) | [🚀 快速开始](#快速开始) | [📦 示例程序](#示例程序) | [📚 API 教程](./docs/API_Tutorial/01_Quick_Start.md)
</div>
@ -40,68 +40,12 @@
### ✨ 核心特性
- **🎯 Switch 原生支持**:专为 Nintendo Switch 硬件优化,支持掌机/主机双模式
- **🎬 高级动画系统**:支持骨骼动画、精灵动画、补间动画,提供 ALS 动画格式支持
- **📜 脚本系统**:集成 Squirrel 脚本引擎,支持热更新和快速迭代开发
- **🎵 音频系统**:基于 SDL2 的高质量音频播放,支持 BGM 和音效
- **🎨 特效系统**:粒子系统、后处理效果、自定义着色器支持
- **🎬 高级动画系统**:支持骨骼动画、精灵动画、补间动画
- **🎵 音频系统**:基于 SDL2_mixer 的高质量音频播放,支持 BGM 和音效
- **🎨 渲染系统**:基于 OpenGL ES 的 2D 渲染,支持自定义着色器
- **💾 数据持久化**:游戏存档、配置文件的便捷读写
---
## 🗺️ 架构概览
```mermaid
mindmap
root((Extra2D 引擎架构))
核心系统
应用管理 Application
渲染后端 RenderBackend
窗口管理 Window
输入处理 Input
音频引擎 AudioEngine
资源管理 ResourceManager
事件系统 EventDispatcher
场景管理
场景 Scene
场景管理器 SceneManager
过渡动画 Transition
空间索引 SpatialManager
节点系统
基础节点 Node
精灵 Sprite
文本 Text
形状 ShapeNode
摄像机 Camera
动画节点 AnimationNode
动画系统
动作系统 Action
精灵动画 AnimatedSprite
骨骼动画支持
动画缓存 AnimationCache
动画事件 AnimationEvent
脚本系统
Squirrel 脚本引擎
脚本节点 ScriptNode
完整 API 绑定
特效系统
粒子系统 ParticleSystem
后处理 PostProcess
自定义效果管理器
UI 系统
基础控件 Widget
按钮 Button
工具库
音频播放 Sound
数据持久化 Data
随机数 Random
定时器 Timer
字体 FontAtlas
数学库
向量 Vec2/Vec3
矩形 Rect
大小 Size
颜色 Color
```
- **🔧 空间索引**:内置四叉树和空间哈希碰撞检测系统
- **🖱️ UI 系统**:完整的 UI 控件支持(按钮、文本、滑块等)
---
@ -116,319 +60,24 @@ mindmap
| 构建工具 | xmake |
| 目标平台 | Nintendo Switch / Windows (MinGW) |
### 安装 devkitPro
### 安装 xmake
```bash
# Windows (以管理员身份运行 PowerShell)
Invoke-WebRequest -Uri "https://github.com/devkitPro/pacman/releases/latest/download/devkitpro-pacman.amd64.exe" -OutFile "devkitpro-pacman.exe"
.\devkitpro-pacman.exe
# Windows (PowerShell)
Invoke-Expression (Invoke-WebRequest 'https://xmake.io/psget.text' -UseBasicParsing).Content
# 安装 Switch 开发工具链
pacman -S switch-dev switch-portlibs
# macOS
brew install xmake
# Linux
sudo add-apt-repository ppa:xmake-io/xmake
sudo apt update
sudo apt install xmake
```
### 构建项目
## 📚 文档
#### Switch 平台
```bash
# 克隆仓库
git clone https://github.com/ChestnutYueyue/extra2d.git
cd extra2d
# 配置 Switch 平台构建
xmake f -p switch --mode=release
# 构建引擎
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
# 打包推箱子游戏示例
xmake package push_box
# 生成的文件位于
# build/switch/release/push_box/push_box.nsp
```
---
## 📝 Hello World 示例
```cpp
#include <extra2d/extra2d.h>
using namespace extra2d;
int main()
{
// 初始化日志
Logger::init();
Logger::setLevel(LogLevel::Info);
// 配置应用
AppConfig config;
config.title = "Hello Extra2D";
config.width = 1280;
config.height = 720;
config.vsync = true;
// 初始化应用
auto& app = Application::instance();
if (!app.init(config)) {
Logger::shutdown();
return -1;
}
// 创建场景
auto scene = makePtr<Scene>();
scene->setBackgroundColor(Color(0.1f, 0.1f, 0.15f, 1.0f));
// 创建文本节点
auto text = Text::create("Hello, Extra2D on Switch!");
text->setPosition(Vec2(640, 360));
text->setAnchor(Vec2(0.5f, 0.5f));
text->setTextColor(Color(1.0f, 0.5f, 0.2f, 1.0f));
text->setFontSize(48);
// 添加动画效果
text->runAction(makePtr<Repeat>(
makePtr<Sequence>(std::vector<Ptr<Action>>{
makePtr<ScaleTo>(1.0f, Vec2(1.5f, 1.5f)),
makePtr<ScaleTo>(1.0f, Vec2(1.0f, 1.0f))
})
));
// 添加到场景
scene->addChild(text);
// 进入场景
app.enterScene(scene);
// 运行主循环
app.run();
// 清理
app.shutdown();
Logger::shutdown();
return 0;
}
```
---
## 🏗️ 项目结构
```
Extra2D/
├── 📁 Extra2D/ # 引擎核心代码
│ ├── 📁 include/ # 头文件
│ │ ├── 📁 extra2d/ # 引擎头文件
│ │ │ ├── extra2d.h # 主头文件
│ │ │ ├── app/ # 应用管理
│ │ │ ├── action/ # 动作系统
│ │ │ ├── animation/ # 动画系统
│ │ │ ├── audio/ # 音频系统
│ │ │ ├── core/ # 核心类型
│ │ │ ├── effects/ # 特效系统
│ │ │ ├── event/ # 事件系统
│ │ │ ├── graphics/ # 图形渲染
│ │ │ ├── platform/ # 平台抽象
│ │ │ ├── resource/ # 资源管理
│ │ │ ├── scene/ # 场景系统
│ │ │ ├── script/ # 脚本系统
│ │ │ ├── spatial/ # 空间索引
│ │ │ ├── ui/ # UI 系统
│ │ │ └── utils/ # 工具库
│ │ ├── 📁 glad/ # OpenGL Loader
│ │ ├── 📁 json/ # JSON 库
│ │ ├── 📁 simpleini/ # INI 配置文件库
│ │ └── 📁 stb/ # STB 图像库
│ └── 📁 src/ # 源文件
│ ├── action/ # 动作系统实现
│ ├── animation/ # 动画系统实现
│ ├── app/ # 应用管理实现
│ ├── audio/ # 音频系统实现
│ ├── core/ # 核心类型实现
│ ├── effects/ # 特效系统实现
│ ├── event/ # 事件系统实现
│ ├── glad/ # GLAD 实现
│ ├── graphics/ # 图形渲染实现
│ ├── platform/ # 平台抽象实现
│ ├── resource/ # 资源管理实现
│ ├── scene/ # 场景系统实现
│ ├── script/ # 脚本系统实现
│ ├── spatial/ # 空间索引实现
│ ├── ui/ # UI 系统实现
│ └── utils/ # 工具库实现
├── 📁 docs/ # 文档
│ ├── 📁 API_Tutorial/ # API 教程
│ └── Extra2D 构建系统文档.md # 构建系统文档
├── 📁 examples/ # 示例程序
│ ├── hello_world/ # Hello World 示例
│ ├── collision_demo/ # 碰撞检测示例
│ ├── push_box/ # 推箱子游戏
│ └── spatial_index_demo/ # 空间索引示例
├── 📁 logo/ # Logo 资源
├── 📁 squirrel/ # Squirrel 脚本引擎
├── <20> xmake/ # Xmake 构建配置
│ └── toolchains/ # 工具链定义
├── 📄 xmake.lua # 主构建配置
├── 📄 LICENSE # MIT 许可证
└── 📄 README.md # 本文件
```
---
## 🎮 Switch 特定功能
### 双模式支持
```cpp
// 检测当前模式
if (app.isDocked()) {
// 主机模式:可使用更高分辨率
config.width = 1920;
config.height = 1080;
} else {
// 掌机模式
config.width = 1280;
config.height = 720;
}
```
### 控制器输入
```cpp
auto& input = app.input();
// Joy-Con 支持
if (input.isKeyDown(KeyCode::ButtonA)) {
// A 键按下
}
if (input.isKeyDown(KeyCode::ButtonLeft)) {
// 左摇杆向左
}
```
### ROMFS 资源加载
```cpp
// 自动从 ROMFS 加载资源
auto texture = resources.loadTexture("romfs:/images/player.png");
auto sound = audio.loadSound("romfs:/audio/jump.wav");
```
---
## 📋 API 速查
### 应用控制
```cpp
auto& app = Application::instance();
app.init(config);
app.run();
app.quit();
```
### 场景管理
```cpp
auto scene = makePtr<Scene>();
app.enterScene(scene);
app.enterScene(scene, makePtr<FadeTransition>(1.0f));
```
### 节点操作
```cpp
auto sprite = Sprite::create(texture);
sprite->setPosition(Vec2(100, 200));
sprite->setRotation(45.0f);
sprite->runAction(makePtr<MoveTo>(1.0f, Vec2(200, 300)));
```
### 动画系统
```cpp
// 精灵动画
auto anim = AnimatedSprite::createFromGrid(
"player.png", 96, 96, 125.0f, 16);
anim->setFrameRange(0, 3);
anim->play();
// 动作动画
node->runAction(makePtr<Sequence>(
makePtr<MoveTo>(1.0f, Vec2(100, 200)),
makePtr<ScaleTo>(0.5f, Vec2(2.0f, 2.0f))
));
```
### 输入处理
```cpp
auto& input = app.input();
if (input.isKeyDown(KeyCode::ButtonA)) {}
if (input.isKeyPressed(KeyCode::ButtonB)) {}
auto pos = input.getMousePosition();
```
### 音频播放
```cpp
auto& audio = app.audio();
auto sound = audio.loadSound("jump.wav");
sound->play();
sound->setVolume(0.8f);
```
---
## 🛠️ 技术栈
| 技术 | 用途 | 版本 |
|:----:|:-----|:----:|
| OpenGL | 2D 图形渲染 | ES 3.0+ |
| GLFW | 窗口和输入管理 | 3.3+ |
| GLM | 数学库 | 0.9.9+ |
| miniaudio | 音频播放 | 最新版 |
| spdlog | 日志系统 | 最新版 |
| stb_image | 图像加载 | 最新版 |
| Squirrel | 脚本引擎 | 3.2+ |
| xmake | 构建系统 | 2.5+ |
---
## 📖 相关文档
- [📚 API 教程](./docs/API_Tutorial/01_Quick_Start.md) - 完整的 API 使用教程
- [📖 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)
@ -438,8 +87,21 @@ sound->setVolume(0.8f);
- [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) - 项目迁移历史记录
---
## 🛠️ 技术栈
| 技术 | 用途 | 版本 |
|:----:|:-----|:----:|
| OpenGL ES | 2D 图形渲染 | 3.0+ |
| GLFW | 窗口和输入管理 | 3.3+ |
| GLM | 数学库 | 0.9.9+ |
| SDL2_mixer | 音频播放 | 2.0+ |
| spdlog | 日志系统 | 最新版 |
| stb_image | 图像加载 | 最新版 |
| freetype | 字体渲染 | 最新版 |
| xmake | 构建系统 | 2.5+ |
---

View File

@ -1,68 +1,215 @@
# Extra2D API 教程 - 01. 快速开始
# 01. 快速开始
## 简介
本教程将带你快速上手 Extra2D 引擎,通过一个简单的 Hello World 示例了解引擎的基本使用方法。
Extra2D 是一个跨平台的 2D 游戏引擎,支持 Windows (MinGW) 和 Nintendo Switch 平台。
## 示例代码
## 最小示例
完整示例位于 `examples/hello_world/main.cpp`
```cpp
#include <extra2d/extra2d.h>
using namespace extra2d;
int main(int argc, char **argv) {
// 1. 初始化日志系统
Logger::init();
Logger::setLevel(LogLevel::Debug);
// ============================================================================
// Hello World 场景
// ============================================================================
// 2. 获取应用实例
auto &app = Application::instance();
/**
* @brief Hello World 场景类
* 显示简单的 "Hello World" 文字
*/
class HelloWorldScene : public Scene {
public:
/**
* @brief 场景进入时调用
*/
void onEnter() override {
E2D_LOG_INFO("HelloWorldScene::onEnter - 进入场景");
// 3. 配置应用
AppConfig config;
config.title = "My Game";
config.width = 1280;
config.height = 720;
config.vsync = true;
config.fpsLimit = 60;
// 设置背景颜色为深蓝色
setBackgroundColor(Color(0.1f, 0.1f, 0.3f, 1.0f));
// 4. 初始化应用
if (!app.init(config)) {
E2D_LOG_ERROR("应用初始化失败!");
return -1;
// 加载字体(支持多种字体后备)
auto &resources = Application::instance().resources();
font_ = resources.loadFont("assets/font.ttf", 48, true);
if (!font_) {
E2D_LOG_ERROR("字体加载失败,文字渲染将不可用!");
return;
}
// 5. 进入场景
app.enterScene(makePtr<MyScene>());
// 创建 "你好世界" 文本组件 - 使用屏幕空间(固定位置,不随相机移动)
auto text1 = Text::create("你好世界", font_);
text1->withCoordinateSpace(CoordinateSpace::Screen)
->withScreenPosition(640.0f, 360.0f) // 屏幕中心
->withAnchor(0.5f, 0.5f) // 中心锚点,让文字中心对准位置
->withTextColor(Color(1.0f, 1.0f, 1.0f, 1.0f));
addChild(text1);
// 6. 运行应用
app.run();
// 创建提示文本组件 - 使用屏幕空间,固定在屏幕底部
auto text2 = Text::create("退出按键START 按钮)", font_);
text2->withCoordinateSpace(CoordinateSpace::Screen)
->withScreenPosition(640.0f, 650.0f) // 屏幕底部
->withAnchor(0.5f, 0.5f)
->withTextColor(Color(1.0f, 1.0f, 0.0f, 1.0f));
addChild(text2);
}
return 0;
/**
* @brief 每帧更新时调用
* @param dt 时间间隔(秒)
*/
void onUpdate(float dt) override {
Scene::onUpdate(dt);
// 检查退出按键
auto &input = Application::instance().input();
// 使用手柄 START 按钮退出 (GamepadButton::Start)
if (input.isButtonPressed(GamepadButton::Start)) {
E2D_LOG_INFO("退出应用 (START 按钮)");
Application::instance().quit();
}
}
private:
Ptr<FontAtlas> font_; // 字体图集
};
// ============================================================================
// 程序入口
// ============================================================================
int main(int argc, char **argv)
{
// 初始化日志系统
Logger::init();
Logger::setLevel(LogLevel::Debug);
// 获取应用实例
auto &app = Application::instance();
// 配置应用
AppConfig config;
config.title = "Easy2D - Hello World";
config.width = 1280;
config.height = 720;
config.vsync = true;
config.fpsLimit = 60;
// 初始化应用
if (!app.init(config)) {
E2D_LOG_ERROR("应用初始化失败!");
return -1;
}
// 进入 Hello World 场景
app.enterScene(makePtr<HelloWorldScene>());
// 运行应用
app.run();
return 0;
}
```
## 核心概念
### 应用生命周期
### 1. 应用生命周期
Extra2D 应用遵循以下生命周期:
```
Logger::init() → Application::init() → enterScene() → run() → 退出
初始化 (Application::init)
进入场景 (enterScene)
主循环 (run) → 更新 (onUpdate) → 渲染 (onRender)
退出 (quit)
```
### 场景生命周期
### 2. 场景系统
场景是游戏内容的容器,通过继承 `Scene` 类并重写以下方法:
| 方法 | 说明 |
|------|------|
| `onEnter()` | 场景进入时调用,用于初始化资源 |
| `onExit()` | 场景退出时调用,用于清理资源 |
| `onUpdate(dt)` | 每帧更新时调用,用于处理游戏逻辑 |
| `onRender(renderer)` | 渲染时调用,用于自定义绘制 |
### 3. 坐标空间
Extra2D 支持三种坐标空间:
```cpp
// 屏幕空间 - 固定位置,不随相机移动
text->withCoordinateSpace(CoordinateSpace::Screen)
->withScreenPosition(640.0f, 360.0f);
// 相机空间 - 跟随相机但保持相对偏移
text->withCoordinateSpace(CoordinateSpace::Camera)
->withCameraOffset(50.0f, 50.0f);
// 世界空间 - 随相机移动(默认行为)
text->withCoordinateSpace(CoordinateSpace::World)
->withPosition(100.0f, 100.0f);
```
onEnter() → onUpdate(dt) → onRender() → onExit()
### 4. 输入处理
支持手柄输入检测:
```cpp
auto &input = Application::instance().input();
// 检测按键按下(持续触发)
if (input.isButtonDown(GamepadButton::A)) { }
// 检测按键按下(单次触发)
if (input.isButtonPressed(GamepadButton::A)) { }
// 检测按键释放
if (input.isButtonReleased(GamepadButton::A)) { }
```
常用按键:
- `GamepadButton::A` - A 键
- `GamepadButton::B` - B 键
- `GamepadButton::X` - X 键
- `GamepadButton::Y` - Y 键
- `GamepadButton::Start` - + 键 (Switch)
- `GamepadButton::DPadUp/Down/Left/Right` - 方向键
### 5. 资源加载
通过资源管理器加载字体、纹理等资源:
```cpp
auto &resources = Application::instance().resources();
// 加载字体
auto font = resources.loadFont("assets/font.ttf", 48, true);
// 加载纹理
auto texture = resources.loadTexture("assets/image.png");
```
### 6. 日志系统
使用宏进行日志输出:
```cpp
E2D_LOG_DEBUG("调试信息");
E2D_LOG_INFO("普通信息");
E2D_LOG_WARN("警告信息");
E2D_LOG_ERROR("错误信息");
```
## 下一步
- [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)
- [02. 场景系统](./02_Scene_System.md) - 深入了解场景管理
- [03. 节点系统](./03_Node_System.md) - 学习节点和精灵的使用

View File

@ -1,9 +1,17 @@
# Extra2D API 教程 - 02. 场景系统
# 02. 场景系统
Extra2D 的场景系统提供了游戏内容的分层管理和切换功能。本教程将详细介绍场景的生命周期、切换和过渡效果。
## 完整示例
参考 `examples/push_box/` 中的实现:
- `StartScene.h/cpp` - 开始菜单场景
- `PlayScene.h/cpp` - 游戏主场景
- `SuccessScene.h/cpp` - 通关场景
## 场景基础
场景(Scene)是游戏的基本组织单位,负责管理节点和渲染。
### 创建场景
```cpp
@ -11,71 +19,54 @@
using namespace extra2d;
class MyScene : public Scene {
class GameScene : public Scene {
public:
// 场景进入时调用
void onEnter() override {
// 必须先调用父类的 onEnter()
// 必须先调用父类方法
Scene::onEnter();
// 设置背景
// 设置背景色
setBackgroundColor(Color(0.1f, 0.1f, 0.3f, 1.0f));
E2D_LOG_INFO("场景已进入");
// 设置视口大小用于UI布局
setViewportSize(1280.0f, 720.0f);
}
// 每帧更新时调用
void onUpdate(float dt) override {
Scene::onUpdate(dt);
// dt 是时间间隔(秒)
}
// 渲染时调用
void onRender(RenderBackend &renderer) override {
Scene::onRender(renderer);
// 绘制自定义内容
}
// 场景退出时调用
void onExit() override {
// 清理资源
removeAllChildren();
Scene::onExit();
}
void onUpdate(float dt) override {
Scene::onUpdate(dt);
// 游戏逻辑更新
}
};
```
### 重要提示
**必须调用 `Scene::onEnter()`**
```cpp
void onEnter() override {
Scene::onEnter(); // 必须调用!
// 你的初始化代码
}
```
如果不调用,会导致:
- `running_` 状态未设置
- 子节点无法正确注册到空间索引
- 碰撞检测失效
## 场景管理
### 进入场景
### 场景切换
```cpp
// 进入场景
app.enterScene(makePtr<MyScene>());
// 进入场景(无过渡)
app.enterScene(makePtr<GameScene>());
// 替换当前场景(带过渡效果)
app.scenes().replaceScene(
makePtr<PlayScene>(),
TransitionType::Fade, // 淡入淡出
0.25f // 过渡时间(秒)
);
// 进入场景(有过渡效果)
app.enterScene(makePtr<GameScene>(), TransitionType::Fade, 0.5f);
// 替换当前场景
app.scenes().replaceScene(makePtr<NewScene>());
// 推入场景(保留当前场景)
app.scenes().pushScene(makePtr<NewScene>());
// 弹出场景(返回上一个场景)
app.scenes().popScene();
```
### 场景过渡类型
### 过渡效果类型
```cpp
enum class TransitionType {
@ -88,84 +79,184 @@ enum class TransitionType {
};
```
## 场景配置
## 场景管理器
### 视口设置
通过 `app.scenes()` 访问场景管理器:
```cpp
void onEnter() override {
Scene::onEnter();
// 设置视口大小(影响坐标系)
setViewportSize(1280.0f, 720.0f);
// 设置背景颜色
setBackgroundColor(Colors::Black);
// 启用/禁用空间索引
setSpatialIndexingEnabled(true);
}
auto& scenes = app.scenes();
// 获取当前场景
auto current = scenes.currentScene();
// 获取场景栈深度
size_t depth = scenes.stackDepth();
// 清空场景栈
scenes.clearStack();
```
### 空间索引
## 场景生命周期
```cpp
// 获取空间管理器
auto &spatialManager = getSpatialManager();
// 切换空间索引策略
spatialManager.setStrategy(SpatialStrategy::QuadTree); // 四叉树
spatialManager.setStrategy(SpatialStrategy::SpatialHash); // 空间哈希
// 查询所有碰撞
auto collisions = queryCollisions();
```
创建场景 (makePtr<Scene>)
进入场景 (enterScene)
onEnter() - 初始化资源
主循环
├── onUpdate(dt) - 每帧更新
└── onRender(renderer) - 每帧渲染
退出场景
onExit() - 清理资源
场景销毁
```
## 完整示例
## 推箱子示例场景结构
```
┌─────────────────────────────────────┐
│ StartScene │
│ ┌─────────────────────────────┐ │
│ │ 开始菜单界面 │ │
│ │ - 新游戏 │ │
│ │ - 继续游戏 │ │
│ │ - 退出 │ │
│ └─────────────────────────────┘ │
└──────────────┬──────────────────────┘
│ 选择"新游戏"
┌─────────────────────────────────────┐
│ PlayScene │
│ ┌─────────────────────────────┐ │
│ │ 游戏主界面 │ │
│ │ - 地图渲染 │ │
│ │ - 玩家控制 │ │
│ │ - 关卡信息 │ │
│ └─────────────────────────────┘ │
└──────────────┬──────────────────────┘
│ 通关
┌─────────────────────────────────────┐
│ SuccessScene │
│ ┌─────────────────────────────┐ │
│ │ 通关界面 │ │
│ │ - 显示成绩 │ │
│ │ - 下一关/返回菜单 │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────────┘
```
## 代码示例:菜单场景
```cpp
class GameScene : public Scene {
class MenuScene : public Scene {
public:
void onEnter() override {
Scene::onEnter();
// 设置视口和背景
setViewportSize(1280.0f, 720.0f);
setBackgroundColor(Color(0.1f, 0.2f, 0.3f, 1.0f));
auto& app = Application::instance();
auto& resources = app.resources();
// 启用空间索引
setSpatialIndexingEnabled(true);
// 加载背景
auto bgTex = resources.loadTexture("assets/bg.jpg");
if (bgTex) {
auto bg = Sprite::create(bgTex);
bg->setAnchor(0.0f, 0.0f);
addChild(bg);
}
E2D_LOG_INFO("游戏场景已加载");
// 加载字体
font_ = resources.loadFont("assets/font.ttf", 28, true);
// 创建菜单按钮
createMenuButtons();
}
void onUpdate(float dt) override {
Scene::onUpdate(dt);
// 检查退出按键
auto &input = Application::instance().input();
if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_START)) {
Application::instance().quit();
auto& input = Application::instance().input();
// 方向键导航
if (input.isButtonPressed(GamepadButton::DPadUp)) {
selectedIndex_ = (selectedIndex_ - 1 + menuCount_) % menuCount_;
updateMenuColors();
}
else if (input.isButtonPressed(GamepadButton::DPadDown)) {
selectedIndex_ = (selectedIndex_ + 1) % menuCount_;
updateMenuColors();
}
// A键确认
if (input.isButtonPressed(GamepadButton::A)) {
executeMenuItem();
}
}
void onRender(RenderBackend &renderer) override {
Scene::onRender(renderer);
private:
void createMenuButtons() {
float centerX = 640.0f;
float startY = 300.0f;
float spacing = 50.0f;
// 绘制 FPS
auto &app = Application::instance();
std::string fpsText = "FPS: " + std::to_string(app.fps());
// ...
for (int i = 0; i < menuCount_; ++i) {
auto btn = Button::create();
btn->setFont(font_);
btn->setAnchor(0.5f, 0.5f);
btn->setPosition(centerX, startY + i * spacing);
addChild(btn);
buttons_.push_back(btn);
}
buttons_[0]->setText("开始游戏");
buttons_[1]->setText("设置");
buttons_[2]->setText("退出");
updateMenuColors();
}
void onExit() override {
E2D_LOG_INFO("游戏场景退出");
Scene::onExit();
void updateMenuColors() {
for (int i = 0; i < buttons_.size(); ++i) {
auto color = (i == selectedIndex_) ? Colors::Red : Colors::White;
buttons_[i]->setTextColor(color);
}
}
void executeMenuItem() {
switch (selectedIndex_) {
case 0:
Application::instance().scenes().replaceScene(
makePtr<GameScene>(), TransitionType::Fade, 0.25f);
break;
case 1:
// 打开设置
break;
case 2:
Application::instance().quit();
break;
}
}
Ptr<FontAtlas> font_;
std::vector<Ptr<Button>> buttons_;
int selectedIndex_ = 0;
int menuCount_ = 3;
};
```
## 最佳实践
1. **始终在 onEnter 中调用 Scene::onEnter()** - 确保场景正确初始化
2. **在 onExit 中清理资源** - 避免内存泄漏
3. **使用过渡效果** - 提升用户体验
4. **分离场景逻辑** - 每个场景负责自己的功能
## 下一步
- [03. 节点系统](03_Node_System.md)
- [04. 资源管理](04_Resource_Management.md)
- [03. 节点系统](./03_Node_System.md) - 学习节点和精灵的使用
- [04. 资源管理](./04_Resource_Management.md) - 深入了解资源加载

View File

@ -1,219 +1,353 @@
# Extra2D API 教程 - 03. 节点系统
# 03. 节点系统
## 节点基础
Extra2D 的节点系统是构建游戏对象的基础。所有可见的游戏元素都是节点的子类。
节点(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);
// 自定义渲染
}
};
```
Node (基类)
├── Sprite (精灵)
├── Text (文本)
├── Button (按钮)
├── Widget (UI控件基类)
│ ├── Label (标签)
│ ├── CheckBox (复选框)
│ ├── RadioButton (单选按钮)
│ ├── Slider (滑块)
│ └── ProgressBar (进度条)
└── 自定义节点...
```
## 节点层级
## 基础节点操作
### 添加节点
### 创建和添加节点
```cpp
void onEnter() override {
Scene::onEnter();
// 创建子节点
auto child = makePtr<MyNode>();
// 添加到场景
addChild(child);
// 在指定位置添加
addChild(child, 0); // z-order = 0
}
// 创建精灵
auto sprite = Sprite::create(texture);
sprite->setPosition(Vec2(100, 200));
sprite->setAnchor(Vec2(0.5f, 0.5f)); // 中心锚点
addChild(sprite);
// 创建文本
auto text = Text::create("Hello World", font);
text->setPosition(Vec2(400, 300));
text->setTextColor(Color(1, 1, 1, 1));
addChild(text);
```
### 移除子节点
### 节点属性
```cpp
// 移除指定子节点
removeChild(child);
// 位置
node->setPosition(Vec2(x, y));
Vec2 pos = node->getPosition();
// 移除所有子节点
removeAllChildren();
// 旋转(角度)
node->setRotation(45.0f);
float angle = node->getRotation();
// 通过名称移除
removeChildByName("myNode");
// 缩放
node->setScale(Vec2(2.0f, 2.0f));
Vec2 scale = node->getScale();
// 锚点0,0 左上角0.5,0.5 中心1,1 右下角)
node->setAnchor(Vec2(0.5f, 0.5f));
// 可见性
node->setVisible(true);
bool visible = node->isVisible();
// Z轴顺序
node->setZOrder(10);
```
### 获取子节点
### 链式调用Builder 模式)
```cpp
// 获取子节点数量
size_t count = getChildren().size();
// 通过名称查找
auto node = getChildByName("myNode");
// 遍历子节点
for (auto &child : getChildren()) {
// 处理子节点
}
// 使用链式调用快速配置节点
auto text = Text::create("标题", font)
->withPosition(640.0f, 100.0f)
->withAnchor(0.5f, 0.5f)
->withTextColor(Color(1.0f, 1.0f, 0.0f, 1.0f))
->withCoordinateSpace(CoordinateSpace::Screen);
addChild(text);
```
## 空间索引
### 启用空间索引
```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)) {
// 边界框相交
}
```
## 精灵节点
## 精灵Sprite
### 创建精灵
```cpp
// 加载纹理
auto texture = resources.loadTexture("assets/player.png");
auto& resources = Application::instance().resources();
// 创建精灵
// 从纹理创建
auto texture = resources.loadTexture("assets/player.png");
auto sprite = Sprite::create(texture);
// 设置位置
sprite->setPosition(Vec2(640.0f, 360.0f));
// 设置精灵属性
sprite->setPosition(Vec2(400, 300));
sprite->setAnchor(Vec2(0.5f, 0.5f));
// 设置锚点(中心)
sprite->setAnchor(0.5f, 0.5f);
// 添加到场景
addChild(sprite);
// 切换纹理
auto newTexture = resources.loadTexture("assets/player2.png");
sprite->setTexture(newTexture);
```
### 精灵动画
```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"));
// 创建帧动画
auto anim = AnimatedSprite::createFromGrid(
"player.png", // 纹理
32, 32, // 单帧宽高
100.0f, // 帧间隔(ms)
8 // 总帧数
);
// 播放动画
sprite->playAnimation(animation, true); // true = 循环播放
anim->play();
// 设置帧范围
anim->setFrameRange(0, 3);
// 循环播放
anim->setLoop(true);
// 停止动画
sprite->stopAnimation();
anim->stop();
```
## 完整示例
## 文本Text
### 创建文本
```cpp
// 加载字体
auto font = resources.loadFont("assets/font.ttf", 24, true);
// 创建文本
auto text = Text::create("Hello World", font);
text->setPosition(Vec2(400, 300));
addChild(text);
// 动态修改文本
text->setText("新的文本内容");
// 使用格式化文本
text->setFormat("得分: %d", score);
```
### 文本样式
```cpp
text->setTextColor(Color(1.0f, 1.0f, 1.0f, 1.0f)); // 颜色
text->setFontSize(48); // 字体大小
text->setAnchor(Vec2(0.5f, 0.5f)); // 锚点
// 坐标空间
text->withCoordinateSpace(CoordinateSpace::Screen) // 屏幕空间
->withScreenPosition(100.0f, 50.0f);
```
## 按钮Button
### 创建按钮
```cpp
auto button = Button::create();
button->setFont(font);
button->setText("点击我");
button->setPosition(Vec2(400, 300));
button->setCustomSize(200.0f, 60.0f);
// 设置颜色
button->setTextColor(Colors::White);
button->setBackgroundColor(
Colors::Blue, // 正常状态
Colors::Green, // 悬停状态
Colors::Red // 按下状态
);
// 设置点击回调
button->onClick([]() {
E2D_LOG_INFO("按钮被点击!");
});
addChild(button);
```
### 透明按钮(用于菜单)
```cpp
// 创建纯文本按钮(透明背景)
auto menuBtn = Button::create();
menuBtn->setFont(font);
menuBtn->setText("菜单项");
menuBtn->setTextColor(Colors::Black);
menuBtn->setBackgroundColor(
Colors::Transparent,
Colors::Transparent,
Colors::Transparent
);
menuBtn->setBorder(Colors::Transparent, 0.0f);
menuBtn->setAnchor(0.5f, 0.5f);
menuBtn->setPosition(centerX, centerY);
addChild(menuBtn);
```
## 节点层级管理
### 父子关系
```cpp
// 添加子节点
parent->addChild(child);
// 移除子节点
parent->removeChild(child);
// 移除所有子节点
parent->removeAllChildren();
// 获取父节点
Node* parent = child->getParent();
// 获取子节点列表
const auto& children = parent->getChildren();
```
### Z轴顺序
```cpp
// 设置Z轴顺序值越大越在上层
node->setZOrder(10);
// 重新排序子节点
parent->reorderChild(child, newZOrder);
```
## 自定义节点
### 继承 Node 创建自定义节点
```cpp
class Player : public Node {
public:
Player() {
setSpatialIndexed(true);
// 加载精灵
auto &resources = Application::instance().resources();
auto texture = resources.loadTexture("assets/player.png");
static Ptr<Player> create(Ptr<Texture> texture) {
auto player = makePtr<Player>();
if (player->init(texture)) {
return player;
}
return nullptr;
}
bool init(Ptr<Texture> texture) {
sprite_ = Sprite::create(texture);
sprite_->setAnchor(0.5f, 0.5f);
addChild(sprite_);
return true;
}
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;
}
void update(float dt) override {
// 更新逻辑
velocity_.y += gravity_ * dt;
setPosition(getPosition() + velocity_ * dt);
}
Rect getBoundingBox() const override {
Vec2 pos = getPosition();
return Rect(pos.x - 25.0f, pos.y - 25.0f, 50.0f, 50.0f);
void jump() {
velocity_.y = jumpForce_;
}
private:
Ptr<Sprite> sprite_;
Vec2 velocity_{100.0f, 100.0f};
Vec2 velocity_;
float gravity_ = -980.0f;
float jumpForce_ = 500.0f;
};
// 使用
auto player = Player::create(texture);
scene->addChild(player);
```
## 空间索引(碰撞检测)
### 启用空间索引
```cpp
class CollidableNode : public Node {
public:
CollidableNode() {
// 启用空间索引
setSpatialIndexed(true);
}
// 必须实现 getBoundingBox
Rect getBoundingBox() const override {
Vec2 pos = getPosition();
return Rect(pos.x - 25, pos.y - 25, 50, 50);
}
};
```
### 查询碰撞
```cpp
// 在场景中查询所有碰撞
auto collisions = scene->queryCollisions();
for (const auto& [nodeA, nodeB] : collisions) {
// 处理碰撞
handleCollision(nodeA, nodeB);
}
```
## 完整示例
参考 `examples/push_box/PlayScene.cpp` 中的节点使用:
```cpp
void PlayScene::onEnter() {
Scene::onEnter();
auto& resources = app.resources();
// 加载纹理资源
texWall_ = resources.loadTexture("assets/images/wall.gif");
texBox_ = resources.loadTexture("assets/images/box.gif");
// 创建地图层
mapLayer_ = makePtr<Node>();
addChild(mapLayer_);
// 创建地图元素
for (int y = 0; y < mapHeight; ++y) {
for (int x = 0; x < mapWidth; ++x) {
char cell = map_[y][x];
if (cell == '#') {
auto wall = Sprite::create(texWall_);
wall->setPosition(x * tileSize, y * tileSize);
mapLayer_->addChild(wall);
}
else if (cell == '$') {
auto box = Sprite::create(texBox_);
box->setPosition(x * tileSize, y * tileSize);
mapLayer_->addChild(box);
}
}
}
// 创建UI文本
font28_ = resources.loadFont("assets/font.ttf", 28, true);
levelText_ = Text::create("Level: 1", font28_);
levelText_->setPosition(50, 30);
addChild(levelText_);
}
```
## 下一步
- [04. 资源管理](04_Resource_Management.md)
- [05. 输入处理](05_Input_Handling.md)
- [04. 资源管理](./04_Resource_Management.md) - 深入了解资源加载
- [05. 输入处理](./05_Input_Handling.md) - 学习输入处理
- [06. 碰撞检测](./06_Collision_Detection.md) - 学习碰撞检测系统

View File

@ -1,148 +1,167 @@
# Extra2D API 教程 - 04. 资源管理
# 04. 资源管理
Extra2D 提供了统一的资源管理系统,用于加载和管理游戏中的各种资源。
## 资源管理器
Extra2D 使用资源管理器来统一加载和管理资源。
### 获取资源管理器
通过 `Application::instance().resources()` 访问资源管理器:
```cpp
auto &resources = Application::instance().resources();
auto& resources = Application::instance().resources();
```
## 字体资源
## 支持的资源类型
### 加载字体
| 资源类型 | 加载方法 | 说明 |
|---------|---------|------|
| 纹理 | `loadTexture()` | 图片文件 (PNG, JPG, etc.) |
| 字体 | `loadFont()` | TrueType 字体文件 |
| 音频 | `loadSound()` / `loadMusic()` | 音频文件 |
```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");
auto texture = resources.loadTexture("assets/images/player.png");
if (!texture) {
E2D_LOG_ERROR("纹理加载失败!");
if (texture) {
// 创建精灵
auto sprite = Sprite::create(texture);
addChild(sprite);
}
```
### 创建精灵
### 纹理缓存
资源管理器会自动缓存已加载的纹理,多次加载同一文件会返回缓存的实例:
```cpp
auto sprite = Sprite::create(texture);
sprite->setPosition(Vec2(640.0f, 360.0f));
addChild(sprite);
// 第一次加载 - 从文件读取
auto tex1 = resources.loadTexture("assets/image.png");
// 第二次加载 - 返回缓存
auto tex2 = resources.loadTexture("assets/image.png");
// tex1 和 tex2 指向同一个纹理对象
```
## 音效资源
## 字体加载
### 加载音效
### 基本用法
```cpp
// 加载音效
auto sound = resources.loadSound("assets/jump.wav");
// 加载字体(指定字号)
auto font24 = resources.loadFont("assets/font.ttf", 24, true);
// 播放音效
sound->play();
// 循环播放
sound->play(true);
// 停止播放
sound->stop();
// 创建文本
auto text = Text::create("Hello World", font24);
addChild(text);
```
## 资源路径解析
### 字体后备
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");
// 加载主字体和后备字体
auto mainFont = resources.loadFont("assets/main.ttf", 24, true);
auto fallbackFont = resources.loadFont("assets/fallback.ttf", 24, true);
// 设置后备字体
mainFont->setFallback(fallbackFont);
```
## 资源路径
### 路径格式
```cpp
// 相对路径(相对于工作目录)
auto tex = resources.loadTexture("assets/images/player.png");
// Switch 平台使用 romfs
auto tex = resources.loadTexture("romfs:/images/player.png");
// SD 卡路径
auto tex = resources.loadTexture("sdmc:/switch/game/images/player.png");
```
### 路径辅助函数
```cpp
// 获取平台特定的资源路径
std::string path = ResourceManager::getPlatformPath("images/player.png");
// Windows: "assets/images/player.png"
// Switch: "romfs:/images/player.png"
```
## 资源释放
### 自动释放
资源使用智能指针管理,当没有引用时会自动释放:
```cpp
{
auto tex = resources.loadTexture("assets/temp.png");
// 使用纹理...
} // 超出作用域,如果没有其他引用,纹理自动释放
```
### 手动清理缓存
```cpp
// 清理未使用的资源
resources.cleanupUnused();
// 清空所有缓存(谨慎使用)
resources.clearCache();
```
## 完整示例
参考 `examples/push_box/StartScene.cpp`
```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 StartScene::onEnter() {
Scene::onEnter();
auto& app = Application::instance();
auto& resources = app.resources();
// 加载背景纹理
auto bgTex = resources.loadTexture("assets/images/start.jpg");
if (bgTex) {
auto background = Sprite::create(bgTex);
background->setAnchor(0.0f, 0.0f);
addChild(background);
}
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);
}
// 加载音效图标纹理
auto soundOn = resources.loadTexture("assets/images/soundon.png");
auto soundOff = resources.loadTexture("assets/images/soundoff.png");
if (soundOn && soundOff) {
soundIcon_ = Sprite::create(g_SoundOpen ? soundOn : soundOff);
addChild(soundIcon_);
}
private:
Ptr<FontAtlas> titleFont_;
Ptr<FontAtlas> infoFont_;
Ptr<Texture> playerTexture_;
Ptr<Texture> enemyTexture_;
Ptr<Sprite> player_;
};
// 加载字体
font_ = resources.loadFont("assets/font.ttf", 28, true);
// 创建按钮...
}
```
## 最佳实践
1. **预加载资源** - 在场景 `onEnter()` 中加载所需资源
2. **检查资源有效性** - 始终检查加载结果是否为 nullptr
3. **复用资源** - 多次使用同一资源时保存指针,避免重复加载
4. **合理设置字号** - 字体加载时会生成对应字号的图集
## 下一步
- [05. 输入处理](05_Input_Handling.md)
- [06. 碰撞检测](06_Collision_Detection.md)
- [05. 输入处理](./05_Input_Handling.md) - 学习输入处理
- [06. 碰撞检测](./06_Collision_Detection.md) - 学习碰撞检测系统

View File

@ -1,216 +1,189 @@
# Extra2D API 教程 - 05. 输入处理
# 05. 输入处理
## 输入系统
Extra2D 提供了统一的输入处理系统,支持手柄、键盘等多种输入设备。
Extra2D 提供统一的输入处理接口,支持键盘和游戏手柄。
## 输入管理器
### 获取输入管理器
通过 `Application::instance().input()` 访问输入管理器:
```cpp
auto &input = Application::instance().input();
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();
}
// 按键是否按下(持续触发)
if (input.isButtonDown(GamepadButton::A)) {
// 每帧都会触发,只要按键保持按下
}
// 按键是否刚按下(单次触发)
if (input.isButtonPressed(GamepadButton::A)) {
// 只在按下瞬间触发一次
}
// 按键是否刚释放
if (input.isButtonReleased(GamepadButton::A)) {
// 只在释放瞬间触发一次
}
```
### 检测按键按住
### 常用按键
```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 |
| 按键 | 说明 | Switch 对应 |
|------|------|------------|
| `GamepadButton::A` | A 键 | A 键 |
| `GamepadButton::B` | B 键 | B 键 |
| `GamepadButton::X` | X 键 | X 键 |
| `GamepadButton::Y` | Y 键 | Y 键 |
| `GamepadButton::Start` | 开始键 | + 键 |
| `GamepadButton::Select` | 选择键 | - 键 |
| `GamepadButton::DPadUp` | 方向上 | 方向键上 |
| `GamepadButton::DPadDown` | 方向下 | 方向键下 |
| `GamepadButton::DPadLeft` | 方向左 | 方向键左 |
| `GamepadButton::DPadRight` | 方向右 | 方向键右 |
| `GamepadButton::LeftStick` | 左摇杆按下 | L3 |
| `GamepadButton::RightStick` | 右摇杆按下 | R3 |
| `GamepadButton::LeftShoulder` | 左肩键 | L |
| `GamepadButton::RightShoulder` | 右肩键 | R |
## 摇杆输入
### 获取摇杆值
```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);
}
}
// 获取左摇杆位置(范围 -1.0 到 1.0
Vec2 leftStick = input.getLeftStick();
// 获取右摇杆位置
Vec2 rightStick = input.getRightStick();
// 应用摇杆输入
float speed = 200.0f;
player->setPosition(player->getPosition() + leftStick * speed * 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();
}
}
// 设置摇杆死区(默认 0.15
input.setStickDeadZone(0.2f);
```
## 完整示例
### 菜单导航
参考 `examples/push_box/StartScene.cpp`
```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();
}
void StartScene::onUpdate(float dt) {
Scene::onUpdate(dt);
auto& input = Application::instance().input();
// 方向键上下切换选择
if (input.isButtonPressed(GamepadButton::DPadUp)) {
selectedIndex_ = (selectedIndex_ - 1 + menuCount_) % menuCount_;
updateMenuColors();
}
else if (input.isButtonPressed(GamepadButton::DPadDown)) {
selectedIndex_ = (selectedIndex_ + 1) % menuCount_;
updateMenuColors();
}
private:
float speed_ = 200.0f;
void jump() {
// 跳跃逻辑
// A键确认
if (input.isButtonPressed(GamepadButton::A)) {
executeMenuItem();
}
void attack() {
// 攻击逻辑
// X键切换音效
if (input.isButtonPressed(GamepadButton::X)) {
g_SoundOpen = !g_SoundOpen;
AudioManager::instance().setEnabled(g_SoundOpen);
updateSoundIcon();
}
};
}
```
### 玩家移动
```cpp
void Player::update(float dt) {
auto& input = Application::instance().input();
Vec2 moveDir;
// 方向键移动
if (input.isButtonDown(GamepadButton::DPadLeft)) {
moveDir.x -= 1;
}
if (input.isButtonDown(GamepadButton::DPadRight)) {
moveDir.x += 1;
}
if (input.isButtonDown(GamepadButton::DPadUp)) {
moveDir.y -= 1;
}
if (input.isButtonDown(GamepadButton::DPadDown)) {
moveDir.y += 1;
}
// 摇杆移动
Vec2 stick = input.getLeftStick();
if (stick.length() > 0.1f) {
moveDir = stick;
}
// 应用移动
if (moveDir.length() > 0) {
moveDir.normalize();
setPosition(getPosition() + moveDir * speed_ * dt);
}
// 跳跃
if (input.isButtonPressed(GamepadButton::A)) {
jump();
}
}
```
## 输入映射
### 自定义按键映射
```cpp
// 定义动作
enum class Action {
Jump,
Attack,
Pause
};
// 映射按键到动作
std::unordered_map<Action, GamepadButton> actionMap = {
{Action::Jump, GamepadButton::A},
{Action::Attack, GamepadButton::B},
{Action::Pause, GamepadButton::Start}
};
// 检查动作
bool isActionPressed(Action action) {
auto& input = Application::instance().input();
return input.isButtonPressed(actionMap[action]);
}
```
## 最佳实践
1. **使用 isButtonPressed 进行菜单操作** - 避免持续触发
2. **使用 isButtonDown 进行移动控制** - 实现流畅移动
3. **支持多种输入方式** - 同时支持方向键和摇杆
4. **添加输入缓冲** - 提升操作手感
## 下一步
- [06. 碰撞检测](06_Collision_Detection.md)
- [07. UI 系统](07_UI_System.md)
- [06. 碰撞检测](./06_Collision_Detection.md) - 学习碰撞检测系统
- [07. UI 系统](./07_UI_System.md) - 学习 UI 控件使用

View File

@ -1,81 +1,90 @@
# Extra2D API 教程 - 06. 碰撞检测
# 06. 碰撞检测
## 空间索引系统
Extra2D 提供了基于空间索引的高效碰撞检测系统,支持四叉树和空间哈希两种策略。
Extra2D 内置了空间索引系统,用于高效地进行碰撞检测。
## 完整示例
### 启用空间索引
参考示例代码:
- `examples/collision_demo/main.cpp` - 基础碰撞检测演示
- `examples/spatial_index_demo/main.cpp` - 空间索引性能演示
## 启用碰撞检测
### 1. 创建可碰撞节点
```cpp
void onEnter() override {
Scene::onEnter();
// 启用空间索引
setSpatialIndexingEnabled(true);
}
```
## 碰撞节点
### 创建可碰撞节点
```cpp
class PhysicsNode : public Node {
class CollidableBox : public Node {
public:
PhysicsNode(float size, const Color &color)
: size_(size), color_(color), isColliding_(false) {
// 启用空间索引(关键!)
CollidableBox(float width, float height, const Color& color)
: width_(width), height_(height), color_(color), isColliding_(false) {
// 启用空间索引 - 这是关键!
setSpatialIndexed(true);
}
// 必须实现 getBoundingBox()
// 必须实现 getBoundingBox 方法
Rect getBoundingBox() const override {
Vec2 pos = getPosition();
return Rect(pos.x - size_ / 2, pos.y - size_ / 2, size_, size_);
return Rect(pos.x - width_ / 2, pos.y - height_ / 2, width_, height_);
}
void setColliding(bool colliding) { isColliding_ = colliding; }
void onRender(RenderBackend &renderer) override {
void onRender(RenderBackend& renderer) override {
Vec2 pos = getPosition();
// 碰撞时变红色
Color fillColor = isColliding_ ? Color(1.0f, 0.2f, 0.2f, 0.9f) : color_;
renderer.fillRect(Rect(pos.x - size_ / 2, pos.y - size_ / 2, size_, size_),
fillColor);
Color fillColor = isColliding_ ? Color(1.0f, 0.2f, 0.2f, 0.8f) : color_;
renderer.fillRect(
Rect(pos.x - width_ / 2, pos.y - height_ / 2, width_, height_),
fillColor
);
// 绘制边框
Color borderColor = isColliding_ ? Color(1.0f, 0.0f, 0.0f, 1.0f)
: Color(1.0f, 1.0f, 1.0f, 0.5f);
renderer.drawRect(
Rect(pos.x - width_ / 2, pos.y - height_ / 2, width_, height_),
borderColor, 2.0f
);
}
private:
float size_;
float width_, height_;
Color color_;
bool isColliding_;
};
```
## 碰撞检测
### 查询所有碰撞
### 2. 执行碰撞检测
```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);
class GameScene : public Scene {
public:
void onUpdate(float dt) override {
Scene::onUpdate(dt);
// 清除之前的碰撞状态
for (auto& box : boxes_) {
box->setColliding(false);
}
if (auto boxB = dynamic_cast<PhysicsNode *>(nodeB)) {
boxB->setColliding(true);
// 使用场景的空间索引查询所有碰撞
auto collisions = queryCollisions();
// 处理碰撞
for (const auto& [nodeA, nodeB] : collisions) {
if (auto boxA = dynamic_cast<CollidableBox*>(nodeA)) {
boxA->setColliding(true);
}
if (auto boxB = dynamic_cast<CollidableBox*>(nodeB)) {
boxB->setColliding(true);
}
}
}
}
private:
std::vector<Ptr<CollidableBox>> boxes_;
};
```
## 空间索引策略
@ -83,141 +92,105 @@ void performCollisionDetection() {
### 切换策略
```cpp
void onEnter() override {
Scene::onEnter();
// 启用空间索引
setSpatialIndexingEnabled(true);
// 设置空间索引策略
auto &spatialManager = getSpatialManager();
spatialManager.setStrategy(SpatialStrategy::QuadTree); // 四叉树
// 或
spatialManager.setStrategy(SpatialStrategy::SpatialHash); // 空间哈希
}
// 获取空间管理器
auto& spatialManager = getSpatialManager();
// 切换策略
void toggleStrategy() {
auto &spatialManager = getSpatialManager();
SpatialStrategy current = spatialManager.getCurrentStrategy();
if (current == SpatialStrategy::QuadTree) {
spatialManager.setStrategy(SpatialStrategy::SpatialHash);
} else {
spatialManager.setStrategy(SpatialStrategy::QuadTree);
}
}
// 切换到四叉树
spatialManager.setStrategy(SpatialStrategy::QuadTree);
// 切换到空间哈希
spatialManager.setStrategy(SpatialStrategy::SpatialHash);
// 获取当前策略名称
const char* name = spatialManager.getStrategyName();
```
### 策略对比
| 策略 | 适用场景 | 特点 |
|------|----------|------|
| QuadTree | 节点分布不均匀 | 适合稀疏场景 |
| SpatialHash | 节点分布均匀 | 适合密集场景 |
|------|---------|------|
| QuadTree | 节点分布不均匀 | 分层划分,适合稀疏分布 |
| SpatialHash | 节点分布均匀 | 均匀网格,适合密集分布 |
## 完整示例
## 性能演示
`examples/spatial_index_demo/main.cpp` 展示了空间索引的性能优势:
```cpp
class CollisionScene : public Scene {
class SpatialIndexDemoScene : public Scene {
public:
void onEnter() override {
Scene::onEnter();
// 启用空间索引
setSpatialIndexingEnabled(true);
// 创建碰撞节点
// 创建100个碰撞节点
createNodes(100);
E2D_LOG_INFO("创建了 {} 个碰撞节点", nodes_.size());
E2D_LOG_INFO("空间索引已启用: {}", isSpatialIndexingEnabled());
}
void onUpdate(float dt) override {
Scene::onUpdate(dt);
// 更新节点位置
for (auto &node : nodes_) {
node->update(dt);
// 更新所有节点位置
for (auto& node : nodes_) {
node->update(dt, screenWidth_, screenHeight_);
}
// 行碰撞检测
// 使用空间索引进行碰撞检测
performCollisionDetection();
}
void onRender(RenderBackend &renderer) override {
Scene::onRender(renderer);
// 绘制碰撞统计
std::string text = "Collisions: " + std::to_string(collisionCount_);
// ...
// 按 X 键切换索引策略
auto& input = Application::instance().input();
if (input.isButtonPressed(GamepadButton::X)) {
toggleSpatialStrategy();
}
}
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_) {
// 清除之前的碰撞状态
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)) {
// 标记碰撞的节点
for (const auto& [nodeA, nodeB] : collisions) {
if (auto boxA = dynamic_cast<PhysicsNode*>(nodeA)) {
boxA->setColliding(true);
}
if (auto boxB = dynamic_cast<PhysicsNode *>(nodeB)) {
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_);
void toggleSpatialStrategy() {
auto& spatialManager = getSpatialManager();
SpatialStrategy currentStrategy = spatialManager.getCurrentStrategy();
if (currentStrategy == SpatialStrategy::QuadTree) {
spatialManager.setStrategy(SpatialStrategy::SpatialHash);
E2D_LOG_INFO("切换到空间哈希策略");
} else {
spatialManager.setStrategy(SpatialStrategy::QuadTree);
E2D_LOG_INFO("切换到四叉树策略");
}
}
};
```
## 关键要点
1. **必须调用 `setSpatialIndexed(true)`** - 启用节点的空间索引
2. **必须实现 `getBoundingBox()`** - 返回准确的边界框
3. **在 `onEnter()` 中调用 `Scene::onEnter()`** - 确保节点正确注册到空间索引
4. **使用 `queryCollisions()`** - 自动利用空间索引优化检测
## 下一步
- [07. UI 系统](07_UI_System.md)
- [08. 音频系统](08_Audio_System.md)
- [07. UI 系统](./07_UI_System.md) - 学习 UI 控件使用
- [08. 音频系统](./08_Audio_System.md) - 学习音频播放

View File

@ -1,337 +1,606 @@
# Extra2D API 教程 - 07. UI 系统
# 07. UI 系统
## 按钮 (Button)
Extra2D 提供了一套完整的 UI 系统,支持按钮、文本、标签、复选框、单选按钮、滑块、进度条等常用控件。
Extra2D 提供 Button 组件用于创建交互式按钮。
## UI 控件类型
```
Widget (UI控件基类)
├── Button (按钮)
├── Text (文本)
├── Label (标签)
├── CheckBox (复选框)
├── RadioButton (单选按钮)
├── Slider (滑块)
└── ProgressBar (进度条)
```
## 坐标空间
UI 控件支持三种坐标空间:
```cpp
enum class CoordinateSpace {
World, // 世界空间 - 随相机移动(默认)
Screen, // 屏幕空间 - 固定位置,不随相机移动
Camera, // 相机空间 - 相对于相机位置的偏移
};
```
设置坐标空间:
```cpp
// 屏幕空间UI 常用)
button->setCoordinateSpace(CoordinateSpace::Screen);
button->setScreenPosition(100.0f, 50.0f);
// 或使用链式调用
button->withCoordinateSpace(CoordinateSpace::Screen)
->withScreenPosition(100.0f, 50.0f);
```
## 通用链式调用方法
所有 UI 组件都支持以下链式调用方法:
```cpp
widget->withPosition(x, y) // 设置位置
->withAnchor(x, y) // 设置锚点 (0-1)
->withCoordinateSpace(space) // 设置坐标空间
->withScreenPosition(x, y) // 设置屏幕位置
->withCameraOffset(x, y); // 设置相机偏移
```
## 按钮Button
### 创建按钮
```cpp
// 创建按钮
auto& resources = Application::instance().resources();
auto font = resources.loadFont("assets/font.ttf", 24);
// 方式1简单创建
auto button = Button::create();
button->setText("点击我");
button->setFont(font);
button->setPosition(Vec2(400, 300));
// 设置位置
button->setPosition(Vec2(640.0f, 360.0f));
// 方式2链式调用创建
auto button = Button::create()
->withText("点击我")
->withFont(font)
->withPosition(400, 300)
->withSize(200, 60)
->withTextColor(Colors::White)
->withBackgroundColor(Colors::Blue, Colors::Green, Colors::Red)
->withBorder(Colors::White, 2.0f);
// 设置锚点(中心)
button->setAnchor(0.5f, 0.5f);
// 设置点击回调
button->setOnClick([]() {
E2D_LOG_INFO("按钮被点击!");
});
// 添加到场景
addChild(button);
```
### 设置按钮文本
### 按钮属性设置
```cpp
// 加载字体
auto font = resources.loadFont("assets/font.ttf", 28, true);
// 设置按钮字体和文本
// 文本和字体
button->setText("新文本");
button->setFont(font);
button->setText("点击我");
button->setTextColor(Colors::Black);
```
button->setTextColor(Colors::White);
### 设置按钮样式
// 尺寸和内边距
button->setCustomSize(200.0f, 60.0f);
button->setPadding(Vec2(10.0f, 5.0f));
```cpp
// 设置背景颜色(普通、悬停、按下)
// 背景颜色(正常、悬停、按下三种状态)
button->setBackgroundColor(
Colors::White, // 普通状态
Colors::LightGray, // 悬停状态
Colors::Gray // 按下状态
Colors::Blue, // 正常状态
Colors::Green, // 悬停状态
Colors::Red // 按下状态
);
// 设置边框
button->setBorder(Colors::Black, 2.0f);
// 边框
button->setBorder(Colors::White, 2.0f);
// 设置内边距
button->setPadding(Vec2(20.0f, 10.0f));
// 圆角
button->setRoundedCornersEnabled(true);
button->setCornerRadius(8.0f);
// 设置固定大小
button->setCustomSize(200.0f, 50.0f);
// 图片背景
button->setBackgroundImage(normalTex, hoverTex, pressedTex);
button->setBackgroundImageScaleMode(ImageScaleMode::ScaleFit);
// 悬停光标
button->setHoverCursor(CursorShape::Hand);
```
### 透明按钮
### 图片缩放模式
```cpp
// 创建透明按钮(仅文本可点击)
auto button = Button::create();
button->setFont(font);
button->setText("菜单项");
button->setTextColor(Colors::Black);
enum class ImageScaleMode {
Original, // 使用原图大小
Stretch, // 拉伸填充
ScaleFit, // 等比缩放,保持完整显示
ScaleFill // 等比缩放,填充整个区域(可能裁剪)
};
```
// 透明背景
button->setBackgroundColor(
Colors::Transparent,
Colors::Transparent,
Colors::Transparent
### 透明按钮(菜单项)
```cpp
// 创建纯文本按钮(透明背景,用于菜单)
auto menuBtn = Button::create();
menuBtn->setFont(font);
menuBtn->setText("新游戏");
menuBtn->setTextColor(Colors::Black);
menuBtn->setBackgroundColor(
Colors::Transparent, // 正常
Colors::Transparent, // 悬停
Colors::Transparent // 按下
);
// 无边框
button->setBorder(Colors::Transparent, 0.0f);
// 无内边距
button->setPadding(Vec2(0.0f, 0.0f));
menuBtn->setBorder(Colors::Transparent, 0.0f);
menuBtn->setPadding(Vec2(0.0f, 0.0f));
menuBtn->setCustomSize(200.0f, 40.0f);
menuBtn->setAnchor(0.5f, 0.5f); // 中心锚点
menuBtn->setPosition(centerX, centerY);
addChild(menuBtn);
```
## 菜单系统
## 文本Text
### 创建菜单
### 创建文本
```cpp
class MenuScene : public Scene {
// 方式1简单创建
auto text = Text::create("Hello World", font);
text->setPosition(Vec2(100, 50));
// 方式2链式调用
auto text = Text::create("Hello World")
->withFont(font)
->withPosition(100, 50)
->withTextColor(Colors::White)
->withFontSize(24)
->withAlignment(Alignment::Center);
addChild(text);
```
### 文本属性设置
```cpp
// 设置文本
text->setText("新文本");
text->setFormat("Score: %d", score); // 格式化文本
// 字体和颜色
text->setFont(font);
text->setTextColor(Colors::White);
text->setFontSize(24);
// 对齐方式
text->setAlignment(Alignment::Left); // 水平:左/中/右
text->setVerticalAlignment(VerticalAlignment::Middle); // 垂直:上/中/下
// 获取文本尺寸
Size size = text->getTextSize();
float lineHeight = text->getLineHeight();
```
### 对齐方式枚举
```cpp
enum class Alignment { Left, Center, Right };
enum class VerticalAlignment { Top, Middle, Bottom };
```
## 标签Label
Label 是功能更丰富的文本组件,支持阴影、描边、多行文本。
### 创建标签
```cpp
// 创建标签
auto label = Label::create("玩家名称", font);
label->setPosition(Vec2(100, 50));
label->setTextColor(Colors::White);
// 链式调用
auto label = Label::create("玩家名称")
->withFont(font)
->withPosition(100, 50)
->withTextColor(Colors::White)
->withFontSize(24);
addChild(label);
```
### 标签特效
```cpp
// 阴影
label->setShadowEnabled(true);
label->setShadowColor(Colors::Black);
label->setShadowOffset(Vec2(2.0f, 2.0f));
// 描边
label->setOutlineEnabled(true);
label->setOutlineColor(Colors::Black);
label->setOutlineWidth(1.0f);
// 多行文本
label->setMultiLine(true);
label->setLineSpacing(1.2f);
label->setMaxWidth(300.0f); // 自动换行宽度
// 对齐方式
label->setHorizontalAlign(HorizontalAlign::Center);
label->setVerticalAlign(VerticalAlign::Middle);
```
## 复选框CheckBox
### 创建复选框
```cpp
// 方式1简单创建
auto checkBox = CheckBox::create();
checkBox->setPosition(Vec2(100, 200));
checkBox->setChecked(true);
// 方式2带标签
auto checkBox = CheckBox::create("启用音效");
checkBox->setPosition(Vec2(100, 200));
// 方式3链式调用
auto checkBox = CheckBox::create("启用音效")
->withPosition(100, 200)
->withFont(font)
->withTextColor(Colors::White);
// 状态改变回调
checkBox->setOnStateChange([](bool checked) {
E2D_LOG_INFO("复选框状态: {}", checked);
});
addChild(checkBox);
```
### 复选框属性
```cpp
// 状态
checkBox->setChecked(true);
checkBox->toggle();
bool isChecked = checkBox->isChecked();
// 标签
checkBox->setLabel("新标签");
checkBox->setFont(font);
checkBox->setTextColor(Colors::White);
// 外观
checkBox->setBoxSize(20.0f); // 复选框大小
checkBox->setSpacing(10.0f); // 复选框与标签间距
checkBox->setCheckedColor(Colors::Green);
checkBox->setUncheckedColor(Colors::Gray);
checkBox->setCheckMarkColor(Colors::White);
```
## 单选按钮RadioButton
### 创建单选按钮
```cpp
// 创建单选按钮
auto radio1 = RadioButton::create("选项 A");
radio1->setPosition(Vec2(100, 300));
radio1->setSelected(true);
auto radio2 = RadioButton::create("选项 B");
radio2->setPosition(Vec2(100, 340));
auto radio3 = RadioButton::create("选项 C");
radio3->setPosition(Vec2(100, 380));
// 添加到组(互斥选择)
radio1->setGroupId(1);
radio2->setGroupId(1);
radio3->setGroupId(1);
// 或使用 RadioButtonGroup
auto group = std::make_shared<RadioButtonGroup>();
group->addButton(radio1.get());
group->addButton(radio2.get());
group->addButton(radio3.get());
// 选择改变回调
group->setOnSelectionChange([](RadioButton* selected) {
if (selected) {
E2D_LOG_INFO("选中: {}", selected->getLabel());
}
});
addChild(radio1);
addChild(radio2);
addChild(radio3);
```
### 单选按钮属性
```cpp
// 状态
radio->setSelected(true);
bool isSelected = radio->isSelected();
// 标签
radio->setLabel("新标签");
radio->setFont(font);
radio->setTextColor(Colors::White);
// 外观
radio->setCircleSize(16.0f); // 圆形大小
radio->setSpacing(10.0f); // 圆形与标签间距
radio->setSelectedColor(Colors::Green);
radio->setUnselectedColor(Colors::Gray);
radio->setDotColor(Colors::White);
// 分组
radio->setGroupId(1); // 相同 groupId 的按钮互斥
```
## 滑块Slider
### 创建滑块
```cpp
// 方式1简单创建
auto slider = Slider::create();
slider->setPosition(Vec2(200, 400));
slider->setRange(0.0f, 100.0f);
slider->setValue(50.0f);
// 方式2带初始值创建
auto slider = Slider::create(0.0f, 100.0f, 50.0f);
// 方式3链式调用
auto slider = Slider::create()
->withPosition(200, 400)
->withSize(200, 20)
->withMinValue(0.0f)
->withMaxValue(100.0f)
->withValue(50.0f);
// 值改变回调
slider->setOnValueChange([](float value) {
E2D_LOG_INFO("滑块值: {}", value);
});
// 拖动开始/结束回调
slider->setOnDragStart([]() {
E2D_LOG_INFO("开始拖动");
});
slider->setOnDragEnd([]() {
E2D_LOG_INFO("结束拖动");
});
addChild(slider);
```
### 滑块属性
```cpp
// 值和范围
slider->setRange(0.0f, 100.0f);
slider->setValue(50.0f);
slider->setStep(5.0f); // 步进值0表示无步进
float value = slider->getValue();
float min = slider->getMin();
float max = slider->getMax();
// 方向
slider->setVertical(false); // false=水平, true=垂直
// 外观
slider->setTrackSize(4.0f); // 轨道粗细
slider->setThumbSize(16.0f); // 滑块大小
// 颜色
slider->setTrackColor(Colors::Gray);
slider->setFillColor(Colors::Green);
slider->setThumbColor(Colors::White);
slider->setThumbHoverColor(Colors::Yellow);
slider->setThumbPressedColor(Colors::Orange);
// 显示选项
slider->setShowThumb(true); // 显示滑块
slider->setShowFill(true); // 显示填充
slider->setTextEnabled(true); // 显示数值文本
slider->setTextFormat("{:.0f}%"); // 数值格式
slider->setFont(font);
slider->setTextColor(Colors::White);
```
## 进度条ProgressBar
### 创建进度条
```cpp
// 方式1简单创建
auto progressBar = ProgressBar::create();
progressBar->setPosition(Vec2(200, 500));
progressBar->setSize(300.0f, 30.0f);
progressBar->setValue(75.0f); // 75%
// 方式2带范围创建
auto progressBar = ProgressBar::create(0.0f, 100.0f, 75.0f);
// 方式3链式调用
auto progressBar = ProgressBar::create()
->withPosition(200, 500)
->withSize(300, 30)
->withProgress(0.75f); // 0-1 的进度值
addChild(progressBar);
```
### 进度条属性
```cpp
// 值和范围
progressBar->setRange(0.0f, 100.0f);
progressBar->setValue(75.0f);
float value = progressBar->getValue();
float percent = progressBar->getPercent(); // 0.0-1.0
// 方向
progressBar->setDirection(Direction::LeftToRight);
// Direction::LeftToRight, RightToLeft, BottomToTop, TopToBottom
// 颜色
progressBar->setBackgroundColor(Colors::DarkGray);
progressBar->setFillColor(Colors::Green);
// 渐变填充
progressBar->setGradientFillEnabled(true);
progressBar->setFillColorEnd(Colors::LightGreen);
// 分段颜色(根据进度显示不同颜色)
progressBar->setSegmentedColorsEnabled(true);
progressBar->addColorSegment(0.3f, Colors::Red); // <30% 红色
progressBar->addColorSegment(0.7f, Colors::Yellow); // 30-70% 黄色
// >70% 使用默认填充色(绿色)
// 圆角
progressBar->setRoundedCornersEnabled(true);
progressBar->setCornerRadius(8.0f);
// 边框
progressBar->setBorderEnabled(true);
progressBar->setBorderColor(Colors::White);
progressBar->setBorderWidth(2.0f);
progressBar->setPadding(2.0f);
// 文本
progressBar->setTextEnabled(true);
progressBar->setTextFormat("{:.0f}%");
progressBar->setFont(font);
progressBar->setTextColor(Colors::White);
// 动画效果
progressBar->setAnimatedChangeEnabled(true);
progressBar->setAnimationSpeed(5.0f);
// 延迟显示效果
progressBar->setDelayedDisplayEnabled(true);
progressBar->setDelayTime(0.5f);
progressBar->setDelayedFillColor(Colors::Yellow);
// 条纹效果
progressBar->setStripedEnabled(true);
progressBar->setStripeColor(Colors::White);
progressBar->setStripeSpeed(1.0f);
```
## 完整示例:设置场景
```cpp
class SettingsScene : public Scene {
public:
void onEnter() override {
Scene::onEnter();
auto &resources = Application::instance().resources();
font_ = resources.loadFont("assets/font.ttf", 28, true);
auto& resources = Application::instance().resources();
font_ = resources.loadFont("assets/font.ttf", 24);
float centerX = 640.0f;
float startY = 300.0f;
// 标题
auto title = Text::create("设置", font_);
title->setPosition(Vec2(400, 100));
title->setAlignment(Alignment::Center);
addChild(title);
// 创建菜单按钮
createMenuButton("开始游戏", centerX, startY, 0);
createMenuButton("继续游戏", centerX, startY + 50.0f, 1);
createMenuButton("退出", centerX, startY + 100.0f, 2);
// 音效开关
auto soundLabel = Label::create("音效", font_);
soundLabel->setPosition(Vec2(200, 200));
addChild(soundLabel);
menuCount_ = 3;
selectedIndex_ = 0;
updateMenuColors();
}
void onUpdate(float dt) override {
Scene::onUpdate(dt);
soundCheck_ = CheckBox::create();
soundCheck_->setPosition(Vec2(350, 200));
soundCheck_->setChecked(true);
soundCheck_->setOnStateChange([this](bool checked) {
E2D_LOG_INFO("音效: {}", checked ? "开启" : "关闭");
});
addChild(soundCheck_);
auto &input = Application::instance().input();
// 音量滑块
auto volumeLabel = Label::create("音量", font_);
volumeLabel->setPosition(Vec2(200, 280));
addChild(volumeLabel);
// 方向键切换选择
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();
}
volumeSlider_ = Slider::create(0.0f, 1.0f, 0.8f);
volumeSlider_->setPosition(Vec2(350, 280));
volumeSlider_->setSize(200, 20);
volumeSlider_->setOnValueChange([](float value) {
E2D_LOG_INFO("音量: {:.0f}%", value * 100);
});
addChild(volumeSlider_);
// A键确认
if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_A)) {
executeMenuItem();
}
// 难度选择
auto difficultyLabel = Label::create("难度", font_);
difficultyLabel->setPosition(Vec2(200, 360));
addChild(difficultyLabel);
auto easyRadio = RadioButton::create("简单");
easyRadio->setPosition(Vec2(350, 360));
easyRadio->setSelected(true);
easyRadio->setGroupId(1);
auto normalRadio = RadioButton::create("普通");
normalRadio->setPosition(Vec2(450, 360));
normalRadio->setGroupId(1);
auto hardRadio = RadioButton::create("困难");
hardRadio->setPosition(Vec2(550, 360));
hardRadio->setGroupId(1);
addChild(easyRadio);
addChild(normalRadio);
addChild(hardRadio);
// 返回按钮
auto backBtn = Button::create("返回", font_);
backBtn->setPosition(Vec2(400, 500));
backBtn->setCustomSize(150.0f, 50.0f);
backBtn->setBackgroundColor(
Colors::Blue,
Colors::LightBlue,
Colors::DarkBlue
);
backBtn->setOnClick([]() {
Application::instance().scenes().popScene();
});
addChild(backBtn);
}
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();
}
Ptr<CheckBox> soundCheck_;
Ptr<Slider> volumeSlider_;
};
```
## 绘制文字
## 最佳实践
### 基本文字绘制
```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;
}
}
};
```
1. **使用屏幕空间** - UI 控件通常使用 `CoordinateSpace::Screen` 固定在屏幕上
2. **使用链式调用** - 创建控件时优先使用链式调用,代码更简洁
3. **设置合适的锚点** - 使用锚点0.5, 0.5)让控件中心对齐,方便布局
4. **复用字体资源** - 避免重复加载相同字体
5. **使用回调函数** - 使用 `setOnClick`、`setOnValueChange` 等回调响应用户操作
## 下一步
- [08. 音频系统](08_Audio_System.md)
- [08. 音频系统](./08_Audio_System.md) - 学习音频播放

View File

@ -1,323 +1,279 @@
# Extra2D API 教程 - 08. 音频系统
# 08. 音频系统
## 音频系统概述
Extra2D 提供了基于 SDL2_mixer 的音频播放系统,支持音效播放。
Extra2D 使用 SDL2_mixer 作为音频后端,支持 WAV、MP3、OGG 等格式。
## 音频引擎
## 音效播放
### 加载和播放音效
通过 `Application::instance().audio()` 访问音频引擎:
```cpp
// 获取资源管理器
auto &resources = Application::instance().resources();
auto& audio = Application::instance().audio();
```
## 播放音效
### 基本用法
```cpp
// 加载音效
auto jumpSound = resources.loadSound("assets/jump.wav");
auto attackSound = resources::loadSound("assets/attack.ogg");
auto sound = audio.loadSound("assets/audio/jump.wav");
// 播放音效(一次)
jumpSound->play();
// 播放音效
sound->play();
// 设置音量 (0.0 - 1.0)
sound->setVolume(0.8f);
```
### 音效控制
```cpp
// 停止播放
sound->stop();
// 暂停
sound->pause();
// 恢复
sound->resume();
// 循环播放
backgroundMusic->play(true);
sound->setLooping(true);
// 停止播放
jumpSound->stop();
// 检查播放状态
bool playing = sound->isPlaying();
bool paused = sound->isPaused();
```
## 音频控制器
### 创建音频控制器节点
### 音调和播放位置
```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;
};
// 设置音调(当前实现不支持)
sound->setPitch(1.0f);
// 获取/设置播放位置(当前实现不支持)
float cursor = sound->getCursor();
sound->setCursor(0.0f);
// 获取音频时长(当前实现不支持)
float duration = sound->getDuration();
```
### 在场景中使用
## 全局音量控制
```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();
}
};
// 设置主音量
audio.setMasterVolume(0.8f);
// 获取主音量
float volume = audio.getMasterVolume();
```
## 全局播放控制
```cpp
// 暂停所有音效
audio.pauseAll();
// 恢复所有音效
audio.resumeAll();
// 停止所有音效
audio.stopAll();
// 卸载指定音效
audio.unloadSound("jump");
// 卸载所有音效
audio.unloadAllSounds();
```
## 完整示例
```cpp
// AudioController.h
// audio_manager.h
#pragma once
#include <extra2d/extra2d.h>
#include <unordered_map>
namespace game {
namespace pushbox {
class AudioController : public extra2d::Node {
class AudioManager {
public:
static extra2d::Ptr<AudioController> create();
static AudioManager& instance() {
static AudioManager instance;
return instance;
}
void onEnter() override;
void playSound(const std::string &name);
void init();
void setEnabled(bool enabled);
bool isEnabled() const { return enabled_; }
void playMoveSound();
void playBoxMoveSound();
void playWinSound();
private:
std::unordered_map<std::string, extra2d::Ptr<extra2d::Sound>> sounds_;
AudioManager() = default;
bool enabled_ = true;
extra2d::Ptr<extra2d::Sound> moveSound_;
extra2d::Ptr<extra2d::Sound> boxMoveSound_;
extra2d::Ptr<extra2d::Sound> winSound_;
};
} // 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());
}
}
};
} // namespace pushbox
```
## 音频格式支持
| 格式 | 支持 |
|------|------|
| WAV | ✓ |
| MP3 | ✓ |
| OGG | ✓ |
| FLAC | ✓ |
| MOD | ✓ |
## 音量控制
```cpp
// 设置音效音量 (0-128)
Mix_Volume(-1, MIX_MAX_VOLUME / 2); // 所有音效
Mix_Volume(channel, volume); // 指定通道
// audio_manager.cpp
#include "audio_manager.h"
// 设置音乐音量 (0-128)
Mix_VolumeMusic(volume);
namespace pushbox {
void AudioManager::init() {
auto& audio = extra2d::Application::instance().audio();
// 加载音效
moveSound_ = audio.loadSound("move", "assets/audio/manmove.wav");
boxMoveSound_ = audio.loadSound("boxmove", "assets/audio/boxmove.wav");
winSound_ = audio.loadSound("win", "assets/audio/win.wav");
}
void AudioManager::setEnabled(bool enabled) {
enabled_ = enabled;
if (!enabled) {
extra2d::Application::instance().audio().stopAll();
}
}
void AudioManager::playMoveSound() {
if (enabled_ && moveSound_) {
moveSound_->play();
}
}
void AudioManager::playBoxMoveSound() {
if (enabled_ && boxMoveSound_) {
boxMoveSound_->play();
}
}
void AudioManager::playWinSound() {
if (enabled_ && winSound_) {
winSound_->play();
}
}
} // namespace pushbox
```
## 使用音频管理器
```cpp
// main.cpp
int main(int argc, char** argv) {
// ... 初始化应用 ...
// 初始化音频管理器
pushbox::AudioManager::instance().init();
// ... 运行应用 ...
}
// PlayScene.cpp
void PlayScene::move(int dx, int dy, int direct) {
// ... 移动逻辑 ...
if (isBoxMoved) {
// 播放推箱子音效
pushbox::AudioManager::instance().playBoxMoveSound();
} else {
// 播放移动音效
pushbox::AudioManager::instance().playMoveSound();
}
}
```
## 音频开关控制
```cpp
// 在菜单中切换音效
void StartScene::onUpdate(float dt) {
auto& input = Application::instance().input();
// X键切换音效
if (input.isButtonPressed(GamepadButton::X)) {
g_SoundOpen = !g_SoundOpen;
AudioManager::instance().setEnabled(g_SoundOpen);
updateSoundIcon();
}
}
```
## 支持的音频格式
- WAV
- OGG
- MP3需要 SDL2_mixer 支持)
## 最佳实践
1. **预加载音效**: 在 `onEnter()` 中加载所有需要的音效
2. **使用音频控制器**: 统一管理音效,方便控制开关
3. **音效开关**: 提供用户选项控制音效开关
4. **资源释放**: 音效资源会自动管理,无需手动释放
1. **使用单例管理器** - 集中管理音频资源
2. **预加载常用音效** - 在初始化时加载
3. **提供开关选项** - 让用户控制音效
4. **合理设置音量** - 避免音量过大
5. **及时卸载不用的音效** - 释放内存资源
## API 参考
### Sound 类
| 方法 | 说明 |
|------|------|
| `play()` | 播放音效 |
| `pause()` | 暂停播放 |
| `resume()` | 恢复播放 |
| `stop()` | 停止播放 |
| `isPlaying()` | 是否正在播放 |
| `isPaused()` | 是否已暂停 |
| `setVolume(float)` | 设置音量 (0.0-1.0) |
| `getVolume()` | 获取音量 |
| `setLooping(bool)` | 设置循环播放 |
| `isLooping()` | 是否循环播放 |
| `setPitch(float)` | 设置音调(当前不支持) |
| `getPitch()` | 获取音调 |
| `getDuration()` | 获取时长(当前不支持) |
| `getCursor()` | 获取播放位置(当前不支持) |
| `setCursor(float)` | 设置播放位置(当前不支持) |
### AudioEngine 类
| 方法 | 说明 |
|------|------|
| `getInstance()` | 获取单例实例 |
| `initialize()` | 初始化音频引擎 |
| `shutdown()` | 关闭音频引擎 |
| `loadSound(path)` | 加载音效(以路径为名称) |
| `loadSound(name, path)` | 加载音效(指定名称) |
| `getSound(name)` | 获取已加载的音效 |
| `unloadSound(name)` | 卸载指定音效 |
| `unloadAllSounds()` | 卸载所有音效 |
| `setMasterVolume(float)` | 设置主音量 |
| `getMasterVolume()` | 获取主音量 |
| `pauseAll()` | 暂停所有音效 |
| `resumeAll()` | 恢复所有音效 |
| `stopAll()` | 停止所有音效 |
## 总结
Extra2D 的音频系统简单易用:
至此,你已经学习了 Extra2D 引擎的核心功能
```cpp
// 加载
auto sound = resources.loadSound("assets/sound.wav");
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) - 音频播放
// 播放
sound->play(); // 一次
sound->play(true); // 循环
// 停止
sound->stop();
```
---
**教程完成!** 您已经学习了 Extra2D 的所有核心功能:
1. [快速开始](01_Quick_Start.md)
2. [场景系统](02_Scene_System.md)
3. [节点系统](03_Node_System.md)
4. [资源管理](04_Resource_Management.md)
5. [输入处理](05_Input_Handling.md)
6. [碰撞检测](06_Collision_Detection.md)
7. [UI 系统](07_UI_System.md)
8. [音频系统](08_Audio_System.md)
开始你的游戏开发之旅吧!

View File

@ -1,327 +1,218 @@
# Extra2D 构建系统文档
## 概述
本文档详细介绍 Extra2D 引擎的构建系统配置和使用方法。
Extra2D 使用 **Xmake** 作为构建系统,支持 **MinGW (Windows)****Nintendo Switch** 两个平台。
## 构建工具
## 项目结构
Extra2D 使用 [xmake](https://xmake.io/) 作为构建工具,支持多平台构建。
```
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`
- 构建后复制资源文件
## 构建命令
### 配置项目
### 安装 xmake
```bash
# 默认配置 (MinGW)
xmake f -c
# Windows (使用 PowerShell)
Invoke-Expression (Invoke-WebRequest 'https://xmake.io/psget.text' -UseBasicParsing).Content
# 指定平台 (使用 -p 参数)
xmake f -c -p mingw
xmake f -c -p switch
# macOS
brew install xmake
# 指定 MinGW 路径(如果不在默认位置)
xmake f -c -p mingw --mingw=C:\mingw
# 禁用示例
xmake f --examples=n
# 启用调试日志
xmake f --debug_logs=y
# Linux
sudo add-apt-repository ppa:xmake-io/xmake
sudo apt update
sudo apt install xmake
```
### 安装依赖 (MinGW)
## 平台支持
| 平台 | 目标 | 说明 |
|------|------|------|
| Windows | `mingw` | MinGW-w64 工具链 |
| Nintendo Switch | `switch` | devkitPro 工具链 |
## 构建配置
### Windows (MinGW)
```bash
# 配置构建
xmake f -p mingw --mode=release
# 安装依赖
xmake require -y
# 构建引擎
xmake
# 构建示例
xmake -g examples
# 运行示例
xmake run hello_world
```
### 构建项目
### Nintendo Switch
```bash
# 配置构建
xmake f -p switch --mode=release
# 构建引擎
xmake
# 构建示例
xmake -g examples
# 打包 NSP
xmake package push_box
```
## 构建选项
### 配置参数
```bash
# 设置构建模式
xmake f --mode=debug # 调试模式
xmake f --mode=release # 发布模式
# 设置目标平台
xmake f -p mingw # Windows
xmake f -p switch # Nintendo Switch
```
### 构建目标
```bash
# 构建所有目标
xmake
# 构建特定目标
xmake -r extra2d
xmake -r push_box
xmake -t extra2d # 引擎库
xmake -t hello_world # Hello World 示例
xmake -t push_box # 推箱子游戏
xmake -t collision_demo # 碰撞检测演示
xmake -t spatial_index_demo # 空间索引演示
# 并行构建
xmake -j4
# 构建示例组
xmake -g examples
```
### 运行程序
```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/
└── ...
Extra2D/
├── xmake.lua # 主构建配置
├── xmake/
│ ├── engine.lua # 引擎构建规则
│ └── toolchains/ # 工具链定义
├── Extra2D/ # 引擎源码
│ ├── include/ # 头文件
│ └── src/ # 源文件
└── examples/ # 示例程序
├── hello_world/
├── push_box/
├── collision_demo/
└── spatial_index_demo/
```
## 关键设计决策
## 添加新示例
### 1. 平台检测
- 使用 `is_plat()` 而不是手动检测,确保与主项目一致
- 示例脚本继承主项目的平台配置
创建新的示例程序:
### 2. 资源处理
- **Switch**: 使用 romfs 嵌入 NRO 文件
- **MinGW**: 构建后复制到输出目录
1. 在 `examples/` 下创建目录
2. 添加 `main.cpp``xmake.lua`
### 3. 依赖管理
- **MinGW**: 使用 Xmake 包管理器 (`add_requires`)
- **Switch**: 使用 devkitPro 提供的库
### 示例 xmake.lua
### 4. 工具链隔离
- Switch 工具链定义在单独文件中
- 通过 `set_toolchains("switch")` 切换
```lua
-- examples/my_demo/xmake.lua
target("my_demo")
set_kind("binary")
add_deps("extra2d")
add_files("*.cpp")
add_packages("spdlog", "glm")
-- 资源文件
add_files("romfs/**", {install = true})
-- Switch 特定配置
if is_plat("switch") then
add_rules("switch.nro")
add_files("icon.jpg")
end
```
## 常见问题
### 1. 依赖包找不到
### 依赖安装失败
```bash
xmake repo -u
xmake require -y
# 强制重新安装依赖
xmake require -f -y
# 清理构建缓存
xmake clean
xmake f -c
```
### 2. Switch 工具链找不到
- 确保 DEVKITPRO 环境变量设置正确
- 默认路径: `C:/devkitPro`
### Switch 构建失败
### 3. 平台配置不匹配
- 使用 `xmake show` 查看当前配置
- 使用 `xmake f -c` 重新配置
确保已安装 devkitPro
### 4. MinGW 路径问题
如果 MinGW 安装在非默认位置,使用 `--mingw` 参数指定:
```bash
xmake f -c -p mingw --mingw=D:\Tools\mingw64
# 安装 Switch 开发工具链
pacman -S switch-dev switch-portlibs
# 设置环境变量
$env:DEVKITPRO = "C:\devkitPro"
$env:DEVKITA64 = "C:\devkitPro\devkitA64"
```
### 5. Switch 库找不到
确保在 MSYS2 中安装了必要的库:
```bash
pacman -S switch-sdl2 switch-sdl2_mixer switch-glm
### 运行时找不到资源
确保资源文件已正确配置:
```lua
-- 在 xmake.lua 中添加资源
add_files("romfs/**", {install = true})
```
## 扩展指南
## 高级配置
### 添加新示例
1. 在 `examples/` 下创建新目录
2. 创建 `xmake.lua` 构建脚本
3. 在 `xmake.lua` 中添加 `includes("examples/new_example")`
### 自定义编译选项
### 添加新平台
1. 在 `xmake/toolchains/` 下创建工具链定义
2. 在 `xmake.lua` 中添加平台检测逻辑
3. 在 `xmake/engine.lua` 中添加平台配置
```lua
-- 添加编译选项
add_cxxflags("-O3", "-ffast-math")
-- 添加宏定义
add_defines("E2D_ENABLE_PROFILING")
-- 添加包含路径
add_includedirs("third_party/include")
-- 添加链接库
add_links("pthread")
```
### 条件编译
```lua
if is_plat("windows") then
add_defines("E2D_PLATFORM_WINDOWS")
elseif is_plat("switch") then
add_defines("E2D_PLATFORM_SWITCH")
end
if is_mode("debug") then
add_defines("E2D_DEBUG")
add_cxxflags("-g", "-O0")
else
add_defines("E2D_RELEASE")
add_cxxflags("-O3")
end
```
## 参考链接
- [xmake 官方文档](https://xmake.io/#/zh-cn/)
- [devkitPro 官网](https://devkitpro.org/)

View File

@ -21,7 +21,7 @@ add_rules("mode.debug", "mode.release")
-- ==============================================
option("examples")
set_default(true)
set_default(false)
set_showmenu(true)
set_description("Build example programs")
option_end()
@ -84,7 +84,7 @@ includes("xmake/engine.lua")
define_extra2d_engine()
-- 示例程序目标(作为子项目)
if has_config("examples") then
if is_config("examples","true") then
includes("examples/hello_world", {rootdir = "examples/hello_world"})
includes("examples/spatial_index_demo", {rootdir = "examples/spatial_index_demo"})
includes("examples/collision_demo", {rootdir = "examples/collision_demo"})