Animal AI

This commit is contained in:
tovjemam 2026-06-17 20:49:05 +02:00
parent 9a9c182347
commit 030539f8f0
9 changed files with 254 additions and 13 deletions

View File

@ -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<uint32_t>(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<float>() * 0.5f;
yaw = glm::mod(yaw, glm::two_pi<float>());
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<float>()), 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_;
}

View File

@ -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_{};
};
}

View File

@ -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)

View File

@ -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();

View File

@ -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()

View File

@ -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();

View File

@ -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");

View File

@ -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();

View File

@ -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);
}