diff --git a/docs/API_Tutorial/01_Quick_Start.md b/docs/API_Tutorial/01_Quick_Start.md deleted file mode 100644 index 071b9a7..0000000 --- a/docs/API_Tutorial/01_Quick_Start.md +++ /dev/null @@ -1,215 +0,0 @@ -# 01. 快速开始 - -本教程将带你快速上手 Extra2D 引擎,通过一个简单的 Hello World 示例了解引擎的基本使用方法。 - -## 示例代码 - -完整示例位于 `examples/hello_world/main.cpp`: - -```cpp -#include - -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 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()); - - // 运行应用 - 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) - 学习节点和精灵的使用 diff --git a/docs/API_Tutorial/02_Scene_System.md b/docs/API_Tutorial/02_Scene_System.md deleted file mode 100644 index 02369d8..0000000 --- a/docs/API_Tutorial/02_Scene_System.md +++ /dev/null @@ -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 - -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(app.window().getWidth()); - float windowHeight = static_cast(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(); - 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(app.window().getWidth()); - float currentWindowHeight = static_cast(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(viewportOffsetX_), static_cast(viewportOffsetY_), - static_cast(scaledGameWidth_), static_cast(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->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 - -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()); - -// 替换当前场景(无过渡) -scenes.replaceScene(makePtr()); - -// 替换当前场景(有过渡效果) -scenes.replaceScene(makePtr(), TransitionType::Fade, 0.5f); - -// 推入场景(保留当前场景) -scenes.pushScene(makePtr()); -scenes.pushScene(makePtr(), 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) - ↓ -进入场景 (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(), - TransitionType::Fade, 0.25f); - break; - case 1: - // 打开设置 - break; - case 2: - Application::instance().quit(); - break; - } - } - - Ptr font_; - std::vector> 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) - 深入了解资源加载 diff --git a/docs/API_Tutorial/03_Node_System.md b/docs/API_Tutorial/03_Node_System.md deleted file mode 100644 index 7c33dbf..0000000 --- a/docs/API_Tutorial/03_Node_System.md +++ /dev/null @@ -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(); -scoreNumber->setPosition(Vec2(95.0f, 10.0f)); // 相对于 panel 中心 -panel->addChild(scoreNumber); - -// 当 panel 移动时,scoreNumber 会自动跟随 -// 因为 scoreNumber 的世界变换包含了 panel 的世界变换 -``` - -### 常见错误 - -**错误1:混淆本地坐标和世界坐标** -```cpp -// ❌ 错误:在世界坐标系中设置位置,但节点是子节点 -child->setPosition(worldPos); // 这会被解释为本地坐标 - -// ✅ 正确:使用 convertToNodeSpace 转换 -child->setPosition(parent->convertToNodeSpace(worldPos)); -``` - -**错误2:锚点设置不当** -```cpp -// ❌ 错误:锚点 (0,0) 但期望中心对齐 -sprite->setAnchor(Vec2(0.0f, 0.0f)); -sprite->setPosition(Vec2(100, 100)); // 左上角在 (100, 100) - -// ✅ 正确:使用中心锚点 -sprite->setAnchor(Vec2(0.5f, 0.5f)); -sprite->setPosition(Vec2(100, 100)); // 中心在 (100, 100) -``` - -**错误3:忽略父节点变换** -```cpp -// ❌ 错误:直接设置世界坐标,忽略父节点 -child->setPosition(Vec2(100, 100)); // 这是本地坐标! - -// ✅ 正确:考虑父节点的世界变换 -// 如果父节点在 (50, 50),子节点相对于父节点应该是 (50, 50) -child->setPosition(Vec2(50, 50)); -``` - -## 精灵(Sprite) - -### 创建精灵 - -```cpp -auto& resources = Application::instance().resources(); - -// 从纹理创建 -auto texture = resources.loadTexture("assets/player.png"); -auto sprite = Sprite::create(texture); - -// 设置精灵属性 -sprite->setPosition(Vec2(400, 300)); -sprite->setAnchor(Vec2(0.5f, 0.5f)); - -// 切换纹理 -auto newTexture = resources.loadTexture("assets/player2.png"); -sprite->setTexture(newTexture); -``` - -### 从图集创建精灵(Texture Atlas) - -```cpp -// 加载图集纹理 -auto atlasTexture = resources.loadTexture("assets/atlas.png"); - -// 创建精灵,指定图集中的矩形区域 -Rect spriteRect(100, 100, 32, 32); // x, y, width, height -auto sprite = Sprite::create(atlasTexture, spriteRect); -``` - -### 精灵渲染流程 - -```cpp -void Sprite::onDraw(RenderBackend& renderer) { - // 1. 获取世界变换矩阵 - auto worldTransform = getWorldTransform(); - - // 2. 从世界变换中提取位置和缩放 - float worldX = worldTransform[3][0]; - float worldY = worldTransform[3][1]; - float worldScaleX = glm::length(glm::vec2(worldTransform[0][0], worldTransform[0][1])); - float worldScaleY = glm::length(glm::vec2(worldTransform[1][0], worldTransform[1][1])); - - // 3. 计算目标矩形(不包含锚点偏移) - Rect destRect(worldX, worldY, width * worldScaleX, height * worldScaleY); - - // 4. 提取旋转角度 - float worldRotation = std::atan2(worldTransform[0][1], worldTransform[0][0]); - - // 5. 绘制精灵(锚点偏移在 GLSpriteBatch 中处理) - renderer.drawSprite(*texture_, destRect, srcRect, color_, worldRotation, anchor); -} -``` - -## 按钮(Button) - -### 创建按钮 - -```cpp -auto button = Button::create(); -button->setFont(font); -button->setText("点击我"); -button->setPosition(Vec2(400, 300)); -button->setCustomSize(200.0f, 60.0f); - -// 设置颜色 -button->setTextColor(Colors::White); -button->setBackgroundColor( - Colors::Blue, // 正常状态 - Colors::Green, // 悬停状态 - Colors::Red // 按下状态 -); - -// 设置点击回调 -button->setOnClick([]() { - E2D_LOG_INFO("按钮被点击!"); -}); - -addChild(button); -``` - -### 使用图片按钮 - -```cpp -// 从图集创建按钮 -auto buttonFrame = ResLoader::getKeyFrame("button_play"); -if (buttonFrame) { - auto button = Button::create(); - button->setBackgroundImage( - buttonFrame->getTexture(), - buttonFrame->getRect() // 使用图集中的矩形区域 - ); - button->setPosition(Vec2(screenWidth / 2, 300)); - button->setOnClick([]() { - // 处理点击 - }); - addChild(button); -} -``` - -### 按钮坐标空间 - -**按钮使用世界坐标空间(World Space)**: - -```cpp -// Button::getBoundingBox() 返回世界坐标系的包围盒 -Rect Button::getBoundingBox() const { - auto pos = getRenderPosition(); // 获取世界坐标位置 - // ... 计算包围盒 - return Rect(x0, y0, w, h); // 世界坐标 -} -``` - -**这意味着**: -- 按钮的位置是相对于父节点的本地坐标 -- 但按钮的点击检测使用世界坐标 -- 父节点的变换会自动应用到按钮 - -### 按钮事件处理 - -```cpp -// 在 onUpdate 中检测手柄按键 -void GameOverLayer::onUpdate(float dt) { - Node::onUpdate(dt); - - auto& input = Application::instance().input(); - - // A 键触发重新开始按钮 - if (input.isButtonPressed(GamepadButton::A)) { - restartBtn_->getEventDispatcher().dispatch(EventType::UIClicked); - } -} -``` - -## 节点层级管理 - -### 父子关系 - -```cpp -// 添加子节点 -parent->addChild(child); - -// 移除子节点 -parent->removeChild(child); - -// 移除所有子节点 -parent->removeAllChildren(); - -// 获取父节点 -Node* parent = child->getParent(); - -// 获取子节点列表 -const auto& children = parent->getChildren(); -``` - -### Z轴顺序 - -```cpp -// 设置Z轴顺序(值越大越在上层) -node->setZOrder(10); - -// 重新排序子节点 -parent->reorderChild(child, newZOrder); -``` - -## 自定义节点 - -### 继承 Node 创建自定义节点 - -```cpp -class Player : public Node { -public: - static Ptr create(Ptr texture) { - auto player = makePtr(); - if (player->init(texture)) { - return player; - } - return nullptr; - } - - bool init(Ptr 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_; - 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(app.getConfig().width); - float screenHeight = static_cast(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( - 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(); - 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(); - 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> 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(child.get())) { - // 处理 sprite - } - } - } - }; - ``` - -## 动画系统 - -### 动作类型 - -Extra2D 提供了丰富的动作类: - -| 动作类 | 说明 | -|--------|------| -| `MoveTo` | 移动到指定位置 | -| `MoveBy` | 移动指定偏移量 | -| `ScaleTo` | 缩放到指定比例 | -| `ScaleBy` | 缩放指定比例 | -| `RotateTo` | 旋转到指定角度 | -| `RotateBy` | 旋转指定角度 | -| `FadeIn` | 淡入 | -| `FadeOut` | 淡出 | -| `FadeTo` | 淡化到指定透明度 | -| `Delay` | 延迟 | -| `Sequence` | 顺序执行多个动作 | -| `Spawn` | 同时执行多个动作 | -| `Loop` | 循环执行动作 | -| `CallFunc` | 回调函数 | - -### 运行动作 - -```cpp -// 移动动画 -auto moveAction = makePtr(1.0f, Vec2(0.0f, -100.0f)); -node->runAction(moveAction); - -// 缩放动画 -auto scaleAction = makePtr(0.5f, 2.0f); -node->runAction(scaleAction); - -// 淡入淡出 -auto fadeOut = makePtr(0.3f); -auto fadeIn = makePtr(0.3f); -node->runAction(fadeOut); -``` - -### 动画完成回调 - -使用 `setCompletionCallback` 在动画完成时执行回调: - -```cpp -// 参考 examples/flappy_bird/GameOverLayer.cpp -void GameOverLayer::onEnter() { - Node::onEnter(); - - // 创建向上移动的动画 - auto moveAction = extra2d::makePtr( - 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({ - new MoveTo(1.0f, Vec2(100, 100)), - new ScaleTo(0.5f, 2.0f), - new FadeOut(0.3f) -}); -node->runAction(sequence); - -// 并行执行:同时移动和旋转 -auto spawn = makePtr({ - new MoveTo(1.0f, Vec2(100, 100)), - new RotateBy(1.0f, 360.0f) -}); -node->runAction(spawn); - -// 循环执行 -auto loop = makePtrrunAction(loop); -``` - -### 动画进度回调 - -```cpp -auto action = makePtr(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) - 学习碰撞检测系统 diff --git a/docs/API_Tutorial/04_Resource_Management.md b/docs/API_Tutorial/04_Resource_Management.md deleted file mode 100644 index f614dfa..0000000 --- a/docs/API_Tutorial/04_Resource_Management.md +++ /dev/null @@ -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, 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 -#include -#include - -namespace flappybird { - -/** - * @brief 音频类型枚举 - */ -enum class MusicType { - Click, // 按键声音 - Hit, // 小鸟死亡声音 - Fly, // 小鸟飞翔声音 - Point, // 得分声音 - Swoosh // 转场声音 -}; - -/** - * @brief 资源加载器类 - * 管理纹理图集、精灵帧和音频资源的加载 - */ -class ResLoader { -public: - static void init(); - - static extra2d::Ptr getKeyFrame(const std::string& name); - - static void playMusic(MusicType type); - -private: - struct ImageInfo { - float width, height, x, y; - }; - - static extra2d::Ptr atlasTexture_; - static std::map imageMap_; - static std::map> soundMap_; -}; - -} // namespace flappybird -``` - -```cpp -// ResLoader.cpp -#include "ResLoader.h" -#include - -namespace flappybird { - -extra2d::Ptr ResLoader::atlasTexture_; -std::map ResLoader::imageMap_; -std::map> 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 -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( - 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 - -// 方式1:直接使用对象池 -extra2d::ObjectPool pool; -MyObject* obj = pool.allocate(); -pool.deallocate(obj); - -// 方式2:使用智能指针自动管理(推荐) -auto obj = E2D_MAKE_POOLED(MyObject, arg1, arg2); -// 离开作用域自动回收 - -// 方式3:使用 PooledAllocator -extra2d::PooledAllocator 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> 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(); -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 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) - 学习碰撞检测系统 diff --git a/docs/API_Tutorial/05_Input_Handling.md b/docs/API_Tutorial/05_Input_Handling.md deleted file mode 100644 index b687646..0000000 --- a/docs/API_Tutorial/05_Input_Handling.md +++ /dev/null @@ -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(), - 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(), - 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(), - 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(), - 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 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 控件使用 diff --git a/docs/API_Tutorial/06_Collision_Detection.md b/docs/API_Tutorial/06_Collision_Detection.md deleted file mode 100644 index 9347d26..0000000 --- a/docs/API_Tutorial/06_Collision_Detection.md +++ /dev/null @@ -1,222 +0,0 @@ -# 06. 碰撞检测 - -Extra2D 提供了基于空间索引的高效碰撞检测系统,支持四叉树和空间哈希两种策略。 - -## 完整示例 - -参考示例代码: -- `examples/collision_demo/main.cpp` - 基础碰撞检测演示 -- `examples/spatial_index_demo/main.cpp` - 空间索引性能演示 - -## 启用碰撞检测 - -### 1. 创建可碰撞节点 - -```cpp -class CollidableBox : public Node { -public: - CollidableBox(float width, float height, const Color& color) - : width_(width), height_(height), color_(color), isColliding_(false) { - // 启用空间索引 - 这是关键! - setSpatialIndexed(true); - } - - // 必须实现 getBoundingBox 方法 - Rect getBoundingBox() const override { - Vec2 pos = getPosition(); - return Rect(pos.x - width_ / 2, pos.y - height_ / 2, width_, height_); - } - - void setColliding(bool colliding) { isColliding_ = colliding; } - - void onRender(RenderBackend& renderer) override { - Vec2 pos = getPosition(); - - // 碰撞时变红色 - Color fillColor = isColliding_ ? Color(1.0f, 0.2f, 0.2f, 0.8f) : color_; - renderer.fillRect( - Rect(pos.x - width_ / 2, pos.y - height_ / 2, width_, height_), - fillColor - ); - - // 绘制边框 - Color borderColor = isColliding_ ? Color(1.0f, 0.0f, 0.0f, 1.0f) - : Color(1.0f, 1.0f, 1.0f, 0.5f); - renderer.drawRect( - Rect(pos.x - width_ / 2, pos.y - height_ / 2, width_, height_), - borderColor, 2.0f - ); - } - -private: - float width_, height_; - Color color_; - bool isColliding_; -}; -``` - -### 2. 执行碰撞检测 - -```cpp -class GameScene : public Scene { -public: - void onUpdate(float dt) override { - Scene::onUpdate(dt); - - // 清除之前的碰撞状态 - for (const auto& child : getChildren()) { - if (auto box = dynamic_cast(child.get())) { - box->setColliding(false); - } - } - - // 使用场景的空间索引查询所有碰撞 - auto collisions = queryCollisions(); - - // 处理碰撞 - for (const auto& [nodeA, nodeB] : collisions) { - if (auto boxA = dynamic_cast(nodeA)) { - boxA->setColliding(true); - } - if (auto boxB = dynamic_cast(nodeB)) { - boxB->setColliding(true); - } - } - } - - void createBox(float x, float y) { - auto box = makePtr(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(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(child.get())) { - node->setColliding(false); - } - } - - // 使用引擎自带的空间索引进行碰撞检测 - auto collisions = queryCollisions(); - - // 标记碰撞的节点 - for (const auto& [nodeA, nodeB] : collisions) { - if (auto boxA = dynamic_cast(nodeA)) { - boxA->setColliding(true); - } - if (auto boxB = dynamic_cast(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(child.get())) { - ++count; - } - } - return count; - } -}; -``` - -**关键改进**: -- 使用 `getChildren()` 代替私有 vector 存储节点引用 -- 通过 `dynamic_cast` 筛选特定类型的子节点 -- 避免双重引用,简化生命周期管理 - -## 关键要点 - -1. **必须调用 `setSpatialIndexed(true)`** - 启用节点的空间索引 -2. **必须实现 `getBoundingBox()`** - 返回准确的边界框 -3. **在 `onEnter()` 中调用 `Scene::onEnter()`** - 确保节点正确注册到空间索引 -4. **使用 `queryCollisions()`** - 自动利用空间索引优化检测 - -## 下一步 - -- [07. UI 系统](./07_UI_System.md) - 学习 UI 控件使用 -- [08. 音频系统](./08_Audio_System.md) - 学习音频播放 diff --git a/docs/API_Tutorial/07_UI_System.md b/docs/API_Tutorial/07_UI_System.md deleted file mode 100644 index db65b4c..0000000 --- a/docs/API_Tutorial/07_UI_System.md +++ /dev/null @@ -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 restartBtn_; - extra2d::Ptr menuBtn_; -}; - -// GameOverLayer.cpp -void GameOverLayer::onEnter() { - Node::onEnter(); - - // 初始化按钮(初始禁用) - initButtons(); - - // 创建动画 - auto moveAction = extra2d::makePtr( - 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(); -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 font_; - Ptr