feat: implemented melee attacks for Tank enemy

This commit is contained in:
Sara 2024-07-16 19:35:55 +02:00
parent 1021920e1f
commit a78bbfd3b5
17 changed files with 369 additions and 268 deletions

View file

@ -1,6 +1,9 @@
[gd_resource type="Goal" format=3 uid="uid://b4i4e34046n44"]
[resource]
desired_state_dict = {
"goal1": true
requirements_dict = {
"is_target_enemy": true
}
desired_state_dict = {
"is_target_dead": true
}

Binary file not shown.

View file

@ -1,145 +1,10 @@
[gd_scene load_steps=9 format=3 uid="uid://ba17jrcaduowj"]
[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)]
}
tracks/1/type = "value"
tracks/1/imported = false
tracks/1/enabled = true
tracks/1/path = NodePath("MeshInstance3D:position")
tracks/1/interp = 1
tracks/1/loop_wrap = true
tracks/1/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"update": 0,
"values": [Vector3(0, 1, 0)]
}
tracks/2/type = "value"
tracks/2/imported = false
tracks/2/enabled = true
tracks/2/path = NodePath("MeshInstance3D:rotation")
tracks/2/interp = 1
tracks/2/loop_wrap = true
tracks/2/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"update": 0,
"values": [Vector3(0, 0, 0)]
}
tracks/3/type = "value"
tracks/3/imported = false
tracks/3/enabled = true
tracks/3/path = NodePath("MeshInstance3D2:rotation")
tracks/3/interp = 1
tracks/3/loop_wrap = true
tracks/3/keys = {
"times": PackedFloat32Array(0),
"transitions": PackedFloat32Array(1),
"update": 0,
"values": [Vector3(0, 0, 0)]
}
[ext_resource type="Goal" uid="uid://b4i4e34046n44" path="res://AI/defeat_enemy_unit.tres" id="1_b1qo1"]
[ext_resource type="AnimationLibrary" uid="uid://crkh5gahl2ci6" path="res://Animation/bean_characters.res" id="2_lrpu6"]
[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="Animation" id="Animation_yj5ao"]
resource_name = "death"
tracks/0/type = "value"
tracks/0/imported = false
tracks/0/enabled = true
tracks/0/path = NodePath("MeshInstance3D:position")
tracks/0/interp = 1
tracks/0/loop_wrap = true
tracks/0/keys = {
"times": PackedFloat32Array(0, 0.1, 1),
"transitions": PackedFloat32Array(1, 1, 1),
"update": 0,
"values": [Vector3(0, 1, 0), Vector3(0, 0.495427, 0.341233), Vector3(0, -0.527121, 0.341233)]
}
tracks/1/type = "value"
tracks/1/imported = false
tracks/1/enabled = true
tracks/1/path = NodePath("MeshInstance3D:rotation")
tracks/1/interp = 1
tracks/1/loop_wrap = true
tracks/1/keys = {
"times": PackedFloat32Array(0, 0.1),
"transitions": PackedFloat32Array(1, 1),
"update": 0,
"values": [Vector3(0, 0, 0), Vector3(1.59169, 0, 0)]
}
tracks/2/type = "value"
tracks/2/imported = false
tracks/2/enabled = true
tracks/2/path = NodePath("MeshInstance3D2:position")
tracks/2/interp = 1
tracks/2/loop_wrap = true
tracks/2/keys = {
"times": PackedFloat32Array(0, 0.1, 1),
"transitions": PackedFloat32Array(1, 1, 1),
"update": 0,
"values": [Vector3(-0.363891, 1.24027, 0.471381), Vector3(-0.363891, 0.0191291, 0.571603), Vector3(-0.363891, -1.00342, 0.571603)]
}
tracks/3/type = "value"
tracks/3/imported = false
tracks/3/enabled = true
tracks/3/path = NodePath("MeshInstance3D2:rotation")
tracks/3/interp = 1
tracks/3/loop_wrap = true
tracks/3/keys = {
"times": PackedFloat32Array(0, 0.1),
"transitions": PackedFloat32Array(1, 1),
"update": 0,
"values": [Vector3(0, 0, 0), Vector3(1.59169, 0, 0)]
}
[sub_resource type="AnimationLibrary" id="AnimationLibrary_3jmw4"]
_data = {
"RESET": SubResource("Animation_va30j"),
"death": SubResource("Animation_yj5ao"),
"fire_weapon": SubResource("Animation_opwhc")
}
[sub_resource type="SphereShape3D" id="SphereShape3D_5pqvg"]
radius = 7.38627
[sub_resource type="SphereShape3D" id="SphereShape3D_drlm2"]
radius = 1.0
@ -158,17 +23,36 @@ configure_team = 3
collision_layer = 6
collision_mask = 0
[node name="ActorWorldState" type="UnitWorldState" parent="."]
[node name="ActorWorldState" type="EnemyWorldState" parent="."]
editor_available_goals = [ExtResource("1_b1qo1")]
unique_name_in_owner = true
[node name="Planner" type="Planner" parent="."]
actions_inspector = [3, 2, 4]
unique_name_in_owner = true
[node name="EntityHealth" type="EntityHealth" parent="."]
unique_name_in_owner = true
[node name="NavigationAgent3D" type="NavigationAgent3D" parent="."]
unique_name_in_owner = true
path_height_offset = 0.5
[node name="AwarenessArea" type="Area3D" parent="."]
unique_name_in_owner = true
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.1216, 3.55442)
collision_layer = 0
collision_mask = 2
input_ray_pickable = false
[node name="CollisionShape3D" type="CollisionShape3D" parent="AwarenessArea"]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 2.36036)
shape = SubResource("SphereShape3D_5pqvg")
[node name="AnimationPlayer" type="AnimationPlayer" parent="."]
unique_name_in_owner = true
libraries = {
"": SubResource("AnimationLibrary_3jmw4")
"": ExtResource("2_lrpu6")
}
[node name="Eyes" type="Node3D" parent="."]
@ -179,7 +63,7 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.4512, 0)
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.31501, 0)
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0)
transform = Transform3D(1, 0, 0, 0, 0.999222, -0.0394342, 0, 0.0394342, 0.999222, 0, 1.01253, 0.443459)
shape = SubResource("SphereShape3D_drlm2")
[node name="MeshInstance3D" type="MeshInstance3D" parent="."]

View file

@ -1,57 +1,9 @@
[gd_scene load_steps=8 format=3 uid="uid://pme230qx1377"]
[gd_scene load_steps=6 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)]
}
[ext_resource type="AnimationLibrary" uid="uid://crkh5gahl2ci6" path="res://Animation/bean_characters.res" id="1_70627"]
[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="SphereShape3D" id="SphereShape3D_drlm2"]
radius = 1.0
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_n4q15"]
albedo_color = Color(0.407843, 1, 0.447059, 1)
@ -59,9 +11,6 @@ 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)
@ -71,19 +20,24 @@ collision_layer = 6
collision_mask = 0
[node name="ActorWorldState" type="UnitWorldState" parent="."]
unique_name_in_owner = true
[node name="Planner" type="Planner" parent="."]
actions_inspector = [0, 1, 2]
unique_name_in_owner = true
[node name="EntityHealth" type="EntityHealth" parent="."]
unique_name_in_owner = true
[node name="NavigationAgent3D" type="NavigationAgent3D" parent="."]
unique_name_in_owner = true
path_height_offset = 0.5
debug_enabled = true
[node name="AnimationPlayer" type="AnimationPlayer" parent="."]
unique_name_in_owner = true
libraries = {
"": SubResource("AnimationLibrary_3jmw4")
"": ExtResource("1_70627")
}
[node name="Eyes" type="Node3D" parent="."]
@ -93,14 +47,14 @@ 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="MeshInstance3D" type="MeshInstance3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0)
mesh = SubResource("CapsuleMesh_5r0b3")
[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")

105
src/enemy_world_state.cpp Normal file
View file

@ -0,0 +1,105 @@
#include "enemy_world_state.hpp"
#include "entity_health.hpp"
#include "goap/goal.hpp"
#include "godot_cpp/variant/utility_functions.hpp"
#include "unit.hpp"
#include "utils/godot_macros.hpp"
#include <cmath>
void EnemyWorldState::_bind_methods() {
#define CLASSNAME EnemyWorldState
GDPROPERTY_HINTED(editor_available_goals,
gd::Variant::ARRAY,
gd::PROPERTY_HINT_ARRAY_TYPE,
GDRESOURCETYPE("Goal"));
}
void EnemyWorldState::_ready() {
this->awareness_area = this->get_node<gd::Area3D>("%AwarenessArea");
this->awareness_area->connect("body_entered", callable_mp(this, &EnemyWorldState::on_awareness_entered));
this->awareness_area->connect("body_exited", callable_mp(this, &EnemyWorldState::on_awareness_exited));
this->health = this->get_node<EntityHealth>("%EntityHealth");
if(this->health == nullptr)
this->health->connect("damage", callable_mp(this, &EnemyWorldState::on_damaged));
}
void EnemyWorldState::on_awareness_entered(gd::Node3D *node) {
gd::UtilityFunctions::print("1) object entered awareness");
Unit *unit{gd::Object::cast_to<Unit>(node)};
if(unit == nullptr) return;
if(unit == this->parent_unit) return;
gd::UtilityFunctions::print("2) object was Unit");
this->known_enemies.push_back(unit);
this->try_set_target(this->select_target_from_known());
}
void EnemyWorldState::on_awareness_exited(gd::Node3D *node) {
Unit *unit{gd::Object::cast_to<Unit>(node)};
this->known_enemies.erase(unit);
}
void EnemyWorldState::on_damaged(int remaining, int delta, Unit *source) {
float highest_priority{0.f};
Unit *highest_priority_unit{this->select_target_from_known_with_priority(&highest_priority)};
float const priority{this->calculate_priority(source)};
if(priority >= highest_priority)
this->try_set_target(source);
}
Unit *EnemyWorldState::select_target_from_known_with_priority(float *out_priority) {
Unit *out{nullptr};
*out_priority = -INFINITY;
for(Unit *unit : this->known_enemies) {
float const priority{this->calculate_priority(unit)};
if(priority > *out_priority) {
out = unit;
*out_priority = priority;
}
}
return out;
}
Unit *EnemyWorldState::select_target_from_known() {
float dummy{};
return this->select_target_from_known_with_priority(&dummy);
}
float EnemyWorldState::calculate_priority(Unit *target) {
return target->get_global_position().distance_squared_to(this->parent_unit->get_global_position());
}
gd::Ref<goap::Goal> EnemyWorldState::get_goal_for_target(Unit *unit) {
gd::Node3D *store{this->target_node};
this->target_node = unit;
for(gd::Ref<goap::Goal> const &goal : this->available_goals) {
if(goal->check_requirements_met(this)) {
this->target_node = store;
return goal;
}
}
this->target_node = store;
return nullptr;
}
void EnemyWorldState::try_set_target(Unit *unit) {
gd::UtilityFunctions::print("3) selecting goal");
gd::Ref<goap::Goal> goal{this->get_goal_for_target(unit)};
if(!goal.is_valid()) return;
gd::UtilityFunctions::print("4) selected goal ", goal->get_path());
this->parent_unit->set_target_goal(unit, goal);
}
void EnemyWorldState::set_editor_available_goals(gd::Array array) {
this->available_goals.clear();
while(!array.is_empty()) {
gd::Ref<goap::Goal> goal{array.pop_back()};
this->available_goals.push_back(goal);
}
}
gd::Array EnemyWorldState::get_editor_available_goals() const {
gd::Array a{};
for(gd::Ref<goap::Goal> const &goal : this->available_goals)
a.push_back(goal);
return a;
}

44
src/enemy_world_state.hpp Normal file
View file

@ -0,0 +1,44 @@
#ifndef ENEMY_WORLD_STATE_HPP
#define ENEMY_WORLD_STATE_HPP
#include "unit_world_state.hpp"
#include "goap/goal.hpp"
#include <godot_cpp/classes/area3d.hpp>
#include <godot_cpp/templates/vector.hpp>
#include <godot_cpp/variant/array.hpp>
namespace gd = godot;
class EntityHealth;
class EnemyWorldState : public UnitWorldState {
GDCLASS(EnemyWorldState, UnitWorldState);
static void _bind_methods();
public:
virtual void _ready() override;
void on_awareness_entered(gd::Node3D *node);
void on_awareness_exited(gd::Node3D *node);
void on_damaged(int remaining, int delta, Unit *source);
/*! Select the target with the highest priority and return it.
* Assigns the priority of found target to out_priority.
* \param out_priority Assigned to highest found priority, cannot be nullptr.
*/
Unit *select_target_from_known_with_priority(float *out_priority);
//! Shorthand for select_target_from_known_with_priority(nullptr)
Unit *select_target_from_known();
private:
float calculate_priority(Unit *target);
gd::Ref<goap::Goal> get_goal_for_target(Unit *unit);
void try_set_target(Unit *target);
void set_editor_available_goals(gd::Array array);
gd::Array get_editor_available_goals() const;
private:
gd::Vector<gd::Ref<goap::Goal>> available_goals{};
gd::Vector<Unit *> known_enemies{};
gd::Area3D *awareness_area{nullptr};
EntityHealth *health{nullptr};
};
#endif // !ENEMY_WORLD_STATE_HPP

View file

@ -4,9 +4,9 @@
void EntityHealth::_bind_methods() {
#define CLASSNAME EntityHealth
GDPROPERTY(max_health, gd::Variant::INT);
GDPROPERTY(injury_max, gd::Variant::INT);
GDPROPERTY(wounds_max, gd::Variant::INT);
gd::PropertyInfo const source_arg{};
GDSIGNAL("damage",
gd::PropertyInfo(gd::Variant::INT, "remaining"),
gd::PropertyInfo(gd::Variant::INT, "change"),
@ -22,39 +22,48 @@ void EntityHealth::_bind_methods() {
}
void EntityHealth::_enter_tree() {
this->current_health = this->max_health;
this->emit_signal("damage", this->current_health, 0, nullptr);
this->injury_current = this->injury_max;
this->wounds_current = this->wounds_max;
this->emit_signal("damage", this->injury_current, 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->injury_current -= amount;
this->emit_signal("damage", this->injury_current, amount, source);
this->emit_signal("health_changed", this->injury_current, -amount);
if(this->injury_current <= 0) {
this->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);
this->injury_current = gd::Math::min(this->injury_max, this->injury_current + amount);
this->emit_signal("heal", this->injury_current, amount, source);
this->emit_signal("health_changed", this->injury_current, amount);
}
void EntityHealth::set_max_health(int max_health) {
this->max_health = max_health;
void EntityHealth::set_injury_max(int max_health) {
this->injury_max = max_health;
}
int EntityHealth::get_max_health() const {
return this->max_health;
int EntityHealth::get_injury_max() const {
return this->injury_max;
}
void EntityHealth::set_current_health(int current_health) {
this->current_health = current_health;
int EntityHealth::get_injury_current() const {
return this->injury_current;
}
int EntityHealth::get_current_health() const {
return this->current_health;
void EntityHealth::set_wounds_max(int max_health) {
this->wounds_max = max_health;
}
int EntityHealth::get_wounds_max() const {
return this->wounds_max;
}
int EntityHealth::get_wounds_current() const {
return this->wounds_current;
}

View file

@ -16,13 +16,19 @@ public:
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;
void set_injury_max(int max_health);
int get_injury_max() const;
int get_injury_current() const;
void set_wounds_max(int max_health);
int get_wounds_max() const;
int get_wounds_current() const;
private:
int max_health{100};
int current_health{0};
// long term health
int injury_max{10};
int injury_current{0};
// short term health
int wounds_max{10};
int wounds_current{0};
};
#endif // !RTS_ENTITY_HEALTH_HPP

View file

@ -1,4 +1,5 @@
#include "register_types.h"
#include "enemy_world_state.hpp"
#include "entity_health.hpp"
#include "goap/state.hpp"
#include "rts_actions.hpp"
@ -32,6 +33,8 @@ void initialize_gdextension_types(ModuleInitializationLevel p_level)
goap::ActionDB::register_action<MoveToTarget>();
goap::ActionDB::register_action<FireAtTarget>();
goap::ActionDB::register_action<FindTarget>();
goap::ActionDB::register_action<GetInMeleeRange>();
goap::ActionDB::register_action<MeleeAttack>();
ClassDB::register_class<goap::ActorWorldState>();
ClassDB::register_class<goap::Goal>();
@ -43,6 +46,7 @@ void initialize_gdextension_types(ModuleInitializationLevel p_level)
ClassDB::register_class<Activate>();
ClassDB::register_class<UnitWorldState>();
ClassDB::register_class<EnemyWorldState>();
ClassDB::register_class<GoalMarker>();
ClassDB::register_class<Unit>();
ClassDB::register_class<RTSGameMode>();

View file

@ -7,16 +7,16 @@
MoveToTarget::MoveToTarget()
: Action() {
effects.insert("is_at_target", true);
this->effects.insert("is_at_target", true);
}
goap::State *MoveToTarget::get_apply_state(goap::ActorWorldState *context) const {
gd::Node3D *target = gd::Object::cast_to<gd::Node3D>(context->get_world_property("target_node"));
gd::Node3D *target{gd::Object::cast_to<gd::Node3D>(context->get_world_property("target_node"))};
if(target == nullptr) {
gd::UtilityFunctions::push_warning("Failed to get target node of action ", get_static_class());
return nullptr;
} else {
MoveTo *state = this->create_state<MoveTo>();
MoveTo *state{this->create_state<MoveTo>()};
state->target_node = target;
return state;
}
@ -24,24 +24,50 @@ goap::State *MoveToTarget::get_apply_state(goap::ActorWorldState *context) const
FireAtTarget::FireAtTarget()
: Action() {
effects.insert("is_target_dead", true);
required.insert("can_see_target", true);
this->effects.insert("is_target_dead", true);
this->required.insert("can_see_target", true);
}
goap::State *FireAtTarget::get_apply_state(goap::ActorWorldState *context) const {
Animate *state = this->create_state<Animate>();
Animate *state{this->create_state<Animate>()};
state->animation = "fire_weapon";
return state;
}
FindTarget::FindTarget()
: Action() {
effects.insert("can_see_target", true);
this->effects.insert("can_see_target", true);
}
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 = this->create_state<MoveTo>();
gd::Node3D *target{gd::Object::cast_to<gd::Node3D>(context->get_world_property("target_node"))};
MoveTo *state{this->create_state<MoveTo>()};
state->target_node = target;
return state;
}
GetInMeleeRange::GetInMeleeRange()
: Action() {
this->effects.insert("is_in_melee_range", true);
this->required.insert("can_see_target", true);
}
goap::State *GetInMeleeRange::get_apply_state(goap::ActorWorldState *context) const {
gd::Node3D *target{gd::Object::cast_to<gd::Node3D>(context->get_world_property("target_node"))};
MoveTo *state{this->create_state<MoveTo>()};
state->target_node = target;
return state;
}
MeleeAttack::MeleeAttack()
: Action() {
this->effects.insert("is_target_dead", true);
this->required.insert("can_see_target", true);
this->required.insert("is_in_melee_range", true);
}
goap::State *MeleeAttack::get_apply_state(goap::ActorWorldState *context) const {
Animate *state{this->create_state<Animate>()};
state->animation = "melee_attack";
return state;
}

View file

@ -25,4 +25,18 @@ public:
virtual goap::State *get_apply_state(goap::ActorWorldState *context) const override;
};
class GetInMeleeRange : public goap::Action {
GOAP_ACTION(GetInMeleeRange);
public:
GetInMeleeRange();
virtual goap::State *get_apply_state(goap::ActorWorldState *context) const override;
};
class MeleeAttack : public goap::Action {
GOAP_ACTION(MeleeAttack);
public:
MeleeAttack();
virtual goap::State *get_apply_state(goap::ActorWorldState *context) const override;
};
#endif // !RTS_ACTIONS_HPP

19
src/tank_unit.cpp Normal file
View file

@ -0,0 +1,19 @@
#include "tank_unit.hpp"
#include "entity_health.hpp"
void TankUnit::_bind_methods() {}
void TankUnit::use_weapon() {
gd::Node3D *target{this->world_state->get_target_node()};
if(target == nullptr)
return;
if(!this->world_state->get_can_see_target())
return;
if(1.f < target->get_global_position().distance_squared_to(this->get_global_position()))
return;
this->aim_at(target);
EntityHealth *health = target->get_node<EntityHealth>("EntityHealth");
if(health == nullptr)
return;
health->damaged_by(2, this);
}

13
src/tank_unit.hpp Normal file
View file

@ -0,0 +1,13 @@
#ifndef TANK_UNIT_HPP
#define TANK_UNIT_HPP
#include "unit.hpp"
class TankUnit : public Unit {
GDCLASS(TankUnit, Unit);
static void _bind_methods();
public:
virtual void use_weapon() override;
};
#endif // !TANK_UNIT_HPP

View file

@ -9,18 +9,18 @@
void Unit::_bind_methods() {
#define CLASSNAME Unit
GDSIGNAL("goal_finished");
GDSIGNAL("plan_failed");
GDPROPERTY_HINTED(configure_team, gd::Variant::INT, gd::PROPERTY_HINT_ENUM, UnitTeam::get_property_hint());
GDFUNCTION(fire_at_target);
GDFUNCTION(use_weapon);
}
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->agent = this->get_node<gd::NavigationAgent3D>("%NavigationAgent3D");
this->planner = this->get_node<goap::Planner>("%Planner");
this->world_state = this->get_node<UnitWorldState>("%ActorWorldState");
this->world_state->connect("attention_changed", callable_mp(this, &Unit::stop_plan));
this->anim_player = this->get_node<gd::AnimationPlayer>("AnimationPlayer");
EntityHealth *health{this->get_node<EntityHealth>("EntityHealth")};
this->anim_player = this->get_node<gd::AnimationPlayer>("%AnimationPlayer");
EntityHealth *health{this->get_node<EntityHealth>("%EntityHealth")};
health->connect("death", callable_mp(this, &Unit::on_death));
}
@ -30,11 +30,13 @@ void Unit::stop_plan() {
if(this->state && !this->state->is_queued_for_deletion())
this->destroy_state();
this->state = nullptr;
this->emit_signal("plan_failed");
}
void Unit::begin_marker_temporary(GoalMarker *marker) {
this->destroy_state();
this->world_state->set_target_node(marker);
this->destroy_state();
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
@ -58,15 +60,15 @@ void Unit::begin_goal(gd::Ref<goap::Goal> goal) {
this->next_action();
}
void Unit::fire_at_target() {
void Unit::use_weapon() {
gd::Node3D *target{this->world_state->get_target_node()};
if(!target)
if(target == nullptr)
return;
this->aim_at(target);
EntityHealth *health = target->get_node<EntityHealth>("EntityHealth");
if(!health)
EntityHealth *health{target->get_node<EntityHealth>("EntityHealth")};
if(health == nullptr)
return;
health->damaged_by(10, this);
health->damaged_by(1, this);
}
void Unit::aim_at(gd::Node3D *target) {
@ -86,10 +88,6 @@ UnitWorldState *Unit::get_world_state() const {
return this->world_state;
}
gd::Transform3D Unit::get_eye_transform() const {
return this->eyes->get_global_transform();
}
UnitTeam Unit::get_team() const {
return this->team;
}
@ -105,6 +103,9 @@ int Unit::get_configure_team() const {
void Unit::set_goal_and_plan(gd::Ref<goap::Goal> goal) {
this->current_goal = goal;
this->current_plan = this->planner->plan_for_goal(goal);
if(this->current_plan.is_empty()) {
this->current_goal.unref();
}
}
void Unit::destroy_state() {

View file

@ -32,12 +32,11 @@ public:
void begin_goal(gd::Ref<goap::Goal> goal);
void set_target_goal(gd::Node3D *target, gd::Ref<goap::Goal> goal);
void fire_at_target();
virtual void use_weapon();
void aim_at(gd::Node3D *node);
void on_death(Unit *damage_source);
UnitWorldState *get_world_state() const;
gd::Transform3D get_eye_transform() const;
UnitTeam get_team() const;
void set_configure_team(int value);
@ -49,15 +48,13 @@ private:
void next_action();
void replan_goal();
private:
gd::Callable on_state_finished{callable_mp(this, &Unit::state_finished)};
gd::Callable on_plan_failed{callable_mp(this, &Unit::replan_goal)};
protected:
goap::Plan current_plan{};
gd::Ref<goap::Goal> current_goal{};
goap::State *state{nullptr};
gd::Node3D *eyes{nullptr};
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};

View file

@ -15,17 +15,20 @@ void UnitWorldState::_bind_methods() {
GDFUNCTION(get_is_target_dead);
GDFUNCTION(get_is_at_target);
GDFUNCTION(get_target_node);
GDFUNCTION(get_is_target_enemy);
GDFUNCTION(get_is_in_melee_range);
}
void UnitWorldState::_enter_tree() { GDGAMEONLY();
this->parent_unit = gd::Object::cast_to<Unit>(this->get_parent());
this->agent = this->get_node<gd::NavigationAgent3D>("../NavigationAgent3D");
this->agent = this->get_node<gd::NavigationAgent3D>("%NavigationAgent3D");
this->eye_location = this->get_node<gd::Node3D>("%Eyes");
if(this->parent_unit == nullptr)
gd::UtilityFunctions::push_warning("UnitWorldState needs to be a child node of a Unit");
}
bool UnitWorldState::get_can_see_target() const {
gd::Transform3D const eyes{this->parent_unit->get_eye_transform()};
gd::Transform3D const eyes{this->eye_location->get_global_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{
@ -56,13 +59,28 @@ 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 health->get_injury_current() <= 0.f;
return false;
}
bool UnitWorldState::get_is_target_unit() const {
return gd::ClassDB::is_parent_class(this->target_node->get_class(), "Unit");
}
bool UnitWorldState::get_is_target_enemy() const {
Unit *unit{gd::Object::cast_to<Unit>(this->target_node)};
return unit != nullptr
&& unit->get_team() != UnitTeam::Neutral
&& unit->get_team() != this->parent_unit->get_team();
}
bool UnitWorldState::get_is_in_melee_range() const {
return this->target_node != nullptr
&& this->target_node->get_global_position()
.distance_squared_to(this->parent_unit->get_global_position()) <= 4.f;
}
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;

View file

@ -15,6 +15,9 @@ public:
bool get_can_see_target() const;
bool get_is_at_target() const;
bool get_is_target_dead() const;
bool get_is_target_unit() const;
bool get_is_target_enemy() const;
bool get_is_in_melee_range() const;
void set_target_node(gd::Node3D *node);
gd::Node3D *get_target_node() const;
@ -22,10 +25,11 @@ private:
void target_destroyed(gd::Node3D *target);
private:
gd::Callable const target_node_exited_tree{callable_mp(this, &UnitWorldState::target_destroyed)};
protected:
Unit *parent_unit{nullptr};
gd::NavigationAgent3D *agent{nullptr};
gd::Node3D *target_node{nullptr};
gd::Node3D *eye_location{nullptr};
};
#endif // !UNIT_WORLD_STATE_HPP