C++游戏开发之旅 19

问题概述

现在我们已经实现了动画系统,是时候给游戏角色添加生命组件了

我们依旧是延续之前数据驱动的模式,为特定的游戏对象添加生命系统

核心思想

  • 创建通用HealthComponent组件
  • 在 Tiled 中定义初始生命值
  • 添加受伤(Hurt)和死亡(Dead)状态
  • 实现无敌帧机制

本章我们需要完成的目标有:添加生命值组件、Tiled中设置自定义生命属性供level_loader读取、实现两个新状态hurt和dead。

1.HealthComponent 创建

我们将生命值组件放在引擎层中,其可以附加到任何需要生命值的对象上。

生命组件主要负责记录最大生命、当前生命、是否无敌以及无敌时间

src/engine/component/health_component.h

#pragma once

#include "component.h"

namespace engine::component {

/**
 * @brief 生命组件
 * 记录最大生命、当前生命、是否无敌以及无敌时间
 */
class HealthComponent final : public Component {
    friend class GameObject;
private:
    int max_health_ = 1;
    int current_health_ = 1;
    bool is_invincible_ = false;
    float invincible_duration_ = 1.5f;
    float invincible_timer_ = 0.0f;

public:
    HealthComponent(int max_health, float invincible_time);
    ~HealthComponent() override = default;

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

    // 伤害和回复 如果成功造成伤害返回true
    bool takeDamage(int damage);
    void heal(int heal);


    // setter getter
    bool isAlive() const { return current_health_ > 0; }
    int getMaxHealth() const { return max_health_; }
    int getCurrentHealth() const { return current_health_; }
    bool isInvincible() const { return is_invincible_; }
    float getInvincibleTime() const { return invincible_duration_; }

    void setMaxHealth(int max_health);
    void setCurrentHealth(int current_health);
    void setInvincible(float duration);
    void setInvincibleTime(float invincible_duration) { invincible_duration_ = invincible_duration; }

private:
    void update(float, engine::core::Context&) override;
};

}

src/engine/component/health_component.cpp

#include "health_component.h"
#include <glm/common.hpp>
#include <spdlog/spdlog.h>
#include "../../engine/object/game_object.h"

namespace engine::component{

    HealthComponent::HealthComponent(int max_health, float invincible_time)
    : max_health_(glm::max(1,max_health)), current_health_(max_health), invincible_duration_(invincible_time)
    {}

    void HealthComponent::update(float delta_time, engine::core::Context &)
    {
        if(is_invincible_ && invincible_duration_ > 0.0f){
            invincible_timer_ -= delta_time;
            if(invincible_timer_ <= 0.0f){
                invincible_timer_ = 0.0f;
                is_invincible_ = false;
            }
        }
    }

    void HealthComponent::setInvincible(float duration)
    {
        if(duration > 0.0f) {
            // 如果当前依然处于无敌时间,并且需要设置的无敌时间还比当前剩余时间短,那么就返回
            if(duration < invincible_timer_){
                return;
            }
            // 否则就更新无敌时间
            is_invincible_ = true;
            invincible_timer_ = duration;
            
        } else {
            // 如果传入的duration为负或0,则取消无敌状态
            is_invincible_ = false;
            invincible_timer_ = 0.0f;
        }
            
    }

    bool HealthComponent::takeDamage(int damage)
    {
        // 如果当前已经死亡或者伤害小于等于0,则返回false
        if(!isAlive() || damage <= 0){
            return false;
        }
        // 如果当前处于无敌状态,则返回false
        if(is_invincible_){
            return false;
        }
        // 否则就扣血
        current_health_ -= damage;
        current_health_ = glm::max(0,current_health_); // 防止血量小于0
        if(current_health_ > 0){ // 如果扣血后依然存活,则给一个无敌时间
            setInvincible(invincible_duration_);
        }
        spdlog::debug("游戏对象{}受到{}点伤害,剩余血量:{}/{}", owner_->getName(), damage, current_health_, max_health_);
        return true;
    }

    void HealthComponent::heal(int heal)
    {
        if(isAlive() && heal > 0) {
            current_health_ += heal;
            if(current_health_ > max_health_){
                current_health_ = max_health_;
            }
            spdlog::debug("游戏对象{}受到{}点治疗,剩余血量:{}/{}", owner_->getName(), heal, current_health_, max_health_);
        }
    }

    
    void HealthComponent::setMaxHealth(int max_health)
    {
        max_health_ = glm::max(1,max_health);
        // 如果当前血量大于最大血量,则将当前血量设置为最大血量
        current_health_ = glm::min(max_health_,current_health_);
    }

    void HealthComponent::setCurrentHealth(int current_health)
    {
        // 将当前血量设置为 [0, max_health] 范围内的值
        current_health_ = glm::max(0, glm::min(current_health,max_health_));
    }
}

核心成员变量
  • max_health_ --- 最大生命值
  • current_health_ --- 当前生命值
  • is_invincible_ --- 是否无敌
  • invincible_duration_ --- 无敌持续时间
  • invincible_timer_ --- 无敌计时器
takeDamage()流程

检查是否还活着/伤害值是否大于0/是否处于无敌状态 ---> 受到伤害 ---> 限制血量防止小于0 ---> 判断是否活着给予无敌时间 ---> 返回true

update()

我们生命组件中只需要实时减少无敌帧时间

invincible_timer_ -= delta_time;
if(invincible_timer <= 0.0f){
    invincible_timer_ = 0.0f;
    is_invincible = false;
}

2.数据驱动生命值

和动画一样,我们需要在 Tiled 中进行自定义属性。

玩家给3点血量,敌人1点血

LevelLoader升级

在对象层载入的函数中新增加HealthComponent

// 获取生命值
auto health = getTileProperty<int>(tile_json,"health");
if(health){
    auto* hc = obj->addComponent<component::HealthComponent>(health.value());
}

自动配置流程

LevelLoader 处理生命值:
    ↓
1. 读取对象的 gid
    ↓
2. 获取对应的 tile_json
    ↓
3. 调用 getTileProperty<int>("health")
    ↓
4. 如果存在 health 属性
    ↓
5. addComponent<HealthComponent>(health.value())
    ↓
6. 组件自动配置完成

现在,LevelLoader 在创建游戏对象时,会 自动检查 并为其附加一个配置好最大生命值的 HealthComponent

3.玩家新状态:受伤&死亡

当玩家受到伤害或是死亡的时候需要有相应的反馈

比如在受伤的时候被击退,并且短时间无法操作,播放受伤动画

3.1受伤状态 HurtState

该状态下需要模拟玩家受到攻击后短暂失控。

src/game/component/state/hurt_state.h
#pragma once
#include "player_state.h"

namespace game::component {
class PlayerComponent;
}

namespace game::component::state {

class HurtState final : public PlayerState
{
    friend class game::component::PlayerComponent;
private:
    float stunned_timer_ = 0.0f; // 硬直计时器
public:
    HurtState(PlayerComponent* player_component): PlayerState(player_component) {};
    ~HurtState() override = default;


private:
    void enter() override;
    void exit() override;
    std::unique_ptr<state::PlayerState> update(float dt, engine::core::Context& context) override;
    std::unique_ptr<state::PlayerState> handleInput(engine::core::Context& context) override;
};
}
src/game/component/state/hurt_state.cpp
#include "hurt_state.h"
#include "idle_state.h"
#include "fall_state.h"
#include "dead_state.h"
#include "../player_component.h"
#include "../../../engine/component/sprite_component.h"
#include "../../../engine/component/physics_component.h"

namespace game::component::state
{
    void HurtState::enter()
    {
        playAnimation("hurt");
        // 击退效果
        auto* pc = player_component_->getPhysicsComponent();
        auto* sc = player_component_->getSpriteComponent();
        auto knockback_velocity = glm::vec2(-100.0f, -150.0f); // 默认是左上角击退
        if(!sc->isFlipped()){
            pc->velocity_ = knockback_velocity;
        } else{
            pc->velocity_ = knockback_velocity * glm::vec2(-1.0f, 1.0f);
        }
        stunned_timer_ = player_component_->getStunnedTime();
    }

    void HurtState::exit()
    {
    }

    std::unique_ptr<state::PlayerState> HurtState::update(float dt, engine::core::Context &context)
    {
        // 如果玩家被击晕,提前掉到地上了,进入idle状态
        auto* pc = player_component_->getPhysicsComponent();
        if(pc->hasCollidedBelow()) {
            return std::make_unique<state::IdleState>(player_component_);
        }
        // 当击晕状态结束时,还没有与地面碰撞,则进入fall状态
        if(stunned_timer_ > 0.0f){
            stunned_timer_ -= dt;
            if(stunned_timer_ <= 0.0f){
                return std::make_unique<state::FallState>(player_component_);
            }
        }
        return nullptr;
    }

    std::unique_ptr<state::PlayerState> HurtState::handleInput(engine::core::Context &context)
    {
        // 玩家在hurt状态下,不能进行操作
        return nullptr;
    }
}

HurtState逻辑详解

  • enter() --- 播放"hurt"动画,根据朝向决定击退方向,设置击退速度
  • handleInput() --- 硬直期间无法操作,返回nullptr
  • update() --- 硬直计时器记录时间,根据落地以及计时器来决定之后的状态

3.2死亡状态 DeadState

角色结束状态,一旦进入,玩家失去控制

src/game/component/state/dead_state.h
#pragma once
#include "player_state.h"

namespace game::component {
class PlayerComponent;
}

namespace game::component::state {

class DeadState final : public PlayerState
{
    friend class game::component::PlayerComponent;
public:
    DeadState(PlayerComponent* player_component): PlayerState(player_component) {};
    ~DeadState() override = default;


private:
    void enter() override;
    void exit() override;
    std::unique_ptr<state::PlayerState> update(float dt, engine::core::Context& context) override;
    std::unique_ptr<state::PlayerState> handleInput(engine::core::Context& context) override;
};
}
src/game/component/state/dead_state.cpp
#include "dead_state.h"
#include "hurt_state.h"
#include "idle_state.h"
#include "fall_state.h"
#include "../player_component.h"
#include "../../../engine/component/sprite_component.h"
#include "../../../engine/component/physics_component.h"
#include "../../../engine/object/game_object.h"
#include "../../../engine/component/collider_component.h"

namespace game::component::state
{
    void DeadState::enter()
    {
        playAnimation("hurt");
        auto* pc = player_component_->getPhysicsComponent();
        // 只给一个向上的速度
        pc->velocity_ = glm::vec2(0.f, -200.f);

        // 禁用碰撞
        auto* cc = player_component_->getOwner()->getComponent<engine::component::ColliderComponent>();
        if(cc) cc->setActive(false);

    }
    
    void DeadState::exit()
    {
    }

    std::unique_ptr<state::PlayerState> DeadState::update(float dt, engine::core::Context &context)
    {
        // 死亡状态下,玩家不能做任何操作
        return nullptr;
    }
    
    std::unique_ptr<state::PlayerState> DeadState::handleInput(engine::core::Context &context)
    {
        // 死亡状态下,玩家不能做任何操作
        return nullptr;
    }

}

DeadState逻辑详解

  • enter() --- 播放死亡动画,向上弹跳,velocity = (0,-200),禁用碰撞器,cc->setActive(false)
  • handleInput() --- 死亡期间无法操作,返回nullptr
  • update() --- 无法更新逻辑,返回nullptr

通过禁用碰撞,我们可以实现类似超级玛丽的死亡时的效果

物理引擎设置

为了让"禁用碰撞器"后角色可以掉出屏幕,我们对物理引擎做一个调整:

这里我们修改了瓦片碰撞逻辑,原本我们是if (!tc || !cc || !cc->isActive() || cc->isTrigger()) return;,这会导致我们的碰撞器没有激活就不会更新位置,因为直接返回了,我们希望的效果是会正常更新位置,tc->translate(ds);,这里我们直接设置位置就可以,而后面我们通过轴分离检测,会对xy方向分别进行判断,得到新的位置之后,tc->translate(new_pos - obj_pos);

void PhysicsEngine::resolveTileCollisions(engine::component::PhysicsComponent* pc, float delta_time) {
    // 检查组件是否有效
    auto* obj = pc->getOwner();
    if (!obj) return;
    auto* tc = obj->getComponent<engine::component::TransformComponent>();
    auto* cc = obj->getComponent<engine::component::ColliderComponent>();
    if (!tc || !cc || cc->isTrigger()) return;
    // ...

    if (!cc->isActive()) {  // 如果碰撞器未激活,直接让物体正常移动,然后返回。
        tc->translate(ds);
        pc->velocity_ = glm::clamp(pc->velocity_, -max_speed_, max_speed_);
        return;
    }
    // ...
}

当瓦片层检测完之后,紧接着是对象间的碰撞检测,对象间的检测我们会检查对象的碰撞组件是否处于激活,如果是非激活状态,则跳过

碰撞检测流程更新
resolveTileCollisions() 流程:
    ↓
1. 检查组件有效性
    ↓
2. 检查是否为 Trigger
    ↓
3. 检查碰撞器是否激活  ← 新增
    ↓ isActive() == false
    直接更新位置,跳过碰撞检测
    ↓
    物体自由下落

现在,如果一个物体的碰撞器被禁用了,物理引擎将 不再为它做任何碰撞解析,只会单纯地根据速度更新它的位置,这样玩家死亡后就能掉出世界边界了。

4.连接 PlayerComponent 和新状态

最后,我们需要在PlayerComponent中添加HealthComponent,由PlayerComponent决定切换到HurtState还是DeadState

架构设计

伤害处理流程:
外部调用
    ↓
PlayerComponent::takeDamage()
    ↓ 委托
HealthComponent::takeDamage()
    ↓ 返回是否成功
PlayerComponent 检查结果
    ↓ 成功造成伤害
检查是否存活
    ↓
┌───────┴────────┐
│                │
│ 还活着         │ 已死亡
↓                ↓
HurtState        DeadState
// player_component.h
#pragma once
#include "../../engine/component/component.h"
#include "state/player_state.h"
#include <memory>

namespace engine::object {
class GameObject;
}

namespace engine::component
{
class TransformComponent;
class SpriteComponent;
class PhysicsComponent;
class AnimationComponent;
class HealthComponent;
} 


namespace game::component {
/**
 * @brief 处理玩家输入、状态、控制GameObject移动等组件
 */
class PlayerComponent final : public engine::component::Component {
    friend class engine::object::GameObject;
private:
    engine::component::TransformComponent* transform_component_ = nullptr; // 指向的transform组件的指针
    engine::component::SpriteComponent* sprite_component_ = nullptr; // 指向的sprite组件的指针
    engine::component::PhysicsComponent* physics_component_ = nullptr; // 指向的physics组件的指针
    engine::component::AnimationComponent* animation_component_ = nullptr; // 指向的animation组件的指针
    engine::component::HealthComponent* health_component_ = nullptr; // 指向的health组件的指针

    std::unique_ptr<state::PlayerState> current_state_; // 当前状态
    bool is_dead_ = false;

    // 相关属性
    float move_force_ = 250.0f; // 移动力
    float max_speed_ = 180.0f; // 最大速度
    float friction_ = 0.85f;  // 摩擦力
    float jump_velocity_ = 350.0f; // 跳跃速度

    float stunned_time_ = 0.4f; // 晕眩时间

public:
    PlayerComponent() = default;
    ~PlayerComponent() = default;

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

    // getter setter
    engine::component::TransformComponent* getTransformComponent() const { return transform_component_; }
    engine::component::SpriteComponent* getSpriteComponent() const { return sprite_component_; }
    engine::component::PhysicsComponent* getPhysicsComponent() const { return physics_component_; }
    engine::component::AnimationComponent* getAnimationComponent() const { return animation_component_; }
    engine::component::HealthComponent* getHealthComponent() const { return health_component_; }
    
    bool isDead() const { return is_dead_; }
    void setDead(bool dead) { is_dead_ = dead; }
    float getMoveForce() const { return move_force_; }
    float getMaxSpeed() const { return max_speed_; }
    float getFriction() const { return friction_; }
    float getJumpVelocity() const { return jump_velocity_; }
    void setMoveForce(float force) { move_force_ = force; }
    void setMaxSpeed(float speed) { max_speed_ = speed; }
    void setFriction(float friction) { friction_ = friction; }
    void setJumpVelocity(float velocity) { jump_velocity_ = velocity; }
    float getStunnedTime() const { return stunned_time_; }
    void setStunnedTime(float time) { stunned_time_ = time; }

    state::PlayerState* getCurrentState() const { return current_state_.get(); }
    void changeState(std::unique_ptr<state::PlayerState> state); // 设置当前状态
    bool takeDamage(int damage);

private:
    void init() override;
    void update(float dt, engine::core::Context& context) override;
    void handleInput(engine::core::Context& context) override;

};


}

// cpp
#include "player_component.h"
#include "../../engine/object/game_object.h"
#include "../../engine/component/transform_component.h"
#include "../../engine/component/sprite_component.h"
#include "../../engine/component/physics_component.h"
#include "../../engine/component/animation_component.h"
#include "../../engine/component/health_component.h"
#include "../../engine/core/context.h"
#include "state/idle_state.h"
#include "state/hurt_state.h"
#include "state/dead_state.h"
#include <spdlog/spdlog.h>
#include <typeinfo>


namespace game::component{

    void PlayerComponent::init()
    {
        if(!owner_){
            spdlog::error("PlayerComponent的owner为空");
            return;
        }
        // 组件初始化
        transform_component_ = owner_->getComponent<engine::component::TransformComponent>();
        sprite_component_ = owner_->getComponent<engine::component::SpriteComponent>();
        physics_component_ = owner_->getComponent<engine::component::PhysicsComponent>();
        animation_component_ = owner_->getComponent<engine::component::AnimationComponent>();
        health_component_ = owner_->getComponent<engine::component::HealthComponent>();
        
        // 检查必要组件
        if(!transform_component_ || !sprite_component_ || !physics_component_ ||
            !animation_component_ || !health_component_){
            spdlog::error("PlayerComponent初始化失败, 缺少必要组件");
            return;
        }

        // 初始化状态
        auto idle = std::make_unique<state::IdleState>(this);
        if(idle){
            changeState(std::move(idle));
        } else{
            spdlog::error("PlayerComponent初始化失败, 状态初始化失败");
            return;
        }

        spdlog::debug("PlayerComponent初始化成功");
    }

    void PlayerComponent::changeState(std::unique_ptr<state::PlayerState> new_state)
    {
        if(!new_state){
            spdlog::error("PlayerComponent设置状态失败, 状态为空");
            return;
        }
        // 如果当前状态存在, 则退出当前状态
        if(current_state_) current_state_->exit();
        // 设置新状态
        current_state_ = std::move(new_state);
        spdlog::debug("PlayerComponent设置状态为: {}", typeid(*current_state_).name());
        // 进入新状态
        current_state_->enter();
    }

    bool PlayerComponent::takeDamage(int damage)
    {
        if(is_dead_ || damage < 0 || !health_component_) return false;

        if(!health_component_->takeDamage(damage)){
            return false;
        }
        // 前面确实造成了伤害
        if(health_component_->isAlive()){
            // 切换受伤状态
            changeState(std::make_unique<state::HurtState>(this));    
        } else{
            // 切换死亡状态
            spdlog::debug("玩家死亡");
            is_dead_ = true;
            changeState(std::make_unique<state::DeadState>(this));
        }
        return true;
    }

    void PlayerComponent::update(float dt, engine::core::Context &context)
    {
        if(!current_state_) return;

        auto new_state = current_state_->update(dt, context);
        if(new_state){
            changeState(std::move(new_state));
        }
    }

    void PlayerComponent::handleInput(engine::core::Context &context)
    {
        if(!current_state_) return;
        auto new_state = current_state_->handleInput(context);
        if(new_state){
            changeState(std::move(new_state));
        }
    }

} // namespace game::component

takeDamage() 方向详解

我们会先进行一个安全检查,角色是否死亡、生命组件是否存在、伤害是否有效,之后委托给HealthComponent进行处理,(health_component_->takeDamage())返回的是布尔值,可以判断是否成功造成伤害, 判断当前是否存活来决定进入哪个状态。

设计亮点

外部接口唯一性

  • 外部只能通过 PlayerComponent::takeDamage() 造成伤害
  • 内部逻辑完全封装
  • 状态切换自动处理

5.测试

我们在GameScene中添加一个临时函数,来测试

void GameScene::handleInput() {
    Scene::handleInput();
    testHealth();  // 测试生命值组件
}
// ...
void GameScene::testHealth()
{
    auto input_manager = context_.getInputManager();
    if (input_manager.isActionPressed("attack")) {
        player_->getComponent<game::component::PlayerComponent>()->takeDamage(1);
    }
}

效果如期望的一样。

章节总结

本章我们完成了生命组件HealthComponent,该组件负责记录最大生命、当前生命、是否无敌以及无敌时间。我们使用 Tiled 编辑器定义游戏对象的 health 属性,通过levelLoader解析该属性,创建生命组件添加到游戏对象中。我们的玩家组件PlayerComponent中有一个takeDamage(),通过委托给HealthComponent处理数值和无敌帧,这里这个PlayerComponent::takeDamage()由外部进行调用,我们的玩家持有HealthComponent、PlayerComponent,生命组件专门处理血量扣减、无敌帧、数值校验,而玩家组件相当于接口层,对外提供takeDamage()函数,作为"扣血操作"入口,外部调用:碰撞到陷阱、怪物攻击玩家等等。takeDamage还通过血量来决定进入HurtState、DeadState状态

组件协作关系

组件协作图:
┌──────────────────┐
│ HealthComponent  │ ← 管理生命值
└────────┬─────────┘
         │ 报告伤害
         ↓
┌──────────────────┐
│ PlayerComponent  │ ← 决策中心
└────────┬─────────┘
         │ 切换状态
         ↓
┌──────────────────┐
│ PlayerState      │ ← 控制行为
│ - HurtState      │
│ - DeadState      │
└────────┬─────────┘
         │ 更新表现
         ↓
┌──────────────────┐
│ PhysicsComponent │ ← 击退效果
│ SpriteComponent  │ ← 动画播放
│ ColliderComponent│ ← 碰撞控制
└──────────────────┘

设计模式应用

模式 应用 优势
组件模式 HealthComponent 通用、可复用
状态模式 HurtState、DeadState 清晰的状态管理
数据驱动 Tiled 配置 易于调整平衡
委托模式 PlayerComponent -> HealthComponent 职责分离
posted @ 2026-02-28 20:20  wenyiGamecpp  阅读(43)  评论(0)    收藏  举报