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://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="."]

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_exited", callable_mp(this, &EnemyWorldState::on_awareness_exited));
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) {
gd::UtilityFunctions::print("1) object entered awareness");
Unit *unit{gd::Object::cast_to<Unit>(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<goap::Goal> EnemyWorldState::get_goal_for_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)};
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<goap::Goal> goal{array.pop_back()};
gd::Ref<goap::Goal> goal{array.pop_front()};
this->available_goals.push_back(goal);
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,8 @@
#include "rts_states.hpp"
#include "utils/util_functions.hpp"
#include <godot_cpp/core/math.hpp>
#include <godot_cpp/variant/utility_functions.hpp>
#include <cmath>
void MoveTo::_bind_methods() {}
@ -7,6 +10,7 @@ void MoveTo::_ready() {
this->agent = this->get_node<gd::NavigationAgent3D>("../NavigationAgent3D");
this->agent->set_target_position(this->target_node->get_global_position());
this->parent_node3d = Object::cast_to<gd::Node3D>(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<gd::AnimationPlayer>("../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();
}

View file

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

View file

@ -20,8 +20,8 @@ void Unit::_enter_tree() { GDGAMEONLY();
this->world_state = this->get_node<UnitWorldState>("%ActorWorldState");
this->world_state->connect("attention_changed", callable_mp(this, &Unit::stop_plan));
this->anim_player = this->get_node<gd::AnimationPlayer>("%AnimationPlayer");
EntityHealth *health{this->get_node<EntityHealth>("%EntityHealth")};
health->connect("death", callable_mp(this, &Unit::on_death));
this->health = this->get_node<EntityHealth>("%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);
}

View file

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

View file

@ -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<Unit>(this->get_parent());
this->agent = this->get_node<gd::NavigationAgent3D>("%NavigationAgent3D");
this->eye_location = this->get_node<gd::Node3D>("%Eyes");
this->health = this->get_node<EntityHealth>("%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) {

View file

@ -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 <godot_cpp/classes/navigation_agent3d.hpp>
#include <godot_cpp/classes/node3d.hpp>
@ -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};