C++游戏开发之旅 32
问题概述
需要完成游戏暂停功能
因为之前的暂停效果是在游戏场景上加一个菜单场景,这样场景管理器会只更新栈顶的场景,所以看上去游戏世界就好像停止了
这章节引入了一个新的概念:全局游戏状态(Game State)
核心思想:
- 引入GameState管理游戏状态
- 集成到Context全局访问
- 条件式更新物理和相机
- 创建MenuScene实现暂停
暂停系统架构:
┌──────────────────────────┐
│ Context │ ← 全局上下文
│ ┌────────────────────┐ │
│ │ GameState │ │ ← 状态管理器 ✨
│ │ • Title │ │
│ │ • Playing │ │
│ │ • Paused │ │
│ │ • GameOver │ │
│ └────────────────────┘ │
└────────┬─────────────────┘
│ 查询状态
↓
┌──────────────────────────┐
│ Scene::update() │ ← 条件更新
│ if (isPlaying()) { │
│ 物理更新 ✅ │
│ 相机更新 ✅ │
│ } else { │
│ 跳过更新 ⏸️ │
│ } │
└──────────────────────────┘
第一部分:GameState 设计
全局游戏状态,我们的游戏是在运行不错,但是有的时候,我们需要对其进行更精细的划分,游戏可能处于"标题界面","正常游玩"或是"暂停"等不同状态,GameScene为此而生,它负责跟踪游戏的这些宏观状态。
Title 标题界面
↓ 开始游戏
Playing 游戏进行中
↓ 按下暂停键
Paused 游戏暂停
↓ 继续
Playing 继续玩
↓ 死了
GameOver 结束界面
GameState
我们需要定义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 始终保持活跃以支持菜单交互。

浙公网安备 33010602011771号