docs: 更新API教程文档和构建系统文档
refactor: 重构xmake.lua中的示例构建逻辑 style: 统一文档格式和代码风格
This commit is contained in:
parent
6975f69d64
commit
39a0ab7124
404
README.md
404
README.md
|
|
@ -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+ |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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) - 学习节点和精灵的使用
|
||||
|
|
|
|||
|
|
@ -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) - 深入了解资源加载
|
||||
|
|
|
|||
|
|
@ -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) - 学习碰撞检测系统
|
||||
|
|
|
|||
|
|
@ -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) - 学习碰撞检测系统
|
||||
|
|
|
|||
|
|
@ -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 控件使用
|
||||
|
|
|
|||
|
|
@ -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) - 学习音频播放
|
||||
|
|
|
|||
|
|
@ -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) - 学习音频播放
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
开始你的游戏开发之旅吧!
|
||||
|
|
|
|||
|
|
@ -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/)
|
||||
|
|
|
|||
|
|
@ -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"})
|
||||
|
|
|
|||
Loading…
Reference in New Issue