docs: 删除Extra2D构建系统文档和API教程文档

删除以下文档文件:
- docs/Extra2D构建系统文档.md
- docs/API_Tutorial/01_Quick_Start.md
- docs/API_Tutorial/02_Scene_System.md
- docs/API_Tutorial/03_Node_System.md
- docs/API_Tutorial/05_Input_Handling.md
- docs/API_Tutorial/06_Collision_Detection.md
- docs/API_Tutorial/08_Audio_System.md
- docs/API_Tutorial/09_Action_System.md
This commit is contained in:
ChestnutYueyue 2026-02-14 17:43:50 +08:00
parent 55c66e5038
commit b949d1a3da
11 changed files with 0 additions and 5263 deletions

View File

@ -1,215 +0,0 @@
# 01. 快速开始
本教程将带你快速上手 Extra2D 引擎,通过一个简单的 Hello World 示例了解引擎的基本使用方法。
## 示例代码
完整示例位于 `examples/hello_world/main.cpp`
```cpp
#include <extra2d/extra2d.h>
using namespace extra2d;
// ============================================================================
// Hello World 场景
// ============================================================================
/**
* @brief Hello World 场景类
* 显示简单的 "Hello World" 文字
*/
class HelloWorldScene : public Scene {
public:
/**
* @brief 场景进入时调用
*/
void onEnter() override {
E2D_LOG_INFO("HelloWorldScene::onEnter - 进入场景");
// 设置背景颜色为深蓝色
setBackgroundColor(Color(0.1f, 0.1f, 0.3f, 1.0f));
// 加载字体(支持多种字体后备)
auto &resources = Application::instance().resources();
font_ = resources.loadFont("assets/font.ttf", 48, true);
if (!font_) {
E2D_LOG_ERROR("字体加载失败,文字渲染将不可用!");
return;
}
// 创建 "你好世界" 文本组件 - 使用屏幕空间(固定位置,不随相机移动)
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);
// 创建提示文本组件 - 使用屏幕空间,固定在屏幕底部
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);
}
/**
* @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 应用遵循以下生命周期:
```
初始化 (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);
```
### 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) - 学习节点和精灵的使用

View File

@ -1,486 +0,0 @@
# 02. 场景系统
Extra2D 的场景系统提供了游戏内容的分层管理和切换功能。本教程将详细介绍场景的生命周期、切换和过渡效果,以及视口适配功能。
## 完整示例
参考 `examples/flappy_bird/``examples/push_box/` 中的实现:
- `BaseScene.h/cpp` - 基础场景类(视口适配)
- `StartScene.h/cpp` - 开始菜单场景
- `GameScene.h/cpp` - 游戏主场景
- `GameOverLayer.h/cpp` - 游戏结束层
## 视口适配(重要)
### 问题背景
在游戏开发中,不同设备有不同的屏幕分辨率。如果直接使用窗口坐标,游戏内容可能会变形或显示不完整。
Extra2D 提供了**居中视口适配**方案,让游戏在任意分辨率下都能正确显示:
- 游戏使用固定的逻辑分辨率(如 Flappy Bird 的 288×512
- 根据窗口大小自动缩放,保持宽高比
- 游戏内容居中显示,四周显示黑边
### 实现方案
参考 `examples/flappy_bird/BaseScene.h/cpp`
```cpp
// BaseScene.h
#pragma once
#include <extra2d/extra2d.h>
namespace flappybird {
// 游戏逻辑分辨率(原始 Flappy Bird 尺寸)
static constexpr float GAME_WIDTH = 288.0f;
static constexpr float GAME_HEIGHT = 512.0f;
/**
* @brief Flappy Bird 基础场景类
* 所有游戏场景都应继承此类,以获得统一的居中视口适配功能
*/
class BaseScene : public extra2d::Scene {
public:
BaseScene();
void onEnter() override;
void onRender(extra2d::RenderBackend &renderer) override;
protected:
void updateViewport();
float scaledGameWidth_ = 0.0f;
float scaledGameHeight_ = 0.0f;
float viewportOffsetX_ = 0.0f;
float viewportOffsetY_ = 0.0f;
};
} // namespace flappybird
```
```cpp
// BaseScene.cpp
#include "BaseScene.h"
namespace flappybird {
BaseScene::BaseScene() {
setBackgroundColor(extra2d::Color(0.0f, 0.0f, 0.0f, 1.0f));
}
void BaseScene::onEnter() {
extra2d::Scene::onEnter();
updateViewport();
}
void BaseScene::updateViewport() {
auto &app = extra2d::Application::instance();
float windowWidth = static_cast<float>(app.window().getWidth());
float windowHeight = static_cast<float>(app.window().getHeight());
// 保持游戏原始宽高比,进行"黑边"适配
float scaleX = windowWidth / GAME_WIDTH;
float scaleY = windowHeight / GAME_HEIGHT;
float scale = std::min(scaleX, scaleY);
scaledGameWidth_ = GAME_WIDTH * scale;
scaledGameHeight_ = GAME_HEIGHT * scale;
viewportOffsetX_ = (windowWidth - scaledGameWidth_) * 0.5f;
viewportOffsetY_ = (windowHeight - scaledGameHeight_) * 0.5f;
// 设置视口大小为游戏逻辑分辨率
setViewportSize(GAME_WIDTH, GAME_HEIGHT);
// 创建并设置相机
auto camera = extra2d::makePtr<extra2d::Camera>();
camera->setViewport(0.0f, GAME_WIDTH, GAME_HEIGHT, 0.0f);
setCamera(camera);
}
void BaseScene::onRender(extra2d::RenderBackend &renderer) {
// 检查窗口大小是否改变
auto &app = extra2d::Application::instance();
float currentWindowWidth = static_cast<float>(app.window().getWidth());
float currentWindowHeight = static_cast<float>(app.window().getHeight());
float expectedWidth = scaledGameWidth_ + viewportOffsetX_ * 2.0f;
float expectedHeight = scaledGameHeight_ + viewportOffsetY_ * 2.0f;
if (std::abs(currentWindowWidth - expectedWidth) > 1.0f ||
std::abs(currentWindowHeight - expectedHeight) > 1.0f) {
updateViewport();
}
// 设置视口为居中区域
renderer.setViewport(
static_cast<int>(viewportOffsetX_), static_cast<int>(viewportOffsetY_),
static_cast<int>(scaledGameWidth_), static_cast<int>(scaledGameHeight_));
extra2d::Scene::onRender(renderer);
}
} // namespace flappybird
```
### 使用基础场景类
所有游戏场景继承 `BaseScene`,自动获得视口适配功能:
```cpp
// GameScene.h
class GameScene : public BaseScene {
public:
void onEnter() override;
void onUpdate(float dt) override;
};
// GameScene.cpp
void GameScene::onEnter() {
BaseScene::onEnter(); // 必须调用父类方法
// 使用游戏逻辑分辨率进行布局
float screenWidth = GAME_WIDTH; // 288.0f
float screenHeight = GAME_HEIGHT; // 512.0f
// 所有坐标都基于逻辑分辨率
auto bird = extra2d::makePtr<Bird>();
bird->setPosition(extra2d::Vec2(screenWidth / 2.0f - 50.0f, screenHeight / 2.0f));
addChild(bird);
}
void GameScene::onUpdate(float dt) {
// 游戏逻辑更新...
// 必须调用父类方法,确保子节点动画正常播放
BaseScene::onUpdate(dt);
}
```
### 视口适配原理图
```
┌─────────────────────────────────────────────┐
│ 窗口 (1280 x 720) │
│ │
│ ┌─────────────────────────────────┐ │
│ │ 黑边区域 │ │
│ │ │ │
│ │ ┌───────────────────────────┐ │ │
│ │ │ 游戏视口 (288 x 512) │ │ │
│ │ │ 自动缩放并居中显示 │ │ │
│ │ │ │ │ │
│ │ │ ┌─────────────────┐ │ │ │
│ │ │ │ 游戏内容 │ │ │ │
│ │ │ │ (逻辑分辨率) │ │ │ │
│ │ │ └─────────────────┘ │ │ │
│ │ │ │ │ │
│ │ └───────────────────────────┘ │ │
│ │ │ │
│ │ 黑边区域 │ │
│ └─────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────┘
```
### 关键要点
1. **定义游戏逻辑分辨率** - 如 `GAME_WIDTH = 288.0f, GAME_HEIGHT = 512.0f`
2. **在 `onEnter()` 中调用 `updateViewport()`** - 初始化视口
3. **在 `onRender()` 中设置渲染器视口** - 确保正确渲染区域
4. **所有坐标使用逻辑分辨率** - 不依赖窗口实际大小
5. **处理窗口大小变化** - 在 `onRender()` 中检测并更新
### 注意事项
1. **相机设置必须正确** - 使用 `camera->setViewport(0.0f, GAME_WIDTH, GAME_HEIGHT, 0.0f)` 确保相机覆盖整个游戏区域
2. **渲染器视口与相机视口不同** - 渲染器视口控制实际渲染到屏幕的区域,相机视口控制世界坐标到屏幕坐标的映射
3. **窗口大小变化检测** - 在 `onRender()` 中检测窗口大小变化并重新计算视口,确保窗口调整时正确适配
4. **子类必须调用父类方法** - `onEnter()``onUpdate()` 中必须调用 `BaseScene::onEnter()``BaseScene::onUpdate()`
5. **UI元素坐标空间** - UI控件通常使用 `CoordinateSpace::Screen` 固定在屏幕上,不受视口适配影响
## 场景基础
### 创建场景
```cpp
#include <extra2d/extra2d.h>
using namespace extra2d;
class GameScene : public Scene {
public:
void onEnter() override {
// 必须先调用父类方法
Scene::onEnter();
// 设置背景色
setBackgroundColor(Color(0.1f, 0.1f, 0.3f, 1.0f));
// 设置视口大小用于UI布局
setViewportSize(1280.0f, 720.0f);
}
void onExit() override {
// 清理资源
removeAllChildren();
Scene::onExit();
}
void onUpdate(float dt) override {
Scene::onUpdate(dt);
// 游戏逻辑更新
}
};
```
### 场景切换新API
通过 `SceneManager` 进行场景切换:
```cpp
auto& scenes = app.scenes();
// 运行第一个场景
scenes.runWithScene(makePtr<GameScene>());
// 替换当前场景(无过渡)
scenes.replaceScene(makePtr<GameScene>());
// 替换当前场景(有过渡效果)
scenes.replaceScene(makePtr<GameScene>(), TransitionType::Fade, 0.5f);
// 推入场景(保留当前场景)
scenes.pushScene(makePtr<NewScene>());
scenes.pushScene(makePtr<NewScene>(), TransitionType::SlideLeft, 0.5f);
// 弹出场景(返回上一个场景)
scenes.popScene();
scenes.popScene(TransitionType::Fade, 0.5f);
// 弹出到根场景
scenes.popToRootScene();
scenes.popToRootScene(TransitionType::Fade, 0.5f);
// 弹出到指定场景
scenes.popToScene("SceneName");
scenes.popToScene("SceneName", TransitionType::Fade, 0.5f);
```
### 过渡效果类型
```cpp
enum class TransitionType {
None, // 无过渡
Fade, // 淡入淡出
SlideLeft, // 向左滑动
SlideRight, // 向右滑动
SlideUp, // 向上滑动
SlideDown, // 向下滑动
Scale, // 缩放过渡
Flip, // 翻转过渡
Box // 盒子过渡
};
```
### 场景管理器
通过 `app.scenes()` 访问场景管理器:
```cpp
auto& scenes = app.scenes();
// 获取当前场景
auto current = scenes.getCurrentScene();
// 获取上一个场景
auto previous = scenes.getPreviousScene();
// 获取根场景
auto root = scenes.getRootScene();
// 通过名称获取场景
auto scene = scenes.getSceneByName("SceneName");
// 获取场景栈深度
size_t count = scenes.getSceneCount();
// 检查是否正在过渡
bool transitioning = scenes.isTransitioning();
// 清空场景栈
scenes.end();
```
## 场景生命周期
```
创建场景 (makePtr<Scene>)
进入场景 (runWithScene/replaceScene/pushScene)
onEnter() - 初始化资源
主循环
├── onUpdate(dt) - 每帧更新
└── onRender(renderer) - 每帧渲染
退出场景
onExit() - 清理资源
场景销毁
```
## 推箱子示例场景结构
```
┌─────────────────────────────────────┐
│ StartScene │
│ ┌─────────────────────────────┐ │
│ │ 开始菜单界面 │ │
│ │ - 新游戏 │ │
│ │ - 继续游戏 │ │
│ │ - 退出 │ │
│ └─────────────────────────────┘ │
└──────────────┬──────────────────────┘
│ 选择"新游戏"
┌─────────────────────────────────────┐
│ PlayScene │
│ ┌─────────────────────────────┐ │
│ │ 游戏主界面 │ │
│ │ - 地图渲染 │ │
│ │ - 玩家控制 │ │
│ │ - 关卡信息 │ │
│ └─────────────────────────────┘ │
└──────────────┬──────────────────────┘
│ 通关
┌─────────────────────────────────────┐
│ SuccessScene │
│ ┌─────────────────────────────┐ │
│ │ 通关界面 │ │
│ │ - 显示成绩 │ │
│ │ - 下一关/返回菜单 │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────────┘
```
## 代码示例:菜单场景
```cpp
class MenuScene : public Scene {
public:
void onEnter() override {
Scene::onEnter();
auto& app = Application::instance();
auto& resources = app.resources();
// 加载背景
auto bgTex = resources.loadTexture("assets/bg.jpg");
if (bgTex) {
auto bg = Sprite::create(bgTex);
bg->setAnchor(0.0f, 0.0f);
addChild(bg);
}
// 加载字体
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(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();
}
}
private:
void createMenuButtons() {
float centerX = 640.0f;
float startY = 300.0f;
float spacing = 50.0f;
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 updateMenuColors() {
for (int i = 0; i < buttons_.size(); ++i) {
auto color = (i == selectedIndex_) ? Colors::Red : Colors::White;
buttons_[i]->setTextColor(color);
}
}
void executeMenuItem() {
auto& scenes = Application::instance().scenes();
switch (selectedIndex_) {
case 0:
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. **分离场景逻辑** - 每个场景负责自己的功能
5. **使用视口适配** - 确保游戏在不同分辨率下正确显示
6. **正确处理窗口大小变化** - 在 `onRender()` 中检测并更新视口
## 下一步
- [03. 节点系统](./03_Node_System.md) - 学习节点和精灵的使用
- [04. 资源管理](./04_Resource_Management.md) - 深入了解资源加载

View File

@ -1,685 +0,0 @@
# 03. 节点系统
Extra2D 的节点系统是构建游戏对象的基础。所有可见的游戏元素都是节点的子类。
## 核心节点类型
```
Node (基类)
├── Sprite (精灵)
├── Text (文本)
├── Button (按钮)
├── Widget (UI控件基类)
│ ├── Label (标签)
│ ├── CheckBox (复选框)
│ ├── RadioButton (单选按钮)
│ ├── Slider (滑块)
│ └── ProgressBar (进度条)
└── 自定义节点...
```
## 基础节点操作
### 创建和添加节点
```cpp
// 创建精灵
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
// 位置(相对于父节点的本地坐标)
node->setPosition(Vec2(x, y));
Vec2 pos = node->getPosition();
// 旋转(角度,单位:度)
node->setRotation(45.0f);
float angle = node->getRotation();
// 缩放
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);
```
## ⚠️ 重要:坐标系与变换系统
### 坐标系说明
Extra2D 使用以下坐标系:
1. **本地坐标系Local Space**:相对于父节点的坐标
- 原点 `(0, 0)` 是父节点的锚点位置
- `setPosition(x, y)` 设置的是本地坐标
2. **世界坐标系World Space**:相对于场景根节点的绝对坐标
- 通过 `convertToWorldSpace()` 转换
- 通过 `getWorldTransform()` 获取变换矩阵
3. **屏幕坐标系Screen Space**:相对于屏幕左上角的坐标
- 原点 `(0, 0)` 在屏幕左上角
- Y轴向下为正方向
### 锚点Anchor机制
**锚点定义了节点的原点位置**
```cpp
// 锚点 (0, 0) - 左上角为原点
sprite->setAnchor(Vec2(0.0f, 0.0f));
sprite->setPosition(Vec2(100, 100)); // 左上角在 (100, 100)
// 锚点 (0.5, 0.5) - 中心为原点(推荐)
sprite->setAnchor(Vec2(0.5f, 0.5f));
sprite->setPosition(Vec2(100, 100)); // 中心在 (100, 100)
// 锚点 (1, 1) - 右下角为原点
sprite->setAnchor(Vec2(1.0f, 1.0f));
sprite->setPosition(Vec2(100, 100)); // 右下角在 (100, 100)
```
**⚠️ 重要:锚点偏移在渲染时处理,不在本地变换中**
这意味着:
- `getLocalTransform()` 返回的矩阵**不包含**锚点偏移
- 锚点偏移在 `GLSpriteBatch::addVertices()` 中应用
- 这样可以避免锚点偏移被父节点的缩放影响
### 父子变换传递
**变换层级**
```
Scene (世界坐标系)
└── GameOverLayer (本地坐标 + 父变换 = 世界坐标)
└── Panel (本地坐标 + 父变换 = 世界坐标)
└── ScoreNumber (本地坐标 + 父变换 = 世界坐标)
```
**关键方法**
```cpp
// 获取本地变换矩阵(不包含锚点偏移)
glm::mat4 localTransform = node->getLocalTransform();
// 获取世界变换矩阵(包含所有父节点的变换)
glm::mat4 worldTransform = node->getWorldTransform();
// 本地坐标转世界坐标
Vec2 worldPos = node->convertToWorldSpace(Vec2::Zero());
// 世界坐标转本地坐标
Vec2 localPos = node->convertToNodeSpace(worldPos);
```
**变换更新流程**
1. **标记脏状态**:当位置、旋转、缩放改变时
```cpp
void Node::setPosition(const Vec2& pos) {
position_ = pos;
markTransformDirty(); // 标记变换需要更新
}
```
2. **递归标记**`markTransformDirty()` 会递归标记所有子节点
```cpp
void Node::markTransformDirty() {
transformDirty_ = true;
worldTransformDirty_ = true;
for (auto& child : children_) {
child->markTransformDirty(); // 递归标记子节点
}
}
```
3. **批量更新**:在渲染前,`Scene::renderContent()` 调用 `batchUpdateTransforms()`
```cpp
void Scene::renderContent(RenderBackend& renderer) {
batchUpdateTransforms(); // 更新所有节点的世界变换
// ... 渲染
}
```
### 正确使用变换的示例
```cpp
// 创建层级结构
auto panel = Sprite::create(panelTexture);
panel->setAnchor(Vec2(0.5f, 0.5f)); // 中心锚点
panel->setPosition(Vec2(0, 256)); // 相对于父节点的中心
addChild(panel);
// 添加子节点到 panel
auto scoreNumber = makePtr<Number>();
scoreNumber->setPosition(Vec2(95.0f, 10.0f)); // 相对于 panel 中心
panel->addChild(scoreNumber);
// 当 panel 移动时scoreNumber 会自动跟随
// 因为 scoreNumber 的世界变换包含了 panel 的世界变换
```
### 常见错误
**错误1混淆本地坐标和世界坐标**
```cpp
// ❌ 错误:在世界坐标系中设置位置,但节点是子节点
child->setPosition(worldPos); // 这会被解释为本地坐标
// ✅ 正确:使用 convertToNodeSpace 转换
child->setPosition(parent->convertToNodeSpace(worldPos));
```
**错误2锚点设置不当**
```cpp
// ❌ 错误:锚点 (0,0) 但期望中心对齐
sprite->setAnchor(Vec2(0.0f, 0.0f));
sprite->setPosition(Vec2(100, 100)); // 左上角在 (100, 100)
// ✅ 正确:使用中心锚点
sprite->setAnchor(Vec2(0.5f, 0.5f));
sprite->setPosition(Vec2(100, 100)); // 中心在 (100, 100)
```
**错误3忽略父节点变换**
```cpp
// ❌ 错误:直接设置世界坐标,忽略父节点
child->setPosition(Vec2(100, 100)); // 这是本地坐标!
// ✅ 正确:考虑父节点的世界变换
// 如果父节点在 (50, 50),子节点相对于父节点应该是 (50, 50)
child->setPosition(Vec2(50, 50));
```
## 精灵Sprite
### 创建精灵
```cpp
auto& resources = Application::instance().resources();
// 从纹理创建
auto texture = resources.loadTexture("assets/player.png");
auto sprite = Sprite::create(texture);
// 设置精灵属性
sprite->setPosition(Vec2(400, 300));
sprite->setAnchor(Vec2(0.5f, 0.5f));
// 切换纹理
auto newTexture = resources.loadTexture("assets/player2.png");
sprite->setTexture(newTexture);
```
### 从图集创建精灵Texture Atlas
```cpp
// 加载图集纹理
auto atlasTexture = resources.loadTexture("assets/atlas.png");
// 创建精灵,指定图集中的矩形区域
Rect spriteRect(100, 100, 32, 32); // x, y, width, height
auto sprite = Sprite::create(atlasTexture, spriteRect);
```
### 精灵渲染流程
```cpp
void Sprite::onDraw(RenderBackend& renderer) {
// 1. 获取世界变换矩阵
auto worldTransform = getWorldTransform();
// 2. 从世界变换中提取位置和缩放
float worldX = worldTransform[3][0];
float worldY = worldTransform[3][1];
float worldScaleX = glm::length(glm::vec2(worldTransform[0][0], worldTransform[0][1]));
float worldScaleY = glm::length(glm::vec2(worldTransform[1][0], worldTransform[1][1]));
// 3. 计算目标矩形(不包含锚点偏移)
Rect destRect(worldX, worldY, width * worldScaleX, height * worldScaleY);
// 4. 提取旋转角度
float worldRotation = std::atan2(worldTransform[0][1], worldTransform[0][0]);
// 5. 绘制精灵(锚点偏移在 GLSpriteBatch 中处理)
renderer.drawSprite(*texture_, destRect, srcRect, color_, worldRotation, anchor);
}
```
## 按钮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->setOnClick([]() {
E2D_LOG_INFO("按钮被点击!");
});
addChild(button);
```
### 使用图片按钮
```cpp
// 从图集创建按钮
auto buttonFrame = ResLoader::getKeyFrame("button_play");
if (buttonFrame) {
auto button = Button::create();
button->setBackgroundImage(
buttonFrame->getTexture(),
buttonFrame->getRect() // 使用图集中的矩形区域
);
button->setPosition(Vec2(screenWidth / 2, 300));
button->setOnClick([]() {
// 处理点击
});
addChild(button);
}
```
### 按钮坐标空间
**按钮使用世界坐标空间World Space**
```cpp
// Button::getBoundingBox() 返回世界坐标系的包围盒
Rect Button::getBoundingBox() const {
auto pos = getRenderPosition(); // 获取世界坐标位置
// ... 计算包围盒
return Rect(x0, y0, w, h); // 世界坐标
}
```
**这意味着**
- 按钮的位置是相对于父节点的本地坐标
- 但按钮的点击检测使用世界坐标
- 父节点的变换会自动应用到按钮
### 按钮事件处理
```cpp
// 在 onUpdate 中检测手柄按键
void GameOverLayer::onUpdate(float dt) {
Node::onUpdate(dt);
auto& input = Application::instance().input();
// A 键触发重新开始按钮
if (input.isButtonPressed(GamepadButton::A)) {
restartBtn_->getEventDispatcher().dispatch(EventType::UIClicked);
}
}
```
## 节点层级管理
### 父子关系
```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:
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);
addChild(sprite_);
return true;
}
void onUpdate(float dt) override {
// 更新逻辑
velocity_.y += gravity_ * dt;
setPosition(getPosition() + velocity_ * dt);
// 重要:调用父类的 onUpdate确保子节点也被更新
Node::onUpdate(dt);
}
void jump() {
velocity_.y = jumpForce_;
}
private:
Ptr<Sprite> sprite_;
Vec2 velocity_;
float gravity_ = -980.0f;
float jumpForce_ = 500.0f;
};
// 使用
auto player = Player::create(texture);
scene->addChild(player);
```
## 完整示例GameOverLayer
```cpp
void GameOverLayer::onEnter() {
Node::onEnter();
auto& app = extra2d::Application::instance();
float screenWidth = static_cast<float>(app.getConfig().width);
float screenHeight = static_cast<float>(app.getConfig().height);
// 整体居中x 坐标相对于屏幕中心)
setPosition(extra2d::Vec2(screenWidth / 2.0f, screenHeight));
// 显示 "Game Over" 文字
auto gameOverFrame = ResLoader::getKeyFrame("text_game_over");
if (gameOverFrame) {
auto gameOver = extra2d::Sprite::create(
gameOverFrame->getTexture(),
gameOverFrame->getRect()
);
gameOver->setAnchor(extra2d::Vec2(0.5f, 0.0f));
gameOver->setPosition(extra2d::Vec2(0.0f, 120.0f));
addChild(gameOver);
}
// 初始化得分面板
initPanel(score_, screenHeight);
// 初始化按钮
initButtons();
// 创建向上移动的动画
auto moveAction = extra2d::makePtr<extra2d::MoveBy>(
1.0f, extra2d::Vec2(0.0f, -screenHeight)
);
runAction(moveAction);
}
void GameOverLayer::initPanel(int score, float screenHeight) {
// 显示得分板
auto panelFrame = ResLoader::getKeyFrame("score_panel");
if (!panelFrame) return;
auto panel = extra2d::Sprite::create(
panelFrame->getTexture(),
panelFrame->getRect()
);
panel->setAnchor(extra2d::Vec2(0.5f, 0.5f));
panel->setPosition(extra2d::Vec2(0.0f, screenHeight / 2.0f));
addChild(panel);
// 显示本局得分(相对于 panel 的本地坐标)
auto scoreNumber = extra2d::makePtr<Number>();
scoreNumber->setLittleNumber(score);
scoreNumber->setPosition(extra2d::Vec2(95.0f, 10.0f));
panel->addChild(scoreNumber);
// 显示最高分
static int bestScore = 0;
if (score > bestScore) bestScore = score;
auto bestNumber = extra2d::makePtr<Number>();
bestNumber->setLittleNumber(bestScore);
bestNumber->setPosition(extra2d::Vec2(95.0f, 50.0f));
panel->addChild(bestNumber);
// 显示 "New" 标记(如果破了记录)
if (score >= bestScore && score > 0) {
auto newFrame = ResLoader::getKeyFrame("new");
if (newFrame) {
auto newSprite = extra2d::Sprite::create(
newFrame->getTexture(),
newFrame->getRect()
);
newSprite->setAnchor(extra2d::Vec2(0.5f, 0.5f));
newSprite->setPosition(extra2d::Vec2(80.0f, 25.0f));
panel->addChild(newSprite);
}
}
}
```
## 最佳实践
1. **始终使用中心锚点0.5, 0.5**:除非有特殊需求,否则使用中心锚点可以使定位更直观
2. **理解本地坐标和世界坐标**
- `setPosition()` 设置的是本地坐标
- 使用 `convertToWorldSpace()``convertToNodeSpace()` 进行转换
3. **利用父子关系**
- 将相关的 UI 元素组织为父子关系
- 父节点移动时,子节点会自动跟随
4. **在 onEnter 中初始化**
- 不要在构造函数中创建子节点
- 在 `onEnter()` 中初始化,此时 `weak_from_this()` 可用
5. **调用父类的虚函数**
- 重写 `onUpdate()` 时,记得调用 `Node::onUpdate(dt)`
- 重写 `onEnter()` 时,记得调用 `Node::onEnter()`
6. **使用 batchUpdateTransforms**
- 引擎会自动在渲染前调用
- 如果需要强制更新变换,可以手动调用
7. **避免双重引用**
- 节点通过 `addChild()` 添加到场景后,由场景统一管理
- **不要**额外存储 `shared_ptr` 到 vector 中,避免双重引用问题
- 使用 `getChildren()` 访问子节点,配合 `dynamic_cast` 筛选特定类型
```cpp
// ❌ 错误:双重引用
class BadScene : public Scene {
private:
std::vector<Ptr<Sprite>> sprites_; // 不要这样做!
public:
void createSprite() {
auto sprite = Sprite::create(texture);
addChild(sprite);
sprites_.push_back(sprite); // 双重引用!
}
};
// ✅ 正确:通过 getChildren() 访问
class GoodScene : public Scene {
public:
void createSprite() {
auto sprite = Sprite::create(texture);
addChild(sprite); // 场景统一管理
}
void updateSprites() {
for (const auto& child : getChildren()) {
if (auto sprite = dynamic_cast<Sprite*>(child.get())) {
// 处理 sprite
}
}
}
};
```
## 动画系统
### 动作类型
Extra2D 提供了丰富的动作类:
| 动作类 | 说明 |
|--------|------|
| `MoveTo` | 移动到指定位置 |
| `MoveBy` | 移动指定偏移量 |
| `ScaleTo` | 缩放到指定比例 |
| `ScaleBy` | 缩放指定比例 |
| `RotateTo` | 旋转到指定角度 |
| `RotateBy` | 旋转指定角度 |
| `FadeIn` | 淡入 |
| `FadeOut` | 淡出 |
| `FadeTo` | 淡化到指定透明度 |
| `Delay` | 延迟 |
| `Sequence` | 顺序执行多个动作 |
| `Spawn` | 同时执行多个动作 |
| `Loop` | 循环执行动作 |
| `CallFunc` | 回调函数 |
### 运行动作
```cpp
// 移动动画
auto moveAction = makePtr<MoveBy>(1.0f, Vec2(0.0f, -100.0f));
node->runAction(moveAction);
// 缩放动画
auto scaleAction = makePtr<ScaleTo>(0.5f, 2.0f);
node->runAction(scaleAction);
// 淡入淡出
auto fadeOut = makePtr<FadeOut>(0.3f);
auto fadeIn = makePtr<FadeIn>(0.3f);
node->runAction(fadeOut);
```
### 动画完成回调
使用 `setCompletionCallback` 在动画完成时执行回调:
```cpp
// 参考 examples/flappy_bird/GameOverLayer.cpp
void GameOverLayer::onEnter() {
Node::onEnter();
// 创建向上移动的动画
auto moveAction = extra2d::makePtr<extra2d::MoveBy>(
1.0f, extra2d::Vec2(0.0f, -screenHeight));
// 设置动画完成回调
moveAction->setCompletionCallback([this]() {
animationDone_ = true;
// 动画完成后启用按钮
if (restartBtn_)
restartBtn_->setEnabled(true);
if (menuBtn_)
menuBtn_->setEnabled(true);
});
runAction(moveAction);
}
```
### 顺序和并行动画
```cpp
// 顺序执行:先移动,再缩放,最后淡出
auto sequence = makePtr<Sequence>({
new MoveTo(1.0f, Vec2(100, 100)),
new ScaleTo(0.5f, 2.0f),
new FadeOut(0.3f)
});
node->runAction(sequence);
// 并行执行:同时移动和旋转
auto spawn = makePtr<Spawn>({
new MoveTo(1.0f, Vec2(100, 100)),
new RotateBy(1.0f, 360.0f)
});
node->runAction(spawn);
// 循环执行
auto loop = makePtr<Loop(new RotateBy(1.0f, 360.0f), 5); // 旋转5次
node->runAction(loop);
```
### 动画进度回调
```cpp
auto action = makePtr<MoveTo>(2.0f, Vec2(100, 100));
action->setProgressCallback([](float progress) {
// progress: 0.0 - 1.0
E2D_LOG_INFO("动画进度: {}%", progress * 100);
});
node->runAction(action);
```
### 停止动画
```cpp
// 停止所有动画
node->stopAllActions();
// 停止特定动画(需要先设置 tag
action->setTag(1);
node->runAction(action);
node->stopActionByTag(1);
```
## 下一步
- [04. 资源管理](./04_Resource_Management.md) - 深入了解资源加载
- [05. 输入处理](./05_Input_Handling.md) - 学习输入处理
- [06. 碰撞检测](./06_Collision_Detection.md) - 学习碰撞检测系统

View File

@ -1,754 +0,0 @@
# 04. 资源管理
Extra2D 提供了统一的资源管理系统,用于加载和管理游戏中的各种资源。
## 资源管理器
通过 `Application::instance().resources()` 访问资源管理器:
```cpp
auto& resources = Application::instance().resources();
```
## 支持的资源类型
| 资源类型 | 加载方法 | 说明 |
|---------|---------|------|
| 纹理 | `loadTexture()` | 图片文件 (PNG, JPG, etc.) |
| 字体 | `loadFont()` | TrueType 字体文件 |
| 音频 | `loadSound()` / `loadMusic()` | 音频文件 |
## 纹理加载
### 基本用法
```cpp
// 加载纹理
auto texture = resources.loadTexture("assets/images/player.png");
if (texture) {
// 创建精灵
auto sprite = Sprite::create(texture);
addChild(sprite);
}
```
### 异步加载
Extra2D 支持异步加载纹理,避免阻塞主线程:
```cpp
// 同步加载(默认)
auto texture = resources.loadTexture("assets/images/player.png");
// 异步加载
auto texture = resources.loadTexture("assets/images/player.png", true);
// 使用回调函数处理异步加载完成
resources.loadTextureAsync("assets/images/player.png",
TextureFormat::Auto,
[](Ptr<Texture> texture, const std::string& path) {
if (texture) {
// 加载成功,可以安全使用
auto sprite = Sprite::create(texture);
// ...
}
});
```
### 纹理压缩格式
Extra2D 支持多种纹理压缩格式,可显著减少显存占用:
```cpp
// 支持的纹理格式
enum class TextureFormat {
Auto, // 自动选择最佳格式
RGBA8, // 32位 RGBA无压缩
RGB8, // 24位 RGB无压缩
DXT1, // DXT1 压缩(适用于不透明纹理)
DXT5, // DXT5 压缩(适用于透明纹理)
ETC2, // ETC2 压缩(移动平台)
ASTC4x4, // ASTC 4x4 高质量压缩
ASTC8x8 // ASTC 8x8 高压缩率
};
// 使用压缩格式加载纹理
auto texture = resources.loadTexture("assets/images/player.png", false, TextureFormat::DXT5);
// 异步加载 + 压缩
auto texture = resources.loadTexture("assets/images/player.png", true, TextureFormat::ASTC4x4);
```
**格式选择建议:**
| 格式 | 压缩比 | 质量 | 适用场景 |
|------|--------|------|---------|
| RGBA8 | 1:1 | 最高 | 小图标、需要最高质量 |
| DXT1 | 1:8 | 高 | 不透明纹理、大背景图 |
| DXT5 | 1:4 | 高 | 透明纹理、角色精灵 |
| ETC2 | 1:4 | 高 | 移动设备、跨平台 |
| ASTC4x4 | 1:4 | 很高 | 高质量透明纹理 |
| ASTC8x8 | 1:16 | 中等 | 大纹理、远景贴图 |
### 纹理缓存
资源管理器会自动缓存已加载的纹理,多次加载同一文件会返回缓存的实例:
```cpp
// 第一次加载 - 从文件读取
auto tex1 = resources.loadTexture("assets/image.png");
// 第二次加载 - 返回缓存
auto tex2 = resources.loadTexture("assets/image.png");
// tex1 和 tex2 指向同一个纹理对象
```
### LRU 缓存机制
Extra2D 使用 LRU (Least Recently Used) 算法管理纹理缓存,自动清理最久未使用的纹理:
```cpp
// 配置纹理缓存参数
auto& resources = Application::instance().resources();
// 设置缓存参数:最大缓存大小(字节)、最大纹理数量、自动清理间隔(秒)
resources.setTextureCache(
128 * 1024 * 1024, // 128MB 最大缓存
512, // 最多 512 个纹理
30.0f // 每 30 秒检查一次
);
// 在主循环中更新资源管理器(用于自动清理)
void GameScene::update(float dt) {
// 这会触发缓存清理检查
resources.update(dt);
}
```
#### 缓存统计
```cpp
// 获取缓存使用情况
size_t memoryUsage = resources.getTextureCacheMemoryUsage(); // 当前缓存大小(字节)
float hitRate = resources.getTextureCacheHitRate(); // 缓存命中率 (0.0 - 1.0)
size_t cacheSize = resources.getTextureCacheSize(); // 缓存中的纹理数量
// 打印详细统计信息
resources.printTextureCacheStats();
// 输出示例:
// [INFO] 纹理缓存统计:
// [INFO] 缓存纹理数: 45/512
// [INFO] 缓存大小: 32 / 128 MB
// [INFO] 缓存命中: 1024
// [INFO] 缓存未命中: 56
// [INFO] 命中率: 94.8%
```
#### 自动清理策略
LRU 缓存会自动执行以下清理策略:
1. **容量限制**:当缓存超过 `maxCacheSize``maxTextureCount` 时,自动驱逐最久未使用的纹理
2. **定时清理**:每 `unloadInterval` 秒检查一次,如果缓存超过 80%,清理到 50%
3. **访问更新**:每次访问纹理时,自动将其移到 LRU 链表头部(标记为最近使用)
```cpp
// 手动清理缓存
resources.clearTextureCache(); // 清空所有纹理缓存
resources.clearAllCaches(); // 清空所有资源缓存(纹理、字体、音效)
// 手动卸载特定纹理
resources.unloadTexture("assets/images/old_texture.png");
```
#### 缓存配置建议
| 平台 | 最大缓存大小 | 最大纹理数 | 清理间隔 | 说明 |
|------|-------------|-----------|---------|------|
| Switch 掌机模式 | 64-128 MB | 256-512 | 30s | 内存有限,保守设置 |
| Switch 主机模式 | 128-256 MB | 512-1024 | 30s | 内存充足,可以更大 |
| PC (MinGW) | 256-512 MB | 1024+ | 60s | 内存充足,可以更大 |
```cpp
// 根据平台设置不同的缓存策略
void setupCache() {
auto& resources = Application::instance().resources();
#ifdef __SWITCH__
// Switch 平台使用保守设置
resources.setTextureCache(64 * 1024 * 1024, 256, 30.0f);
#else
// PC 平台可以使用更大的缓存
resources.setTextureCache(256 * 1024 * 1024, 1024, 60.0f);
#endif
}
```
### 纹理图集Texture Atlas
Extra2D 自动使用纹理图集优化渲染性能:
```cpp
// 获取纹理图集管理器
auto& atlasManager = resources.getTextureAtlasManager();
// 将多个纹理打包到图集(自动进行)
// 渲染时,相同图集的精灵会自动批处理
// 手动创建图集(高级用法)
auto atlas = atlasManager.createAtlas("ui_atlas", 2048, 2048);
atlas->addTexture("button", buttonTexture);
atlas->addTexture("icon", iconTexture);
atlas->pack(); // 执行打包
```
## 精灵帧SpriteFrame
### 什么是精灵帧?
精灵帧是纹理图集中的一个矩形区域,用于从单个大纹理中提取小图像。使用精灵帧可以:
- 减少纹理切换,提高渲染性能
- 方便管理动画帧和 UI 元素
- 支持从 JSON 文件加载精灵帧数据
### 完整示例:资源加载器
参考 `examples/flappy_bird/ResLoader.h/cpp`
```cpp
// ResLoader.h
#pragma once
#include <extra2d/extra2d.h>
#include <map>
#include <string>
namespace flappybird {
/**
* @brief 音频类型枚举
*/
enum class MusicType {
Click, // 按键声音
Hit, // 小鸟死亡声音
Fly, // 小鸟飞翔声音
Point, // 得分声音
Swoosh // 转场声音
};
/**
* @brief 资源加载器类
* 管理纹理图集、精灵帧和音频资源的加载
*/
class ResLoader {
public:
static void init();
static extra2d::Ptr<extra2d::SpriteFrame> getKeyFrame(const std::string& name);
static void playMusic(MusicType type);
private:
struct ImageInfo {
float width, height, x, y;
};
static extra2d::Ptr<extra2d::Texture> atlasTexture_;
static std::map<std::string, ImageInfo> imageMap_;
static std::map<MusicType, extra2d::Ptr<extra2d::Sound>> soundMap_;
};
} // namespace flappybird
```
```cpp
// ResLoader.cpp
#include "ResLoader.h"
#include <json/json.hpp>
namespace flappybird {
extra2d::Ptr<extra2d::Texture> ResLoader::atlasTexture_;
std::map<std::string, ResLoader::ImageInfo> ResLoader::imageMap_;
std::map<MusicType, extra2d::Ptr<extra2d::Sound>> ResLoader::soundMap_;
void ResLoader::init() {
auto &resources = extra2d::Application::instance().resources();
// 加载图集纹理
atlasTexture_ = resources.loadTexture("assets/images/atlas.png");
if (!atlasTexture_) {
E2D_LOG_ERROR("无法加载图集纹理 atlas.png");
return;
}
// 加载 JSON 文件
std::string jsonContent = resources.loadJsonFile("assets/images/atlas.json");
if (jsonContent.empty()) {
E2D_LOG_ERROR("无法加载 atlas.json 文件");
return;
}
// 解析 JSON 图集数据
try {
nlohmann::json jsonData = nlohmann::json::parse(jsonContent);
for (const auto &sprite : jsonData["sprites"]) {
std::string name = sprite["name"];
float x = sprite["x"];
float y = sprite["y"];
float width = sprite["width"];
float height = sprite["height"];
ImageInfo info = {width, height, x, y};
imageMap_[name] = info;
}
E2D_LOG_INFO("成功加载 {} 个精灵帧", imageMap_.size());
} catch (const std::exception &e) {
E2D_LOG_ERROR("解析 atlas.json 失败: {}", e.what());
return;
}
// 加载音效
soundMap_[MusicType::Click] = resources.loadSound("assets/sound/click.wav");
soundMap_[MusicType::Hit] = resources.loadSound("assets/sound/hit.wav");
soundMap_[MusicType::Fly] = resources.loadSound("assets/sound/fly.wav");
soundMap_[MusicType::Point] = resources.loadSound("assets/sound/point.wav");
soundMap_[MusicType::Swoosh] = resources.loadSound("assets/sound/swoosh.wav");
E2D_LOG_INFO("资源加载完成");
}
extra2d::Ptr<extra2d::SpriteFrame>
ResLoader::getKeyFrame(const std::string &name) {
auto it = imageMap_.find(name);
if (it == imageMap_.end()) {
E2D_LOG_WARN("找不到精灵帧: %s", name.c_str());
return nullptr;
}
const ImageInfo &info = it->second;
return extra2d::makePtr<extra2d::SpriteFrame>(
atlasTexture_, extra2d::Rect(info.x, info.y, info.width, info.height));
}
void ResLoader::playMusic(MusicType type) {
auto it = soundMap_.find(type);
if (it != soundMap_.end() && it->second) {
it->second->play();
}
}
} // namespace flappybird
```
### 使用精灵帧创建精灵
```cpp
// 参考 examples/flappy_bird/GameScene.cpp
void GameScene::onEnter() {
BaseScene::onEnter();
// 从图集获取精灵帧
auto bgFrame = ResLoader::getKeyFrame("bg_day");
if (bgFrame) {
// 使用精灵帧创建精灵
auto background = extra2d::Sprite::create(
bgFrame->getTexture(),
bgFrame->getRect());
background->setAnchor(extra2d::Vec2(0.0f, 0.0f));
background->setPosition(extra2d::Vec2(0.0f, 0.0f));
addChild(background);
}
// 创建按钮
auto buttonFrame = ResLoader::getKeyFrame("button_play");
if (buttonFrame) {
auto button = extra2d::Button::create();
button->setBackgroundImage(buttonFrame->getTexture(), buttonFrame->getRect());
button->setAnchor(extra2d::Vec2(0.5f, 0.5f));
button->setPosition(extra2d::Vec2(screenWidth / 2.0f, 300.0f));
button->setOnClick([]() {
// 处理点击
});
addChild(button);
}
}
```
### JSON 图集格式
```json
{
"sprites": [
{ "name": "bg_day", "x": 0, "y": 0, "width": 288, "height": 512 },
{ "name": "bird0_0", "x": 288, "y": 0, "width": 34, "height": 24 },
{ "name": "button_play", "x": 322, "y": 0, "width": 116, "height": 70 }
]
}
```
## 字体加载
### 基本用法
```cpp
// 加载字体(指定字号)
auto font24 = resources.loadFont("assets/font.ttf", 24, true);
// 创建文本
auto text = Text::create("Hello World", font24);
addChild(text);
```
### 字体后备
支持设置后备字体,当主字体缺少某些字符时自动使用后备字体:
```cpp
// 加载主字体和后备字体
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.purgeUnused();
// 清空特定类型的缓存
resources.clearTextureCache(); // 清空纹理缓存
resources.clearFontCache(); // 清空字体缓存
resources.clearSoundCache(); // 清空音效缓存
// 清空所有缓存(谨慎使用)
resources.clearAllCaches();
// 检查是否有正在进行的异步加载
if (resources.hasPendingAsyncLoads()) {
// 等待所有异步加载完成
resources.waitForAsyncLoads();
}
```
## 内存管理
### 对象池Object Pool
Extra2D 提供高性能的对象池系统,用于高效分配和回收小对象,减少频繁的内存分配/释放开销。
#### 特性
| 特性 | 说明 |
|------|------|
| **自动内存对齐** | 自动使用 `alignof(T)` 确保对象正确对齐 |
| **侵入式空闲链表** | 零额外内存开销管理空闲对象 |
| **线程本地缓存** | 自动为每个线程提供本地缓存,减少锁竞争 |
| **自动容量管理** | 根据使用模式自动扩展和收缩 |
| **自动预热** | 首次使用时智能预分配 |
| **异常安全** | 自动处理析构异常 |
#### 基本用法
```cpp
#include <extra2d/utils/object_pool.h>
// 方式1直接使用对象池
extra2d::ObjectPool<MyObject> pool;
MyObject* obj = pool.allocate();
pool.deallocate(obj);
// 方式2使用智能指针自动管理推荐
auto obj = E2D_MAKE_POOLED(MyObject, arg1, arg2);
// 离开作用域自动回收
// 方式3使用 PooledAllocator
extra2d::PooledAllocator<MyObject> allocator;
auto obj = allocator.makeShared(arg1, arg2);
```
#### 完整示例:游戏撤销系统
```cpp
// 定义移动记录结构体
struct MoveRecord {
int fromX, fromY;
int toX, toY;
int boxFromX, boxFromY;
int boxToX, boxToY;
bool pushedBox;
MoveRecord() = default;
MoveRecord(int fx, int fy, int tx, int ty, bool pushed = false)
: fromX(fx), fromY(fy), toX(tx), toY(ty)
, boxFromX(-1), boxFromY(-1), boxToX(-1), boxToY(-1)
, pushedBox(pushed) {}
};
// 使用对象池创建移动记录
class GameScene : public Scene {
private:
std::stack<extra2d::Ptr<MoveRecord>> moveHistory_;
void move(int dx, int dy) {
// 使用对象池创建记录(自动管理内存)
auto record = E2D_MAKE_POOLED(MoveRecord, playerX, playerY,
playerX + dx, playerY + dy);
// 记录推箱子信息
if (pushedBox) {
record->pushedBox = true;
record->boxFromX = boxX;
record->boxFromY = boxY;
record->boxToX = newBoxX;
record->boxToY = newBoxY;
}
// 保存到历史栈
moveHistory_.push(record);
}
void undoMove() {
if (moveHistory_.empty()) return;
auto record = moveHistory_.top();
moveHistory_.pop();
// 恢复游戏状态
playerX = record->fromX;
playerY = record->fromY;
if (record->pushedBox) {
// 恢复箱子位置
}
// record 离开作用域后自动回收到对象池
}
};
```
#### 内存统计
```cpp
// 获取对象池内存使用情况
auto pool = extra2d::ObjectPoolManager::getInstance().getPool<MyObject>();
size_t allocated = pool->allocatedCount(); // 已分配对象数
size_t capacity = pool->capacity(); // 总容量
size_t memory = pool->memoryUsage(); // 内存使用量(字节)
```
#### 配置参数
```cpp
// 对象池配置(在 PoolConfig 中定义)
struct PoolConfig {
static constexpr size_t DEFAULT_BLOCK_SIZE = 64; // 每块对象数
static constexpr size_t THREAD_CACHE_SIZE = 16; // 线程缓存大小
static constexpr size_t SHRINK_THRESHOLD_MS = 30000; // 收缩检查间隔
static constexpr double SHRINK_RATIO = 0.5; // 收缩阈值
};
```
#### 性能优势
| 场景 | 传统分配 | 对象池 |
|------|---------|--------|
| 频繁分配/释放 | 大量内存碎片 | 零碎片 |
| 多线程竞争 | 锁竞争严重 | 线程本地缓存 |
| 内存对齐 | 手动处理 | 自动对齐 |
| 首次分配延迟 | 可能卡顿 | 自动预热 |
### 内存池(内部自动管理)
Extra2D 使用内存池优化小对象分配,无需用户干预:
```cpp
// 内存池自动管理以下对象:
// - 场景节点
// - 渲染命令
// - 碰撞形状
// - 事件对象
// 用户代码无需特殊处理,正常使用即可
auto node = Node::create(); // 自动使用内存池
auto sprite = Sprite::create(texture); // 自动使用内存池
```
### 批量更新(内部自动进行)
Extra2D 自动批量更新节点变换,优化性能:
```cpp
// 以下操作会自动批处理:
// - 节点变换更新
// - 渲染命令提交
// - 纹理绑定
// 用户代码无需特殊处理
for (int i = 0; i < 1000; ++i) {
auto sprite = Sprite::create(texture);
sprite->setPosition(i * 10, 100);
addChild(sprite); // 变换更新会自动批处理
}
```
## 渲染批处理
### 自动批处理
Extra2D 自动将渲染命令批处理以优化性能:
```cpp
// 以下情况会自动批处理:
// 1. 相同纹理的精灵
// 2. 相同图层的节点
// 3. 相同混合模式
// 示例1000 个相同纹理的精灵会自动批处理为少量 draw call
for (int i = 0; i < 1000; ++i) {
auto sprite = Sprite::create(texture);
addChild(sprite);
}
```
### 手动控制渲染顺序
```cpp
// 设置节点的渲染层级z-order
sprite->setZOrder(10); // 值越大,渲染越靠前
// 同层级的节点会自动批处理
```
## 完整示例
参考 `examples/push_box/StartScene.cpp`
```cpp
void StartScene::onEnter() {
Scene::onEnter();
auto& app = Application::instance();
auto& resources = app.resources();
// 加载背景纹理(异步 + 压缩)
auto bgTex = resources.loadTexture("assets/images/start.jpg", true, TextureFormat::DXT1);
if (bgTex) {
auto background = Sprite::create(bgTex);
background->setAnchor(0.0f, 0.0f);
addChild(background);
}
// 加载音效图标纹理(异步 + DXT5 压缩支持透明)
auto soundOn = resources.loadTexture("assets/images/soundon.png", true, TextureFormat::DXT5);
auto soundOff = resources.loadTexture("assets/images/soundoff.png", true, TextureFormat::DXT5);
if (soundOn && soundOff) {
soundIcon_ = Sprite::create(g_SoundOpen ? soundOn : soundOff);
addChild(soundIcon_);
}
// 加载字体
font_ = resources.loadFont("assets/font.ttf", 28, true);
// 创建按钮...
}
```
## 性能优化建议
### 纹理优化
1. **使用纹理压缩** - 对大型纹理使用 DXT/ASTC 压缩减少显存占用
2. **使用纹理图集** - 将多个小纹理打包到图集,减少 draw call
3. **异步加载大纹理** - 避免在主线程加载大型资源造成卡顿
4. **合理设置纹理尺寸** - 避免使用过大的纹理(建议最大 2048x2048
5. **配置合适的缓存大小** - 根据平台内存设置合理的 LRU 缓存参数
6. **监控缓存命中率** - 使用 `printTextureCacheStats()` 检查缓存效率
### 资源加载策略
```cpp
// 场景预加载
void GameScene::onEnter() {
auto& resources = Application::instance().resources();
// 预加载关键资源
resources.loadTexture("assets/textures/player.png", true);
resources.loadTexture("assets/textures/enemy.png", true);
resources.loadFont("assets/fonts/main.ttf", 24, true);
}
// 异步加载非关键资源
void GameScene::loadOptionalResources() {
resources.loadTextureAsync("assets/textures/background.jpg",
TextureFormat::DXT1,
[](Ptr<Texture> tex, const std::string& path) {
if (tex) {
// 加载完成后创建背景
}
});
}
```
## 最佳实践
1. **预加载资源** - 在场景 `onEnter()` 中加载所需资源
2. **检查资源有效性** - 始终检查加载结果是否为 nullptr
3. **复用资源** - 多次使用同一资源时保存指针,避免重复加载
4. **合理设置字号** - 字体加载时会生成对应字号的图集
5. **使用异步加载** - 对大型资源使用异步加载避免卡顿
6. **选择合适的压缩格式** - 根据纹理用途选择最佳压缩格式
7. **利用自动批处理** - 相同纹理的精灵会自动批处理,无需手动优化
8. **配置 LRU 缓存** - 根据平台内存配置合适的缓存大小
9. **定期监控缓存** - 在开发阶段定期检查缓存命中率和内存使用
10. **在主循环更新资源管理器** - 确保调用 `resources.update(dt)` 以触发自动清理
## 下一步
- [05. 输入处理](./05_Input_Handling.md) - 学习输入处理
- [06. 碰撞检测](./06_Collision_Detection.md) - 学习碰撞检测系统

View File

@ -1,320 +0,0 @@
# 05. 输入处理
Extra2D 提供了统一的输入处理系统,支持手柄、键盘等多种输入设备。
## 输入管理器
通过 `Application::instance().input()` 访问输入管理器:
```cpp
auto& input = Application::instance().input();
```
## 按键检测
### 检测方法
```cpp
// 按键是否按下(持续触发)
if (input.isButtonDown(GamepadButton::A)) {
// 每帧都会触发,只要按键保持按下
}
// 按键是否刚按下(单次触发)
if (input.isButtonPressed(GamepadButton::A)) {
// 只在按下瞬间触发一次
}
// 按键是否刚释放
if (input.isButtonReleased(GamepadButton::A)) {
// 只在释放瞬间触发一次
}
```
### 常用按键
| 按键 | 说明 | Xbox 对应 | Switch 对应 |
|------|------|-----------|-------------|
| `GamepadButton::A` | A 键 | A 键 | A 键 |
| `GamepadButton::B` | B 键 | B 键 | B 键 |
| `GamepadButton::X` | X 键 | X 键 | X 键 |
| `GamepadButton::Y` | Y 键 | Y 键 | Y 键 |
| `GamepadButton::Start` | 开始键 | Menu 键 | + 键 |
| `GamepadButton::Back` | 返回键 | View 键 | - 键 |
| `GamepadButton::Guide` | 导航键 | Xbox 键 | Home 键 |
| `GamepadButton::DPadUp` | 方向上 | 方向键上 | 方向键上 |
| `GamepadButton::DPadDown` | 方向下 | 方向键下 | 方向键下 |
| `GamepadButton::DPadLeft` | 方向左 | 方向键左 | 方向键左 |
| `GamepadButton::DPadRight` | 方向右 | 方向键右 | 方向键右 |
| `GamepadButton::LeftStick` | 左摇杆按下 | L3 | L3 |
| `GamepadButton::RightStick` | 右摇杆按下 | R3 | R3 |
| `GamepadButton::LeftShoulder` | 左肩键 | LB | L |
| `GamepadButton::RightShoulder` | 右肩键 | RB | R |
| `GamepadButton::LeftTrigger` | 左扳机键 | LT | ZL |
| `GamepadButton::RightTrigger` | 右扳机键 | RT | ZR |
## 摇杆输入
### 获取摇杆值
```cpp
// 获取左摇杆位置(范围 -1.0 到 1.0
Vec2 leftStick = input.getLeftStick();
// 获取右摇杆位置
Vec2 rightStick = input.getRightStick();
// 应用摇杆输入
float speed = 200.0f;
player->setPosition(player->getPosition() + leftStick * speed * dt);
```
### 摇杆死区
```cpp
// 设置摇杆死区(默认 0.15
input.setStickDeadZone(0.2f);
```
## 鼠标输入
### 鼠标按钮检测
```cpp
auto& input = Application::instance().input();
// 检测鼠标按钮按下(单次触发)
if (input.isMousePressed(MouseButton::Left)) {
// 左键刚按下
}
if (input.isMousePressed(MouseButton::Right)) {
// 右键刚按下
}
// 检测鼠标按钮状态(持续触发)
if (input.isMouseDown(MouseButton::Left)) {
// 左键保持按下
}
// 检测鼠标按钮释放
if (input.isMouseReleased(MouseButton::Left)) {
// 左键刚释放
}
```
### 鼠标位置
```cpp
// 获取鼠标位置(屏幕坐标)
Vec2 mousePos = input.getMousePosition();
// 获取鼠标在游戏视口中的位置(考虑视口适配)
// 参考 examples/flappy_bird 的 BaseScene 实现
```
### 鼠标按钮枚举
| 枚举值 | 说明 |
|--------|------|
| `MouseButton::Left` | 左键 |
| `MouseButton::Right` | 右键 |
| `MouseButton::Middle` | 中键 |
### 完整示例Flappy Bird 输入处理
参考 `examples/flappy_bird/GameScene.cpp`
```cpp
void GameScene::onUpdate(float dt) {
if (!gameOver_) {
auto &input = extra2d::Application::instance().input();
// 同时支持手柄 A 键和鼠标左键
if (input.isButtonPressed(extra2d::GamepadButton::A) ||
input.isMousePressed(extra2d::MouseButton::Left)) {
if (!started_) {
started_ = true;
startGame();
}
bird_->jump();
}
// 游戏逻辑更新...
}
BaseScene::onUpdate(dt);
}
```
### 完整示例Game Over 界面输入
参考 `examples/flappy_bird/GameOverLayer.cpp`
```cpp
void GameOverLayer::onUpdate(float dt) {
Node::onUpdate(dt);
// 动画完成后才响应输入
if (!animationDone_)
return;
auto &input = extra2d::Application::instance().input();
// A 键重新开始游戏
if (input.isButtonPressed(extra2d::GamepadButton::A)) {
ResLoader::playMusic(MusicType::Click);
auto &app = extra2d::Application::instance();
app.scenes().replaceScene(extra2d::makePtr<GameScene>(),
extra2d::TransitionType::Fade, 0.5f);
}
// B 键返回主菜单
if (input.isButtonPressed(extra2d::GamepadButton::B)) {
ResLoader::playMusic(MusicType::Click);
auto &app = extra2d::Application::instance();
app.scenes().replaceScene(extra2d::makePtr<StartScene>(),
extra2d::TransitionType::Fade, 0.5f);
}
}
```
## 完整示例
### 菜单导航
参考 `examples/flappy_bird/StartScene.cpp`
```cpp
void StartScene::onUpdate(float dt) {
Scene::onUpdate(dt);
auto& input = Application::instance().input();
// A 键开始游戏
if (input.isButtonPressed(GamepadButton::A)) {
ResLoader::playMusic(MusicType::Click);
startGame();
}
// BACK 键退出游戏
if (input.isButtonPressed(GamepadButton::Back)) {
ResLoader::playMusic(MusicType::Click);
auto &app = Application::instance();
app.quit();
}
}
```
### Game Over 界面手柄控制
参考 `examples/flappy_bird/GameOverLayer.cpp`
```cpp
void GameOverLayer::onUpdate(float dt) {
Node::onUpdate(dt);
// 检测手柄按键
auto &input = extra2d::Application::instance().input();
// A 键重新开始游戏
if (input.isButtonPressed(extra2d::GamepadButton::A)) {
ResLoader::playMusic(MusicType::Click);
auto &app = extra2d::Application::instance();
app.scenes().replaceScene(extra2d::makePtr<GameScene>(),
extra2d::TransitionType::Fade, 0.5f);
}
// B 键返回主菜单
if (input.isButtonPressed(extra2d::GamepadButton::B)) {
ResLoader::playMusic(MusicType::Click);
auto &app = extra2d::Application::instance();
app.scenes().replaceScene(extra2d::makePtr<StartScene>(),
extra2d::TransitionType::Fade, 0.5f);
}
}
```
### 玩家移动
```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. **添加输入缓冲** - 提升操作手感
5. **为常用操作分配标准按键**
- **A 键**:确认、跳跃、主要动作
- **B 键**:取消、返回、次要动作
- **Start 键**:暂停菜单
- **Back 键**:返回上一级/退出
## 下一步
- [06. 碰撞检测](./06_Collision_Detection.md) - 学习碰撞检测系统
- [07. UI 系统](./07_UI_System.md) - 学习 UI 控件使用

View File

@ -1,222 +0,0 @@
# 06. 碰撞检测
Extra2D 提供了基于空间索引的高效碰撞检测系统,支持四叉树和空间哈希两种策略。
## 完整示例
参考示例代码:
- `examples/collision_demo/main.cpp` - 基础碰撞检测演示
- `examples/spatial_index_demo/main.cpp` - 空间索引性能演示
## 启用碰撞检测
### 1. 创建可碰撞节点
```cpp
class CollidableBox : public Node {
public:
CollidableBox(float width, float height, const Color& color)
: width_(width), height_(height), color_(color), isColliding_(false) {
// 启用空间索引 - 这是关键!
setSpatialIndexed(true);
}
// 必须实现 getBoundingBox 方法
Rect getBoundingBox() const override {
Vec2 pos = getPosition();
return Rect(pos.x - width_ / 2, pos.y - height_ / 2, width_, height_);
}
void setColliding(bool colliding) { isColliding_ = colliding; }
void onRender(RenderBackend& renderer) override {
Vec2 pos = getPosition();
// 碰撞时变红色
Color fillColor = isColliding_ ? Color(1.0f, 0.2f, 0.2f, 0.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 width_, height_;
Color color_;
bool isColliding_;
};
```
### 2. 执行碰撞检测
```cpp
class GameScene : public Scene {
public:
void onUpdate(float dt) override {
Scene::onUpdate(dt);
// 清除之前的碰撞状态
for (const auto& child : getChildren()) {
if (auto box = dynamic_cast<CollidableBox*>(child.get())) {
box->setColliding(false);
}
}
// 使用场景的空间索引查询所有碰撞
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);
}
}
}
void createBox(float x, float y) {
auto box = makePtr<CollidableBox>(50.0f, 50.0f, Color(0.3f, 0.7f, 1.0f, 0.8f));
box->setPosition(Vec2(x, y));
addChild(box); // 通过 addChild 管理节点生命周期
}
};
```
**注意**:节点通过 `addChild()` 添加到场景后,由场景统一管理生命周期。不要额外存储 `shared_ptr` 到 vector 中,避免双重引用问题。
## 空间索引策略
### 切换策略
```cpp
// 获取空间管理器
auto& spatialManager = getSpatialManager();
// 切换到四叉树
spatialManager.setStrategy(SpatialStrategy::QuadTree);
// 切换到空间哈希
spatialManager.setStrategy(SpatialStrategy::SpatialHash);
// 获取当前策略名称
const char* name = spatialManager.getStrategyName();
```
### 策略对比
| 策略 | 适用场景 | 特点 |
|------|---------|------|
| QuadTree | 节点分布不均匀 | 分层划分,适合稀疏分布 |
| SpatialHash | 节点分布均匀 | 均匀网格,适合密集分布 |
## 性能演示
`examples/spatial_index_demo/main.cpp` 展示了空间索引的性能优势:
```cpp
class SpatialIndexDemoScene : public Scene {
public:
void onEnter() override {
Scene::onEnter();
// 创建100个碰撞节点
createPhysicsNodes(100);
E2D_LOG_INFO("空间索引已启用: {}", isSpatialIndexingEnabled());
}
void onUpdate(float dt) override {
Scene::onUpdate(dt);
// 更新所有物理节点位置(通过 getChildren() 访问)
for (const auto& child : getChildren()) {
if (auto node = dynamic_cast<PhysicsNode*>(child.get())) {
node->update(dt, screenWidth_, screenHeight_);
}
}
// 使用空间索引进行碰撞检测
performCollisionDetection();
// 按 X 键切换索引策略
auto& input = Application::instance().input();
if (input.isButtonPressed(GamepadButton::X)) {
toggleSpatialStrategy();
}
}
private:
void performCollisionDetection() {
// 清除之前的碰撞状态
for (const auto& child : getChildren()) {
if (auto node = dynamic_cast<PhysicsNode*>(child.get())) {
node->setColliding(false);
}
}
// 使用引擎自带的空间索引进行碰撞检测
auto collisions = queryCollisions();
// 标记碰撞的节点
for (const auto& [nodeA, nodeB] : collisions) {
if (auto boxA = dynamic_cast<PhysicsNode*>(nodeA)) {
boxA->setColliding(true);
}
if (auto boxB = dynamic_cast<PhysicsNode*>(nodeB)) {
boxB->setColliding(true);
}
}
}
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("切换到四叉树策略");
}
}
// 获取物理节点数量
size_t getPhysicsNodeCount() const {
size_t count = 0;
for (const auto& child : getChildren()) {
if (dynamic_cast<PhysicsNode*>(child.get())) {
++count;
}
}
return count;
}
};
```
**关键改进**
- 使用 `getChildren()` 代替私有 vector 存储节点引用
- 通过 `dynamic_cast` 筛选特定类型的子节点
- 避免双重引用,简化生命周期管理
## 关键要点
1. **必须调用 `setSpatialIndexed(true)`** - 启用节点的空间索引
2. **必须实现 `getBoundingBox()`** - 返回准确的边界框
3. **在 `onEnter()` 中调用 `Scene::onEnter()`** - 确保节点正确注册到空间索引
4. **使用 `queryCollisions()`** - 自动利用空间索引优化检测
## 下一步
- [07. UI 系统](./07_UI_System.md) - 学习 UI 控件使用
- [08. 音频系统](./08_Audio_System.md) - 学习音频播放

View File

@ -1,718 +0,0 @@
# 07. UI 系统
Extra2D 提供了一套完整的 UI 系统,支持按钮、文本、标签、复选框、单选按钮、滑块、进度条等常用控件。
## 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);
```
## 通用设置方法
所有 UI 组件都支持以下设置方法(来自基类 Node 和 Widget
```cpp
// 来自 Node 基类
widget->setPosition(x, y); // 设置位置
widget->setAnchor(x, y); // 设置锚点 (0-1)
// 来自 Widget 基类
widget->setCoordinateSpace(space); // 设置坐标空间
widget->setScreenPosition(x, y); // 设置屏幕位置
widget->setCameraOffset(x, y); // 设置相机偏移
```
## 按钮Button
### 创建按钮
```cpp
auto& resources = Application::instance().resources();
auto font = resources.loadFont("assets/font.ttf", 24);
// 创建按钮
auto button = Button::create();
button->setText("点击我");
button->setFont(font);
button->setPosition(Vec2(400, 300));
button->setSize(200, 60);
button->setTextColor(Colors::White);
button->setBackgroundColor(Colors::Blue, Colors::Green, Colors::Red);
button->setBorder(Colors::White, 2.0f);
// 设置点击回调
button->setOnClick([]() {
E2D_LOG_INFO("按钮被点击!");
});
addChild(button);
```
### 按钮属性设置
```cpp
// 文本和字体
button->setText("新文本");
button->setFont(font);
button->setTextColor(Colors::White);
// 尺寸和内边距
button->setCustomSize(200.0f, 60.0f);
button->setPadding(Vec2(10.0f, 5.0f));
// 背景颜色(正常、悬停、按下三种状态)
button->setBackgroundColor(
Colors::Blue, // 正常状态
Colors::Green, // 悬停状态
Colors::Red // 按下状态
);
// 边框
button->setBorder(Colors::White, 2.0f);
// 圆角
button->setRoundedCornersEnabled(true);
button->setCornerRadius(8.0f);
// 图片背景(普通按钮)
button->setBackgroundImage(normalTex, hoverTex, pressedTex);
button->setBackgroundImageScaleMode(ImageScaleMode::ScaleFit);
// 悬停光标
button->setHoverCursor(CursorShape::Hand);
```
### 切换按钮模式Toggle Button
按钮支持切换模式,可以在 on/off 两种状态间切换:
```cpp
// 创建切换按钮
auto toggleBtn = Button::create();
toggleBtn->setToggleMode(true); // 启用切换模式
// 设置状态图片off状态图片, on状态图片
toggleBtn->setStateBackgroundImage(soundOffTex, soundOnTex);
// 或设置带悬停/按下效果的状态图片
toggleBtn->setStateBackgroundImage(
soundOffTex, soundOnTex, // 普通状态
soundOffHoverTex, soundOnHoverTex, // 悬停状态(可选)
soundOffPressedTex, soundOnPressedTex // 按下状态(可选)
);
// 设置状态文字
toggleBtn->setStateText("关闭", "开启");
// 设置状态文字颜色
toggleBtn->setStateTextColor(Colors::Red, Colors::Green);
// 设置初始状态
toggleBtn->setOn(true);
// 获取当前状态
bool isOn = toggleBtn->isOn();
// 手动切换状态
toggleBtn->toggle();
// 状态改变回调
toggleBtn->setOnStateChange([](bool isOn) {
E2D_LOG_INFO("切换按钮状态: {}", isOn ? "开启" : "关闭");
});
// 点击回调(切换模式也会触发此回调)
toggleBtn->setOnClick([]() {
E2D_LOG_INFO("按钮被点击");
});
```
### 图片缩放模式
```cpp
enum class ImageScaleMode {
Original, // 使用原图大小
Stretch, // 拉伸填充
ScaleFit, // 等比缩放,保持完整显示
ScaleFill // 等比缩放,填充整个区域(可能裁剪)
};
```
### 透明按钮(菜单项)
```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->setPadding(Vec2(0.0f, 0.0f));
menuBtn->setCustomSize(200.0f, 40.0f);
menuBtn->setAnchor(0.5f, 0.5f); // 中心锚点
menuBtn->setPosition(centerX, centerY);
addChild(menuBtn);
```
### 按钮启用/禁用
Widget 基类提供了 `setEnabled()` 方法控制按钮的交互状态:
```cpp
// 禁用按钮
button->setEnabled(false);
// 启用按钮
button->setEnabled(true);
// 检查按钮状态
bool isEnabled = button->isEnabled();
```
### 完整示例:动画完成后启用按钮
参考 `examples/flappy_bird/GameOverLayer.cpp`
```cpp
// GameOverLayer.h
class GameOverLayer : public extra2d::Node {
public:
GameOverLayer(int score);
void onEnter() override;
void onUpdate(float dt) override;
private:
void initButtons();
int score_ = 0;
bool animationDone_ = false;
extra2d::Ptr<extra2d::Button> restartBtn_;
extra2d::Ptr<extra2d::Button> menuBtn_;
};
// GameOverLayer.cpp
void GameOverLayer::onEnter() {
Node::onEnter();
// 初始化按钮(初始禁用)
initButtons();
// 创建动画
auto moveAction = extra2d::makePtr<extra2d::MoveBy>(
1.0f, extra2d::Vec2(0.0f, -screenHeight));
// 动画完成后启用按钮
moveAction->setCompletionCallback([this]() {
animationDone_ = true;
if (restartBtn_)
restartBtn_->setEnabled(true);
if (menuBtn_)
menuBtn_->setEnabled(true);
});
runAction(moveAction);
}
void GameOverLayer::initButtons() {
auto restartFrame = ResLoader::getKeyFrame("button_restart");
if (restartFrame) {
restartBtn_ = extra2d::Button::create();
restartBtn_->setBackgroundImage(restartFrame->getTexture(),
restartFrame->getRect());
restartBtn_->setAnchor(extra2d::Vec2(0.5f, 0.5f));
restartBtn_->setPosition(extra2d::Vec2(0.0f, 360.0f));
restartBtn_->setEnabled(false); // 初始禁用
restartBtn_->setOnClick([]() {
// 处理点击
});
addChild(restartBtn_);
}
// 菜单按钮类似...
}
void GameOverLayer::onUpdate(float dt) {
Node::onUpdate(dt);
// 动画完成后才响应手柄输入
if (!animationDone_)
return;
auto &input = extra2d::Application::instance().input();
if (input.isButtonPressed(extra2d::GamepadButton::A)) {
// 重新开始
}
}
```
### 使用场景
- **动画播放期间**:禁用按钮,防止用户过早操作
- **加载过程中**:禁用按钮,显示加载状态
- **条件限制**:当条件不满足时禁用按钮(如未选择关卡)
- **切换按钮**:音效开关、设置开关等需要显示两种状态的按钮
## 文本Text
### 创建文本
```cpp
// 创建文本
auto text = Text::create("Hello World", font);
text->setPosition(Vec2(100, 50));
text->setTextColor(Colors::White);
text->setFontSize(24);
text->setAlignment(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);
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带标签
checkBox = CheckBox::create("启用音效");
checkBox->setPosition(Vec2(100, 200));
checkBox->setFont(font);
checkBox->setTextColor(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);
// 值改变回调
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);
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", 24);
// 标题
auto title = Text::create("设置", font_);
title->setPosition(Vec2(400, 100));
title->setAlignment(Alignment::Center);
addChild(title);
// 音效开关(使用切换按钮)
auto soundLabel = Label::create("音效", font_);
soundLabel->setPosition(Vec2(200, 200));
addChild(soundLabel);
auto soundOn = resources.loadTexture("assets/sound_on.png");
auto soundOff = resources.loadTexture("assets/sound_off.png");
soundToggle_ = Button::create();
soundToggle_->setPosition(Vec2(350, 200));
soundToggle_->setToggleMode(true);
soundToggle_->setStateBackgroundImage(soundOff, soundOn);
soundToggle_->setOn(true);
soundToggle_->setOnStateChange([](bool isOn) {
E2D_LOG_INFO("音效: {}", isOn ? "开启" : "关闭");
AudioManager::instance().setEnabled(isOn);
});
addChild(soundToggle_);
// 音量滑块
auto volumeLabel = Label::create("音量", font_);
volumeLabel->setPosition(Vec2(200, 280));
addChild(volumeLabel);
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_);
// 难度选择
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_;
Ptr<Button> soundToggle_;
Ptr<Slider> volumeSlider_;
};
```
## 最佳实践
1. **使用屏幕空间** - UI 控件通常使用 `CoordinateSpace::Screen` 固定在屏幕上
2. **设置合适的锚点** - 使用锚点0.5, 0.5)让控件中心对齐,方便布局
3. **复用字体资源** - 避免重复加载相同字体
4. **使用回调函数** - 使用 `setOnClick`、`setOnValueChange` 等回调响应用户操作
5. **使用切换按钮** - 对于需要显示两种状态的按钮(如开关),使用 `setToggleMode(true)`
## 下一步
- [08. 音频系统](./08_Audio_System.md) - 学习音频播放

View File

@ -1,369 +0,0 @@
# 08. 音频系统
Extra2D 提供了基于 SDL2_mixer 的音频播放系统,支持音效播放。
## 音频引擎
通过 `Application::instance().audio()` 访问音频引擎:
```cpp
auto& audio = Application::instance().audio();
```
## 播放音效
### 基本用法
```cpp
// 加载音效
auto sound = audio.loadSound("assets/audio/jump.wav");
// 播放音效
sound->play();
// 设置音量 (0.0 - 1.0)
sound->setVolume(0.8f);
```
### 音效控制
```cpp
// 停止播放
sound->stop();
// 暂停
sound->pause();
// 恢复
sound->resume();
// 循环播放
sound->setLooping(true);
// 检查播放状态
bool playing = sound->isPlaying();
bool paused = sound->isPaused();
```
### 音调和播放位置
```cpp
// 设置音调(当前实现不支持)
sound->setPitch(1.0f);
// 获取/设置播放位置(当前实现不支持)
float cursor = sound->getCursor();
sound->setCursor(0.0f);
// 获取音频时长(当前实现不支持)
float duration = sound->getDuration();
```
## 全局音量控制
```cpp
// 设置主音量
audio.setMasterVolume(0.8f);
// 获取主音量
float volume = audio.getMasterVolume();
```
## 全局播放控制
```cpp
// 暂停所有音效
audio.pauseAll();
// 恢复所有音效
audio.resumeAll();
// 停止所有音效
audio.stopAll();
// 卸载指定音效
audio.unloadSound("jump");
// 卸载所有音效
audio.unloadAllSounds();
```
## 完整示例
### Flappy Bird 音效管理器
参考 `examples/flappy_bird/ResLoader.h/cpp`
```cpp
// ResLoader.h
#pragma once
#include <extra2d/extra2d.h>
#include <map>
namespace flappybird {
enum class MusicType {
Click, // 按键声音
Hit, // 小鸟死亡声音
Fly, // 小鸟飞翔声音
Point, // 得分声音
Swoosh // 转场声音
};
class ResLoader {
public:
static void init();
static void playMusic(MusicType type);
private:
static std::map<MusicType, extra2d::Ptr<extra2d::Sound>> soundMap_;
};
} // namespace flappybird
```
```cpp
// ResLoader.cpp
#include "ResLoader.h"
namespace flappybird {
std::map<MusicType, extra2d::Ptr<extra2d::Sound>> ResLoader::soundMap_;
void ResLoader::init() {
auto &resources = extra2d::Application::instance().resources();
// 加载所有音效
soundMap_[MusicType::Click] = resources.loadSound("assets/sound/click.wav");
soundMap_[MusicType::Hit] = resources.loadSound("assets/sound/hit.wav");
soundMap_[MusicType::Fly] = resources.loadSound("assets/sound/fly.wav");
soundMap_[MusicType::Point] = resources.loadSound("assets/sound/point.wav");
soundMap_[MusicType::Swoosh] = resources.loadSound("assets/sound/swoosh.wav");
}
void ResLoader::playMusic(MusicType type) {
auto it = soundMap_.find(type);
if (it != soundMap_.end() && it->second) {
it->second->play();
}
}
} // namespace flappybird
```
### 在游戏中使用
```cpp
// GameScene.cpp - 得分时播放音效
if (pipeX <= birdX) {
score_++;
scoreNumber_->setNumber(score_);
firstPipe->scored = true;
ResLoader::playMusic(MusicType::Point); // 播放得分音效
}
// bird.cpp - 跳跃时播放音效
void Bird::jump() {
velocity_.y = jumpForce_;
ResLoader::playMusic(MusicType::Fly); // 播放飞翔音效
}
// GameOverLayer.cpp - 按钮点击时播放音效
restartBtn_->setOnClick([]() {
ResLoader::playMusic(MusicType::Click); // 播放点击音效
auto &app = extra2d::Application::instance();
app.scenes().replaceScene(extra2d::makePtr<GameScene>(),
extra2d::TransitionType::Fade, 0.5f);
});
```
### 推箱子音效管理器
```cpp
// audio_manager.h
#pragma once
#include <extra2d/extra2d.h>
namespace pushbox {
class AudioManager {
public:
static AudioManager& instance() {
static AudioManager instance;
return instance;
}
void init();
void setEnabled(bool enabled);
void playMoveSound();
void playBoxMoveSound();
void playWinSound();
private:
AudioManager() = default;
bool enabled_ = true;
extra2d::Ptr<extra2d::Sound> moveSound_;
extra2d::Ptr<extra2d::Sound> boxMoveSound_;
extra2d::Ptr<extra2d::Sound> winSound_;
};
} // namespace pushbox
```
```cpp
// audio_manager.cpp
#include "audio_manager.h"
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. **使用单例管理器** - 集中管理音频资源
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 引擎的核心功能:
1. [快速开始](./01_Quick_Start.md) - 引擎基础
2. [场景系统](./02_Scene_System.md) - 场景管理
3. [节点系统](./03_Node_System.md) - 游戏对象
4. [资源管理](./04_Resource_Management.md) - 资源加载
5. [输入处理](./05_Input_Handling.md) - 输入控制
6. [碰撞检测](./06_Collision_Detection.md) - 空间索引
7. [UI 系统](./07_UI_System.md) - 界面控件
8. [音频系统](./08_Audio_System.md) - 音频播放
开始你的游戏开发之旅吧!

View File

@ -1,598 +0,0 @@
# 动作系统 (Action System)
Extra2D 的动作系统提供了一套完整的动画解决方案,参考 Cocos2d-x 的设计模式,支持丰富的动作类型和缓动效果。
## 目录
1. [概述](#概述)
2. [核心概念](#核心概念)
3. [基本动作](#基本动作)
4. [组合动作](#组合动作)
5. [缓动动作](#缓动动作)
6. [特殊动作](#特殊动作)
7. [瞬时动作](#瞬时动作)
8. [动作管理](#动作管理)
9. [完整示例](#完整示例)
---
## 概述
动作系统用于在节点上创建动画效果,通过修改节点的属性(位置、缩放、旋转、透明度等)实现各种动画。
### 类层次结构
```
Action (基类)
├── FiniteTimeAction (有限时间动作)
│ ├── ActionInterval (时间间隔动作)
│ │ ├── MoveBy/MoveTo
│ │ ├── JumpBy/JumpTo
│ │ ├── BezierBy/BezierTo
│ │ ├── ScaleBy/ScaleTo
│ │ ├── RotateBy/RotateTo
│ │ ├── FadeIn/FadeOut/FadeTo
│ │ ├── Blink
│ │ ├── TintBy/TintTo
│ │ ├── Sequence
│ │ ├── Spawn
│ │ ├── Repeat/RepeatForever
│ │ ├── DelayTime
│ │ ├── ReverseTime
│ │ └── ActionEase (缓动动作基类)
│ └── ActionInstant (瞬时动作)
│ ├── CallFunc/CallFuncN
│ ├── Place
│ ├── FlipX/FlipY
│ ├── Show/Hide
│ ├── ToggleVisibility
│ └── RemoveSelf
├── Speed (速度控制)
├── Follow (跟随动作)
└── TargetedAction (目标动作)
```
---
## 核心概念
### 创建动作
所有动作都使用静态工厂方法 `create()` 创建:
```cpp
// 创建移动动作
auto move = MoveTo::create(2.0f, Vec2(100, 100));
// 创建缩放动作
auto scale = ScaleTo::create(1.0f, 2.0f);
// 创建旋转动作
auto rotate = RotateBy::create(3.0f, 360.0f);
```
### 运行动作
通过节点的 `runAction()` 方法运行动作:
```cpp
sprite->runAction(move);
```
### 动作完成回调
使用 `setCompletionCallback()` 设置动作完成时的回调:
```cpp
auto action = MoveTo::create(2.0f, Vec2(100, 100));
action->setCompletionCallback([]() {
E2D_LOG_INFO("动作完成!");
});
sprite->runAction(action);
```
---
## 基本动作
### 移动动作
```cpp
// MoveTo - 移动到指定位置
auto moveTo = MoveTo::create(2.0f, Vec2(100, 100));
// MoveBy - 相对移动
auto moveBy = MoveBy::create(2.0f, Vec2(50, 0)); // 向右移动 50 像素
```
### 跳跃动作
```cpp
// JumpTo - 跳跃到指定位置
auto jumpTo = JumpTo::create(2.0f, Vec2(100, 100), 50, 3); // 跳到 (100,100),高度 50跳 3 次
// JumpBy - 相对跳跃
auto jumpBy = JumpBy::create(2.0f, Vec2(100, 0), 50, 3); // 向右跳跃 100 像素
```
### 贝塞尔曲线动作
```cpp
BezierConfig bezier;
bezier.controlPoint1 = Vec2(100, 200);
bezier.controlPoint2 = Vec2(200, 100);
bezier.endPosition = Vec2(300, 150);
// BezierTo - 贝塞尔曲线移动到指定位置
auto bezierTo = BezierTo::create(3.0f, bezier);
// BezierBy - 相对贝塞尔曲线移动
auto bezierBy = BezierBy::create(3.0f, bezier);
```
### 缩放动作
```cpp
// ScaleTo - 缩放到指定比例
auto scaleTo = ScaleTo::create(1.0f, 2.0f); // 缩放到 2 倍
auto scaleToXY = ScaleTo::create(1.0f, 2.0f, 1.5f); // X 缩放 2 倍Y 缩放 1.5 倍
// ScaleBy - 相对缩放
auto scaleBy = ScaleBy::create(1.0f, 0.5f); // 缩小一半
```
### 旋转动作
```cpp
// RotateTo - 旋转到指定角度
auto rotateTo = RotateTo::create(2.0f, 90.0f); // 旋转到 90 度
// RotateBy - 相对旋转
auto rotateBy = RotateBy::create(2.0f, 360.0f); // 旋转 360 度
```
### 淡入淡出动作
```cpp
// FadeIn - 淡入(透明度从 0 到 1
auto fadeIn = FadeIn::create(1.0f);
// FadeOut - 淡出(透明度从 1 到 0
auto fadeOut = FadeOut::create(1.0f);
// FadeTo - 淡入到指定透明度
auto fadeTo = FadeTo::create(1.0f, 0.5f); // 淡入到 50% 透明度
```
### 闪烁动作
```cpp
// Blink - 闪烁
auto blink = Blink::create(2.0f, 5); // 2 秒内闪烁 5 次
```
### 色调动作
```cpp
// TintTo - 变化到指定颜色
auto tintTo = TintTo::create(1.0f, 255, 0, 0); // 变为红色
// TintBy - 相对颜色变化
auto tintBy = TintBy::create(1.0f, 50, 0, 0); // 红色通道增加 50
```
---
## 组合动作
### Sequence - 序列动作
按顺序依次执行多个动作:
```cpp
auto sequence = Sequence::create(
MoveTo::create(1.0f, Vec2(100, 100)),
DelayTime::create(0.5f),
FadeOut::create(1.0f),
CallFunc::create([]() { E2D_LOG_INFO("完成"); }),
nullptr // 必须以 nullptr 结尾
);
sprite->runAction(sequence);
```
### Spawn - 并行动作
同时执行多个动作:
```cpp
auto spawn = Spawn::create(
MoveTo::create(2.0f, Vec2(200, 200)),
RotateBy::create(2.0f, 360),
FadeIn::create(2.0f),
nullptr
);
sprite->runAction(spawn);
```
### Repeat - 重复动作
```cpp
// Repeat - 重复指定次数
auto repeat = Repeat::create(
Sequence::create(
MoveBy::create(0.5f, Vec2(50, 0)),
MoveBy::create(0.5f, Vec2(-50, 0)),
nullptr
),
5 // 重复 5 次
);
// RepeatForever - 永久重复
auto repeatForever = RepeatForever::create(
RotateBy::create(1.0f, 360)
);
sprite->runAction(repeatForever);
```
### DelayTime - 延时动作
```cpp
auto sequence = Sequence::create(
MoveTo::create(1.0f, Vec2(100, 0)),
DelayTime::create(1.0f), // 延时 1 秒
MoveTo::create(1.0f, Vec2(200, 0)),
nullptr
);
```
### ReverseTime - 反向动作
```cpp
auto move = MoveBy::create(1.0f, Vec2(100, 0));
auto reverse = ReverseTime::create(move->clone());
// 反向执行,相当于 MoveBy(1.0f, Vec2(-100, 0))
```
---
## 缓动动作
缓动动作使用装饰器模式包装其他动作,实现各种缓动效果。
### 使用缓动
```cpp
// 创建基础动作
auto move = MoveTo::create(3.0f, Vec2(500, 300));
// 用缓动包装
auto easeMove = EaseElasticOut::create(move);
sprite->runAction(easeMove);
```
### 可用缓动类型
| 缓动类 | 效果 |
|--------|------|
| `EaseQuadIn/Out/InOut` | 二次缓动 |
| `EaseCubicIn/Out/InOut` | 三次缓动 |
| `EaseQuartIn/Out/InOut` | 四次缓动 |
| `EaseQuintIn/Out/InOut` | 五次缓动 |
| `EaseSineIn/Out/InOut` | 正弦缓动 |
| `EaseExponentialIn/Out/InOut` | 指数缓动 |
| `EaseCircleIn/Out/InOut` | 圆形缓动 |
| `EaseBackIn/Out/InOut` | 回震缓动 |
| `EaseElasticIn/Out/InOut` | 弹性缓动 |
| `EaseBounceIn/Out/InOut` | 弹跳缓动 |
### 缓动效果说明
- **In**: 开始时慢,逐渐加速
- **Out**: 开始时快,逐渐减速
- **InOut**: 开始和结束时慢,中间快
### 示例
```cpp
// 弹性缓动
auto jump = JumpBy::create(2.0f, Vec2(200, 0), 100, 1);
auto elasticJump = EaseElasticOut::create(jump, 0.5f);
sprite->runAction(elasticJump);
// 弹跳缓动
auto scale = ScaleTo::create(1.0f, 2.0f);
auto bounceScale = EaseBounceOut::create(scale);
sprite->runAction(bounceScale);
// 指数缓动
auto move = MoveTo::create(2.0f, Vec2(300, 200));
auto expoMove = EaseExponentialInOut::create(move);
sprite->runAction(expoMove);
```
### 自定义缓动
```cpp
// 使用自定义缓动函数
auto customEase = EaseCustom::create(move, [](float t) {
// 自定义缓动函数
return t * t * (3.0f - 2.0f * t); // smoothstep
});
```
---
## 特殊动作
### Speed - 速度控制
动态控制动作的播放速度:
```cpp
auto move = MoveTo::create(5.0f, Vec2(500, 300));
auto speed = Speed::create(move, 0.5f); // 半速播放
sprite->runAction(speed);
// 运行时调整速度
speed->setSpeed(2.0f); // 2 倍速
```
### Follow - 跟随动作
使节点跟随另一个节点移动(常用于相机):
```cpp
// 无边界跟随
auto follow = Follow::create(player);
// 带边界跟随
Rect boundary(0, 0, 2000, 2000); // 世界边界
auto followWithBoundary = Follow::create(player, boundary);
camera->runAction(follow);
```
### TargetedAction - 目标动作
在一个节点上运行动作,但作用于另一个节点:
```cpp
// 在 spriteA 上运行,但影响 spriteB
auto targeted = TargetedAction::create(spriteB, MoveTo::create(2.0f, Vec2(100, 100)));
spriteA->runAction(targeted);
```
---
## 瞬时动作
瞬时动作立即完成,没有动画过程。
### CallFunc - 回调动作
```cpp
// 无参数回调
auto callFunc = CallFunc::create([]() {
E2D_LOG_INFO("回调执行");
});
// 带节点参数的回调
auto callFuncN = CallFuncN::create([](Node* node) {
E2D_LOG_INFO("节点: %p", node);
});
```
### Place - 放置动作
```cpp
// 立即放置到指定位置
auto place = Place::create(Vec2(100, 100));
```
### FlipX/FlipY - 翻转动作
```cpp
auto flipX = FlipX::create(true); // 水平翻转
auto flipY = FlipY::create(true); // 垂直翻转
```
### Show/Hide - 显示/隐藏动作
```cpp
auto show = Show::create(); // 显示
auto hide = Hide::create(); // 隐藏
auto toggle = ToggleVisibility::create(); // 切换可见性
```
### RemoveSelf - 移除自身
```cpp
// 从父节点移除
auto removeSelf = RemoveSelf::create();
```
---
## 动作管理
### 停止动作
```cpp
// 停止所有动作
sprite->stopAllActions();
// 停止指定动作
sprite->stopAction(action);
// 根据标签停止动作
sprite->stopActionByTag(1);
// 根据标志位停止动作
sprite->stopActionsByFlags(0x01);
```
### 查询动作
```cpp
// 获取动作数量
size_t count = sprite->getActionCount();
// 检查是否有动作在运行
bool running = sprite->isRunningActions();
// 根据标签获取动作
Action* action = sprite->getActionByTag(1);
```
### 动作标签和标志位
```cpp
auto action = MoveTo::create(2.0f, Vec2(100, 100));
action->setTag(1); // 设置标签
action->setFlags(0x01); // 设置标志位
sprite->runAction(action);
```
---
## 完整示例
### 示例 1角色移动动画
```cpp
void Player::moveTo(const Vec2& target) {
// 停止当前移动
stopActionByTag(TAG_MOVE);
// 创建移动动作
auto move = MoveTo::create(1.0f, target);
move->setTag(TAG_MOVE);
// 添加缓动效果
auto easeMove = EaseQuadInOut::create(move);
runAction(easeMove);
}
```
### 示例 2UI 弹出动画
```cpp
void UIPanel::show() {
// 初始状态
setScale(0.0f);
setOpacity(0.0f);
setVisible(true);
// 并行动画:缩放 + 淡入
auto spawn = Spawn::create(
EaseBackOut::create(ScaleTo::create(0.3f, 1.0f)),
FadeIn::create(0.3f),
nullptr
);
runAction(spawn);
}
```
### 示例 3游戏结束动画
```cpp
void GameOverLayer::playAnimation() {
// 从屏幕底部滑入
setPosition(Vec2(screenWidth / 2, screenHeight));
auto moveUp = MoveBy::create(1.0f, Vec2(0, -screenHeight));
moveUp->setCompletionCallback([this]() {
// 动画完成后启用按钮
restartBtn_->setEnabled(true);
});
// 添加缓动效果
auto easeMove = EaseQuadOut::create(moveUp);
runAction(easeMove);
}
```
### 示例 4复杂序列动画
```cpp
void Enemy::playDeathAnimation() {
auto sequence = Sequence::create(
// 闪烁
Blink::create(0.5f, 3),
// 放大
ScaleTo::create(0.2f, 1.5f),
// 淡出
FadeOut::create(0.3f),
// 移除自身
RemoveSelf::create(),
nullptr
);
runAction(sequence);
}
```
### 示例 5循环动画
```cpp
void Coin::startFloating() {
// 上下浮动
auto floatUp = MoveBy::create(0.5f, Vec2(0, 10));
auto floatDown = floatUp->reverse();
auto floatSequence = Sequence::create(
EaseSineInOut::create(floatUp),
EaseSineInOut::create(floatDown),
nullptr
);
// 永久循环
auto floatForever = RepeatForever::create(floatSequence);
floatForever->setTag(TAG_FLOAT);
runAction(floatForever);
}
```
---
## 性能优化建议
1. **复用动作**:对于频繁使用的动作,可以 `clone()` 复用
2. **合理使用标签**:便于管理和停止特定动作
3. **避免过多并发动作**:大量节点同时运行动作会影响性能
4. **及时停止不需要的动作**:节点销毁前调用 `stopAllActions()`
---
## API 参考
### Action 基类
| 方法 | 说明 |
|------|------|
| `isDone()` | 检查动作是否完成 |
| `startWithTarget(node)` | 启动动作 |
| `stop()` | 停止动作 |
| `pause()` | 暂停动作 |
| `resume()` | 恢复动作 |
| `clone()` | 克隆动作 |
| `reverse()` | 创建反向动作 |
| `getTag()` / `setTag()` | 获取/设置标签 |
| `getFlags()` / `setFlags()` | 获取/设置标志位 |
| `setCompletionCallback()` | 设置完成回调 |
### Node 动作接口
| 方法 | 说明 |
|------|------|
| `runAction(action)` | 运行动作 |
| `stopAllActions()` | 停止所有动作 |
| `stopAction(action)` | 停止指定动作 |
| `stopActionByTag(tag)` | 根据标签停止动作 |
| `stopActionsByFlags(flags)` | 根据标志位停止动作 |
| `getActionByTag(tag)` | 根据标签获取动作 |
| `getActionCount()` | 获取动作数量 |
| `isRunningActions()` | 检查是否有动作运行 |

View File

@ -1,678 +0,0 @@
# Extra2D 动作系统重构规格文档
## 一、概述
本文档描述了 Extra2D 动作系统的重构方案,参考 Cocos2d-x 的动作系统设计模式,实现一个更加完善、易用、功能丰富的动作系统。
## 二、现有系统分析
### 2.1 当前类层次结构
```
Action (基类)
├── IntervalAction (有限时间动作基类)
│ ├── MoveBy/MoveTo
│ ├── ScaleBy/ScaleTo
│ ├── RotateBy/RotateTo
│ ├── FadeIn/FadeOut/FadeTo
│ ├── Sequence
│ ├── Spawn
│ ├── Delay
│ └── Loop
└── InstantAction (瞬时动作基类)
└── CallFunc
```
### 2.2 与 Cocos2d-x 的差异
| 特性 | Extra2D 当前 | Cocos2d-x | 差异说明 |
|------|-------------|-----------|---------|
| 类层次 | Action -> IntervalAction/InstantAction | Action -> FiniteTimeAction -> ActionInterval/ActionInstant | 缺少 FiniteTimeAction 中间层 |
| 动作管理 | Node 内部管理 | ActionManager 单例集中管理 | 缺少集中式管理 |
| 缓动动作 | EaseAction 静态方法 | ActionEase 类系(装饰器模式) | 不够灵活,无法组合 |
| 速度控制 | Action::speed_ 成员 | Speed 动作包装器 | 缺少动态速度控制 |
| 跟随动作 | 无 | Follow 类 | 缺失 |
| 跳跃动作 | 无 | JumpBy/JumpTo | 缺失 |
| 贝塞尔动作 | 无 | BezierBy/BezierTo | 缺失 |
| 闪烁动作 | 无 | Blink | 缺失 |
| 色调动作 | 无 | TintBy/TintTo | 缺失 |
| 重复动作 | Loop | Repeat/RepeatForever | 命名和功能差异 |
| 反向动作 | reverse() 方法 | reverse() 方法 | 相同 |
| 克隆动作 | clone() 方法 | clone() 方法 | 相同 |
## 三、重构目标
### 3.1 核心目标
1. **完善类层次结构** - 添加 FiniteTimeAction 中间层
2. **集中式动作管理** - 实现 ActionManager 单例
3. **装饰器模式缓动** - 实现 ActionEase 类系
4. **丰富动作类型** - 添加跳跃、贝塞尔、闪烁、色调等动作
5. **统一API风格** - 与 Cocos2d-x API 保持一致
### 3.2 设计原则
1. **组合优于继承** - 使用装饰器模式实现缓动和速度控制
2. **单一职责** - ActionManager 只负责动作调度
3. **开放封闭** - 易于扩展新动作类型
4. **接口隔离** - 不同类型动作提供不同接口
## 四、新类层次结构
```
Action (基类)
├── FiniteTimeAction (有限时间动作基类)
│ ├── ActionInterval (时间间隔动作)
│ │ ├── MoveBy/MoveTo
│ │ ├── MoveBy3D/MoveTo3D (新增)
│ │ ├── JumpBy/JumpTo (新增)
│ │ ├── BezierBy/BezierTo (新增)
│ │ ├── ScaleBy/ScaleTo
│ │ ├── RotateBy/RotateTo
│ │ ├── FadeIn/FadeOut/FadeTo
│ │ ├── Blink (新增)
│ │ ├── TintBy/TintTo (新增)
│ │ ├── Sequence
│ │ ├── Spawn
│ │ ├── DelayTime
│ │ ├── Repeat (重命名自 Loop)
│ │ ├── RepeatForever (新增)
│ │ ├── ReverseTime (新增)
│ │ └── ActionEase (缓动动作基类,新增)
│ │ ├── EaseIn/EaseOut/EaseInOut
│ │ ├── EaseSineIn/EaseSineOut/EaseSineInOut
│ │ ├── EaseExponentialIn/EaseExponentialOut/EaseExponentialInOut
│ │ ├── EaseBounceIn/EaseBounceOut/EaseBounceInOut
│ │ ├── EaseElasticIn/EaseElasticOut/EaseElasticInOut
│ │ ├── EaseBackIn/EaseBackOut/EaseBackInOut
│ │ └── EaseBezier (新增)
│ └── ActionInstant (瞬时动作)
│ ├── CallFunc
│ ├── CallFuncN (新增)
│ ├── Place (新增)
│ ├── FlipX/FlipY (新增)
│ ├── Show/Hide (新增)
│ ├── ToggleVisibility (新增)
│ └── RemoveSelf (新增)
├── Speed (速度控制动作,新增)
├── Follow (跟随动作,新增)
└── TargetedAction (目标动作,新增)
```
## 五、核心类设计
### 5.1 Action 基类
```cpp
class Action {
public:
virtual ~Action() = default;
// 核心接口
virtual bool isDone() const;
virtual void startWithTarget(Node* target);
virtual void stop();
virtual void step(float dt);
virtual void update(float time);
// 克隆和反向
virtual Action* clone() const = 0;
virtual Action* reverse() const = 0;
// 属性访问
Node* getTarget() const;
Node* getOriginalTarget() const;
int getTag() const;
void setTag(int tag);
unsigned int getFlags() const;
void setFlags(unsigned int flags);
protected:
Node* target_ = nullptr;
Node* originalTarget_ = nullptr;
int tag_ = -1;
unsigned int flags_ = 0;
};
```
### 5.2 FiniteTimeAction 中间层
```cpp
class FiniteTimeAction : public Action {
public:
float getDuration() const { return duration_; }
void setDuration(float duration) { duration_ = duration; }
virtual FiniteTimeAction* clone() const = 0;
virtual FiniteTimeAction* reverse() const = 0;
protected:
float duration_ = 0.0f;
};
```
### 5.3 ActionInterval 时间间隔动作
```cpp
class ActionInterval : public FiniteTimeAction {
public:
float getElapsed() const { return elapsed_; }
bool isDone() const override;
void startWithTarget(Node* target) override;
void step(float dt) override;
void setAmplitudeRate(float amp) { amplitudeRate_ = amp; }
float getAmplitudeRate() const { return amplitudeRate_; }
protected:
float elapsed_ = 0.0f;
bool firstTick_ = true;
float amplitudeRate_ = 1.0f;
EaseFunction easeFunc_ = nullptr; // 内置缓动函数
};
```
### 5.4 ActionInstant 瞬时动作
```cpp
class ActionInstant : public FiniteTimeAction {
public:
bool isDone() const override { return true; }
void step(float dt) override;
protected:
bool done_ = false;
};
```
### 5.5 ActionManager 动作管理器
```cpp
class ActionManager {
public:
static ActionManager* getInstance();
// 动作管理
void addAction(Action* action, Node* target, bool paused = false);
void removeAction(Action* action);
void removeActionByTag(int tag, Node* target);
void removeAllActionsFromTarget(Node* target);
void removeAllActions();
// 查询
Action* getActionByTag(int tag, Node* target);
size_t getActionCount(Node* target) const;
// 暂停/恢复
void pauseTarget(Node* target);
void resumeTarget(Node* target);
bool isPaused(Node* target) const;
// 更新(每帧调用)
void update(float dt);
private:
struct ActionElement {
std::vector<Action*> actions;
Node* target;
bool paused;
int actionIndex;
Action* currentAction;
bool currentActionSalvaged;
};
std::unordered_map<Node*, ActionElement> targets_;
static ActionManager* instance_;
};
```
### 5.6 ActionEase 缓动动作基类
```cpp
class ActionEase : public ActionInterval {
public:
static ActionEase* create(ActionInterval* action);
ActionInterval* getInnerAction() const { return innerAction_; }
void startWithTarget(Node* target) override;
void stop() override;
void update(float time) override;
Action* reverse() const override;
Action* clone() const override;
protected:
ActionInterval* innerAction_ = nullptr;
};
```
### 5.7 Speed 速度控制动作
```cpp
class Speed : public Action {
public:
static Speed* create(ActionInterval* action, float speed);
float getSpeed() const { return speed_; }
void setSpeed(float speed) { speed_ = speed; }
void startWithTarget(Node* target) override;
void stop() override;
void step(float dt) override;
bool isDone() const override;
Action* reverse() const override;
Action* clone() const override;
protected:
ActionInterval* innerAction_ = nullptr;
float speed_ = 1.0f;
};
```
## 六、新增动作类型
### 6.1 JumpBy/JumpTo 跳跃动作
```cpp
class JumpBy : public ActionInterval {
public:
static JumpBy* create(float duration, const Vec2& position,
float height, int jumps);
protected:
Vec2 startPosition_;
Vec2 delta_;
float height_;
int jumps_;
};
class JumpTo : public JumpBy {
// 继承实现,目标位置版本
};
```
### 6.2 BezierBy/BezierTo 贝塞尔动作
```cpp
struct BezierConfig {
Vec2 controlPoint1;
Vec2 controlPoint2;
Vec2 endPosition;
};
class BezierBy : public ActionInterval {
public:
static BezierBy* create(float duration, const BezierConfig& config);
protected:
BezierConfig config_;
Vec2 startPosition_;
};
```
### 6.3 Blink 闪烁动作
```cpp
class Blink : public ActionInterval {
public:
static Blink* create(float duration, int times);
protected:
int times_;
int currentTimes_;
bool originalVisible_;
};
```
### 6.4 TintBy/TintTo 色调动作
```cpp
class TintTo : public ActionInterval {
public:
static TintTo* create(float duration,
uint8_t red, uint8_t green, uint8_t blue);
protected:
Color3B startColor_;
Color3B endColor_;
Color3B deltaColor_;
};
```
### 6.5 Follow 跟随动作
```cpp
class Follow : public Action {
public:
static Follow* create(Node* followedNode,
const Rect& boundary = Rect::ZERO);
bool isDone() const override;
void step(float dt) override;
protected:
Node* followedNode_ = nullptr;
Rect boundary_;
bool boundarySet_ = false;
};
```
### 6.6 瞬时动作扩展
```cpp
// 放置到指定位置
class Place : public ActionInstant {
public:
static Place* create(const Vec2& position);
};
// X/Y轴翻转
class FlipX : public ActionInstant {
public:
static FlipX* create(bool flipX);
};
class FlipY : public ActionInstant {
public:
static FlipY* create(bool flipY);
};
// 显示/隐藏
class Show : public ActionInstant {
public:
static Show* create();
};
class Hide : public ActionInstant {
public:
static Hide* create();
};
// 切换可见性
class ToggleVisibility : public ActionInstant {
public:
static ToggleVisibility* create();
};
// 移除自身
class RemoveSelf : public ActionInstant {
public:
static RemoveSelf* create();
};
// 带Node参数的回调
class CallFuncN : public ActionInstant {
public:
static CallFuncN* create(const std::function<void(Node*)>& func);
};
```
## 七、缓动动作完整实现
### 7.1 通用缓动包装器
```cpp
// 通用缓动包装器
class EaseCustom : public ActionEase {
public:
static EaseCustom* create(ActionInterval* action,
EaseFunction easeFunc);
};
// 指数缓动
class EaseExponentialIn : public ActionEase { /* ... */ };
class EaseExponentialOut : public ActionEase { /* ... */ };
class EaseExponentialInOut : public ActionEase { /* ... */ };
// 正弦缓动
class EaseSineIn : public ActionEase { /* ... */ };
class EaseSineOut : public ActionEase { /* ... */ };
class EaseSineInOut : public ActionEase { /* ... */ };
// 弹性缓动
class EaseElasticIn : public ActionEase {
public:
static EaseElasticIn* create(ActionInterval* action, float period = 0.3f);
};
class EaseElasticOut : public ActionEase { /* ... */ };
class EaseElasticInOut : public ActionEase { /* ... */ };
// 弹跳缓动
class EaseBounceIn : public ActionEase { /* ... */ };
class EaseBounceOut : public ActionEase { /* ... */ };
class EaseBounceInOut : public ActionEase { /* ... */ };
// 回震缓动
class EaseBackIn : public ActionEase { /* ... */ };
class EaseBackOut : public ActionEase { /* ... */ };
class EaseBackInOut : public ActionEase { /* ... */ };
// 贝塞尔缓动(自定义曲线)
class EaseBezier : public ActionEase {
public:
static EaseBezier* create(ActionInterval* action);
void setBezierParamer(float p0, float p1, float p2, float p3);
};
```
## 八、Node 类接口更新
### 8.1 动作相关接口
```cpp
class Node {
public:
// 运行动作
Action* runAction(Action* action);
// 停止动作
void stopAllActions();
void stopAction(Action* action);
void stopActionByTag(int tag);
void stopActionsByFlags(unsigned int flags);
// 获取动作
Action* getActionByTag(int tag);
size_t getNumberOfRunningActions() const;
// 暂停/恢复所有动作
void pauseAllActions();
void resumeAllActions();
// 检查是否有动作在运行
bool isRunningActions() const;
};
```
## 九、使用示例
### 9.1 基本动作
```cpp
// 移动
auto moveTo = MoveTo::create(2.0f, Vec2(100, 100));
sprite->runAction(moveTo);
// 跳跃
auto jump = JumpBy::create(2.0f, Vec2(200, 0), 100, 3);
sprite->runAction(jump);
// 贝塞尔曲线
BezierConfig bezier;
bezier.controlPoint1 = Vec2(100, 200);
bezier.controlPoint2 = Vec2(200, 100);
bezier.endPosition = Vec2(300, 150);
auto bezierAction = BezierTo::create(3.0f, bezier);
sprite->runAction(bezierAction);
```
### 9.2 组合动作
```cpp
// 序列动作
auto seq = Sequence::create(
MoveTo::create(1.0f, Vec2(100, 100)),
DelayTime::create(0.5f),
FadeOut::create(1.0f),
CallFunc::create([](){ log("Done!"); }),
nullptr
);
sprite->runAction(seq);
// 并行动作
auto spawn = Spawn::create(
MoveTo::create(2.0f, Vec2(200, 200)),
RotateBy::create(2.0f, 360),
FadeIn::create(2.0f),
nullptr
);
sprite->runAction(spawn);
// 重复动作
auto repeat = Repeat::create(MoveBy::create(1.0f, Vec2(50, 0)), 5);
sprite->runAction(repeat);
// 永久重复
auto repeatForever = RepeatForever::create(
Sequence::create(
MoveBy::create(1.0f, Vec2(100, 0)),
MoveBy::create(1.0f, Vec2(-100, 0)),
nullptr
)
);
sprite->runAction(repeatForever);
```
### 9.3 缓动动作
```cpp
// 使用缓动包装器
auto move = MoveTo::create(3.0f, Vec2(500, 300));
auto easeMove = EaseElasticOut::create(move, 0.5f);
sprite->runAction(easeMove);
// 指数缓动
auto jump = JumpBy::create(2.0f, Vec2(200, 0), 100, 1);
auto easeJump = EaseExponentialOut::create(jump);
sprite->runAction(easeJump);
// 弹跳缓动
auto scale = ScaleTo::create(1.0f, 2.0f);
auto bounceScale = EaseBounceOut::create(scale);
sprite->runAction(bounceScale);
```
### 9.4 速度控制
```cpp
// 慢动作回放
auto action = MoveTo::create(5.0f, Vec2(500, 300));
auto speed = Speed::create(action, 0.5f); // 半速
sprite->runAction(speed);
// 动态调整速度
speed->setSpeed(2.0f); // 2倍速
```
### 9.5 跟随动作
```cpp
// 相机跟随玩家
auto follow = Follow::create(player, Rect(0, 0, 2000, 2000));
camera->runAction(follow);
```
## 十、文件结构
```
Extra2D/include/extra2d/action/
├── action.h # Action 基类
├── finite_time_action.h # FiniteTimeAction 中间层
├── action_interval.h # ActionInterval 及其子类
├── action_instant.h # ActionInstant 及其子类
├── action_ease.h # 缓动动作类系
├── action_manager.h # ActionManager
├── ease_functions.h # 缓动函数
└── action_factory.h # 动作工厂(可选)
Extra2D/src/action/
├── action.cpp
├── finite_time_action.cpp
├── action_interval.cpp
├── action_instant.cpp
├── action_ease.cpp
├── action_manager.cpp
└── ease_functions.cpp
```
## 十一、实现优先级
### 第一阶段(核心重构)
1. Action 基类重构
2. FiniteTimeAction 中间层
3. ActionInterval 重构
4. ActionInstant 重构
5. ActionManager 实现
### 第二阶段(新增动作)
1. JumpBy/JumpTo
2. BezierBy/BezierTo
3. Blink
4. TintBy/TintTo
5. Follow
6. Speed
### 第三阶段(缓动系统)
1. ActionEase 基类
2. 各类缓动包装器
3. EaseCustom 自定义缓动
### 第四阶段(瞬时动作扩展)
1. Place
2. FlipX/FlipY
3. Show/Hide
4. ToggleVisibility
5. RemoveSelf
6. CallFuncN
## 十二、兼容性考虑
### 12.1 API 变更
- 动作创建方式改为 `create()` 静态工厂方法
- `Loop` 重命名为 `Repeat`
- `Delay` 重命名为 `DelayTime`
- 动作管理由 `ActionManager` 集中处理
### 12.2 迁移指南
```cpp
// 旧写法
auto action = std::make_shared<MoveTo>(2.0f, Vec2(100, 100));
sprite->runAction(action);
// 新写法(推荐)
auto action = MoveTo::create(2.0f, Vec2(100, 100));
sprite->runAction(action);
// 旧写法(缓动)
auto action = EaseAction::create(MoveTo::create(2.0f, Vec2(100, 100)), easeInQuad);
// 新写法(缓动)
auto action = EaseQuadIn::create(MoveTo::create(2.0f, Vec2(100, 100)));
```
## 十三、性能优化
1. **对象池集成** - 动作对象使用对象池管理
2. **批量更新** - ActionManager 统一调度减少调用开销
3. **延迟删除** - 动作完成时标记删除,统一清理
4. **缓存友好** - 连续存储同一类型动作
## 十四、测试计划
1. 单元测试:每个动作类型的独立测试
2. 集成测试:组合动作测试
3. 性能测试:大量动作并发测试
4. 内存测试:动作对象生命周期测试

View File

@ -1,218 +0,0 @@
# Extra2D 构建系统文档
本文档详细介绍 Extra2D 引擎的构建系统配置和使用方法。
## 构建工具
Extra2D 使用 [xmake](https://xmake.io/) 作为构建工具,支持多平台构建。
### 安装 xmake
```bash
# Windows (使用 PowerShell)
Invoke-Expression (Invoke-WebRequest 'https://xmake.io/psget.text' -UseBasicParsing).Content
# macOS
brew install xmake
# Linux
sudo add-apt-repository ppa:xmake-io/xmake
sudo apt update
sudo apt install xmake
```
## 平台支持
| 平台 | 目标 | 说明 |
|------|------|------|
| 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 -t extra2d # 引擎库
xmake -t hello_world # Hello World 示例
xmake -t push_box # 推箱子游戏
xmake -t collision_demo # 碰撞检测演示
xmake -t spatial_index_demo # 空间索引演示
# 构建示例组
xmake -g examples
```
## 项目结构
```
Extra2D/
├── xmake.lua # 主构建配置
├── xmake/
│ ├── engine.lua # 引擎构建规则
│ └── toolchains/ # 工具链定义
├── Extra2D/ # 引擎源码
│ ├── include/ # 头文件
│ └── src/ # 源文件
└── examples/ # 示例程序
├── hello_world/
├── push_box/
├── collision_demo/
└── spatial_index_demo/
```
## 添加新示例
创建新的示例程序:
1. 在 `examples/` 下创建目录
2. 添加 `main.cpp``xmake.lua`
### 示例 xmake.lua
```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
```
## 常见问题
### 依赖安装失败
```bash
# 强制重新安装依赖
xmake require -f -y
# 清理构建缓存
xmake clean
xmake f -c
```
### Switch 构建失败
确保已安装 devkitPro
```bash
# 安装 Switch 开发工具链
pacman -S switch-dev switch-portlibs
# 设置环境变量
$env:DEVKITPRO = "C:\devkitPro"
$env:DEVKITA64 = "C:\devkitPro\devkitA64"
```
### 运行时找不到资源
确保资源文件已正确配置:
```lua
-- 在 xmake.lua 中添加资源
add_files("romfs/**", {install = true})
```
## 高级配置
### 自定义编译选项
```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/)