Extra2D/docs/API_Tutorial/02_Scene_System.md

487 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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