diff --git a/godot/AI/new_goal.tres b/godot/AI/new_goal.tres new file mode 100644 index 0000000..b16a917 --- /dev/null +++ b/godot/AI/new_goal.tres @@ -0,0 +1,6 @@ +[gd_resource type="Goal" format=3 uid="uid://b4i4e34046n44"] + +[resource] +desired_state_dict = { +"goal1": true +} diff --git a/godot/GameObjects/ground_goal_marker.tscn b/godot/GameObjects/ground_goal_marker.tscn new file mode 100644 index 0000000..02c0b71 --- /dev/null +++ b/godot/GameObjects/ground_goal_marker.tscn @@ -0,0 +1,24 @@ +[gd_scene load_steps=4 format=3 uid="uid://btnlq0xajbfit"] + +[sub_resource type="Goal" id="Goal_nmxy4"] +desired_state_dict = { +"is_at_target": true +} + +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_t1mgt"] +albedo_color = Color(0.596078, 0.745098, 0.635294, 1) + +[sub_resource type="CylinderMesh" id="CylinderMesh_4i5c2"] +material = SubResource("StandardMaterial3D_t1mgt") +height = 0.1 + +[node name="GroundGoalMarker" type="GoalMarker"] +goal = SubResource("Goal_nmxy4") +transform = Transform3D(-4.37114e-08, 0, -1, 0, 1, 0, 1, 0, -4.37114e-08, -2.38494, 0, -2.58246) +disable_mode = 1 +collision_layer = 0 +collision_mask = 0 +input_ray_pickable = false + +[node name="MeshInstance3D" type="MeshInstance3D" parent="."] +mesh = SubResource("CylinderMesh_4i5c2") diff --git a/godot/GameObjects/player_camera.tscn b/godot/GameObjects/player_camera.tscn index 1fb9bdc..ce7a9e9 100644 --- a/godot/GameObjects/player_camera.tscn +++ b/godot/GameObjects/player_camera.tscn @@ -1,9 +1,12 @@ -[gd_scene load_steps=2 format=3 uid="uid://bmm2quy62exfr"] +[gd_scene load_steps=3 format=3 uid="uid://bmm2quy62exfr"] + +[ext_resource type="PackedScene" uid="uid://btnlq0xajbfit" path="res://GameObjects/ground_goal_marker.tscn" id="1_t46fa"] [sub_resource type="CylinderMesh" id="CylinderMesh_hd6t5"] height = 0.1 [node name="PlayerCamera" type="RTSPlayer"] +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) diff --git a/godot/GameObjects/unit.tscn b/godot/GameObjects/unit.tscn new file mode 100644 index 0000000..33831dd --- /dev/null +++ b/godot/GameObjects/unit.tscn @@ -0,0 +1,32 @@ +[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 e156914..4c38fca 100644 --- a/godot/Levels/test_level.tscn +++ b/godot/Levels/test_level.tscn @@ -1,7 +1,17 @@ -[gd_scene load_steps=4 format=3 uid="uid://c62s1jmtgajjk"] +[gd_scene load_steps=7 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"] + +[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)] +geometry_parsed_geometry_type = 1 +geometry_collision_mask = 4294967289 + +[sub_resource type="BoxShape3D" id="BoxShape3D_ubt1u"] +size = Vector3(10, 0.1, 10) [sub_resource type="PlaneMesh" id="PlaneMesh_hohcb"] size = Vector2(10, 10) @@ -12,10 +22,23 @@ game_mode_prototype = ExtResource("1_4nchg") [node name="WorldEnvironment" type="WorldEnvironment" parent="."] environment = ExtResource("2_jq6bw") +[node name="NavigationRegion3D" type="NavigationRegion3D" parent="WorldEnvironment"] +navigation_mesh = SubResource("NavigationMesh_8a2j6") + +[node name="StaticBody3D" type="StaticBody3D" parent="WorldEnvironment/NavigationRegion3D"] +collision_layer = 5 + +[node name="CollisionShape3D" type="CollisionShape3D" parent="WorldEnvironment/NavigationRegion3D/StaticBody3D"] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.05, 0) +shape = SubResource("BoxShape3D_ubt1u") + +[node name="MeshInstance3D" type="MeshInstance3D" parent="WorldEnvironment/NavigationRegion3D/StaticBody3D"] +mesh = SubResource("PlaneMesh_hohcb") +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="MeshInstance3D" type="MeshInstance3D" parent="WorldEnvironment"] -mesh = SubResource("PlaneMesh_hohcb") -skeleton = NodePath("../..") +[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) diff --git a/godot/project.godot b/godot/project.godot index 334a1e3..8258e28 100644 --- a/godot/project.godot +++ b/godot/project.godot @@ -62,3 +62,9 @@ rotate_right={ "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":69,"key_label":0,"unicode":101,"echo":false,"script":null) ] } + +[layer_names] + +3d_physics/layer_1="Default" +3d_physics/layer_2="Units" +3d_physics/layer_3="Objects" diff --git a/src/goal_marker.cpp b/src/goal_marker.cpp new file mode 100644 index 0000000..f1da7cb --- /dev/null +++ b/src/goal_marker.cpp @@ -0,0 +1,21 @@ +#include "goal_marker.hpp" +#include "utils/godot_macros.hpp" +#include +#include + +void GoalMarker::_bind_methods() { +#define CLASSNAME GoalMarker + GDPROPERTY_HINTED(goal, gd::Variant::OBJECT, gd::PROPERTY_HINT_RESOURCE_TYPE, "Goal"); +} + +void GoalMarker::destroy_on_forgotten() { + callable_mp(gd::Object::cast_to(this), &gd::Node::queue_free).call_deferred(); +} + +void GoalMarker::set_goal(gd::Ref goal) { + this->goal = goal; +} + +gd::Ref GoalMarker::get_goal() const { + return this->goal; +} diff --git a/src/goal_marker.hpp b/src/goal_marker.hpp new file mode 100644 index 0000000..17a0dc7 --- /dev/null +++ b/src/goal_marker.hpp @@ -0,0 +1,21 @@ +#ifndef GOAL_MARKER_HPP +#define GOAL_MARKER_HPP + +#include "goap/goal.hpp" +#include "godot_cpp/classes/static_body3d.hpp" +#include + +namespace gd = godot; + +class GoalMarker : public gd::StaticBody3D { + GDCLASS(GoalMarker, gd::StaticBody3D); + static void _bind_methods(); +public: + void destroy_on_forgotten(); + void set_goal(gd::Ref goal); + gd::Ref get_goal() const; +private: + gd::Ref goal{}; +}; + +#endif // !GOAL_MARKER_HPP diff --git a/src/goap/action.hpp b/src/goap/action.hpp index b3336f8..9267be3 100644 --- a/src/goap/action.hpp +++ b/src/goap/action.hpp @@ -9,8 +9,8 @@ #define GOAP_ACTION(Name_) \ public: \ - _FORCE_INLINE_ static gd::String get_static_class() { return #Name_; }\ - _FORCE_INLINE_ virtual gd::String get_class() const override { return #Name_; }\ + _FORCE_INLINE_ static gd::StringName get_static_class() { return #Name_; }\ + _FORCE_INLINE_ virtual gd::StringName get_class() const override { return #Name_; }\ private: namespace goap { @@ -23,11 +23,11 @@ typedef int ActionID; class Action { friend class ActionDB; public: - static gd::String get_static_class() { return "Action"; } - virtual gd::String get_class() const { return "Action"; } + static gd::StringName get_static_class() { return "Action"; } + virtual gd::StringName get_class() const { return "Action"; } virtual ~Action(); - virtual State *get_apply_state() const = 0; + virtual State *get_apply_state(ActorWorldState *context) const = 0; bool is_completed(ActorWorldState *context) const; bool is_possible(ActorWorldState *context) const; @@ -38,7 +38,8 @@ public: ActionID get_id() const; protected: Action() = default; - + template + TState *create_state() const; virtual bool procedural_is_possible(ActorWorldState *context) const; virtual bool procedural_is_completed(ActorWorldState *context) const; protected: @@ -49,6 +50,13 @@ protected: private: ActionID id{-1}; }; + +template +TState *Action::create_state() const { + TState *state = memnew(TState); + state->action = this; + return state; +} } #endif // !GOAP_ACTION_HPP diff --git a/src/goap/planner.cpp b/src/goap/planner.cpp index 7a86268..178e5b2 100644 --- a/src/goap/planner.cpp +++ b/src/goap/planner.cpp @@ -1,5 +1,6 @@ #include "planner.hpp" #include "action_db.hpp" +#include "godot_cpp/classes/engine.hpp" #include "godot_cpp/classes/global_constants.hpp" #include "godot_cpp/templates/hashfuncs.hpp" #include "utils/godot_macros.hpp" @@ -106,49 +107,58 @@ Plan Planner::plan_for_goal(gd::Ref goal) { return {}; } gd::Vector open{goal_node}; - NodeMap from{}; - NodeScoreMap best_path_cost{}; - NodeScoreMap heuristic_cost{}; + NodeMap from{}; // mapping nodes to the next node in the plan + NodeScoreMap best_path_cost{}; // map nodes to the cost of the best path found so far + NodeScoreMap heuristic_cost{}; // map nodes to their approximate cost to get to their goal + // insert with the goal node into the cost maps, + // path scored 0 as it's the goal best_path_cost.insert(goal_node, 0.f); heuristic_cost.insert(goal_node, goal_node.requirements_unmet()); WorldStateNode current{goal_node}; + // repeat until current has no traversible edges while(!open.is_empty()) { current = open.get(0); + // return found path when all requirements are met if(current.requirements_unmet() == 0) return this->unroll_plan(current, from); - open.remove_at(0); + open.remove_at(0); // pop current off the top of the open set + // get all actions that contribute to the open requirements of `current` gd::Vector edges{this->get_neighbours(current)}; - gd::UtilityFunctions::print("found ", edges.size(), " possible actions"); for(Action const * action : edges) { + // construct the node produced by traversing edge WorldStateNode node_through{current, action}; + // calculate the cost of getting here from current through action. float const path_cost{best_path_cost.get(current) + 1.f}; - gd::UtilityFunctions::print("hashes ", WorldStateNodeHasher::hash(node_through), " ; ", WorldStateNodeHasher::hash(current)); - gd::UtilityFunctions::print("equiv: ", current == node_through); + // store only if path through `current` represents a new fastest way to get to the new node if(!best_path_cost.has(node_through) || best_path_cost.get(node_through) >= path_cost) { - gd::UtilityFunctions::print("node through action ", action->get_class(), " added to open set"); + // update cheapest route found best_path_cost[node_through] = path_cost; + // approximate cost to goal heuristic_cost[node_through] = path_cost + node_through.requirements_unmet(); + // store path to get to this point from[node_through] = current; - open.erase(node_through); + // (re-) insert node according to priority based on unmet requirements + if(open.has(node_through)) + open.erase(node_through); open.ordered_insert(node_through); - } else { - gd::UtilityFunctions::print("node through action ", action->get_class(), " scores worse than previous at same state. ", path_cost, " vs ", best_path_cost[node_through]); } } } - gd::UtilityFunctions::print("failed to find plan for goal ", goal->get_path()); + gd::UtilityFunctions::push_warning("failed to find plan for goal ", goal->get_path()); return {}; } void Planner::set_actions(gd::Array array) { this->actions.clear(); for(size_t i = 0; i < array.size(); ++i) { - Action const *action = ActionDB::get_action(array[i]); + Action const *action = int(array[i]) < 0 ? nullptr : ActionDB::get_action(array[i]); if(action != nullptr && !this->actions.has(action)) { this->actions.push_back(action); + } else if(gd::Engine::get_singleton()->is_editor_hint() && !this->actions.has(nullptr)) { + this->actions.push_back(nullptr); } } } @@ -156,7 +166,7 @@ void Planner::set_actions(gd::Array array) { gd::Array Planner::get_actions() const { gd::Array array{}; for(Action const *action : this->actions) - array.push_back(action->get_id()); + array.push_back(action == nullptr ? -1 : action->get_id()); return array; } diff --git a/src/goap/planner.hpp b/src/goap/planner.hpp index 9db68f3..a034fe9 100644 --- a/src/goap/planner.hpp +++ b/src/goap/planner.hpp @@ -7,7 +7,6 @@ #include #include #include -#include namespace gd = godot; @@ -25,7 +24,7 @@ struct WorldStateNode { WorldStateNode(WorldStateNode const &last_state, Action const *last_action); ~WorldStateNode() = default; int requirements_unmet() const; - WorldState state{}; + WorldState state{}; // the only part considered in hashing WorldState open_requirements{}; Action const *last_action{nullptr}; ActorWorldState *context; diff --git a/src/goap/state.cpp b/src/goap/state.cpp index 65df71d..6660604 100644 --- a/src/goap/state.cpp +++ b/src/goap/state.cpp @@ -1,32 +1,33 @@ #include "state.hpp" +#include "goap/action.hpp" +#include "goap/actor_world_state.hpp" +#include "utils/godot_macros.hpp" namespace goap { -void State::_bind_methods() {} - -void MoveTo::_bind_methods() {} - -void MoveTo::_enter_tree() { - 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 State::_bind_methods() { +#define CLASSNAME State + GDSIGNAL("end_state"); } -void MoveTo::_exit_tree() { - this->agent->set_target_position(this->parent_node3d->get_global_position()); +void State::_enter_tree() { + this->world_state = this->get_node("../ActorWorldState"); } -void MoveTo::_process(double delta_time) { - gd::Vector3 const pos = this->parent_node3d->get_global_position(); - gd::Vector3 const target = this->agent->get_next_path_position(); - gd::Vector3 const direction = (target - pos).normalized(); - this->parent_node3d->set_global_position(direction * delta_time); - this->parent_node3d->look_at(pos - gd::Vector3{direction.x, 0.f, direction.z}); - - if(this->agent->is_navigation_finished()) - this->queue_free(); +void State::_process(double delta_time) { + if(this->action->is_completed(this->world_state)) { + this->end_state(); + } } -void Activate::_bind_methods() {} - -void Animate::_bind_methods() {} +Action const *State::get_action() const { + return this->action; +} + +void State::_end_state() {} + +void State::end_state() { + this->_end_state(); + this->queue_free(); + this->emit_signal("end_state"); +} } diff --git a/src/goap/state.hpp b/src/goap/state.hpp index 2edb220..96d275a 100644 --- a/src/goap/state.hpp +++ b/src/goap/state.hpp @@ -1,47 +1,29 @@ #ifndef GOAP_STATE_HPP #define GOAP_STATE_HPP -#include "godot_cpp/classes/navigation_agent3d.hpp" +#include "goap/actor_world_state.hpp" #include #include namespace gd = godot; namespace goap { -class State : public gd::Node { - GDCLASS(State, gd::Node); - static void _bind_methods(); -}; +class Action; -/*! Uses navigation to chase the desired target node. - */ -class MoveTo : public State { - GDCLASS(MoveTo, State); +class State : public gd::Node { +friend class Action; + GDCLASS(State, gd::Node); static void _bind_methods(); public: virtual void _enter_tree() override; - virtual void _exit_tree() override; virtual void _process(double delta_time) override; -public: - gd::Node3D *target_node{nullptr}; +protected: + Action const *get_action() const; + virtual void _end_state(); + void end_state(); private: - gd::Node3D *parent_node3d{nullptr}; - gd::NavigationAgent3D *agent{nullptr}; -}; - -class Activate : public State { - GDCLASS(Activate, State); - static void _bind_methods(); -public: - gd::Node3D *target_node{nullptr}; - gd::String animation{}; -}; - -class Animate : public State { - GDCLASS(Animate, State); - static void _bind_methods(); -public: - gd::String animation{}; + ActorWorldState *world_state{nullptr}; + Action const *action{nullptr}; }; } diff --git a/src/register_types.cpp b/src/register_types.cpp index f8200ae..d961c8e 100644 --- a/src/register_types.cpp +++ b/src/register_types.cpp @@ -1,12 +1,16 @@ #include "register_types.h" +#include "goap/state.hpp" +#include "rts_actions.hpp" +#include "rts_game_mode.hpp" +#include "rts_player.hpp" +#include "rts_states.hpp" +#include "unit.hpp" +#include "unit_world_state.hpp" #include "goap/action.hpp" #include "goap/action_db.hpp" #include "goap/actor_world_state.hpp" #include "goap/goal.hpp" #include "goap/planner.hpp" -#include "rts_game_mode.hpp" -#include "rts_player.hpp" -#include "unit.hpp" #include "utils/register_types.hpp" #include #include @@ -24,14 +28,24 @@ void initialize_gdextension_types(ModuleInitializationLevel p_level) // always register actions before classes, // so that ActionDB::get_enum_hint is complete before _bind_methods - - ClassDB::register_class(); - ClassDB::register_class(); - ClassDB::register_class(); + goap::ActionDB::register_action(); + goap::ActionDB::register_action(); + goap::ActionDB::register_action(); ClassDB::register_class(); ClassDB::register_class(); ClassDB::register_class(); + ClassDB::register_class(); + + ClassDB::register_class(); + ClassDB::register_class(); + ClassDB::register_class(); + + ClassDB::register_class(); + 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 new file mode 100644 index 0000000..88cdcc4 --- /dev/null +++ b/src/rts_actions.cpp @@ -0,0 +1,47 @@ +#include "rts_actions.hpp" +#include "rts_states.hpp" +#include "goap/actor_world_state.hpp" +#include "goap/state.hpp" +#include +#include + +MoveToTarget::MoveToTarget() +: Action() { + effects.insert("is_at_target", true); +} + +goap::State *MoveToTarget::get_apply_state(goap::ActorWorldState *context) const { + gd::Node3D *target = gd::Object::cast_to(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(); + state->target_node = target; + return state; + } +} + +FireAtTarget::FireAtTarget() +: Action() { + effects.insert("target_dead", true); + required.insert("can_see_target", true); +} + +goap::State *FireAtTarget::get_apply_state(goap::ActorWorldState *context) const { + Animate *state = memnew(Animate); + state->animation = "fire_weapon"; + return state; +} + +FindTarget::FindTarget() +: Action() { + effects.insert("can_see_target", true); +} + +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); + state->target_node = target; + return state; +} diff --git a/src/rts_actions.hpp b/src/rts_actions.hpp new file mode 100644 index 0000000..8238a8e --- /dev/null +++ b/src/rts_actions.hpp @@ -0,0 +1,28 @@ +#ifndef RTS_ACTIONS_HPP +#define RTS_ACTIONS_HPP + +#include "goap/action.hpp" +#include "goap/actor_world_state.hpp" + +class MoveToTarget : public goap::Action { + GOAP_ACTION(MoveToTarget); +public: + MoveToTarget(); + virtual goap::State *get_apply_state(goap::ActorWorldState *context) const override; +}; + +class FireAtTarget : public goap::Action { + GOAP_ACTION(FireAtTarget); +public: + FireAtTarget(); + virtual goap::State *get_apply_state(goap::ActorWorldState *context) const override; +}; + +class FindTarget : public goap::Action { + GOAP_ACTION(FindTarget); +public: + FindTarget(); + virtual goap::State *get_apply_state(goap::ActorWorldState *context) const override; +}; + +#endif // !RTS_ACTIONS_HPP diff --git a/src/rts_player.cpp b/src/rts_player.cpp index 960b13e..0a0aa9b 100644 --- a/src/rts_player.cpp +++ b/src/rts_player.cpp @@ -1,8 +1,25 @@ #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 +#include +#include +#include +#include #include void RTSPlayer::_bind_methods() { #define CLASSNAME RTSPlayer + GDPROPERTY_HINTED(ground_marker_scene, gd::Variant::OBJECT, gd::PROPERTY_HINT_RESOURCE_TYPE, "PackedScene"); +} + +void RTSPlayer::_ready() { + this->camera = this->get_node("Camera3D"); } void RTSPlayer::_process(double delta_time) { @@ -13,6 +30,7 @@ void RTSPlayer::_process(double delta_time) { 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); this->camera_mouse_rotation = 0.f; + this->process_mouse(); } void RTSPlayer::setup_player_input(utils::PlayerInput *input) { @@ -34,11 +52,74 @@ void RTSPlayer::spawn_at_position(gd::Transform3D const &at) { this->set_global_transform(at); } +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); +} + void RTSPlayer::move(gd::Vector2 motion) { gd::Vector3 pos = this->get_global_position(); this->set_global_position(pos + this->get_forward_direction() * motion.y + this->get_left_direction() * motion.x); } +gd::Dictionary RTSPlayer::raycast_from_cursor(uint32_t layers) const { + gd::Vector3 const origin = this->camera->get_global_position(); + gd::Vector3 const destination = origin + this->cursor_camera_normal * 1000.f; + gd::Ref const query{gd::PhysicsRayQueryParameters3D::create(origin, destination, layers)}; + gd::PhysicsDirectSpaceState3D *state = this->get_world_3d()->get_direct_space_state(); + return state->intersect_ray(query); +} + +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"]); +} + +void RTSPlayer::select_unit_under_cursor() { + Unit *unit = Object::cast_to(this->node_under_cursor(0x2)); + if(unit != nullptr) + this->select_unit(unit); +} + +void RTSPlayer::order_activate_object_under_cursor() { + if(this->selected_unit == nullptr) + return; + 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()); + this->add_child(marker); + marker->set_as_top_level(true); + marker->set_global_position(dict["position"]); + } + 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)); + } +} + +void RTSPlayer::clear_selected_unit() { + if(this->selected_unit != nullptr) { + this->selected_unit->disconnect("tree_exited", this->unit_destroyed_callable()); + this->selected_unit = nullptr; + } +} + +void RTSPlayer::select_unit(Unit *unit) { + this->clear_selected_unit(); + this->selected_unit = unit; + unit->connect("tree_exited", this->unit_destroyed_callable()); +} + void RTSPlayer::on_mouse_horizontal(gd::Ref event, float value) { if(this->mmb_down) this->camera_mouse_motion.x = value; @@ -64,10 +145,18 @@ 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()) + 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()) + this->select_unit_under_cursor(); + this->lmb_down = value != 0.f; + this->lmb_last_change = utils::time_seconds(); } void RTSPlayer::on_mclick(gd::Ref event, float value) { @@ -83,3 +172,23 @@ gd::Vector3 RTSPlayer::get_left_direction() const { gd::Vector3 left = this->get_global_basis().get_column(0); 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; +} + +gd::Ref RTSPlayer::get_ground_marker_scene() const { + return this->ground_marker_scene; +} diff --git a/src/rts_player.hpp b/src/rts_player.hpp index 08c9d6f..0d5316a 100644 --- a/src/rts_player.hpp +++ b/src/rts_player.hpp @@ -1,9 +1,13 @@ #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 namespace gd = godot; @@ -13,13 +17,25 @@ class RTSPlayer : public gd::Node3D, GDCLASS(RTSPlayer, godot::Node3D); static void _bind_methods(); public: + virtual void _ready() override; virtual void _process(double delta_time) override; virtual void setup_player_input(utils::PlayerInput *input) override; virtual Node *to_node() override; virtual void spawn_at_position(gd::Transform3D const &at) override; private: + //! process any changes to mouse position + void process_mouse(); + //! move the camera along the ground void move(gd::Vector2 motion); + gd::Dictionary raycast_from_cursor(uint32_t layers = UINT32_MAX) const; + //! \return the first node from the camera under the cursor + gd::Node3D *node_under_cursor(uint32_t layers = UINT32_MAX) const; + //! add units under the cursor to this->selected_units + void select_unit_under_cursor(); + void order_activate_object_under_cursor(); + void clear_selected_unit(); + void select_unit(Unit *unit); // input functions void on_mouse_horizontal(gd::Ref event, float value); @@ -31,22 +47,39 @@ private: void on_rclick(gd::Ref event, float value); void on_mclick(gd::Ref event, float value); - // getters + // 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: + Unit* selected_unit{nullptr}; + bool mmb_down{false}; + bool lmb_down{false}; + float lmb_last_change{0.f}; bool rmb_down{false}; + float rmb_last_change{0.f}; + 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::Vector3 cursor_camera_normal{0.f, 0.f, 0.f}; + + gd::Camera3D *camera{nullptr}; + + gd::Ref ground_marker_scene{nullptr}; + float const camera_keys_speed{10.f}; float const camera_keys_rotation_speed{1.f}; float const camera_mouse_speed{0.01f}; float const camera_mouse_rotation_speed{-0.003f}; + double const time_to_held{0.1}; }; #endif // !RTS_PLAYER_HPP diff --git a/src/rts_states.cpp b/src/rts_states.cpp new file mode 100644 index 0000000..84abe7e --- /dev/null +++ b/src/rts_states.cpp @@ -0,0 +1,31 @@ +#include "rts_states.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("../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()); +} + +void MoveTo::_process(double delta_time) { + gd::Vector3 const pos = this->parent_node3d->get_global_position(); + gd::Vector3 const target = this->agent->get_next_path_position(); + 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); +} + +void Activate::_bind_methods() {} + +void Animate::_bind_methods() {} diff --git a/src/rts_states.hpp b/src/rts_states.hpp new file mode 100644 index 0000000..1395740 --- /dev/null +++ b/src/rts_states.hpp @@ -0,0 +1,38 @@ +#ifndef RTS_STATES_HPP +#define RTS_STATES_HPP + +#include "goap/state.hpp" +#include + +/*! Uses navigation to chase the desired target node. + */ +class MoveTo : public goap::State { + GDCLASS(MoveTo, goap::State); + static void _bind_methods(); +public: + virtual void _ready() override; + virtual void _end_state() override; + virtual void _process(double delta_time) override; +public: + gd::Node3D *target_node{nullptr}; +private: + gd::Node3D *parent_node3d{nullptr}; + gd::NavigationAgent3D *agent{nullptr}; +}; + +class Activate : public goap::State { + GDCLASS(Activate, goap::State); + static void _bind_methods(); +public: + gd::Node3D *target_node{nullptr}; + gd::String animation{}; +}; + +class Animate : public goap::State { + GDCLASS(Animate, goap::State); + static void _bind_methods(); +public: + gd::String animation{}; +}; + +#endif // !RTS_STATES_HPP diff --git a/src/unit.cpp b/src/unit.cpp index ecffd33..b987728 100644 --- a/src/unit.cpp +++ b/src/unit.cpp @@ -1,8 +1,70 @@ #include "unit.hpp" +#include "goap/goal.hpp" +#include "godot_cpp/variant/callable_method_pointer.hpp" +#include "godot_cpp/variant/utility_functions.hpp" +#include "utils/godot_macros.hpp" +#include void Unit::_bind_methods() { #define CLASSNAME Unit } -void Unit::_enter_tree() { +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->world_state->connect("attention_changed", callable_mp(this, &Unit::stop_plan)); +} + +void Unit::_process(double delta_time) { +} + +void Unit::stop_plan() { + if(this->is_queued_for_deletion()) + return; + this->current_plan.clear(); + if(this->state && !this->state->is_queued_for_deletion()) + this->destroy_state(); + this->state = nullptr; +} + +void Unit::plan_for_marker(GoalMarker *marker) { + gd::UtilityFunctions::print("plan_for_marker on ", this->get_path()); + this->destroy_state(); + this->world_state->set_target_node(marker); + this->plan_for_goal(marker->get_goal()); +} + +void Unit::plan_for_goal(gd::Ref goal) { + gd::UtilityFunctions::print("plan_for_goal on ", this->get_path()); + this->current_plan = this->planner->plan_for_goal(goal); + this->next_action(); +} + +UnitWorldState *Unit::get_world_state() const { + return this->world_state; +} + +void Unit::destroy_state() { + if(this->state == nullptr || this->state->is_queued_for_deletion() || !this->state->is_inside_tree()) + return; + this->state->queue_free(); + this->remove_child(this->state); + this->state = nullptr; +} + +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; + this->state = this->current_plan.get(0)->get_apply_state(this->world_state); + if(state == nullptr) { + this->stop_plan(); + gd::UtilityFunctions::push_error("Plan failed to be executed, abandoning"); + return; + } + this->current_plan.remove_at(0); + this->add_child(this->state); + this->state->connect("end_state", this->on_end_state); } diff --git a/src/unit.hpp b/src/unit.hpp index 40d505f..ac016a3 100644 --- a/src/unit.hpp +++ b/src/unit.hpp @@ -1,19 +1,43 @@ #ifndef RTS_UNIT_HPP #define RTS_UNIT_HPP +#include "goal_marker.hpp" +#include "unit_world_state.hpp" +#include "goap/goal.hpp" +#include "goap/planner.hpp" +#include #include #include +#include namespace gd = godot; -class Unit : public gd::Node3D { - GDCLASS(Unit, gd::Node3D); +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); + UnitWorldState *get_world_state() const; private: + void destroy_state(); + void next_action(); +private: + goap::Plan current_plan{}; + goap::State *state{nullptr}; + + gd::Node3D *eyes{nullptr}; + + gd::Callable on_end_state{callable_mp(this, &Unit::next_action)}; + gd::NavigationAgent3D *agent{nullptr}; + UnitWorldState *world_state{nullptr}; + goap::Planner *planner{nullptr}; }; #endif // !RTS_UNIT_HPP diff --git a/src/unit_world_state.cpp b/src/unit_world_state.cpp new file mode 100644 index 0000000..785b515 --- /dev/null +++ b/src/unit_world_state.cpp @@ -0,0 +1,45 @@ +#include "unit_world_state.hpp" +#include "unit.hpp" +#include "utils/godot_macros.hpp" +#include + +void UnitWorldState::_bind_methods() { +#define CLASSNAME UnitWorldState + GDSIGNAL("attention_changed"); + GDFUNCTION(get_can_see_target); + GDFUNCTION(get_target_dead); + GDFUNCTION(get_is_at_target); + GDFUNCTION(get_target_node); +} + +void UnitWorldState::_enter_tree() { GDGAMEONLY(); + this->parent_unit = gd::Object::cast_to(this->get_parent()); + this->agent = this->get_node("../NavigationAgent3D"); + 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 { + return false; +} + +bool UnitWorldState::get_is_at_target() const { + if(this->target_node == nullptr) return true; + gd::Vector3 const target = this->target_node->get_global_position(); + gd::Vector3 const current = this->parent_unit->get_global_position(); + float const min_dist = this->agent->get_target_desired_distance(); + return (target - current).length_squared() < min_dist * min_dist; +} + +bool UnitWorldState::get_target_dead() const { + return false; +} + +void UnitWorldState::set_target_node(gd::Node3D *node) { + this->target_node = node; + this->emit_signal("attention_changed"); +} + +gd::Node3D *UnitWorldState::get_target_node() const { + return this->target_node; +} diff --git a/src/unit_world_state.hpp b/src/unit_world_state.hpp new file mode 100644 index 0000000..ae7308f --- /dev/null +++ b/src/unit_world_state.hpp @@ -0,0 +1,27 @@ +#ifndef UNIT_WORLD_STATE_HPP +#define UNIT_WORLD_STATE_HPP + +#include "goap/actor_world_state.hpp" +#include "godot_cpp/classes/navigation_agent3d.hpp" +#include + +class Unit; + +class UnitWorldState : public goap::ActorWorldState { + GDCLASS(UnitWorldState, goap::ActorWorldState); + static void _bind_methods(); +public: + virtual void _enter_tree() override; + bool get_can_see_target() const; + bool get_is_at_target() const; + bool get_target_dead() const; + + void set_target_node(gd::Node3D *node); + gd::Node3D *get_target_node() const; +private: + Unit *parent_unit{nullptr}; + gd::NavigationAgent3D *agent{nullptr}; + gd::Node3D *target_node{nullptr}; +}; + +#endif // !UNIT_WORLD_STATE_HPP