C++游戏开发之旅 31
问题概述
这节完成标题场景,也就是主菜单(Title Scene)
我们的游戏应该首先进入主菜单,然后有四个按钮,开始新游戏,加载游戏,帮助,退出。
第一部分:调整游戏入口
首先,我们的GameApp从TitleScene启动。
我们需要调整GameApp中的初始化创建。
// ...
#include "../scene/scene_manager.h"
// #include "../../game/scene/game_scene.h" // 旧的入口
#include "../../game/scene/title_scene.h" // 新的入口
// ...
bool GameApp::init() {
// ... (其他初始化代码不变) ...
if (!initSceneManager()) return false;
// 创建第一个场景并压入栈 (现在是 TitleScene)
auto scene = std::make_unique<game::scene::TitleScene>(*context_, *scene_manager_);
scene_manager_->requestPushScene(std::move(scene));
is_running_ = true;
return true;
}
第二部分:标题场景设计
TitleScene是一个完整的场景,需要有自己的背景,UI和逻辑,我们设计了四个按钮
TitleScene 设计
src/game/scene/title_scene.h
#pragma once
#include "../../engine/scene/scene.h"
#include <glm/vec2.hpp>
#include <memory>
namespace game::data{
class SessionData;
}
namespace game::scene {
class TitleScene : public engine::scene::Scene {
private:
std::shared_ptr<data::SessionData> game_session_data_; // 场景间共享数据
public:
TitleScene(
engine::core::Context& context,
engine::scene::SceneManager& sceneManager,
std::shared_ptr<data::SessionData> game_session_data = nullptr
);
~TitleScene() override = default;
// 禁止拷贝和移动
TitleScene(const TitleScene&) = delete;
TitleScene& operator=(const TitleScene&) = delete;
TitleScene(TitleScene&&) = delete;
TitleScene& operator=(TitleScene&&) = delete;
void init() override;
void update(float deltaTime) override;
private:
void createUI();
// 按键回调函数
void OnStartGameClick();
void OnLoadGameClick();
void OnHelpGameClick();
void OnExitGameClick();
};
}
src/game/scene/title_scene.cpp
#include "title_scene.h"
#include "../../engine/ui/ui_panel.h"
#include "../../engine/ui/ui_image.h"
#include "../../engine/ui/ui_label.h"
#include "../../engine/ui/ui_button.h"
#include "../../engine/ui/ui_manager.h"
#include "../../engine/core/context.h"
#include "../../engine/render/text_renderer.h"
#include "../../engine/input/input_manager.h"
#include "../../engine/audio/audio_player.h"
#include "../../engine/resource/resource_manager.h"
#include "../../engine/scene/scene_manager.h"
#include "../../engine/render/camera.h"
#include "../../engine/scene/level_loader.h"
#include "../data/session_data.h"
#include "../../engine/utils/math.h"
#include "game_scene.h"
#include "help_scene.h"
#include <spdlog/spdlog.h>
namespace game::scene {
TitleScene::TitleScene(engine::core::Context &context, engine::scene::SceneManager &sceneManager, std::shared_ptr<data::SessionData> game_session_data)
: Scene("TitleScene", context, sceneManager), game_session_data_(game_session_data)
{
if(game_session_data_ == nullptr){
game_session_data_ = std::make_shared<data::SessionData>();
}
}
void TitleScene::init()
{
if(is_initialized_) return;
engine::scene::LevelLoader loader;
// 使用辅助函数根据场景名获取路径
auto level_path = game_session_data_->getMapPath();
if(!loader.loadLevel(level_path, *this)) {
spdlog::error("关卡{}加载失败,无法继续", level_path);
}
createUI();
Scene::init();
spdlog::info("TitleScene初始化完成");
}
void TitleScene::update(float deltaTime)
{
Scene::update(deltaTime);
// 移动相机
context_.getCamera().move(deltaTime * glm::vec2(50,0));
}
void TitleScene::createUI()
{
auto window_size = glm::vec2(640.0f,360.0f);
if(!ui_manager_->init(window_size)){
spdlog::error("UIManager初始化失败");
return;
}
// 音量设置
context_.getAudioPlayer().setMusicVolume(0.2f);
context_.getAudioPlayer().setSoundVolume(0.5f);
context_.getAudioPlayer().playMusic("assets/audio/platformer_level03_loop.ogg");
// 创建标题图片
auto title_image = std::make_unique<engine::ui::UIImage>(
"assets/textures/UI/title-screen.png"
);
auto size = context_.getResourceManager().getTextureSize(title_image->getSprite().getTextureId());
title_image->setPosition(glm::vec2(window_size.x / 2 - size.x / 2, window_size.y / 2 - 80.f));
ui_manager_->addElement(std::move(title_image));
// 创建Panel 把按钮放进去,然后把Panel放进去
float button_width = 96.0f;
float button_height = 32.0f;
float button_spacing = 20.0f;
float button_nums = 4;
glm::vec2 button_space = glm::vec2(
button_width * button_nums + button_spacing * (button_nums - 1),
button_height
);
auto panel = std::make_unique<engine::ui::UIPanel>(
glm::vec2(0),
button_space
);
// 开始新游戏
auto button_start = std::make_unique<engine::ui::UIButton>(
context_,
"assets/textures/UI/buttons/Start1.png", // normal
"assets/textures/UI/buttons/Start2.png", // hover
"assets/textures/UI/buttons/Start3.png", // pressed
"assets/audio/button_hover.wav",
"assets/audio/button_click.wav",
glm::vec2(0),
glm::vec2(0),
[this]() { this->OnStartGameClick(); }
);
panel->addChild(std::move(button_start));
// 加载游戏
auto button_load = std::make_unique<engine::ui::UIButton>(
context_,
"assets/textures/UI/buttons/Load1.png", // normal
"assets/textures/UI/buttons/Load2.png", // hover
"assets/textures/UI/buttons/Load3.png", // pressed
"assets/audio/button_hover.wav",
"assets/audio/button_click.wav",
glm::vec2(0),
glm::vec2(0),
[this]() { this->OnLoadGameClick(); }
);
panel->addChild(std::move(button_load));
// 帮助
auto button_help = std::make_unique<engine::ui::UIButton>(
context_,
"assets/textures/UI/buttons/Helps1.png", // normal
"assets/textures/UI/buttons/Helps2.png", // hover
"assets/textures/UI/buttons/Helps3.png", // pressed
"assets/audio/button_hover.wav",
"assets/audio/button_click.wav",
glm::vec2(0),
glm::vec2(0),
[this]() { this->OnHelpGameClick(); }
);
panel->addChild(std::move(button_help));
// 退出游戏
auto button_exit = std::make_unique<engine::ui::UIButton>(
context_,
"assets/textures/UI/buttons/Quit1.png", // normal
"assets/textures/UI/buttons/Quit2.png", // hover
"assets/textures/UI/buttons/Quit3.png", // pressed
"assets/audio/button_hover.wav",
"assets/audio/button_click.wav",
glm::vec2(0),
glm::vec2(0),
[this]() { this->OnExitGameClick(); }
);
panel->addChild(std::move(button_exit));
for(int i = 0; i < panel->getChildren().size(); i++){
const auto& button = panel->getChildren()[i];
button->setPosition(glm::vec2(i*(button_width + button_spacing), 0));
}
// 把Panel 放到中间
panel->setPosition(glm::vec2(window_size.x / 2 - button_space.x / 2, window_size.y / 2 + 25.f));
ui_manager_->addElement(std::move(panel));
// 创建文字
auto ui_text = std::make_unique<engine::ui::UILabel>(
&context_.getTextRenderer(), "SunLandy Credits: XXX-2026",
"assets/fonts/VonwaonBitmap-16px.ttf",16,
glm::vec2(window_size.x / 2 - 100.f, window_size.y - 16.0f),
engine::utils::FColor(1.0f, 1.0f, 1.0f, 1.0f)
);
ui_manager_->addElement(std::move(ui_text));
}
void TitleScene::OnStartGameClick()
{
game_session_data_->reset();
auto scene = std::make_unique<game::scene::GameScene>(context_, sceneManager_, game_session_data_);
sceneManager_.requestReplaceScene(std::move(scene));
}
void TitleScene::OnLoadGameClick()
{
// TODO: 加载游戏
if(game_session_data_->loadFromFile("assets/save_test.json")){
spdlog::info("开始加载游戏");
auto scene = std::make_unique<game::scene::GameScene>(context_, sceneManager_, game_session_data_);
sceneManager_.requestReplaceScene(std::move(scene));
} else {
spdlog::error("加载游戏失败");
}
}
void TitleScene::OnHelpGameClick()
{
auto help_scene = std::make_unique<game::scene::HelpScene>(context_, sceneManager_);
sceneManager_.requestPushScene(std::move(help_scene));
}
void TitleScene::OnExitGameClick()
{
auto& input_manager = context_.getInputManager();
input_manager.setShouldQuit(true);
}
}
这部分难度并不大,简单介绍一下,我们标题场景的背景是缓慢移动的,所以让我们的相机缓慢朝向右边移动,由于视差组件,效果就完成了。
其次,完成四个按钮的UI绘制,通过回调函数编写相应的逻辑事件:
- Start --- 重新开始游戏,
game_session_data重置一下,然后作为参数传入GameScene场景创建,请求替换场景 - Load --- 加载游戏,通过读取存档,加载游戏关卡
- Help --- 压入一个场景
- Quit --- 发送退出请求
UI布局设计
这部分主要是一些计算了,将UI按钮先绘制出来放到一个UIPanel中,最后通过移动父类panel就可以整体调整位置了。
第三部分:场景切换
这里加深一下之前场景管理器SceneManager的印象,因为场景管理器是维护着一个场景栈,我们通过发送不同的请求来操作,更新逻辑update是只作用栈顶场景,渲染则是都有的。
1. Replace - 替换所有
2. Push - 压入新场景
3. Pop - 弹出顶层
第四部分:帮助场景实现
场景重叠,我们的帮助场景是通过push压入场景栈的,里面也很简单,只是一个UIImage。
// src/game/scene/helps_scene.h
#pragma once
#include "../../engine/scene/scene.h"
#include <glm/vec2.hpp>
namespace game::scene {
class HelpScene : public engine::scene::Scene {
public:
HelpScene(
engine::core::Context& context,
engine::scene::SceneManager& sceneManager
);
~HelpScene() override = default;
// 禁止拷贝和移动
HelpScene(const HelpScene&) = delete;
HelpScene& operator=(const HelpScene&) = delete;
HelpScene(HelpScene&&) = delete;
HelpScene& operator=(HelpScene&&) = delete;
void init() override;
void handleInput() override;
};
}
// src/game/scene/helps_scene.cpp
#include "help_scene.h"
#include "../../engine/ui/ui_image.h"
#include "../../engine/ui/ui_manager.h"
#include "../../engine/core/context.h"
#include "../../engine/input/input_manager.h"
#include "../../engine/scene/scene_manager.h"
namespace game::scene {
HelpScene::HelpScene(engine::core::Context &context, engine::scene::SceneManager &sceneManager)
: Scene("HelpScene",context, sceneManager){}
void HelpScene::init()
{
if(is_initialized_) return;
// 创建一个UIImage
auto image = std::make_unique<engine::ui::UIImage>(
"assets/textures/UI/instructions.png",
glm::vec2(0),
glm::vec2(640.0f,360.0f)
);
ui_manager_->addElement(std::move(image));
Scene::init();
}
void HelpScene::handleInput()
{
if(!is_initialized_) return;
auto input_manager = context_.getInputManager();
if(input_manager.isActionReleased("MouseLeftClick")){
sceneManager_.requestPopScene();
}
}
}
运行测试
游戏可以实现相应功能,还有一个小细节就是在游戏场景初始化的时候要设置一下相机位置,因为在标题场景中我们让相机朝向右边移动了,而我们相机跟随玩家是滑动跟随。
总结
完整场景系统框架
场景系统完整架构:
┌──────────────────────────┐
│ GameApp │ ← 应用层
│ ┌────────────────────┐ │
│ │ SceneManager │ │ ← 管理层
│ │ ┌────────────────┐ │ │
│ │ │ Scene Stack │ │ │ ← 堆栈 ✨
│ │ │ │ │ │
│ │ │ ┌────────────┐ │ │ │
│ │ │ │HelpsScene │ │ │ │ Pop
│ │ │ ├────────────┤ │ │ │ ↑
│ │ │ │TitleScene │ │ │ │ Push
│ │ │ └────────────┘ │ │ │ ↑
│ │ └────────────────┘ │ │ Replace
│ └────────────────────┘ │ ↑
└──────────────────────────┘
↑
三种切换方式

浙公网安备 33010602011771号