C++游戏开发之旅 31

问题概述

这节完成标题场景,也就是主菜单(Title Scene)

我们的游戏应该首先进入主菜单,然后有四个按钮,开始新游戏加载游戏帮助退出

第一部分:调整游戏入口

首先,我们的GameAppTitleScene启动。

我们需要调整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
│  └────────────────────┘  │   ↑
└──────────────────────────┘
         ↑
    三种切换方式
posted @ 2026-03-14 13:26  wenyiGamecpp  阅读(22)  评论(0)    收藏  举报