diff --git a/modules/wave_survival/enemies/enemy_wretched.cpp b/modules/wave_survival/enemies/enemy_wretched.cpp index 3e2023bd..c09f52ff 100644 --- a/modules/wave_survival/enemies/enemy_wretched.cpp +++ b/modules/wave_survival/enemies/enemy_wretched.cpp @@ -10,6 +10,8 @@ void EnemyWretched::_bind_methods() { void EnemyWretched::ready() { if (StateMachine * fsm{ cast_to(get_node(NodePath("%StateMachine"))) }) { fsm->add_state(memnew(WretchedPatrolState)); + fsm->add_state(memnew(WretchedChaseState)); + fsm->add_state(memnew(WretchedAttackState)); } } @@ -28,28 +30,69 @@ void EnemyWretched::_notification(int what) { void WretchedState::set_target(Node *node) { this->target = cast_to(node); + this->nav = this->target->get_nav(); + this->unit = this->target->get_unit(); } EnemyWretched *WretchedState::get_target() const { return this->target; } +NpcUnit *WretchedState::get_unit() const { + return this->unit; +} + +NavigationAgent3D *WretchedState::get_nav() const { + return this->nav; +} + void WretchedPatrolState::enter_state() { - this->nav = get_target()->get_nav(); this->path = get_target()->get_unit()->get_patrol_path(); - float const max_speed{ get_target()->get_unit()->get_patrol_speed() }; + float const max_speed{ get_unit()->get_patrol_speed() }; get_target()->set_movement_speed(max_speed); - this->nav->set_max_speed(max_speed); + get_nav()->set_max_speed(max_speed); this->path_point = this->path->get_closest_point(get_target()->get_global_position()); - this->nav->set_target_position(this->path->point_at(this->path_point)); + get_nav()->set_target_position(this->path->point_at(this->path_point)); } void WretchedPatrolState::process(double delta) { - if (this->nav->is_navigation_finished()) { + if (get_nav()->is_navigation_finished()) { this->path_point += 1; - this->nav->set_target_position(this->path->point_at(this->path_point)); + get_nav()->set_target_position(this->path->point_at(this->path_point)); } - Vector3 const direction{ get_target()->get_global_position().direction_to(this->nav->get_next_path_position()) }; + Vector3 const direction{ get_target()->get_global_position().direction_to(get_nav()->get_next_path_position()) }; get_target()->set_movement_direction(Vector2{ direction.x, direction.z }.normalized()); } + +String WretchedPatrolState::get_next_state() const { + if (get_target()->get_unit()->is_aware_of_player()) { + return WretchedChaseState::get_class_static(); + } + return get_class(); +} + +void WretchedChaseState::enter_state() { + // TODO: replace this with a setting somewhere + get_target()->set_movement_speed(get_unit()->get_patrol_speed() * 2.f); + get_nav()->set_max_speed(get_unit()->get_patrol_speed() * 2.f); + get_nav()->set_target_position(PlayerBody::get_singleton()->get_global_position()); +} + +void WretchedChaseState::process(double delta) { + // TODO: optimize this with some checks to avoid re-pathing every frame + get_nav()->set_target_position(PlayerBody::get_singleton()->get_global_position()); + Vector3 const direction{ get_target()->get_global_position().direction_to(get_nav()->get_next_path_position()) }; + get_target()->set_movement_direction(Vector2{ direction.x, direction.z }.normalized()); +} + +String WretchedChaseState::get_next_state() const { + if (get_target()->get_global_position().distance_to(PlayerBody::get_singleton()->get_global_position()) < 2.f) { + return WretchedAttackState::get_class_static(); + } + return get_class(); +} + +String WretchedAttackState::get_next_state() const { + return WretchedChaseState::get_class_static(); +} diff --git a/modules/wave_survival/enemies/enemy_wretched.h b/modules/wave_survival/enemies/enemy_wretched.h index 8384bd33..9db4e2b4 100644 --- a/modules/wave_survival/enemies/enemy_wretched.h +++ b/modules/wave_survival/enemies/enemy_wretched.h @@ -4,6 +4,7 @@ #include "wave_survival/enemy_body.h" #include "wave_survival/state.h" class PatrolPath; +class NpcUnit; class EnemyWretched : public EnemyBody { GDCLASS(EnemyWretched, EnemyBody); @@ -23,9 +24,13 @@ class WretchedState : public State { public: virtual void set_target(Node *node) override; EnemyWretched *get_target() const; + NpcUnit *get_unit() const; + NavigationAgent3D *get_nav() const; private: + NpcUnit *unit{ nullptr }; EnemyWretched *target{ nullptr }; + NavigationAgent3D *nav{ nullptr }; }; class WretchedPatrolState : public WretchedState { @@ -35,11 +40,29 @@ class WretchedPatrolState : public WretchedState { public: virtual void enter_state() override; virtual void process(double delta) override; + virtual String get_next_state() const override; private: - NavigationAgent3D *nav{ nullptr }; int path_point{ 0 }; PatrolPath *path{ nullptr }; }; +class WretchedChaseState : public WretchedState { + GDCLASS(WretchedChaseState, WretchedState); + static void _bind_methods() {} + +public: + virtual void enter_state() override; + virtual void process(double delta) override; + virtual String get_next_state() const override; +}; + +class WretchedAttackState : public WretchedState { + GDCLASS(WretchedAttackState, WretchedState); + static void _bind_methods() {} + +public: + virtual String get_next_state() const override; +}; + #endif // !ENEMY_WRETCHED_H diff --git a/modules/wave_survival/npc_unit.cpp b/modules/wave_survival/npc_unit.cpp index 8a64b6eb..00fcad3b 100644 --- a/modules/wave_survival/npc_unit.cpp +++ b/modules/wave_survival/npc_unit.cpp @@ -2,15 +2,24 @@ #include "enemy_body.h" #include "macros.h" #include "patrol_path.h" +#include "player_detector.h" void NpcUnit::_bind_methods() { BIND_HPROPERTY(Variant::OBJECT, patrol_path, PROPERTY_HINT_NODE_TYPE, "PatrolPath"); } +void NpcUnit::on_npc_awareness_changed(bool value) { + if (value) { + this->aware_of_player = true; + } +} + void NpcUnit::child_added(Node *node) { if (EnemyBody * npc{ cast_to(node) }) { this->npcs.push_back(npc); npc->set_unit(this); + PlayerDetector *detector{ cast_to(npc->get_node(NodePath("%PlayerDetector"))) }; + detector->connect(PlayerDetector::sig_awareness_changed, callable_mp(this, &self_type::on_npc_awareness_changed)); } } @@ -40,7 +49,7 @@ PatrolPath *NpcUnit::get_patrol_path() const { } bool NpcUnit::is_aware_of_player() const { - return false; + return this->aware_of_player; } void NpcUnit::set_patrol_speed(float speed) { diff --git a/modules/wave_survival/npc_unit.h b/modules/wave_survival/npc_unit.h index 573b5ad8..0737617c 100644 --- a/modules/wave_survival/npc_unit.h +++ b/modules/wave_survival/npc_unit.h @@ -9,6 +9,7 @@ class PatrolPath; class NpcUnit : public Node { GDCLASS(NpcUnit, Node); static void _bind_methods(); + void on_npc_awareness_changed(bool seen); void child_added(Node *node); void enter_tree(); @@ -23,6 +24,7 @@ public: float get_patrol_speed() const; private: + bool aware_of_player{ false }; Vector npcs{}; PatrolPath *patrol_path{ nullptr }; float patrol_speed{ 3.f }; diff --git a/modules/wave_survival/player_detector.cpp b/modules/wave_survival/player_detector.cpp new file mode 100644 index 00000000..b62d807d --- /dev/null +++ b/modules/wave_survival/player_detector.cpp @@ -0,0 +1,74 @@ +#include "player_detector.h" + +String PlayerDetector::sig_awareness_changed{ "awareness_changed" }; + +void PlayerDetector::_bind_methods() { + ADD_SIGNAL(MethodInfo(sig_awareness_changed, PropertyInfo(Variant::BOOL, "aware"))); +} + +// Check if the player is in a bounded area in front of the detector and unobscured. +// As all tests are required to pass, we do them in increasing order of complexity, to minimize unneeded resource use. +bool PlayerDetector::check() const { + Vector3 const forward{ get_global_basis().get_column(2) }; + Vector3 const position{ get_global_position() }; + Vector3 const target{ PlayerBody::get_singleton()->get_global_position() + Vector3{ 0.f, 1.5f, 0.f } }; + // check if the target is in a view cone + if (forward.dot(target - position) < this->min_dot) { + return false; + } + // check if the target is in range + if (position.distance_squared_to(target) > this->max_distance * this->max_distance) { + return false; + } + // check if the target is obscured + PhysicsDirectSpaceState3D::RayParameters params{ this->ray_params }; + params.from = position; + params.to = target; + PhysicsDirectSpaceState3D *space{ get_world_3d()->get_direct_space_state() }; + PhysicsDirectSpaceState3D::RayResult result{}; + space->intersect_ray(params, result); + return result.collider == nullptr; +} + +void PlayerDetector::ready() { + this->ray_params.exclude.insert(PlayerBody::get_singleton()->get_rid()); +} + +void PlayerDetector::process(double delta) { + if (this->query_timer > 0.0) { + this->query_timer -= delta; + } else { + this->query_timer = this->query_time; + bool const new_awareness{ check() }; + if (new_awareness != this->aware_of_player) { + set_aware_of_player(new_awareness); + } + } +} + +void PlayerDetector::set_aware_of_player(bool value) { + print_line(vformat("awareness changed to %s", value)); + emit_signal(sig_awareness_changed, value); + this->aware_of_player = value; +} + +void PlayerDetector::_notification(int what) { + if (Engine::get_singleton()->is_editor_hint()) { + return; + } + switch (what) { + default: + return; + case NOTIFICATION_READY: + set_process(true); + ready(); + return; + case NOTIFICATION_PROCESS: + process(get_process_delta_time()); + return; + } +} + +bool PlayerDetector::is_aware_of_player() const { + return this->aware_of_player; +} diff --git a/modules/wave_survival/player_detector.h b/modules/wave_survival/player_detector.h new file mode 100644 index 00000000..839e5b87 --- /dev/null +++ b/modules/wave_survival/player_detector.h @@ -0,0 +1,33 @@ +#ifndef PLAYER_DETECTOR_H +#define PLAYER_DETECTOR_H + +#include "player_body.h" +#include "scene/3d/node_3d.h" + +class PlayerDetector : public Node3D { + GDCLASS(PlayerDetector, Node3D); + static void _bind_methods(); + bool check() const; + void ready(); + void process(double delta); + void set_aware_of_player(bool value); + +protected: + void _notification(int what); + +public: + bool is_aware_of_player() const; + +private: + bool aware_of_player{ false }; + float max_distance{ 100.f }; + float min_dot{ 0.1f }; + double query_time{ 0.3 }; + double query_timer{ 0.0 }; + PhysicsDirectSpaceState3D::RayParameters ray_params{}; + +public: + static String sig_awareness_changed; +}; + +#endif // !PLAYER_DETECTOR_H diff --git a/modules/wave_survival/register_types.cpp b/modules/wave_survival/register_types.cpp index 4ce98e40..1f2c5f29 100644 --- a/modules/wave_survival/register_types.cpp +++ b/modules/wave_survival/register_types.cpp @@ -9,6 +9,7 @@ #include "wave_survival/patrol_path.h" #include "wave_survival/player_body.h" #include "wave_survival/player_camera.h" +#include "wave_survival/player_detector.h" #include "wave_survival/player_input.h" #include "wave_survival/state.h" #include "wave_survival/state_machine.h" @@ -34,6 +35,10 @@ void initialize_wave_survival_module(ModuleInitializationLevel p_level) { GDREGISTER_CLASS(PatrolPath); GDREGISTER_CLASS(NpcUnit); GDREGISTER_CLASS(EnemyWretched); + GDREGISTER_CLASS(WretchedPatrolState); + GDREGISTER_CLASS(WretchedChaseState); + GDREGISTER_CLASS(WretchedAttackState); + GDREGISTER_CLASS(PlayerDetector); } void uninitialize_wave_survival_module(ModuleInitializationLevel p_level) { diff --git a/modules/wave_survival/state_machine.cpp b/modules/wave_survival/state_machine.cpp index 3ec6ef26..7566ce54 100644 --- a/modules/wave_survival/state_machine.cpp +++ b/modules/wave_survival/state_machine.cpp @@ -15,15 +15,12 @@ void StateMachine::switch_to_state(State *state) { } } -void StateMachine::ready() { -} - void StateMachine::process(double delta) { if (this->current_state) { this->current_state->process(delta); String new_state{ this->current_state->get_next_state() }; if (new_state != this->current_state->get_class()) { - this->switch_to_state(this->states[new_state]); + this->switch_to_state(this->states.has(new_state) ? this->states[new_state] : nullptr); } } } @@ -37,7 +34,6 @@ void StateMachine::_notification(int what) { return; case NOTIFICATION_READY: set_process(true); - ready(); return; case NOTIFICATION_PROCESS: process(get_process_delta_time()); @@ -47,7 +43,9 @@ void StateMachine::_notification(int what) { StateMachine::~StateMachine() { for (KeyValue kvp : this->states) { - memdelete(kvp.value); + if (kvp.value != nullptr) { + memdelete(kvp.value); + } } } @@ -58,4 +56,8 @@ void StateMachine::add_state(State *state) { if (this->current_state == nullptr) { this->switch_to_state(state); } + print_line("states:"); + for (KeyValue kv : this->states) { + print_line(vformat("\t\t| %s %s", kv.key, kv.value)); + } } diff --git a/modules/wave_survival/state_machine.h b/modules/wave_survival/state_machine.h index 2a5ad611..fc2ce297 100644 --- a/modules/wave_survival/state_machine.h +++ b/modules/wave_survival/state_machine.h @@ -8,7 +8,6 @@ class StateMachine : public Node { GDCLASS(StateMachine, Node); static void _bind_methods(); void switch_to_state(State *state); - void ready(); void process(double delta); protected: diff --git a/project/maps/testmap.tscn b/project/maps/testmap.tscn index e1f1b57c..6ceac7fc 100644 --- a/project/maps/testmap.tscn +++ b/project/maps/testmap.tscn @@ -29,6 +29,8 @@ navigation_mesh = SubResource("NavigationMesh_7ng1a") [node name="CSGCombiner3D" type="CSGCombiner3D" parent="NavigationRegion3D"] use_collision = true +collision_layer = 3 +collision_mask = 0 [node name="CSGBox3D2" type="CSGBox3D" parent="NavigationRegion3D/CSGCombiner3D"] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -7.424776, 3.1962457, -1.9109192) @@ -399,7 +401,16 @@ slide_on_ceiling = false patrol_path = NodePath("../PatrolPath") [node name="EnemyWretched" parent="NpcUnit" instance=ExtResource("3_7ng1a")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.017590523, 0.023196908, 3.073964) +transform = Transform3D(-1, 0, -8.742278e-08, 0, 1, 0, 8.742278e-08, 0, -1, 0.017590523, 0.023196908, 3.073964) + +[node name="EnemyWretched2" parent="NpcUnit" instance=ExtResource("3_7ng1a")] +transform = Transform3D(-1, 0, -8.742278e-08, 0, 1, 0, 8.742278e-08, 0, -1, -1.0904664, 0.023196908, 1.6584808) + +[node name="EnemyWretched3" parent="NpcUnit" instance=ExtResource("3_7ng1a")] +transform = Transform3D(-1, 0, -8.742278e-08, 0, 1, 0, 8.742278e-08, 0, -1, 1.1613213, 0.023196908, 1.513694) + +[node name="EnemyWretched4" parent="NpcUnit" instance=ExtResource("3_7ng1a")] +transform = Transform3D(-1, 0, -8.742278e-08, 0, 1, 0, 8.742278e-08, 0, -1, -0.977286, 0.023196908, 4.202239) [node name="PatrolPath" type="PatrolPath" parent="."] diff --git a/project/objects/enemies/enemy_wretched.tscn b/project/objects/enemies/enemy_wretched.tscn index 57565f91..6347b0fd 100644 --- a/project/objects/enemies/enemy_wretched.tscn +++ b/project/objects/enemies/enemy_wretched.tscn @@ -19,3 +19,7 @@ unique_name_in_owner = true unique_name_in_owner = true path_desired_distance = 0.25 debug_enabled = true + +[node name="PlayerDetector" type="PlayerDetector" parent="."] +unique_name_in_owner = true +transform = Transform3D(-1, 0, -8.742278e-08, 0, 1, 0, 8.742278e-08, 0, -1, 0, 1.4599279, 0) diff --git a/project/project.godot b/project/project.godot index d1bb13f3..168cee50 100644 --- a/project/project.godot +++ b/project/project.godot @@ -72,3 +72,5 @@ switch_weapon={ 3d_render/layer_1="Default" 3d_render/layer_2="FirstPerson" +3d_physics/layer_1="Movement" +3d_physics/layer_2="Hit"