bug(goap): fixed unexpected AI behaviour
- Death now removes entity from awareness - State failure now causes replan or goal reevaluation
This commit is contained in:
parent
a2240797b8
commit
111485437c
36 changed files with 845 additions and 88 deletions
|
|
@ -16,32 +16,45 @@ void EnemyWorldState::_bind_methods() {
|
|||
void EnemyWorldState::_ready() { GDGAMEONLY();
|
||||
this->awareness_area = this->get_node<gd::Area3D>("%AwarenessArea");
|
||||
this->awareness_area->connect("body_entered", callable_mp(this, &EnemyWorldState::on_awareness_entered));
|
||||
this->awareness_area->connect("body_exited", callable_mp(this, &EnemyWorldState::on_awareness_exited));
|
||||
// this->awareness_area->connect("body_exited", callable_mp(this, &EnemyWorldState::on_awareness_exited));
|
||||
this->health = this->get_node<EntityHealth>("%EntityHealth");
|
||||
this->health->connect("damage", callable_mp(this, &EnemyWorldState::on_damaged));
|
||||
this->parent_unit->connect("plan_interrupted", callable_mp(this, &EnemyWorldState::select_and_set_target));
|
||||
}
|
||||
|
||||
void EnemyWorldState::on_awareness_entered(gd::Node3D *node) {
|
||||
Unit *unit{gd::Object::cast_to<Unit>(node)};
|
||||
if(unit == nullptr) return;
|
||||
if(unit == this->parent_unit) return;
|
||||
if(unit == nullptr)
|
||||
return;
|
||||
if(unit == this->parent_unit)
|
||||
return;
|
||||
if(unit->get_entity_health()->get_is_dead())
|
||||
return;
|
||||
unit->get_entity_health()->connect("death", this->aware_unit_death.bind(unit));
|
||||
this->known_enemies.push_back(unit);
|
||||
this->try_set_target(this->select_target_from_known());
|
||||
}
|
||||
|
||||
void EnemyWorldState::on_awareness_exited(gd::Node3D *node) {
|
||||
Unit *unit{gd::Object::cast_to<Unit>(node)};
|
||||
this->known_enemies.erase(unit);
|
||||
if(unit == nullptr)
|
||||
return;
|
||||
if(this->known_enemies.has(unit)) {
|
||||
unit->get_entity_health()->disconnect("death", this->aware_unit_death.bind(unit));
|
||||
this->known_enemies.erase(unit);
|
||||
}
|
||||
if(unit == this->target_node) {
|
||||
this->select_and_set_target();
|
||||
}
|
||||
}
|
||||
|
||||
void EnemyWorldState::on_damaged(int remaining, int delta, Unit *source) {
|
||||
if(this->parent_unit->has_plan())
|
||||
return;
|
||||
float highest_priority{0.f};
|
||||
Unit *highest_priority_unit{this->select_target_from_known_with_priority(&highest_priority)};
|
||||
float const priority{this->calculate_priority(source)};
|
||||
if(priority >= highest_priority)
|
||||
this->try_set_target(source);
|
||||
if(!this->known_enemies.has(source)) {
|
||||
source->get_entity_health()->connect("death", this->aware_unit_death.bind(source));
|
||||
this->known_enemies.push_back(source);
|
||||
}
|
||||
if(!this->parent_unit->has_plan())
|
||||
this->try_set_target(this->select_target_from_known());
|
||||
}
|
||||
|
||||
Unit *EnemyWorldState::select_target_from_known_with_priority(float *out_priority) {
|
||||
|
|
@ -62,6 +75,14 @@ Unit *EnemyWorldState::select_target_from_known() {
|
|||
return this->select_target_from_known_with_priority(&dummy);
|
||||
}
|
||||
|
||||
void EnemyWorldState::on_aware_unit_death(Unit *_, Unit *dead_entity) {
|
||||
this->on_awareness_exited(dead_entity);
|
||||
}
|
||||
|
||||
void EnemyWorldState::select_and_set_target() {
|
||||
this->try_set_target(this->select_target_from_known());
|
||||
}
|
||||
|
||||
float EnemyWorldState::calculate_priority(Unit *target) {
|
||||
return target->get_global_position().distance_squared_to(this->parent_unit->get_global_position());
|
||||
}
|
||||
|
|
@ -80,6 +101,11 @@ gd::Ref<goap::Goal> EnemyWorldState::get_goal_for_target(Unit *unit) {
|
|||
}
|
||||
|
||||
void EnemyWorldState::try_set_target(Unit *unit) {
|
||||
if(unit == nullptr) {
|
||||
this->target_node = nullptr;
|
||||
this->parent_unit->stop_plan();
|
||||
return;
|
||||
}
|
||||
gd::Ref<goap::Goal> goal{this->get_goal_for_target(unit)};
|
||||
if(goal.is_valid())
|
||||
this->parent_unit->set_target_goal(unit, goal);
|
||||
|
|
|
|||
|
|
@ -28,12 +28,15 @@ public:
|
|||
//! Shorthand for select_target_from_known_with_priority(nullptr)
|
||||
Unit *select_target_from_known();
|
||||
private:
|
||||
void select_and_set_target();
|
||||
void on_aware_unit_death(Unit *_, Unit *dead_entity);
|
||||
float calculate_priority(Unit *target);
|
||||
gd::Ref<goap::Goal> get_goal_for_target(Unit *unit);
|
||||
void try_set_target(Unit *target);
|
||||
void set_editor_available_goals(gd::Array array);
|
||||
gd::Array get_editor_available_goals() const;
|
||||
private:
|
||||
gd::Callable aware_unit_death{callable_mp(this, &EnemyWorldState::on_aware_unit_death)};
|
||||
gd::Vector<gd::Ref<goap::Goal>> available_goals{};
|
||||
gd::Vector<Unit *> known_enemies{};
|
||||
|
||||
|
|
|
|||
|
|
@ -52,6 +52,10 @@ void EntityHealth::healed_by(int amount, Unit *source) {
|
|||
this->emit_signal("health_changed", this->injury_current, amount);
|
||||
}
|
||||
|
||||
bool EntityHealth::get_is_dead() const {
|
||||
return this->injury_current <= 0;
|
||||
}
|
||||
|
||||
void EntityHealth::set_injury_max(int max_health) {
|
||||
this->injury_max = max_health;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ public:
|
|||
void damage(int amount);
|
||||
void healed_by(int amount, Unit *source);
|
||||
void heal(int amount);
|
||||
bool get_is_dead() const;
|
||||
void set_injury_max(int max_health);
|
||||
int get_injury_max() const;
|
||||
int get_injury_current() const;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,11 @@ namespace goap {
|
|||
Action::~Action() {}
|
||||
|
||||
bool Action::is_possible(ActorWorldState *context) const {
|
||||
for(WorldProperty const &prop : this->required) {
|
||||
if(!context->check_property_match(prop)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return this->procedural_is_possible(context);
|
||||
}
|
||||
|
||||
|
|
@ -17,6 +22,14 @@ bool Action::is_completed(ActorWorldState *context) const {
|
|||
return this->procedural_is_completed(context);
|
||||
}
|
||||
|
||||
bool Action::procedural_is_possible(ActorWorldState *context) const {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Action::procedural_is_completed(ActorWorldState *context) const {
|
||||
return true;
|
||||
}
|
||||
|
||||
WorldState const &Action::get_required() const {
|
||||
return this->required;
|
||||
}
|
||||
|
|
@ -32,12 +45,4 @@ ActionID Action::get_id() const {
|
|||
bool Action::get_require_state_complete() const {
|
||||
return this->require_state_complete;
|
||||
}
|
||||
|
||||
bool Action::procedural_is_possible(ActorWorldState *context) const {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Action::procedural_is_completed(ActorWorldState *context) const {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ public:
|
|||
|
||||
bool is_completed(ActorWorldState *context) const;
|
||||
bool is_possible(ActorWorldState *context) const;
|
||||
virtual bool procedural_is_possible(ActorWorldState *context) const;
|
||||
virtual bool procedural_is_completed(ActorWorldState *context) const;
|
||||
|
||||
WorldState const &get_required() const;
|
||||
WorldState const &get_effects() const;
|
||||
|
|
@ -48,8 +50,6 @@ protected:
|
|||
Action() = default;
|
||||
template<class TState>
|
||||
TState *create_state() const;
|
||||
virtual bool procedural_is_possible(ActorWorldState *context) const;
|
||||
virtual bool procedural_is_completed(ActorWorldState *context) const;
|
||||
protected:
|
||||
WorldState blocking_required{};
|
||||
WorldState required{};
|
||||
|
|
|
|||
|
|
@ -171,7 +171,7 @@ gd::Vector<Action const *> &Planner::get_actions() {
|
|||
gd::Vector<Action const *> Planner::get_neighbours(WorldStateNode const &from) const {
|
||||
gd::Vector<Action const *> retval{};
|
||||
for(Action const *action : this->actions) {
|
||||
if(!action->is_possible(from.context) || !this->does_action_contribute(action, from))
|
||||
if(!action->procedural_is_possible(from.context) || !this->does_action_contribute(action, from))
|
||||
continue;
|
||||
retval.push_back(action);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,16 +92,18 @@ goap::State *TankSelfHeal::get_apply_state(goap::ActorWorldState *context) const
|
|||
|
||||
TakeCover::TakeCover()
|
||||
: Action () {
|
||||
this->require_state_complete = false;
|
||||
this->effects.insert("can_see_target", false);
|
||||
}
|
||||
|
||||
bool TakeCover::procedural_is_possible(goap::ActorWorldState *context) const {
|
||||
// positions of the context and the target
|
||||
gd::Vector3 const global_position{context->get_world_property("parent_global_position")};
|
||||
gd::Vector3 const target_position{context->get_world_property("target_global_position")};
|
||||
// if there is no available navigation room, it is not possible to find a cover marker
|
||||
NavRoom *room{NavRoom::get_closest_room(global_position)};
|
||||
if(room == nullptr)
|
||||
return false;
|
||||
// immediately return true if there is at least one marker that counts as cover from the target
|
||||
for(NavMarker *marker : room->get_markers())
|
||||
if(TakeCover::is_marker_cover_from(marker, target_position))
|
||||
return true;
|
||||
|
|
@ -109,22 +111,30 @@ bool TakeCover::procedural_is_possible(goap::ActorWorldState *context) const {
|
|||
}
|
||||
|
||||
goap::State *TakeCover::get_apply_state(goap::ActorWorldState *context) const {
|
||||
MoveTo *state{this->create_state<MoveTo>()};
|
||||
// positions of the context and the target to hide from
|
||||
gd::Vector3 const global_position{context->get_world_property("parent_global_position")};
|
||||
gd::Vector3 const target_position{context->get_world_property("target_global_position")};
|
||||
// find the room this entity is located in
|
||||
NavRoom *room{NavRoom::get_closest_room(global_position)};
|
||||
if(room == nullptr)
|
||||
return nullptr;
|
||||
float best_score{-INFINITY};
|
||||
NavMarker *best_marker{nullptr}; // marker with the best score found so far
|
||||
float best_score{0.f}; // best score found so far
|
||||
for(NavMarker *marker : room->get_markers()) {
|
||||
gd::Vector3 const marker_position{marker->get_global_position()};
|
||||
float const score{100.f - marker_position.distance_to(global_position)};
|
||||
gd::Vector3 const marker_position{marker->get_global_position()}; // position of the marker being considered
|
||||
float const score{(marker_position.distance_squared_to(target_position) * 1.2f) // score is a comparison between distances to the target and context
|
||||
- (marker_position.distance_squared_to(global_position) * 0.8f)};
|
||||
if(score > best_score && TakeCover::is_marker_cover_from(marker, target_position)) {
|
||||
best_score = score;
|
||||
state->target_node = marker;
|
||||
gd::UtilityFunctions::print("!!! best cover so far: ", marker->get_path());
|
||||
best_marker = marker;
|
||||
}
|
||||
}
|
||||
// don't bother creating a state if there is no target
|
||||
if(best_marker == nullptr)
|
||||
return nullptr;
|
||||
// create state, set target, and return
|
||||
MoveTo *state{this->create_state<MoveTo>()};
|
||||
state->target_node = best_marker;
|
||||
return state;
|
||||
}
|
||||
|
||||
|
|
@ -132,5 +142,5 @@ bool TakeCover::is_marker_cover_from(NavMarker *marker, gd::Vector3 const &targe
|
|||
return marker->get_marker_type() == MarkerType::Cover
|
||||
&& marker->get_global_basis()
|
||||
.get_column(2)
|
||||
.dot((marker->get_global_position() - target).normalized()) < -0.7f;
|
||||
.dot(marker->get_global_position() - target) < -2.f;
|
||||
}
|
||||
|
|
|
|||
34
src/unit.cpp
34
src/unit.cpp
|
|
@ -1,5 +1,6 @@
|
|||
#include "unit.hpp"
|
||||
#include "entity_health.hpp"
|
||||
#include "goap/action.hpp"
|
||||
#include "goap/goal.hpp"
|
||||
#include "utils/godot_macros.hpp"
|
||||
#include <godot_cpp/classes/navigation_agent3d.hpp>
|
||||
|
|
@ -10,6 +11,7 @@ void Unit::_bind_methods() {
|
|||
#define CLASSNAME Unit
|
||||
GDSIGNAL("goal_finished");
|
||||
GDSIGNAL("plan_failed");
|
||||
GDSIGNAL("plan_interrupted");
|
||||
GDPROPERTY_HINTED(configure_team, gd::Variant::INT, gd::PROPERTY_HINT_ENUM, UnitTeam::get_property_hint());
|
||||
GDFUNCTION(use_weapon);
|
||||
}
|
||||
|
|
@ -30,7 +32,7 @@ void Unit::stop_plan() {
|
|||
if(this->state && !this->state->is_queued_for_deletion())
|
||||
this->destroy_state();
|
||||
this->state = nullptr;
|
||||
this->emit_signal("plan_failed");
|
||||
this->call_deferred("emit_signal", "plan_failed");
|
||||
}
|
||||
|
||||
void Unit::begin_marker_temporary(GoalMarker *marker) {
|
||||
|
|
@ -62,6 +64,8 @@ void Unit::use_weapon() {
|
|||
gd::Node3D *target{this->world_state->get_target_node()};
|
||||
if(target == nullptr)
|
||||
return;
|
||||
if(!this->world_state->get_can_see_target())
|
||||
return;
|
||||
this->aim_at(target);
|
||||
EntityHealth *health{target->get_node<EntityHealth>("EntityHealth")};
|
||||
if(health == nullptr)
|
||||
|
|
@ -100,14 +104,22 @@ int Unit::get_configure_team() const {
|
|||
}
|
||||
|
||||
bool Unit::has_plan() const {
|
||||
return !this->current_plan.size();
|
||||
return !this->current_plan.is_empty();
|
||||
}
|
||||
|
||||
EntityHealth *Unit::get_entity_health() {
|
||||
return this->health;
|
||||
}
|
||||
|
||||
void Unit::set_goal_and_plan(gd::Ref<goap::Goal> goal) {
|
||||
this->current_goal = goal;
|
||||
this->current_plan = this->planner->plan_for_goal(goal);
|
||||
if(this->current_plan.is_empty()) {
|
||||
this->current_goal.unref();
|
||||
if(goal.is_null()) {
|
||||
this->current_plan.clear();
|
||||
} else {
|
||||
this->current_plan = this->planner->plan_for_goal(goal);
|
||||
if(this->current_plan.is_empty()) {
|
||||
this->current_goal.unref();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -128,17 +140,21 @@ void Unit::next_action() {
|
|||
// cannot perform actions while dead
|
||||
if(this->health->get_injury_current() <= 0)
|
||||
return;
|
||||
// destroy active state
|
||||
// destroy active state if relevant
|
||||
if(this->state != nullptr && !this->state->is_queued_for_deletion())
|
||||
this->destroy_state();
|
||||
this->state = nullptr;
|
||||
if(this->current_plan.is_empty())
|
||||
return;
|
||||
goap::Action const *action{this->current_plan.get(0)};
|
||||
if(!action->is_possible(this->world_state)) {
|
||||
this->call_deferred("emit_signal", "plan_interrupted");
|
||||
return;
|
||||
}
|
||||
// pop next action and apply state
|
||||
this->state = this->current_plan.get(0)->get_apply_state(this->world_state);
|
||||
this->state = action->get_apply_state(this->world_state);
|
||||
if(state == nullptr) {
|
||||
this->stop_plan();
|
||||
gd::UtilityFunctions::push_error("Plan failed to be executed, abandoning");
|
||||
this->call_deferred("emit_signal", "plan_interrupted");
|
||||
return;
|
||||
}
|
||||
this->current_plan.remove_at(0);
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ public:
|
|||
void set_configure_team(int value);
|
||||
int get_configure_team() const;
|
||||
bool has_plan() const;
|
||||
EntityHealth *get_entity_health();
|
||||
private:
|
||||
void set_goal_and_plan(gd::Ref<goap::Goal> goal);
|
||||
void destroy_state();
|
||||
|
|
|
|||
|
|
@ -50,9 +50,7 @@ bool UnitWorldState::get_can_see_target() const {
|
|||
target_position,
|
||||
0x1 | 0x4, ignore_list)
|
||||
};
|
||||
bool const can_see{this->parent_unit->get_world_3d()->get_direct_space_state()->intersect_ray(query).is_empty()};
|
||||
gd::UtilityFunctions::print(this->parent_unit->get_path(), can_see ? " can see " : " can't see ", this->target_node->get_path());
|
||||
return can_see;
|
||||
return this->parent_unit->get_world_3d()->get_direct_space_state()->intersect_ray(query).is_empty();
|
||||
}
|
||||
|
||||
bool UnitWorldState::get_is_at_target() const {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue