chore: 删除过时文档文件
移除不再使用的Extra2D构建系统文档和API教程文件,包括快速开始、场景系统、节点系统、输入处理、碰撞检测、音频系统和动作系统等过时内容
This commit is contained in:
parent
ea5ecd383f
commit
bd3cebf0b5
|
|
@ -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) - 学习节点和精灵的使用
|
||||
|
|
@ -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) - 深入了解资源加载
|
||||
|
|
@ -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::boundingBox() 返回世界坐标系的包围盒
|
||||
Rect Button::boundingBox() 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) - 学习碰撞检测系统
|
||||
|
|
@ -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 <nlohmann/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) - 学习碰撞检测系统
|
||||
|
|
@ -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 控件使用
|
||||
|
|
@ -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 boundingBox() 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. **必须实现 `boundingBox()`** - 返回准确的边界框
|
||||
3. **在 `onEnter()` 中调用 `Scene::onEnter()`** - 确保节点正确注册到空间索引
|
||||
4. **使用 `queryCollisions()`** - 自动利用空间索引优化检测
|
||||
|
||||
## 下一步
|
||||
|
||||
- [07. UI 系统](./07_UI_System.md) - 学习 UI 控件使用
|
||||
- [08. 音频系统](./08_Audio_System.md) - 学习音频播放
|
||||
|
|
@ -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) - 学习音频播放
|
||||
|
|
@ -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) - 音频播放
|
||||
|
||||
开始你的游戏开发之旅吧!
|
||||
|
|
@ -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);
|
||||
}
|
||||
```
|
||||
|
||||
### 示例 2:UI 弹出动画
|
||||
|
||||
```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()` | 检查是否有动作运行 |
|
||||
|
|
@ -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. 内存测试:动作对象生命周期测试
|
||||
|
|
@ -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/)
|
||||
Loading…
Reference in New Issue