feat: implemented fire at target action

This commit is contained in:
Sara 2024-06-25 21:16:35 +02:00
parent ffafcac23a
commit 7cf5b128ff
24 changed files with 569 additions and 111 deletions

View file

@ -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")

View file

@ -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)

View file

@ -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")

View file

@ -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")

View file

@ -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)

60
src/entity_health.cpp Normal file
View file

@ -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;
}

28
src/entity_health.hpp Normal file
View file

@ -0,0 +1,28 @@
#ifndef RTS_ENTITY_HEALTH_HPP
#define RTS_ENTITY_HEALTH_HPP
#include <godot_cpp/classes/node.hpp>
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

View file

@ -2,6 +2,7 @@
#define GOAP_ACTION_HPP
#include "actor_world_state.hpp"
#include "godot_cpp/variant/utility_functions.hpp"
#include "state.hpp"
#include <godot_cpp/variant/string.hpp>
#include <godot_cpp/templates/vector.hpp>
@ -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;

View file

@ -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{};
}

View file

@ -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<Action *> 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.

View file

@ -9,6 +9,12 @@ void Goal::_bind_methods() {
GDPROPERTY(desired_state_dict, gd::Variant::DICTIONARY);
}
gd::Ref<Goal> Goal::create(gd::StringName key, gd::Variant value) {
gd::Ref<Goal> 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<gd::StringName, gd::Variant>(dict);
}

View file

@ -11,6 +11,7 @@ class Goal : public gd::Resource {
GDCLASS(Goal, gd::Resource);
static void _bind_methods();
public:
static gd::Ref<Goal> 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);

View file

@ -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> 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<Action const *> &Planner::get_actions() {
return this->actions;
}
gd::Vector<Action const *> Planner::get_neighbours(WorldStateNode const &from) const {
gd::Vector<Action const *> retval{};
for(Action const *action : this->actions) {

View file

@ -58,8 +58,9 @@ public:
//! Compute a plan to achieve the passed `Goal`.
Plan plan_for_goal(gd::Ref<Goal> 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<Action const*> &get_actions();
private:
//! \returns A vector of all actions that satisfy any of the requirements in `unsatisfied`
gd::Vector<Action const *> get_neighbours(WorldStateNode const &from) const;

View file

@ -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<Unit>();
ClassDB::register_class<RTSGameMode>();
ClassDB::register_class<RTSPlayer>();
ClassDB::register_class<EntityHealth>();
}
extern "C"

View file

@ -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<Animate>();
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<gd::Node3D>(context->get_world_property("target_node"));
MoveTo *state = memnew(MoveTo);
MoveTo *state = this->create_state<MoveTo>();
state->target_node = target;
return state;
}

View file

@ -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 <godot_cpp/classes/packed_scene.hpp>
#include <godot_cpp/classes/physics_direct_space_state3d.hpp>
@ -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<gd::Node>(result["collider"])->get_path());
else
gd::UtilityFunctions::print("No object found");
return result.is_empty() ? nullptr : Object::cast_to<gd::Node3D>(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<gd::Node3D>(dict["collider"]);
GoalMarker *marker = gd::Object::cast_to<GoalMarker>(hit);
bool const is_temporary_marker = marker == nullptr;
if(is_temporary_marker) {
marker = gd::Object::cast_to<GoalMarker>(this->ground_marker_scene->instantiate());
if(!this->order_activate_object(gd::Object::cast_to<Node3D>(dict["collider"]))) {
GoalMarker *marker = gd::Object::cast_to<GoalMarker>(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<GoalMarker>(node)) {
this->selected_unit->set_target_goal(marker, marker->get_goal());
return true;
}
Unit *unit{gd::Object::cast_to<Unit>(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<gd::InputEvent> event, float value) {
@ -125,11 +136,13 @@ void RTSPlayer::on_mouse_horizontal(gd::Ref<gd::InputEvent> 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<gd::InputEvent> 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<gd::InputEvent> event, float value) {
@ -145,15 +158,14 @@ void RTSPlayer::on_rotate_horizontal(gd::Ref<gd::InputEvent> event, float value)
}
void RTSPlayer::on_rclick(gd::Ref<gd::InputEvent> 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<gd::InputEvent> 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<gd::PackedScene> scene) {
this->ground_marker_scene = scene;
}

View file

@ -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 <cstdint>
#include <godot_cpp/classes/input_event.hpp>
#include <godot_cpp/classes/node3d.hpp>
#include <godot_cpp/classes/packed_scene.hpp>
#include <godot_cpp/classes/camera3d.hpp>
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<gd::PackedScene> scene);
gd::Ref<gd::PackedScene> 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<gd::PackedScene> 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};

View file

@ -1,17 +1,15 @@
#include "rts_states.hpp"
#include "godot_cpp/variant/utility_functions.hpp"
#include <godot_cpp/variant/utility_functions.hpp>
void MoveTo::_bind_methods() {}
void MoveTo::_ready() {
gd::UtilityFunctions::print("_ready", this->get_path());
this->agent = this->get_node<gd::NavigationAgent3D>("../NavigationAgent3D");
this->agent->set_target_position(this->target_node->get_global_position());
this->parent_node3d = Object::cast_to<gd::Node3D>(this->get_parent());
}
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<gd::AnimationPlayer>("../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();
}

View file

@ -2,6 +2,7 @@
#define RTS_STATES_HPP
#include "goap/state.hpp"
#include <godot_cpp/classes/animation_player.hpp>
#include <godot_cpp/classes/navigation_agent3d.hpp>
/*! 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

View file

@ -1,4 +1,5 @@
#include "unit.hpp"
#include "entity_health.hpp"
#include "goap/goal.hpp"
#include "utils/godot_macros.hpp"
#include <godot_cpp/classes/navigation_agent3d.hpp>
@ -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<gd::NavigationAgent3D>("NavigationAgent3D");
this->planner = this->get_node<goap::Planner>("Planner");
this->world_state = this->get_node<UnitWorldState>("ActorWorldState");
this->eyes = this->get_node<gd::Node3D>("%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>("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<goap::Goal> goal) {
this->current_plan = this->planner->plan_for_goal(goal);
this->current_goal = goal;
void Unit::set_target_goal(gd::Node3D *target, gd::Ref<goap::Goal> goal) {
this->destroy_state();
this->world_state->set_target_node(target);
this->begin_goal(goal);
}
void Unit::begin_goal(gd::Ref<goap::Goal> 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>("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<goap::Goal> 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);
}

View file

@ -5,6 +5,7 @@
#include "unit_world_state.hpp"
#include "goap/goal.hpp"
#include "goap/planner.hpp"
#include "utils/godot_macros.hpp"
#include <godot_cpp/classes/character_body3d.hpp>
#include <godot_cpp/classes/navigation_agent3d.hpp>
#include <godot_cpp/classes/node3d.hpp>
@ -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<goap::Goal> goal);
void begin_marker_temporary(GoalMarker *marker);
void begin_goal(gd::Ref<goap::Goal> goal);
void set_target_goal(gd::Node3D *target, gd::Ref<goap::Goal> 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<goap::Goal> 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};

View file

@ -1,4 +1,5 @@
#include "unit_world_state.hpp"
#include "entity_health.hpp"
#include "unit.hpp"
#include "utils/godot_macros.hpp"
#include <godot_cpp/classes/physics_ray_query_parameters3d.hpp>
@ -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<gd::PhysicsRayQueryParameters3D> 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<gd::Node3D>("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<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());
}
// 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)
};
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>("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");
}

View file

@ -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};