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 | 职责分离 |

浙公网安备 33010602011771号