From 030539f8f0eff4ceedf74f3574fba95c46267174 Mon Sep 17 00:00:00 2001 From: tovjemam Date: Wed, 17 Jun 2026 20:49:05 +0200 Subject: [PATCH] Animal AI --- src/game/animal.cpp | 192 ++++++++++++++++++++++++++++++++++++++++- src/game/animal.hpp | 36 ++++++++ src/game/character.cpp | 2 + src/game/character.hpp | 2 + src/game/cow.cpp | 13 +-- src/game/cow.hpp | 4 +- src/game/rideable.cpp | 4 +- src/game/rideable.hpp | 4 +- src/utils/random.hpp | 10 +++ 9 files changed, 254 insertions(+), 13 deletions(-) diff --git a/src/game/animal.cpp b/src/game/animal.cpp index 32f9af0..f0efb27 100644 --- a/src/game/animal.cpp +++ b/src/game/animal.cpp @@ -2,6 +2,7 @@ #include "player_character.hpp" #include "input_mapping.hpp" +#include "utils/random.hpp" game::Animal::Animal(World& world, const CharacterTuning& tuning, const glm::vec3& position, float yaw) : Character(world, tuning), Usable(root_.matrix), Rideable(*this, RIDEABLE_ANIMAL) @@ -14,6 +15,13 @@ game::Animal::Animal(World& world, const CharacterTuning& tuning, const glm::vec collision::AddObjectFlags(&GetController()->GetBtGhost(), collision::OF_USABLE); } +void game::Animal::Update() +{ + Think(); + just_hit_ = false; + Super::Update(); +} + bool game::Animal::QueryUseTarget(PlayerCharacter& character, uint32_t target_id, UseTargetQueryResult& res) { if (character.GetRideable()) @@ -38,12 +46,25 @@ void game::Animal::Use(PlayerCharacter& character, uint32_t target_id) void game::Animal::SetRideableInput(PlayerInputFlags in) { - SetInputs(MapPlayerInputToCharacterInput(in)); + if (think_state_ == ANIMAL_THINKSTATE_MOUNTED) + { + SetInputs(MapPlayerInputToCharacterInput(in)); + } } void game::Animal::SetRideableViewAngles(float yaw, float pitch) { - SetViewAngles(yaw, pitch); + if (think_state_ == ANIMAL_THINKSTATE_MOUNTED) + { + SetViewAngles(yaw, pitch); + } +} + +void game::Animal::OnBulletHit(const game::BulletInfo& bullet, const std::string_view hit_bone) +{ + just_hit_ = true; + // attacker_ = bullet.shooter->GetEntNum(); + hit_from_ = bullet.start; } void game::Animal::OnPassengerChanged(size_t seat_idx, HumanCharacter* passenger) @@ -65,3 +86,170 @@ void game::Animal::AddAnimalSeat(const glm::vec3& offset) use_targets_.emplace_back(this, static_cast(seat_idx), offset + glm::vec3(0.0f, 0.0f, 1.0f), use_message_ + " (místo " + std::to_string(seat_idx + 1) + ")"); } + +bool game::Animal::IsMounted() const +{ + return GetPassenger(0) != nullptr; +} + +void game::Animal::ChangeDirection() +{ + auto yaw = GetViewYaw(); + yaw += RandomFloat(-1.0f, 1.0f) * glm::half_pi() * 0.5f; + yaw = glm::mod(yaw, glm::two_pi()); + SetViewAngles(yaw, 0.0f); +} + +void game::Animal::TryMakeSound() +{ + auto time = GetWorld().GetTime(); + if (time - last_sound_time_ < 3000) + return; + + last_sound_time_ = time; + MakeSound(); +} + +void game::Animal::Think() +{ + while (true) + { + auto new_state = CheckThinkStateTransition(); + if (new_state == think_state_) + break; + + EnterThinkState(new_state); + } +} + +static game::CharacterInputFlags GetRandomRoamInput() +{ + game::CharacterInputFlags in = 0; + auto dir_choice = RandomFloat(0.0f, 1.0f); + + if (dir_choice < 0.4f) + in |= 1 << game::CIN_FORWARD; + else if (dir_choice < 0.6f) + in |= (1 << game::CIN_FORWARD) | (1 << game::CIN_RIGHT); + else if (dir_choice < 0.8f) + in |= (1 << game::CIN_FORWARD) | (1 << game::CIN_RIGHT); + + return in; +} + +static float GetAwayYaw(const glm::vec3& my_pos, const glm::vec3& enemy) +{ + auto away_dir = glm::normalize(glm::vec2(my_pos - enemy)); + auto yaw = glm::atan(-away_dir.x, away_dir.y); + return yaw; +} + +void game::Animal::EnterThinkState(AnimalThinkState state) +{ + think_state_ = state; + think_state_start_ = GetWorld().GetTime(); + + switch (state) + { + case ANIMAL_THINKSTATE_IDLE: + SetInputs(0); + break; + + case ANIMAL_THINKSTATE_ROAM: + SetViewAngles(RandomFloat(0.0f, glm::two_pi()), 0.0f); + SetInputs(GetRandomRoamInput()); + SetWeightSpeedMult(0.3f); + break; + + case ANIMAL_THINKSTATE_MOUNTED: + SetInputs(0); + SetWeightSpeedMult(1.0f); + break; + + case ANIMAL_THINKSTATE_HURT: + // SetInput(CIN_JUMP, true); + SetWeightSpeedMult(1.0f); + MakeHurtSound(); + break; + + case ANIMAL_THINKSTATE_RUN_AWAY: + SetWeightSpeedMult(1.0f); + SetInputs((1 << CIN_FORWARD) | (1 << CIN_SPRINT)); + SetViewAngles(GetAwayYaw(root_.GetGlobalPosition(), hit_from_), 0.0f); + break; + + default: + break; + } +} + +game::AnimalThinkState game::Animal::CheckThinkStateTransition() +{ + switch (think_state_) + { + case ANIMAL_THINKSTATE_IDLE: + if (IsMounted()) + return ANIMAL_THINKSTATE_MOUNTED; + + if (just_hit_) + return ANIMAL_THINKSTATE_HURT; + + if (ChanceAvgTime(5.0f) || GetCurrentThinkStateDuration() > 10000) + return ANIMAL_THINKSTATE_ROAM; + + if (ChanceAvgTime(15.0f)) + TryMakeSound(); + + return ANIMAL_THINKSTATE_IDLE; + + case ANIMAL_THINKSTATE_ROAM: + if (just_hit_) + return ANIMAL_THINKSTATE_HURT; + + if (ChanceAvgTime(5.0f) || GetCurrentThinkStateDuration() > 15000) + return ANIMAL_THINKSTATE_IDLE; + + if (ChanceAvgTime(1.0f)) + ChangeDirection(); + + return ANIMAL_THINKSTATE_ROAM; + + case ANIMAL_THINKSTATE_MOUNTED: + if (!IsMounted()) + return ANIMAL_THINKSTATE_IDLE; + + if (just_hit_ && Chance(0.07f)) + return ANIMAL_THINKSTATE_HURT; + + return ANIMAL_THINKSTATE_MOUNTED; + + case ANIMAL_THINKSTATE_HURT: + if (GetCurrentThinkStateDuration() > 0) + return ANIMAL_THINKSTATE_RUN_AWAY; + + return ANIMAL_THINKSTATE_HURT; + + case ANIMAL_THINKSTATE_RUN_AWAY: + if (!IsMounted() && GetCurrentThinkStateDuration() > 4000 && + (ChanceAvgTime(7.0f) || GetCurrentThinkStateDuration() > 9000)) + return ANIMAL_THINKSTATE_ROAM; + + if (IsMounted() && (ChanceAvgTime(1.0f) || GetCurrentThinkStateDuration() > 2000)) + return ANIMAL_THINKSTATE_IDLE; + + if (ChanceAvgTime(1.0f)) + ChangeDirection(); + + return ANIMAL_THINKSTATE_RUN_AWAY; + + + default: + return ANIMAL_THINKSTATE_IDLE; + } + +} + +int64_t game::Animal::GetCurrentThinkStateDuration() const +{ + return GetWorld().GetTime() - think_state_start_; +} diff --git a/src/game/animal.hpp b/src/game/animal.hpp index 34dc20d..c478d13 100644 --- a/src/game/animal.hpp +++ b/src/game/animal.hpp @@ -7,11 +7,24 @@ namespace game { +enum AnimalThinkState +{ + ANIMAL_THINKSTATE_IDLE, + ANIMAL_THINKSTATE_ROAM, + ANIMAL_THINKSTATE_MOUNTED, + ANIMAL_THINKSTATE_HURT, + ANIMAL_THINKSTATE_RUN_AWAY, +}; + class Animal : public Character, public Usable, public Rideable { public: + using Super = Character; + Animal(World& world, const CharacterTuning& tuning, const glm::vec3& position, float yaw); + virtual void Update() override; + virtual bool QueryUseTarget(PlayerCharacter& character, uint32_t target_id, UseTargetQueryResult& res) override; virtual void Use(PlayerCharacter& character, uint32_t target_id) override; @@ -19,13 +32,36 @@ public: virtual void SetRideableViewAngles(float yaw, float pitch) override; protected: + virtual void OnBulletHit(const game::BulletInfo& bullet, const std::string_view hit_bone) override; + virtual void OnPassengerChanged(size_t seat_idx, HumanCharacter* passenger) override; void SetUseMessage(const std::string& message); void AddAnimalSeat(const glm::vec3& offset); + virtual void MakeSound() {} + virtual void MakeHurtSound() {} + +private: + bool IsMounted() const; + void ChangeDirection(); + void TryMakeSound(); + + void Think(); + void EnterThinkState(AnimalThinkState state); + AnimalThinkState CheckThinkStateTransition(); + int64_t GetCurrentThinkStateDuration() const; + private: std::string use_message_; + AnimalThinkState think_state_ = ANIMAL_THINKSTATE_IDLE; + int64_t think_state_start_ = 0; + int64_t last_sound_time_ = 0; + + bool just_hit_ = false; + // net::EntNum attacker_ = 0; + glm::vec3 hit_from_{}; + }; } \ No newline at end of file diff --git a/src/game/character.cpp b/src/game/character.cpp index dcb483e..780006b 100644 --- a/src/game/character.cpp +++ b/src/game/character.cpp @@ -89,6 +89,8 @@ void game::Character::OnBulletHit(const game::BulletInfo& bullet, const btCollis std::string text = "au! " + std::string(hit_name); GetWorld().SendChat(text); + + OnBulletHit(bullet, hit_name); } void game::Character::Attach(net::EntNum parentnum) diff --git a/src/game/character.hpp b/src/game/character.hpp index 36c9d48..9e3d2b6 100644 --- a/src/game/character.hpp +++ b/src/game/character.hpp @@ -116,6 +116,8 @@ protected: void SetViewItem(const std::string& item_name); void SendFire(); + virtual void OnBulletHit(const game::BulletInfo& bullet, const std::string_view hit_bone) {} + private: void SyncControllerTransform(); void SyncTransformFromController(); diff --git a/src/game/cow.cpp b/src/game/cow.cpp index b71bf02..d257a1f 100644 --- a/src/game/cow.cpp +++ b/src/game/cow.cpp @@ -22,7 +22,6 @@ game::Cow::Cow(World& world, const glm::vec3& position, float yaw) : Animal(worl SetIdleAnim("idle"); SetWalkAnim("walk"); - ScheduleRandomMoo(); } void game::Cow::OnPassengerChanged(size_t seat_idx, HumanCharacter* passenger) @@ -35,12 +34,14 @@ void game::Cow::OnPassengerChanged(size_t seat_idx, HumanCharacter* passenger) } } -void game::Cow::ScheduleRandomMoo() +void game::Cow::MakeSound() { - Schedule(rand() % 15000 + 5000, [this]() { - PlayRandomMoo(); - ScheduleRandomMoo(); - }); + PlayRandomMoo(); +} + +void game::Cow::MakeHurtSound() +{ + PlayUseSound(); } void game::Cow::PlayRandomMoo() diff --git a/src/game/cow.hpp b/src/game/cow.hpp index 3d6f1d1..9b2c439 100644 --- a/src/game/cow.hpp +++ b/src/game/cow.hpp @@ -15,8 +15,10 @@ public: protected: virtual void OnPassengerChanged(size_t seat_idx, HumanCharacter* passenger) override; + virtual void MakeSound() override; + virtual void MakeHurtSound() override; + private: - void ScheduleRandomMoo(); void PlayRandomMoo(); void PlayUseSound(); diff --git a/src/game/rideable.cpp b/src/game/rideable.cpp index 6efbe0e..47c409d 100644 --- a/src/game/rideable.cpp +++ b/src/game/rideable.cpp @@ -31,7 +31,7 @@ void game::Rideable::SetPassenger(size_t seat_idx, HumanCharacter* passenger) OnPassengerChanged(seat_idx, passenger); } -game::HumanCharacter* game::Rideable::GetPassenger(size_t seat_idx) +game::HumanCharacter* game::Rideable::GetPassenger(size_t seat_idx) const { if (seat_idx >= seats_.size()) return nullptr; @@ -39,7 +39,7 @@ game::HumanCharacter* game::Rideable::GetPassenger(size_t seat_idx) return seats_[seat_idx].passenger; } -const glm::vec3& game::Rideable::GetSeatOffset(size_t seat_idx) +const glm::vec3& game::Rideable::GetSeatOffset(size_t seat_idx) const { if (seat_idx >= seats_.size()) throw std::runtime_error("Invalid seat index"); diff --git a/src/game/rideable.hpp b/src/game/rideable.hpp index 2deddf5..efe1730 100644 --- a/src/game/rideable.hpp +++ b/src/game/rideable.hpp @@ -25,8 +25,8 @@ public: Rideable(Entity& entity, RideableType type); void SetPassenger(size_t seat_idx, HumanCharacter* passenger); - HumanCharacter* GetPassenger(size_t seat_idx); - const glm::vec3& GetSeatOffset(size_t seat_idx); + HumanCharacter* GetPassenger(size_t seat_idx) const; + const glm::vec3& GetSeatOffset(size_t seat_idx) const; size_t GetNumSeats() const { return seats_.size(); } void KickAll(); diff --git a/src/utils/random.hpp b/src/utils/random.hpp index f4775b1..d35432c 100644 --- a/src/utils/random.hpp +++ b/src/utils/random.hpp @@ -21,4 +21,14 @@ inline glm::vec3 ApplyRandomDispersion(const glm::vec3& dir, float dispersion) auto up = glm::normalize(glm::cross(right, dir)); return dir + (right * glm::sin(rand_rotation) + up * glm::cos(rand_rotation)) * rand_dispersion; +} + +inline bool Chance(float probability) +{ + return RandomFloat(0.0f, 1.0f) < probability; +} + +inline bool ChanceAvgTime(float avg_time) +{ + return Chance((1.0f / 25.0f) / avg_time); } \ No newline at end of file