C++游戏开发之旅 32

问题概述

需要完成游戏暂停功能

因为之前的暂停效果是在游戏场景上加一个菜单场景,这样场景管理器会只更新栈顶的场景,所以看上去游戏世界就好像停止了

这章节引入了一个新的概念:全局游戏状态(Game State)

核心思想:

  1. 引入GameState管理游戏状态
  2. 集成到Context全局访问
  3. 条件式更新物理和相机
  4. 创建MenuScene实现暂停

暂停系统架构:

┌──────────────────────────┐
│      Context             │ ← 全局上下文
│  ┌────────────────────┐  │
│  │ GameState          │  │ ← 状态管理器 ✨
│  │ • Title            │  │
│  │ • Playing          │  │
│  │ • Paused           │  │
│  │ • GameOver         │  │
│  └────────────────────┘  │
└────────┬─────────────────┘
         │ 查询状态
         ↓
┌──────────────────────────┐
│      Scene::update()     │ ← 条件更新
│  if (isPlaying()) {      │
│    物理更新 ✅           │
│    相机更新 ✅           │
│  } else {                │
│    跳过更新 ⏸️           │
│  }                       │
└──────────────────────────┘

第一部分:GameState 设计

全局游戏状态,我们的游戏是在运行不错,但是有的时候,我们需要对其进行更精细的划分,游戏可能处于"标题界面","正常游玩"或是"暂停"等不同状态,GameScene为此而生,它负责跟踪游戏的这些宏观状态。

Title 	标题界面
	↓ 开始游戏
Playing	 游戏进行中
	↓ 按下暂停键
Paused	游戏暂停
	↓ 继续
Playing 继续玩
	↓ 死了
GameOver 结束界面

GameState

image-20260314145944986

我们需要定义State来表示相关状态

src/engine/core/game_state.h

#pragma once 
#include <SDL3/SDL.h>
#include <glm/vec2.hpp>

namespace engine::core{

enum class State{
    TITLE,
    PLAYING,
    PAUSED,
    GAME_OVER,
};

class GameState{
private:
    State current_state_;
    SDL_Window* window_ = nullptr;
    SDL_Renderer* renderer_ = nullptr; 

public:
    GameState(SDL_Window* window, SDL_Renderer* renderer, State init_state);
    ~GameState() = default;

    // 禁止拷贝和移动
    GameState(const GameState&) = delete;
    GameState& operator=(const GameState&) = delete;
    GameState(GameState&&) = delete;
    GameState& operator=(GameState&&) = delete;

    State getCurrentState() const { return current_state_; }
    void setCurrentState(State state){ current_state_ = state; }

    glm::vec2 getWindowSize() const;
    void setWindowSize(glm::vec2 size);

    glm::vec2 getLogicSize() const;
    void setLogicSize(glm::vec2 size);

    // 状态判断
    bool isPlaying() const { return current_state_ == State::PLAYING; }
    bool isPaused() const { return current_state_ == State::PAUSED; }
    bool isGameOver() const { return current_state_ == State::GAME_OVER; }
    bool isTitle() const { return current_state_ == State::TITLE; }

};

}


状态 说明 典型场景 更新行为
Title 标题界面 TitleScene UI更新,背景滚动
Playing 游戏进行中 GameScene 完整更新
Paused 游戏暂停 MenuScene叠加 只更新UI
GameOver 游戏结束 GameOverScene 只更新UI

第二部分:GameState 集成

将GameState整合到Context中,让引擎各个部分可以感知到

第三部分:调整Scene基类

我们希望在非Playing的游戏状态,不要更新物理引擎和相机。

void Scene::update(float delta_time)
    {
        if(!is_initialized_) return;

        if(context_.getGameState().isPlaying()) {
            // 更新物理引擎
            context_.getPhysicsEngine().update(delta_time);
            // 更新相机
            context_.getCamera().update(delta_time);
        }

        // 更新所有游戏对象,并删除需要移除的对象
        for (auto it = game_objects_.begin(); it != game_objects_.end();){
            if(*it && !(*it)->needRemove()){
                (*it)->update(delta_time,context_);
                ++it;
            } else {
                if(*it){  // 安全的删除需要移除对象
                    (*it)->clean(); // 清理对象
                }
                it = game_objects_.erase(it); // 删除游戏对象,智能指针自动管理内存
            }
        }
        // 更新UI
        ui_manager_->update(delta_time, context_);

        // 处理待添加(延时添加)的游戏对象
        processPendingAdditions();
    }

更新策略

这里游戏对象是正常更新的,因为希望场景中的游戏对象或者ui正常更新。

这样游戏状态切换到Pause,整个游戏世界的动态就停了,而UI系统依然是正常工作的。

第四部分:MenuScene

现在是时候创建菜单场景了,和标题场景类似,也是四个按钮,对应不同的逻辑

// src/game/scene/menu_scene.h
#pragma once 
#include "../../engine/scene/scene.h"
#include <glm/vec2.hpp>
#include <memory>

namespace game::data{
class SessionData;
}


namespace game::scene {
class MenuScene : public engine::scene::Scene {
    std::shared_ptr<game::data::SessionData> game_session_data_ = nullptr;
public:
    MenuScene(
        engine::core::Context& context,
        engine::scene::SceneManager& sceneManager,
        std::shared_ptr<game::data::SessionData> game_session_data
    );
    ~MenuScene() override = default;

    // 禁止拷贝和移动
    MenuScene(const MenuScene&) = delete;
    MenuScene& operator=(const MenuScene&) = delete;
    MenuScene(MenuScene&&) = delete;
    MenuScene& operator=(MenuScene&&) = delete;

    void init() override;
    void handleInput() override;

private:
    void createUI();

    // 按键回调函数
    void OnResumeGameClick();
    void OnSaveGameClick();
    void OnReStartGameClick();
    void OnBackGameClick();

};

}
// src/game/scene/menu_scene.cpp
#include "menu_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/core/game_state.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 "title_scene.h"
#include <spdlog/spdlog.h>

namespace game::scene{
    MenuScene::MenuScene(engine::core::Context &context, engine::scene::SceneManager &sceneManager, std::shared_ptr<game::data::SessionData> game_session_data)
    :Scene("MenuScene", context, sceneManager), game_session_data_(game_session_data)
    {}

    void MenuScene::init()
    {
        if(is_initialized_) return;

        context_.getGameState().setCurrentState(engine::core::State::PAUSED);
        createUI();
        Scene::init();

    }

    void MenuScene::handleInput()
    {
        auto &input_manager = context_.getInputManager();
        if(input_manager.isActionReleased("pause")){
            OnResumeGameClick();
        }
        Scene::handleInput();
    }

    void MenuScene::createUI()
    {
        auto window_size = context_.getGameState().getLogicSize();
        if(!ui_manager_->init(window_size)){
            spdlog::error("UIManager初始化失败");
            return;
        }

        // 创建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_resume = std::make_unique<engine::ui::UIButton>(
            context_, 
            "assets/textures/UI/buttons/Resume1.png",  // normal
            "assets/textures/UI/buttons/Resume2.png",  // hover
            "assets/textures/UI/buttons/Resume3.png",  // pressed
            "assets/audio/button_hover.wav",
            "assets/audio/button_click.wav",
            glm::vec2(0),
            glm::vec2(0),
            [this]() { this->OnResumeGameClick(); }
        );
        panel->addChild(std::move(button_resume));

        // 加载游戏
        auto button_save = std::make_unique<engine::ui::UIButton>(
            context_,
            "assets/textures/UI/buttons/Save1.png",  // normal
            "assets/textures/UI/buttons/Save2.png",  // hover
            "assets/textures/UI/buttons/Save3.png",  // pressed
            "assets/audio/button_hover.wav",
            "assets/audio/button_click.wav",
            glm::vec2(0),
            glm::vec2(0),
            [this]() { this->OnSaveGameClick(); }
        );
        panel->addChild(std::move(button_save));

        // 帮助
        auto button_restart = std::make_unique<engine::ui::UIButton>(
            context_,
            "assets/textures/UI/buttons/ReStart1.png",  // normal
            "assets/textures/UI/buttons/ReStart2.png",  // hover
            "assets/textures/UI/buttons/ReStart3.png",  // pressed
            "assets/audio/button_hover.wav",
            "assets/audio/button_click.wav",
            glm::vec2(0),
            glm::vec2(0),
            [this]() { this->OnReStartGameClick(); }
        );
        panel->addChild(std::move(button_restart));

        // 退出游戏
        auto button_back = std::make_unique<engine::ui::UIButton>(
            context_,
            "assets/textures/UI/buttons/Back1.png",  // normal
            "assets/textures/UI/buttons/Back2.png",  // hover
            "assets/textures/UI/buttons/Back3.png",  // pressed
            "assets/audio/button_hover.wav",
            "assets/audio/button_click.wav",
            glm::vec2(0),
            glm::vec2(0),
            [this]() { this->OnBackGameClick(); }
        );
        panel->addChild(std::move(button_back));

        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(), "Pause Menu",
            "assets/fonts/VonwaonBitmap-16px.ttf",24,
            glm::vec2(window_size.x / 2 - 50.f, window_size.y / 2 - 30.0f),
            engine::utils::FColor(1.0f, 1.0f, 0.0f, 1.0f)
        );
        ui_manager_->addElement(std::move(ui_text));


    }

    void MenuScene::OnResumeGameClick()
    {
        sceneManager_.requestPopScene();
        context_.getGameState().setCurrentState(engine::core::State::PLAYING);
    }

    void MenuScene::OnSaveGameClick()
    {
        // 保存游戏
        game_session_data_->saveToFile("assets/save_test.json");
    }

    void MenuScene::OnReStartGameClick()
    {
        // 重新开始游戏
        auto game_session_data = std::make_shared<game::data::SessionData>();
        game_session_data->setMapPath("assets/maps/level1.tmj");
        auto new_scene = std::make_unique<game::scene::GameScene>(context_, sceneManager_, game_session_data);
        sceneManager_.requestReplaceScene(std::move(new_scene));
    }

    void MenuScene::OnBackGameClick()
    {
        // 回到标题场景
        auto new_scene = std::make_unique<game::scene::TitleScene>(context_, sceneManager_);
        sceneManager_.requestReplaceScene(std::move(new_scene));
    }
}

按钮功能

按钮 功能 操作 状态变化
继续游戏 恢复游戏 Pop + setState(Playing) Paused→Playing
保存游戏 保存进度 保存数据 保持Paused
返回标题 回到主菜单 Replace TitleScene Paused→Title
ESC键 快捷恢复 同继续游戏 Paused→Playing

第五部分:触发暂停

最后我们在GameScene中监听暂停输入,触发MenuScene压入

void GameScene::init()
    {
        if(is_initialized_) return;

        if (!initLevel()) {
            spdlog::error("关卡初始化失败,无法继续。");
            context_.getInputManager().setShouldQuit(true);
            return;
        }
        if (!initPlayer()) {
            spdlog::error("玩家初始化失败,无法继续。");
            context_.getInputManager().setShouldQuit(true);
            return;
        }
        if (!initEnemyAndItem()) {
            spdlog::error("敌人初始化失败,无法继续。");
            context_.getInputManager().setShouldQuit(true);
            return;
        }
        if(!initUI()){
            spdlog::error("UI初始化失败,无法继续。");
            context_.getInputManager().setShouldQuit(true);
            return;
        }
        updateHealthUI();

        context_.getGameState().setCurrentState(engine::core::State::PLAYING);
        // 完成后,调用父类的init
        Scene::init();
    }

void GameScene::handleInput()
    {
        Scene::handleInput();
        pauseMenu();
    }

    void GameScene::pauseMenu()
    {
        auto& input_manager = context_.getInputManager();
        if (input_manager.isActionReleased("pause")) {
            auto menu_scene = std::make_unique<game::scene::MenuScene>(
            context_, sceneManager_, game_session_data_);
            sceneManager_.requestPushScene(std::move(menu_scene));
        }
    }

暂停触发流程

暂停触发流程:
    GameScene运行
    (状态: Playing)
         ↓
    用户按ESC键
         ↓
    handleInput()检测到
         ↓
    requestPushScene(MenuScene)
         ↓
    MenuScene::init()
         ↓
    setState(Paused)
         ↓
    Scene::update()检测状态
         ↓
    跳过物理和相机更新
         ↓
    游戏冻结!

运行测试

游戏可以实现正常的暂停和相应的功能

章节总结

这节完成了游戏中的暂停功能,我们通过定义一个GameState状态进行了条件式的更新,使得游戏场景中的游戏对象实现真正的暂停。

遇到的问题

1.刚开始还以为设置这个GameState是多余的,我以为暂停就是只要更新了顶层的场景就算是暂停了,可是我们的场景基类Scene的update是这样的

void Scene::update(float delta_time)
    {
        if(!is_initialized_) return;

        // 更新物理引擎
        context_.getPhysicsEngine().update(delta_time);
        // 更新相机
        context_.getCamera().update(delta_time);

        // 更新所有游戏对象,并删除需要移除的对象
        for (auto it = game_objects_.begin(); it != game_objects_.end();){
            if(*it && !(*it)->needRemove()){
                (*it)->update(delta_time,context_);
                ++it;
            } else {
                if(*it){  // 安全的删除需要移除对象
                    (*it)->clean(); // 清理对象
                }
                it = game_objects_.erase(it); // 删除游戏对象,智能指针自动管理内存
            }
        }
        // 更新UI
        ui_manager_->update(delta_time, context_);

        // 处理待添加(延时添加)的游戏对象
        processPendingAdditions();
    }

我们会先更新物理引擎、相机然后再是游戏对象。如果没有这个判断

if (context_.getGameState().isPlaying()){
        context_.getPhysicsEngine().update(delta_time);
        context_.getCamera().update(delta_time);
}

那么,在更新Menu场景的时候,它也会调用物理引擎和相机,这样会去更新注册到物理引擎中的游戏对象的位置,从效果来看就像是游戏对象是定住了,但没有完全定住,还是在动。

在本节课中,引入了全局游戏状态管理的概念。GameState 不仅仅是一个简单的枚举变量,而是一个中心化的状态管理器,负责协调整个游戏的宏观行为。通过将 GameState 集成到 Context 中,我们让所有模块都能感知当前的游戏状态,并据此做出相应的行为调整。条件式更新是实现真暂停的关键——只在 Playing 状态下更新物理和相机,而 UI 始终保持活跃以支持菜单交互。

posted @ 2026-03-14 20:21  wenyiGamecpp  阅读(34)  评论(0)    收藏  举报