feat: added sound events for enemy awareness

This commit is contained in:
Sara 2025-08-04 17:00:23 +02:00
parent 1373656f90
commit 66aede32bd
15 changed files with 234 additions and 16 deletions

View file

@ -2,6 +2,7 @@
#include "health_status.h" #include "health_status.h"
#include "hitbox.h" #include "hitbox.h"
#include "macros.h" #include "macros.h"
#include "muzzle_effect.h"
#include "scene/resources/packed_scene.h" #include "scene/resources/packed_scene.h"
void HitscanMuzzle::_bind_methods() { void HitscanMuzzle::_bind_methods() {
@ -9,6 +10,7 @@ void HitscanMuzzle::_bind_methods() {
BIND_PROPERTY(Variant::FLOAT, spread); BIND_PROPERTY(Variant::FLOAT, spread);
BIND_PROPERTY(Variant::INT, damage); BIND_PROPERTY(Variant::INT, damage);
BIND_PROPERTY(Variant::INT, ray_count); BIND_PROPERTY(Variant::INT, ray_count);
BIND_HPROPERTY(Variant::OBJECT, muzzle_effect, PROPERTY_HINT_NODE_TYPE, "MuzzleEffect");
} }
void HitscanMuzzle::instantiate_impact_effect() { void HitscanMuzzle::instantiate_impact_effect() {
@ -45,6 +47,7 @@ void HitscanMuzzle::ready() {
} else { } else {
print_error("HitscanMuzzle::ready: impact effect is invalid"); print_error("HitscanMuzzle::ready: impact effect is invalid");
} }
set_enabled(false); set_enabled(false);
} }
@ -62,6 +65,9 @@ void HitscanMuzzle::_notification(int what) {
} }
void HitscanMuzzle::shoot() { void HitscanMuzzle::shoot() {
if (this->muzzle_effect != nullptr) {
this->muzzle_effect->GDVIRTUAL_CALL(trigger);
}
for (int i{ this->ray_count }; i > 0; --i) { for (int i{ this->ray_count }; i > 0; --i) {
set_transform(this->home_transform); set_transform(this->home_transform);
rotate_object_local(Vector3(0.f, 1.f, 0.f), Math::random(-Math::PI, Math::PI)); rotate_object_local(Vector3(0.f, 1.f, 0.f), Math::random(-Math::PI, Math::PI));
@ -95,3 +101,11 @@ void HitscanMuzzle::set_ray_count(int amount) {
int HitscanMuzzle::get_ray_count() const { int HitscanMuzzle::get_ray_count() const {
return this->ray_count; return this->ray_count;
} }
void HitscanMuzzle::set_muzzle_effect(MuzzleEffect *effect) {
this->muzzle_effect = effect;
}
MuzzleEffect *HitscanMuzzle::get_muzzle_effect() const {
return this->muzzle_effect;
}

View file

@ -2,6 +2,7 @@
#define HITSCAN_MUZZLE_H #define HITSCAN_MUZZLE_H
#include "scene/3d/physics/ray_cast_3d.h" #include "scene/3d/physics/ray_cast_3d.h"
class MuzzleEffect;
class HitscanMuzzle : public RayCast3D { class HitscanMuzzle : public RayCast3D {
GDCLASS(HitscanMuzzle, RayCast3D); GDCLASS(HitscanMuzzle, RayCast3D);
@ -21,11 +22,14 @@ public:
int get_damage() const; int get_damage() const;
void set_ray_count(int amount); void set_ray_count(int amount);
int get_ray_count() const; int get_ray_count() const;
void set_muzzle_effect(MuzzleEffect *effect);
MuzzleEffect *get_muzzle_effect() const;
private: private:
float spread{ 0.001f }; float spread{ 0.001f };
int damage{ 1 }; int damage{ 1 };
int ray_count{ 1 }; int ray_count{ 1 };
MuzzleEffect *muzzle_effect{ nullptr };
Transform3D home_transform{}; Transform3D home_transform{};
Ref<PackedScene> impact_effect{}; Ref<PackedScene> impact_effect{};

View file

@ -0,0 +1,5 @@
#include "muzzle_effect.h"
void MuzzleEffect::_bind_methods() {
GDVIRTUAL_BIND(trigger);
}

View file

@ -0,0 +1,14 @@
#ifndef MUZZLE_EFFECT_H
#define MUZZLE_EFFECT_H
#include "scene/3d/node_3d.h"
class MuzzleEffect : public Node3D {
GDCLASS(MuzzleEffect, Node3D);
static void _bind_methods();
public:
GDVIRTUAL0_REQUIRED(trigger);
};
#endif // !MUZZLE_EFFECT_H

View file

@ -1,4 +1,5 @@
#include "player_detector.h" #include "player_detector.h"
#include "sound_event_patchboard.h"
String PlayerDetector::sig_awareness_changed{ "awareness_changed" }; String PlayerDetector::sig_awareness_changed{ "awareness_changed" };
@ -6,6 +7,17 @@ void PlayerDetector::_bind_methods() {
ADD_SIGNAL(MethodInfo(sig_awareness_changed, PropertyInfo(Variant::BOOL, "aware"))); ADD_SIGNAL(MethodInfo(sig_awareness_changed, PropertyInfo(Variant::BOOL, "aware")));
} }
// check if there is geometry between detector and player
bool PlayerDetector::line_of_sight_exists() const {
PhysicsDirectSpaceState3D::RayParameters params{ this->ray_params };
params.from = get_global_position();
params.to = PlayerBody::get_singleton()->get_global_position();
PhysicsDirectSpaceState3D *space{ get_world_3d()->get_direct_space_state() };
PhysicsDirectSpaceState3D::RayResult result{};
space->intersect_ray(params, result);
return result.collider == nullptr;
}
// Check if the player is in a bounded area in front of the detector and unobscured. // 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. // As all tests are required to pass, we do them in increasing order of complexity, to minimize unneeded resource use.
bool PlayerDetector::check() const { bool PlayerDetector::check() const {
@ -13,24 +25,18 @@ bool PlayerDetector::check() const {
Vector3 const position{ get_global_position() }; Vector3 const position{ get_global_position() };
Vector3 const target{ PlayerBody::get_singleton()->get_global_position() + Vector3{ 0.f, 1.5f, 0.f } }; 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 // check if the target is in a view cone
if (forward.dot(target - position) < this->min_dot) { if (forward.dot(target - position) < this->min_sight_dot) {
return false; return false;
} }
// check if the target is in range // check if the target is in range
if (position.distance_squared_to(target) > this->max_distance * this->max_distance) { if (position.distance_squared_to(target) > this->max_sight_range * this->max_sight_range) {
return false; return false;
} }
// check if the target is obscured return line_of_sight_exists();
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() { void PlayerDetector::ready() {
SoundEventPatchboard::get_singleton()->connect(SoundEventPatchboard::sig_sound_triggered, callable_mp(this, &self_type::on_player_sound));
this->ray_params.exclude.insert(PlayerBody::get_singleton()->get_rid()); this->ray_params.exclude.insert(PlayerBody::get_singleton()->get_rid());
} }
@ -46,6 +52,12 @@ void PlayerDetector::process(double delta) {
} }
} }
void PlayerDetector::on_player_sound(Vector3 at, float range) {
if (get_global_position().distance_squared_to(at) <= range * range && line_of_sight_exists()) {
set_aware_of_player(true);
}
}
void PlayerDetector::set_aware_of_player(bool value) { void PlayerDetector::set_aware_of_player(bool value) {
emit_signal(sig_awareness_changed, value); emit_signal(sig_awareness_changed, value);
this->aware_of_player = value; this->aware_of_player = value;

View file

@ -7,9 +7,11 @@
class PlayerDetector : public Node3D { class PlayerDetector : public Node3D {
GDCLASS(PlayerDetector, Node3D); GDCLASS(PlayerDetector, Node3D);
static void _bind_methods(); static void _bind_methods();
bool line_of_sight_exists() const;
bool check() const; bool check() const;
void ready(); void ready();
void process(double delta); void process(double delta);
void on_player_sound(Vector3 at, float range);
void set_aware_of_player(bool value); void set_aware_of_player(bool value);
protected: protected:
@ -20,10 +22,15 @@ public:
private: private:
bool aware_of_player{ false }; bool aware_of_player{ false };
float max_distance{ 100.f };
float min_dot{ 0.1f }; float max_hearing_range{ 40.f };
float max_sight_range{ 100.f };
float min_sight_dot{ 0.1f };
double query_time{ 0.3 }; double query_time{ 0.3 };
double query_timer{ 0.0 }; double query_timer{ 0.0 };
PhysicsDirectSpaceState3D::RayParameters ray_params{}; PhysicsDirectSpaceState3D::RayParameters ray_params{};
public: public:

View file

@ -8,6 +8,7 @@
#include "wave_survival/hitbox.h" #include "wave_survival/hitbox.h"
#include "wave_survival/hitscan_muzzle.h" #include "wave_survival/hitscan_muzzle.h"
#include "wave_survival/interactable.h" #include "wave_survival/interactable.h"
#include "wave_survival/muzzle_effect.h"
#include "wave_survival/npc_unit.h" #include "wave_survival/npc_unit.h"
#include "wave_survival/patrol_path.h" #include "wave_survival/patrol_path.h"
#include "wave_survival/player_body.h" #include "wave_survival/player_body.h"
@ -15,6 +16,8 @@
#include "wave_survival/player_detector.h" #include "wave_survival/player_detector.h"
#include "wave_survival/player_input.h" #include "wave_survival/player_input.h"
#include "wave_survival/player_interactor.h" #include "wave_survival/player_interactor.h"
#include "wave_survival/sound_event_node.h"
#include "wave_survival/sound_event_patchboard.h"
#include "wave_survival/state.h" #include "wave_survival/state.h"
#include "wave_survival/state_machine.h" #include "wave_survival/state_machine.h"
#include "wave_survival/weapon_base.h" #include "wave_survival/weapon_base.h"
@ -49,6 +52,12 @@ void initialize_wave_survival_module(ModuleInitializationLevel p_level) {
GDREGISTER_CLASS(PlayerInteractor); GDREGISTER_CLASS(PlayerInteractor);
GDREGISTER_CLASS(Interactable); GDREGISTER_CLASS(Interactable);
GDREGISTER_CLASS(Revolver); GDREGISTER_CLASS(Revolver);
GDREGISTER_CLASS(SoundEventPatchboard);
GDREGISTER_CLASS(SoundEventNode);
GDREGISTER_CLASS(MuzzleEffect);
memnew(SoundEventPatchboard);
Engine::get_singleton()->add_singleton(Engine::Singleton("SoundEventPatchboard", SoundEventPatchboard::get_singleton()));
} }
void uninitialize_wave_survival_module(ModuleInitializationLevel p_level) { void uninitialize_wave_survival_module(ModuleInitializationLevel p_level) {

View file

@ -0,0 +1,44 @@
#include "sound_event_node.h"
#include "macros.h"
#include "sound_event_patchboard.h"
void SoundEventNode::_bind_methods() {
ClassDB::bind_method(D_METHOD("trigger_event"), &self_type::trigger_event);
BIND_PROPERTY(Variant::BOOL, trigger_on_ready);
}
void SoundEventNode::ready() {
if (this->trigger_on_ready) {
trigger_event();
}
}
void SoundEventNode::_notification(int what) {
if (Engine::get_singleton()->is_editor_hint()) {
return;
}
if (what == NOTIFICATION_READY) {
ready();
}
}
void SoundEventNode::trigger_event() {
SoundEventPatchboard::get_singleton()->trigger_sound(get_global_position(), this->range);
}
void SoundEventNode::set_trigger_on_ready(bool value) {
this->trigger_on_ready = value;
}
bool SoundEventNode::get_trigger_on_ready() const {
return this->trigger_on_ready;
}
void SoundEventNode::set_range(float value) {
this->range = value;
}
float SoundEventNode::get_range() const {
return this->range;
}

View file

@ -0,0 +1,26 @@
#ifndef SOUND_EVENT_NODE_H
#define SOUND_EVENT_NODE_H
#include "scene/3d/node_3d.h"
class SoundEventNode : public Node3D {
GDCLASS(SoundEventNode, Node3D);
static void _bind_methods();
void ready();
protected:
void _notification(int what);
public:
void trigger_event();
void set_trigger_on_ready(bool value);
bool get_trigger_on_ready() const;
void set_range(float value);
float get_range() const;
private:
float range{ 20.f };
bool trigger_on_ready{ true };
};
#endif // !SOUND_EVENT_NODE_H

View file

@ -0,0 +1,27 @@
#include "sound_event_patchboard.h"
SoundEventPatchboard *SoundEventPatchboard::singleton_instance{ nullptr };
String const SoundEventPatchboard::sig_sound_triggered{ "sound_triggered" };
void SoundEventPatchboard::_bind_methods() {
ClassDB::bind_method(D_METHOD("trigger_sound", "at", "range"), &self_type::trigger_sound);
ADD_SIGNAL(MethodInfo(sig_sound_triggered, PropertyInfo(Variant::VECTOR3, "at"), PropertyInfo(Variant::FLOAT, "range")));
}
SoundEventPatchboard::SoundEventPatchboard() {
singleton_instance = this;
}
SoundEventPatchboard::~SoundEventPatchboard() {
if (singleton_instance == this) {
singleton_instance = nullptr;
}
}
SoundEventPatchboard *SoundEventPatchboard::get_singleton() {
return singleton_instance;
}
void SoundEventPatchboard::trigger_sound(Vector3 at, float range) {
emit_signal(sig_sound_triggered, at, range);
}

View file

@ -0,0 +1,22 @@
#ifndef SOUND_EVENT_PATCHBOARD_H
#define SOUND_EVENT_PATCHBOARD_H
#include "core/object/class_db.h"
#include "core/object/object.h"
class SoundEventPatchboard : public Object {
GDCLASS(SoundEventPatchboard, Object);
static void _bind_methods();
static SoundEventPatchboard *singleton_instance;
public:
SoundEventPatchboard();
virtual ~SoundEventPatchboard();
static SoundEventPatchboard *get_singleton();
void trigger_sound(Vector3 at, float range);
public:
static String const sig_sound_triggered;
};
#endif // !SOUND_EVENT_PATCHBOARD_H

View file

@ -1,7 +1,14 @@
[gd_scene load_steps=2 format=3 uid="uid://cfgwif53qypko"] [gd_scene load_steps=3 format=3 uid="uid://cfgwif53qypko"]
[ext_resource type="PackedScene" uid="uid://bkw6pt33nbn2" path="res://assets/models/weapons/revolver.blend" id="1_5ynga"] [ext_resource type="PackedScene" uid="uid://bkw6pt33nbn2" path="res://assets/models/weapons/revolver.blend" id="1_5ynga"]
[sub_resource type="GDScript" id="GDScript_5ynga"]
script/source = "extends MuzzleEffect
func trigger() -> void:
SoundEventPatchboard.trigger_sound(global_position, 100)
"
[node name="Revolver" type="Revolver" node_paths=PackedStringArray("anim")] [node name="Revolver" type="Revolver" node_paths=PackedStringArray("anim")]
anim = NodePath("revolver/AnimationPlayer") anim = NodePath("revolver/AnimationPlayer")
single_action_spread = 0.003 single_action_spread = 0.003
@ -15,12 +22,22 @@ layers = 2
[node name="Cube" parent="revolver/Character/Skeleton3D" index="1"] [node name="Cube" parent="revolver/Character/Skeleton3D" index="1"]
layers = 2 layers = 2
[node name="HitscanMuzzle" type="HitscanMuzzle" parent="."] [node name="BoneAttachment3D" type="BoneAttachment3D" parent="revolver/Character/Skeleton3D" index="2"]
transform = Transform3D(1, -6.350722e-17, 4.732016e-17, 4.732016e-17, 0.95822614, 0.28601173, -6.350722e-17, -0.28601173, 0.95822614, -1.1196792e-16, -0.03667751, 0.009908612)
bone_name = "root"
bone_idx = 39
[node name="MuzzleEffect" type="MuzzleEffect" parent="revolver/Character/Skeleton3D/BoneAttachment3D"]
transform = Transform3D(1, 0, 0, 0, 0.95768696, -0.28781185, 0, 0.28781185, 0.95768696, 2.4018701e-17, 0.19591203, -0.24464998)
script = SubResource("GDScript_5ynga")
[node name="HitscanMuzzle" type="HitscanMuzzle" parent="." node_paths=PackedStringArray("muzzle_effect")]
unique_name_in_owner = true unique_name_in_owner = true
transform = Transform3D(1, 0, 0, 0, -4.3711385e-08, 0.9999999, 0, -0.9999999, -4.3711385e-08, 0, 0, 0) transform = Transform3D(1, 0, 0, 0, -4.3711385e-08, 0.9999999, 0, -0.9999999, -4.3711385e-08, 0, 0, 0)
target_position = Vector3(0, 200, 0) target_position = Vector3(0, 200, 0)
collision_mask = 6 collision_mask = 6
collide_with_areas = true collide_with_areas = true
spread = 0.02 spread = 0.02
muzzle_effect = NodePath("../revolver/Character/Skeleton3D/BoneAttachment3D/MuzzleEffect")
[editable path="revolver"] [editable path="revolver"]

View file

@ -1,7 +1,14 @@
[gd_scene load_steps=2 format=3 uid="uid://ce40pq785yoyi"] [gd_scene load_steps=3 format=3 uid="uid://ce40pq785yoyi"]
[ext_resource type="PackedScene" uid="uid://bodfxp36t6v36" path="res://assets/models/weapons/rifle.blend" id="1_afgyw"] [ext_resource type="PackedScene" uid="uid://bodfxp36t6v36" path="res://assets/models/weapons/rifle.blend" id="1_afgyw"]
[sub_resource type="GDScript" id="GDScript_afgyw"]
script/source = "extends MuzzleEffect
func trigger() -> void:
SoundEventPatchboard.trigger_sound(global_position, 100)
"
[node name="Rifle" type="Rifle" node_paths=PackedStringArray("anim")] [node name="Rifle" type="Rifle" node_paths=PackedStringArray("anim")]
anim = NodePath("rifle/AnimationPlayer") anim = NodePath("rifle/AnimationPlayer")
@ -13,10 +20,19 @@ layers = 2
[node name="mesh" parent="rifle/Character/Skeleton3D" index="1"] [node name="mesh" parent="rifle/Character/Skeleton3D" index="1"]
layers = 2 layers = 2
[node name="BoneAttachment3D" type="BoneAttachment3D" parent="rifle/Character/Skeleton3D" index="2"]
transform = Transform3D(1, -2.0932122e-15, 2.3524223e-18, 2.3525281e-18, 0.0022477505, 0.99999744, -2.0932126e-15, -0.9999974, 0.0022477508, 0.07988295, -0.13953947, -0.33976445)
bone_name = "root"
bone_idx = 39
[node name="MuzzleEffect" type="MuzzleEffect" parent="rifle/Character/Skeleton3D/BoneAttachment3D"]
transform = Transform3D(1, 0, 0, 0, 0.95768696, -0.28781185, 0, 0.28781185, 0.95768696, 2.4018701e-17, 0.19591203, -0.24464998)
script = SubResource("GDScript_afgyw")
[node name="AnimationPlayer" parent="rifle" index="2"] [node name="AnimationPlayer" parent="rifle" index="2"]
playback_default_blend_time = 0.1 playback_default_blend_time = 0.1
[node name="HitscanMuzzle" type="HitscanMuzzle" parent="."] [node name="HitscanMuzzle" type="HitscanMuzzle" parent="." node_paths=PackedStringArray("muzzle_effect")]
unique_name_in_owner = true unique_name_in_owner = true
transform = Transform3D(1, -2.0932111e-15, -2.7987948e-18, 2.3525281e-18, -0.00021316158, 0.99999976, -2.0932126e-15, -0.9999997, -0.00021316111, 0, 0, 0) transform = Transform3D(1, -2.0932111e-15, -2.7987948e-18, 2.3525281e-18, -0.00021316158, 0.99999976, -2.0932126e-15, -0.9999997, -0.00021316111, 0, 0, 0)
target_position = Vector3(0, 200, 0) target_position = Vector3(0, 200, 0)
@ -24,5 +40,6 @@ collision_mask = 6
collide_with_areas = true collide_with_areas = true
spread = 0.003 spread = 0.003
damage = 3 damage = 3
muzzle_effect = NodePath("../rifle/Character/Skeleton3D/BoneAttachment3D/MuzzleEffect")
[editable path="rifle"] [editable path="rifle"]