diff --git a/README.md b/README.md index c3285b0..5c0b393 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ 高性能、易用、原生支持 Switch 平台

-[📖 构建指南](./SWITCH_BUILD_GUIDE.md) | [🚀 快速开始](#快速开始) | [📦 项目结构](#项目结构) | [💬 问题反馈](https://github.com/ChestnutYueyue/extra2d/issues) +[📖 构建指南](./docs/Extra2D%20构建系统文档.md) | [🚀 快速开始](#快速开始) | [📦 示例程序](#示例程序) | [📚 API 教程](./docs/API_Tutorial/01_Quick_Start.md) @@ -40,68 +40,12 @@ ### ✨ 核心特性 - **🎯 Switch 原生支持**:专为 Nintendo Switch 硬件优化,支持掌机/主机双模式 -- **🎬 高级动画系统**:支持骨骼动画、精灵动画、补间动画,提供 ALS 动画格式支持 -- **📜 脚本系统**:集成 Squirrel 脚本引擎,支持热更新和快速迭代开发 -- **🎵 音频系统**:基于 SDL2 的高质量音频播放,支持 BGM 和音效 -- **🎨 特效系统**:粒子系统、后处理效果、自定义着色器支持 +- **🎬 高级动画系统**:支持骨骼动画、精灵动画、补间动画 +- **🎵 音频系统**:基于 SDL2_mixer 的高质量音频播放,支持 BGM 和音效 +- **🎨 渲染系统**:基于 OpenGL ES 的 2D 渲染,支持自定义着色器 - **💾 数据持久化**:游戏存档、配置文件的便捷读写 - ---- - -## 🗺️ 架构概览 - -```mermaid -mindmap - root((Extra2D 引擎架构)) - 核心系统 - 应用管理 Application - 渲染后端 RenderBackend - 窗口管理 Window - 输入处理 Input - 音频引擎 AudioEngine - 资源管理 ResourceManager - 事件系统 EventDispatcher - 场景管理 - 场景 Scene - 场景管理器 SceneManager - 过渡动画 Transition - 空间索引 SpatialManager - 节点系统 - 基础节点 Node - 精灵 Sprite - 文本 Text - 形状 ShapeNode - 摄像机 Camera - 动画节点 AnimationNode - 动画系统 - 动作系统 Action - 精灵动画 AnimatedSprite - 骨骼动画支持 - 动画缓存 AnimationCache - 动画事件 AnimationEvent - 脚本系统 - Squirrel 脚本引擎 - 脚本节点 ScriptNode - 完整 API 绑定 - 特效系统 - 粒子系统 ParticleSystem - 后处理 PostProcess - 自定义效果管理器 - UI 系统 - 基础控件 Widget - 按钮 Button - 工具库 - 音频播放 Sound - 数据持久化 Data - 随机数 Random - 定时器 Timer - 字体 FontAtlas - 数学库 - 向量 Vec2/Vec3 - 矩形 Rect - 大小 Size - 颜色 Color -``` +- **🔧 空间索引**:内置四叉树和空间哈希碰撞检测系统 +- **🖱️ UI 系统**:完整的 UI 控件支持(按钮、文本、滑块等) --- @@ -116,319 +60,24 @@ mindmap | 构建工具 | xmake | | 目标平台 | Nintendo Switch / Windows (MinGW) | -### 安装 devkitPro +### 安装 xmake ```bash -# Windows (以管理员身份运行 PowerShell) -Invoke-WebRequest -Uri "https://github.com/devkitPro/pacman/releases/latest/download/devkitpro-pacman.amd64.exe" -OutFile "devkitpro-pacman.exe" -.\devkitpro-pacman.exe +# Windows (PowerShell) +Invoke-Expression (Invoke-WebRequest 'https://xmake.io/psget.text' -UseBasicParsing).Content -# 安装 Switch 开发工具链 -pacman -S switch-dev switch-portlibs +# macOS +brew install xmake + +# Linux +sudo add-apt-repository ppa:xmake-io/xmake +sudo apt update +sudo apt install xmake ``` -### 构建项目 +## 📚 文档 -#### Switch 平台 - -```bash -# 克隆仓库 -git clone https://github.com/ChestnutYueyue/extra2d.git -cd extra2d - -# 配置 Switch 平台构建 -xmake f -p switch --mode=release - -# 构建引擎 -xmake - -# 构建示例游戏 -xmake -g examples -``` - -#### Windows (MinGW) 平台 - -```bash -# 克隆仓库 -git clone https://github.com/ChestnutYueyue/extra2d.git -cd extra2d - -# 配置 MinGW 平台构建 -xmake f -p mingw --mode=release - -# 安装依赖 -xmake require -y - -# 构建引擎和示例 -xmake - -# 运行示例 -xmake run hello_world -``` - -### 生成 NSP 可运行文件 - -```bash -# 打包推箱子游戏示例 -xmake package push_box - -# 生成的文件位于 -# build/switch/release/push_box/push_box.nsp -``` - ---- - -## 📝 Hello World 示例 - -```cpp -#include - -using namespace extra2d; - -int main() -{ - // 初始化日志 - Logger::init(); - Logger::setLevel(LogLevel::Info); - - // 配置应用 - AppConfig config; - config.title = "Hello Extra2D"; - config.width = 1280; - config.height = 720; - config.vsync = true; - - // 初始化应用 - auto& app = Application::instance(); - if (!app.init(config)) { - Logger::shutdown(); - return -1; - } - - // 创建场景 - auto scene = makePtr(); - scene->setBackgroundColor(Color(0.1f, 0.1f, 0.15f, 1.0f)); - - // 创建文本节点 - auto text = Text::create("Hello, Extra2D on Switch!"); - text->setPosition(Vec2(640, 360)); - text->setAnchor(Vec2(0.5f, 0.5f)); - text->setTextColor(Color(1.0f, 0.5f, 0.2f, 1.0f)); - text->setFontSize(48); - - // 添加动画效果 - text->runAction(makePtr( - makePtr(std::vector>{ - makePtr(1.0f, Vec2(1.5f, 1.5f)), - makePtr(1.0f, Vec2(1.0f, 1.0f)) - }) - )); - - // 添加到场景 - scene->addChild(text); - - // 进入场景 - app.enterScene(scene); - - // 运行主循环 - app.run(); - - // 清理 - app.shutdown(); - Logger::shutdown(); - return 0; -} -``` - ---- - -## 🏗️ 项目结构 - -``` -Extra2D/ -├── 📁 Extra2D/ # 引擎核心代码 -│ ├── 📁 include/ # 头文件 -│ │ ├── 📁 extra2d/ # 引擎头文件 -│ │ │ ├── extra2d.h # 主头文件 -│ │ │ ├── app/ # 应用管理 -│ │ │ ├── action/ # 动作系统 -│ │ │ ├── animation/ # 动画系统 -│ │ │ ├── audio/ # 音频系统 -│ │ │ ├── core/ # 核心类型 -│ │ │ ├── effects/ # 特效系统 -│ │ │ ├── event/ # 事件系统 -│ │ │ ├── graphics/ # 图形渲染 -│ │ │ ├── platform/ # 平台抽象 -│ │ │ ├── resource/ # 资源管理 -│ │ │ ├── scene/ # 场景系统 -│ │ │ ├── script/ # 脚本系统 -│ │ │ ├── spatial/ # 空间索引 -│ │ │ ├── ui/ # UI 系统 -│ │ │ └── utils/ # 工具库 -│ │ ├── 📁 glad/ # OpenGL Loader -│ │ ├── 📁 json/ # JSON 库 -│ │ ├── 📁 simpleini/ # INI 配置文件库 -│ │ └── 📁 stb/ # STB 图像库 -│ └── 📁 src/ # 源文件 -│ ├── action/ # 动作系统实现 -│ ├── animation/ # 动画系统实现 -│ ├── app/ # 应用管理实现 -│ ├── audio/ # 音频系统实现 -│ ├── core/ # 核心类型实现 -│ ├── effects/ # 特效系统实现 -│ ├── event/ # 事件系统实现 -│ ├── glad/ # GLAD 实现 -│ ├── graphics/ # 图形渲染实现 -│ ├── platform/ # 平台抽象实现 -│ ├── resource/ # 资源管理实现 -│ ├── scene/ # 场景系统实现 -│ ├── script/ # 脚本系统实现 -│ ├── spatial/ # 空间索引实现 -│ ├── ui/ # UI 系统实现 -│ └── utils/ # 工具库实现 -├── 📁 docs/ # 文档 -│ ├── 📁 API_Tutorial/ # API 教程 -│ └── Extra2D 构建系统文档.md # 构建系统文档 -├── 📁 examples/ # 示例程序 -│ ├── hello_world/ # Hello World 示例 -│ ├── collision_demo/ # 碰撞检测示例 -│ ├── push_box/ # 推箱子游戏 -│ └── spatial_index_demo/ # 空间索引示例 -├── 📁 logo/ # Logo 资源 -├── 📁 squirrel/ # Squirrel 脚本引擎 -├── � xmake/ # Xmake 构建配置 -│ └── toolchains/ # 工具链定义 -├── 📄 xmake.lua # 主构建配置 -├── 📄 LICENSE # MIT 许可证 -└── 📄 README.md # 本文件 -``` - ---- - -## 🎮 Switch 特定功能 - -### 双模式支持 - -```cpp -// 检测当前模式 -if (app.isDocked()) { - // 主机模式:可使用更高分辨率 - config.width = 1920; - config.height = 1080; -} else { - // 掌机模式 - config.width = 1280; - config.height = 720; -} -``` - -### 控制器输入 - -```cpp -auto& input = app.input(); - -// Joy-Con 支持 -if (input.isKeyDown(KeyCode::ButtonA)) { - // A 键按下 -} - -if (input.isKeyDown(KeyCode::ButtonLeft)) { - // 左摇杆向左 -} -``` - -### ROMFS 资源加载 - -```cpp -// 自动从 ROMFS 加载资源 -auto texture = resources.loadTexture("romfs:/images/player.png"); -auto sound = audio.loadSound("romfs:/audio/jump.wav"); -``` - ---- - -## 📋 API 速查 - -### 应用控制 - -```cpp -auto& app = Application::instance(); -app.init(config); -app.run(); -app.quit(); -``` - -### 场景管理 - -```cpp -auto scene = makePtr(); -app.enterScene(scene); -app.enterScene(scene, makePtr(1.0f)); -``` - -### 节点操作 - -```cpp -auto sprite = Sprite::create(texture); -sprite->setPosition(Vec2(100, 200)); -sprite->setRotation(45.0f); -sprite->runAction(makePtr(1.0f, Vec2(200, 300))); -``` - -### 动画系统 - -```cpp -// 精灵动画 -auto anim = AnimatedSprite::createFromGrid( - "player.png", 96, 96, 125.0f, 16); -anim->setFrameRange(0, 3); -anim->play(); - -// 动作动画 -node->runAction(makePtr( - makePtr(1.0f, Vec2(100, 200)), - makePtr(0.5f, Vec2(2.0f, 2.0f)) -)); -``` - -### 输入处理 - -```cpp -auto& input = app.input(); -if (input.isKeyDown(KeyCode::ButtonA)) {} -if (input.isKeyPressed(KeyCode::ButtonB)) {} -auto pos = input.getMousePosition(); -``` - -### 音频播放 - -```cpp -auto& audio = app.audio(); -auto sound = audio.loadSound("jump.wav"); -sound->play(); -sound->setVolume(0.8f); -``` - ---- - -## 🛠️ 技术栈 - -| 技术 | 用途 | 版本 | -|:----:|:-----|:----:| -| OpenGL | 2D 图形渲染 | ES 3.0+ | -| GLFW | 窗口和输入管理 | 3.3+ | -| GLM | 数学库 | 0.9.9+ | -| miniaudio | 音频播放 | 最新版 | -| spdlog | 日志系统 | 最新版 | -| stb_image | 图像加载 | 最新版 | -| Squirrel | 脚本引擎 | 3.2+ | -| xmake | 构建系统 | 2.5+ | - ---- - -## 📖 相关文档 - -- [📚 API 教程](./docs/API_Tutorial/01_Quick_Start.md) - 完整的 API 使用教程 +- [📖 API 教程](./docs/API_Tutorial/01_Quick_Start.md) - 完整的 API 使用教程 - [01. 快速开始](./docs/API_Tutorial/01_Quick_Start.md) - [02. 场景系统](./docs/API_Tutorial/02_Scene_System.md) - [03. 节点系统](./docs/API_Tutorial/03_Node_System.md) @@ -438,8 +87,21 @@ sound->setVolume(0.8f); - [07. UI 系统](./docs/API_Tutorial/07_UI_System.md) - [08. 音频系统](./docs/API_Tutorial/08_Audio_System.md) - [🔧 构建系统文档](./docs/Extra2D%20构建系统文档.md) - 详细的构建系统说明 -- [🎮 Switch 构建指南](./SWITCH_BUILD_GUIDE.md) - Switch 平台构建教程 -- [📝 迁移完成记录](./SWITCH_MIGRATION_COMPLETE.md) - 项目迁移历史记录 + +--- + +## 🛠️ 技术栈 + +| 技术 | 用途 | 版本 | +|:----:|:-----|:----:| +| OpenGL ES | 2D 图形渲染 | 3.0+ | +| GLFW | 窗口和输入管理 | 3.3+ | +| GLM | 数学库 | 0.9.9+ | +| SDL2_mixer | 音频播放 | 2.0+ | +| spdlog | 日志系统 | 最新版 | +| stb_image | 图像加载 | 最新版 | +| freetype | 字体渲染 | 最新版 | +| xmake | 构建系统 | 2.5+ | --- diff --git a/docs/API_Tutorial/01_Quick_Start.md b/docs/API_Tutorial/01_Quick_Start.md index 9490a63..071b9a7 100644 --- a/docs/API_Tutorial/01_Quick_Start.md +++ b/docs/API_Tutorial/01_Quick_Start.md @@ -1,68 +1,215 @@ -# Extra2D API 教程 - 01. 快速开始 +# 01. 快速开始 -## 简介 +本教程将带你快速上手 Extra2D 引擎,通过一个简单的 Hello World 示例了解引擎的基本使用方法。 -Extra2D 是一个跨平台的 2D 游戏引擎,支持 Windows (MinGW) 和 Nintendo Switch 平台。 +## 示例代码 -## 最小示例 +完整示例位于 `examples/hello_world/main.cpp`: ```cpp #include using namespace extra2d; -int main(int argc, char **argv) { - // 1. 初始化日志系统 - Logger::init(); - Logger::setLevel(LogLevel::Debug); +// ============================================================================ +// Hello World 场景 +// ============================================================================ - // 2. 获取应用实例 - auto &app = Application::instance(); +/** + * @brief Hello World 场景类 + * 显示简单的 "Hello World" 文字 + */ +class HelloWorldScene : public Scene { +public: + /** + * @brief 场景进入时调用 + */ + void onEnter() override { + E2D_LOG_INFO("HelloWorldScene::onEnter - 进入场景"); - // 3. 配置应用 - AppConfig config; - config.title = "My Game"; - config.width = 1280; - config.height = 720; - config.vsync = true; - config.fpsLimit = 60; + // 设置背景颜色为深蓝色 + setBackgroundColor(Color(0.1f, 0.1f, 0.3f, 1.0f)); - // 4. 初始化应用 - if (!app.init(config)) { - E2D_LOG_ERROR("应用初始化失败!"); - return -1; + // 加载字体(支持多种字体后备) + auto &resources = Application::instance().resources(); + font_ = resources.loadFont("assets/font.ttf", 48, true); + + if (!font_) { + E2D_LOG_ERROR("字体加载失败,文字渲染将不可用!"); + return; } - // 5. 进入场景 - app.enterScene(makePtr()); + // 创建 "你好世界" 文本组件 - 使用屏幕空间(固定位置,不随相机移动) + auto text1 = Text::create("你好世界", font_); + text1->withCoordinateSpace(CoordinateSpace::Screen) + ->withScreenPosition(640.0f, 360.0f) // 屏幕中心 + ->withAnchor(0.5f, 0.5f) // 中心锚点,让文字中心对准位置 + ->withTextColor(Color(1.0f, 1.0f, 1.0f, 1.0f)); + addChild(text1); - // 6. 运行应用 - app.run(); + // 创建提示文本组件 - 使用屏幕空间,固定在屏幕底部 + auto text2 = Text::create("退出按键(START 按钮)", font_); + text2->withCoordinateSpace(CoordinateSpace::Screen) + ->withScreenPosition(640.0f, 650.0f) // 屏幕底部 + ->withAnchor(0.5f, 0.5f) + ->withTextColor(Color(1.0f, 1.0f, 0.0f, 1.0f)); + addChild(text2); + } - return 0; + /** + * @brief 每帧更新时调用 + * @param dt 时间间隔(秒) + */ + void onUpdate(float dt) override { + Scene::onUpdate(dt); + + // 检查退出按键 + auto &input = Application::instance().input(); + + // 使用手柄 START 按钮退出 (GamepadButton::Start) + if (input.isButtonPressed(GamepadButton::Start)) { + E2D_LOG_INFO("退出应用 (START 按钮)"); + Application::instance().quit(); + } + } + +private: + Ptr 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 应用遵循以下生命周期: ``` -Logger::init() → Application::init() → enterScene() → run() → 退出 +初始化 (Application::init) + ↓ +进入场景 (enterScene) + ↓ +主循环 (run) → 更新 (onUpdate) → 渲染 (onRender) + ↓ +退出 (quit) ``` -### 场景生命周期 +### 2. 场景系统 +场景是游戏内容的容器,通过继承 `Scene` 类并重写以下方法: + +| 方法 | 说明 | +|------|------| +| `onEnter()` | 场景进入时调用,用于初始化资源 | +| `onExit()` | 场景退出时调用,用于清理资源 | +| `onUpdate(dt)` | 每帧更新时调用,用于处理游戏逻辑 | +| `onRender(renderer)` | 渲染时调用,用于自定义绘制 | + +### 3. 坐标空间 + +Extra2D 支持三种坐标空间: + +```cpp +// 屏幕空间 - 固定位置,不随相机移动 +text->withCoordinateSpace(CoordinateSpace::Screen) + ->withScreenPosition(640.0f, 360.0f); + +// 相机空间 - 跟随相机但保持相对偏移 +text->withCoordinateSpace(CoordinateSpace::Camera) + ->withCameraOffset(50.0f, 50.0f); + +// 世界空间 - 随相机移动(默认行为) +text->withCoordinateSpace(CoordinateSpace::World) + ->withPosition(100.0f, 100.0f); ``` -onEnter() → onUpdate(dt) → onRender() → onExit() + +### 4. 输入处理 + +支持手柄输入检测: + +```cpp +auto &input = Application::instance().input(); + +// 检测按键按下(持续触发) +if (input.isButtonDown(GamepadButton::A)) { } + +// 检测按键按下(单次触发) +if (input.isButtonPressed(GamepadButton::A)) { } + +// 检测按键释放 +if (input.isButtonReleased(GamepadButton::A)) { } +``` + +常用按键: +- `GamepadButton::A` - A 键 +- `GamepadButton::B` - B 键 +- `GamepadButton::X` - X 键 +- `GamepadButton::Y` - Y 键 +- `GamepadButton::Start` - + 键 (Switch) +- `GamepadButton::DPadUp/Down/Left/Right` - 方向键 + +### 5. 资源加载 + +通过资源管理器加载字体、纹理等资源: + +```cpp +auto &resources = Application::instance().resources(); + +// 加载字体 +auto font = resources.loadFont("assets/font.ttf", 48, true); + +// 加载纹理 +auto texture = resources.loadTexture("assets/image.png"); +``` + +### 6. 日志系统 + +使用宏进行日志输出: + +```cpp +E2D_LOG_DEBUG("调试信息"); +E2D_LOG_INFO("普通信息"); +E2D_LOG_WARN("警告信息"); +E2D_LOG_ERROR("错误信息"); ``` ## 下一步 -- [02. 场景系统](02_Scene_System.md) -- [03. 节点系统](03_Node_System.md) -- [04. 资源管理](04_Resource_Management.md) -- [05. 输入处理](05_Input_Handling.md) -- [06. 碰撞检测](06_Collision_Detection.md) -- [07. UI 系统](07_UI_System.md) -- [08. 音频系统](08_Audio_System.md) +- [02. 场景系统](./02_Scene_System.md) - 深入了解场景管理 +- [03. 节点系统](./03_Node_System.md) - 学习节点和精灵的使用 diff --git a/docs/API_Tutorial/02_Scene_System.md b/docs/API_Tutorial/02_Scene_System.md index d885e14..1c68a68 100644 --- a/docs/API_Tutorial/02_Scene_System.md +++ b/docs/API_Tutorial/02_Scene_System.md @@ -1,9 +1,17 @@ -# Extra2D API 教程 - 02. 场景系统 +# 02. 场景系统 + +Extra2D 的场景系统提供了游戏内容的分层管理和切换功能。本教程将详细介绍场景的生命周期、切换和过渡效果。 + +## 完整示例 + +参考 `examples/push_box/` 中的实现: + +- `StartScene.h/cpp` - 开始菜单场景 +- `PlayScene.h/cpp` - 游戏主场景 +- `SuccessScene.h/cpp` - 通关场景 ## 场景基础 -场景(Scene)是游戏的基本组织单位,负责管理节点和渲染。 - ### 创建场景 ```cpp @@ -11,71 +19,54 @@ using namespace extra2d; -class MyScene : public Scene { +class GameScene : public Scene { public: - // 场景进入时调用 void onEnter() override { - // 必须先调用父类的 onEnter() + // 必须先调用父类方法 Scene::onEnter(); - // 设置背景颜色 + // 设置背景色 setBackgroundColor(Color(0.1f, 0.1f, 0.3f, 1.0f)); - E2D_LOG_INFO("场景已进入"); + // 设置视口大小(用于UI布局) + setViewportSize(1280.0f, 720.0f); } - // 每帧更新时调用 - void onUpdate(float dt) override { - Scene::onUpdate(dt); - // dt 是时间间隔(秒) - } - - // 渲染时调用 - void onRender(RenderBackend &renderer) override { - Scene::onRender(renderer); - // 绘制自定义内容 - } - - // 场景退出时调用 void onExit() override { // 清理资源 + removeAllChildren(); + Scene::onExit(); } + + void onUpdate(float dt) override { + Scene::onUpdate(dt); + + // 游戏逻辑更新 + } }; ``` -### 重要提示 - -**必须调用 `Scene::onEnter()`**: -```cpp -void onEnter() override { - Scene::onEnter(); // 必须调用! - // 你的初始化代码 -} -``` - -如果不调用,会导致: -- `running_` 状态未设置 -- 子节点无法正确注册到空间索引 -- 碰撞检测失效 - -## 场景管理 - -### 进入场景 +### 场景切换 ```cpp -// 进入新场景 -app.enterScene(makePtr()); +// 进入场景(无过渡) +app.enterScene(makePtr()); -// 替换当前场景(带过渡效果) -app.scenes().replaceScene( - makePtr(), - TransitionType::Fade, // 淡入淡出 - 0.25f // 过渡时间(秒) -); +// 进入场景(有过渡效果) +app.enterScene(makePtr(), TransitionType::Fade, 0.5f); + +// 替换当前场景 +app.scenes().replaceScene(makePtr()); + +// 推入场景(保留当前场景) +app.scenes().pushScene(makePtr()); + +// 弹出场景(返回上一个场景) +app.scenes().popScene(); ``` -### 场景过渡类型 +### 过渡效果类型 ```cpp enum class TransitionType { @@ -88,84 +79,184 @@ enum class TransitionType { }; ``` -## 场景配置 +## 场景管理器 -### 视口设置 +通过 `app.scenes()` 访问场景管理器: ```cpp -void onEnter() override { - Scene::onEnter(); - - // 设置视口大小(影响坐标系) - setViewportSize(1280.0f, 720.0f); - - // 设置背景颜色 - setBackgroundColor(Colors::Black); - - // 启用/禁用空间索引 - setSpatialIndexingEnabled(true); -} +auto& scenes = app.scenes(); + +// 获取当前场景 +auto current = scenes.currentScene(); + +// 获取场景栈深度 +size_t depth = scenes.stackDepth(); + +// 清空场景栈 +scenes.clearStack(); ``` -### 空间索引 +## 场景生命周期 -```cpp -// 获取空间管理器 -auto &spatialManager = getSpatialManager(); - -// 切换空间索引策略 -spatialManager.setStrategy(SpatialStrategy::QuadTree); // 四叉树 -spatialManager.setStrategy(SpatialStrategy::SpatialHash); // 空间哈希 - -// 查询所有碰撞 -auto collisions = queryCollisions(); +``` +创建场景 (makePtr) + ↓ +进入场景 (enterScene) + ↓ +onEnter() - 初始化资源 + ↓ +主循环 + ├── onUpdate(dt) - 每帧更新 + └── onRender(renderer) - 每帧渲染 + ↓ +退出场景 + ↓ +onExit() - 清理资源 + ↓ +场景销毁 ``` -## 完整示例 +## 推箱子示例场景结构 + +``` +┌─────────────────────────────────────┐ +│ StartScene │ +│ ┌─────────────────────────────┐ │ +│ │ 开始菜单界面 │ │ +│ │ - 新游戏 │ │ +│ │ - 继续游戏 │ │ +│ │ - 退出 │ │ +│ └─────────────────────────────┘ │ +└──────────────┬──────────────────────┘ + │ 选择"新游戏" + ↓ +┌─────────────────────────────────────┐ +│ PlayScene │ +│ ┌─────────────────────────────┐ │ +│ │ 游戏主界面 │ │ +│ │ - 地图渲染 │ │ +│ │ - 玩家控制 │ │ +│ │ - 关卡信息 │ │ +│ └─────────────────────────────┘ │ +└──────────────┬──────────────────────┘ + │ 通关 + ↓ +┌─────────────────────────────────────┐ +│ SuccessScene │ +│ ┌─────────────────────────────┐ │ +│ │ 通关界面 │ │ +│ │ - 显示成绩 │ │ +│ │ - 下一关/返回菜单 │ │ +│ └─────────────────────────────┘ │ +└─────────────────────────────────────┘ +``` + +## 代码示例:菜单场景 ```cpp -class GameScene : public Scene { +class MenuScene : public Scene { public: void onEnter() override { Scene::onEnter(); - // 设置视口和背景 - setViewportSize(1280.0f, 720.0f); - setBackgroundColor(Color(0.1f, 0.2f, 0.3f, 1.0f)); + auto& app = Application::instance(); + auto& resources = app.resources(); - // 启用空间索引 - setSpatialIndexingEnabled(true); + // 加载背景 + auto bgTex = resources.loadTexture("assets/bg.jpg"); + if (bgTex) { + auto bg = Sprite::create(bgTex); + bg->setAnchor(0.0f, 0.0f); + addChild(bg); + } - E2D_LOG_INFO("游戏场景已加载"); + // 加载字体 + font_ = resources.loadFont("assets/font.ttf", 28, true); + + // 创建菜单按钮 + createMenuButtons(); } void onUpdate(float dt) override { Scene::onUpdate(dt); - // 检查退出按键 - auto &input = Application::instance().input(); - if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_START)) { - Application::instance().quit(); + auto& input = Application::instance().input(); + + // 方向键导航 + if (input.isButtonPressed(GamepadButton::DPadUp)) { + selectedIndex_ = (selectedIndex_ - 1 + menuCount_) % menuCount_; + updateMenuColors(); + } + else if (input.isButtonPressed(GamepadButton::DPadDown)) { + selectedIndex_ = (selectedIndex_ + 1) % menuCount_; + updateMenuColors(); + } + + // A键确认 + if (input.isButtonPressed(GamepadButton::A)) { + executeMenuItem(); } } - void onRender(RenderBackend &renderer) override { - Scene::onRender(renderer); +private: + void createMenuButtons() { + float centerX = 640.0f; + float startY = 300.0f; + float spacing = 50.0f; - // 绘制 FPS - auto &app = Application::instance(); - std::string fpsText = "FPS: " + std::to_string(app.fps()); - // ... + for (int i = 0; i < menuCount_; ++i) { + auto btn = Button::create(); + btn->setFont(font_); + btn->setAnchor(0.5f, 0.5f); + btn->setPosition(centerX, startY + i * spacing); + addChild(btn); + buttons_.push_back(btn); + } + + buttons_[0]->setText("开始游戏"); + buttons_[1]->setText("设置"); + buttons_[2]->setText("退出"); + + updateMenuColors(); } - void onExit() override { - E2D_LOG_INFO("游戏场景退出"); - Scene::onExit(); + void updateMenuColors() { + for (int i = 0; i < buttons_.size(); ++i) { + auto color = (i == selectedIndex_) ? Colors::Red : Colors::White; + buttons_[i]->setTextColor(color); + } } + + void executeMenuItem() { + switch (selectedIndex_) { + case 0: + Application::instance().scenes().replaceScene( + makePtr(), 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. **分离场景逻辑** - 每个场景负责自己的功能 + ## 下一步 -- [03. 节点系统](03_Node_System.md) -- [04. 资源管理](04_Resource_Management.md) +- [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 index 63a7b08..fa61066 100644 --- a/docs/API_Tutorial/03_Node_System.md +++ b/docs/API_Tutorial/03_Node_System.md @@ -1,219 +1,353 @@ -# Extra2D API 教程 - 03. 节点系统 +# 03. 节点系统 -## 节点基础 +Extra2D 的节点系统是构建游戏对象的基础。所有可见的游戏元素都是节点的子类。 -节点(Node)是游戏对象的基本单位,可以包含子节点,形成树形结构。 +## 核心节点类型 -### 创建节点 - -```cpp -#include - -using namespace extra2d; - -class MyNode : public Node { -public: - MyNode() { - // 设置位置 - setPosition(Vec2(100.0f, 200.0f)); - - // 设置旋转(度) - setRotation(45.0f); - - // 设置缩放 - setScale(Vec2(2.0f, 2.0f)); - - // 设置锚点(0-1范围,默认0.5是中心) - setAnchor(0.5f, 0.5f); - - // 设置可见性 - setVisible(true); - } - - // 每帧更新 - void onUpdate(float dt) override { - Node::onUpdate(dt); - // 自定义更新逻辑 - } - - // 渲染 - void onRender(RenderBackend &renderer) override { - Node::onRender(renderer); - // 自定义渲染 - } -}; +``` +Node (基类) +├── Sprite (精灵) +├── Text (文本) +├── Button (按钮) +├── Widget (UI控件基类) +│ ├── Label (标签) +│ ├── CheckBox (复选框) +│ ├── RadioButton (单选按钮) +│ ├── Slider (滑块) +│ └── ProgressBar (进度条) +└── 自定义节点... ``` -## 节点层级 +## 基础节点操作 -### 添加子节点 +### 创建和添加节点 ```cpp -void onEnter() override { - Scene::onEnter(); - - // 创建子节点 - auto child = makePtr(); - - // 添加到场景 - addChild(child); - - // 在指定位置添加 - addChild(child, 0); // z-order = 0 -} +// 创建精灵 +auto sprite = Sprite::create(texture); +sprite->setPosition(Vec2(100, 200)); +sprite->setAnchor(Vec2(0.5f, 0.5f)); // 中心锚点 +addChild(sprite); + +// 创建文本 +auto text = Text::create("Hello World", font); +text->setPosition(Vec2(400, 300)); +text->setTextColor(Color(1, 1, 1, 1)); +addChild(text); ``` -### 移除子节点 +### 节点属性 ```cpp -// 移除指定子节点 -removeChild(child); +// 位置 +node->setPosition(Vec2(x, y)); +Vec2 pos = node->getPosition(); -// 移除所有子节点 -removeAllChildren(); +// 旋转(角度) +node->setRotation(45.0f); +float angle = node->getRotation(); -// 通过名称移除 -removeChildByName("myNode"); +// 缩放 +node->setScale(Vec2(2.0f, 2.0f)); +Vec2 scale = node->getScale(); + +// 锚点(0,0 左上角,0.5,0.5 中心,1,1 右下角) +node->setAnchor(Vec2(0.5f, 0.5f)); + +// 可见性 +node->setVisible(true); +bool visible = node->isVisible(); + +// Z轴顺序 +node->setZOrder(10); ``` -### 获取子节点 +### 链式调用(Builder 模式) ```cpp -// 获取子节点数量 -size_t count = getChildren().size(); - -// 通过名称查找 -auto node = getChildByName("myNode"); - -// 遍历子节点 -for (auto &child : getChildren()) { - // 处理子节点 -} +// 使用链式调用快速配置节点 +auto text = Text::create("标题", font) + ->withPosition(640.0f, 100.0f) + ->withAnchor(0.5f, 0.5f) + ->withTextColor(Color(1.0f, 1.0f, 0.0f, 1.0f)) + ->withCoordinateSpace(CoordinateSpace::Screen); +addChild(text); ``` -## 空间索引 - -### 启用空间索引 - -```cpp -class PhysicsNode : public Node { -public: - PhysicsNode() { - // 启用空间索引(用于碰撞检测) - setSpatialIndexed(true); - } - - // 必须实现 getBoundingBox() - Rect getBoundingBox() const override { - Vec2 pos = getPosition(); - return Rect(pos.x - 25.0f, pos.y - 25.0f, 50.0f, 50.0f); - } -}; -``` - -### 边界框 - -```cpp -// 获取节点边界框 -Rect bounds = node->getBoundingBox(); - -// 检查点是否在边界框内 -if (bounds.contains(Vec2(x, y))) { - // 点在边界框内 -} - -// 检查两个边界框是否相交 -if (bounds.intersects(otherBounds)) { - // 边界框相交 -} -``` - -## 精灵节点 +## 精灵(Sprite) ### 创建精灵 ```cpp -// 加载纹理 -auto texture = resources.loadTexture("assets/player.png"); +auto& resources = Application::instance().resources(); -// 创建精灵 +// 从纹理创建 +auto texture = resources.loadTexture("assets/player.png"); auto sprite = Sprite::create(texture); -// 设置位置 -sprite->setPosition(Vec2(640.0f, 360.0f)); +// 设置精灵属性 +sprite->setPosition(Vec2(400, 300)); +sprite->setAnchor(Vec2(0.5f, 0.5f)); -// 设置锚点(中心) -sprite->setAnchor(0.5f, 0.5f); - -// 添加到场景 -addChild(sprite); +// 切换纹理 +auto newTexture = resources.loadTexture("assets/player2.png"); +sprite->setTexture(newTexture); ``` ### 精灵动画 ```cpp -// 创建动画 -auto animation = Animation::create("walk", 0.1f); -animation->addFrame(resources.loadTexture("assets/walk1.png")); -animation->addFrame(resources.loadTexture("assets/walk2.png")); -animation->addFrame(resources.loadTexture("assets/walk3.png")); +// 创建帧动画 +auto anim = AnimatedSprite::createFromGrid( + "player.png", // 纹理 + 32, 32, // 单帧宽高 + 100.0f, // 帧间隔(ms) + 8 // 总帧数 +); // 播放动画 -sprite->playAnimation(animation, true); // true = 循环播放 +anim->play(); + +// 设置帧范围 +anim->setFrameRange(0, 3); + +// 循环播放 +anim->setLoop(true); // 停止动画 -sprite->stopAnimation(); +anim->stop(); ``` -## 完整示例 +## 文本(Text) + +### 创建文本 + +```cpp +// 加载字体 +auto font = resources.loadFont("assets/font.ttf", 24, true); + +// 创建文本 +auto text = Text::create("Hello World", font); +text->setPosition(Vec2(400, 300)); +addChild(text); + +// 动态修改文本 +text->setText("新的文本内容"); + +// 使用格式化文本 +text->setFormat("得分: %d", score); +``` + +### 文本样式 + +```cpp +text->setTextColor(Color(1.0f, 1.0f, 1.0f, 1.0f)); // 颜色 +text->setFontSize(48); // 字体大小 +text->setAnchor(Vec2(0.5f, 0.5f)); // 锚点 + +// 坐标空间 +text->withCoordinateSpace(CoordinateSpace::Screen) // 屏幕空间 + ->withScreenPosition(100.0f, 50.0f); +``` + +## 按钮(Button) + +### 创建按钮 + +```cpp +auto button = Button::create(); +button->setFont(font); +button->setText("点击我"); +button->setPosition(Vec2(400, 300)); +button->setCustomSize(200.0f, 60.0f); + +// 设置颜色 +button->setTextColor(Colors::White); +button->setBackgroundColor( + Colors::Blue, // 正常状态 + Colors::Green, // 悬停状态 + Colors::Red // 按下状态 +); + +// 设置点击回调 +button->onClick([]() { + E2D_LOG_INFO("按钮被点击!"); +}); + +addChild(button); +``` + +### 透明按钮(用于菜单) + +```cpp +// 创建纯文本按钮(透明背景) +auto menuBtn = Button::create(); +menuBtn->setFont(font); +menuBtn->setText("菜单项"); +menuBtn->setTextColor(Colors::Black); +menuBtn->setBackgroundColor( + Colors::Transparent, + Colors::Transparent, + Colors::Transparent +); +menuBtn->setBorder(Colors::Transparent, 0.0f); +menuBtn->setAnchor(0.5f, 0.5f); +menuBtn->setPosition(centerX, centerY); +addChild(menuBtn); +``` + +## 节点层级管理 + +### 父子关系 + +```cpp +// 添加子节点 +parent->addChild(child); + +// 移除子节点 +parent->removeChild(child); + +// 移除所有子节点 +parent->removeAllChildren(); + +// 获取父节点 +Node* parent = child->getParent(); + +// 获取子节点列表 +const auto& children = parent->getChildren(); +``` + +### Z轴顺序 + +```cpp +// 设置Z轴顺序(值越大越在上层) +node->setZOrder(10); + +// 重新排序子节点 +parent->reorderChild(child, newZOrder); +``` + +## 自定义节点 + +### 继承 Node 创建自定义节点 ```cpp class Player : public Node { public: - Player() { - setSpatialIndexed(true); - - // 加载精灵 - auto &resources = Application::instance().resources(); - auto texture = resources.loadTexture("assets/player.png"); + static Ptr create(Ptr texture) { + auto player = makePtr(); + if (player->init(texture)) { + return player; + } + return nullptr; + } + + bool init(Ptr texture) { sprite_ = Sprite::create(texture); - sprite_->setAnchor(0.5f, 0.5f); addChild(sprite_); + return true; } - void onUpdate(float dt) override { - Node::onUpdate(dt); - - // 移动 - Vec2 pos = getPosition(); - pos = pos + velocity_ * dt; - setPosition(pos); - - // 边界检查 - auto &app = Application::instance(); - float width = static_cast(app.getConfig().width); - float height = static_cast(app.getConfig().height); - - if (pos.x < 0 || pos.x > width) { - velocity_.x = -velocity_.x; - } - if (pos.y < 0 || pos.y > height) { - velocity_.y = -velocity_.y; - } + void update(float dt) override { + // 更新逻辑 + velocity_.y += gravity_ * dt; + setPosition(getPosition() + velocity_ * dt); } - Rect getBoundingBox() const override { - Vec2 pos = getPosition(); - return Rect(pos.x - 25.0f, pos.y - 25.0f, 50.0f, 50.0f); + void jump() { + velocity_.y = jumpForce_; } private: Ptr sprite_; - Vec2 velocity_{100.0f, 100.0f}; + Vec2 velocity_; + float gravity_ = -980.0f; + float jumpForce_ = 500.0f; }; + +// 使用 +auto player = Player::create(texture); +scene->addChild(player); +``` + +## 空间索引(碰撞检测) + +### 启用空间索引 + +```cpp +class CollidableNode : public Node { +public: + CollidableNode() { + // 启用空间索引 + setSpatialIndexed(true); + } + + // 必须实现 getBoundingBox + Rect getBoundingBox() const override { + Vec2 pos = getPosition(); + return Rect(pos.x - 25, pos.y - 25, 50, 50); + } +}; +``` + +### 查询碰撞 + +```cpp +// 在场景中查询所有碰撞 +auto collisions = scene->queryCollisions(); + +for (const auto& [nodeA, nodeB] : collisions) { + // 处理碰撞 + handleCollision(nodeA, nodeB); +} +``` + +## 完整示例 + +参考 `examples/push_box/PlayScene.cpp` 中的节点使用: + +```cpp +void PlayScene::onEnter() { + Scene::onEnter(); + + auto& resources = app.resources(); + + // 加载纹理资源 + texWall_ = resources.loadTexture("assets/images/wall.gif"); + texBox_ = resources.loadTexture("assets/images/box.gif"); + + // 创建地图层 + mapLayer_ = makePtr(); + addChild(mapLayer_); + + // 创建地图元素 + for (int y = 0; y < mapHeight; ++y) { + for (int x = 0; x < mapWidth; ++x) { + char cell = map_[y][x]; + + if (cell == '#') { + auto wall = Sprite::create(texWall_); + wall->setPosition(x * tileSize, y * tileSize); + mapLayer_->addChild(wall); + } + else if (cell == '$') { + auto box = Sprite::create(texBox_); + box->setPosition(x * tileSize, y * tileSize); + mapLayer_->addChild(box); + } + } + } + + // 创建UI文本 + font28_ = resources.loadFont("assets/font.ttf", 28, true); + levelText_ = Text::create("Level: 1", font28_); + levelText_->setPosition(50, 30); + addChild(levelText_); +} ``` ## 下一步 -- [04. 资源管理](04_Resource_Management.md) -- [05. 输入处理](05_Input_Handling.md) +- [04. 资源管理](./04_Resource_Management.md) - 深入了解资源加载 +- [05. 输入处理](./05_Input_Handling.md) - 学习输入处理 +- [06. 碰撞检测](./06_Collision_Detection.md) - 学习碰撞检测系统 diff --git a/docs/API_Tutorial/04_Resource_Management.md b/docs/API_Tutorial/04_Resource_Management.md index 18d8108..fdcc3b3 100644 --- a/docs/API_Tutorial/04_Resource_Management.md +++ b/docs/API_Tutorial/04_Resource_Management.md @@ -1,148 +1,167 @@ -# Extra2D API 教程 - 04. 资源管理 +# 04. 资源管理 + +Extra2D 提供了统一的资源管理系统,用于加载和管理游戏中的各种资源。 ## 资源管理器 -Extra2D 使用资源管理器来统一加载和管理资源。 - -### 获取资源管理器 +通过 `Application::instance().resources()` 访问资源管理器: ```cpp -auto &resources = Application::instance().resources(); +auto& resources = Application::instance().resources(); ``` -## 字体资源 +## 支持的资源类型 -### 加载字体 +| 资源类型 | 加载方法 | 说明 | +|---------|---------|------| +| 纹理 | `loadTexture()` | 图片文件 (PNG, JPG, etc.) | +| 字体 | `loadFont()` | TrueType 字体文件 | +| 音频 | `loadSound()` / `loadMusic()` | 音频文件 | -```cpp -// 加载字体(路径,大小,使用后备字体) -auto font = resources.loadFont("assets/font.ttf", 48, true); +## 纹理加载 -if (!font) { - E2D_LOG_ERROR("字体加载失败!"); -} -``` - -### 使用字体 - -```cpp -void onRender(RenderBackend &renderer) override { - if (font) { - renderer.drawText(*font, "Hello World", Vec2(100.0f, 100.0f), - Colors::White); - } -} -``` - -## 纹理资源 - -### 加载纹理 +### 基本用法 ```cpp // 加载纹理 -auto texture = resources.loadTexture("assets/player.png"); +auto texture = resources.loadTexture("assets/images/player.png"); -if (!texture) { - E2D_LOG_ERROR("纹理加载失败!"); +if (texture) { + // 创建精灵 + auto sprite = Sprite::create(texture); + addChild(sprite); } ``` -### 创建精灵 +### 纹理缓存 + +资源管理器会自动缓存已加载的纹理,多次加载同一文件会返回缓存的实例: ```cpp -auto sprite = Sprite::create(texture); -sprite->setPosition(Vec2(640.0f, 360.0f)); -addChild(sprite); +// 第一次加载 - 从文件读取 +auto tex1 = resources.loadTexture("assets/image.png"); + +// 第二次加载 - 返回缓存 +auto tex2 = resources.loadTexture("assets/image.png"); + +// tex1 和 tex2 指向同一个纹理对象 ``` -## 音效资源 +## 字体加载 -### 加载音效 +### 基本用法 ```cpp -// 加载音效 -auto sound = resources.loadSound("assets/jump.wav"); +// 加载字体(指定字号) +auto font24 = resources.loadFont("assets/font.ttf", 24, true); -// 播放音效 -sound->play(); - -// 循环播放 -sound->play(true); - -// 停止播放 -sound->stop(); +// 创建文本 +auto text = Text::create("Hello World", font24); +addChild(text); ``` -## 资源路径解析 +### 字体后备 -Extra2D 的资源管理器支持多平台路径解析: - -### 路径优先级 - -1. **原始路径**: `assets/font.ttf` -2. **romfs 路径**: `romfs:/assets/font.ttf` (Switch) -3. **sdmc 路径**: `sdmc:/assets/font.ttf` (Switch SD卡) -4. **可执行文件相对路径** (Windows) - -### 使用示例 +支持设置后备字体,当主字体缺少某些字符时自动使用后备字体: ```cpp -// 所有平台使用相同的路径 -auto font = resources.loadFont("assets/font.ttf", 48, true); -auto texture = resources.loadTexture("assets/images/player.png"); -auto sound = resources.loadSound("assets/audio/jump.wav"); +// 加载主字体和后备字体 +auto mainFont = resources.loadFont("assets/main.ttf", 24, true); +auto fallbackFont = resources.loadFont("assets/fallback.ttf", 24, true); + +// 设置后备字体 +mainFont->setFallback(fallbackFont); +``` + +## 资源路径 + +### 路径格式 + +```cpp +// 相对路径(相对于工作目录) +auto tex = resources.loadTexture("assets/images/player.png"); + +// Switch 平台使用 romfs +auto tex = resources.loadTexture("romfs:/images/player.png"); + +// SD 卡路径 +auto tex = resources.loadTexture("sdmc:/switch/game/images/player.png"); +``` + +### 路径辅助函数 + +```cpp +// 获取平台特定的资源路径 +std::string path = ResourceManager::getPlatformPath("images/player.png"); +// Windows: "assets/images/player.png" +// Switch: "romfs:/images/player.png" +``` + +## 资源释放 + +### 自动释放 + +资源使用智能指针管理,当没有引用时会自动释放: + +```cpp +{ + auto tex = resources.loadTexture("assets/temp.png"); + // 使用纹理... +} // 超出作用域,如果没有其他引用,纹理自动释放 +``` + +### 手动清理缓存 + +```cpp +// 清理未使用的资源 +resources.cleanupUnused(); + +// 清空所有缓存(谨慎使用) +resources.clearCache(); ``` ## 完整示例 +参考 `examples/push_box/StartScene.cpp`: + ```cpp -class GameScene : public Scene { -public: - void onEnter() override { - Scene::onEnter(); - - auto &resources = Application::instance().resources(); - - // 加载字体 - titleFont_ = resources.loadFont("assets/font.ttf", 60, true); - infoFont_ = resources.loadFont("assets/font.ttf", 24, true); - - // 加载纹理 - playerTexture_ = resources.loadTexture("assets/player.png"); - enemyTexture_ = resources.loadTexture("assets/enemy.png"); - - // 创建精灵 - player_ = Sprite::create(playerTexture_); - player_->setPosition(Vec2(640.0f, 360.0f)); - addChild(player_); +void StartScene::onEnter() { + Scene::onEnter(); + + auto& app = Application::instance(); + auto& resources = app.resources(); + + // 加载背景纹理 + auto bgTex = resources.loadTexture("assets/images/start.jpg"); + if (bgTex) { + auto background = Sprite::create(bgTex); + background->setAnchor(0.0f, 0.0f); + addChild(background); } - void onRender(RenderBackend &renderer) override { - Scene::onRender(renderer); - - // 绘制文字 - if (titleFont_) { - renderer.drawText(*titleFont_, "Game Title", - Vec2(50.0f, 50.0f), Colors::White); - } - - if (infoFont_) { - std::string fps = "FPS: " + std::to_string(Application::instance().fps()); - renderer.drawText(*infoFont_, fps, - Vec2(50.0f, 100.0f), Colors::Yellow); - } + // 加载音效图标纹理 + auto soundOn = resources.loadTexture("assets/images/soundon.png"); + auto soundOff = resources.loadTexture("assets/images/soundoff.png"); + if (soundOn && soundOff) { + soundIcon_ = Sprite::create(g_SoundOpen ? soundOn : soundOff); + addChild(soundIcon_); } -private: - Ptr titleFont_; - Ptr infoFont_; - Ptr playerTexture_; - Ptr enemyTexture_; - Ptr player_; -}; + // 加载字体 + font_ = resources.loadFont("assets/font.ttf", 28, true); + + // 创建按钮... +} ``` +## 最佳实践 + +1. **预加载资源** - 在场景 `onEnter()` 中加载所需资源 +2. **检查资源有效性** - 始终检查加载结果是否为 nullptr +3. **复用资源** - 多次使用同一资源时保存指针,避免重复加载 +4. **合理设置字号** - 字体加载时会生成对应字号的图集 + ## 下一步 -- [05. 输入处理](05_Input_Handling.md) -- [06. 碰撞检测](06_Collision_Detection.md) +- [05. 输入处理](./05_Input_Handling.md) - 学习输入处理 +- [06. 碰撞检测](./06_Collision_Detection.md) - 学习碰撞检测系统 diff --git a/docs/API_Tutorial/05_Input_Handling.md b/docs/API_Tutorial/05_Input_Handling.md index 24ed607..d280294 100644 --- a/docs/API_Tutorial/05_Input_Handling.md +++ b/docs/API_Tutorial/05_Input_Handling.md @@ -1,216 +1,189 @@ -# Extra2D API 教程 - 05. 输入处理 +# 05. 输入处理 -## 输入系统 +Extra2D 提供了统一的输入处理系统,支持手柄、键盘等多种输入设备。 -Extra2D 提供统一的输入处理接口,支持键盘和游戏手柄。 +## 输入管理器 -### 获取输入管理器 +通过 `Application::instance().input()` 访问输入管理器: ```cpp -auto &input = Application::instance().input(); +auto& input = Application::instance().input(); ``` -## 游戏手柄输入 +## 按键检测 -Extra2D 提供了 `GamepadButton` 和 `GamepadAxis` 命名空间来映射 SDL 按键。 - -### 检测按键按下 +### 检测方法 ```cpp -void onUpdate(float dt) override { - auto &input = Application::instance().input(); - - // 检测按键按下(每帧只触发一次) - if (input.isButtonPressed(GamepadButton::A)) { - // A 键被按下 - jump(); - } - - if (input.isButtonPressed(GamepadButton::B)) { - // B 键被按下 - attack(); - } +// 按键是否按下(持续触发) +if (input.isButtonDown(GamepadButton::A)) { + // 每帧都会触发,只要按键保持按下 +} + +// 按键是否刚按下(单次触发) +if (input.isButtonPressed(GamepadButton::A)) { + // 只在按下瞬间触发一次 +} + +// 按键是否刚释放 +if (input.isButtonReleased(GamepadButton::A)) { + // 只在释放瞬间触发一次 } ``` -### 检测按键按住 +### 常用按键 -```cpp -void onUpdate(float dt) override { - auto &input = Application::instance().input(); - - // 检测按键按住(每帧都触发) - if (input.isButtonDown(GamepadButton::DPadLeft)) { - // 左方向键按住 - moveLeft(); - } - - if (input.isButtonDown(GamepadButton::DPadRight)) { - // 右方向键按住 - moveRight(); - } -} -``` - -### 按键映射表 - -| Extra2D 枚举 | 对应按键 | -|-------------|----------| -| `GamepadButton::A` | A 键 (Xbox) / × 键 (PlayStation) | -| `GamepadButton::B` | B 键 (Xbox) / ○ 键 (PlayStation) | -| `GamepadButton::X` | X 键 (Xbox) / □ 键 (PlayStation) | -| `GamepadButton::Y` | Y 键 (Xbox) / △ 键 (PlayStation) | -| `GamepadButton::LeftBumper` | 左肩键 (LB/L1) | -| `GamepadButton::RightBumper` | 右肩键 (RB/R1) | -| `GamepadButton::Back` | 返回键 (View/Share) | -| `GamepadButton::Start` | 开始键 (Menu/Options) | -| `GamepadButton::Guide` | 主页键 (Xbox/PS) | -| `GamepadButton::LeftThumb` | 左摇杆按下 (L3) | -| `GamepadButton::RightThumb` | 右摇杆按下 (R3) | -| `GamepadButton::DPadUp` | 方向键上 | -| `GamepadButton::DPadDown` | 方向键下 | -| `GamepadButton::DPadLeft` | 方向键左 | -| `GamepadButton::DPadRight` | 方向键右 | - -### PlayStation 风格别名 - -| Extra2D 枚举 | 对应按键 | -|-------------|----------| -| `GamepadButton::Cross` | A | -| `GamepadButton::Circle` | B | -| `GamepadButton::Square` | X | -| `GamepadButton::Triangle` | Y | +| 按键 | 说明 | Switch 对应 | +|------|------|------------| +| `GamepadButton::A` | A 键 | A 键 | +| `GamepadButton::B` | B 键 | B 键 | +| `GamepadButton::X` | X 键 | X 键 | +| `GamepadButton::Y` | Y 键 | Y 键 | +| `GamepadButton::Start` | 开始键 | + 键 | +| `GamepadButton::Select` | 选择键 | - 键 | +| `GamepadButton::DPadUp` | 方向上 | 方向键上 | +| `GamepadButton::DPadDown` | 方向下 | 方向键下 | +| `GamepadButton::DPadLeft` | 方向左 | 方向键左 | +| `GamepadButton::DPadRight` | 方向右 | 方向键右 | +| `GamepadButton::LeftStick` | 左摇杆按下 | L3 | +| `GamepadButton::RightStick` | 右摇杆按下 | R3 | +| `GamepadButton::LeftShoulder` | 左肩键 | L | +| `GamepadButton::RightShoulder` | 右肩键 | R | ## 摇杆输入 ### 获取摇杆值 ```cpp -void onUpdate(float dt) override { - auto &input = Application::instance().input(); - - // 左摇杆(范围 -1.0 到 1.0) - float leftX = input.getAxis(GamepadAxis::LeftX); - float leftY = input.getAxis(GamepadAxis::LeftY); - - // 右摇杆 - float rightX = input.getAxis(GamepadAxis::RightX); - float rightY = input.getAxis(GamepadAxis::RightY); - - // 使用摇杆值移动 - if (std::abs(leftX) > 0.1f || std::abs(leftY) > 0.1f) { - Vec2 velocity(leftX * speed, leftY * speed); - player->setPosition(player->getPosition() + velocity * dt); - } -} +// 获取左摇杆位置(范围 -1.0 到 1.0) +Vec2 leftStick = input.getLeftStick(); + +// 获取右摇杆位置 +Vec2 rightStick = input.getRightStick(); + +// 应用摇杆输入 +float speed = 200.0f; +player->setPosition(player->getPosition() + leftStick * speed * dt); ``` -### 摇杆轴映射表 - -| Extra2D 枚举 | 说明 | -|-------------|------| -| `GamepadAxis::LeftX` | 左摇杆 X 轴 | -| `GamepadAxis::LeftY` | 左摇杆 Y 轴 | -| `GamepadAxis::RightX` | 右摇杆 X 轴 | -| `GamepadAxis::RightY` | 右摇杆 Y 轴 | -| `GamepadAxis::LeftTrigger` | 左扳机 (LT/L2) | -| `GamepadAxis::RightTrigger` | 右扳机 (RT/R2) | - -## 键盘输入 - -### 检测键盘按键 +### 摇杆死区 ```cpp -void onUpdate(float dt) override { - auto &input = Application::instance().input(); - - // 检测按键按下 - if (input.isKeyPressed(SDLK_SPACE)) { - jump(); - } - - // 检测按键按住 - if (input.isKeyDown(SDLK_LEFT)) { - moveLeft(); - } - - if (input.isKeyDown(SDLK_RIGHT)) { - moveRight(); - } -} +// 设置摇杆死区(默认 0.15) +input.setStickDeadZone(0.2f); ``` ## 完整示例 +### 菜单导航 + +参考 `examples/push_box/StartScene.cpp`: + ```cpp -class Player : public Node { -public: - void onUpdate(float dt) override { - Node::onUpdate(dt); - - auto &input = Application::instance().input(); - Vec2 velocity(0.0f, 0.0f); - - // 方向键移动 - if (input.isButtonDown(GamepadButton::DPadLeft)) { - velocity.x = -speed_; - } else if (input.isButtonDown(GamepadButton::DPadRight)) { - velocity.x = speed_; - } - - if (input.isButtonDown(GamepadButton::DPadUp)) { - velocity.y = -speed_; - } else if (input.isButtonDown(GamepadButton::DPadDown)) { - velocity.y = speed_; - } - - // 摇杆移动(如果方向键没有按下) - if (velocity.x == 0.0f && velocity.y == 0.0f) { - float axisX = input.getAxis(GamepadAxis::LeftX); - float axisY = input.getAxis(GamepadAxis::LeftY); - - if (std::abs(axisX) > 0.1f) { - velocity.x = axisX * speed_; - } - if (std::abs(axisY) > 0.1f) { - velocity.y = axisY * speed_; - } - } - - // 应用移动 - Vec2 pos = getPosition(); - pos = pos + velocity * dt; - setPosition(pos); - - // 动作键 - if (input.isButtonPressed(GamepadButton::A)) { - jump(); - } - - if (input.isButtonPressed(GamepadButton::B)) { - attack(); - } - - // 退出游戏 - if (input.isButtonPressed(GamepadButton::Start)) { - Application::instance().quit(); - } +void StartScene::onUpdate(float dt) { + Scene::onUpdate(dt); + + auto& input = Application::instance().input(); + + // 方向键上下切换选择 + if (input.isButtonPressed(GamepadButton::DPadUp)) { + selectedIndex_ = (selectedIndex_ - 1 + menuCount_) % menuCount_; + updateMenuColors(); + } + else if (input.isButtonPressed(GamepadButton::DPadDown)) { + selectedIndex_ = (selectedIndex_ + 1) % menuCount_; + updateMenuColors(); } - -private: - float speed_ = 200.0f; - - void jump() { - // 跳跃逻辑 + + // A键确认 + if (input.isButtonPressed(GamepadButton::A)) { + executeMenuItem(); } - - void attack() { - // 攻击逻辑 + + // X键切换音效 + if (input.isButtonPressed(GamepadButton::X)) { + g_SoundOpen = !g_SoundOpen; + AudioManager::instance().setEnabled(g_SoundOpen); + updateSoundIcon(); } -}; +} ``` +### 玩家移动 + +```cpp +void Player::update(float dt) { + auto& input = Application::instance().input(); + + Vec2 moveDir; + + // 方向键移动 + if (input.isButtonDown(GamepadButton::DPadLeft)) { + moveDir.x -= 1; + } + if (input.isButtonDown(GamepadButton::DPadRight)) { + moveDir.x += 1; + } + if (input.isButtonDown(GamepadButton::DPadUp)) { + moveDir.y -= 1; + } + if (input.isButtonDown(GamepadButton::DPadDown)) { + moveDir.y += 1; + } + + // 摇杆移动 + Vec2 stick = input.getLeftStick(); + if (stick.length() > 0.1f) { + moveDir = stick; + } + + // 应用移动 + if (moveDir.length() > 0) { + moveDir.normalize(); + setPosition(getPosition() + moveDir * speed_ * dt); + } + + // 跳跃 + if (input.isButtonPressed(GamepadButton::A)) { + jump(); + } +} +``` + +## 输入映射 + +### 自定义按键映射 + +```cpp +// 定义动作 +enum class Action { + Jump, + Attack, + Pause +}; + +// 映射按键到动作 +std::unordered_map actionMap = { + {Action::Jump, GamepadButton::A}, + {Action::Attack, GamepadButton::B}, + {Action::Pause, GamepadButton::Start} +}; + +// 检查动作 +bool isActionPressed(Action action) { + auto& input = Application::instance().input(); + return input.isButtonPressed(actionMap[action]); +} +``` + +## 最佳实践 + +1. **使用 isButtonPressed 进行菜单操作** - 避免持续触发 +2. **使用 isButtonDown 进行移动控制** - 实现流畅移动 +3. **支持多种输入方式** - 同时支持方向键和摇杆 +4. **添加输入缓冲** - 提升操作手感 + ## 下一步 -- [06. 碰撞检测](06_Collision_Detection.md) -- [07. UI 系统](07_UI_System.md) +- [06. 碰撞检测](./06_Collision_Detection.md) - 学习碰撞检测系统 +- [07. UI 系统](./07_UI_System.md) - 学习 UI 控件使用 diff --git a/docs/API_Tutorial/06_Collision_Detection.md b/docs/API_Tutorial/06_Collision_Detection.md index 8c213bd..1fd85bf 100644 --- a/docs/API_Tutorial/06_Collision_Detection.md +++ b/docs/API_Tutorial/06_Collision_Detection.md @@ -1,81 +1,90 @@ -# Extra2D API 教程 - 06. 碰撞检测 +# 06. 碰撞检测 -## 空间索引系统 +Extra2D 提供了基于空间索引的高效碰撞检测系统,支持四叉树和空间哈希两种策略。 -Extra2D 内置了空间索引系统,用于高效地进行碰撞检测。 +## 完整示例 -### 启用空间索引 +参考示例代码: +- `examples/collision_demo/main.cpp` - 基础碰撞检测演示 +- `examples/spatial_index_demo/main.cpp` - 空间索引性能演示 + +## 启用碰撞检测 + +### 1. 创建可碰撞节点 ```cpp -void onEnter() override { - Scene::onEnter(); - - // 启用空间索引 - setSpatialIndexingEnabled(true); -} -``` - -## 碰撞节点 - -### 创建可碰撞节点 - -```cpp -class PhysicsNode : public Node { +class CollidableBox : public Node { public: - PhysicsNode(float size, const Color &color) - : size_(size), color_(color), isColliding_(false) { - // 启用空间索引(关键!) + CollidableBox(float width, float height, const Color& color) + : width_(width), height_(height), color_(color), isColliding_(false) { + // 启用空间索引 - 这是关键! setSpatialIndexed(true); } - // 必须实现 getBoundingBox() + // 必须实现 getBoundingBox 方法 Rect getBoundingBox() const override { Vec2 pos = getPosition(); - return Rect(pos.x - size_ / 2, pos.y - size_ / 2, size_, size_); + return Rect(pos.x - width_ / 2, pos.y - height_ / 2, width_, height_); } void setColliding(bool colliding) { isColliding_ = colliding; } - void onRender(RenderBackend &renderer) override { + void onRender(RenderBackend& renderer) override { Vec2 pos = getPosition(); // 碰撞时变红色 - Color fillColor = isColliding_ ? Color(1.0f, 0.2f, 0.2f, 0.9f) : color_; - renderer.fillRect(Rect(pos.x - size_ / 2, pos.y - size_ / 2, size_, size_), - fillColor); + Color fillColor = isColliding_ ? Color(1.0f, 0.2f, 0.2f, 0.8f) : color_; + renderer.fillRect( + Rect(pos.x - width_ / 2, pos.y - height_ / 2, width_, height_), + fillColor + ); + + // 绘制边框 + Color borderColor = isColliding_ ? Color(1.0f, 0.0f, 0.0f, 1.0f) + : Color(1.0f, 1.0f, 1.0f, 0.5f); + renderer.drawRect( + Rect(pos.x - width_ / 2, pos.y - height_ / 2, width_, height_), + borderColor, 2.0f + ); } private: - float size_; + float width_, height_; Color color_; bool isColliding_; }; ``` -## 碰撞检测 - -### 查询所有碰撞 +### 2. 执行碰撞检测 ```cpp -void performCollisionDetection() { - // 清除之前的碰撞状态 - for (auto &node : nodes_) { - node->setColliding(false); - } - - // 查询所有碰撞(使用空间索引) - auto collisions = queryCollisions(); - - // 标记碰撞的节点 - for (const auto &[nodeA, nodeB] : collisions) { - if (auto boxA = dynamic_cast(nodeA)) { - boxA->setColliding(true); +class GameScene : public Scene { +public: + void onUpdate(float dt) override { + Scene::onUpdate(dt); + + // 清除之前的碰撞状态 + for (auto& box : boxes_) { + box->setColliding(false); } - if (auto boxB = dynamic_cast(nodeB)) { - boxB->setColliding(true); + + // 使用场景的空间索引查询所有碰撞 + 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); + } } } -} + +private: + std::vector> boxes_; +}; ``` ## 空间索引策略 @@ -83,141 +92,105 @@ void performCollisionDetection() { ### 切换策略 ```cpp -void onEnter() override { - Scene::onEnter(); - - // 启用空间索引 - setSpatialIndexingEnabled(true); - - // 设置空间索引策略 - auto &spatialManager = getSpatialManager(); - spatialManager.setStrategy(SpatialStrategy::QuadTree); // 四叉树 - // 或 - spatialManager.setStrategy(SpatialStrategy::SpatialHash); // 空间哈希 -} +// 获取空间管理器 +auto& spatialManager = getSpatialManager(); -// 切换策略 -void toggleStrategy() { - auto &spatialManager = getSpatialManager(); - SpatialStrategy current = spatialManager.getCurrentStrategy(); - - if (current == SpatialStrategy::QuadTree) { - spatialManager.setStrategy(SpatialStrategy::SpatialHash); - } else { - spatialManager.setStrategy(SpatialStrategy::QuadTree); - } -} +// 切换到四叉树 +spatialManager.setStrategy(SpatialStrategy::QuadTree); + +// 切换到空间哈希 +spatialManager.setStrategy(SpatialStrategy::SpatialHash); + +// 获取当前策略名称 +const char* name = spatialManager.getStrategyName(); ``` ### 策略对比 | 策略 | 适用场景 | 特点 | -|------|----------|------| -| QuadTree | 节点分布不均匀 | 适合稀疏场景 | -| SpatialHash | 节点分布均匀 | 适合密集场景 | +|------|---------|------| +| QuadTree | 节点分布不均匀 | 分层划分,适合稀疏分布 | +| SpatialHash | 节点分布均匀 | 均匀网格,适合密集分布 | -## 完整示例 +## 性能演示 + +`examples/spatial_index_demo/main.cpp` 展示了空间索引的性能优势: ```cpp -class CollisionScene : public Scene { +class SpatialIndexDemoScene : public Scene { public: void onEnter() override { Scene::onEnter(); - // 启用空间索引 - setSpatialIndexingEnabled(true); - - // 创建碰撞节点 + // 创建100个碰撞节点 createNodes(100); + + E2D_LOG_INFO("创建了 {} 个碰撞节点", nodes_.size()); + E2D_LOG_INFO("空间索引已启用: {}", isSpatialIndexingEnabled()); } void onUpdate(float dt) override { Scene::onUpdate(dt); - // 更新节点位置 - for (auto &node : nodes_) { - node->update(dt); + // 更新所有节点位置 + for (auto& node : nodes_) { + node->update(dt, screenWidth_, screenHeight_); } - // 执行碰撞检测 + // 使用空间索引进行碰撞检测 performCollisionDetection(); - } - - void onRender(RenderBackend &renderer) override { - Scene::onRender(renderer); - // 绘制碰撞统计 - std::string text = "Collisions: " + std::to_string(collisionCount_); - // ... + // 按 X 键切换索引策略 + auto& input = Application::instance().input(); + if (input.isButtonPressed(GamepadButton::X)) { + toggleSpatialStrategy(); + } } private: - std::vector> nodes_; - size_t collisionCount_ = 0; - - void createNodes(size_t count) { - for (size_t i = 0; i < count; ++i) { - auto node = makePtr(20.0f, Color(0.5f, 0.5f, 0.9f, 0.7f)); - node->setPosition(randomPosition()); - addChild(node); - nodes_.push_back(node); - } - } - void performCollisionDetection() { - // 清除碰撞状态 - for (auto &node : nodes_) { + // 清除之前的碰撞状态 + for (auto& node : nodes_) { node->setColliding(false); } - // 查询碰撞 + // 使用引擎自带的空间索引进行碰撞检测 auto collisions = queryCollisions(); - collisionCount_ = collisions.size(); - // 标记碰撞节点 - for (const auto &[nodeA, nodeB] : collisions) { - if (auto boxA = dynamic_cast(nodeA)) { + // 标记碰撞的节点 + for (const auto& [nodeA, nodeB] : collisions) { + if (auto boxA = dynamic_cast(nodeA)) { boxA->setColliding(true); } - if (auto boxB = dynamic_cast(nodeB)) { + if (auto boxB = dynamic_cast(nodeB)) { boxB->setColliding(true); } } } -}; -``` - -## 注意事项 - -### 必须调用 Scene::onEnter() - -```cpp -void onEnter() override { - Scene::onEnter(); // 必须调用! - // 否则子节点无法注册到空间索引 - setSpatialIndexingEnabled(true); -} -``` - -### 必须实现 getBoundingBox() - -```cpp -class MyNode : public Node { -public: - MyNode() { - setSpatialIndexed(true); - } - - // 必须实现! - Rect getBoundingBox() const override { - Vec2 pos = getPosition(); - return Rect(pos.x - width_/2, pos.y - height_/2, width_, height_); + void toggleSpatialStrategy() { + auto& spatialManager = getSpatialManager(); + SpatialStrategy currentStrategy = spatialManager.getCurrentStrategy(); + + if (currentStrategy == SpatialStrategy::QuadTree) { + spatialManager.setStrategy(SpatialStrategy::SpatialHash); + E2D_LOG_INFO("切换到空间哈希策略"); + } else { + spatialManager.setStrategy(SpatialStrategy::QuadTree); + E2D_LOG_INFO("切换到四叉树策略"); + } } }; ``` +## 关键要点 + +1. **必须调用 `setSpatialIndexed(true)`** - 启用节点的空间索引 +2. **必须实现 `getBoundingBox()`** - 返回准确的边界框 +3. **在 `onEnter()` 中调用 `Scene::onEnter()`** - 确保节点正确注册到空间索引 +4. **使用 `queryCollisions()`** - 自动利用空间索引优化检测 + ## 下一步 -- [07. UI 系统](07_UI_System.md) -- [08. 音频系统](08_Audio_System.md) +- [07. UI 系统](./07_UI_System.md) - 学习 UI 控件使用 +- [08. 音频系统](./08_Audio_System.md) - 学习音频播放 diff --git a/docs/API_Tutorial/07_UI_System.md b/docs/API_Tutorial/07_UI_System.md index 1b82ad2..848a1f6 100644 --- a/docs/API_Tutorial/07_UI_System.md +++ b/docs/API_Tutorial/07_UI_System.md @@ -1,337 +1,606 @@ -# Extra2D API 教程 - 07. UI 系统 +# 07. UI 系统 -## 按钮 (Button) +Extra2D 提供了一套完整的 UI 系统,支持按钮、文本、标签、复选框、单选按钮、滑块、进度条等常用控件。 -Extra2D 提供 Button 组件用于创建交互式按钮。 +## UI 控件类型 + +``` +Widget (UI控件基类) +├── Button (按钮) +├── Text (文本) +├── Label (标签) +├── CheckBox (复选框) +├── RadioButton (单选按钮) +├── Slider (滑块) +└── ProgressBar (进度条) +``` + +## 坐标空间 + +UI 控件支持三种坐标空间: + +```cpp +enum class CoordinateSpace { + World, // 世界空间 - 随相机移动(默认) + Screen, // 屏幕空间 - 固定位置,不随相机移动 + Camera, // 相机空间 - 相对于相机位置的偏移 +}; +``` + +设置坐标空间: + +```cpp +// 屏幕空间(UI 常用) +button->setCoordinateSpace(CoordinateSpace::Screen); +button->setScreenPosition(100.0f, 50.0f); + +// 或使用链式调用 +button->withCoordinateSpace(CoordinateSpace::Screen) + ->withScreenPosition(100.0f, 50.0f); +``` + +## 通用链式调用方法 + +所有 UI 组件都支持以下链式调用方法: + +```cpp +widget->withPosition(x, y) // 设置位置 + ->withAnchor(x, y) // 设置锚点 (0-1) + ->withCoordinateSpace(space) // 设置坐标空间 + ->withScreenPosition(x, y) // 设置屏幕位置 + ->withCameraOffset(x, y); // 设置相机偏移 +``` + +## 按钮(Button) ### 创建按钮 ```cpp -// 创建按钮 +auto& resources = Application::instance().resources(); +auto font = resources.loadFont("assets/font.ttf", 24); + +// 方式1:简单创建 auto button = Button::create(); +button->setText("点击我"); +button->setFont(font); +button->setPosition(Vec2(400, 300)); -// 设置位置 -button->setPosition(Vec2(640.0f, 360.0f)); +// 方式2:链式调用创建 +auto button = Button::create() + ->withText("点击我") + ->withFont(font) + ->withPosition(400, 300) + ->withSize(200, 60) + ->withTextColor(Colors::White) + ->withBackgroundColor(Colors::Blue, Colors::Green, Colors::Red) + ->withBorder(Colors::White, 2.0f); -// 设置锚点(中心) -button->setAnchor(0.5f, 0.5f); +// 设置点击回调 +button->setOnClick([]() { + E2D_LOG_INFO("按钮被点击!"); +}); -// 添加到场景 addChild(button); ``` -### 设置按钮文本 +### 按钮属性设置 ```cpp -// 加载字体 -auto font = resources.loadFont("assets/font.ttf", 28, true); - -// 设置按钮字体和文本 +// 文本和字体 +button->setText("新文本"); button->setFont(font); -button->setText("点击我"); -button->setTextColor(Colors::Black); -``` +button->setTextColor(Colors::White); -### 设置按钮样式 +// 尺寸和内边距 +button->setCustomSize(200.0f, 60.0f); +button->setPadding(Vec2(10.0f, 5.0f)); -```cpp -// 设置背景颜色(普通、悬停、按下) +// 背景颜色(正常、悬停、按下三种状态) button->setBackgroundColor( - Colors::White, // 普通状态 - Colors::LightGray, // 悬停状态 - Colors::Gray // 按下状态 + Colors::Blue, // 正常状态 + Colors::Green, // 悬停状态 + Colors::Red // 按下状态 ); -// 设置边框 -button->setBorder(Colors::Black, 2.0f); +// 边框 +button->setBorder(Colors::White, 2.0f); -// 设置内边距 -button->setPadding(Vec2(20.0f, 10.0f)); +// 圆角 +button->setRoundedCornersEnabled(true); +button->setCornerRadius(8.0f); -// 设置固定大小 -button->setCustomSize(200.0f, 50.0f); +// 图片背景 +button->setBackgroundImage(normalTex, hoverTex, pressedTex); +button->setBackgroundImageScaleMode(ImageScaleMode::ScaleFit); + +// 悬停光标 +button->setHoverCursor(CursorShape::Hand); ``` -### 透明按钮 +### 图片缩放模式 ```cpp -// 创建透明按钮(仅文本可点击) -auto button = Button::create(); -button->setFont(font); -button->setText("菜单项"); -button->setTextColor(Colors::Black); +enum class ImageScaleMode { + Original, // 使用原图大小 + Stretch, // 拉伸填充 + ScaleFit, // 等比缩放,保持完整显示 + ScaleFill // 等比缩放,填充整个区域(可能裁剪) +}; +``` -// 透明背景 -button->setBackgroundColor( - Colors::Transparent, - Colors::Transparent, - Colors::Transparent +### 透明按钮(菜单项) + +```cpp +// 创建纯文本按钮(透明背景,用于菜单) +auto menuBtn = Button::create(); +menuBtn->setFont(font); +menuBtn->setText("新游戏"); +menuBtn->setTextColor(Colors::Black); +menuBtn->setBackgroundColor( + Colors::Transparent, // 正常 + Colors::Transparent, // 悬停 + Colors::Transparent // 按下 ); - -// 无边框 -button->setBorder(Colors::Transparent, 0.0f); - -// 无内边距 -button->setPadding(Vec2(0.0f, 0.0f)); +menuBtn->setBorder(Colors::Transparent, 0.0f); +menuBtn->setPadding(Vec2(0.0f, 0.0f)); +menuBtn->setCustomSize(200.0f, 40.0f); +menuBtn->setAnchor(0.5f, 0.5f); // 中心锚点 +menuBtn->setPosition(centerX, centerY); +addChild(menuBtn); ``` -## 菜单系统 +## 文本(Text) -### 创建菜单 +### 创建文本 ```cpp -class MenuScene : public Scene { +// 方式1:简单创建 +auto text = Text::create("Hello World", font); +text->setPosition(Vec2(100, 50)); + +// 方式2:链式调用 +auto text = Text::create("Hello World") + ->withFont(font) + ->withPosition(100, 50) + ->withTextColor(Colors::White) + ->withFontSize(24) + ->withAlignment(Alignment::Center); + +addChild(text); +``` + +### 文本属性设置 + +```cpp +// 设置文本 +text->setText("新文本"); +text->setFormat("Score: %d", score); // 格式化文本 + +// 字体和颜色 +text->setFont(font); +text->setTextColor(Colors::White); +text->setFontSize(24); + +// 对齐方式 +text->setAlignment(Alignment::Left); // 水平:左/中/右 +text->setVerticalAlignment(VerticalAlignment::Middle); // 垂直:上/中/下 + +// 获取文本尺寸 +Size size = text->getTextSize(); +float lineHeight = text->getLineHeight(); +``` + +### 对齐方式枚举 + +```cpp +enum class Alignment { Left, Center, Right }; +enum class VerticalAlignment { Top, Middle, Bottom }; +``` + +## 标签(Label) + +Label 是功能更丰富的文本组件,支持阴影、描边、多行文本。 + +### 创建标签 + +```cpp +// 创建标签 +auto label = Label::create("玩家名称", font); +label->setPosition(Vec2(100, 50)); +label->setTextColor(Colors::White); + +// 链式调用 +auto label = Label::create("玩家名称") + ->withFont(font) + ->withPosition(100, 50) + ->withTextColor(Colors::White) + ->withFontSize(24); + +addChild(label); +``` + +### 标签特效 + +```cpp +// 阴影 +label->setShadowEnabled(true); +label->setShadowColor(Colors::Black); +label->setShadowOffset(Vec2(2.0f, 2.0f)); + +// 描边 +label->setOutlineEnabled(true); +label->setOutlineColor(Colors::Black); +label->setOutlineWidth(1.0f); + +// 多行文本 +label->setMultiLine(true); +label->setLineSpacing(1.2f); +label->setMaxWidth(300.0f); // 自动换行宽度 + +// 对齐方式 +label->setHorizontalAlign(HorizontalAlign::Center); +label->setVerticalAlign(VerticalAlign::Middle); +``` + +## 复选框(CheckBox) + +### 创建复选框 + +```cpp +// 方式1:简单创建 +auto checkBox = CheckBox::create(); +checkBox->setPosition(Vec2(100, 200)); +checkBox->setChecked(true); + +// 方式2:带标签 +auto checkBox = CheckBox::create("启用音效"); +checkBox->setPosition(Vec2(100, 200)); + +// 方式3:链式调用 +auto checkBox = CheckBox::create("启用音效") + ->withPosition(100, 200) + ->withFont(font) + ->withTextColor(Colors::White); + +// 状态改变回调 +checkBox->setOnStateChange([](bool checked) { + E2D_LOG_INFO("复选框状态: {}", checked); +}); + +addChild(checkBox); +``` + +### 复选框属性 + +```cpp +// 状态 +checkBox->setChecked(true); +checkBox->toggle(); +bool isChecked = checkBox->isChecked(); + +// 标签 +checkBox->setLabel("新标签"); +checkBox->setFont(font); +checkBox->setTextColor(Colors::White); + +// 外观 +checkBox->setBoxSize(20.0f); // 复选框大小 +checkBox->setSpacing(10.0f); // 复选框与标签间距 +checkBox->setCheckedColor(Colors::Green); +checkBox->setUncheckedColor(Colors::Gray); +checkBox->setCheckMarkColor(Colors::White); +``` + +## 单选按钮(RadioButton) + +### 创建单选按钮 + +```cpp +// 创建单选按钮 +auto radio1 = RadioButton::create("选项 A"); +radio1->setPosition(Vec2(100, 300)); +radio1->setSelected(true); + +auto radio2 = RadioButton::create("选项 B"); +radio2->setPosition(Vec2(100, 340)); + +auto radio3 = RadioButton::create("选项 C"); +radio3->setPosition(Vec2(100, 380)); + +// 添加到组(互斥选择) +radio1->setGroupId(1); +radio2->setGroupId(1); +radio3->setGroupId(1); + +// 或使用 RadioButtonGroup +auto group = std::make_shared(); +group->addButton(radio1.get()); +group->addButton(radio2.get()); +group->addButton(radio3.get()); + +// 选择改变回调 +group->setOnSelectionChange([](RadioButton* selected) { + if (selected) { + E2D_LOG_INFO("选中: {}", selected->getLabel()); + } +}); + +addChild(radio1); +addChild(radio2); +addChild(radio3); +``` + +### 单选按钮属性 + +```cpp +// 状态 +radio->setSelected(true); +bool isSelected = radio->isSelected(); + +// 标签 +radio->setLabel("新标签"); +radio->setFont(font); +radio->setTextColor(Colors::White); + +// 外观 +radio->setCircleSize(16.0f); // 圆形大小 +radio->setSpacing(10.0f); // 圆形与标签间距 +radio->setSelectedColor(Colors::Green); +radio->setUnselectedColor(Colors::Gray); +radio->setDotColor(Colors::White); + +// 分组 +radio->setGroupId(1); // 相同 groupId 的按钮互斥 +``` + +## 滑块(Slider) + +### 创建滑块 + +```cpp +// 方式1:简单创建 +auto slider = Slider::create(); +slider->setPosition(Vec2(200, 400)); +slider->setRange(0.0f, 100.0f); +slider->setValue(50.0f); + +// 方式2:带初始值创建 +auto slider = Slider::create(0.0f, 100.0f, 50.0f); + +// 方式3:链式调用 +auto slider = Slider::create() + ->withPosition(200, 400) + ->withSize(200, 20) + ->withMinValue(0.0f) + ->withMaxValue(100.0f) + ->withValue(50.0f); + +// 值改变回调 +slider->setOnValueChange([](float value) { + E2D_LOG_INFO("滑块值: {}", value); +}); + +// 拖动开始/结束回调 +slider->setOnDragStart([]() { + E2D_LOG_INFO("开始拖动"); +}); +slider->setOnDragEnd([]() { + E2D_LOG_INFO("结束拖动"); +}); + +addChild(slider); +``` + +### 滑块属性 + +```cpp +// 值和范围 +slider->setRange(0.0f, 100.0f); +slider->setValue(50.0f); +slider->setStep(5.0f); // 步进值,0表示无步进 +float value = slider->getValue(); +float min = slider->getMin(); +float max = slider->getMax(); + +// 方向 +slider->setVertical(false); // false=水平, true=垂直 + +// 外观 +slider->setTrackSize(4.0f); // 轨道粗细 +slider->setThumbSize(16.0f); // 滑块大小 + +// 颜色 +slider->setTrackColor(Colors::Gray); +slider->setFillColor(Colors::Green); +slider->setThumbColor(Colors::White); +slider->setThumbHoverColor(Colors::Yellow); +slider->setThumbPressedColor(Colors::Orange); + +// 显示选项 +slider->setShowThumb(true); // 显示滑块 +slider->setShowFill(true); // 显示填充 +slider->setTextEnabled(true); // 显示数值文本 +slider->setTextFormat("{:.0f}%"); // 数值格式 +slider->setFont(font); +slider->setTextColor(Colors::White); +``` + +## 进度条(ProgressBar) + +### 创建进度条 + +```cpp +// 方式1:简单创建 +auto progressBar = ProgressBar::create(); +progressBar->setPosition(Vec2(200, 500)); +progressBar->setSize(300.0f, 30.0f); +progressBar->setValue(75.0f); // 75% + +// 方式2:带范围创建 +auto progressBar = ProgressBar::create(0.0f, 100.0f, 75.0f); + +// 方式3:链式调用 +auto progressBar = ProgressBar::create() + ->withPosition(200, 500) + ->withSize(300, 30) + ->withProgress(0.75f); // 0-1 的进度值 + +addChild(progressBar); +``` + +### 进度条属性 + +```cpp +// 值和范围 +progressBar->setRange(0.0f, 100.0f); +progressBar->setValue(75.0f); +float value = progressBar->getValue(); +float percent = progressBar->getPercent(); // 0.0-1.0 + +// 方向 +progressBar->setDirection(Direction::LeftToRight); +// Direction::LeftToRight, RightToLeft, BottomToTop, TopToBottom + +// 颜色 +progressBar->setBackgroundColor(Colors::DarkGray); +progressBar->setFillColor(Colors::Green); + +// 渐变填充 +progressBar->setGradientFillEnabled(true); +progressBar->setFillColorEnd(Colors::LightGreen); + +// 分段颜色(根据进度显示不同颜色) +progressBar->setSegmentedColorsEnabled(true); +progressBar->addColorSegment(0.3f, Colors::Red); // <30% 红色 +progressBar->addColorSegment(0.7f, Colors::Yellow); // 30-70% 黄色 +// >70% 使用默认填充色(绿色) + +// 圆角 +progressBar->setRoundedCornersEnabled(true); +progressBar->setCornerRadius(8.0f); + +// 边框 +progressBar->setBorderEnabled(true); +progressBar->setBorderColor(Colors::White); +progressBar->setBorderWidth(2.0f); +progressBar->setPadding(2.0f); + +// 文本 +progressBar->setTextEnabled(true); +progressBar->setTextFormat("{:.0f}%"); +progressBar->setFont(font); +progressBar->setTextColor(Colors::White); + +// 动画效果 +progressBar->setAnimatedChangeEnabled(true); +progressBar->setAnimationSpeed(5.0f); + +// 延迟显示效果 +progressBar->setDelayedDisplayEnabled(true); +progressBar->setDelayTime(0.5f); +progressBar->setDelayedFillColor(Colors::Yellow); + +// 条纹效果 +progressBar->setStripedEnabled(true); +progressBar->setStripeColor(Colors::White); +progressBar->setStripeSpeed(1.0f); +``` + +## 完整示例:设置场景 + +```cpp +class SettingsScene : public Scene { public: void onEnter() override { Scene::onEnter(); - auto &resources = Application::instance().resources(); - font_ = resources.loadFont("assets/font.ttf", 28, true); + auto& resources = Application::instance().resources(); + font_ = resources.loadFont("assets/font.ttf", 24); - float centerX = 640.0f; - float startY = 300.0f; + // 标题 + auto title = Text::create("设置", font_); + title->setPosition(Vec2(400, 100)); + title->setAlignment(Alignment::Center); + addChild(title); - // 创建菜单按钮 - createMenuButton("开始游戏", centerX, startY, 0); - createMenuButton("继续游戏", centerX, startY + 50.0f, 1); - createMenuButton("退出", centerX, startY + 100.0f, 2); + // 音效开关 + auto soundLabel = Label::create("音效", font_); + soundLabel->setPosition(Vec2(200, 200)); + addChild(soundLabel); - menuCount_ = 3; - selectedIndex_ = 0; - updateMenuColors(); - } - - void onUpdate(float dt) override { - Scene::onUpdate(dt); + soundCheck_ = CheckBox::create(); + soundCheck_->setPosition(Vec2(350, 200)); + soundCheck_->setChecked(true); + soundCheck_->setOnStateChange([this](bool checked) { + E2D_LOG_INFO("音效: {}", checked ? "开启" : "关闭"); + }); + addChild(soundCheck_); - auto &input = Application::instance().input(); + // 音量滑块 + auto volumeLabel = Label::create("音量", font_); + volumeLabel->setPosition(Vec2(200, 280)); + addChild(volumeLabel); - // 方向键切换选择 - if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_DPAD_UP)) { - selectedIndex_ = (selectedIndex_ - 1 + menuCount_) % menuCount_; - updateMenuColors(); - } else if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_DPAD_DOWN)) { - selectedIndex_ = (selectedIndex_ + 1) % menuCount_; - updateMenuColors(); - } + volumeSlider_ = Slider::create(0.0f, 1.0f, 0.8f); + volumeSlider_->setPosition(Vec2(350, 280)); + volumeSlider_->setSize(200, 20); + volumeSlider_->setOnValueChange([](float value) { + E2D_LOG_INFO("音量: {:.0f}%", value * 100); + }); + addChild(volumeSlider_); - // A键确认 - if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_A)) { - executeMenuItem(); - } + // 难度选择 + auto difficultyLabel = Label::create("难度", font_); + difficultyLabel->setPosition(Vec2(200, 360)); + addChild(difficultyLabel); + + auto easyRadio = RadioButton::create("简单"); + easyRadio->setPosition(Vec2(350, 360)); + easyRadio->setSelected(true); + easyRadio->setGroupId(1); + + auto normalRadio = RadioButton::create("普通"); + normalRadio->setPosition(Vec2(450, 360)); + normalRadio->setGroupId(1); + + auto hardRadio = RadioButton::create("困难"); + hardRadio->setPosition(Vec2(550, 360)); + hardRadio->setGroupId(1); + + addChild(easyRadio); + addChild(normalRadio); + addChild(hardRadio); + + // 返回按钮 + auto backBtn = Button::create("返回", font_); + backBtn->setPosition(Vec2(400, 500)); + backBtn->setCustomSize(150.0f, 50.0f); + backBtn->setBackgroundColor( + Colors::Blue, + Colors::LightBlue, + Colors::DarkBlue + ); + backBtn->setOnClick([]() { + Application::instance().scenes().popScene(); + }); + addChild(backBtn); } private: Ptr font_; - std::vector> buttons_; - int selectedIndex_ = 0; - int menuCount_ = 0; - - void createMenuButton(const std::string &text, float x, float y, int index) { - auto button = Button::create(); - button->setFont(font_); - button->setText(text); - button->setTextColor(Colors::Black); - button->setBackgroundColor( - Colors::Transparent, - Colors::Transparent, - Colors::Transparent - ); - button->setBorder(Colors::Transparent, 0.0f); - button->setPadding(Vec2(0.0f, 0.0f)); - button->setCustomSize(200.0f, 40.0f); - button->setAnchor(0.5f, 0.5f); - button->setPosition(x, y); - addChild(button); - buttons_.push_back(button); - } - - void updateMenuColors() { - for (int i = 0; i < buttons_.size(); ++i) { - if (buttons_[i]) { - buttons_[i]->setTextColor( - i == selectedIndex_ ? Colors::Red : Colors::Black - ); - } - } - } - - void executeMenuItem() { - switch (selectedIndex_) { - case 0: startGame(); break; - case 1: continueGame(); break; - case 2: exitGame(); break; - } - } - - void startGame() { - // 切换到游戏场景 - } - - void continueGame() { - // 继续游戏 - } - - void exitGame() { - Application::instance().quit(); - } + Ptr soundCheck_; + Ptr volumeSlider_; }; ``` -## 绘制文字 +## 最佳实践 -### 基本文字绘制 - -```cpp -void onRender(RenderBackend &renderer) override { - Scene::onRender(renderer); - - if (font_) { - // 绘制文字 - renderer.drawText(*font_, "Hello World", - Vec2(100.0f, 100.0f), Colors::White); - - // 绘制带颜色的文字 - renderer.drawText(*font_, "红色文字", - Vec2(100.0f, 150.0f), Colors::Red); - } -} -``` - -### 格式化文字 - -```cpp -void onRender(RenderBackend &renderer) override { - Scene::onRender(renderer); - - if (infoFont_) { - auto &app = Application::instance(); - - // 绘制 FPS - std::stringstream ss; - ss << "FPS: " << app.fps(); - renderer.drawText(*infoFont_, ss.str(), - Vec2(50.0f, 50.0f), Colors::Yellow); - - // 绘制节点数量 - ss.str(""); - ss << "Nodes: " << nodes_.size(); - renderer.drawText(*infoFont_, ss.str(), - Vec2(50.0f, 80.0f), Colors::White); - } -} -``` - -## 完整示例 - -```cpp -class StartScene : public Scene { -public: - void onEnter() override { - Scene::onEnter(); - - auto &app = Application::instance(); - auto &resources = app.resources(); - - // 加载背景 - auto bgTex = resources.loadTexture("assets/background.jpg"); - if (bgTex) { - auto bg = Sprite::create(bgTex); - bg->setAnchor(0.0f, 0.0f); - addChild(bg); - } - - // 加载字体 - font_ = resources.loadFont("assets/font.ttf", 32, true); - - float centerX = app.getConfig().width / 2.0f; - - // 创建标题 - titleBtn_ = Button::create(); - titleBtn_->setFont(font_); - titleBtn_->setText("游戏标题"); - titleBtn_->setTextColor(Colors::Gold); - titleBtn_->setBackgroundColor( - Colors::Transparent, - Colors::Transparent, - Colors::Transparent - ); - titleBtn_->setBorder(Colors::Transparent, 0.0f); - titleBtn_->setAnchor(0.5f, 0.5f); - titleBtn_->setPosition(centerX, 200.0f); - addChild(titleBtn_); - - // 创建菜单按钮 - createMenuButton("新游戏", centerX, 350.0f, 0); - createMenuButton("继续", centerX, 400.0f, 1); - createMenuButton("退出", centerX, 450.0f, 2); - - menuCount_ = 3; - updateMenuColors(); - } - - void onUpdate(float dt) override { - Scene::onUpdate(dt); - - auto &input = Application::instance().input(); - - if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_DPAD_UP)) { - selectedIndex_ = (selectedIndex_ - 1 + menuCount_) % menuCount_; - updateMenuColors(); - } else if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_DPAD_DOWN)) { - selectedIndex_ = (selectedIndex_ + 1) % menuCount_; - updateMenuColors(); - } - - if (input.isButtonPressed(SDL_CONTROLLER_BUTTON_A)) { - executeMenuItem(); - } - } - -private: - Ptr font_; - Ptr