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个月左右,期间还有一堆杂事情,写论文,改论文,硬盘服务器坏了,又得去修,但是还是完成了,这是一个挑战也是一个开始,目标成为游戏制作大师,加油!

posted @ 2026-03-15 10:43  wenyiGamecpp  阅读(1)  评论(0)    收藏  举报