feat: implemented heal action and goal
This commit is contained in:
parent
a78bbfd3b5
commit
f9b3c6eb3f
9
godot/AI/maintain_health.tres
Normal file
9
godot/AI/maintain_health.tres
Normal 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.
|
@ -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="."]
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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};
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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>();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
13
src/unit.cpp
13
src/unit.cpp
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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};
|
||||
};
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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};
|
||||
|
|
Loading…
Reference in a new issue