feat(场景系统): 重构场景过渡系统并添加视口适配功能

重构过渡系统为基于 TransitionScene 的场景中介模式,新增多种过渡效果
为 BaseScene 添加视口适配功能,支持居中显示游戏内容
将按钮组件扩展支持切换模式,优化 UI 系统文档
更新示例项目使用新过渡系统和视口适配

新增 TransitionScene 基类及多种过渡效果实现:
- TransitionFadeScene 淡入淡出过渡
- TransitionSlideScene 滑动过渡
- TransitionScaleScene 缩放过渡
- TransitionFlipScene 翻页过渡
- TransitionBoxScene 方块过渡

BaseScene 提供视口适配功能:
- 计算居中视口参数
- 处理窗口大小变化
- 支持作为 TransitionScene 子场景渲染

UI 系统改进:
- Button 组件新增切换模式支持
- 合并 ToggleImageButton 功能到 Button
- 更新文档和示例使用切换按钮

示例项目更新:
- flappy_bird 使用新过渡系统
- push_box 实现完整场景结构和视口适配
- 更新场景切换 API 使用方式
This commit is contained in:
ChestnutYueyue 2026-02-13 13:56:18 +08:00
parent 4f641a2854
commit b78c493590
35 changed files with 2275 additions and 1481 deletions

View File

@ -90,7 +90,7 @@ public:
// 便捷方法
// ------------------------------------------------------------------------
void enterScene(Ptr<class Scene> scene);
void enterScene(Ptr<class Scene> scene, Ptr<class Transition> transition);
void enterScene(Ptr<class Scene> scene, Ptr<class TransitionScene> transitionScene);
float deltaTime() const { return deltaTime_; }
float totalTime() const { return totalTime_; }

View File

@ -29,7 +29,12 @@
#include <extra2d/scene/sprite.h>
#include <extra2d/scene/shape_node.h>
#include <extra2d/scene/scene_manager.h>
#include <extra2d/scene/transition.h>
#include <extra2d/scene/transition_scene.h>
#include <extra2d/scene/transition_fade_scene.h>
#include <extra2d/scene/transition_slide_scene.h>
#include <extra2d/scene/transition_scale_scene.h>
#include <extra2d/scene/transition_flip_scene.h>
#include <extra2d/scene/transition_box_scene.h>
// Animation
#include <extra2d/animation/sprite_frame.h>

View File

@ -56,7 +56,7 @@ public:
// 渲染和更新
// ------------------------------------------------------------------------
void renderScene(RenderBackend &renderer);
void renderContent(RenderBackend &renderer);
virtual void renderContent(RenderBackend &renderer);
void updateScene(float dt);
void collectRenderCommands(std::vector<RenderCommand> &commands,
int parentZOrder = 0) override;
@ -92,7 +92,12 @@ protected:
void onEnter() override;
void onExit() override;
// 过渡场景生命周期回调(供 TransitionScene 使用)
virtual void onExitTransitionDidStart() {}
virtual void onEnterTransitionDidFinish() {}
friend class SceneManager;
friend class TransitionScene;
private:
Color backgroundColor_ = Colors::Black;

View File

@ -2,6 +2,7 @@
#include <extra2d/core/types.h>
#include <extra2d/scene/scene.h>
#include <extra2d/scene/transition_scene.h>
#include <functional>
#include <stack>
#include <string>
@ -12,21 +13,7 @@ namespace extra2d {
// 前向声明
struct RenderCommand;
class Transition;
// ============================================================================
// 场景切换特效类型
// ============================================================================
enum class TransitionType {
None,
Fade,
SlideLeft,
SlideRight,
SlideUp,
SlideDown,
Scale,
Flip
};
class TransitionScene;
// ============================================================================
// 场景管理器 - 管理场景的生命周期和切换
@ -116,27 +103,27 @@ public:
// 场景切换(供 Application 使用)
void enterScene(Ptr<Scene> scene);
void enterScene(Ptr<Scene> scene, Ptr<class Transition> transition);
void enterScene(Ptr<Scene> scene, Ptr<TransitionScene> transitionScene);
private:
void doSceneSwitch();
void startTransition(Ptr<Scene> from, Ptr<Scene> to, TransitionType type,
float duration, Function<void()> stackAction);
void updateTransition(float dt);
void finishTransition();
void dispatchPointerEvents(Scene &scene);
// 创建过渡场景
Ptr<TransitionScene> createTransitionScene(TransitionType type,
float duration,
Ptr<Scene> inScene);
std::stack<Ptr<Scene>> sceneStack_;
std::unordered_map<std::string, Ptr<Scene>> namedScenes_;
// Transition state
bool isTransitioning_ = false;
TransitionType currentTransition_ = TransitionType::None;
float transitionDuration_ = 0.0f;
float transitionElapsed_ = 0.0f;
Ptr<Scene> outgoingScene_;
Ptr<Scene> incomingScene_;
Ptr<Transition> activeTransition_;
Ptr<TransitionScene> activeTransitionScene_;
Function<void()> transitionStackAction_;
TransitionCallback transitionCallback_;

View File

@ -1,137 +0,0 @@
#pragma once
#include <extra2d/core/color.h>
#include <extra2d/core/types.h>
#include <extra2d/scene/scene.h>
#include <functional>
namespace extra2d {
// ============================================================================
// 过渡方向
// ============================================================================
enum class TransitionDirection { Left, Right, Up, Down };
// ============================================================================
// 过渡效果基类
// ============================================================================
class Transition : public std::enable_shared_from_this<Transition> {
public:
using FinishCallback = std::function<void()>;
Transition(float duration);
virtual ~Transition() = default;
// 开始过渡
void start(Ptr<Scene> from, Ptr<Scene> to);
// 更新过渡进度
void update(float dt);
// 渲染过渡效果
virtual void render(RenderBackend &renderer);
// 是否完成
bool isFinished() const { return isFinished_; }
// 获取进度 [0, 1]
float getProgress() const { return progress_; }
// 获取淡入淡出进度 (0->1 for fade in, 1->0 for fade out)
float getFadeInAlpha() const;
float getFadeOutAlpha() const;
// 设置完成回调
void setFinishCallback(FinishCallback callback) {
finishCallback_ = callback;
}
// 获取源场景和目标场景
Ptr<Scene> getOutgoingScene() const { return outgoingScene_; }
Ptr<Scene> getIncomingScene() const { return incomingScene_; }
protected:
// 子类实现具体的渲染效果
virtual void onRenderTransition(RenderBackend &renderer, float progress) = 0;
// 过渡完成时调用
virtual void onFinish();
float duration_;
float elapsed_;
float progress_;
bool isFinished_;
bool isStarted_;
Ptr<Scene> outgoingScene_;
Ptr<Scene> incomingScene_;
FinishCallback finishCallback_;
};
// ============================================================================
// 淡入淡出过渡
// ============================================================================
class FadeTransition : public Transition {
public:
FadeTransition(float duration);
protected:
void onRenderTransition(RenderBackend &renderer, float progress) override;
};
// ============================================================================
// 滑动过渡
// ============================================================================
class SlideTransition : public Transition {
public:
SlideTransition(float duration, TransitionDirection direction);
protected:
void onRenderTransition(RenderBackend &renderer, float progress) override;
private:
TransitionDirection direction_;
};
// ============================================================================
// 缩放过渡
// ============================================================================
class ScaleTransition : public Transition {
public:
ScaleTransition(float duration);
protected:
void onRenderTransition(RenderBackend &renderer, float progress) override;
};
// ============================================================================
// 翻页过渡
// ============================================================================
class FlipTransition : public Transition {
public:
enum class Axis { Horizontal, Vertical };
FlipTransition(float duration, Axis axis = Axis::Horizontal);
protected:
void onRenderTransition(RenderBackend &renderer, float progress) override;
private:
Axis axis_;
};
// ============================================================================
// 马赛克/方块过渡
// ============================================================================
class BoxTransition : public Transition {
public:
BoxTransition(float duration, int divisions = 8);
protected:
void onRenderTransition(RenderBackend &renderer, float progress) override;
private:
int divisions_;
};
} // namespace extra2d

View File

@ -0,0 +1,34 @@
#pragma once
#include <extra2d/scene/transition_scene.h>
namespace extra2d {
// ============================================================================
// 方块/马赛克过渡场景
// 实现原理:
// 1. 将屏幕分成多个方块
// 2. 方块逐个消失,显示新场景
// ============================================================================
class TransitionBoxScene : public TransitionScene {
public:
/**
* @brief
* @param duration
* @param inScene
* @param divisions 8 8x8
*/
TransitionBoxScene(float duration, Ptr<Scene> inScene, int divisions = 8);
static Ptr<TransitionBoxScene> create(float duration, Ptr<Scene> inScene,
int divisions = 8);
protected:
void onTransitionStart() override;
void renderContent(RenderBackend &renderer) override;
private:
int divisions_;
};
} // namespace extra2d

View File

@ -0,0 +1,54 @@
#pragma once
#include <extra2d/scene/transition_scene.h>
#include <extra2d/scene/sprite.h>
#include <extra2d/core/color.h>
namespace extra2d {
// ============================================================================
// 淡入淡出过渡场景
// 实现原理:
// 1. 创建一个纯色精灵作为遮罩层
// 2. 第一阶段:遮罩从透明淡入到不透明(黑屏),同时显示旧场景
// 3. 切换显示新场景
// 4. 第二阶段:遮罩从不透明淡出到透明,显示新场景
// ============================================================================
class TransitionFadeScene : public TransitionScene {
public:
/**
* @brief
* @param duration
* @param inScene
* @param color
*/
TransitionFadeScene(float duration, Ptr<Scene> inScene,
const Color &color = Colors::Black);
static Ptr<TransitionFadeScene> create(float duration, Ptr<Scene> inScene,
const Color &color = Colors::Black);
protected:
/**
* @brief
*
*/
void onTransitionStart() override;
/**
* @brief
*
*/
void renderContent(RenderBackend &renderer) override;
private:
/**
* @brief 退
*/
void hideOutShowIn();
Color maskColor_; // 遮罩颜色
bool hasSwitched_ = false; // 是否已经切换场景
};
} // namespace extra2d

View File

@ -0,0 +1,37 @@
#pragma once
#include <extra2d/scene/transition_scene.h>
namespace extra2d {
// ============================================================================
// 翻页过渡场景
// 实现原理:
// 1. 前半段:旧场景翻转消失
// 2. 后半段:新场景翻转出现
// ============================================================================
class TransitionFlipScene : public TransitionScene {
public:
enum class Axis { Horizontal, Vertical };
/**
* @brief
* @param duration
* @param inScene
* @param axis
*/
TransitionFlipScene(float duration, Ptr<Scene> inScene,
Axis axis = Axis::Horizontal);
static Ptr<TransitionFlipScene> create(float duration, Ptr<Scene> inScene,
Axis axis = Axis::Horizontal);
protected:
void onTransitionStart() override;
void renderContent(RenderBackend &renderer) override;
private:
Axis axis_;
};
} // namespace extra2d

View File

@ -0,0 +1,29 @@
#pragma once
#include <extra2d/scene/transition_scene.h>
namespace extra2d {
// ============================================================================
// 缩放过渡场景
// 实现原理:
// 1. 旧场景缩小消失
// 2. 新场景放大出现
// ============================================================================
class TransitionScaleScene : public TransitionScene {
public:
/**
* @brief
* @param duration
* @param inScene
*/
TransitionScaleScene(float duration, Ptr<Scene> inScene);
static Ptr<TransitionScaleScene> create(float duration, Ptr<Scene> inScene);
protected:
void onTransitionStart() override;
void renderContent(RenderBackend &renderer) override;
};
} // namespace extra2d

View File

@ -0,0 +1,127 @@
#pragma once
#include <extra2d/scene/scene.h>
#include <functional>
namespace extra2d {
// ============================================================================
// 过渡方向
// ============================================================================
enum class TransitionDirection { Left, Right, Up, Down };
// ============================================================================
// 过渡效果类型
// ============================================================================
enum class TransitionType {
None,
Fade,
SlideLeft,
SlideRight,
SlideUp,
SlideDown,
Scale,
Flip,
Box
};
// ============================================================================
// 过渡场景基类 - 继承自 Scene作为中介场景管理过渡效果
// 设计参考 Cocos2d-x 的 TransitionScene
// ============================================================================
class TransitionScene : public Scene {
public:
using FinishCallback = std::function<void()>;
/**
* @brief
* @param duration
* @param inScene
*/
TransitionScene(float duration, Ptr<Scene> inScene);
~TransitionScene() override = default;
// ------------------------------------------------------------------------
// 场景管理
// ------------------------------------------------------------------------
/**
* @brief
*/
Ptr<Scene> getInScene() const { return inScene_; }
/**
* @brief 退
*/
Ptr<Scene> getOutScene() const { return outScene_; }
/**
* @brief 退 SceneManager
*/
void setOutScene(Ptr<Scene> outScene) { outScene_ = outScene; }
/**
* @brief
*/
void setFinishCallback(FinishCallback callback) { finishCallback_ = callback; }
/**
* @brief
*/
float getDuration() const { return duration_; }
/**
* @brief [0, 1]
*/
float getProgress() const { return progress_; }
/**
* @brief
*/
bool isFinished() const { return isFinished_; }
/**
* @brief SceneManager
*/
void finish();
// ------------------------------------------------------------------------
// 渲染 - 在 TransitionScene 上渲染新旧两个子场景
// ------------------------------------------------------------------------
void renderContent(RenderBackend &renderer) override;
// ------------------------------------------------------------------------
// 生命周期
// ------------------------------------------------------------------------
void onEnter() override;
void onExit() override;
protected:
/**
* @brief
* onEnter finish()
*/
virtual void onTransitionStart() = 0;
/**
* @brief
*/
virtual void drawOutScene(RenderBackend &renderer);
/**
* @brief
*/
virtual void drawInScene(RenderBackend &renderer);
float duration_;
float elapsed_ = 0.0f;
float progress_ = 0.0f;
bool isFinished_ = false;
Ptr<Scene> inScene_; // 要进入的场景
Ptr<Scene> outScene_; // 要退出的场景
FinishCallback finishCallback_;
};
} // namespace extra2d

View File

@ -0,0 +1,35 @@
#pragma once
#include <extra2d/scene/transition_scene.h>
namespace extra2d {
// ============================================================================
// 滑动过渡场景
// 实现原理:
// 1. 旧场景向指定方向滑出
// 2. 新场景从相反方向滑入
// ============================================================================
class TransitionSlideScene : public TransitionScene {
public:
/**
* @brief
* @param duration
* @param inScene
* @param direction
*/
TransitionSlideScene(float duration, Ptr<Scene> inScene,
TransitionDirection direction);
static Ptr<TransitionSlideScene> create(float duration, Ptr<Scene> inScene,
TransitionDirection direction);
protected:
void onTransitionStart() override;
void renderContent(RenderBackend &renderer) override;
private:
TransitionDirection direction_;
};
} // namespace extra2d

View File

@ -95,6 +95,8 @@ public:
// 边框设置
// ------------------------------------------------------------------------
void setBorder(const Color &color, float width);
float getBorderWidth() const { return borderWidth_; }
Color getBorderColor() const { return borderColor_; }
// ------------------------------------------------------------------------
// 图片背景设置
@ -102,6 +104,22 @@ public:
void setBackgroundImage(Ptr<Texture> normal, Ptr<Texture> hover = nullptr,
Ptr<Texture> pressed = nullptr);
void setBackgroundImage(Ptr<Texture> texture, const Rect &rect);
/**
* @brief
* @param offNormal
* @param onNormal
* @param offHover
* @param onHover
* @param offPressed
* @param onPressed
*/
void setStateBackgroundImage(Ptr<Texture> offNormal, Ptr<Texture> onNormal,
Ptr<Texture> offHover = nullptr,
Ptr<Texture> onHover = nullptr,
Ptr<Texture> offPressed = nullptr,
Ptr<Texture> onPressed = nullptr);
void setBackgroundImageScaleMode(ImageScaleMode mode);
void setCustomSize(const Vec2 &size);
void setCustomSize(float width, float height);
@ -131,6 +149,63 @@ public:
// ------------------------------------------------------------------------
void setOnClick(Function<void()> callback);
// ------------------------------------------------------------------------
// 切换模式支持Toggle Button
// ------------------------------------------------------------------------
/**
* @brief
* @param enabled true on/off
*/
void setToggleMode(bool enabled);
/**
* @brief
* @return true
*/
bool isToggleMode() const { return toggleMode_; }
/**
* @brief
* @param on true false
*/
void setOn(bool on);
/**
* @brief
* @return true false
*/
bool isOn() const { return isOn_; }
/**
* @brief
*/
void toggle();
/**
* @brief
* @param callback
*/
void setOnStateChange(Function<void(bool)> callback);
// ------------------------------------------------------------------------
// 状态文字设置(用于切换按钮)
// ------------------------------------------------------------------------
/**
* @brief
* @param textOff
* @param textOn
*/
void setStateText(const std::string &textOff, const std::string &textOn);
/**
* @brief
* @param colorOff
* @param colorOn
*/
void setStateTextColor(const Color &colorOff, const Color &colorOn);
Rect getBoundingBox() const override;
protected:
@ -170,6 +245,12 @@ private:
bool useImageBackground_ = false;
bool useTextureRect_ = false;
// 切换按钮状态图片
Ptr<Texture> imgOffNormal_, imgOnNormal_;
Ptr<Texture> imgOffHover_, imgOnHover_;
Ptr<Texture> imgOffPressed_, imgOnPressed_;
bool useStateImages_ = false;
// 边框
Color borderColor_ = Color(0.6f, 0.6f, 0.6f, 1.0f);
float borderWidth_ = 1.0f;
@ -188,48 +269,10 @@ private:
bool hovered_ = false;
bool pressed_ = false;
Function<void()> onClick_;
};
// ============================================================================
// 切换按钮 - 点击切换两种状态(支持图片和文字)
// ============================================================================
class ToggleImageButton : public Button {
public:
ToggleImageButton();
~ToggleImageButton() override = default;
static Ptr<ToggleImageButton> create();
// 设置两种状态的图片
void setStateImages(Ptr<Texture> stateOffNormal, Ptr<Texture> stateOnNormal,
Ptr<Texture> stateOffHover = nullptr,
Ptr<Texture> stateOnHover = nullptr,
Ptr<Texture> stateOffPressed = nullptr,
Ptr<Texture> stateOnPressed = nullptr);
// 设置两种状态的文字
void setStateText(const std::string &textOff, const std::string &textOn);
// 设置两种状态的文字颜色
void setStateTextColor(const Color &colorOff, const Color &colorOn);
// 获取/设置当前状态
bool isOn() const { return isOn_; }
void setOn(bool on);
void toggle();
// 设置状态改变回调
void setOnStateChange(Function<void(bool)> callback);
protected:
void onDrawWidget(RenderBackend &renderer) override;
private:
// 状态图片
Ptr<Texture> imgOffNormal_, imgOnNormal_;
Ptr<Texture> imgOffHover_, imgOnHover_;
Ptr<Texture> imgOffPressed_, imgOnPressed_;
// 切换模式相关
bool toggleMode_ = false;
bool isOn_ = false;
Function<void(bool)> onStateChange_;
// 状态文字
std::string textOff_, textOn_;
@ -240,8 +283,7 @@ private:
Color textColorOn_ = Colors::White;
bool useStateTextColor_ = false;
bool isOn_ = false;
Function<void(bool)> onStateChange_;
Function<void()> onClick_;
};
} // namespace extra2d

View File

@ -364,11 +364,11 @@ Camera &Application::camera() { return *camera_; }
void Application::enterScene(Ptr<Scene> scene) { enterScene(scene, nullptr); }
void Application::enterScene(Ptr<Scene> scene,
Ptr<class Transition> transition) {
Ptr<class TransitionScene> transitionScene) {
if (sceneManager_ && scene) {
scene->setViewportSize(static_cast<float>(window_->getWidth()),
static_cast<float>(window_->getHeight()));
sceneManager_->enterScene(scene, transition);
sceneManager_->enterScene(scene, transitionScene);
}
}

View File

@ -4,57 +4,16 @@
#include <extra2d/graphics/render_command.h>
#include <extra2d/platform/input.h>
#include <extra2d/scene/scene_manager.h>
#include <extra2d/scene/transition.h>
#include <extra2d/scene/transition_box_scene.h>
#include <extra2d/scene/transition_fade_scene.h>
#include <extra2d/scene/transition_flip_scene.h>
#include <extra2d/scene/transition_scale_scene.h>
#include <extra2d/scene/transition_scene.h>
#include <extra2d/scene/transition_slide_scene.h>
#include <extra2d/utils/logger.h>
namespace extra2d {
// ============================================================================
// Transition 工厂映射 - 使用函数指针数组替代 switch
// ============================================================================
using TransitionFactory = Ptr<Transition> (*)(float);
static Ptr<Transition> createFadeTransition(float duration) {
return makePtr<FadeTransition>(duration);
}
static Ptr<Transition> createSlideLeftTransition(float duration) {
return makePtr<SlideTransition>(duration, TransitionDirection::Left);
}
static Ptr<Transition> createSlideRightTransition(float duration) {
return makePtr<SlideTransition>(duration, TransitionDirection::Right);
}
static Ptr<Transition> createSlideUpTransition(float duration) {
return makePtr<SlideTransition>(duration, TransitionDirection::Up);
}
static Ptr<Transition> createSlideDownTransition(float duration) {
return makePtr<SlideTransition>(duration, TransitionDirection::Down);
}
static Ptr<Transition> createScaleTransition(float duration) {
return makePtr<ScaleTransition>(duration);
}
static Ptr<Transition> createFlipTransition(float duration) {
return makePtr<FlipTransition>(duration);
}
// 工厂函数指针数组,索引对应 TransitionType 枚举值
static constexpr TransitionFactory TRANSITION_FACTORIES[] = {
createFadeTransition, // TransitionType::Fade = 0
createSlideLeftTransition, // TransitionType::SlideLeft = 1
createSlideRightTransition,// TransitionType::SlideRight = 2
createSlideUpTransition, // TransitionType::SlideUp = 3
createSlideDownTransition, // TransitionType::SlideDown = 4
createScaleTransition, // TransitionType::Scale = 5
createFlipTransition // TransitionType::Flip = 6
};
static constexpr size_t TRANSITION_FACTORY_COUNT = sizeof(TRANSITION_FACTORIES) / sizeof(TRANSITION_FACTORIES[0]);
namespace {
Node *hitTestTopmost(const Ptr<Node> &node, const Vec2 &worldPos) {
@ -150,17 +109,22 @@ void SceneManager::enterScene(Ptr<Scene> scene) {
}
void SceneManager::enterScene(Ptr<Scene> scene,
Ptr<class Transition> transition) {
Ptr<TransitionScene> transitionScene) {
if (!scene || isTransitioning_) {
return;
}
if (!transition) {
// 如果没有过渡场景,使用无过渡切换
if (!transitionScene) {
enterScene(scene);
return;
}
auto current = getCurrentScene();
if (!current) {
enterScene(scene);
return;
}
// 在过渡开始前,发送 UIHoverExit 给当前悬停的节点,重置按钮状态
if (hoverTarget_) {
@ -173,32 +137,39 @@ void SceneManager::enterScene(Ptr<Scene> scene,
captureTarget_ = nullptr;
hasLastPointerWorld_ = false;
transition->start(current, scene);
outgoingScene_ = current;
incomingScene_ = scene;
activeTransition_ = transition;
currentTransition_ = TransitionType::None;
// 设置过渡场景
transitionScene->setOutScene(current);
transitionScene->setFinishCallback([this]() { finishTransition(); });
// 暂停当前场景
current->pause();
// 推入过渡场景(作为中介场景)
transitionScene->onEnter();
transitionScene->onAttachToScene(transitionScene.get());
sceneStack_.push(transitionScene);
isTransitioning_ = true;
transitionStackAction_ = [this]() {
activeTransitionScene_ = transitionScene;
transitionStackAction_ = [this, transitionScene]() {
// 退出旧场景
auto outgoing = outgoingScene_;
if (!sceneStack_.empty() && outgoing && sceneStack_.top() == outgoing) {
outgoing->onExit();
outgoing->onDetachFromScene();
auto outScene = transitionScene->getOutScene();
if (!sceneStack_.empty() && outScene) {
// 过渡场景已经在栈顶,弹出它
if (sceneStack_.top().get() == transitionScene.get()) {
sceneStack_.pop();
}
// 推入新场景并调用 onEnter
auto incoming = incomingScene_;
if (incoming) {
sceneStack_.push(incoming);
if (!incoming->isRunning()) {
incoming->onEnter();
incoming->onAttachToScene(incoming.get());
outScene->onExit();
outScene->onDetachFromScene();
}
// 推入新场景
auto inScene = transitionScene->getInScene();
if (inScene) {
inScene->onAttachToScene(inScene.get());
sceneStack_.push(inScene);
}
};
// 注意:不在此处调用新场景的 onEnter由 transitionStackAction_
// 在过渡完成后调用
}
void SceneManager::replaceScene(Ptr<Scene> scene, TransitionType transition,
@ -216,19 +187,26 @@ void SceneManager::replaceScene(Ptr<Scene> scene, TransitionType transition,
startTransition(oldScene, scene, transition, duration, [this]() {
// 过渡完成后,退出旧场景并从堆栈中移除
auto outgoing = outgoingScene_;
auto incoming = incomingScene_;
if (!sceneStack_.empty() && outgoing && sceneStack_.top() == outgoing) {
outgoing->onExit();
outgoing->onDetachFromScene();
if (!sceneStack_.empty() && activeTransitionScene_) {
// 弹出过渡场景
if (sceneStack_.top().get() == activeTransitionScene_.get()) {
sceneStack_.pop();
}
// 将新场景推入堆栈并调用 onEnter
if (incoming) {
sceneStack_.push(incoming);
if (!incoming->isRunning()) {
incoming->onEnter();
incoming->onAttachToScene(incoming.get());
// 退出旧场景
auto outScene = activeTransitionScene_->getOutScene();
if (outScene) {
outScene->onExit();
outScene->onDetachFromScene();
}
}
// 将新场景推入堆栈
if (activeTransitionScene_) {
auto inScene = activeTransitionScene_->getInScene();
if (inScene) {
inScene->onAttachToScene(inScene.get());
sceneStack_.push(inScene);
}
}
});
@ -267,12 +245,19 @@ void SceneManager::pushScene(Ptr<Scene> scene, TransitionType transition,
auto currentScene = sceneStack_.top();
startTransition(currentScene, scene, transition, duration, [this]() {
// 过渡完成后,将新场景推入堆栈并调用 onEnter
if (incomingScene_) {
sceneStack_.push(incomingScene_);
if (!incomingScene_->isRunning()) {
incomingScene_->onEnter();
incomingScene_->onAttachToScene(incomingScene_.get());
// 过渡完成后,将新场景推入堆栈
if (!sceneStack_.empty() && activeTransitionScene_) {
// 弹出过渡场景
if (sceneStack_.top().get() == activeTransitionScene_.get()) {
sceneStack_.pop();
}
}
if (activeTransitionScene_) {
auto inScene = activeTransitionScene_->getInScene();
if (inScene) {
inScene->onAttachToScene(inScene.get());
sceneStack_.push(inScene);
}
}
});
@ -304,20 +289,26 @@ void SceneManager::popScene(TransitionType transition, float duration) {
startTransition(current, previous, transition, duration, [this]() {
// 过渡完成后,退出当前场景并从堆栈中移除
auto outgoing = outgoingScene_;
if (!sceneStack_.empty() && outgoing && sceneStack_.top() == outgoing) {
outgoing->onExit();
outgoing->onDetachFromScene();
if (!sceneStack_.empty() && activeTransitionScene_) {
// 弹出过渡场景
if (sceneStack_.top().get() == activeTransitionScene_.get()) {
sceneStack_.pop();
}
// 恢复前一个场景
auto incoming = incomingScene_;
if (!sceneStack_.empty() && incoming && sceneStack_.top() == incoming) {
if (!incoming->isRunning()) {
incoming->onEnter();
incoming->onAttachToScene(incoming.get());
// 退出当前场景
auto outScene = activeTransitionScene_->getOutScene();
if (outScene) {
outScene->onExit();
outScene->onDetachFromScene();
}
}
// 恢复前一个场景
if (activeTransitionScene_) {
auto inScene = activeTransitionScene_->getInScene();
if (inScene && !sceneStack_.empty() && sceneStack_.top() == inScene) {
inScene->resume();
}
incoming->resume();
}
});
}
@ -349,18 +340,15 @@ void SceneManager::popToRootScene(TransitionType transition, float duration) {
startTransition(current, root, transition, duration, [this, root]() {
// 退出所有场景直到根场景
while (!sceneStack_.empty() && sceneStack_.top() != root) {
while (!sceneStack_.empty() && sceneStack_.top().get() != root.get()) {
auto scene = sceneStack_.top();
scene->onExit();
scene->onDetachFromScene();
sceneStack_.pop();
}
// 恢复根场景
if (!sceneStack_.empty() && sceneStack_.top() == root) {
if (!root->isRunning()) {
root->onEnter();
root->onAttachToScene(root.get());
}
if (!sceneStack_.empty() && sceneStack_.top().get() == root.get()) {
root->resume();
}
});
@ -409,12 +397,9 @@ void SceneManager::popToScene(const std::string &name,
scene->onDetachFromScene();
sceneStack_.pop();
}
// 恢复目标场景
if (!sceneStack_.empty() && sceneStack_.top() == target) {
if (!target->isRunning()) {
target->onEnter();
target->onAttachToScene(target.get());
}
target->resume();
}
});
@ -479,7 +464,7 @@ bool SceneManager::hasScene(const std::string &name) const {
void SceneManager::update(float dt) {
if (isTransitioning_) {
updateTransition(dt);
// 过渡场景在栈顶,正常更新即可
hoverTarget_ = nullptr;
captureTarget_ = nullptr;
hasLastPointerWorld_ = false;
@ -496,13 +481,7 @@ void SceneManager::update(float dt) {
void SceneManager::render(RenderBackend &renderer) {
Color clearColor = Colors::Black;
if (isTransitioning_) {
if (outgoingScene_) {
clearColor = outgoingScene_->getBackgroundColor();
} else if (incomingScene_) {
clearColor = incomingScene_->getBackgroundColor();
}
} else if (!sceneStack_.empty()) {
if (!sceneStack_.empty()) {
clearColor = sceneStack_.top()->getBackgroundColor();
}
@ -510,10 +489,7 @@ void SceneManager::render(RenderBackend &renderer) {
clearColor.r, clearColor.g, clearColor.b);
renderer.beginFrame(clearColor);
if (isTransitioning_ && activeTransition_) {
E2D_LOG_TRACE("SceneManager::render - rendering transition");
activeTransition_->render(renderer);
} else if (!sceneStack_.empty()) {
if (!sceneStack_.empty()) {
E2D_LOG_TRACE("SceneManager::render - rendering scene content");
sceneStack_.top()->renderContent(renderer);
} else {
@ -525,13 +501,7 @@ void SceneManager::render(RenderBackend &renderer) {
}
void SceneManager::collectRenderCommands(std::vector<RenderCommand> &commands) {
if (isTransitioning_ && outgoingScene_) {
// During transition, collect commands from both scenes
outgoingScene_->collectRenderCommands(commands, 0);
if (incomingScene_) {
incomingScene_->collectRenderCommands(commands, 0);
}
} else if (!sceneStack_.empty()) {
if (!sceneStack_.empty()) {
sceneStack_.top()->collectRenderCommands(commands, 0);
}
}
@ -555,18 +525,14 @@ void SceneManager::startTransition(Ptr<Scene> from, Ptr<Scene> to,
return;
}
// 使用工厂映射替代 switch
Ptr<Transition> transition;
size_t typeIndex = static_cast<size_t>(type);
if (typeIndex < TRANSITION_FACTORY_COUNT) {
transition = TRANSITION_FACTORIES[typeIndex](duration);
} else {
// 默认使用 Fade 过渡
transition = TRANSITION_FACTORIES[0](duration);
// 创建过渡场景
auto transitionScene = createTransitionScene(type, duration, to);
if (!transitionScene) {
// 回退到无过渡切换
replaceScene(to);
return;
}
transition->start(from, to);
// 在过渡开始前,发送 UIHoverExit 给当前悬停的节点,重置按钮状态
if (hoverTarget_) {
Event evt;
@ -578,29 +544,55 @@ void SceneManager::startTransition(Ptr<Scene> from, Ptr<Scene> to,
captureTarget_ = nullptr;
hasLastPointerWorld_ = false;
// 设置过渡场景
transitionScene->setOutScene(from);
transitionScene->setFinishCallback([this]() { finishTransition(); });
// 暂停当前场景
from->pause();
// 推入过渡场景(作为中介场景)
transitionScene->onEnter();
transitionScene->onAttachToScene(transitionScene.get());
sceneStack_.push(transitionScene);
isTransitioning_ = true;
currentTransition_ = type;
transitionDuration_ = duration;
transitionElapsed_ = 0.0f;
outgoingScene_ = from;
incomingScene_ = to;
activeTransition_ = transition;
activeTransitionScene_ = transitionScene;
transitionStackAction_ = std::move(stackAction);
// 注意:不在此处调用新场景的 onEnter由 transitionStackAction_
// 在过渡完成后调用
}
void SceneManager::updateTransition(float dt) {
transitionElapsed_ += dt;
if (activeTransition_) {
activeTransition_->update(dt);
if (activeTransition_->isFinished()) {
finishTransition();
Ptr<TransitionScene> SceneManager::createTransitionScene(TransitionType type,
float duration,
Ptr<Scene> inScene) {
if (!inScene) {
return nullptr;
}
} else {
finishTransition();
switch (type) {
case TransitionType::Fade:
return TransitionFadeScene::create(duration, inScene);
case TransitionType::SlideLeft:
return TransitionSlideScene::create(duration, inScene,
TransitionDirection::Left);
case TransitionType::SlideRight:
return TransitionSlideScene::create(duration, inScene,
TransitionDirection::Right);
case TransitionType::SlideUp:
return TransitionSlideScene::create(duration, inScene,
TransitionDirection::Up);
case TransitionType::SlideDown:
return TransitionSlideScene::create(duration, inScene,
TransitionDirection::Down);
case TransitionType::Scale:
return TransitionScaleScene::create(duration, inScene);
case TransitionType::Flip:
return TransitionFlipScene::create(duration, inScene);
case TransitionType::Box:
return TransitionBoxScene::create(duration, inScene);
default:
// 默认使用淡入淡出
return TransitionFadeScene::create(duration, inScene);
}
}
@ -625,9 +617,7 @@ void SceneManager::finishTransition() {
dispatchToNode(lastHoverTarget, evt);
}
outgoingScene_.reset();
incomingScene_.reset();
activeTransition_.reset();
activeTransitionScene_.reset();
transitionStackAction_ = nullptr;
if (transitionCallback_) {

View File

@ -1,396 +0,0 @@
#include <algorithm>
#include <extra2d/core/math_types.h>
#include <extra2d/graphics/camera.h>
#include <extra2d/graphics/render_backend.h>
#include <extra2d/scene/transition.h>
#include <glm/gtc/matrix_transform.hpp>
namespace extra2d {
// ============================================================================
// 缓动函数
// ============================================================================
static float easeInOutQuad(float t) {
return t < 0.5f ? 2.0f * t * t : -1.0f + (4.0f - 2.0f * t) * t;
}
static float easeOutQuad(float t) { return t * (2.0f - t); }
// ============================================================================
// Transition 基类
// ============================================================================
Transition::Transition(float duration)
: duration_(duration), elapsed_(0.0f), progress_(0.0f), isFinished_(false),
isStarted_(false) {}
void Transition::start(Ptr<Scene> from, Ptr<Scene> to) {
outgoingScene_ = from;
incomingScene_ = to;
elapsed_ = 0.0f;
progress_ = 0.0f;
isFinished_ = false;
isStarted_ = true;
}
void Transition::update(float dt) {
if (!isStarted_ || isFinished_) {
return;
}
elapsed_ += dt;
progress_ = duration_ > 0.0f ? std::min(1.0f, elapsed_ / duration_) : 1.0f;
if (progress_ >= 1.0f) {
onFinish();
}
}
void Transition::render(RenderBackend &renderer) {
if (!isStarted_ || isFinished_) {
return;
}
onRenderTransition(renderer, easeInOutQuad(progress_));
}
float Transition::getFadeInAlpha() const { return easeOutQuad(progress_); }
float Transition::getFadeOutAlpha() const {
return 1.0f - easeOutQuad(progress_);
}
void Transition::onFinish() {
isFinished_ = true;
if (finishCallback_) {
finishCallback_();
}
}
// ============================================================================
// FadeTransition - 淡入淡出
// ============================================================================
FadeTransition::FadeTransition(float duration) : Transition(duration) {}
void FadeTransition::onRenderTransition(RenderBackend &renderer,
float progress) {
float screenWidth = 800.0f;
float screenHeight = 600.0f;
if (outgoingScene_) {
Size viewportSize = outgoingScene_->getViewportSize();
if (viewportSize.width > 0 && viewportSize.height > 0) {
screenWidth = viewportSize.width;
screenHeight = viewportSize.height;
}
} else if (incomingScene_) {
Size viewportSize = incomingScene_->getViewportSize();
if (viewportSize.width > 0 && viewportSize.height > 0) {
screenWidth = viewportSize.width;
screenHeight = viewportSize.height;
}
}
glm::mat4 overlayVP =
glm::ortho(0.0f, screenWidth, screenHeight, 0.0f, -1.0f, 1.0f);
if (progress < 0.5f) {
if (outgoingScene_) {
outgoingScene_->renderContent(renderer);
}
float a = std::clamp(progress * 2.0f, 0.0f, 1.0f);
renderer.setViewProjection(overlayVP);
renderer.fillRect(Rect(0.0f, 0.0f, screenWidth, screenHeight),
Color(0.0f, 0.0f, 0.0f, a));
} else {
if (incomingScene_) {
incomingScene_->renderContent(renderer);
}
float a = std::clamp((1.0f - progress) * 2.0f, 0.0f, 1.0f);
renderer.setViewProjection(overlayVP);
renderer.fillRect(Rect(0.0f, 0.0f, screenWidth, screenHeight),
Color(0.0f, 0.0f, 0.0f, a));
}
}
// ============================================================================
// SlideTransition - 滑动
// ============================================================================
SlideTransition::SlideTransition(float duration, TransitionDirection direction)
: Transition(duration), direction_(direction) {}
void SlideTransition::onRenderTransition(RenderBackend &renderer,
float progress) {
// 获取视口尺寸
float screenWidth = 800.0f;
float screenHeight = 600.0f;
if (outgoingScene_) {
Size viewportSize = outgoingScene_->getViewportSize();
if (viewportSize.width > 0 && viewportSize.height > 0) {
screenWidth = viewportSize.width;
screenHeight = viewportSize.height;
}
} else if (incomingScene_) {
Size viewportSize = incomingScene_->getViewportSize();
if (viewportSize.width > 0 && viewportSize.height > 0) {
screenWidth = viewportSize.width;
screenHeight = viewportSize.height;
}
}
// 渲染源场景(滑出)
if (outgoingScene_) {
float offsetX = 0.0f;
float offsetY = 0.0f;
switch (direction_) {
case TransitionDirection::Left:
offsetX = -screenWidth * progress;
break;
case TransitionDirection::Right:
offsetX = screenWidth * progress;
break;
case TransitionDirection::Up:
offsetY = -screenHeight * progress;
break;
case TransitionDirection::Down:
offsetY = screenHeight * progress;
break;
}
// 保存原始相机位置
Camera *camera = outgoingScene_->getActiveCamera();
Vec2 originalPos = camera ? camera->getPosition() : Vec2::Zero();
// 应用偏移
if (camera) {
camera->setPosition(originalPos.x + offsetX, originalPos.y + offsetY);
}
// 渲染场景
outgoingScene_->renderContent(renderer);
// 恢复相机位置
if (camera) {
camera->setPosition(originalPos);
}
}
// 渲染目标场景(滑入)
if (incomingScene_) {
float offsetX = 0.0f;
float offsetY = 0.0f;
switch (direction_) {
case TransitionDirection::Left:
offsetX = screenWidth * (1.0f - progress);
break;
case TransitionDirection::Right:
offsetX = -screenWidth * (1.0f - progress);
break;
case TransitionDirection::Up:
offsetY = screenHeight * (1.0f - progress);
break;
case TransitionDirection::Down:
offsetY = -screenHeight * (1.0f - progress);
break;
}
// 保存原始相机位置
Camera *camera = incomingScene_->getActiveCamera();
Vec2 originalPos = camera ? camera->getPosition() : Vec2::Zero();
// 应用偏移
if (camera) {
camera->setPosition(originalPos.x + offsetX, originalPos.y + offsetY);
}
// 渲染场景
incomingScene_->renderContent(renderer);
// 恢复相机位置
if (camera) {
camera->setPosition(originalPos);
}
}
}
// ============================================================================
// ScaleTransition - 缩放
// ============================================================================
ScaleTransition::ScaleTransition(float duration) : Transition(duration) {}
void ScaleTransition::onRenderTransition(RenderBackend &renderer,
float progress) {
// 源场景:缩小消失
if (outgoingScene_) {
float scale = std::max(0.01f, 1.0f - progress);
// 保存原始相机状态
Camera *camera = outgoingScene_->getActiveCamera();
float originalZoom = camera ? camera->getZoom() : 1.0f;
Vec2 originalPos = camera ? camera->getPosition() : Vec2::Zero();
// 应用缩放(通过调整相机 zoom 实现)
if (camera) {
camera->setZoom(originalZoom * scale);
}
// 渲染场景
outgoingScene_->renderContent(renderer);
// 恢复相机状态
if (camera) {
camera->setZoom(originalZoom);
camera->setPosition(originalPos);
}
}
// 目标场景:放大出现
if (incomingScene_) {
float scale = std::max(0.01f, progress);
// 保存原始相机状态
Camera *camera = incomingScene_->getActiveCamera();
float originalZoom = camera ? camera->getZoom() : 1.0f;
Vec2 originalPos = camera ? camera->getPosition() : Vec2::Zero();
// 应用缩放
if (camera) {
camera->setZoom(originalZoom * scale);
}
// 渲染场景
incomingScene_->renderContent(renderer);
// 恢复相机状态
if (camera) {
camera->setZoom(originalZoom);
camera->setPosition(originalPos);
}
}
}
// ============================================================================
// FlipTransition - 翻页
// ============================================================================
FlipTransition::FlipTransition(float duration, Axis axis)
: Transition(duration), axis_(axis) {}
void FlipTransition::onRenderTransition(RenderBackend &renderer,
float progress) {
float angle = progress * PI_F; // 180度翻转
if (progress < 0.5f) {
// 前半段:翻转源场景
if (outgoingScene_) {
float currentAngle = angle;
// 保存原始相机状态
Camera *camera = outgoingScene_->getActiveCamera();
float originalRotation = camera ? camera->getRotation() : 0.0f;
// 应用旋转水平翻转绕Y轴垂直翻转绕X轴
if (axis_ == Axis::Horizontal) {
// 水平轴翻转 - 模拟绕X轴旋转
if (camera) {
camera->setRotation(originalRotation + currentAngle * RAD_TO_DEG);
}
} else {
// 垂直轴翻转 - 模拟绕Y轴旋转
if (camera) {
camera->setRotation(originalRotation - currentAngle * RAD_TO_DEG);
}
}
// 渲染场景
outgoingScene_->renderContent(renderer);
// 恢复相机状态
if (camera) {
camera->setRotation(originalRotation);
}
}
} else {
// 后半段:翻转目标场景
if (incomingScene_) {
float currentAngle = angle - PI_F;
// 保存原始相机状态
Camera *camera = incomingScene_->getActiveCamera();
float originalRotation = camera ? camera->getRotation() : 0.0f;
// 应用旋转
if (axis_ == Axis::Horizontal) {
if (camera) {
camera->setRotation(originalRotation + currentAngle * RAD_TO_DEG);
}
} else {
if (camera) {
camera->setRotation(originalRotation - currentAngle * RAD_TO_DEG);
}
}
// 渲染场景
incomingScene_->renderContent(renderer);
// 恢复相机状态
if (camera) {
camera->setRotation(originalRotation);
}
}
}
}
// ============================================================================
// BoxTransition - 方块过渡
// ============================================================================
BoxTransition::BoxTransition(float duration, int divisions)
: Transition(duration), divisions_(divisions) {}
void BoxTransition::onRenderTransition(RenderBackend &renderer,
float progress) {
float screenWidth = 800.0f;
float screenHeight = 600.0f;
if (incomingScene_) {
Size viewportSize = incomingScene_->getViewportSize();
if (viewportSize.width > 0 && viewportSize.height > 0) {
screenWidth = viewportSize.width;
screenHeight = viewportSize.height;
}
} else if (outgoingScene_) {
Size viewportSize = outgoingScene_->getViewportSize();
if (viewportSize.width > 0 && viewportSize.height > 0) {
screenWidth = viewportSize.width;
screenHeight = viewportSize.height;
}
}
if (incomingScene_) {
incomingScene_->renderContent(renderer);
} else if (outgoingScene_) {
outgoingScene_->renderContent(renderer);
} else {
return;
}
int div = std::max(1, divisions_);
int total = div * div;
int visible = std::clamp(static_cast<int>(total * progress), 0, total);
float cellW = screenWidth / static_cast<float>(div);
float cellH = screenHeight / static_cast<float>(div);
glm::mat4 overlayVP =
glm::ortho(0.0f, screenWidth, screenHeight, 0.0f, -1.0f, 1.0f);
renderer.setViewProjection(overlayVP);
for (int idx = visible; idx < total; ++idx) {
int x = idx % div;
int y = idx / div;
renderer.fillRect(Rect(x * cellW, y * cellH, cellW + 1.0f, cellH + 1.0f),
Colors::Black);
}
}
} // namespace extra2d

View File

@ -0,0 +1,72 @@
#include <extra2d/scene/transition_box_scene.h>
#include <extra2d/app/application.h>
#include <extra2d/graphics/render_backend.h>
#include <extra2d/core/color.h>
#include <glm/gtc/matrix_transform.hpp>
#include <algorithm>
namespace extra2d {
TransitionBoxScene::TransitionBoxScene(float duration, Ptr<Scene> inScene,
int divisions)
: TransitionScene(duration, inScene), divisions_(divisions) {}
Ptr<TransitionBoxScene> TransitionBoxScene::create(float duration,
Ptr<Scene> inScene,
int divisions) {
return makePtr<TransitionBoxScene>(duration, inScene, divisions);
}
void TransitionBoxScene::onTransitionStart() {
// 方块过渡不需要特殊的初始化
}
void TransitionBoxScene::renderContent(RenderBackend &renderer) {
// 获取窗口大小
auto &app = Application::instance();
float windowWidth = static_cast<float>(app.window().getWidth());
float windowHeight = static_cast<float>(app.window().getHeight());
// 更新进度
elapsed_ += 1.0f / 60.0f;
progress_ = duration_ > 0.0f ? std::min(1.0f, elapsed_ / duration_) : 1.0f;
// 先渲染新场景
if (inScene_) {
inScene_->renderContent(renderer);
} else if (outScene_) {
outScene_->renderContent(renderer);
}
// 设置视口为整个窗口
renderer.setViewport(0, 0, static_cast<int>(windowWidth),
static_cast<int>(windowHeight));
// 计算要显示的方块数量
int div = std::max(1, divisions_);
int total = div * div;
int visible = std::clamp(static_cast<int>(total * progress_), 0, total);
float cellW = windowWidth / static_cast<float>(div);
float cellH = windowHeight / static_cast<float>(div);
// 设置正交投影
glm::mat4 overlayVP =
glm::ortho(0.0f, windowWidth, windowHeight, 0.0f, -1.0f, 1.0f);
renderer.setViewProjection(overlayVP);
// 绘制剩余的方块(作为遮罩)
for (int idx = visible; idx < total; ++idx) {
int x = idx % div;
int y = idx / div;
renderer.fillRect(Rect(x * cellW, y * cellH, cellW + 1.0f, cellH + 1.0f),
Colors::Black);
}
// 检查是否完成
if (progress_ >= 1.0f && !isFinished_) {
finish();
}
}
} // namespace extra2d

View File

@ -0,0 +1,88 @@
#include <extra2d/scene/transition_fade_scene.h>
#include <extra2d/app/application.h>
#include <extra2d/graphics/render_backend.h>
#include <extra2d/graphics/render_target.h>
#include <extra2d/utils/logger.h>
#include <glm/gtc/matrix_transform.hpp>
namespace extra2d {
TransitionFadeScene::TransitionFadeScene(float duration, Ptr<Scene> inScene,
const Color &color)
: TransitionScene(duration, inScene), maskColor_(color) {}
Ptr<TransitionFadeScene> TransitionFadeScene::create(float duration,
Ptr<Scene> inScene,
const Color &color) {
return makePtr<TransitionFadeScene>(duration, inScene, color);
}
void TransitionFadeScene::onTransitionStart() {
E2D_LOG_DEBUG("TransitionFadeScene::onTransitionStart - 启动淡入淡出过渡");
// 使用一个定时器来更新进度
// 由于我们没有直接的动作系统集成到 Scene使用简单的 update 逻辑
// 实际进度更新由 SceneManager 的 update 驱动
}
void TransitionFadeScene::renderContent(RenderBackend &renderer) {
// 获取窗口大小
auto &app = Application::instance();
float windowWidth = static_cast<float>(app.window().getWidth());
float windowHeight = static_cast<float>(app.window().getHeight());
// 计算当前进度
elapsed_ += 1.0f / 60.0f; // 假设 60fps实际应该由 update 传递 dt
progress_ = duration_ > 0.0f ? std::min(1.0f, elapsed_ / duration_) : 1.0f;
// 检查是否需要切换场景(进度过半时)
if (!hasSwitched_ && progress_ >= 0.5f) {
hideOutShowIn();
}
// 根据进度渲染
if (progress_ < 0.5f) {
// 第一阶段:显示旧场景
drawOutScene(renderer);
} else {
// 第二阶段:显示新场景
drawInScene(renderer);
}
// 绘制遮罩层
// 计算遮罩透明度
float maskAlpha;
if (progress_ < 0.5f) {
// 前半段:从透明到不透明
maskAlpha = progress_ * 2.0f; // 0 -> 1
} else {
// 后半段:从不透明到透明
maskAlpha = (1.0f - progress_) * 2.0f; // 1 -> 0
}
// 设置视口为整个窗口
renderer.setViewport(0, 0, static_cast<int>(windowWidth),
static_cast<int>(windowHeight));
// 设置正交投影
glm::mat4 overlayVP =
glm::ortho(0.0f, windowWidth, windowHeight, 0.0f, -1.0f, 1.0f);
renderer.setViewProjection(overlayVP);
// 绘制遮罩
Color maskColor = maskColor_;
maskColor.a = maskAlpha;
renderer.fillRect(Rect(0.0f, 0.0f, windowWidth, windowHeight), maskColor);
// 检查是否完成
if (progress_ >= 1.0f && !isFinished_) {
finish();
}
}
void TransitionFadeScene::hideOutShowIn() {
hasSwitched_ = true;
E2D_LOG_DEBUG("TransitionFadeScene::hideOutShowIn - 切换场景显示");
}
} // namespace extra2d

View File

@ -0,0 +1,91 @@
#include <extra2d/scene/transition_flip_scene.h>
#include <extra2d/graphics/camera.h>
#include <extra2d/graphics/render_backend.h>
#include <extra2d/core/math_types.h>
namespace extra2d {
TransitionFlipScene::TransitionFlipScene(float duration, Ptr<Scene> inScene,
Axis axis)
: TransitionScene(duration, inScene), axis_(axis) {}
Ptr<TransitionFlipScene> TransitionFlipScene::create(float duration,
Ptr<Scene> inScene,
Axis axis) {
return makePtr<TransitionFlipScene>(duration, inScene, axis);
}
void TransitionFlipScene::onTransitionStart() {
// 翻页过渡不需要特殊的初始化
}
void TransitionFlipScene::renderContent(RenderBackend &renderer) {
// 更新进度
elapsed_ += 1.0f / 60.0f;
progress_ = duration_ > 0.0f ? std::min(1.0f, elapsed_ / duration_) : 1.0f;
// 缓动函数
float easeProgress = progress_ < 0.5f ? 2.0f * progress_ * progress_
: -1.0f + (4.0f - 2.0f * progress_) * progress_;
float angle = easeProgress * PI_F; // 180度翻转
if (progress_ < 0.5f) {
// 前半段:翻转源场景
if (outScene_) {
float currentAngle = angle;
Camera *camera = outScene_->getActiveCamera();
float originalRotation = camera ? camera->getRotation() : 0.0f;
if (axis_ == Axis::Horizontal) {
// 水平轴翻转 - 模拟绕X轴旋转
if (camera) {
camera->setRotation(originalRotation + currentAngle * RAD_TO_DEG);
}
} else {
// 垂直轴翻转 - 模拟绕Y轴旋转
if (camera) {
camera->setRotation(originalRotation - currentAngle * RAD_TO_DEG);
}
}
outScene_->renderContent(renderer);
if (camera) {
camera->setRotation(originalRotation);
}
}
} else {
// 后半段:翻转目标场景
if (inScene_) {
float currentAngle = angle - PI_F;
Camera *camera = inScene_->getActiveCamera();
float originalRotation = camera ? camera->getRotation() : 0.0f;
if (axis_ == Axis::Horizontal) {
if (camera) {
camera->setRotation(originalRotation + currentAngle * RAD_TO_DEG);
}
} else {
if (camera) {
camera->setRotation(originalRotation - currentAngle * RAD_TO_DEG);
}
}
inScene_->renderContent(renderer);
if (camera) {
camera->setRotation(originalRotation);
}
}
}
// 检查是否完成
if (progress_ >= 1.0f && !isFinished_) {
finish();
}
}
} // namespace extra2d

View File

@ -0,0 +1,71 @@
#include <extra2d/scene/transition_scale_scene.h>
#include <extra2d/graphics/camera.h>
#include <extra2d/graphics/render_backend.h>
#include <algorithm>
namespace extra2d {
TransitionScaleScene::TransitionScaleScene(float duration, Ptr<Scene> inScene)
: TransitionScene(duration, inScene) {}
Ptr<TransitionScaleScene> TransitionScaleScene::create(float duration,
Ptr<Scene> inScene) {
return makePtr<TransitionScaleScene>(duration, inScene);
}
void TransitionScaleScene::onTransitionStart() {
// 缩放过渡不需要特殊的初始化
}
void TransitionScaleScene::renderContent(RenderBackend &renderer) {
// 更新进度
elapsed_ += 1.0f / 60.0f;
progress_ = duration_ > 0.0f ? std::min(1.0f, elapsed_ / duration_) : 1.0f;
// 缓动函数
float easeProgress = progress_ < 0.5f ? 2.0f * progress_ * progress_
: -1.0f + (4.0f - 2.0f * progress_) * progress_;
// 源场景:缩小消失
if (outScene_) {
float scale = std::max(0.01f, 1.0f - easeProgress);
Camera *camera = outScene_->getActiveCamera();
float originalZoom = camera ? camera->getZoom() : 1.0f;
if (camera) {
camera->setZoom(originalZoom * scale);
}
outScene_->renderContent(renderer);
if (camera) {
camera->setZoom(originalZoom);
}
}
// 目标场景:放大出现
if (inScene_) {
float scale = std::max(0.01f, easeProgress);
Camera *camera = inScene_->getActiveCamera();
float originalZoom = camera ? camera->getZoom() : 1.0f;
if (camera) {
camera->setZoom(originalZoom * scale);
}
inScene_->renderContent(renderer);
if (camera) {
camera->setZoom(originalZoom);
}
}
// 检查是否完成
if (progress_ >= 1.0f && !isFinished_) {
finish();
}
}
} // namespace extra2d

View File

@ -0,0 +1,81 @@
#include <extra2d/scene/transition_scene.h>
#include <extra2d/graphics/render_backend.h>
#include <extra2d/utils/logger.h>
namespace extra2d {
TransitionScene::TransitionScene(float duration, Ptr<Scene> inScene)
: duration_(duration), inScene_(inScene) {}
void TransitionScene::onEnter() {
// 调用基类的 onEnter
Scene::onEnter();
// 调用退出场景的 onExitTransitionDidStart
if (outScene_) {
outScene_->onExitTransitionDidStart();
}
// 调用进入场景的 onEnter
if (inScene_) {
inScene_->onEnter();
inScene_->onAttachToScene(inScene_.get());
}
// 启动过渡
onTransitionStart();
}
void TransitionScene::onExit() {
// 调用退出场景的 onExit
if (outScene_) {
outScene_->onExit();
outScene_->onDetachFromScene();
}
// 调用进入场景的 onEnterTransitionDidFinish
if (inScene_) {
inScene_->onEnterTransitionDidFinish();
}
// 调用基类的 onExit
Scene::onExit();
}
void TransitionScene::finish() {
if (isFinished_) {
return;
}
isFinished_ = true;
E2D_LOG_DEBUG("TransitionScene::finish - 过渡完成,切换到目标场景");
// 调用完成回调,通知 SceneManager 进行场景切换
if (finishCallback_) {
finishCallback_();
}
}
void TransitionScene::renderContent(RenderBackend &renderer) {
// 在 TransitionScene 上渲染新旧两个子场景
// 子类可以重写此方法来控制渲染顺序和效果
// 默认先渲染退出场景,再渲染进入场景(在上方)
drawOutScene(renderer);
drawInScene(renderer);
}
void TransitionScene::drawOutScene(RenderBackend &renderer) {
if (outScene_) {
outScene_->renderContent(renderer);
}
}
void TransitionScene::drawInScene(RenderBackend &renderer) {
if (inScene_) {
inScene_->renderContent(renderer);
}
}
} // namespace extra2d

View File

@ -0,0 +1,121 @@
#include <extra2d/scene/transition_slide_scene.h>
#include <extra2d/graphics/camera.h>
#include <extra2d/graphics/render_backend.h>
namespace extra2d {
TransitionSlideScene::TransitionSlideScene(float duration, Ptr<Scene> inScene,
TransitionDirection direction)
: TransitionScene(duration, inScene), direction_(direction) {}
Ptr<TransitionSlideScene> TransitionSlideScene::create(
float duration, Ptr<Scene> inScene, TransitionDirection direction) {
return makePtr<TransitionSlideScene>(duration, inScene, direction);
}
void TransitionSlideScene::onTransitionStart() {
// 滑动过渡不需要特殊的初始化
}
void TransitionSlideScene::renderContent(RenderBackend &renderer) {
// 获取视口尺寸
float screenWidth = 800.0f;
float screenHeight = 600.0f;
if (outScene_) {
Size viewportSize = outScene_->getViewportSize();
if (viewportSize.width > 0 && viewportSize.height > 0) {
screenWidth = viewportSize.width;
screenHeight = viewportSize.height;
}
} else if (inScene_) {
Size viewportSize = inScene_->getViewportSize();
if (viewportSize.width > 0 && viewportSize.height > 0) {
screenWidth = viewportSize.width;
screenHeight = viewportSize.height;
}
}
// 更新进度
elapsed_ += 1.0f / 60.0f;
progress_ = duration_ > 0.0f ? std::min(1.0f, elapsed_ / duration_) : 1.0f;
// 缓动函数
float easeProgress = progress_ < 0.5f ? 2.0f * progress_ * progress_
: -1.0f + (4.0f - 2.0f * progress_) * progress_;
// 渲染退出场景(滑出)
if (outScene_) {
float offsetX = 0.0f;
float offsetY = 0.0f;
switch (direction_) {
case TransitionDirection::Left:
offsetX = -screenWidth * easeProgress;
break;
case TransitionDirection::Right:
offsetX = screenWidth * easeProgress;
break;
case TransitionDirection::Up:
offsetY = -screenHeight * easeProgress;
break;
case TransitionDirection::Down:
offsetY = screenHeight * easeProgress;
break;
}
Camera *camera = outScene_->getActiveCamera();
Vec2 originalPos = camera ? camera->getPosition() : Vec2::Zero();
if (camera) {
camera->setPosition(originalPos.x + offsetX, originalPos.y + offsetY);
}
outScene_->renderContent(renderer);
if (camera) {
camera->setPosition(originalPos);
}
}
// 渲染进入场景(滑入)
if (inScene_) {
float offsetX = 0.0f;
float offsetY = 0.0f;
switch (direction_) {
case TransitionDirection::Left:
offsetX = screenWidth * (1.0f - easeProgress);
break;
case TransitionDirection::Right:
offsetX = -screenWidth * (1.0f - easeProgress);
break;
case TransitionDirection::Up:
offsetY = screenHeight * (1.0f - easeProgress);
break;
case TransitionDirection::Down:
offsetY = -screenHeight * (1.0f - easeProgress);
break;
}
Camera *camera = inScene_->getActiveCamera();
Vec2 originalPos = camera ? camera->getPosition() : Vec2::Zero();
if (camera) {
camera->setPosition(originalPos.x + offsetX, originalPos.y + offsetY);
}
inScene_->renderContent(renderer);
if (camera) {
camera->setPosition(originalPos);
}
}
// 检查是否完成
if (progress_ >= 1.0f && !isFinished_) {
finish();
}
}
} // namespace extra2d

View File

@ -1,9 +1,9 @@
#include <algorithm>
#include <cmath>
#include <extra2d/app/application.h>
#include <extra2d/core/string.h>
#include <extra2d/graphics/render_backend.h>
#include <extra2d/ui/button.h>
#include <extra2d/core/string.h>
namespace extra2d {
@ -37,21 +37,21 @@ Button::Button() {
dispatcher.addListener(EventType::UIReleased,
[this](Event &) { pressed_ = false; });
dispatcher.addListener(EventType::UIClicked, [this](Event &) {
if (onClick_)
if (toggleMode_) {
toggle();
}
if (onClick_) {
onClick_();
}
});
}
Button::Button(const std::string &text) : Button() {
text_ = text;
}
Button::Button(const std::string &text) : Button() { text_ = text; }
// ------------------------------------------------------------------------
// 静态创建方法
// ------------------------------------------------------------------------
Ptr<Button> Button::create() {
return makePtr<Button>();
}
Ptr<Button> Button::create() { return makePtr<Button>(); }
Ptr<Button> Button::create(const std::string &text) {
return makePtr<Button>(text);
@ -197,9 +197,7 @@ void Button::setPadding(const Vec2 &padding) {
}
}
void Button::setTextColor(const Color &color) {
textColor_ = color;
}
void Button::setTextColor(const Color &color) { textColor_ = color; }
void Button::setBackgroundColor(const Color &normal, const Color &hover,
const Color &pressed) {
@ -229,9 +227,37 @@ void Button::setOnClick(Function<void()> callback) {
onClick_ = std::move(callback);
}
void Button::setHoverCursor(CursorShape cursor) {
hoverCursor_ = cursor;
void Button::setToggleMode(bool enabled) { toggleMode_ = enabled; }
void Button::setOn(bool on) {
if (isOn_ != on) {
isOn_ = on;
if (onStateChange_) {
onStateChange_(isOn_);
}
}
}
void Button::toggle() { setOn(!isOn_); }
void Button::setOnStateChange(Function<void(bool)> callback) {
onStateChange_ = std::move(callback);
}
void Button::setStateText(const std::string &textOff,
const std::string &textOn) {
textOff_ = textOff;
textOn_ = textOn;
useStateText_ = true;
}
void Button::setStateTextColor(const Color &colorOff, const Color &colorOn) {
textColorOff_ = colorOff;
textColorOn_ = colorOn;
useStateTextColor_ = true;
}
void Button::setHoverCursor(CursorShape cursor) { hoverCursor_ = cursor; }
void Button::setBackgroundImage(Ptr<Texture> normal, Ptr<Texture> hover,
Ptr<Texture> pressed) {
@ -256,12 +282,33 @@ void Button::setBackgroundImage(Ptr<Texture> texture, const Rect &rect) {
imgPressedRect_ = rect;
useImageBackground_ = (texture != nullptr);
useTextureRect_ = true;
useStateImages_ = false;
if (useImageBackground_ && scaleMode_ == ImageScaleMode::Original) {
setSize(rect.size.width, rect.size.height);
}
}
void Button::setStateBackgroundImage(
Ptr<Texture> offNormal, Ptr<Texture> onNormal, Ptr<Texture> offHover,
Ptr<Texture> onHover, Ptr<Texture> offPressed, Ptr<Texture> onPressed) {
imgOffNormal_ = offNormal;
imgOnNormal_ = onNormal;
imgOffHover_ = offHover ? offHover : offNormal;
imgOnHover_ = onHover ? onHover : onNormal;
imgOffPressed_ = offPressed ? offPressed : offNormal;
imgOnPressed_ = onPressed ? onPressed : onNormal;
useStateImages_ = (offNormal != nullptr && onNormal != nullptr);
useImageBackground_ = useStateImages_;
useTextureRect_ = false;
if (useStateImages_ && scaleMode_ == ImageScaleMode::Original && offNormal) {
setSize(static_cast<float>(offNormal->getWidth()),
static_cast<float>(offNormal->getHeight()));
}
}
void Button::setBackgroundImageScaleMode(ImageScaleMode mode) {
scaleMode_ = mode;
if (useImageBackground_ && scaleMode_ == ImageScaleMode::Original &&
@ -271,9 +318,7 @@ void Button::setBackgroundImageScaleMode(ImageScaleMode mode) {
}
}
void Button::setCustomSize(const Vec2 &size) {
setSize(size.x, size.y);
}
void Button::setCustomSize(const Vec2 &size) { setSize(size.x, size.y); }
void Button::setCustomSize(float width, float height) {
setSize(width, height);
@ -326,15 +371,50 @@ void Button::drawBackgroundImage(RenderBackend &renderer, const Rect &rect) {
Texture *texture = nullptr;
Rect srcRect;
// 如果使用状态图片,根据 isOn_ 状态选择对应的图片
if (useStateImages_) {
if (isOn_) {
if (pressed_ && imgOnPressed_) {
texture = imgOnPressed_.get();
} else if (hovered_ && imgOnHover_) {
texture = imgOnHover_.get();
} else {
texture = imgOnNormal_.get();
}
} else {
if (pressed_ && imgOffPressed_) {
texture = imgOffPressed_.get();
} else if (hovered_ && imgOffHover_) {
texture = imgOffHover_.get();
} else {
texture = imgOffNormal_.get();
}
}
if (texture) {
srcRect = Rect(0, 0, static_cast<float>(texture->getWidth()),
static_cast<float>(texture->getHeight()));
}
} else {
// 使用普通图片背景
if (pressed_ && imgPressed_) {
texture = imgPressed_.get();
srcRect = useTextureRect_ ? imgPressedRect_ : Rect(0, 0, static_cast<float>(imgPressed_->getWidth()), static_cast<float>(imgPressed_->getHeight()));
srcRect = useTextureRect_
? imgPressedRect_
: Rect(0, 0, static_cast<float>(imgPressed_->getWidth()),
static_cast<float>(imgPressed_->getHeight()));
} else if (hovered_ && imgHover_) {
texture = imgHover_.get();
srcRect = useTextureRect_ ? imgHoverRect_ : Rect(0, 0, static_cast<float>(imgHover_->getWidth()), static_cast<float>(imgHover_->getHeight()));
srcRect = useTextureRect_
? imgHoverRect_
: Rect(0, 0, static_cast<float>(imgHover_->getWidth()),
static_cast<float>(imgHover_->getHeight()));
} else if (imgNormal_) {
texture = imgNormal_.get();
srcRect = useTextureRect_ ? imgNormalRect_ : Rect(0, 0, static_cast<float>(imgNormal_->getWidth()), static_cast<float>(imgNormal_->getHeight()));
srcRect = useTextureRect_
? imgNormalRect_
: Rect(0, 0, static_cast<float>(imgNormal_->getWidth()),
static_cast<float>(imgNormal_->getHeight()));
}
}
if (!texture)
@ -349,8 +429,8 @@ void Button::drawBackgroundImage(RenderBackend &renderer, const Rect &rect) {
Rect destRect(drawPos.x, drawPos.y, drawSize.x, drawSize.y);
renderer.drawSprite(*texture, destRect, srcRect,
Colors::White, 0.0f, Vec2::Zero());
renderer.drawSprite(*texture, destRect, srcRect, Colors::White, 0.0f,
Vec2::Zero());
}
void Button::drawRoundedRect(RenderBackend &renderer, const Rect &rect,
@ -371,9 +451,11 @@ void Button::drawRoundedRect(RenderBackend &renderer, const Rect &rect,
float r = radius;
renderer.drawLine(Vec2(x + r, y), Vec2(x + w - r, y), color, borderWidth_);
renderer.drawLine(Vec2(x + r, y + h), Vec2(x + w - r, y + h), color, borderWidth_);
renderer.drawLine(Vec2(x + r, y + h), Vec2(x + w - r, y + h), color,
borderWidth_);
renderer.drawLine(Vec2(x, y + r), Vec2(x, y + h - r), color, borderWidth_);
renderer.drawLine(Vec2(x + w, y + r), Vec2(x + w, y + h - r), color, borderWidth_);
renderer.drawLine(Vec2(x + w, y + r), Vec2(x + w, y + h - r), color,
borderWidth_);
for (int i = 0; i < segments; i++) {
float angle1 = 3.14159f * 0.5f * (float)i / segments + 3.14159f;
@ -384,7 +466,8 @@ void Button::drawRoundedRect(RenderBackend &renderer, const Rect &rect,
}
for (int i = 0; i < segments; i++) {
float angle1 = 3.14159f * 0.5f * (float)i / segments + 3.14159f * 1.5f;
float angle2 = 3.14159f * 0.5f * (float)(i + 1) / segments + 3.14159f * 1.5f;
float angle2 =
3.14159f * 0.5f * (float)(i + 1) / segments + 3.14159f * 1.5f;
Vec2 p1(x + w - r + r * cosf(angle1), y + r + r * sinf(angle1));
Vec2 p2(x + w - r + r * cosf(angle2), y + r + r * sinf(angle2));
renderer.drawLine(p1, p2, color, borderWidth_);
@ -398,7 +481,8 @@ void Button::drawRoundedRect(RenderBackend &renderer, const Rect &rect,
}
for (int i = 0; i < segments; i++) {
float angle1 = 3.14159f * 0.5f * (float)i / segments + 3.14159f * 0.5f;
float angle2 = 3.14159f * 0.5f * (float)(i + 1) / segments + 3.14159f * 0.5f;
float angle2 =
3.14159f * 0.5f * (float)(i + 1) / segments + 3.14159f * 0.5f;
Vec2 p1(x + r + r * cosf(angle1), y + h - r + r * sinf(angle1));
Vec2 p2(x + r + r * cosf(angle2), y + h - r + r * sinf(angle2));
renderer.drawLine(p1, p2, color, borderWidth_);
@ -513,8 +597,25 @@ void Button::onDrawWidget(RenderBackend &renderer) {
renderer.beginSpriteBatch();
}
if (font_ && !text_.empty()) {
Vec2 textSize = font_->measureText(text_);
if (font_) {
// 确定要显示的文字
std::string textToDraw;
if (useStateText_) {
textToDraw = isOn_ ? textOn_ : textOff_;
} else {
textToDraw = text_;
}
// 确定文字颜色
Color colorToUse;
if (useStateTextColor_) {
colorToUse = isOn_ ? textColorOn_ : textColorOff_;
} else {
colorToUse = textColor_;
}
if (!textToDraw.empty()) {
Vec2 textSize = font_->measureText(textToDraw);
Vec2 textPos(rect.center().x - textSize.x * 0.5f,
rect.center().y - textSize.y * 0.5f);
@ -527,155 +628,9 @@ void Button::onDrawWidget(RenderBackend &renderer) {
textPos.x = std::max(minX, std::min(textPos.x, maxX));
textPos.y = std::max(minY, std::min(textPos.y, maxY));
Color finalTextColor = textColor_;
finalTextColor.a = 1.0f;
renderer.drawText(*font_, text_, textPos, finalTextColor);
}
}
// ============================================================================
// ToggleImageButton 实现
// ============================================================================
ToggleImageButton::ToggleImageButton() {
setOnClick([this]() { toggle(); });
}
Ptr<ToggleImageButton> ToggleImageButton::create() {
return makePtr<ToggleImageButton>();
}
void ToggleImageButton::setStateImages(Ptr<Texture> stateOffNormal,
Ptr<Texture> stateOnNormal,
Ptr<Texture> stateOffHover,
Ptr<Texture> stateOnHover,
Ptr<Texture> stateOffPressed,
Ptr<Texture> stateOnPressed) {
imgOffNormal_ = stateOffNormal;
imgOnNormal_ = stateOnNormal;
imgOffHover_ = stateOffHover ? stateOffHover : stateOffNormal;
imgOnHover_ = stateOnHover ? stateOnHover : stateOnNormal;
imgOffPressed_ = stateOffPressed ? stateOffPressed : stateOffNormal;
imgOnPressed_ = stateOnPressed ? stateOnPressed : stateOnNormal;
if (imgOffNormal_) {
setSize(static_cast<float>(imgOffNormal_->getWidth()),
static_cast<float>(imgOffNormal_->getHeight()));
}
}
void ToggleImageButton::setOn(bool on) {
if (isOn_ != on) {
isOn_ = on;
if (onStateChange_) {
onStateChange_(isOn_);
}
}
}
void ToggleImageButton::toggle() {
setOn(!isOn_);
}
void ToggleImageButton::setOnStateChange(Function<void(bool)> callback) {
onStateChange_ = std::move(callback);
}
void ToggleImageButton::setStateText(const std::string &textOff,
const std::string &textOn) {
textOff_ = textOff;
textOn_ = textOn;
useStateText_ = true;
}
void ToggleImageButton::setStateTextColor(const Color &colorOff,
const Color &colorOn) {
textColorOff_ = colorOff;
textColorOn_ = colorOn;
useStateTextColor_ = true;
}
void ToggleImageButton::onDrawWidget(RenderBackend &renderer) {
Rect rect = getBoundingBox();
if (rect.empty()) {
return;
}
Ptr<Texture> texture = nullptr;
if (isOn_) {
if (isPressed() && imgOnPressed_) {
texture = imgOnPressed_;
} else if (isHovered() && imgOnHover_) {
texture = imgOnHover_;
} else {
texture = imgOnNormal_;
}
} else {
if (isPressed() && imgOffPressed_) {
texture = imgOffPressed_;
} else if (isHovered() && imgOffHover_) {
texture = imgOffHover_;
} else {
texture = imgOffNormal_;
}
}
if (texture) {
Vec2 imageSize(static_cast<float>(texture->getWidth()),
static_cast<float>(texture->getHeight()));
Vec2 buttonSize(rect.size.width, rect.size.height);
Vec2 drawSize = imageSize;
Vec2 drawPos(rect.origin.x + (buttonSize.x - drawSize.x) * 0.5f,
rect.origin.y + (buttonSize.y - drawSize.y) * 0.5f);
Rect destRect(drawPos.x, drawPos.y, drawSize.x, drawSize.y);
renderer.drawSprite(*texture, destRect,
Rect(0, 0, imageSize.x, imageSize.y), Colors::White,
0.0f, Vec2::Zero());
}
renderer.endSpriteBatch();
float borderWidth = 1.0f;
Color borderColor =
isOn_ ? Color(0.0f, 1.0f, 0.0f, 0.8f) : Color(0.6f, 0.6f, 0.6f, 1.0f);
if (borderWidth > 0.0f) {
if (isRoundedCornersEnabled()) {
drawRoundedRect(renderer, rect, borderColor, getCornerRadius());
} else {
renderer.drawRect(rect, borderColor, borderWidth);
}
}
renderer.beginSpriteBatch();
auto font = getFont();
if (font) {
std::string textToDraw;
if (useStateText_) {
textToDraw = isOn_ ? textOn_ : textOff_;
} else {
textToDraw = getText();
}
Color colorToUse;
if (useStateTextColor_) {
colorToUse = isOn_ ? textColorOn_ : textColorOff_;
} else {
colorToUse = getTextColor();
}
if (!textToDraw.empty()) {
Vec2 textSize = font->measureText(textToDraw);
Vec2 textPos(rect.center().x - textSize.x * 0.5f,
rect.center().y - textSize.y * 0.5f);
colorToUse.a = 1.0f;
renderer.drawText(*font, textToDraw, textPos, colorToUse);
renderer.drawText(*font_, textToDraw, textPos, colorToUse);
}
}
}

View File

@ -4,7 +4,7 @@ Extra2D 的场景系统提供了游戏内容的分层管理和切换功能。本
## 完整示例
参考 `examples/flappy_bird/` 中的实现:
参考 `examples/flappy_bird/` `examples/push_box/` 中的实现:
- `BaseScene.h/cpp` - 基础场景类(视口适配)
- `StartScene.h/cpp` - 开始菜单场景
@ -193,6 +193,14 @@ void GameScene::onUpdate(float dt) {
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` 固定在屏幕上,不受视口适配影响
## 场景基础
### 创建场景
@ -230,23 +238,37 @@ public:
};
```
### 场景切换
### 场景切换新API
通过 `SceneManager` 进行场景切换:
```cpp
// 进入场景(无过渡)
app.enterScene(makePtr<GameScene>());
auto& scenes = app.scenes();
// 进入场景(有过渡效果)
app.enterScene(makePtr<GameScene>(), TransitionType::Fade, 0.5f);
// 运行第一个场景
scenes.runWithScene(makePtr<GameScene>());
// 替换当前场景
app.scenes().replaceScene(makePtr<NewScene>());
// 替换当前场景(无过渡)
scenes.replaceScene(makePtr<GameScene>());
// 替换当前场景(有过渡效果)
scenes.replaceScene(makePtr<GameScene>(), TransitionType::Fade, 0.5f);
// 推入场景(保留当前场景)
app.scenes().pushScene(makePtr<NewScene>());
scenes.pushScene(makePtr<NewScene>());
scenes.pushScene(makePtr<NewScene>(), TransitionType::SlideLeft, 0.5f);
// 弹出场景(返回上一个场景)
app.scenes().popScene();
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);
```
### 过渡效果类型
@ -258,11 +280,14 @@ enum class TransitionType {
SlideLeft, // 向左滑动
SlideRight, // 向右滑动
SlideUp, // 向上滑动
SlideDown // 向下滑动
SlideDown, // 向下滑动
Scale, // 缩放过渡
Flip, // 翻转过渡
Box // 盒子过渡
};
```
## 场景管理器
### 场景管理器
通过 `app.scenes()` 访问场景管理器:
@ -270,13 +295,25 @@ enum class TransitionType {
auto& scenes = app.scenes();
// 获取当前场景
auto current = scenes.currentScene();
auto current = scenes.getCurrentScene();
// 获取上一个场景
auto previous = scenes.getPreviousScene();
// 获取根场景
auto root = scenes.getRootScene();
// 通过名称获取场景
auto scene = scenes.getSceneByName("SceneName");
// 获取场景栈深度
size_t depth = scenes.stackDepth();
size_t count = scenes.getSceneCount();
// 检查是否正在过渡
bool transitioning = scenes.isTransitioning();
// 清空场景栈
scenes.clearStack();
scenes.end();
```
## 场景生命周期
@ -284,7 +321,7 @@ scenes.clearStack();
```
创建场景 (makePtr<Scene>)
进入场景 (enterScene)
进入场景 (runWithScene/replaceScene/pushScene)
onEnter() - 初始化资源
@ -411,10 +448,12 @@ private:
}
void executeMenuItem() {
auto& scenes = Application::instance().scenes();
switch (selectedIndex_) {
case 0:
Application::instance().scenes().replaceScene(
makePtr<GameScene>(), TransitionType::Fade, 0.25f);
scenes.replaceScene(makePtr<GameScene>(),
TransitionType::Fade, 0.25f);
break;
case 1:
// 打开设置
@ -438,6 +477,8 @@ private:
2. **在 onExit 中清理资源** - 避免内存泄漏
3. **使用过渡效果** - 提升用户体验
4. **分离场景逻辑** - 每个场景负责自己的功能
5. **使用视口适配** - 确保游戏在不同分辨率下正确显示
6. **正确处理窗口大小变化** - 在 `onRender()` 中检测并更新视口
## 下一步

View File

@ -109,7 +109,7 @@ button->setBorder(Colors::White, 2.0f);
button->setRoundedCornersEnabled(true);
button->setCornerRadius(8.0f);
// 图片背景
// 图片背景(普通按钮)
button->setBackgroundImage(normalTex, hoverTex, pressedTex);
button->setBackgroundImageScaleMode(ImageScaleMode::ScaleFit);
@ -117,6 +117,51 @@ 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
@ -245,6 +290,7 @@ void GameOverLayer::onUpdate(float dt) {
- **动画播放期间**:禁用按钮,防止用户过早操作
- **加载过程中**:禁用按钮,显示加载状态
- **条件限制**:当条件不满足时禁用按钮(如未选择关卡)
- **切换按钮**:音效开关、设置开关等需要显示两种状态的按钮
## 文本Text
@ -350,11 +396,11 @@ checkBox->setPosition(Vec2(100, 200));
checkBox->setChecked(true);
// 方式2带标签
auto checkBox = CheckBox::create("启用音效");
checkBox = CheckBox::create("启用音效");
checkBox->setPosition(Vec2(100, 200));
// 方式3链式调用
auto checkBox = CheckBox::create("启用音效")
checkBox = CheckBox::create("启用音效")
->withPosition(100, 200)
->withFont(font)
->withTextColor(Colors::White);
@ -620,18 +666,24 @@ public:
title->setAlignment(Alignment::Center);
addChild(title);
// 音效开关
// 音效开关(使用切换按钮)
auto soundLabel = Label::create("音效", font_);
soundLabel->setPosition(Vec2(200, 200));
addChild(soundLabel);
soundCheck_ = CheckBox::create();
soundCheck_->setPosition(Vec2(350, 200));
soundCheck_->setChecked(true);
soundCheck_->setOnStateChange([this](bool checked) {
E2D_LOG_INFO("音效: {}", checked ? "开启" : "关闭");
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(soundCheck_);
addChild(soundToggle_);
// 音量滑块
auto volumeLabel = Label::create("音量", font_);
@ -685,7 +737,7 @@ public:
private:
Ptr<FontAtlas> font_;
Ptr<CheckBox> soundCheck_;
Ptr<Button> soundToggle_;
Ptr<Slider> volumeSlider_;
};
```
@ -697,6 +749,7 @@ private:
3. **设置合适的锚点** - 使用锚点0.5, 0.5)让控件中心对齐,方便布局
4. **复用字体资源** - 避免重复加载相同字体
5. **使用回调函数** - 使用 `setOnClick`、`setOnValueChange` 等回调响应用户操作
6. **使用切换按钮** - 对于需要显示两种状态的按钮(如开关),使用 `setToggleMode(true)`
## 下一步

View File

@ -3,6 +3,7 @@
// ============================================================================
#include "BaseScene.h"
#include <extra2d/scene/transition_scene.h>
#include <extra2d/utils/logger.h>
namespace flappybird {
@ -66,10 +67,6 @@ void BaseScene::onRender(extra2d::RenderBackend &renderer) {
}
// 设置视口为居中区域
E2D_LOG_INFO(
"BaseScene::onRender - setting viewport: x={}, y={}, width={}, height={}",
static_cast<int>(viewportOffsetX_), static_cast<int>(viewportOffsetY_),
static_cast<int>(scaledGameWidth_), static_cast<int>(scaledGameHeight_));
renderer.setViewport(
static_cast<int>(viewportOffsetX_), static_cast<int>(viewportOffsetY_),
static_cast<int>(scaledGameWidth_), static_cast<int>(scaledGameHeight_));
@ -78,4 +75,53 @@ void BaseScene::onRender(extra2d::RenderBackend &renderer) {
extra2d::Scene::onRender(renderer);
}
void BaseScene::renderContent(extra2d::RenderBackend &renderer) {
// 如果视口参数未初始化onEnter 还没被调用),先初始化
if (scaledGameWidth_ <= 0.0f || scaledGameHeight_ <= 0.0f) {
updateViewport();
}
// 检查窗口大小是否改变
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();
}
// 检查当前场景是否作为 TransitionScene 的子场景被渲染
bool isChildOfTransition = false;
if (auto parent = getParent()) {
if (dynamic_cast<extra2d::TransitionScene *>(parent.get())) {
isChildOfTransition = true;
}
}
if (isChildOfTransition) {
// 作为 TransitionScene 的子场景时,需要设置正确的投影矩阵
// 使用游戏逻辑分辨率作为投影区域,让 TransitionScene 控制整体视口
auto camera = getActiveCamera();
if (camera) {
// 设置投影矩阵覆盖整个游戏逻辑区域
renderer.setViewProjection(camera->getViewProjectionMatrix());
}
// 渲染场景内容(投影矩阵已设置,直接渲染)
batchUpdateTransforms();
renderer.beginSpriteBatch();
render(renderer);
renderer.endSpriteBatch();
} else {
// 正常渲染时,调用父类的 renderContent 处理视口和投影
renderer.setViewport(static_cast<int>(viewportOffsetX_),
static_cast<int>(viewportOffsetY_),
static_cast<int>(scaledGameWidth_),
static_cast<int>(scaledGameHeight_));
extra2d::Scene::renderContent(renderer);
}
}
} // namespace flappybird

View File

@ -35,6 +35,12 @@ public:
*/
void onRender(extra2d::RenderBackend &renderer) override;
/**
* @brief
* @param renderer
*/
void renderContent(extra2d::RenderBackend &renderer) override;
protected:
/**
* @brief 使

View File

@ -43,7 +43,7 @@ void SplashScene::onUpdate(float dt) {
void SplashScene::gotoStartScene() {
auto &app = extra2d::Application::instance();
app.scenes().replaceScene(extra2d::makePtr<StartScene>(),
extra2d::TransitionType::Fade, 0.5f);
extra2d::TransitionType::Fade, 2.0f);
}
} // namespace flappybird

View File

@ -0,0 +1,138 @@
// ============================================================================
// BaseScene.cpp - Push Box 基础场景实现
// ============================================================================
#include "BaseScene.h"
#include <extra2d/scene/transition_scene.h>
#include <extra2d/utils/logger.h>
namespace pushbox {
BaseScene::BaseScene() {
// 设置背景颜色为黑色(窗口四周会显示这个颜色)
setBackgroundColor(extra2d::Color(0.0f, 0.0f, 0.0f, 1.0f));
}
void BaseScene::onEnter() {
extra2d::Scene::onEnter();
// 计算并更新视口
updateViewport();
}
/**
* @brief 使
*/
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>();
// 设置正交投影,覆盖整个游戏逻辑区域
// 注意对于2D游戏Y轴向下增长所以bottom > top
camera->setViewport(0.0f, GAME_WIDTH, GAME_HEIGHT, 0.0f);
setCamera(camera);
}
/**
* @brief
* @param renderer
*/
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) {
E2D_LOG_INFO("BaseScene::onRender - window size changed from ({} x {}) to "
"({} x {}), updating viewport",
expectedWidth, expectedHeight, currentWindowWidth,
currentWindowHeight);
updateViewport();
}
// 设置视口为居中区域
renderer.setViewport(
static_cast<int>(viewportOffsetX_), static_cast<int>(viewportOffsetY_),
static_cast<int>(scaledGameWidth_), static_cast<int>(scaledGameHeight_));
// 调用父类的 onRender 进行实际渲染
extra2d::Scene::onRender(renderer);
}
/**
* @brief
* @param renderer
*/
void BaseScene::renderContent(extra2d::RenderBackend &renderer) {
// 如果视口参数未初始化onEnter 还没被调用),先初始化
if (scaledGameWidth_ <= 0.0f || scaledGameHeight_ <= 0.0f) {
updateViewport();
}
// 检查窗口大小是否改变
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();
}
// 检查当前场景是否作为 TransitionScene 的子场景被渲染
bool isChildOfTransition = false;
if (auto parent = getParent()) {
if (dynamic_cast<extra2d::TransitionScene *>(parent.get())) {
isChildOfTransition = true;
}
}
if (isChildOfTransition) {
// 作为 TransitionScene 的子场景时,需要设置正确的投影矩阵
// 使用游戏逻辑分辨率作为投影区域,让 TransitionScene 控制整体视口
auto camera = getActiveCamera();
if (camera) {
// 设置投影矩阵覆盖整个游戏逻辑区域
renderer.setViewProjection(camera->getViewProjectionMatrix());
}
// 渲染场景内容(投影矩阵已设置,直接渲染)
batchUpdateTransforms();
renderer.beginSpriteBatch();
render(renderer);
renderer.endSpriteBatch();
} else {
// 正常渲染时,调用父类的 renderContent 处理视口和投影
renderer.setViewport(static_cast<int>(viewportOffsetX_),
static_cast<int>(viewportOffsetY_),
static_cast<int>(scaledGameWidth_),
static_cast<int>(scaledGameHeight_));
extra2d::Scene::renderContent(renderer);
}
}
} // namespace pushbox

View File

@ -0,0 +1,57 @@
// ============================================================================
// BaseScene.h - Push Box 基础场景类
// 描述: 提供统一的居中视口适配功能,所有游戏场景都应继承此类
// ============================================================================
#pragma once
#include <extra2d/extra2d.h>
namespace pushbox {
// 游戏逻辑分辨率
static constexpr float GAME_WIDTH = 640.0f;
static constexpr float GAME_HEIGHT = 480.0f;
/**
* @brief Push Box
*
*/
class BaseScene : public extra2d::Scene {
public:
/**
* @brief
*/
BaseScene();
/**
* @brief
*/
void onEnter() override;
/**
* @brief
* @param renderer
*/
void onRender(extra2d::RenderBackend &renderer) override;
/**
* @brief
* @param renderer
*/
void renderContent(extra2d::RenderBackend &renderer) override;
protected:
/**
* @brief 使
*/
void updateViewport();
// 视口适配参数(用于在窗口中居中显示游戏内容)
float scaledGameWidth_ = 0.0f; // 缩放后的游戏宽度
float scaledGameHeight_ = 0.0f; // 缩放后的游戏高度
float viewportOffsetX_ = 0.0f; // 视口水平偏移
float viewportOffsetY_ = 0.0f; // 视口垂直偏移
};
} // namespace pushbox

View File

@ -1,26 +1,29 @@
// ============================================================================
// PlayScene.cpp - Push Box 游戏场景实现
// ============================================================================
#include "PlayScene.h"
#include "audio_manager.h"
#include "storage.h"
#include "StartScene.h"
#include "SuccessScene.h"
#include "audio_manager.h"
#include "storage.h"
#include <extra2d/extra2d.h>
namespace pushbox {
/**
* @brief
* @param size
*/
static extra2d::Ptr<extra2d::FontAtlas> loadFont(int size) {
auto &resources = extra2d::Application::instance().resources();
auto font = resources.loadFont("assets/font.ttf", size);
return font;
}
PlayScene::PlayScene(int level) {
setBackgroundColor(extra2d::Colors::Black);
PlayScene::PlayScene(int level) : BaseScene() {
auto &app = extra2d::Application::instance();
auto& config = app.getConfig();
setViewportSize(static_cast<float>(config.width), static_cast<float>(config.height));
auto &resources = app.resources();
E2D_LOG_INFO("PlayScene: Loading textures...");
@ -37,30 +40,39 @@ PlayScene::PlayScene(int level) {
texMan_[4] = resources.loadTexture("assets/images/player/manright.gif");
texManPush_[1] = resources.loadTexture("assets/images/player/manhandup.gif");
texManPush_[2] = resources.loadTexture("assets/images/player/manhanddown.gif");
texManPush_[3] = resources.loadTexture("assets/images/player/manhandleft.gif");
texManPush_[4] = resources.loadTexture("assets/images/player/manhandright.gif");
texManPush_[2] =
resources.loadTexture("assets/images/player/manhanddown.gif");
texManPush_[3] =
resources.loadTexture("assets/images/player/manhandleft.gif");
texManPush_[4] =
resources.loadTexture("assets/images/player/manhandright.gif");
font28_ = loadFont(28);
font20_ = loadFont(20);
// 获取窗口尺寸
float screenW = static_cast<float>(app.getConfig().width);
float screenH = static_cast<float>(app.getConfig().height);
// 使用游戏逻辑分辨率
float screenW = GAME_WIDTH;
float screenH = GAME_HEIGHT;
// 计算游戏区域居中偏移(假设游戏区域是 640x480
float gameWidth = 640.0f;
float gameHeight = 480.0f;
float offsetX = (screenW - gameWidth) / 2.0f;
float offsetY = (screenH - gameHeight) / 2.0f;
// 计算游戏区域居中偏移
float offsetX = (screenW - GAME_WIDTH) / 2.0f;
float offsetY = (screenH - GAME_HEIGHT) / 2.0f;
// 音效图标(左上角,与主界面一致
// 音效开关按钮(使用 Button 的切换模式)
auto soundOn = resources.loadTexture("assets/images/soundon.png");
auto soundOff = resources.loadTexture("assets/images/soundoff.png");
if (soundOn && soundOff) {
soundIcon_ = extra2d::Sprite::create(g_SoundOpen ? soundOn : soundOff);
soundIcon_->setPosition(offsetX + 50.0f, offsetY + 50.0f);
addChild(soundIcon_);
soundBtn_ = extra2d::Button::create();
soundBtn_->setToggleMode(true);
soundBtn_->setStateBackgroundImage(soundOff, soundOn);
soundBtn_->setOn(g_SoundOpen);
soundBtn_->setAnchor(0.0f, 0.0f);
soundBtn_->setPosition(offsetX + 50.0f, offsetY + 50.0f);
soundBtn_->setOnStateChange([](bool isOn) {
g_SoundOpen = isOn;
AudioManager::instance().setEnabled(isOn);
});
addChild(soundBtn_);
}
levelText_ = extra2d::Text::create("", font28_);
@ -96,31 +108,38 @@ PlayScene::PlayScene(int level) {
}
void PlayScene::onEnter() {
Scene::onEnter();
updateSoundIcon();
BaseScene::onEnter();
if (soundBtn_) {
soundBtn_->setOn(g_SoundOpen);
}
updateMenuColors();
}
/**
* @brief
*/
void PlayScene::updateMenuColors() {
// 选中的项用红色,未选中的用白色
if (restartText_) {
restartText_->setTextColor(menuIndex_ == 0 ? extra2d::Colors::Red : extra2d::Colors::White);
restartText_->setTextColor(menuIndex_ == 0 ? extra2d::Colors::Red
: extra2d::Colors::White);
}
if (soundToggleText_) {
soundToggleText_->setTextColor(menuIndex_ == 1 ? extra2d::Colors::Red : extra2d::Colors::White);
soundToggleText_->setTextColor(menuIndex_ == 1 ? extra2d::Colors::Red
: extra2d::Colors::White);
}
}
void PlayScene::onUpdate(float dt) {
Scene::onUpdate(dt);
BaseScene::onUpdate(dt);
auto &app = extra2d::Application::instance();
auto &input = app.input();
// B 键返回主菜单
if (input.isButtonPressed(extra2d::GamepadButton::B)) {
app.scenes().replaceScene(
extra2d::makePtr<StartScene>(), extra2d::TransitionType::Fade, 0.2f);
app.scenes().replaceScene(extra2d::makePtr<StartScene>(),
extra2d::TransitionType::Fade, 0.5f);
return;
}
@ -130,11 +149,13 @@ void PlayScene::onUpdate(float dt) {
return;
}
// X键直接切换音效
// X键直接切换音效(备用,按钮也可点击切换)
if (input.isButtonPressed(extra2d::GamepadButton::X)) {
g_SoundOpen = !g_SoundOpen;
AudioManager::instance().setEnabled(g_SoundOpen);
updateSoundIcon();
if (soundBtn_) {
soundBtn_->setOn(g_SoundOpen);
}
return;
}
@ -174,6 +195,9 @@ void PlayScene::onUpdate(float dt) {
gameOver();
}
/**
* @brief
*/
void PlayScene::executeMenuItem() {
switch (menuIndex_) {
case 0: // 重开
@ -182,42 +206,31 @@ void PlayScene::executeMenuItem() {
case 1: // 切换音效
g_SoundOpen = !g_SoundOpen;
AudioManager::instance().setEnabled(g_SoundOpen);
updateSoundIcon();
if (soundBtn_) {
soundBtn_->setOn(g_SoundOpen);
}
break;
}
}
void PlayScene::updateSoundIcon() {
if (!soundIcon_) return;
auto& app = extra2d::Application::instance();
auto& resources = app.resources();
auto soundOn = resources.loadTexture("assets/images/soundon.png");
auto soundOff = resources.loadTexture("assets/images/soundoff.png");
if (soundOn && soundOff) {
soundIcon_->setTexture(g_SoundOpen ? soundOn : soundOff);
}
}
/**
* @brief
*/
void PlayScene::flush() {
mapLayer_->removeAllChildren();
int tileW = texFloor_ ? texFloor_->getWidth() : 32;
int tileH = texFloor_ ? texFloor_->getHeight() : 32;
// 获取窗口尺寸,计算游戏区域居中偏移
auto& app = extra2d::Application::instance();
float screenW = static_cast<float>(app.getConfig().width);
float screenH = static_cast<float>(app.getConfig().height);
float gameWidth = 640.0f;
float gameHeight = 480.0f;
float baseOffsetX = (screenW - gameWidth) / 2.0f;
float baseOffsetY = (screenH - gameHeight) / 2.0f;
// 使用游戏逻辑分辨率
float gameWidth = GAME_WIDTH;
float gameHeight = GAME_HEIGHT;
float baseOffsetX = 0.0f;
float baseOffsetY = 0.0f;
// 在 12x12 网格中居中地图
float mapOffsetX = static_cast<float>((12 - map_.width) / 2) * tileW;
float mapOffsetY = static_cast<float>((12 - map_.height) / 2) * tileH;
float mapOffsetX = static_cast<float>((12.0f - map_.width) / 2.0f) * tileW;
float mapOffsetY = static_cast<float>((12.0f - map_.height) / 2.0f) * tileH;
float offsetX = baseOffsetX + mapOffsetX;
float offsetY = baseOffsetY + mapOffsetY;
@ -259,6 +272,10 @@ void PlayScene::flush() {
}
}
/**
* @brief
* @param level
*/
void PlayScene::setLevel(int level) {
g_CurrentLevel = level;
saveCurrentLevel(g_CurrentLevel);
@ -295,6 +312,10 @@ void PlayScene::setLevel(int level) {
flush();
}
/**
* @brief
* @param step
*/
void PlayScene::setStep(int step) {
step_ = step;
if (stepText_) {
@ -302,12 +323,19 @@ void PlayScene::setStep(int step) {
}
}
/**
* @brief
* @param dx X方向偏移
* @param dy Y方向偏移
* @param direct 1=2=3=4=
*/
void PlayScene::move(int dx, int dy, int direct) {
int targetX = dx + map_.roleX;
int targetY = dy + map_.roleY;
g_Direct = direct;
if (targetX < 0 || targetX >= map_.width || targetY < 0 || targetY >= map_.height) {
if (targetX < 0 || targetX >= map_.width || targetY < 0 ||
targetY >= map_.height) {
return;
}
@ -350,7 +378,8 @@ void PlayScene::move(int dx, int dy, int direct) {
return;
}
if (map_.value[boxY][boxX].type == TYPE::Wall || map_.value[boxY][boxX].type == TYPE::Box) {
if (map_.value[boxY][boxX].type == TYPE::Wall ||
map_.value[boxY][boxX].type == TYPE::Box) {
return;
}
@ -368,6 +397,9 @@ void PlayScene::move(int dx, int dy, int direct) {
setStep(step_ + 1);
}
/**
* @brief
*/
void PlayScene::gameOver() {
int bestStep = loadBestStep(g_CurrentLevel, 0);
if (bestStep == 0 || step_ < bestStep) {
@ -375,8 +407,8 @@ void PlayScene::gameOver() {
}
if (g_CurrentLevel == MAX_LEVEL) {
extra2d::Application::instance().scenes().pushScene(extra2d::makePtr<SuccessScene>(),
extra2d::TransitionType::Fade, 0.25f);
extra2d::Application::instance().scenes().pushScene(
extra2d::makePtr<SuccessScene>(), extra2d::TransitionType::Fade, 0.5f);
return;
}

View File

@ -1,25 +1,76 @@
// ============================================================================
// PlayScene.h - Push Box 游戏场景
// ============================================================================
#pragma once
#include "BaseScene.h"
#include "data.h"
#include <extra2d/extra2d.h>
namespace pushbox {
class PlayScene : public extra2d::Scene {
/**
* @brief Push Box
*/
class PlayScene : public BaseScene {
public:
/**
* @brief
* @param level
*/
explicit PlayScene(int level);
/**
* @brief
*/
void onEnter() override;
/**
* @brief
* @param dt
*/
void onUpdate(float dt) override;
private:
/**
* @brief
*/
void updateMenuColors();
/**
* @brief
*/
void executeMenuItem();
void updateSoundIcon();
/**
* @brief
*/
void flush();
/**
* @brief
* @param level
*/
void setLevel(int level);
/**
* @brief
* @param step
*/
void setStep(int step);
/**
* @brief
* @param dx X方向偏移
* @param dy Y方向偏移
* @param direct 1=2=3=4=
*/
void move(int dx, int dy, int direct);
/**
* @brief
*/
void gameOver();
int step_ = 0;
@ -36,7 +87,7 @@ private:
extra2d::Ptr<extra2d::Text> soundToggleText_;
extra2d::Ptr<extra2d::Node> mapLayer_;
extra2d::Ptr<extra2d::Sprite> soundIcon_;
extra2d::Ptr<extra2d::Button> soundBtn_;
extra2d::Ptr<extra2d::Texture> texWall_;
extra2d::Ptr<extra2d::Texture> texPoint_;

View File

@ -1,18 +1,23 @@
// ============================================================================
// StartScene.cpp - Push Box 开始场景实现
// ============================================================================
#include "StartScene.h"
#include "PlayScene.h"
#include "audio_manager.h"
#include "data.h"
#include "PlayScene.h"
#include <extra2d/extra2d.h>
namespace pushbox {
StartScene::StartScene() {
auto& app = extra2d::Application::instance();
auto& config = app.getConfig();
setViewportSize(static_cast<float>(config.width), static_cast<float>(config.height));
StartScene::StartScene() : BaseScene() {
// BaseScene 已处理视口设置
}
/**
* @brief
*/
static extra2d::Ptr<extra2d::FontAtlas> loadMenuFont() {
auto &resources = extra2d::Application::instance().resources();
auto font = resources.loadFont("assets/font.ttf", 28, true);
@ -20,16 +25,15 @@ static extra2d::Ptr<extra2d::FontAtlas> loadMenuFont() {
}
void StartScene::onEnter() {
Scene::onEnter();
BaseScene::onEnter();
auto &app = extra2d::Application::instance();
auto &resources = app.resources();
setBackgroundColor(extra2d::Colors::Black);
if (getChildren().empty()) {
float screenW = static_cast<float>(app.getConfig().width);
float screenH = static_cast<float>(app.getConfig().height);
// 使用游戏逻辑分辨率
float screenW = GAME_WIDTH;
float screenH = GAME_HEIGHT;
auto bgTex = resources.loadTexture("assets/images/start.jpg");
if (bgTex) {
@ -51,7 +55,6 @@ void StartScene::onEnter() {
}
// 创建菜单按钮(使用 Button 实现文本居中)
// 设置按钮锚点为中心点,位置设为屏幕中心,实现真正的居中
startBtn_ = extra2d::Button::create();
startBtn_->setFont(font_);
startBtn_->setText("新游戏");
@ -94,13 +97,21 @@ void StartScene::onEnter() {
exitBtn_->setPosition(centerX, offsetY + 340.0f);
addChild(exitBtn_);
// 音效开关图标(相对于背景图左上角
// 音效开关按钮(使用 Button 的切换模式
auto soundOn = resources.loadTexture("assets/images/soundon.png");
auto soundOff = resources.loadTexture("assets/images/soundoff.png");
if (soundOn && soundOff) {
soundIcon_ = extra2d::Sprite::create(g_SoundOpen ? soundOn : soundOff);
soundIcon_->setPosition(offsetX + 50.0f, offsetY + 50.0f);
addChild(soundIcon_);
soundBtn_ = extra2d::Button::create();
soundBtn_->setToggleMode(true);
soundBtn_->setStateBackgroundImage(soundOff, soundOn);
soundBtn_->setOn(g_SoundOpen);
soundBtn_->setAnchor(0.0f, 0.0f);
soundBtn_->setPosition(offsetX + 50.0f, offsetY + 50.0f);
soundBtn_->setOnStateChange([](bool isOn) {
g_SoundOpen = isOn;
AudioManager::instance().setEnabled(isOn);
});
addChild(soundBtn_);
}
}
}
@ -111,7 +122,7 @@ void StartScene::onEnter() {
}
void StartScene::onUpdate(float dt) {
Scene::onUpdate(dt);
BaseScene::onUpdate(dt);
auto &app = extra2d::Application::instance();
auto &input = app.input();
@ -130,27 +141,34 @@ void StartScene::onUpdate(float dt) {
executeMenuItem();
}
// X键切换音效
// X键切换音效(备用,按钮也可点击切换)
if (input.isButtonPressed(extra2d::GamepadButton::X)) {
g_SoundOpen = !g_SoundOpen;
AudioManager::instance().setEnabled(g_SoundOpen);
updateSoundIcon();
if (soundBtn_) {
soundBtn_->setOn(g_SoundOpen);
}
}
}
/**
* @brief
*/
void StartScene::updateMenuColors() {
// 根据选中状态更新按钮文本颜色
// 选中的项用红色,未选中的用黑色,禁用的项用深灰色
if (startBtn_) {
startBtn_->setTextColor(selectedIndex_ == 0 ? extra2d::Colors::Red : extra2d::Colors::Black);
startBtn_->setTextColor(selectedIndex_ == 0 ? extra2d::Colors::Red
: extra2d::Colors::Black);
}
if (resumeBtn_) {
// "继续关卡"始终显示,但当 g_CurrentLevel == 1 时禁用(深灰色)
if (g_CurrentLevel > 1) {
// 可用状态:选中为红色,未选中为黑色
resumeBtn_->setTextColor(selectedIndex_ == 1 ? extra2d::Colors::Red : extra2d::Colors::Black);
resumeBtn_->setTextColor(selectedIndex_ == 1 ? extra2d::Colors::Red
: extra2d::Colors::Black);
} else {
// 禁用状态:深灰色 (RGB: 80, 80, 80)
resumeBtn_->setTextColor(extra2d::Color(80, 80, 80, 255));
@ -158,23 +176,14 @@ void StartScene::updateMenuColors() {
}
if (exitBtn_) {
exitBtn_->setTextColor(selectedIndex_ == 2 ? extra2d::Colors::Red : extra2d::Colors::Black);
}
}
void StartScene::updateSoundIcon() {
if (!soundIcon_) return;
auto& app = extra2d::Application::instance();
auto& resources = app.resources();
auto soundOn = resources.loadTexture("assets/images/soundon.png");
auto soundOff = resources.loadTexture("assets/images/soundoff.png");
if (soundOn && soundOff) {
soundIcon_->setTexture(g_SoundOpen ? soundOn : soundOff);
exitBtn_->setTextColor(selectedIndex_ == 2 ? extra2d::Colors::Red
: extra2d::Colors::Black);
}
}
/**
* @brief
*/
void StartScene::executeMenuItem() {
// 始终有3个选项但"继续关卡"(索引1)在 g_CurrentLevel == 1 时禁用
switch (selectedIndex_) {
@ -193,18 +202,26 @@ void StartScene::executeMenuItem() {
}
}
/**
* @brief
*/
void StartScene::startNewGame() {
extra2d::Application::instance().scenes().replaceScene(
extra2d::makePtr<PlayScene>(1), extra2d::TransitionType::Fade, 0.25f);
extra2d::makePtr<PlayScene>(1), extra2d::TransitionType::Fade, 0.5f);
}
/**
* @brief
*/
void StartScene::continueGame() {
extra2d::Application::instance().scenes().replaceScene(
extra2d::makePtr<PlayScene>(g_CurrentLevel), extra2d::TransitionType::Fade, 0.25f);
extra2d::makePtr<PlayScene>(g_CurrentLevel),
extra2d::TransitionType::Fade, 0.5f);
}
void StartScene::exitGame() {
extra2d::Application::instance().quit();
}
/**
* @brief 退
*/
void StartScene::exitGame() { extra2d::Application::instance().quit(); }
} // namespace pushbox

View File

@ -1,28 +1,66 @@
// ============================================================================
// StartScene.h - Push Box 开始场景
// ============================================================================
#pragma once
#include "BaseScene.h"
#include <extra2d/extra2d.h>
namespace pushbox {
class StartScene : public extra2d::Scene {
/**
* @brief Push Box
*/
class StartScene : public BaseScene {
public:
/**
* @brief
*/
StartScene();
/**
* @brief
*/
void onEnter() override;
/**
* @brief
* @param dt
*/
void onUpdate(float dt) override;
private:
/**
* @brief
*/
void updateMenuColors();
void updateSoundIcon();
/**
* @brief
*/
void executeMenuItem();
/**
* @brief
*/
void startNewGame();
/**
* @brief
*/
void continueGame();
/**
* @brief 退
*/
void exitGame();
extra2d::Ptr<extra2d::FontAtlas> font_;
extra2d::Ptr<extra2d::Button> startBtn_;
extra2d::Ptr<extra2d::Button> resumeBtn_;
extra2d::Ptr<extra2d::Button> exitBtn_;
extra2d::Ptr<extra2d::Sprite> soundIcon_;
extra2d::Ptr<extra2d::Button> soundBtn_;
int selectedIndex_ = 0;
int menuCount_ = 3;
};

View File

@ -1,15 +1,20 @@
// ============================================================================
// SuccessScene.cpp - Push Box 通关场景实现
// ============================================================================
#include "SuccessScene.h"
#include <extra2d/extra2d.h>
namespace pushbox {
SuccessScene::SuccessScene() {
auto& app = extra2d::Application::instance();
auto& config = app.getConfig();
setViewportSize(static_cast<float>(config.width), static_cast<float>(config.height));
SuccessScene::SuccessScene() : BaseScene() {
// BaseScene 已处理视口设置
}
/**
* @brief
*/
static extra2d::Ptr<extra2d::FontAtlas> loadMenuFont() {
auto& resources = extra2d::Application::instance().resources();
auto font = resources.loadFont("assets/font.ttf", 28);
@ -17,16 +22,15 @@ static extra2d::Ptr<extra2d::FontAtlas> loadMenuFont() {
}
void SuccessScene::onEnter() {
Scene::onEnter();
BaseScene::onEnter();
auto& app = extra2d::Application::instance();
auto& resources = app.resources();
setBackgroundColor(extra2d::Colors::Black);
if (getChildren().empty()) {
// 获取窗口尺寸
float screenW = static_cast<float>(app.getConfig().width);
float screenH = static_cast<float>(app.getConfig().height);
// 使用游戏逻辑分辨率
float screenW = GAME_WIDTH;
float screenH = GAME_HEIGHT;
auto bgTex = resources.loadTexture("assets/images/success.jpg");
if (bgTex) {
@ -61,7 +65,7 @@ void SuccessScene::onEnter() {
}
void SuccessScene::onUpdate(float dt) {
Scene::onUpdate(dt);
BaseScene::onUpdate(dt);
auto& app = extra2d::Application::instance();
auto& input = app.input();
@ -69,8 +73,8 @@ void SuccessScene::onUpdate(float dt) {
// A键确认返回主菜单
if (input.isButtonPressed(extra2d::GamepadButton::A)) {
auto& scenes = extra2d::Application::instance().scenes();
scenes.popScene(extra2d::TransitionType::Fade, 0.2f);
scenes.popScene(extra2d::TransitionType::Fade, 0.2f);
scenes.popScene(extra2d::TransitionType::Fade, 0.5f);
scenes.popScene(extra2d::TransitionType::Fade, 0.5f);
}
}

View File

@ -1,13 +1,33 @@
// ============================================================================
// SuccessScene.h - Push Box 通关场景
// ============================================================================
#pragma once
#include "BaseScene.h"
#include <extra2d/extra2d.h>
namespace pushbox {
class SuccessScene : public extra2d::Scene {
/**
* @brief Push Box
*/
class SuccessScene : public BaseScene {
public:
/**
* @brief
*/
SuccessScene();
/**
* @brief
*/
void onEnter() override;
/**
* @brief
* @param dt
*/
void onUpdate(float dt) override;
private: