C++游戏开发之旅 33 & 结束
最后一章了,需要完成失败和胜利的场景,也就是结束场景
结束场景
如果玩家胜利了,显示YOU WIN!,失败了则显示YOU DIED!;显示得分信息,历史最高分,提供操作选项,重新开始或是返回主菜单。
第一部分:EndScene 实现
该场景需要根据游戏结果,向玩家显示不同的信息,并提供"重新开始"或"返回主菜单"的选项。
功能:
- 胜利 --- 显示 You Win!
- 失败 --- 显示 You Lose!
- 显示本次得分以及历史最高分
- 提供选项按钮,重新开始和返回主菜单
EndScene主要逻辑在createUI方法中,通过在SessionData中新增加一个is_win_标志,可以动态地决定显示胜利或是失败,当然也可以改变文本的颜色。
// src/game/scene/end_scene.h
#pragma once
#include "../../engine/scene/scene.h"
#include <glm/vec2.hpp>
#include <memory>
namespace game::data{
class SessionData;
}
namespace game::scene {
class EndScene : public engine::scene::Scene {
std::shared_ptr<game::data::SessionData> game_session_data_ = nullptr;
public:
EndScene(
engine::core::Context& context,
engine::scene::SceneManager& sceneManager,
std::shared_ptr<game::data::SessionData> game_session_data
);
~EndScene() override = default;
// 禁止拷贝和移动
EndScene(const EndScene&) = delete;
EndScene& operator=(const EndScene&) = delete;
EndScene(EndScene&&) = delete;
EndScene& operator=(EndScene&&) = delete;
void init() override;
void handleInput() override;
private:
void createUI();
// 按键回调函数
void OnRestartGameClick();
void OnBackGameClick();
};
}
// src/game/scene/end_scene.cpp
#include "end_scene.h"
#include "../../engine/ui/ui_panel.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/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 <fstream>
#include <spdlog/spdlog.h>
namespace game::scene {
EndScene::EndScene(engine::core::Context &context, engine::scene::SceneManager &sceneManager, std::shared_ptr<game::data::SessionData> game_session_data)
: Scene("EndScene", context, sceneManager), game_session_data_(game_session_data)
{
}
void EndScene::init()
{
if(is_initialized_) return;
context_.getGameState().setCurrentState(engine::core::State::PAUSED);
createUI();
Scene::init();
}
void EndScene::handleInput()
{
Scene::handleInput();
}
void EndScene::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 = 2;
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_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 + 10.f, window_size.y / 2 + 25.f));
ui_manager_->addElement(std::move(panel));
bool is_win = game_session_data_->isWin();
// 创建文字
auto ui_text = std::make_unique<engine::ui::UILabel>(
&context_.getTextRenderer(), "",
"assets/fonts/VonwaonBitmap-16px.ttf",32,
glm::vec2(window_size.x / 2 - 50.f, window_size.y / 2 - 80.0f),
engine::utils::FColor(1.0f, 1.0f, 0.0f, 1.0f)
);
if(is_win){
ui_text->setText("You Win!");
ui_text->setColor(engine::utils::FColor(0.0f, 1.0f, 0.0f, 1.0f));
} else{
ui_text->setText("You Lose!");
ui_text->setColor(engine::utils::FColor(1.0f, 0.0f, 0.0f, 1.0f));
}
ui_manager_->addElement(std::move(ui_text));
// 创建得分标签 最高分和当前分
auto high_score_text = std::make_unique<engine::ui::UILabel>(
&context_.getTextRenderer(), "",
"assets/fonts/VonwaonBitmap-16px.ttf",16,
glm::vec2(window_size.x / 2 - 50.f, window_size.y / 2 - 40.0f),
engine::utils::FColor(1.0f, 1.0f, 0.0f, 1.0f)
);
std::ifstream file("assets/high_score.txt");
int high_score_value = game_session_data_->getHighScore();
int file_high_score = 0;
if(file.is_open()){
file >> file_high_score;
file.close();
} else { // 如果文件不存在,则创建一个新文件
std::ofstream new_file("assets/high_score.txt");
new_file << high_score_value;
new_file.close();
}
if(high_score_value > file_high_score){
std::ofstream file_o("assets/high_score.txt");
file_o << high_score_value;
file_o.close();
}
high_score_text->setText("High Score: " + std::to_string(file_high_score));
ui_manager_->addElement(std::move(high_score_text));
auto current_score_text = std::make_unique<engine::ui::UILabel>(
&context_.getTextRenderer(), "",
"assets/fonts/VonwaonBitmap-16px.ttf",16,
glm::vec2(window_size.x / 2 - 50.f, window_size.y / 2 - 10.f),
engine::utils::FColor(1.0f, 1.0f, 1.0f, 1.0f)
);
auto current_score_value = game_session_data_->getCurrentScore();
current_score_text->setText("Current Score: " + std::to_string(current_score_value));
if(current_score_value >= file_high_score){
current_score_text->setColor(engine::utils::FColor(1.0f, 1.0f, 0.0f, 1.0f));
}
ui_manager_->addElement(std::move(current_score_text));
}
void EndScene::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 EndScene::OnBackGameClick()
{
// 回到标题场景
auto new_scene = std::make_unique<game::scene::TitleScene>(context_, sceneManager_);
sceneManager_.requestReplaceScene(std::move(new_scene));
}
}
第二部分:触发游戏结束
我们需要决定游戏结束条件,当玩家掉入到悬崖了,或者血量为0,那么就死了,就会进入结束场景。我们可以仿照之前的切换场景的做法,也加一个触发器,在悬崖下画个矩形触发,这样当玩家接触到就死了,或者没血死了进入结束失败场景;而玩家胜利,可以在关卡尾部画个胜利触发,也是同样的道理。
实现结束场景
void GameScene::toEndScene(engine::object::GameObject *trigger)
{
auto trig_name = trigger->getName();
if(trig_name == "lose"){
game_session_data_->setWin(false);
handlePlayerDamage(3); // 扣除最大血量,保证死了
} else if(trig_name == "win"){
game_session_data_->setWin(true);
}
auto scene = std::make_unique<game::scene::EndScene>(context_, sceneManager_, game_session_data_);
sceneManager_.requestPushScene(std::move(scene));
}
void GameScene::handlePlayerDamage(int damage)
{
auto player_component = player_->getComponent<game::component::PlayerComponent>();
if(!player_component->takeDamage(damage)){
return; // 如果没有受伤,则直接返回
}
// 受伤了
if(player_component->isDead()){
// 死了
spdlog::info("玩家{}死亡",player_->getName());
// TODO: 游戏结束
auto scene = std::make_unique<game::scene::EndScene>(context_, sceneManager_, game_session_data_);
sceneManager_.requestPushScene(std::move(scene));
}
// 更新health_UI
updateHealthUI();
}
//
// 玩家掉入悬崖了
handleObjectCollisions(){
// 处理其他
if(obj1->getTag() == "player" && obj2->getTag() == "end"){
toEndScene(obj2);
} else if(obj1->getTag() == "end" && obj2->getTag() == "player"){
toEndScene(obj1);
}
}
我这里采用了一个取巧的方法,我将胜利失败触发矩形都定义为End,然后去获取对象的名字,然后将需要传递的会话数据进行设置,最后传入结束场景EndScene。
第三部分:最高分系统
我们希望玩家的最高分始终被记录
// 创建得分标签 最高分和当前分
auto high_score_text = std::make_unique<engine::ui::UILabel>(
&context_.getTextRenderer(), "",
"assets/fonts/VonwaonBitmap-16px.ttf",16,
glm::vec2(window_size.x / 2 - 50.f, window_size.y / 2 - 40.0f),
engine::utils::FColor(1.0f, 1.0f, 0.0f, 1.0f)
);
std::ifstream file("assets/high_score.txt");
int high_score_value = game_session_data_->getHighScore();
int file_high_score = 0;
if(file.is_open()){
file >> file_high_score;
file.close();
} else { // 如果文件不存在,则创建一个新文件
std::ofstream new_file("assets/high_score.txt");
new_file << high_score_value;
new_file.close();
}
if(high_score_value > file_high_score){
std::ofstream file_o("assets/high_score.txt");
file_o << high_score_value;
file_o.close();
}
high_score_text->setText("High Score: " + std::to_string(file_high_score));
ui_manager_->addElement(std::move(high_score_text));
auto current_score_text = std::make_unique<engine::ui::UILabel>(
&context_.getTextRenderer(), "",
"assets/fonts/VonwaonBitmap-16px.ttf",16,
glm::vec2(window_size.x / 2 - 50.f, window_size.y / 2 - 10.f),
engine::utils::FColor(1.0f, 1.0f, 1.0f, 1.0f)
);
auto current_score_value = game_session_data_->getCurrentScore();
current_score_text->setText("Current Score: " + std::to_string(current_score_value));
if(current_score_value >= file_high_score){
current_score_text->setColor(engine::utils::FColor(1.0f, 1.0f, 0.0f, 1.0f));
}
ui_manager_->addElement(std::move(current_score_text));
逻辑也不复杂,首先读取一个最高分的文件,如果没有就创建,把目前游戏中的会话数据中的当前分数和文件中的最高分进行比对,如果比它大就存进去。当然感觉之前更项目的时候记录的最高分就不太使用的到了,不过也差不多可以实现相应的功能。
我们会在进入endScene的时候进行一次调用,设置最高分数。
第四部分:细节优化
解除相机边界,从GameScene回到TitleScene的时候,我们不希望相机移动受制于关卡边界,我们会在TitleScene初始化中进行设置,将相机的setLimitBounds的参数修改为std::nullopt,解除边界限制。
void TitleScene::init() {
// ...
// 重置相机坐标,不限制边界
context_.getCamera().setPosition(glm::vec2(0.0f, 0.0f));
context_.getCamera().setLimitBounds(std::nullopt);
// ...
}
课程总结
太不容易了,我终于做完了这个项目,前前后后3个月左右,期间还有一堆杂事情,写论文,改论文,硬盘服务器坏了,又得去修,但是还是完成了,这是一个挑战也是一个开始,目标成为游戏制作大师,加油!

浙公网安备 33010602011771号