diff --git a/godot/AI/maintain_health.tres b/godot/AI/maintain_health.tres new file mode 100644 index 0000000..6126e85 --- /dev/null +++ b/godot/AI/maintain_health.tres @@ -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 +} diff --git a/godot/Animation/bean_characters.res b/godot/Animation/bean_characters.res index 2b007e8..1224374 100644 Binary files a/godot/Animation/bean_characters.res and b/godot/Animation/bean_characters.res differ diff --git a/godot/GameObjects/enemy_unit.tscn b/godot/GameObjects/enemy_unit.tscn index d25cf9f..f408f0d 100644 --- a/godot/GameObjects/enemy_unit.tscn +++ b/godot/GameObjects/enemy_unit.tscn @@ -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://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"] [sub_resource type="SphereShape3D" id="SphereShape3D_5pqvg"] @@ -24,11 +25,11 @@ collision_layer = 6 collision_mask = 0 [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 [node name="Planner" type="Planner" parent="."] -actions_inspector = [3, 2, 4] +actions_inspector = [3, 2, 4, 5] unique_name_in_owner = true [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) [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") [node name="MeshInstance3D" type="MeshInstance3D" parent="."] diff --git a/src/enemy_world_state.cpp b/src/enemy_world_state.cpp index 1bd3f45..bd2d296 100644 --- a/src/enemy_world_state.cpp +++ b/src/enemy_world_state.cpp @@ -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_exited", callable_mp(this, &EnemyWorldState::on_awareness_exited)); this->health = this->get_node("%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) { - gd::UtilityFunctions::print("1) object entered awareness"); Unit *unit{gd::Object::cast_to(node)}; if(unit == nullptr) return; if(unit == this->parent_unit) return; - gd::UtilityFunctions::print("2) object was Unit"); this->known_enemies.push_back(unit); this->try_set_target(this->select_target_from_known()); } @@ -82,17 +79,15 @@ gd::Ref EnemyWorldState::get_goal_for_target(Unit *unit) { } void EnemyWorldState::try_set_target(Unit *unit) { - gd::UtilityFunctions::print("3) selecting goal"); gd::Ref goal{this->get_goal_for_target(unit)}; - if(!goal.is_valid()) return; - gd::UtilityFunctions::print("4) selected goal ", goal->get_path()); - this->parent_unit->set_target_goal(unit, goal); + if(goal.is_valid()) + this->parent_unit->set_target_goal(unit, goal); } void EnemyWorldState::set_editor_available_goals(gd::Array array) { this->available_goals.clear(); while(!array.is_empty()) { - gd::Ref goal{array.pop_back()}; + gd::Ref goal{array.pop_front()}; this->available_goals.push_back(goal); } } diff --git a/src/entity_health.cpp b/src/entity_health.cpp index a88e240..ef289ac 100644 --- a/src/entity_health.cpp +++ b/src/entity_health.cpp @@ -4,6 +4,9 @@ void EntityHealth::_bind_methods() { #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(wounds_max, gd::Variant::INT); @@ -27,8 +30,13 @@ void EntityHealth::_enter_tree() { 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) { amount = gd::Math::abs(amount); + if(this->injury_current <= 0) return; // do not take damage when already dead this->injury_current -= amount; this->emit_signal("damage", this->injury_current, amount, source); this->emit_signal("health_changed", this->injury_current, -amount); diff --git a/src/goap/action.cpp b/src/goap/action.cpp index b9be10a..2f5c77e 100644 --- a/src/goap/action.cpp +++ b/src/goap/action.cpp @@ -29,6 +29,10 @@ ActionID Action::get_id() const { return this->id; } +bool Action::get_require_state_complete() const { + return this->require_state_complete; +} + bool Action::procedural_is_possible(ActorWorldState *context) const { return true; } diff --git a/src/goap/action.hpp b/src/goap/action.hpp index 944775c..36f3664 100644 --- a/src/goap/action.hpp +++ b/src/goap/action.hpp @@ -43,6 +43,7 @@ public: WorldState const &get_effects() const; ActionID get_id() const; + bool get_require_state_complete() const; protected: Action() = default; template @@ -54,6 +55,7 @@ protected: WorldState required{}; WorldState effects{}; bool only_proc_is_completed{false}; + bool require_state_complete{true}; private: ActionID id{-1}; }; diff --git a/src/goap/state.cpp b/src/goap/state.cpp index 1c2b90d..2ee25f0 100644 --- a/src/goap/state.cpp +++ b/src/goap/state.cpp @@ -16,8 +16,8 @@ void State::_enter_tree() { } void State::_process(double delta_time) { - if(this->is_action_done()) - this->state_finished(); + if(this->is_action_done() && this->get_action()->get_require_state_complete()) + this->state_ended(); } Action const *State::get_action() const { @@ -26,20 +26,19 @@ Action const *State::get_action() const { void State::_end_state() {} -void State::state_finished() { +void State::state_ended() { this->end_state(); - this->emit_signal("state_finished"); -} - -void State::state_failed() { - this->end_state(); - this->emit_signal("state_failed"); + this->emit_signal(this->is_action_done() ? "state_finished" : "state_failed"); } bool State::is_action_done() const { 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() { this->_end_state(); this->queue_free(); diff --git a/src/goap/state.hpp b/src/goap/state.hpp index 2f08f57..ab3764c 100644 --- a/src/goap/state.hpp +++ b/src/goap/state.hpp @@ -19,10 +19,12 @@ public: virtual void _process(double delta_time) override; protected: 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; + // \returns True if the Action's requirements are complete. Including Action::require_state_complete. + bool is_action_done_interrupt() const; virtual void _end_state(); - void state_finished(); - void state_failed(); + void state_ended(); private: void end_state(); private: diff --git a/src/register_types.cpp b/src/register_types.cpp index 28f6053..6a57c8c 100644 --- a/src/register_types.cpp +++ b/src/register_types.cpp @@ -35,6 +35,7 @@ void initialize_gdextension_types(ModuleInitializationLevel p_level) goap::ActionDB::register_action(); goap::ActionDB::register_action(); goap::ActionDB::register_action(); + goap::ActionDB::register_action(); ClassDB::register_class(); ClassDB::register_class(); diff --git a/src/rts_actions.cpp b/src/rts_actions.cpp index 38a953f..1a3a17b 100644 --- a/src/rts_actions.cpp +++ b/src/rts_actions.cpp @@ -36,6 +36,7 @@ goap::State *FireAtTarget::get_apply_state(goap::ActorWorldState *context) const FindTarget::FindTarget() : Action() { + this->require_state_complete = false; this->effects.insert("can_see_target", true); } @@ -48,6 +49,7 @@ goap::State *FindTarget::get_apply_state(goap::ActorWorldState *context) const { GetInMeleeRange::GetInMeleeRange() : Action() { + this->require_state_complete = false; this->effects.insert("is_in_melee_range", 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"; 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()}; + state->animation = "self_heal"; + return state; +} diff --git a/src/rts_actions.hpp b/src/rts_actions.hpp index 0a7119c..3e77448 100644 --- a/src/rts_actions.hpp +++ b/src/rts_actions.hpp @@ -39,4 +39,11 @@ public: 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 diff --git a/src/rts_states.cpp b/src/rts_states.cpp index 348e013..5ca4ceb 100644 --- a/src/rts_states.cpp +++ b/src/rts_states.cpp @@ -1,5 +1,8 @@ #include "rts_states.hpp" +#include "utils/util_functions.hpp" +#include #include +#include void MoveTo::_bind_methods() {} @@ -7,6 +10,7 @@ void MoveTo::_ready() { this->agent = this->get_node("../NavigationAgent3D"); this->agent->set_target_position(this->target_node->get_global_position()); this->parent_node3d = Object::cast_to(this->get_parent()); + this->calculate_path(); } 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->look_at(pos - gd::Vector3{direction.x, 0.f, direction.z}); - if(this->is_action_done()) - this->state_finished(); - else if(this->agent->is_navigation_finished()) - this->state_failed(); + bool const navigation_finished{this->agent->is_navigation_finished()}; + if(this->is_action_done_interrupt() || navigation_finished) + this->state_ended(); + + 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() {} @@ -32,18 +64,15 @@ void Animate::_bind_methods() {} void Animate::_ready() { this->anim = this->get_node("../AnimationPlayer"); - this->anim->queue(this->animation); + this->anim->play(this->animation); } void Animate::_process(double delta_time) { - if(this->is_action_done()) { - this->state_finished(); - } else if(!this->anim->is_playing() || this->anim->get_current_animation() != this->animation) { - this->state_failed(); - } + bool const animation_finished{!this->anim->is_playing() || this->anim->get_current_animation() != this->animation}; + if(this->is_action_done_interrupt() || animation_finished) + this->state_ended(); } void Animate::_end_state() { - if(this->anim->get_current_animation() == this->animation) - this->anim->stop(); + this->anim->stop(); } diff --git a/src/rts_states.hpp b/src/rts_states.hpp index e2cd532..de1f7f0 100644 --- a/src/rts_states.hpp +++ b/src/rts_states.hpp @@ -14,11 +14,17 @@ public: virtual void _ready() override; virtual void _end_state() 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: gd::Node3D *target_node{nullptr}; private: gd::Node3D *parent_node3d{nullptr}; gd::NavigationAgent3D *agent{nullptr}; + gd::Vector3 target_position_at_last{}; + double last_repath{0.0}; }; class Activate : public goap::State { diff --git a/src/unit.cpp b/src/unit.cpp index 66e991c..8e58a74 100644 --- a/src/unit.cpp +++ b/src/unit.cpp @@ -20,8 +20,8 @@ void Unit::_enter_tree() { GDGAMEONLY(); this->world_state = this->get_node("%ActorWorldState"); this->world_state->connect("attention_changed", callable_mp(this, &Unit::stop_plan)); this->anim_player = this->get_node("%AnimationPlayer"); - EntityHealth *health{this->get_node("%EntityHealth")}; - health->connect("death", callable_mp(this, &Unit::on_death)); + this->health = this->get_node("%EntityHealth"); + this->health->connect("death", callable_mp(this, &Unit::on_death)); } void Unit::stop_plan() { @@ -34,9 +34,7 @@ void Unit::stop_plan() { } void Unit::begin_marker_temporary(GoalMarker *marker) { - this->destroy_state(); this->world_state->set_target_node(marker); - this->destroy_state(); this->set_goal_and_plan(marker->get_goal()); // destroy temporary marker if goal is already achieved or failed // 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) { + this->destroy_state(); this->anim_player->stop(); this->anim_player->play("death"); } @@ -122,11 +121,16 @@ void Unit::state_finished() { } 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()) this->destroy_state(); this->state = nullptr; if(this->current_plan.is_empty()) return; + // pop next action and apply state this->state = this->current_plan.get(0)->get_apply_state(this->world_state); if(state == nullptr) { this->stop_plan(); @@ -135,7 +139,6 @@ void Unit::next_action() { } this->current_plan.remove_at(0); this->add_child(this->state); - this->state->connect("state_finished", this->on_state_finished); this->state->connect("state_failed", this->on_plan_failed); } diff --git a/src/unit.hpp b/src/unit.hpp index bcad7c0..400aea3 100644 --- a/src/unit.hpp +++ b/src/unit.hpp @@ -60,6 +60,7 @@ protected: gd::NavigationAgent3D *agent{nullptr}; gd::AnimationPlayer *anim_player{nullptr}; goap::Planner *planner{nullptr}; + EntityHealth *health{nullptr}; UnitWorldState *world_state{nullptr}; }; diff --git a/src/unit_world_state.cpp b/src/unit_world_state.cpp index c910f66..d453a33 100644 --- a/src/unit_world_state.cpp +++ b/src/unit_world_state.cpp @@ -17,12 +17,14 @@ void UnitWorldState::_bind_methods() { GDFUNCTION(get_target_node); GDFUNCTION(get_is_target_enemy); GDFUNCTION(get_is_in_melee_range); + GDFUNCTION(get_is_health_safe); } void UnitWorldState::_enter_tree() { GDGAMEONLY(); this->parent_unit = gd::Object::cast_to(this->get_parent()); this->agent = this->get_node("%NavigationAgent3D"); this->eye_location = this->get_node("%Eyes"); + this->health = this->get_node("%EntityHealth"); if(this->parent_unit == nullptr) gd::UtilityFunctions::push_warning("UnitWorldState needs to be a child node of a Unit"); } @@ -77,7 +79,11 @@ bool UnitWorldState::get_is_target_enemy() const { bool UnitWorldState::get_is_in_melee_range() const { return this->target_node != nullptr && this->target_node->get_global_position() - .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) { diff --git a/src/unit_world_state.hpp b/src/unit_world_state.hpp index 4970565..bd3db95 100644 --- a/src/unit_world_state.hpp +++ b/src/unit_world_state.hpp @@ -1,6 +1,7 @@ #ifndef UNIT_WORLD_STATE_HPP #define UNIT_WORLD_STATE_HPP +#include "entity_health.hpp" #include "goap/actor_world_state.hpp" #include #include @@ -18,6 +19,7 @@ public: bool get_is_target_unit() const; bool get_is_target_enemy() const; bool get_is_in_melee_range() const; + bool get_is_health_safe() const; void set_target_node(gd::Node3D *node); gd::Node3D *get_target_node() const; @@ -27,6 +29,7 @@ private: gd::Callable const target_node_exited_tree{callable_mp(this, &UnitWorldState::target_destroyed)}; protected: Unit *parent_unit{nullptr}; + EntityHealth *health{nullptr}; gd::NavigationAgent3D *agent{nullptr}; gd::Node3D *target_node{nullptr}; gd::Node3D *eye_location{nullptr};