From 7cf5b128ffe94f869fde24551acfb45b40e55097 Mon Sep 17 00:00:00 2001 From: Sara Date: Tue, 25 Jun 2024 21:16:35 +0200 Subject: [PATCH] feat: implemented fire at target action --- godot/GameObjects/enemy_unit.tscn | 103 ++++++++++++++++++++++++++ godot/GameObjects/player_camera.tscn | 2 +- godot/GameObjects/player_unit.tscn | 106 +++++++++++++++++++++++++++ godot/GameObjects/unit.tscn | 32 -------- godot/Levels/test_level.tscn | 34 +++++++-- src/entity_health.cpp | 60 +++++++++++++++ src/entity_health.hpp | 28 +++++++ src/goap/action.hpp | 11 ++- src/goap/action_db.cpp | 13 ++++ src/goap/action_db.hpp | 3 + src/goap/goal.cpp | 6 ++ src/goap/goal.hpp | 1 + src/goap/planner.cpp | 10 ++- src/goap/planner.hpp | 5 +- src/register_types.cpp | 2 + src/rts_actions.cpp | 6 +- src/rts_player.cpp | 66 ++++++++--------- src/rts_player.hpp | 18 +++-- src/rts_states.cpp | 30 ++++++-- src/rts_states.hpp | 8 +- src/unit.cpp | 78 +++++++++++++++++--- src/unit.hpp | 24 +++++- src/unit_world_state.cpp | 32 +++++++- src/unit_world_state.hpp | 2 + 24 files changed, 569 insertions(+), 111 deletions(-) create mode 100644 godot/GameObjects/enemy_unit.tscn create mode 100644 godot/GameObjects/player_unit.tscn delete mode 100644 godot/GameObjects/unit.tscn create mode 100644 src/entity_health.cpp create mode 100644 src/entity_health.hpp diff --git a/godot/GameObjects/enemy_unit.tscn b/godot/GameObjects/enemy_unit.tscn new file mode 100644 index 0000000..5734f3b --- /dev/null +++ b/godot/GameObjects/enemy_unit.tscn @@ -0,0 +1,103 @@ +[gd_scene load_steps=8 format=3 uid="uid://ba17jrcaduowj"] + +[sub_resource type="Animation" id="Animation_va30j"] +resource_name = "RESET" +length = 0.001 +loop_mode = 1 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath("MeshInstance3D2:position") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 0, +"values": [Vector3(-0.363891, 1.24027, 0.471381)] +} + +[sub_resource type="Animation" id="Animation_opwhc"] +resource_name = "fire" +step = 0.01 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath("MeshInstance3D2:position") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0, 0.04, 0.14), +"transitions": PackedFloat32Array(1, 1, 1), +"update": 0, +"values": [Vector3(-0.363891, 1.24027, 0.471381), Vector3(-0.363891, 1.24027, 0.199307), Vector3(-0.363891, 1.24027, 0.471381)] +} +tracks/1/type = "method" +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/path = NodePath(".") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"values": [{ +"args": [], +"method": &"fire_at_target" +}] +} + +[sub_resource type="AnimationLibrary" id="AnimationLibrary_3jmw4"] +_data = { +"RESET": SubResource("Animation_va30j"), +"fire_weapon": SubResource("Animation_opwhc") +} + +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_ss47r"] +albedo_color = Color(1, 0.24, 0.24, 1) + +[sub_resource type="CapsuleMesh" id="CapsuleMesh_5r0b3"] +material = SubResource("StandardMaterial3D_ss47r") + +[sub_resource type="SphereShape3D" id="SphereShape3D_drlm2"] +radius = 1.0 + +[sub_resource type="BoxMesh" id="BoxMesh_p8wvo"] +size = Vector3(0.2, 0.2, 0.5) + +[node name="Unit" type="Unit"] +configure_team = 3 +collision_layer = 6 +collision_mask = 0 + +[node name="ActorWorldState" type="UnitWorldState" parent="."] + +[node name="Planner" type="Planner" parent="."] + +[node name="EntityHealth" type="EntityHealth" parent="."] + +[node name="NavigationAgent3D" type="NavigationAgent3D" parent="."] + +[node name="AnimationPlayer" type="AnimationPlayer" parent="."] +libraries = { +"": SubResource("AnimationLibrary_3jmw4") +} + +[node name="Eyes" type="Node3D" parent="."] +unique_name_in_owner = true +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.4512, 0) + +[node name="VisionTarget" type="Node3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.31501, 0) + +[node name="MeshInstance3D" type="MeshInstance3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0) +mesh = SubResource("CapsuleMesh_5r0b3") + +[node name="CollisionShape3D" type="CollisionShape3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0) +shape = SubResource("SphereShape3D_drlm2") + +[node name="MeshInstance3D2" type="MeshInstance3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.363891, 1.24027, 0.471381) +mesh = SubResource("BoxMesh_p8wvo") diff --git a/godot/GameObjects/player_camera.tscn b/godot/GameObjects/player_camera.tscn index ce7a9e9..bf4836e 100644 --- a/godot/GameObjects/player_camera.tscn +++ b/godot/GameObjects/player_camera.tscn @@ -9,7 +9,7 @@ height = 0.1 ground_marker_scene = ExtResource("1_t46fa") [node name="Camera3D" type="Camera3D" parent="."] -transform = Transform3D(-1, 7.57103e-08, -4.37114e-08, 0, 0.5, 0.866025, 8.74228e-08, 0.866025, -0.5, 0, 5.86004, -2.59394) +transform = Transform3D(-1, 7.57104e-08, -4.37114e-08, 0, 0.5, 0.866025, 8.74228e-08, 0.866025, -0.5, 0, 8.41077, -4.42292) [node name="MeshInstance3D" type="MeshInstance3D" parent="."] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.0494189, 0) diff --git a/godot/GameObjects/player_unit.tscn b/godot/GameObjects/player_unit.tscn new file mode 100644 index 0000000..c37e11a --- /dev/null +++ b/godot/GameObjects/player_unit.tscn @@ -0,0 +1,106 @@ +[gd_scene load_steps=8 format=3 uid="uid://pme230qx1377"] + +[sub_resource type="Animation" id="Animation_va30j"] +resource_name = "RESET" +length = 0.001 +loop_mode = 1 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath("MeshInstance3D2:position") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"update": 0, +"values": [Vector3(-0.363891, 1.24027, 0.471381)] +} + +[sub_resource type="Animation" id="Animation_opwhc"] +resource_name = "fire" +step = 0.01 +tracks/0/type = "value" +tracks/0/imported = false +tracks/0/enabled = true +tracks/0/path = NodePath("MeshInstance3D2:position") +tracks/0/interp = 1 +tracks/0/loop_wrap = true +tracks/0/keys = { +"times": PackedFloat32Array(0, 0.04, 0.14), +"transitions": PackedFloat32Array(1, 1, 1), +"update": 0, +"values": [Vector3(-0.363891, 1.24027, 0.471381), Vector3(-0.363891, 1.24027, 0.199307), Vector3(-0.363891, 1.24027, 0.471381)] +} +tracks/1/type = "method" +tracks/1/imported = false +tracks/1/enabled = true +tracks/1/path = NodePath(".") +tracks/1/interp = 1 +tracks/1/loop_wrap = true +tracks/1/keys = { +"times": PackedFloat32Array(0), +"transitions": PackedFloat32Array(1), +"values": [{ +"args": [], +"method": &"fire_at_target" +}] +} + +[sub_resource type="AnimationLibrary" id="AnimationLibrary_3jmw4"] +_data = { +"RESET": SubResource("Animation_va30j"), +"fire_weapon": SubResource("Animation_opwhc") +} + +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_n4q15"] +albedo_color = Color(0.407843, 1, 0.447059, 1) + +[sub_resource type="CapsuleMesh" id="CapsuleMesh_5r0b3"] +material = SubResource("StandardMaterial3D_n4q15") + +[sub_resource type="SphereShape3D" id="SphereShape3D_drlm2"] +radius = 1.0 + +[sub_resource type="BoxMesh" id="BoxMesh_p8wvo"] +size = Vector3(0.2, 0.2, 0.5) + +[node name="Unit" type="Unit"] +configure_team = 1 +collision_layer = 6 +collision_mask = 0 + +[node name="ActorWorldState" type="UnitWorldState" parent="."] + +[node name="Planner" type="Planner" parent="."] +actions_inspector = [0, 1, 2] + +[node name="EntityHealth" type="EntityHealth" parent="."] + +[node name="NavigationAgent3D" type="NavigationAgent3D" parent="."] +path_height_offset = 0.5 +debug_enabled = true + +[node name="AnimationPlayer" type="AnimationPlayer" parent="."] +libraries = { +"": SubResource("AnimationLibrary_3jmw4") +} + +[node name="Eyes" type="Node3D" parent="."] +unique_name_in_owner = true +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.49429, 0) + +[node name="VisionTarget" type="Node3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.39151, 0) + +[node name="MeshInstance3D" type="MeshInstance3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0) +mesh = SubResource("CapsuleMesh_5r0b3") + +[node name="CollisionShape3D" type="CollisionShape3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0) +shape = SubResource("SphereShape3D_drlm2") + +[node name="MeshInstance3D2" type="MeshInstance3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.363891, 1.24027, 0.471381) +mesh = SubResource("BoxMesh_p8wvo") diff --git a/godot/GameObjects/unit.tscn b/godot/GameObjects/unit.tscn deleted file mode 100644 index 33831dd..0000000 --- a/godot/GameObjects/unit.tscn +++ /dev/null @@ -1,32 +0,0 @@ -[gd_scene load_steps=4 format=3 uid="uid://pme230qx1377"] - -[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_n4q15"] -albedo_color = Color(0.407843, 1, 0.447059, 1) - -[sub_resource type="CapsuleMesh" id="CapsuleMesh_5r0b3"] -material = SubResource("StandardMaterial3D_n4q15") - -[sub_resource type="SphereShape3D" id="SphereShape3D_drlm2"] -radius = 1.0 - -[node name="Unit" type="Unit"] -collision_layer = 2 -collision_mask = 0 - -[node name="ActorWorldState" type="UnitWorldState" parent="."] - -[node name="Planner" type="Planner" parent="."] -actions = [0, 1, 2] - -[node name="NavigationAgent3D" type="NavigationAgent3D" parent="."] -target_desired_distance = 0.25 - -[node name="AnimationPlayer" type="AnimationPlayer" parent="."] - -[node name="MeshInstance3D" type="MeshInstance3D" parent="."] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0) -mesh = SubResource("CapsuleMesh_5r0b3") - -[node name="CollisionShape3D" type="CollisionShape3D" parent="."] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0) -shape = SubResource("SphereShape3D_drlm2") diff --git a/godot/Levels/test_level.tscn b/godot/Levels/test_level.tscn index 4c38fca..d5eaf48 100644 --- a/godot/Levels/test_level.tscn +++ b/godot/Levels/test_level.tscn @@ -1,20 +1,27 @@ -[gd_scene load_steps=7 format=3 uid="uid://c62s1jmtgajjk"] +[gd_scene load_steps=10 format=3 uid="uid://c62s1jmtgajjk"] [ext_resource type="PackedScene" uid="uid://dsalxxq3xs842" path="res://rts_game_mode.tscn" id="1_4nchg"] [ext_resource type="Environment" uid="uid://cnfk8yrvklysq" path="res://Environments/default_environment.tres" id="2_jq6bw"] -[ext_resource type="PackedScene" uid="uid://pme230qx1377" path="res://GameObjects/unit.tscn" id="3_wl7wm"] +[ext_resource type="PackedScene" uid="uid://pme230qx1377" path="res://GameObjects/player_unit.tscn" id="3_wl7wm"] +[ext_resource type="PackedScene" uid="uid://ba17jrcaduowj" path="res://GameObjects/enemy_unit.tscn" id="4_0o33v"] [sub_resource type="NavigationMesh" id="NavigationMesh_8a2j6"] -vertices = PackedVector3Array(-4.5, 0.4, -4.5, -4.5, 0.4, 4.5, 4.5, 0.4, 4.5, 4.5, 0.4, -4.5) -polygons = [PackedInt32Array(3, 2, 0), PackedInt32Array(0, 2, 1)] +vertices = PackedVector3Array(0, 0.4, -2, 1.75, 0.4, -3.25, 1.75, 0.4, -9.5, -0.75, 0.4, -0.25, 0, 0.4, -2, 1.75, 0.4, -9.5, -9.5, 0.4, -9.5, -9.5, 0.4, -0.25, 4.5, 0.4, -1.25, 4.5, 0.4, -0.5, 9.5, 0.4, -0.5, 3.25, 0.4, -2.5, 4.5, 0.4, -1.25, 9.5, 0.4, -0.5, 1.75, 0.4, -9.5, 1.75, 0.4, -3.25, 3.25, 0.4, -2.5, 9.5, 0.4, -0.5, 9.5, 0.4, -9.5, 1.75, 3.4, 1, 2.25, 3.4, 1, 3.5, 3.4, -0.5, 2.75, 3.4, -1.5, 1.5, 3.4, -2, 0.25, 3.4, -0.5, 3, 0.4, -0.5, 2.5, 0.4, -1.25, 1.5, 0.4, -1.5, 0.75, 0.4, -0.5, 1.75, 0.4, 0.5, 9.5, 0.4, -0.5, 4.5, 0.4, -0.5, 3.5, 0.4, 1.25, 9.5, 0.4, -0.5, 3.5, 0.4, 1.25, 2, 0.4, 2.25, 2, 0.4, 9.5, 9.5, 0.4, 9.5, 2, 0.4, 9.5, 2, 0.4, 2.25, 0, 0.4, 1, 0, 0.4, 1, -0.75, 0.4, -0.25, -9.5, 0.4, -0.25, -9.5, 0.4, 9.5, 2, 0.4, 9.5) +polygons = [PackedInt32Array(2, 1, 0), PackedInt32Array(4, 3, 5), PackedInt32Array(5, 3, 7), PackedInt32Array(5, 7, 6), PackedInt32Array(10, 9, 8), PackedInt32Array(13, 12, 11), PackedInt32Array(15, 14, 16), PackedInt32Array(16, 14, 18), PackedInt32Array(16, 18, 17), PackedInt32Array(20, 19, 21), PackedInt32Array(21, 19, 22), PackedInt32Array(22, 19, 23), PackedInt32Array(23, 19, 24), PackedInt32Array(26, 25, 27), PackedInt32Array(27, 25, 28), PackedInt32Array(28, 25, 29), PackedInt32Array(32, 31, 30), PackedInt32Array(34, 33, 35), PackedInt32Array(35, 33, 36), PackedInt32Array(36, 33, 37), PackedInt32Array(40, 39, 38), PackedInt32Array(42, 41, 43), PackedInt32Array(43, 41, 44), PackedInt32Array(44, 41, 45)] geometry_parsed_geometry_type = 1 geometry_collision_mask = 4294967289 [sub_resource type="BoxShape3D" id="BoxShape3D_ubt1u"] -size = Vector3(10, 0.1, 10) +size = Vector3(20, 0.1, 20) [sub_resource type="PlaneMesh" id="PlaneMesh_hohcb"] -size = Vector2(10, 10) +size = Vector2(20, 20) + +[sub_resource type="BoxShape3D" id="BoxShape3D_nlgbx"] +size = Vector3(3, 3, 3) + +[sub_resource type="BoxMesh" id="BoxMesh_o000f"] +size = Vector3(3, 3, 3) [node name="TestLevel" type="Level3D"] game_mode_prototype = ExtResource("1_4nchg") @@ -36,9 +43,24 @@ shape = SubResource("BoxShape3D_ubt1u") mesh = SubResource("PlaneMesh_hohcb") skeleton = NodePath("../../../..") +[node name="StaticBody3D2" type="StaticBody3D" parent="WorldEnvironment/NavigationRegion3D"] +transform = Transform3D(0.616308, 0, 0.787505, 0, 1, 0, -0.787505, 0, 0.616308, 1.88872, 1.48357, -0.533337) +collision_layer = 5 + +[node name="CollisionShape3D" type="CollisionShape3D" parent="WorldEnvironment/NavigationRegion3D/StaticBody3D2"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.05, 0) +shape = SubResource("BoxShape3D_nlgbx") + +[node name="MeshInstance3D" type="MeshInstance3D" parent="WorldEnvironment/NavigationRegion3D/StaticBody3D2"] +mesh = SubResource("BoxMesh_o000f") +skeleton = NodePath("../../../..") + [node name="DirectionalLight3D" type="DirectionalLight3D" parent="WorldEnvironment"] transform = Transform3D(-0.030678, -0.932282, 0.36043, 0, 0.360599, 0.932721, -0.999529, 0.028614, -0.0110625, 0, 1.28927, 0) light_color = Color(1, 0.926667, 0.78, 1) [node name="Unit" parent="." instance=ExtResource("3_wl7wm")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.7165, 1.8999e-07, 3.29106) + +[node name="Unit2" parent="." instance=ExtResource("4_0o33v")] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 7.57207, -7.63685e-07, -6.40453) diff --git a/src/entity_health.cpp b/src/entity_health.cpp new file mode 100644 index 0000000..f578ee3 --- /dev/null +++ b/src/entity_health.cpp @@ -0,0 +1,60 @@ +#include "entity_health.hpp" +#include "unit.hpp" +#include "utils/godot_macros.hpp" + +void EntityHealth::_bind_methods() { +#define CLASSNAME EntityHealth + GDPROPERTY(max_health, gd::Variant::INT); + + gd::PropertyInfo const source_arg{}; + 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")); + 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")); + GDSIGNAL("health_changed", + gd::PropertyInfo(gd::Variant::INT, "remaining"), + 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->current_health = this->max_health; + this->emit_signal("damage", this->current_health, 0, nullptr); +} + +void EntityHealth::damaged_by(int amount, Unit *source) { + amount = gd::Math::abs(amount); + this->current_health -= amount; + this->emit_signal("damage", this->current_health, amount, source); + this->emit_signal("health_changed", this->current_health, -amount); + if(this->current_health <= 0) { + this->emit_signal("death", source); + } +} + +void EntityHealth::healed_by(int amount, Unit *source) { + amount = gd::Math::abs(amount); + this->current_health = gd::Math::min(this->max_health, this->current_health + amount); + this->emit_signal("heal", this->current_health, amount, source); + this->emit_signal("health_changed", this->current_health, amount); +} + +void EntityHealth::set_max_health(int max_health) { + this->max_health = max_health; +} + +int EntityHealth::get_max_health() const { + return this->max_health; +} + +void EntityHealth::set_current_health(int current_health) { + this->current_health = current_health; +} + +int EntityHealth::get_current_health() const { + return this->current_health; +} diff --git a/src/entity_health.hpp b/src/entity_health.hpp new file mode 100644 index 0000000..ed91fd8 --- /dev/null +++ b/src/entity_health.hpp @@ -0,0 +1,28 @@ +#ifndef RTS_ENTITY_HEALTH_HPP +#define RTS_ENTITY_HEALTH_HPP + +#include + +namespace gd = godot; + +class Unit; + +class EntityHealth : public gd::Node { + GDCLASS(EntityHealth, gd::Node); + static void _bind_methods(); +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); + void set_max_health(int max_health); + int get_max_health() const; + void set_current_health(int current_health); + int get_current_health() const; +private: + int max_health{100}; + int current_health{0}; +}; + +#endif // !RTS_ENTITY_HEALTH_HPP diff --git a/src/goap/action.hpp b/src/goap/action.hpp index 9267be3..944775c 100644 --- a/src/goap/action.hpp +++ b/src/goap/action.hpp @@ -2,6 +2,7 @@ #define GOAP_ACTION_HPP #include "actor_world_state.hpp" +#include "godot_cpp/variant/utility_functions.hpp" #include "state.hpp" #include #include @@ -23,8 +24,14 @@ typedef int ActionID; class Action { friend class ActionDB; public: - static gd::StringName get_static_class() { return "Action"; } - virtual gd::StringName get_class() const { return "Action"; } + static gd::StringName get_static_class() { + gd::UtilityFunctions::push_error("Action::get_static_class has been called on a class where it is not implemented"); + return "Action"; + } + virtual gd::StringName get_class() const { + gd::UtilityFunctions::push_error("Action::get_class has been called on a class where it is not implemented"); + return "Action"; + } virtual ~Action(); virtual State *get_apply_state(ActorWorldState *context) const = 0; diff --git a/src/goap/action_db.cpp b/src/goap/action_db.cpp index a0f03fd..100491c 100644 --- a/src/goap/action_db.cpp +++ b/src/goap/action_db.cpp @@ -5,6 +5,8 @@ namespace goap { ActionDB::StaticData::~StaticData() { if(this->hint != nullptr) delete this->hint; + if(this->array_hint != nullptr) + delete this->array_hint; for(Action *action : this->actions) delete action; } @@ -15,11 +17,18 @@ gd::String &ActionDB::StaticData::get_hint() { return *this->hint; } +gd::String &ActionDB::StaticData::get_array_hint() { + if(this->array_hint == nullptr) + this->array_hint = new gd::String(); + return *this->array_hint; +} + ActionID ActionDB::register_action(Action *instance, gd::String name) { instance->id = ActionDB::data.actions.size(); if(!ActionDB::data.get_hint().is_empty()) ActionDB::data.get_hint() += ","; ActionDB::data.get_hint() += name; + ActionDB::data.get_array_hint() = gd::vformat("%s/%s:%s", gd::Variant::INT, gd::PROPERTY_HINT_ENUM, ActionDB::data.get_hint()); // defer intialization until after emplacement to avoid deleting instance ActionDB::data.actions.push_back(instance); gd::UtilityFunctions::print("registered action ", name); @@ -38,5 +47,9 @@ gd::String const &ActionDB::get_enum_hint() { return ActionDB::data.get_hint(); } +gd::String const &ActionDB::get_array_hint() { + return ActionDB::data.get_array_hint(); +} + ActionDB::StaticData ActionDB::data{}; } diff --git a/src/goap/action_db.hpp b/src/goap/action_db.hpp index 1f1e3da..9907137 100644 --- a/src/goap/action_db.hpp +++ b/src/goap/action_db.hpp @@ -24,7 +24,9 @@ class ActionDB { StaticData() = default; ~StaticData(); gd::String &get_hint(); + gd::String &get_array_hint(); gd::String *hint{nullptr}; + gd::String *array_hint{nullptr}; gd::Vector actions{}; }; static ActionID register_action(Action *instance, gd::String name); @@ -35,6 +37,7 @@ public: /*! Get the csv godot enum hint. */ static gd::String const &get_enum_hint(); + static gd::String const &get_array_hint(); /*! Add action to database. * * \returns Action's assigned ID. diff --git a/src/goap/goal.cpp b/src/goap/goal.cpp index 49d0c3e..99f35e2 100644 --- a/src/goap/goal.cpp +++ b/src/goap/goal.cpp @@ -9,6 +9,12 @@ void Goal::_bind_methods() { GDPROPERTY(desired_state_dict, gd::Variant::DICTIONARY); } +gd::Ref Goal::create(gd::StringName key, gd::Variant value) { + gd::Ref goal = memnew(Goal); + goal->desired_state.insert(key, value); + return goal; +} + void Goal::set_requirements_dict(gd::Dictionary dict) { this->requirements = utils::dictionary_to_hashmap(dict); } diff --git a/src/goap/goal.hpp b/src/goap/goal.hpp index 93498ed..2a5023d 100644 --- a/src/goap/goal.hpp +++ b/src/goap/goal.hpp @@ -11,6 +11,7 @@ class Goal : public gd::Resource { GDCLASS(Goal, gd::Resource); static void _bind_methods(); public: + static gd::Ref create(gd::StringName key, gd::Variant value); void set_requirements_dict(gd::Dictionary dict); gd::Dictionary get_requirements_dict() const; void set_desired_state_dict(gd::Dictionary dict); diff --git a/src/goap/planner.cpp b/src/goap/planner.cpp index efca508..a876a81 100644 --- a/src/goap/planner.cpp +++ b/src/goap/planner.cpp @@ -80,7 +80,7 @@ bool operator>=(goap::WorldStateNode const &lhs, goap::WorldStateNode const &rhs void Planner::_bind_methods() { #define CLASSNAME Planner - GDPROPERTY_HINTED(actions, gd::Variant::ARRAY, gd::PROPERTY_HINT_ARRAY_TYPE, gd::vformat("%s/%s:%s", gd::Variant::INT, gd::PROPERTY_HINT_ENUM, ActionDB::get_enum_hint())); + GDPROPERTY_HINTED(actions_inspector, gd::Variant::ARRAY, gd::PROPERTY_HINT_ARRAY_TYPE, ActionDB::get_array_hint()); } void Planner::_enter_tree() { GDGAMEONLY(); @@ -143,7 +143,7 @@ Plan Planner::plan_for_goal(gd::Ref goal) { return {}; } -void Planner::set_actions(gd::Array array) { +void Planner::set_actions_inspector(gd::Array array) { this->actions.clear(); for(size_t i = 0; i < array.size(); ++i) { Action const *action = int(array[i]) < 0 ? nullptr : ActionDB::get_action(array[i]); @@ -155,13 +155,17 @@ void Planner::set_actions(gd::Array array) { } } -gd::Array Planner::get_actions() const { +gd::Array Planner::get_actions_inspector() const { gd::Array array{}; for(Action const *action : this->actions) array.push_back(action == nullptr ? -1 : action->get_id()); return array; } +gd::Vector &Planner::get_actions() { + return this->actions; +} + gd::Vector Planner::get_neighbours(WorldStateNode const &from) const { gd::Vector retval{}; for(Action const *action : this->actions) { diff --git a/src/goap/planner.hpp b/src/goap/planner.hpp index f9d0cd3..253d615 100644 --- a/src/goap/planner.hpp +++ b/src/goap/planner.hpp @@ -58,8 +58,9 @@ public: //! Compute a plan to achieve the passed `Goal`. Plan plan_for_goal(gd::Ref goal); - void set_actions(gd::Array array); - gd::Array get_actions() const; + void set_actions_inspector(gd::Array array); + gd::Array get_actions_inspector() const; + gd::Vector &get_actions(); private: //! \returns A vector of all actions that satisfy any of the requirements in `unsatisfied` gd::Vector get_neighbours(WorldStateNode const &from) const; diff --git a/src/register_types.cpp b/src/register_types.cpp index d961c8e..ba1de6b 100644 --- a/src/register_types.cpp +++ b/src/register_types.cpp @@ -1,4 +1,5 @@ #include "register_types.h" +#include "entity_health.hpp" #include "goap/state.hpp" #include "rts_actions.hpp" #include "rts_game_mode.hpp" @@ -46,6 +47,7 @@ void initialize_gdextension_types(ModuleInitializationLevel p_level) ClassDB::register_class(); ClassDB::register_class(); ClassDB::register_class(); + ClassDB::register_class(); } extern "C" diff --git a/src/rts_actions.cpp b/src/rts_actions.cpp index 88cdcc4..2de56b0 100644 --- a/src/rts_actions.cpp +++ b/src/rts_actions.cpp @@ -24,12 +24,12 @@ goap::State *MoveToTarget::get_apply_state(goap::ActorWorldState *context) const FireAtTarget::FireAtTarget() : Action() { - effects.insert("target_dead", true); + effects.insert("is_target_dead", true); required.insert("can_see_target", true); } goap::State *FireAtTarget::get_apply_state(goap::ActorWorldState *context) const { - Animate *state = memnew(Animate); + Animate *state = this->create_state(); state->animation = "fire_weapon"; return state; } @@ -41,7 +41,7 @@ FindTarget::FindTarget() goap::State *FindTarget::get_apply_state(goap::ActorWorldState *context) const { gd::Node3D *target = gd::Object::cast_to(context->get_world_property("target_node")); - MoveTo *state = memnew(MoveTo); + MoveTo *state = this->create_state(); state->target_node = target; return state; } diff --git a/src/rts_player.cpp b/src/rts_player.cpp index 0a0aa9b..cbc9c47 100644 --- a/src/rts_player.cpp +++ b/src/rts_player.cpp @@ -1,9 +1,7 @@ #include "rts_player.hpp" #include "goal_marker.hpp" -#include "godot_cpp/variant/utility_functions.hpp" #include "unit.hpp" #include "utils/godot_macros.hpp" -#include "utils/level.hpp" #include "utils/util_functions.hpp" #include #include @@ -26,11 +24,12 @@ void RTSPlayer::_process(double delta_time) { this->move(this->camera_keys_motion * this->camera_keys_speed * delta_time); this->rotate(gd::Vector3{0.f, 1.f, 0.f}, camera_keys_rotation * this->camera_keys_rotation_speed * delta_time); + this->process_mouse(); this->move(this->camera_mouse_motion * this->camera_mouse_speed); this->camera_mouse_motion = {0.f, 0.f}; - this->rotate(gd::Vector3{0.f, 1.f, 0.f}, camera_mouse_rotation * this->camera_mouse_rotation_speed); + if(rmb_is_dragged) + this->rotate(gd::Vector3{0.f, 1.f, 0.f}, camera_mouse_rotation * this->camera_mouse_rotation_speed); this->camera_mouse_rotation = 0.f; - this->process_mouse(); } void RTSPlayer::setup_player_input(utils::PlayerInput *input) { @@ -56,6 +55,10 @@ void RTSPlayer::process_mouse() { gd::Viewport *view = this->get_viewport(); gd::Vector2 const pix_coords = view->get_mouse_position(); this->cursor_camera_normal = this->camera->project_ray_normal(pix_coords); + bool const is_drag_motion = this->total_cursor_motion.length() >= 10; + this->lmb_is_dragged = (this->lmb_is_dragged || is_drag_motion) && this->lmb_down; + this->rmb_is_dragged = (this->rmb_is_dragged || is_drag_motion) && this->rmb_down; + this->total_cursor_motion = {0.f, 0.f}; } void RTSPlayer::move(gd::Vector2 motion) { @@ -73,10 +76,6 @@ gd::Dictionary RTSPlayer::raycast_from_cursor(uint32_t layers) const { gd::Node3D *RTSPlayer::node_under_cursor(uint32_t layers) const { gd::Dictionary result = this->raycast_from_cursor(layers); - if(!result.is_empty()) - gd::UtilityFunctions::print("Found object ", Object::cast_to(result["collider"])->get_path()); - else - gd::UtilityFunctions::print("No object found"); return result.is_empty() ? nullptr : Object::cast_to(result["collider"]); } @@ -89,35 +88,47 @@ void RTSPlayer::select_unit_under_cursor() { void RTSPlayer::order_activate_object_under_cursor() { if(this->selected_unit == nullptr) return; + // fetch whatever's under the cursor in the "objects" layer gd::Dictionary const dict = this->raycast_from_cursor(0x4); if(dict.is_empty()) return; - gd::Node3D *hit = gd::Object::cast_to(dict["collider"]); - GoalMarker *marker = gd::Object::cast_to(hit); - bool const is_temporary_marker = marker == nullptr; - if(is_temporary_marker) { - marker = gd::Object::cast_to(this->ground_marker_scene->instantiate()); + if(!this->order_activate_object(gd::Object::cast_to(dict["collider"]))) { + GoalMarker *marker = gd::Object::cast_to(this->ground_marker_scene->instantiate()); + // setup position before planning to achieve goal this->add_child(marker); marker->set_as_top_level(true); marker->set_global_position(dict["position"]); + this->selected_unit->begin_marker_temporary(marker); } - this->selected_unit->plan_for_marker(marker); - if(is_temporary_marker) { - this->selected_unit->get_world_state()->connect("attention_changed", callable_mp(marker, &GoalMarker::destroy_on_forgotten)); +} +bool RTSPlayer::order_activate_object(gd::Node3D *node) { + // get the hit object as a marker + if(GoalMarker *marker = gd::Object::cast_to(node)) { + this->selected_unit->set_target_goal(marker, marker->get_goal()); + return true; } + Unit *unit{gd::Object::cast_to(node)}; + if(unit && unit->get_team() == UnitTeam::Enemy) { + this->selected_unit->set_target_goal(unit, goap::Goal::create("is_target_dead", true)); + return true; + } + return false; + } void RTSPlayer::clear_selected_unit() { if(this->selected_unit != nullptr) { - this->selected_unit->disconnect("tree_exited", this->unit_destroyed_callable()); + this->selected_unit->disconnect("tree_exited", this->on_selected_unit_destroyed); this->selected_unit = nullptr; } } void RTSPlayer::select_unit(Unit *unit) { + if(unit->get_team() != UnitTeam::Player) + return; this->clear_selected_unit(); this->selected_unit = unit; - unit->connect("tree_exited", this->unit_destroyed_callable()); + unit->connect("tree_exited", this->on_selected_unit_destroyed); } void RTSPlayer::on_mouse_horizontal(gd::Ref event, float value) { @@ -125,11 +136,13 @@ void RTSPlayer::on_mouse_horizontal(gd::Ref event, float value) this->camera_mouse_motion.x = value; else if(this->rmb_down) this->camera_mouse_rotation = value; + this->total_cursor_motion.x = value; } void RTSPlayer::on_mouse_vertical(gd::Ref event, 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 event, float value) { @@ -145,15 +158,14 @@ void RTSPlayer::on_rotate_horizontal(gd::Ref event, float value) } void RTSPlayer::on_rclick(gd::Ref event, float value) { - gd::UtilityFunctions::print("on_rclick on ", this->get_path(), " ---------"); - if(value == 0.f && !this->rmb_is_held()) + 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 event, float value) { - if(value == 0.f && !this->lmb_is_held()) + 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(); @@ -173,18 +185,6 @@ gd::Vector3 RTSPlayer::get_left_direction() const { return gd::Vector3{left.x, 0.f, left.z}.normalized(); } -bool RTSPlayer::lmb_is_held() { - return this->lmb_down && utils::time_seconds() >= (this->lmb_last_change + this->time_to_held); -} - -bool RTSPlayer::rmb_is_held() { - return this->rmb_down && utils::time_seconds() >= (this->rmb_last_change + this->time_to_held); -} - -gd::Callable RTSPlayer::unit_destroyed_callable() { - return callable_mp(this, &RTSPlayer::clear_selected_unit); -} - void RTSPlayer::set_ground_marker_scene(gd::Ref scene) { this->ground_marker_scene = scene; } diff --git a/src/rts_player.hpp b/src/rts_player.hpp index 0d5316a..1797f0e 100644 --- a/src/rts_player.hpp +++ b/src/rts_player.hpp @@ -1,14 +1,14 @@ #ifndef RTS_PLAYER_HPP #define RTS_PLAYER_HPP -#include "godot_cpp/classes/camera3d.hpp" -#include "godot_cpp/classes/input_event.hpp" -#include "godot_cpp/classes/packed_scene.hpp" #include "unit.hpp" #include "utils/player.hpp" #include "utils/player_input.hpp" #include +#include #include +#include +#include namespace gd = godot; @@ -34,6 +34,10 @@ private: //! add units under the cursor to this->selected_units void select_unit_under_cursor(); void order_activate_object_under_cursor(); + //! \returns True if the passed node can and has been activated. + //! \returns False if the passed node cannot be activated. + bool order_activate_object(gd::Node3D *node); + void spawn_movement_marker(gd::Vector3 at); void clear_selected_unit(); void select_unit(Unit *unit); @@ -50,9 +54,6 @@ private: // setters & getters gd::Vector3 get_forward_direction() const; gd::Vector3 get_left_direction() const; - bool lmb_is_held(); - bool rmb_is_held(); - gd::Callable unit_destroyed_callable(); void set_ground_marker_scene(gd::Ref scene); gd::Ref get_ground_marker_scene() const; private: @@ -61,13 +62,16 @@ private: bool mmb_down{false}; bool lmb_down{false}; float lmb_last_change{0.f}; + bool lmb_is_dragged{false}; bool rmb_down{false}; float rmb_last_change{0.f}; + bool rmb_is_dragged{false}; gd::Vector2 camera_mouse_motion{0.f, 0.f}; float camera_mouse_rotation{0.f}; gd::Vector2 camera_keys_motion{0.f, 0.f}; float camera_keys_rotation{0.f}; + gd::Vector2 total_cursor_motion{}; gd::Vector3 cursor_camera_normal{0.f, 0.f, 0.f}; @@ -75,6 +79,8 @@ private: gd::Ref ground_marker_scene{nullptr}; + gd::Callable const on_selected_unit_destroyed{callable_mp(this, &RTSPlayer::clear_selected_unit)}; + float const camera_keys_speed{10.f}; float const camera_keys_rotation_speed{1.f}; float const camera_mouse_speed{0.01f}; diff --git a/src/rts_states.cpp b/src/rts_states.cpp index 84abe7e..348e013 100644 --- a/src/rts_states.cpp +++ b/src/rts_states.cpp @@ -1,17 +1,15 @@ #include "rts_states.hpp" -#include "godot_cpp/variant/utility_functions.hpp" +#include void MoveTo::_bind_methods() {} void MoveTo::_ready() { - gd::UtilityFunctions::print("_ready", this->get_path()); this->agent = this->get_node("../NavigationAgent3D"); this->agent->set_target_position(this->target_node->get_global_position()); this->parent_node3d = Object::cast_to(this->get_parent()); } void MoveTo::_end_state() { - gd::UtilityFunctions::print("_end_state on ", this->get_path()); this->agent->set_target_position(this->parent_node3d->get_global_position()); } @@ -21,11 +19,31 @@ void MoveTo::_process(double delta_time) { gd::Vector3 const direction = (target - pos).normalized(); 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->agent->is_navigation_finished()) - this->end_state(); - State::_process(delta_time); + + if(this->is_action_done()) + this->state_finished(); + else if(this->agent->is_navigation_finished()) + this->state_failed(); } void Activate::_bind_methods() {} void Animate::_bind_methods() {} + +void Animate::_ready() { + this->anim = this->get_node("../AnimationPlayer"); + this->anim->queue(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(); + } +} + +void Animate::_end_state() { + if(this->anim->get_current_animation() == this->animation) + this->anim->stop(); +} diff --git a/src/rts_states.hpp b/src/rts_states.hpp index 1395740..e2cd532 100644 --- a/src/rts_states.hpp +++ b/src/rts_states.hpp @@ -2,6 +2,7 @@ #define RTS_STATES_HPP #include "goap/state.hpp" +#include #include /*! Uses navigation to chase the desired target node. @@ -32,7 +33,12 @@ class Animate : public goap::State { GDCLASS(Animate, goap::State); static void _bind_methods(); public: - gd::String animation{}; + virtual void _ready() override; + virtual void _process(double delta_time) override; + virtual void _end_state() override; + gd::StringName animation{}; +private: + gd::AnimationPlayer *anim{nullptr}; }; #endif // !RTS_STATES_HPP diff --git a/src/unit.cpp b/src/unit.cpp index e4b3872..de82998 100644 --- a/src/unit.cpp +++ b/src/unit.cpp @@ -1,4 +1,5 @@ #include "unit.hpp" +#include "entity_health.hpp" #include "goap/goal.hpp" #include "utils/godot_macros.hpp" #include @@ -7,16 +8,20 @@ void Unit::_bind_methods() { #define CLASSNAME Unit + GDSIGNAL("goal_finished"); + GDPROPERTY_HINTED(configure_team, gd::Variant::INT, gd::PROPERTY_HINT_ENUM, UnitTeam::get_property_hint()); + GDFUNCTION(fire_at_target); } void Unit::_enter_tree() { GDGAMEONLY(); this->agent = this->get_node("NavigationAgent3D"); this->planner = this->get_node("Planner"); this->world_state = this->get_node("ActorWorldState"); + this->eyes = this->get_node("%Eyes"); this->world_state->connect("attention_changed", callable_mp(this, &Unit::stop_plan)); -} -void Unit::_process(double delta_time) { + EntityHealth *health{this->get_node("EntityHealth")}; + health->connect("death", callable_mp(this, &Unit::on_death)); } void Unit::stop_plan() { @@ -27,18 +32,55 @@ void Unit::stop_plan() { this->state = nullptr; } -void Unit::plan_for_marker(GoalMarker *marker) { +void Unit::begin_marker_temporary(GoalMarker *marker) { this->destroy_state(); this->world_state->set_target_node(marker); - this->plan_for_goal(marker->get_goal()); + 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 + if(this->current_plan.is_empty()) { + marker->destroy_on_forgotten(); + } else { + this->world_state->connect("attention_changed", callable_mp(marker, &GoalMarker::destroy_on_forgotten)); + this->connect("goal_finished", callable_mp(marker, &GoalMarker::destroy_on_forgotten)); + } + this->next_action(); } -void Unit::plan_for_goal(gd::Ref goal) { - this->current_plan = this->planner->plan_for_goal(goal); - this->current_goal = goal; +void Unit::set_target_goal(gd::Node3D *target, gd::Ref goal) { + this->destroy_state(); + this->world_state->set_target_node(target); + this->begin_goal(goal); +} + +void Unit::begin_goal(gd::Ref goal) { + this->set_goal_and_plan(goal); this->next_action(); } +void Unit::fire_at_target() { + gd::Node3D *target{this->world_state->get_target_node()}; + if(!target) + return; + this->aim_at(target); + EntityHealth *health = target->get_node("EntityHealth"); + if(!health) + return; + health->damaged_by(10, this); +} + +void Unit::aim_at(gd::Node3D *target) { + gd::Vector3 z{target->get_global_position() - this->get_global_position()}; + z.y = 0; + z.normalize(); + gd::Vector3 x{gd::Vector3{0.f, 1.f, 0.f}.cross(z)}; + this->set_global_basis({x, z.cross(x), z}); +} + +void Unit::on_death(Unit *killer) { + this->queue_free(); +} + UnitWorldState *Unit::get_world_state() const { return this->world_state; } @@ -47,6 +89,23 @@ gd::Transform3D Unit::get_eye_transform() const { return this->eyes->get_global_transform(); } +UnitTeam Unit::get_team() const { + return this->team; +} + +void Unit::set_configure_team(int team) { + this->team = team; +} + +int Unit::get_configure_team() const { + return this->team; +} + +void Unit::set_goal_and_plan(gd::Ref goal) { + this->current_goal = goal; + this->current_plan = this->planner->plan_for_goal(goal); +} + void Unit::destroy_state() { if(this->state == nullptr || this->state->is_queued_for_deletion() || !this->state->is_inside_tree()) return; @@ -64,7 +123,8 @@ void Unit::next_action() { if(this->state != nullptr && !this->state->is_queued_for_deletion()) this->destroy_state(); this->state = nullptr; - if(this->current_plan.is_empty()) return; + if(this->current_plan.is_empty()) + return; this->state = this->current_plan.get(0)->get_apply_state(this->world_state); if(state == nullptr) { this->stop_plan(); @@ -79,5 +139,5 @@ void Unit::next_action() { } void Unit::replan_goal() { - this->plan_for_goal(this->current_goal); + this->begin_goal(this->current_goal); } diff --git a/src/unit.hpp b/src/unit.hpp index 9d43017..5d43bb3 100644 --- a/src/unit.hpp +++ b/src/unit.hpp @@ -5,6 +5,7 @@ #include "unit_world_state.hpp" #include "goap/goal.hpp" #include "goap/planner.hpp" +#include "utils/godot_macros.hpp" #include #include #include @@ -12,21 +13,36 @@ namespace gd = godot; +GDENUM(UnitTeam, + Neutral, + Player, + Ally, + Enemy +); class Unit : public gd::CharacterBody3D { GDCLASS(Unit, gd::CharacterBody3D); static void _bind_methods(); public: virtual void _enter_tree() override; - virtual void _process(double delta_time) override; void stop_plan(); - void plan_for_marker(GoalMarker *marker); - void plan_for_goal(gd::Ref goal); + void begin_marker_temporary(GoalMarker *marker); + void begin_goal(gd::Ref goal); + void set_target_goal(gd::Node3D *target, gd::Ref goal); + + void fire_at_target(); + void aim_at(gd::Node3D *node); + void on_death(Unit *killer); UnitWorldState *get_world_state() const; gd::Transform3D get_eye_transform() const; + + UnitTeam get_team() const; + void set_configure_team(int value); + int get_configure_team() const; private: + void set_goal_and_plan(gd::Ref goal); void destroy_state(); void state_finished(); void next_action(); @@ -41,6 +57,8 @@ private: gd::Callable on_state_finished{callable_mp(this, &Unit::state_finished)}; gd::Callable on_plan_failed{callable_mp(this, &Unit::replan_goal)}; + UnitTeam team{UnitTeam::Neutral}; + gd::NavigationAgent3D *agent{nullptr}; UnitWorldState *world_state{nullptr}; goap::Planner *planner{nullptr}; diff --git a/src/unit_world_state.cpp b/src/unit_world_state.cpp index 3731417..f88a376 100644 --- a/src/unit_world_state.cpp +++ b/src/unit_world_state.cpp @@ -1,4 +1,5 @@ #include "unit_world_state.hpp" +#include "entity_health.hpp" #include "unit.hpp" #include "utils/godot_macros.hpp" #include @@ -24,9 +25,25 @@ void UnitWorldState::_enter_tree() { GDGAMEONLY(); } bool UnitWorldState::get_can_see_target() const { - gd::Transform3D const eyes = this->parent_unit->get_eye_transform(); - gd::Transform3D const target = this->target_node->get_global_transform(); - gd::Ref const query{gd::PhysicsRayQueryParameters3D::create(eyes.origin, target.origin, 0x1 | 0x4, {this->target_node})}; + gd::Transform3D const eyes{this->parent_unit->get_eye_transform()}; + // 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("VisionTarget")}; + gd::Vector3 target_position{ + vision_target + ? vision_target->get_global_position() + : this->target_node->get_global_position() + }; + // construct list for ignored objects including target node and parent unit + gd::TypedArray ignore_list{this->parent_unit->get_rid()}; + if(gd::CollisionObject3D *target_collider{gd::Object::cast_to(this->target_node)}) { + ignore_list.push_back(target_collider->get_rid()); + } + // construct raycast query from unit's eye node to vision target. Ignoring parent unit and target if applicable + gd::Ref const query{gd::PhysicsRayQueryParameters3D::create( + eyes.origin, + target_position, + 0x1 | 0x4, ignore_list) + }; return this->parent_unit->get_world_3d()->get_direct_space_state()->intersect_ray(query).is_empty(); } @@ -39,13 +56,20 @@ bool UnitWorldState::get_is_at_target() const { } bool UnitWorldState::get_is_target_dead() const { + if(EntityHealth *health{this->target_node->get_node("EntityHealth")}) + return health->get_current_health() <= 0.f; return false; } void UnitWorldState::set_target_node(gd::Node3D *node) { + if(node == this->target_node) + return; + if(this->target_node != nullptr) { + this->target_node->disconnect("tree_exited", this->target_node_exited_tree.bind(this->target_node)); + } this->target_node = node; if(node != nullptr) - node->connect("tree_exited", callable_mp(this, &UnitWorldState::target_destroyed).bind(node)); + node->connect("tree_exited", this->target_node_exited_tree.bind(node)); this->emit_signal("attention_changed"); } diff --git a/src/unit_world_state.hpp b/src/unit_world_state.hpp index 6656a60..a5bcca0 100644 --- a/src/unit_world_state.hpp +++ b/src/unit_world_state.hpp @@ -21,6 +21,8 @@ public: private: void target_destroyed(gd::Node3D *target); private: + gd::Callable const target_node_exited_tree{callable_mp(this, &UnitWorldState::target_destroyed)}; + Unit *parent_unit{nullptr}; gd::NavigationAgent3D *agent{nullptr}; gd::Node3D *target_node{nullptr};