fix: general AI behaviour fixes

This commit is contained in:
Sara 2024-08-05 22:56:55 +02:00
parent b87752c1a9
commit 973ba952c2
25 changed files with 155 additions and 114 deletions

1
.gitignore vendored
View file

@ -56,3 +56,4 @@ metro-rts.kdev4
# Caches
.cache/
godot/.gdb_history
builds

@ -1 +1 @@
Subproject commit 54136ee8357c5140a3775c54f08db5f7deda2058
Subproject commit 51c752c46b44769d3b6c661526c364a18ea64781

View file

@ -30,18 +30,23 @@ void EnemyWorldState::on_awareness_entered(gd::Node3D *node) {
}
void EnemyWorldState::on_awareness_exited(gd::Node3D *node) {
Unit *unit{gd::Object::cast_to<Unit>(node)};
this->remove_aware_unit(unit);
if(Unit *unit{gd::Object::cast_to<Unit>(node)})
this->remove_aware_unit(unit);
}
void EnemyWorldState::on_damaged(int remaining, int delta, Unit *source) {
this->add_aware_unit(source);
void EnemyWorldState::on_damaged(EntityHealth *, int, Unit *source) {
if(source != nullptr && !this->known_enemies.has(source))
this->add_aware_unit(source);
else
this->select_and_set_target();
}
Unit *EnemyWorldState::select_target_from_known_with_priority(float *out_priority) {
Unit *out{nullptr};
*out_priority = -INFINITY;
for(Unit *unit : this->known_enemies) {
if(!this->get_can_see_node(unit))
continue;
float const priority{this->calculate_priority(unit)};
if(priority > *out_priority) {
out = unit;

View file

@ -19,7 +19,7 @@ public:
void on_awareness_entered(gd::Node3D *node);
void on_awareness_exited(gd::Node3D *node);
void on_damaged(int remaining, int delta, Unit *source);
void on_damaged(EntityHealth *, int, Unit *source);
/*! Select the target with the highest priority and return it.
* Assigns the priority of found target to out_priority.
* \param out_priority Assigned to highest found priority, cannot be nullptr.

View file

@ -4,52 +4,56 @@
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);
GDSIGNAL("damage",
gd::PropertyInfo(gd::Variant::INT, "remaining"),
gd::PropertyInfo(gd::Variant::INT, "change"),
gd::PropertyInfo(gd::Variant::OBJECT, "source", gd::PROPERTY_HINT_NODE_TYPE, "Unit"));
gd::PropertyInfo(gd::Variant::OBJECT, "health_entity", gd::PROPERTY_HINT_NODE_TYPE, "EntityHealth"),
gd::PropertyInfo(gd::Variant::INT, "change"),
gd::PropertyInfo(gd::Variant::OBJECT, "source", gd::PROPERTY_HINT_NODE_TYPE, "Unit"));
GDSIGNAL("heal",
gd::PropertyInfo(gd::Variant::INT, "remaining"),
gd::PropertyInfo(gd::Variant::INT, "change"),
gd::PropertyInfo(gd::Variant::OBJECT, "source", gd::PROPERTY_HINT_NODE_TYPE, "Unit"));
gd::PropertyInfo(gd::Variant::OBJECT, "health_entity", gd::PROPERTY_HINT_NODE_TYPE, "EntityHealth"),
gd::PropertyInfo(gd::Variant::INT, "change"),
gd::PropertyInfo(gd::Variant::OBJECT, "source", gd::PROPERTY_HINT_NODE_TYPE, "Unit"));
GDSIGNAL("health_changed",
gd::PropertyInfo(gd::Variant::INT, "remaining"),
gd::PropertyInfo(gd::Variant::INT, "change"));
gd::PropertyInfo(gd::Variant::OBJECT, "health_entity", gd::PROPERTY_HINT_NODE_TYPE, "EntityHealth"),
gd::PropertyInfo(gd::Variant::INT, "change"));
GDSIGNAL("death", gd::PropertyInfo(gd::Variant::OBJECT, "source", gd::PROPERTY_HINT_NODE_TYPE, "Unit"));
}
void EntityHealth::_enter_tree() {
this->injury_current = this->injury_max;
this->wounds_current = this->wounds_max;
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) {
// do not take damage when already dead
if(this->injury_current <= 0)
return;
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);
if(this->injury_current <= 0) {
this->wounds_current -= amount;
this->injury_current -= gd::Math::max(1, int(float(amount) * this->get_wounds_damage_factor()));
this->emit_signal("damage", this, amount, source);
this->emit_signal("health_changed", this, -amount);
if(this->injury_current <= 0)
this->emit_signal("death", source);
}
}
void EntityHealth::healed_by(int amount, Unit *source) {
// reviving takes a different action
if(this->injury_current <= 0)
return;
amount = gd::Math::abs(amount);
this->injury_current = gd::Math::min(this->injury_max, this->injury_current + amount);
this->emit_signal("heal", this->injury_current, amount, source);
this->emit_signal("health_changed", this->injury_current, amount);
this->emit_signal("heal", this, amount, source);
this->emit_signal("health_changed", this, amount);
}
float EntityHealth::get_wounds_damage_factor() const {
return float(this->injury_current) / float(this->injury_max);
}
bool EntityHealth::get_is_dead() const {

View file

@ -13,9 +13,9 @@ class EntityHealth : public gd::Node {
public:
virtual void _enter_tree() override;
void damaged_by(int amount, Unit *source);
void damage(int amount);
void healed_by(int amount, Unit *source);
void heal(int amount);
float get_wounds_damage_factor() const;
bool get_is_dead() const;
void set_injury_max(int max_health);
int get_injury_max() const;

View file

@ -17,7 +17,7 @@ class ActorWorldState : public gd::Node {
static void _bind_methods();
public:
bool check_property_match(WorldProperty const &property);
gd::Variant get_world_property(gd::String world_prop_name);
virtual gd::Variant get_world_property(gd::String world_prop_name);
};
}

View file

@ -1,6 +1,6 @@
#include "goal.hpp"
#include "utils/godot_macros.hpp"
#include "utils/util_functions.hpp"
#include "../utils/godot_macros.hpp"
#include "../utils/util_functions.hpp"
namespace goap {
void Goal::_bind_methods() {

View file

@ -1,6 +1,6 @@
#include "planner.hpp"
#include "action_db.hpp"
#include "utils/godot_macros.hpp"
#include "../utils/godot_macros.hpp"
#include <cstdint>
#include <godot_cpp/classes/engine.hpp>
#include <godot_cpp/classes/global_constants.hpp>
@ -192,14 +192,8 @@ bool Planner::does_action_contribute(Action const *action, WorldStateNode const
Plan Planner::unroll_plan(WorldStateNode const &start_node, NodeMap const &from) {
Plan plan{};
WorldStateNode node{start_node};
#ifdef DEBUG_ENABLED
gd::UtilityFunctions::print("plan (", this->get_path(), "):");
#endif
while(node.last_action != nullptr) {
plan.push_back(node.last_action);
#ifdef DEBUG_ENABLED
gd::UtilityFunctions::print(" (", plan.size(), ") ", node.last_action->get_class());
#endif
node = from.get(node);
}
return plan;

View file

@ -1,7 +1,7 @@
#include "state.hpp"
#include "goap/action.hpp"
#include "goap/actor_world_state.hpp"
#include "utils/godot_macros.hpp"
#include "../goap/action.hpp"
#include "../goap/actor_world_state.hpp"
#include "../utils/godot_macros.hpp"
namespace goap {
void State::_bind_methods() {
@ -15,9 +15,20 @@ void State::_enter_tree() {
this->world_state = this->get_node<ActorWorldState>("../ActorWorldState");
}
void State::_process(double delta_time) {
void State::_process(double) {
if(this->is_action_done() && this->get_action()->get_require_state_complete())
this->state_ended();
this->end_state();
}
void State::interrupt_state() {
this->_end_state();
this->queue_free();
}
void State::end_state() {
this->interrupt_state();
this->emit_signal(this->is_action_done() ? "state_finished" : "state_failed");
this->emit_signal("end_state");
}
Action const *State::get_action() const {
@ -26,11 +37,6 @@ Action const *State::get_action() const {
void State::_end_state() {}
void State::state_ended() {
this->end_state();
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);
}
@ -38,10 +44,4 @@ bool State::is_action_done() const {
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();
this->emit_signal("end_state");
}
}

View file

@ -1,7 +1,7 @@
#ifndef GOAP_STATE_HPP
#define GOAP_STATE_HPP
#include "goap/actor_world_state.hpp"
#include "actor_world_state.hpp"
#include <godot_cpp/classes/node.hpp>
#include <godot_cpp/classes/node3d.hpp>
@ -16,17 +16,16 @@ friend class Action;
static void _bind_methods();
public:
virtual void _enter_tree() override;
virtual void _process(double delta_time) override;
virtual void _process(double) override;
void interrupt_state();
void end_state();
protected:
Action const *get_action() const;
// \returns True if the Action's requirements are complete. Without evaluating Action::require_state_complete.
//! \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.
//! \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_ended();
private:
void end_state();
private:
ActorWorldState *world_state{nullptr};
Action const *action{nullptr};

View file

@ -12,7 +12,7 @@ void NavMarker::_bind_methods() {
GDPROPERTY_HINTED(marker_type, gd::Variant::INT, gd::PROPERTY_HINT_ENUM, MarkerType::get_property_hint());
}
void NavMarker::_process(double delta_time) {
void NavMarker::_process(double) {
#ifdef DEBUG_ENABLED
GDEDITORONLY()
if(gd::EditorInterface::get_singleton()->get_selection()->get_selected_nodes().has(this)) {

View file

@ -8,14 +8,15 @@ namespace gd = godot;
GDENUM(MarkerType,
Generic,
Cover
Cover,
HalfCover
);
class NavMarker : public gd::Marker3D {
GDCLASS(NavMarker, gd::Marker3D);
static void _bind_methods();
public:
virtual void _process(double delta_time) override;
virtual void _process(double) override;
void set_marker_type(int type);
int get_marker_type() const;

View file

@ -32,7 +32,7 @@ FireAtTarget::FireAtTarget()
this->effects.insert("is_target_dead", true);
}
goap::State *FireAtTarget::get_apply_state(goap::ActorWorldState *context) const {
goap::State *FireAtTarget::get_apply_state(goap::ActorWorldState *) const {
Animate *state{this->create_state<Animate>()};
state->animation = "fire_weapon";
return state;
@ -72,7 +72,7 @@ MeleeAttack::MeleeAttack()
this->effects.insert("is_target_dead", true);
}
goap::State *MeleeAttack::get_apply_state(goap::ActorWorldState *context) const {
goap::State *MeleeAttack::get_apply_state(goap::ActorWorldState *) const {
Animate *state{this->create_state<Animate>()};
state->animation = "melee_attack";
return state;
@ -84,7 +84,7 @@ TankSelfHeal::TankSelfHeal()
this->effects.insert("is_health_safe", true);
}
goap::State *TankSelfHeal::get_apply_state(goap::ActorWorldState *context) const {
goap::State *TankSelfHeal::get_apply_state(goap::ActorWorldState *) const {
Animate *state{this->create_state<Animate>()};
state->animation = "self_heal";
return state;

View file

@ -15,7 +15,7 @@ class FireAtTarget : public goap::Action {
GOAP_ACTION(FireAtTarget);
public:
FireAtTarget();
virtual goap::State *get_apply_state(goap::ActorWorldState *context) const override;
virtual goap::State *get_apply_state(goap::ActorWorldState *) const override;
};
class FindTarget : public goap::Action {
@ -36,14 +36,14 @@ class MeleeAttack : public goap::Action {
GOAP_ACTION(MeleeAttack);
public:
MeleeAttack();
virtual goap::State *get_apply_state(goap::ActorWorldState *context) const override;
virtual goap::State *get_apply_state(goap::ActorWorldState *) const override;
};
class TankSelfHeal : public goap::Action {
GOAP_ACTION(TankSelfHeal);
public:
TankSelfHeal();
virtual goap::State *get_apply_state(goap::ActorWorldState *context) const override;
virtual goap::State *get_apply_state(goap::ActorWorldState *) const override;
};
class TakeCover : public goap::Action {

View file

@ -1,5 +1,12 @@
#include "rts_game_mode.hpp"
#include "utils/godot_macros.hpp"
#include <godot_cpp/classes/engine.hpp>
#include <godot_cpp/variant/utility_functions.hpp>
void RTSGameMode::_bind_methods() {
#define CLASSNAME RTSGameMode
}
void RTSGameMode::_ready() { GDGAMEONLY();
godot::Engine::get_singleton()->set_max_fps(60);
}

View file

@ -7,7 +7,7 @@ class RTSGameMode : public utils::GameMode {
GDCLASS(RTSGameMode, utils::GameMode);
static void _bind_methods();
public:
private:
virtual void _ready() override;
};
#endif // !RTS_GAME_MODE_HPP

View file

@ -131,7 +131,7 @@ void RTSPlayer::select_unit(Unit *unit) {
unit->connect("tree_exited", this->on_selected_unit_destroyed);
}
void RTSPlayer::on_mouse_horizontal(gd::Ref<gd::InputEvent> event, float value) {
void RTSPlayer::on_mouse_horizontal(gd::Ref<gd::InputEvent>, float value) {
if(this->mmb_down)
this->camera_mouse_motion.x = value;
else if(this->rmb_down)
@ -139,39 +139,39 @@ void RTSPlayer::on_mouse_horizontal(gd::Ref<gd::InputEvent> event, float value)
this->total_cursor_motion.x = value;
}
void RTSPlayer::on_mouse_vertical(gd::Ref<gd::InputEvent> event, float value) {
void RTSPlayer::on_mouse_vertical(gd::Ref<gd::InputEvent>, float value) {
if(this->mmb_down)
this->camera_mouse_motion.y = value;
this->total_cursor_motion.y = value;
}
void RTSPlayer::on_direction_horizontal(gd::Ref<gd::InputEvent> event, float value) {
void RTSPlayer::on_direction_horizontal(gd::Ref<gd::InputEvent>, float value) {
this->camera_keys_motion.x = value;
}
void RTSPlayer::on_direction_vertical(gd::Ref<gd::InputEvent> event, float value) {
void RTSPlayer::on_direction_vertical(gd::Ref<gd::InputEvent>, float value) {
this->camera_keys_motion.y = value;
}
void RTSPlayer::on_rotate_horizontal(gd::Ref<gd::InputEvent> event, float value) {
void RTSPlayer::on_rotate_horizontal(gd::Ref<gd::InputEvent>, float value) {
this->camera_keys_rotation = value;
}
void RTSPlayer::on_rclick(gd::Ref<gd::InputEvent> event, float value) {
void RTSPlayer::on_rclick(gd::Ref<gd::InputEvent>, float value) {
if(value == 0.f && !this->rmb_is_dragged)
this->order_activate_object_under_cursor();
this->rmb_down = value != 0.f;
this->rmb_last_change = utils::time_seconds();
}
void RTSPlayer::on_lclick(gd::Ref<gd::InputEvent> event, float value) {
void RTSPlayer::on_lclick(gd::Ref<gd::InputEvent>, float value) {
if(value == 0.f && !this->lmb_is_dragged)
this->select_unit_under_cursor();
this->lmb_down = value != 0.f;
this->lmb_last_change = utils::time_seconds();
}
void RTSPlayer::on_mclick(gd::Ref<gd::InputEvent> event, float value) {
void RTSPlayer::on_mclick(gd::Ref<gd::InputEvent>, float value) {
this->mmb_down = value != 0.f;
}

View file

@ -42,14 +42,14 @@ private:
void select_unit(Unit *unit);
// input functions
void on_mouse_horizontal(gd::Ref<gd::InputEvent> event, float value);
void on_mouse_vertical(gd::Ref<gd::InputEvent> event, float value);
void on_direction_horizontal(gd::Ref<gd::InputEvent> event, float value);
void on_direction_vertical(gd::Ref<gd::InputEvent> event, float value);
void on_rotate_horizontal(gd::Ref<gd::InputEvent> event, float value);
void on_lclick(gd::Ref<gd::InputEvent> event, float value);
void on_rclick(gd::Ref<gd::InputEvent> event, float value);
void on_mclick(gd::Ref<gd::InputEvent> event, float value);
void on_mouse_horizontal(gd::Ref<gd::InputEvent>, float value);
void on_mouse_vertical(gd::Ref<gd::InputEvent>, float value);
void on_direction_horizontal(gd::Ref<gd::InputEvent>, float value);
void on_direction_vertical(gd::Ref<gd::InputEvent>, float value);
void on_rotate_horizontal(gd::Ref<gd::InputEvent>, float value);
void on_lclick(gd::Ref<gd::InputEvent>, float value);
void on_rclick(gd::Ref<gd::InputEvent>, float value);
void on_mclick(gd::Ref<gd::InputEvent>, float value);
// setters & getters
gd::Vector3 get_forward_direction() const;

View file

@ -10,13 +10,12 @@ void MoveTo::_ready() {
this->parent_unit = Object::cast_to<Unit>(this->get_parent());
this->agent = this->get_node<gd::NavigationAgent3D>("../NavigationAgent3D");
if(this->target_node == nullptr) {
this->state_ended();
this->end_state();
gd::UtilityFunctions::push_warning("failed to start MoveTo state due to missing target");
return;
}
this->agent->connect("velocity_computed", this->nav_velocity_computed);
this->calculate_path();
gd::UtilityFunctions::print(this->parent_unit->get_path(), " MoveTo ", this->target_node->get_path());
}
void MoveTo::_end_state() {
@ -26,14 +25,14 @@ void MoveTo::_end_state() {
this->parent_unit->set_velocity({0.f, 0.f, 0.f});
}
void MoveTo::_process(double delta_time) {
void MoveTo::_process(double) {
gd::Vector3 const pos = this->parent_unit->get_global_position();
gd::Vector3 const target = this->agent->get_next_path_position();
gd::Vector3 const direction = (target - pos).normalized();
this->agent->set_velocity(direction);
bool const navigation_finished{this->agent->is_navigation_finished()};
if(this->is_action_done_interrupt() || navigation_finished)
this->state_ended();
this->end_state();
if((utils::time_seconds() - this->last_repath) > this->get_repath_interval())
this->calculate_path();
}
@ -78,13 +77,13 @@ void Animate::_ready() {
this->anim = this->get_node<gd::AnimationPlayer>("../AnimationPlayer");
this->anim->play(this->animation);
if(!this->anim->has_animation(this->animation))
this->state_ended();
this->end_state();
}
void Animate::_process(double delta_time) {
void Animate::_process(double) {
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();
this->end_state();
}
void Animate::_end_state() {

View file

@ -14,7 +14,7 @@ class MoveTo : public goap::State {
public:
virtual void _ready() override;
virtual void _end_state() override;
virtual void _process(double delta_time) override;
virtual void _process(double) override;
void calculate_path();
void on_velocity_computed(gd::Vector3 vel);
float target_delta_position() const;

View file

@ -28,7 +28,7 @@ void Unit::_enter_tree() { GDGAMEONLY();
this->health->connect("death", callable_mp(this, &Unit::on_death));
}
void Unit::_physics_process(double delta_time) {
void Unit::_physics_process(double) {
this->move_and_slide();
}
@ -131,7 +131,7 @@ void Unit::on_velocity_computed(gd::Vector3 vel) {
void Unit::destroy_state() {
if(this->state == nullptr || this->state->is_queued_for_deletion() || !this->state->is_inside_tree())
return;
this->state->queue_free();
this->state->interrupt_state();
this->state = nullptr;
}

View file

@ -26,7 +26,7 @@ class Unit : public gd::CharacterBody3D {
static void _bind_methods();
public:
virtual void _enter_tree() override;
virtual void _physics_process(double delta_time) override;
virtual void _physics_process(double) override;
void stop_plan();
void begin_marker_temporary(GoalMarker *marker);

View file

@ -2,6 +2,7 @@
#include "entity_health.hpp"
#include "unit.hpp"
#include "utils/godot_macros.hpp"
#include "utils/util_functions.hpp"
#include <godot_cpp/classes/physics_ray_query_parameters3d.hpp>
#include <godot_cpp/classes/world3d.hpp>
#include <godot_cpp/classes/physics_direct_space_state3d.hpp>
@ -30,25 +31,39 @@ void UnitWorldState::_enter_tree() { GDGAMEONLY();
this->health = this->get_node<EntityHealth>("%EntityHealth");
}
bool UnitWorldState::get_can_see_target() const {
gd::Transform3D const eyes{this->eye_location->get_global_transform()};
gd::Variant UnitWorldState::get_world_property(gd::String property) {
if(this->cache.has(property)) {
CachedWorldProperty prop{this->cache[property]};
if(prop.auto_invalidate_time > utils::time_seconds()) {
return prop.value;
}
}
return ActorWorldState::get_world_property(property);
}
bool UnitWorldState::get_can_see_target() {
bool value{this->get_can_see_node(this->target_node)};
this->cache_property("can_see_target", value, utils::time_seconds() + 0.5);
return value;
}
bool UnitWorldState::get_can_see_node(gd::Node3D *node) const {
// check if the target has a separate vision target, or is it's own vision target
gd::Node3D *vision_target{this->target_node->get_node<gd::Node3D>("VisionTarget")};
gd::Vector3 target_position{
vision_target
? vision_target->get_global_position()
: this->target_node->get_global_position()
};
gd::Node3D *vision_target{node->get_node<gd::Node3D>("VisionTarget")};
gd::Transform3D const eyes{this->eye_location->get_global_transform()};
gd::Vector3 const target_position{vision_target ? vision_target->get_global_position() : node->get_global_position()};
// construct list for ignored objects including target node and parent unit
gd::TypedArray<gd::RID> ignore_list{this->parent_unit->get_rid()};
if(gd::CollisionObject3D *target_collider{gd::Object::cast_to<gd::CollisionObject3D>(this->target_node)})
ignore_list.push_back(target_collider->get_rid());
// ignore target if it is a collision object that might obstruct it's own collision test
if(gd::CollisionObject3D *as_collision_object{gd::Object::cast_to<gd::CollisionObject3D>(node)})
ignore_list.push_back(as_collision_object->get_rid());
// construct raycast query from unit's eye node to vision target. Ignoring parent unit and target if applicable
gd::Ref<gd::PhysicsRayQueryParameters3D> const query{gd::PhysicsRayQueryParameters3D::create(
eyes.origin,
target_position,
0x1 | 0x4, ignore_list)
};
0x1 | 0x4, ignore_list
)};
return this->parent_unit->get_world_3d()->get_direct_space_state()->intersect_ray(query).is_empty();
}
@ -88,7 +103,7 @@ bool UnitWorldState::get_is_unit_enemy(Unit *unit) 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()) <= 2.f * 2.f;
}
bool UnitWorldState::get_is_health_safe() const {
@ -104,6 +119,7 @@ gd::Vector3 UnitWorldState::get_target_global_position() const {
}
void UnitWorldState::set_target_node(gd::Node3D *node) {
this->cache.clear();
if(this->target_node != nullptr)
this->target_node->disconnect("tree_exited", this->target_node_exited_tree.bind(this->target_node));
this->target_node = node;
@ -116,6 +132,10 @@ gd::Node3D *UnitWorldState::get_target_node() const {
return this->target_node;
}
void UnitWorldState::cache_property(gd::String property, gd::Variant value, double auto_invalidate_time) {
this->cache[property] = {.value=value, .auto_invalidate_time=auto_invalidate_time};
}
void UnitWorldState::target_destroyed(gd::Node3D *target) {
if(target == this->target_node)
this->set_target_node(nullptr);

View file

@ -6,14 +6,23 @@
#include <godot_cpp/classes/navigation_agent3d.hpp>
#include <godot_cpp/classes/node3d.hpp>
namespace gd = godot;
class Unit;
struct CachedWorldProperty {
gd::Variant value{};
double auto_invalidate_time{0.0};
};
class UnitWorldState : public goap::ActorWorldState {
GDCLASS(UnitWorldState, goap::ActorWorldState);
static void _bind_methods();
public:
virtual void _enter_tree() override;
bool get_can_see_target() const;
virtual gd::Variant get_world_property(gd::String property) override;
bool get_can_see_target();
bool get_can_see_node(gd::Node3D *node) const;
bool get_is_at_target() const;
bool get_has_target() const;
bool get_is_target_dead() const;
@ -28,6 +37,7 @@ public:
void set_target_node(gd::Node3D *node);
gd::Node3D *get_target_node() const;
private:
void cache_property(gd::String property, gd::Variant value, double auto_invalidate_time);
void target_destroyed(gd::Node3D *target);
private:
gd::Callable const target_node_exited_tree{callable_mp(this, &UnitWorldState::target_destroyed)};
@ -37,6 +47,7 @@ protected:
gd::NavigationAgent3D *agent{nullptr};
gd::Node3D *target_node{nullptr};
gd::Node3D *eye_location{nullptr};
gd::HashMap<gd::String, CachedWorldProperty> cache;
};
#endif // !UNIT_WORLD_STATE_HPP