feat: implemented heal action and goal

This commit is contained in:
Sara 2024-07-21 22:18:10 +02:00
parent a78bbfd3b5
commit f9b3c6eb3f
18 changed files with 131 additions and 42 deletions

View file

@ -0,0 +1,9 @@
[gd_resource type="Goal" format=3 uid="uid://btv8ultseri3q"]
[resource]
requirements_dict = {
"is_health_safe": false
}
desired_state_dict = {
"is_health_safe": true
}

Binary file not shown.

View file

@ -1,6 +1,7 @@
[gd_scene load_steps=8 format=3 uid="uid://ba17jrcaduowj"] [gd_scene load_steps=9 format=3 uid="uid://ba17jrcaduowj"]
[ext_resource type="Goal" uid="uid://b4i4e34046n44" path="res://AI/defeat_enemy_unit.tres" id="1_b1qo1"] [ext_resource type="Goal" uid="uid://b4i4e34046n44" path="res://AI/defeat_enemy_unit.tres" id="1_b1qo1"]
[ext_resource type="Goal" uid="uid://btv8ultseri3q" path="res://AI/maintain_health.tres" id="2_k42dl"]
[ext_resource type="AnimationLibrary" uid="uid://crkh5gahl2ci6" path="res://Animation/bean_characters.res" id="2_lrpu6"] [ext_resource type="AnimationLibrary" uid="uid://crkh5gahl2ci6" path="res://Animation/bean_characters.res" id="2_lrpu6"]
[sub_resource type="SphereShape3D" id="SphereShape3D_5pqvg"] [sub_resource type="SphereShape3D" id="SphereShape3D_5pqvg"]
@ -24,11 +25,11 @@ collision_layer = 6
collision_mask = 0 collision_mask = 0
[node name="ActorWorldState" type="EnemyWorldState" parent="."] [node name="ActorWorldState" type="EnemyWorldState" parent="."]
editor_available_goals = [ExtResource("1_b1qo1")] editor_available_goals = [ExtResource("2_k42dl"), ExtResource("1_b1qo1")]
unique_name_in_owner = true unique_name_in_owner = true
[node name="Planner" type="Planner" parent="."] [node name="Planner" type="Planner" parent="."]
actions_inspector = [3, 2, 4] actions_inspector = [3, 2, 4, 5]
unique_name_in_owner = true unique_name_in_owner = true
[node name="EntityHealth" type="EntityHealth" parent="."] [node name="EntityHealth" type="EntityHealth" parent="."]
@ -63,7 +64,7 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.4512, 0)
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.31501, 0) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.31501, 0)
[node name="CollisionShape3D" type="CollisionShape3D" parent="."] [node name="CollisionShape3D" type="CollisionShape3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 0.999222, -0.0394342, 0, 0.0394342, 0.999222, 0, 1.01253, 0.443459) transform = Transform3D(1, 0, 0, 0, 0.999222, -0.0394342, 0, 0.0394342, 0.999222, 0, 1.013, 0)
shape = SubResource("SphereShape3D_drlm2") shape = SubResource("SphereShape3D_drlm2")
[node name="MeshInstance3D" type="MeshInstance3D" parent="."] [node name="MeshInstance3D" type="MeshInstance3D" parent="."]

View file

@ -19,16 +19,13 @@ void EnemyWorldState::_ready() {
this->awareness_area->connect("body_entered", callable_mp(this, &EnemyWorldState::on_awareness_entered)); 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 = this->get_node<EntityHealth>("%EntityHealth");
if(this->health == nullptr)
this->health->connect("damage", callable_mp(this, &EnemyWorldState::on_damaged)); this->health->connect("damage", callable_mp(this, &EnemyWorldState::on_damaged));
} }
void EnemyWorldState::on_awareness_entered(gd::Node3D *node) { void EnemyWorldState::on_awareness_entered(gd::Node3D *node) {
gd::UtilityFunctions::print("1) object entered awareness");
Unit *unit{gd::Object::cast_to<Unit>(node)}; Unit *unit{gd::Object::cast_to<Unit>(node)};
if(unit == nullptr) return; if(unit == nullptr) return;
if(unit == this->parent_unit) return; if(unit == this->parent_unit) return;
gd::UtilityFunctions::print("2) object was Unit");
this->known_enemies.push_back(unit); this->known_enemies.push_back(unit);
this->try_set_target(this->select_target_from_known()); this->try_set_target(this->select_target_from_known());
} }
@ -82,17 +79,15 @@ gd::Ref<goap::Goal> EnemyWorldState::get_goal_for_target(Unit *unit) {
} }
void EnemyWorldState::try_set_target(Unit *unit) { void EnemyWorldState::try_set_target(Unit *unit) {
gd::UtilityFunctions::print("3) selecting goal");
gd::Ref<goap::Goal> goal{this->get_goal_for_target(unit)}; gd::Ref<goap::Goal> goal{this->get_goal_for_target(unit)};
if(!goal.is_valid()) return; if(goal.is_valid())
gd::UtilityFunctions::print("4) selected goal ", goal->get_path());
this->parent_unit->set_target_goal(unit, goal); this->parent_unit->set_target_goal(unit, goal);
} }
void EnemyWorldState::set_editor_available_goals(gd::Array array) { void EnemyWorldState::set_editor_available_goals(gd::Array array) {
this->available_goals.clear(); this->available_goals.clear();
while(!array.is_empty()) { while(!array.is_empty()) {
gd::Ref<goap::Goal> goal{array.pop_back()}; gd::Ref<goap::Goal> goal{array.pop_front()};
this->available_goals.push_back(goal); this->available_goals.push_back(goal);
} }
} }

View file

@ -4,6 +4,9 @@
void EntityHealth::_bind_methods() { void EntityHealth::_bind_methods() {
#define CLASSNAME EntityHealth #define CLASSNAME EntityHealth
GDFUNCTION_ARGS(damage, "amount");
GDFUNCTION_ARGS(damaged_by, "amount", "source");
GDFUNCTION_ARGS(healed_by, "amount", "source");
GDPROPERTY(injury_max, gd::Variant::INT); GDPROPERTY(injury_max, gd::Variant::INT);
GDPROPERTY(wounds_max, gd::Variant::INT); GDPROPERTY(wounds_max, gd::Variant::INT);
@ -27,8 +30,13 @@ void EntityHealth::_enter_tree() {
this->emit_signal("damage", this->injury_current, 0, nullptr); this->emit_signal("damage", this->injury_current, 0, nullptr);
} }
void EntityHealth::damage(int amount) {
this->damaged_by(amount, nullptr);
}
void EntityHealth::damaged_by(int amount, Unit *source) { void EntityHealth::damaged_by(int amount, Unit *source) {
amount = gd::Math::abs(amount); amount = gd::Math::abs(amount);
if(this->injury_current <= 0) return; // do not take damage when already dead
this->injury_current -= amount; this->injury_current -= amount;
this->emit_signal("damage", this->injury_current, amount, source); this->emit_signal("damage", this->injury_current, amount, source);
this->emit_signal("health_changed", this->injury_current, -amount); this->emit_signal("health_changed", this->injury_current, -amount);

View file

@ -29,6 +29,10 @@ ActionID Action::get_id() const {
return this->id; return this->id;
} }
bool Action::get_require_state_complete() const {
return this->require_state_complete;
}
bool Action::procedural_is_possible(ActorWorldState *context) const { bool Action::procedural_is_possible(ActorWorldState *context) const {
return true; return true;
} }

View file

@ -43,6 +43,7 @@ public:
WorldState const &get_effects() const; WorldState const &get_effects() const;
ActionID get_id() const; ActionID get_id() const;
bool get_require_state_complete() const;
protected: protected:
Action() = default; Action() = default;
template<class TState> template<class TState>
@ -54,6 +55,7 @@ protected:
WorldState required{}; WorldState required{};
WorldState effects{}; WorldState effects{};
bool only_proc_is_completed{false}; bool only_proc_is_completed{false};
bool require_state_complete{true};
private: private:
ActionID id{-1}; ActionID id{-1};
}; };

View file

@ -16,8 +16,8 @@ void State::_enter_tree() {
} }
void State::_process(double delta_time) { void State::_process(double delta_time) {
if(this->is_action_done()) if(this->is_action_done() && this->get_action()->get_require_state_complete())
this->state_finished(); this->state_ended();
} }
Action const *State::get_action() const { Action const *State::get_action() const {
@ -26,20 +26,19 @@ Action const *State::get_action() const {
void State::_end_state() {} void State::_end_state() {}
void State::state_finished() { void State::state_ended() {
this->end_state(); this->end_state();
this->emit_signal("state_finished"); this->emit_signal(this->is_action_done() ? "state_finished" : "state_failed");
}
void State::state_failed() {
this->end_state();
this->emit_signal("state_failed");
} }
bool State::is_action_done() const { bool State::is_action_done() const {
return this->action->is_completed(this->world_state); return this->action->is_completed(this->world_state);
} }
bool State::is_action_done_interrupt() const {
return this->is_action_done() && !this->action->get_require_state_complete();
}
void State::end_state() { void State::end_state() {
this->_end_state(); this->_end_state();
this->queue_free(); this->queue_free();

View file

@ -19,10 +19,12 @@ public:
virtual void _process(double delta_time) override; virtual void _process(double delta_time) override;
protected: protected:
Action const *get_action() const; Action const *get_action() const;
// \returns True if the Action's requirements are complete. Without evaluating Action::require_state_complete.
bool is_action_done() const; bool is_action_done() const;
// \returns True if the Action's requirements are complete. Including Action::require_state_complete.
bool is_action_done_interrupt() const;
virtual void _end_state(); virtual void _end_state();
void state_finished(); void state_ended();
void state_failed();
private: private:
void end_state(); void end_state();
private: private:

View file

@ -35,6 +35,7 @@ void initialize_gdextension_types(ModuleInitializationLevel p_level)
goap::ActionDB::register_action<FindTarget>(); goap::ActionDB::register_action<FindTarget>();
goap::ActionDB::register_action<GetInMeleeRange>(); goap::ActionDB::register_action<GetInMeleeRange>();
goap::ActionDB::register_action<MeleeAttack>(); goap::ActionDB::register_action<MeleeAttack>();
goap::ActionDB::register_action<TankSelfHeal>();
ClassDB::register_class<goap::ActorWorldState>(); ClassDB::register_class<goap::ActorWorldState>();
ClassDB::register_class<goap::Goal>(); ClassDB::register_class<goap::Goal>();

View file

@ -36,6 +36,7 @@ goap::State *FireAtTarget::get_apply_state(goap::ActorWorldState *context) const
FindTarget::FindTarget() FindTarget::FindTarget()
: Action() { : Action() {
this->require_state_complete = false;
this->effects.insert("can_see_target", true); this->effects.insert("can_see_target", true);
} }
@ -48,6 +49,7 @@ goap::State *FindTarget::get_apply_state(goap::ActorWorldState *context) const {
GetInMeleeRange::GetInMeleeRange() GetInMeleeRange::GetInMeleeRange()
: Action() { : Action() {
this->require_state_complete = false;
this->effects.insert("is_in_melee_range", true); this->effects.insert("is_in_melee_range", true);
this->required.insert("can_see_target", true); this->required.insert("can_see_target", true);
} }
@ -71,3 +73,14 @@ goap::State *MeleeAttack::get_apply_state(goap::ActorWorldState *context) const
state->animation = "melee_attack"; state->animation = "melee_attack";
return state; return state;
} }
TankSelfHeal::TankSelfHeal()
: Action() {
this->effects.insert("is_health_safe", true);
}
goap::State *TankSelfHeal::get_apply_state(goap::ActorWorldState *context) const {
Animate *state{this->create_state<Animate>()};
state->animation = "self_heal";
return state;
}

View file

@ -39,4 +39,11 @@ public:
virtual goap::State *get_apply_state(goap::ActorWorldState *context) const override; virtual goap::State *get_apply_state(goap::ActorWorldState *context) const override;
}; };
class TankSelfHeal : public goap::Action {
GOAP_ACTION(SelfHeal);
public:
TankSelfHeal();
virtual goap::State *get_apply_state(goap::ActorWorldState *context) const override;
};
#endif // !RTS_ACTIONS_HPP #endif // !RTS_ACTIONS_HPP

View file

@ -1,5 +1,8 @@
#include "rts_states.hpp" #include "rts_states.hpp"
#include "utils/util_functions.hpp"
#include <godot_cpp/core/math.hpp>
#include <godot_cpp/variant/utility_functions.hpp> #include <godot_cpp/variant/utility_functions.hpp>
#include <cmath>
void MoveTo::_bind_methods() {} void MoveTo::_bind_methods() {}
@ -7,6 +10,7 @@ void MoveTo::_ready() {
this->agent = this->get_node<gd::NavigationAgent3D>("../NavigationAgent3D"); this->agent = this->get_node<gd::NavigationAgent3D>("../NavigationAgent3D");
this->agent->set_target_position(this->target_node->get_global_position()); this->agent->set_target_position(this->target_node->get_global_position());
this->parent_node3d = Object::cast_to<gd::Node3D>(this->get_parent()); this->parent_node3d = Object::cast_to<gd::Node3D>(this->get_parent());
this->calculate_path();
} }
void MoveTo::_end_state() { void MoveTo::_end_state() {
@ -20,10 +24,38 @@ void MoveTo::_process(double delta_time) {
this->parent_node3d->set_global_position(pos + direction * delta_time); this->parent_node3d->set_global_position(pos + direction * delta_time);
this->parent_node3d->look_at(pos - gd::Vector3{direction.x, 0.f, direction.z}); this->parent_node3d->look_at(pos - gd::Vector3{direction.x, 0.f, direction.z});
if(this->is_action_done()) bool const navigation_finished{this->agent->is_navigation_finished()};
this->state_finished(); if(this->is_action_done_interrupt() || navigation_finished)
else if(this->agent->is_navigation_finished()) this->state_ended();
this->state_failed();
if((utils::time_seconds() - this->last_repath) > this->get_repath_interval())
this->calculate_path();
}
void MoveTo::calculate_path() {
gd::Vector3 const target_pos{this->target_node->get_global_position()};
this->agent->set_target_position(target_pos);
this->last_repath = utils::time_seconds();
this->target_position_at_last = target_pos;
}
float MoveTo::target_delta_position() const {
return this->target_position_at_last
.distance_to(this->target_node->get_global_position());
}
float MoveTo::distance_to_target() const {
return this->target_position_at_last
.distance_to(this->parent_node3d->get_global_position());
}
double MoveTo::get_repath_interval() const {
float const target_delta_position{this->target_delta_position()};
if(target_delta_position == 0.f)
return INFINITY;
else
return gd::Math::max(gd::Math::min(double(this->distance_to_target()), 5.0)
- target_delta_position, 0.2);
} }
void Activate::_bind_methods() {} void Activate::_bind_methods() {}
@ -32,18 +64,15 @@ void Animate::_bind_methods() {}
void Animate::_ready() { void Animate::_ready() {
this->anim = this->get_node<gd::AnimationPlayer>("../AnimationPlayer"); this->anim = this->get_node<gd::AnimationPlayer>("../AnimationPlayer");
this->anim->queue(this->animation); this->anim->play(this->animation);
} }
void Animate::_process(double delta_time) { void Animate::_process(double delta_time) {
if(this->is_action_done()) { bool const animation_finished{!this->anim->is_playing() || this->anim->get_current_animation() != this->animation};
this->state_finished(); if(this->is_action_done_interrupt() || animation_finished)
} else if(!this->anim->is_playing() || this->anim->get_current_animation() != this->animation) { this->state_ended();
this->state_failed();
}
} }
void Animate::_end_state() { void Animate::_end_state() {
if(this->anim->get_current_animation() == this->animation)
this->anim->stop(); this->anim->stop();
} }

View file

@ -14,11 +14,17 @@ public:
virtual void _ready() override; virtual void _ready() override;
virtual void _end_state() override; virtual void _end_state() override;
virtual void _process(double delta_time) override; virtual void _process(double delta_time) override;
void calculate_path();
float target_delta_position() const;
float distance_to_target() const;
double get_repath_interval() const;
public: public:
gd::Node3D *target_node{nullptr}; gd::Node3D *target_node{nullptr};
private: private:
gd::Node3D *parent_node3d{nullptr}; gd::Node3D *parent_node3d{nullptr};
gd::NavigationAgent3D *agent{nullptr}; gd::NavigationAgent3D *agent{nullptr};
gd::Vector3 target_position_at_last{};
double last_repath{0.0};
}; };
class Activate : public goap::State { class Activate : public goap::State {

View file

@ -20,8 +20,8 @@ void Unit::_enter_tree() { GDGAMEONLY();
this->world_state = this->get_node<UnitWorldState>("%ActorWorldState"); this->world_state = this->get_node<UnitWorldState>("%ActorWorldState");
this->world_state->connect("attention_changed", callable_mp(this, &Unit::stop_plan)); this->world_state->connect("attention_changed", callable_mp(this, &Unit::stop_plan));
this->anim_player = this->get_node<gd::AnimationPlayer>("%AnimationPlayer"); this->anim_player = this->get_node<gd::AnimationPlayer>("%AnimationPlayer");
EntityHealth *health{this->get_node<EntityHealth>("%EntityHealth")}; this->health = this->get_node<EntityHealth>("%EntityHealth");
health->connect("death", callable_mp(this, &Unit::on_death)); this->health->connect("death", callable_mp(this, &Unit::on_death));
} }
void Unit::stop_plan() { void Unit::stop_plan() {
@ -34,9 +34,7 @@ void Unit::stop_plan() {
} }
void Unit::begin_marker_temporary(GoalMarker *marker) { void Unit::begin_marker_temporary(GoalMarker *marker) {
this->destroy_state();
this->world_state->set_target_node(marker); this->world_state->set_target_node(marker);
this->destroy_state();
this->set_goal_and_plan(marker->get_goal()); this->set_goal_and_plan(marker->get_goal());
// destroy temporary marker if goal is already achieved or failed // destroy temporary marker if goal is already achieved or failed
// connect observers if a plan was formed // connect observers if a plan was formed
@ -80,6 +78,7 @@ void Unit::aim_at(gd::Node3D *target) {
} }
void Unit::on_death(Unit *damage_source) { void Unit::on_death(Unit *damage_source) {
this->destroy_state();
this->anim_player->stop(); this->anim_player->stop();
this->anim_player->play("death"); this->anim_player->play("death");
} }
@ -122,11 +121,16 @@ void Unit::state_finished() {
} }
void Unit::next_action() { void Unit::next_action() {
// cannot perform actions while dead
if(this->health->get_injury_current() <= 0)
return;
// destroy active state
if(this->state != nullptr && !this->state->is_queued_for_deletion()) if(this->state != nullptr && !this->state->is_queued_for_deletion())
this->destroy_state(); this->destroy_state();
this->state = nullptr; this->state = nullptr;
if(this->current_plan.is_empty()) if(this->current_plan.is_empty())
return; return;
// pop next action and apply state
this->state = this->current_plan.get(0)->get_apply_state(this->world_state); this->state = this->current_plan.get(0)->get_apply_state(this->world_state);
if(state == nullptr) { if(state == nullptr) {
this->stop_plan(); this->stop_plan();
@ -135,7 +139,6 @@ void Unit::next_action() {
} }
this->current_plan.remove_at(0); this->current_plan.remove_at(0);
this->add_child(this->state); this->add_child(this->state);
this->state->connect("state_finished", this->on_state_finished); this->state->connect("state_finished", this->on_state_finished);
this->state->connect("state_failed", this->on_plan_failed); this->state->connect("state_failed", this->on_plan_failed);
} }

View file

@ -60,6 +60,7 @@ protected:
gd::NavigationAgent3D *agent{nullptr}; gd::NavigationAgent3D *agent{nullptr};
gd::AnimationPlayer *anim_player{nullptr}; gd::AnimationPlayer *anim_player{nullptr};
goap::Planner *planner{nullptr}; goap::Planner *planner{nullptr};
EntityHealth *health{nullptr};
UnitWorldState *world_state{nullptr}; UnitWorldState *world_state{nullptr};
}; };

View file

@ -17,12 +17,14 @@ void UnitWorldState::_bind_methods() {
GDFUNCTION(get_target_node); GDFUNCTION(get_target_node);
GDFUNCTION(get_is_target_enemy); GDFUNCTION(get_is_target_enemy);
GDFUNCTION(get_is_in_melee_range); GDFUNCTION(get_is_in_melee_range);
GDFUNCTION(get_is_health_safe);
} }
void UnitWorldState::_enter_tree() { GDGAMEONLY(); void UnitWorldState::_enter_tree() { GDGAMEONLY();
this->parent_unit = gd::Object::cast_to<Unit>(this->get_parent()); this->parent_unit = gd::Object::cast_to<Unit>(this->get_parent());
this->agent = this->get_node<gd::NavigationAgent3D>("%NavigationAgent3D"); this->agent = this->get_node<gd::NavigationAgent3D>("%NavigationAgent3D");
this->eye_location = this->get_node<gd::Node3D>("%Eyes"); this->eye_location = this->get_node<gd::Node3D>("%Eyes");
this->health = this->get_node<EntityHealth>("%EntityHealth");
if(this->parent_unit == nullptr) if(this->parent_unit == nullptr)
gd::UtilityFunctions::push_warning("UnitWorldState needs to be a child node of a Unit"); gd::UtilityFunctions::push_warning("UnitWorldState needs to be a child node of a Unit");
} }
@ -80,6 +82,10 @@ bool UnitWorldState::get_is_in_melee_range() const {
.distance_squared_to(this->parent_unit->get_global_position()) <= 4.f; .distance_squared_to(this->parent_unit->get_global_position()) <= 4.f;
} }
bool UnitWorldState::get_is_health_safe() const {
return float(this->health->get_injury_current()) > (float(this->health->get_injury_max()) / 2.f);
}
void UnitWorldState::set_target_node(gd::Node3D *node) { void UnitWorldState::set_target_node(gd::Node3D *node) {
if(this->target_node != nullptr) if(this->target_node != nullptr)
this->target_node->disconnect("tree_exited", this->target_node_exited_tree.bind(this->target_node)); this->target_node->disconnect("tree_exited", this->target_node_exited_tree.bind(this->target_node));

View file

@ -1,6 +1,7 @@
#ifndef UNIT_WORLD_STATE_HPP #ifndef UNIT_WORLD_STATE_HPP
#define UNIT_WORLD_STATE_HPP #define UNIT_WORLD_STATE_HPP
#include "entity_health.hpp"
#include "goap/actor_world_state.hpp" #include "goap/actor_world_state.hpp"
#include <godot_cpp/classes/navigation_agent3d.hpp> #include <godot_cpp/classes/navigation_agent3d.hpp>
#include <godot_cpp/classes/node3d.hpp> #include <godot_cpp/classes/node3d.hpp>
@ -18,6 +19,7 @@ public:
bool get_is_target_unit() const; bool get_is_target_unit() const;
bool get_is_target_enemy() const; bool get_is_target_enemy() const;
bool get_is_in_melee_range() const; bool get_is_in_melee_range() const;
bool get_is_health_safe() const;
void set_target_node(gd::Node3D *node); void set_target_node(gd::Node3D *node);
gd::Node3D *get_target_node() const; gd::Node3D *get_target_node() const;
@ -27,6 +29,7 @@ private:
gd::Callable const target_node_exited_tree{callable_mp(this, &UnitWorldState::target_destroyed)}; gd::Callable const target_node_exited_tree{callable_mp(this, &UnitWorldState::target_destroyed)};
protected: protected:
Unit *parent_unit{nullptr}; Unit *parent_unit{nullptr};
EntityHealth *health{nullptr};
gd::NavigationAgent3D *agent{nullptr}; gd::NavigationAgent3D *agent{nullptr};
gd::Node3D *target_node{nullptr}; gd::Node3D *target_node{nullptr};
gd::Node3D *eye_location{nullptr}; gd::Node3D *eye_location{nullptr};