diff --git a/CMakeLists.txt b/CMakeLists.txt index 4ca82b7..7e32827 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -51,6 +51,7 @@ add_executable(${TARGET} src/engine/component/physics_component.cpp src/engine/component/collider_component.cpp src/engine/component/animation_component.cpp + src/engine/component/health_component.cpp src/engine/physics/physics_engine.cpp src/engine/physics/collision.cpp src/engine/scene/scene.cpp @@ -63,6 +64,8 @@ add_executable(${TARGET} src/game/component/state/walk_state.cpp src/game/component/state/jump_state.cpp src/game/component/state/fall_state.cpp + src/game/component/state/hurt_state.cpp + src/game/component/state/dead_state.cpp ) # 链接库 diff --git a/assets/maps/actor.tsj b/assets/maps/actor.tsj index 0b69935..89c60ee 100644 --- a/assets/maps/actor.tsj +++ b/assets/maps/actor.tsj @@ -46,6 +46,11 @@ "type":"string", "value":"{\n \"fly\": {\"frames\": [0,1,2,3]}\n}" }, + { + "name":"health", + "type":"int", + "value":1 + }, { "name":"tag", "type":"string", @@ -94,6 +99,11 @@ "type":"bool", "value":true }, + { + "name":"health", + "type":"int", + "value":3 + }, { "name":"tag", "type":"string", @@ -142,6 +152,11 @@ "type":"bool", "value":true }, + { + "name":"health", + "type":"int", + "value":1 + }, { "name":"tag", "type":"string", @@ -190,6 +205,11 @@ "type":"bool", "value":true }, + { + "name":"health", + "type":"int", + "value":1 + }, { "name":"tag", "type":"string", diff --git a/assets/maps/level1.tmj b/assets/maps/level1.tmj index c29deac..005f38b 100644 --- a/assets/maps/level1.tmj +++ b/assets/maps/level1.tmj @@ -126,8 +126,8 @@ "type":"", "visible":true, "width":32, - "x":124.227272727273, - "y":157.5 + "x":114.060606060606, + "y":156.598484848485 }, { "gid":607, diff --git a/src/engine/component/health_component.cpp b/src/engine/component/health_component.cpp new file mode 100644 index 0000000..cefe26b --- /dev/null +++ b/src/engine/component/health_component.cpp @@ -0,0 +1,84 @@ +#include "health_component.h" +#include "../../engine/object/game_object.h" +#include +#include + +namespace engine::component { + +HealthComponent::HealthComponent(int max_health, float invincibility_duration) + : max_health_(glm::max(1, max_health)), // 确保最大生命值至少为 1 + current_health_(max_health_), // 初始化当前生命值为最大生命值 + invincibility_duration_(invincibility_duration) +{} + +void HealthComponent::update(float delta_time, engine::core::Context&) { + // 更新无敌状态计时器 + if (is_invincible_) { + invincibility_timer_ -= delta_time; + if (invincibility_timer_ <= 0.0f) { + is_invincible_ = false; + invincibility_timer_ = 0.0f; + } + } +} + +bool HealthComponent::takeDamage(int damage_amount) { + if (damage_amount <= 0 || !isAlive()) { + return false; // 不造成伤害或已经死亡 + } + + if (is_invincible_) { + spdlog::debug("游戏对象 '{}' 处于无敌状态,免疫了 {} 点伤害。", owner_ ? owner_->getName() : "Unknown", damage_amount); + return false; // 无敌状态,不受伤 + } + // --- 确实造成伤害了 --- + current_health_ -= damage_amount; + current_health_ = glm::max(0, current_health_); // 防止生命值变为负数 + // 如果受伤但没死,并且设置了无敌时间,则触发无敌 + if (isAlive() && invincibility_duration_ > 0.0f) { + setInvincible(invincibility_duration_); + } + spdlog::debug("游戏对象 '{}' 受到了 {} 点伤害,当前生命值: {}/{}。", + owner_ ? owner_->getName() : "Unknown", damage_amount, current_health_, max_health_); + return true; // 造成伤害,返回true +} + +void HealthComponent::heal(int heal_amount) { + if (heal_amount <= 0 || !isAlive()) { + return; // 不治疗或已经死亡 + } + + current_health_ += heal_amount; + current_health_ = std::min(max_health_, current_health_); // 防止超过最大生命值 + spdlog::debug("游戏对象 '{}' 治疗了 {} 点,当前生命值: {}/{}。", + owner_ ? owner_->getName() : "Unknown", heal_amount, current_health_, max_health_); +} + +void HealthComponent::setInvincible(float duration) +{ + if (duration > 0.0f) + { + is_invincible_ = true; + invincibility_timer_ = duration; + spdlog::debug("游戏对象 '{}' 进入无敌状态,持续 {} 秒。", owner_ ? owner_->getName() : "Unknown", duration); + } else { + // 如果持续时间为 0 或负数,则立即取消无敌 + is_invincible_ = false; + invincibility_timer_ = 0.0f; + spdlog::debug("游戏对象 '{}' 的无敌状态被手动移除。", owner_ ? owner_->getName() : "Unknown"); + } +} + +void HealthComponent::setMaxHealth(int max_health) +{ + max_health_ = glm::max(1, max_health); // 确保最大生命值至少为 1 + current_health_ = glm::min(current_health_, max_health_); // 确保当前生命值不超过最大生命值 +} + +void HealthComponent::setCurrentHealth(int current_health) +{ + // 确保当前生命值在 0 到最大生命值之间 + current_health_ = glm::max(0, glm::min(current_health, max_health_)); +} + +} // namespace engine::component diff --git a/src/engine/component/health_component.h b/src/engine/component/health_component.h new file mode 100644 index 0000000..b4ff6a4 --- /dev/null +++ b/src/engine/component/health_component.h @@ -0,0 +1,59 @@ +#pragma once +#include "../../engine/component/component.h" + +namespace engine::component { + +/** + * @brief 管理 GameObject 的生命值,处理伤害、治疗,并提供无敌帧功能。 + */ +class HealthComponent final : public engine::component::Component { + friend class engine::object::GameObject; +private: + int max_health_ = 1; ///< @brief 最大生命值 + int current_health_ = 1; ///< @brief 当前生命值 + bool is_invincible_ = false; ///< @brief 是否处于无敌状态 + float invincibility_duration_ = 2.0f; ///< @brief 受伤后无敌的总时长(秒) + float invincibility_timer_ = 0.0f; ///< @brief 无敌时间计时器(秒) + +public: + /** + * @brief 构造函数 + * @param max_health 最大生命值,默认为 1 + * @param invincibility_duration 无敌状态持续时间,默认为 2.0 秒 + */ + explicit HealthComponent(int max_health = 1, float invincibility_duration = 2.0f); + ~HealthComponent() override = default; + + // 禁止拷贝和移动 + HealthComponent(const HealthComponent&) = delete; + HealthComponent& operator=(const HealthComponent&) = delete; + HealthComponent(HealthComponent&&) = delete; + HealthComponent& operator=(HealthComponent&&) = delete; + + /** + * @brief 对 GameObject 施加伤害。 + * 如果当前处于无敌状态,则伤害无效。 + * 如果成功造成伤害且设置了无敌时长,则会触发无敌帧。 + * @param damage_amount 造成的伤害量(应为正数)。 + * @return bool 如果成功造成伤害,则返回 true,否则返回 false。 + */ + bool takeDamage(int damage_amount); + void heal(int heal_amount); ///< @brief 治疗 GameObject,增加当前生命值(不超过最大生命值)。 + + // --- Getters and Setters --- + bool isAlive() const { return current_health_ > 0; } ///< @brief 检查 GameObject 是否存活(当前生命值大于 0)。 + bool isInvincible() const { return is_invincible_; } ///< @brief 检查 GameObject 是否处于无敌状态。 + int getCurrentHealth() const { return current_health_; } ///< @brief 获取当前生命值。 + int getMaxHealth() const { return max_health_; } ///< @brief 获取最大生命值。 + + void setCurrentHealth(int current_health); ///< @brief 设置当前生命值 (确保不超过最大生命值)。 + void setMaxHealth(int max_health); ///< @brief 设置最大生命值 (确保不小于 1)。 + void setInvincible(float duration); ///< @brief 设置 GameObject 进入无敌状态,持续时间为 duration 秒。 + void setInvincibilityDuration(float duration) { invincibility_duration_ = duration; } ///< @brief 设置无敌状态持续时间。 + +protected: + // 核心循环函数 + void update(float, engine::core::Context&) override; +}; + +} // namespace engine::component diff --git a/src/engine/physics/physics_engine.cpp b/src/engine/physics/physics_engine.cpp index 2a1668e..52dd0d6 100644 --- a/src/engine/physics/physics_engine.cpp +++ b/src/engine/physics/physics_engine.cpp @@ -111,7 +111,7 @@ void PhysicsEngine::resolveTileCollisions(engine::component::PhysicsComponent* p if (!obj) return; auto* tc = obj->getComponent(); auto* cc = obj->getComponent(); - if (!tc || !cc || !cc->isActive() || cc->isTrigger()) return; + if (!tc || !cc || cc->isTrigger()) return; auto world_aabb = cc->getWorldAABB(); // 使用最小包围盒进行碰撞检测(简化) auto obj_pos = world_aabb.position; auto obj_size = world_aabb.size; @@ -122,6 +122,12 @@ void PhysicsEngine::resolveTileCollisions(engine::component::PhysicsComponent* p auto ds = pc->velocity_ * delta_time; // 计算物体在delta_time内的位移 auto new_obj_pos = obj_pos + ds; // 计算物体在delta_time后的新位置 + if (!cc->isActive()) { // 如果碰撞器未激活,直接让物体正常移动,然后返回。 + tc->translate(ds); + pc->velocity_ = glm::clamp(pc->velocity_, -max_speed_, max_speed_); + return; + } + // 遍历所有注册的碰撞瓦片层 for (auto* layer : collision_tile_layers_) { if (!layer) continue; diff --git a/src/engine/scene/level_loader.cpp b/src/engine/scene/level_loader.cpp index 5d405fa..2fbcf24 100644 --- a/src/engine/scene/level_loader.cpp +++ b/src/engine/scene/level_loader.cpp @@ -6,6 +6,7 @@ #include "../component/collider_component.h" #include "../component/physics_component.h" #include "../component/animation_component.h" +#include "../component/health_component.h" #include "../object/game_object.h" #include "../scene/scene.h" #include "../core/context.h" @@ -248,6 +249,13 @@ void LevelLoader::loadObjectLayer(const nlohmann::json& layer_json, Scene& scene addAnimation(anim_json, ac, src_size); } + // 获取生命值信息并设置 + auto health = getTileProperty(tile_json, "health"); + if (health) { + // 添加 HealthComponent + game_object->addComponent(health.value()); + } + // 添加到场景中 scene.addGameObject(std::move(game_object)); spdlog::info("加载对象: '{}' 完成", object_name); diff --git a/src/game/component/player_component.cpp b/src/game/component/player_component.cpp index 76b80e0..9bdf19e 100644 --- a/src/game/component/player_component.cpp +++ b/src/game/component/player_component.cpp @@ -1,9 +1,12 @@ #include "player_component.h" #include "state/idle_state.h" +#include "state/hurt_state.h" +#include "state/dead_state.h" #include "../../engine/component/transform_component.h" #include "../../engine/component/physics_component.h" #include "../../engine/component/sprite_component.h" #include "../../engine/component/animation_component.h" +#include "../../engine/component/health_component.h" #include "../../engine/object/game_object.h" #include "../../engine/input/input_manager.h" #include @@ -23,9 +26,10 @@ void PlayerComponent::init() { physics_component_ = owner_->getComponent(); sprite_component_ = owner_->getComponent(); animation_component_ = owner_->getComponent(); + health_component_ = owner_->getComponent(); // 检查必要组件是否存在 - if (!transform_component_ || !physics_component_ || !sprite_component_ || !animation_component_) { + if (!transform_component_ || !physics_component_ || !sprite_component_ || !animation_component_ || !health_component_) { spdlog::error("Player 对象缺少必要组件!"); } @@ -39,6 +43,29 @@ void PlayerComponent::init() { spdlog::debug("PlayerComponent 初始化完成。"); } +bool PlayerComponent::takeDamage(int damage) { + if (is_dead_ || !health_component_ || damage <= 0) { + spdlog::warn("玩家已死亡或却少必要组件,并未造成伤害。"); + return false; + } + + bool success = health_component_->takeDamage(damage); + if (!success) return false; + // --- 成功造成伤害了,根据是否存活决定状态切换 + if (health_component_->isAlive()) { + spdlog::debug("玩家受到了 {} 点伤害,当前生命值: {}/{}。", + damage, health_component_->getCurrentHealth(), health_component_->getMaxHealth()); + // 切换到受伤状态 + setState(std::make_unique(this)); + } else { + spdlog::debug("玩家死亡。"); + is_dead_ = true; + // 切换到死亡状态 + setState(std::make_unique(this)); + } + return true; +} + void PlayerComponent::setState(std::unique_ptr new_state) { if (!new_state) { spdlog::warn("尝试设置空的玩家状态!"); diff --git a/src/game/component/player_component.h b/src/game/component/player_component.h index 19b411e..d027d9a 100644 --- a/src/game/component/player_component.h +++ b/src/game/component/player_component.h @@ -11,6 +11,7 @@ namespace engine::component { class PhysicsComponent; class SpriteComponent; class AnimationComponent; + class HealthComponent; } namespace game::component::state { @@ -30,6 +31,7 @@ private: engine::component::SpriteComponent* sprite_component_ = nullptr; engine::component::PhysicsComponent* physics_component_ = nullptr; engine::component::AnimationComponent* animation_component_ = nullptr; + engine::component::HealthComponent* health_component_ = nullptr; std::unique_ptr current_state_; bool is_dead_ = false; @@ -40,6 +42,9 @@ private: float friction_factor_ = 0.85f; ///< @brief 摩擦系数 (Idle时缓冲效果,每帧乘以此系数) float jump_force_ = 350.0f; ///< @brief 跳跃力 (按下"jump"键给的瞬间向上的力) + // --- 属性相关参数 --- + float stunned_duration_ = 0.4f; ///< @brief 玩家被击中后的硬直时间(单位:秒) + public: PlayerComponent() = default; ~PlayerComponent() override = default; @@ -50,11 +55,14 @@ public: PlayerComponent(PlayerComponent&&) = delete; PlayerComponent& operator=(PlayerComponent&&) = delete; + bool takeDamage(int damage); ///< @brief 试图造成伤害,返回是否成功 + // setters and getters 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_; } void setIsDead(bool is_dead) { is_dead_ = is_dead; } ///< @brief 设置玩家是否死亡 bool isDead() const { return is_dead_; } ///< @brief 获取玩家是否死亡 @@ -65,7 +73,9 @@ public: void setFrictionFactor(float friction_factor) { friction_factor_ = friction_factor; } ///< @brief 设置摩擦系数 float getFrictionFactor() const { return friction_factor_; } ///< @brief 获取摩擦系数 void setJumpForce(float jump_force) { jump_force_ = jump_force; } ///< @brief 设置跳跃力 - float getJumpForce() const { return jump_force_; } + float getJumpForce() const { return jump_force_; } ///< @brief 获取跳跃力 + void setStunnedDuration(float duration) { stunned_duration_ = duration; } ///< @brief 设置硬直时间 + float getStunnedDuration() const { return stunned_duration_; } ///< @brief 获取硬直时间 void setState(std::unique_ptr new_state); ///< @brief 切换玩家状态 diff --git a/src/game/component/state/dead_state.cpp b/src/game/component/state/dead_state.cpp new file mode 100644 index 0000000..8e62546 --- /dev/null +++ b/src/game/component/state/dead_state.cpp @@ -0,0 +1,39 @@ +#include "dead_state.h" +#include "../player_component.h" +#include "../../../engine/object/game_object.h" +#include "../../../engine/component/animation_component.h" +#include "../../../engine/component/physics_component.h" +#include "../../../engine/component/collider_component.h" + +namespace game::component::state { + +void DeadState::enter() { + spdlog::debug("玩家进入死亡状态。"); + playAnimation("hurt"); // 播放死亡(受伤)动画 + + // 应用击退力(只向上) + auto physics_component = player_component_->getPhysicsComponent(); + physics_component->velocity_ = glm::vec2(0.0f, -200.0f); // 向上击退 + + // 禁用碰撞(自动掉出屏幕) + auto collider_component = player_component_->getOwner()->getComponent(); + if (collider_component) { + collider_component->setActive(false); + } +} + +void DeadState::exit() { + +} + +std::unique_ptr DeadState::handleInput(engine::core::Context&){ + // 死亡状态下不处理输入 + return nullptr; +} + +std::unique_ptr DeadState::update(float, engine::core::Context&){ + // 死亡状态下不更新状态 + return nullptr; +} + +} \ No newline at end of file diff --git a/src/game/component/state/dead_state.h b/src/game/component/state/dead_state.h new file mode 100644 index 0000000..01f8235 --- /dev/null +++ b/src/game/component/state/dead_state.h @@ -0,0 +1,19 @@ +#pragma once +#include "player_state.h" + +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 handleInput(engine::core::Context&) override; + std::unique_ptr update(float delta_time, engine::core::Context&) override; +}; + +} // namespace game::component::state diff --git a/src/game/component/state/hurt_state.cpp b/src/game/component/state/hurt_state.cpp new file mode 100644 index 0000000..5f1de96 --- /dev/null +++ b/src/game/component/state/hurt_state.cpp @@ -0,0 +1,56 @@ +#include "hurt_state.h" +#include "idle_state.h" +#include "walk_state.h" +#include "fall_state.h" +#include "../player_component.h" +#include "../../../engine/core/context.h" +#include "../../../engine/component/physics_component.h" +#include "../../../engine/component/sprite_component.h" +#include + +namespace game::component::state { + +void HurtState::enter() { + playAnimation("hurt"); // 播放受伤动画 + // --- 造成击退效果 --- + auto physics_component = player_component_->getPhysicsComponent(); + auto sprite_component = player_component_->getSpriteComponent(); + auto knockback_velocity = glm::vec2(-100.0f, -150.0f); // 默认左上方击退效果 + // 根据当前精灵的朝向状态决定是否改成右上方 + if (sprite_component->isFlipped()) { + knockback_velocity.x = -knockback_velocity.x; // 变成向右 + } + physics_component->velocity_ = knockback_velocity; // 设置击退速度 + +} + +void HurtState::exit() { + +} + +std::unique_ptr HurtState::handleInput(engine::core::Context&){ + // 硬直期不能进行任何操控 + return nullptr; +} + +std::unique_ptr HurtState::update(float delta_time, engine::core::Context&){ + stunned_timer_ += delta_time; + // --- 两种情况离开受伤(硬直)状态:--- + // 1. 落地 + auto physics_component = player_component_->getPhysicsComponent(); + if (physics_component->hasCollidedBelow()) { + if (glm::abs(physics_component->velocity_.x) < 1.0f) { + return std::make_unique(player_component_); + } else { + return std::make_unique(player_component_); + } + } + // 2. 硬直时间结束(能走到这里说明没有落地,直接切换到 FallState) + if (stunned_timer_ > player_component_->getStunnedDuration()){ + stunned_timer_ = 0.0f; // 重置硬直计时器 + return std::make_unique(player_component_); // 切换到下落状态 + } + return nullptr; +} + +} \ No newline at end of file diff --git a/src/game/component/state/hurt_state.h b/src/game/component/state/hurt_state.h new file mode 100644 index 0000000..8967c38 --- /dev/null +++ b/src/game/component/state/hurt_state.h @@ -0,0 +1,22 @@ +#pragma once +#include "player_state.h" + +namespace game::component::state { + +class HurtState final : public PlayerState { + friend class game::component::PlayerComponent; +private: + float stunned_timer_ = 0.0f; ///< @brief 硬直计时器,单位为秒 + +public: + HurtState(PlayerComponent* player_component) : PlayerState(player_component) {} + ~HurtState() override = default; + +private: + void enter() override; + void exit() override; + std::unique_ptr handleInput(engine::core::Context&) override; + std::unique_ptr update(float delta_time, engine::core::Context&) override; +}; + +} // namespace game::component::state diff --git a/src/game/scene/game_scene.cpp b/src/game/scene/game_scene.cpp index bbc12a1..71bc803 100644 --- a/src/game/scene/game_scene.cpp +++ b/src/game/scene/game_scene.cpp @@ -60,6 +60,7 @@ void GameScene::render() { void GameScene::handleInput() { Scene::handleInput(); + testHealth(); // 测试生命值组件 } void GameScene::clean() { @@ -167,4 +168,12 @@ bool GameScene::initEnemyAndItem() return success; } +void GameScene::testHealth() +{ + auto input_manager = context_.getInputManager(); + if (input_manager.isActionPressed("attack")) { + player_->getComponent()->takeDamage(1); + } +} + } // namespace game::scene \ No newline at end of file diff --git a/src/game/scene/game_scene.h b/src/game/scene/game_scene.h index 26f7b2e..681890b 100644 --- a/src/game/scene/game_scene.h +++ b/src/game/scene/game_scene.h @@ -30,6 +30,9 @@ private: [[nodiscard]] bool initPlayer(); ///< @brief 初始化玩家 [[nodiscard]] bool initEnemyAndItem(); ///< @brief 初始化敌人和道具 + // 测试函数 + void testHealth(); ///< @brief 测试生命值组件 + }; } // namespace game::scene \ No newline at end of file