diff --git a/godot/player_character.tscn b/godot/player_character.tscn index c970bbc..17d74de 100644 --- a/godot/player_character.tscn +++ b/godot/player_character.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=9 format=3 uid="uid://dpda341t6ipiv"] +[gd_scene load_steps=11 format=3 uid="uid://dpda341t6ipiv"] [sub_resource type="Curve" id="Curve_7rmf4"] min_value = 0.2 @@ -6,6 +6,32 @@ max_value = 2.0 _data = [Vector2(0.145299, 0.2), 0.0, 0.482143, 0, 0, Vector2(0.594017, 2), 0.0, 0.0, 0, 0] point_count = 2 +[sub_resource type="MoveStateArgs" id="MoveStateArgs_ibmkn"] +argument_property = &"g_player_character" + +[sub_resource type="Action" id="Action_gtisq"] +effects = { +"is_near_player": true +} +apply_state = SubResource("MoveStateArgs_ibmkn") + +[sub_resource type="MoveStateArgs" id="MoveStateArgs_vyebd"] +argument_property = &"target" + +[sub_resource type="Action" id="Action_cwmvs"] +effects = { +"is_near_target": true +} +apply_state = SubResource("MoveStateArgs_vyebd") + +[sub_resource type="Goal" id="Goal_sqtwb"] +goal_state = { +"is_near_player": true +} +prerequisites = { +"is_near_player": false +} + [sub_resource type="CapsuleShape3D" id="CapsuleShape3D_3g72p"] height = 1.59321 @@ -18,27 +44,14 @@ albedo_color = Color(0.94902, 0.909804, 0, 1) [sub_resource type="BoxMesh" id="BoxMesh_f5yvh"] size = Vector3(0.125, 0.14, 0.94) -[sub_resource type="MoveStateArgs" id="MoveStateArgs_ibmkn"] -argument_property = &"g_player_character" - -[sub_resource type="Action" id="Action_gtisq"] -effects = { -"is_near_player": true -} -apply_state = SubResource("MoveStateArgs_ibmkn") - -[sub_resource type="Goal" id="Goal_sqtwb"] -goal_state = { -"is_near_player": true -} -prerequisites = { -"is_near_player": false -} - [node name="PlayerCharacter" type="CharacterActor"] rotation_speed_curve = SubResource("Curve_7rmf4") collision_layer = 7 +[node name="Planner" type="Planner" parent="."] +actions = [SubResource("Action_gtisq"), SubResource("Action_cwmvs")] +goals = [SubResource("Goal_sqtwb")] + [node name="Health" type="Health" parent="."] max_health = 5 @@ -64,7 +77,3 @@ surface_material_override/0 = SubResource("StandardMaterial3D_scmx3") [node name="WeaponMuzzle" type="WeaponMuzzle" parent="."] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.53551, 0.931313, 0) - -[node name="Planner" type="Planner" parent="."] -actions = [SubResource("Action_gtisq")] -goals = [SubResource("Goal_sqtwb")] diff --git a/godot/project.godot b/godot/project.godot index 9af50fe..a62669a 100644 --- a/godot/project.godot +++ b/godot/project.godot @@ -42,9 +42,15 @@ fire={ "events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":1,"canceled":false,"pressed":false,"double_click":false,"script":null) ] } +tactics_mode={ +"deadzone": 0.5, +"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":32,"key_label":0,"unicode":32,"echo":false,"script":null) +] +} [layer_names] 3d_physics/layer_1="Default" 3d_physics/layer_2="Vision" 3d_physics/layer_3="Hitboxes" +3d_physics/layer_4="Markers" diff --git a/godot/test_level.tscn b/godot/test_level.tscn index 54de2c2..12682c8 100644 --- a/godot/test_level.tscn +++ b/godot/test_level.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=12 format=3 uid="uid://m36guasmi3c1"] +[gd_scene load_steps=16 format=3 uid="uid://m36guasmi3c1"] [ext_resource type="TunnelsGameState" uid="uid://cl0iikkau5mio" path="res://tunnels_game_state.tres" id="1_aove2"] [ext_resource type="PackedScene" uid="uid://cqkbxe758jr7p" path="res://player.tscn" id="2_6yx24"] @@ -28,6 +28,23 @@ size = Vector3(20, 0.25, 20) [sub_resource type="BoxShape3D" id="BoxShape3D_kacqg"] size = Vector3(20, 0.25, 20) +[sub_resource type="Goal" id="Goal_jou2o"] +goal_state = { +"is_near_target": true +} + +[sub_resource type="CylinderShape3D" id="CylinderShape3D_hp1wp"] +height = 0.497374 +radius = 1.0 + +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_navbx"] + +[sub_resource type="CylinderMesh" id="CylinderMesh_oaw6a"] +material = SubResource("StandardMaterial3D_navbx") +top_radius = 1.0 +bottom_radius = 1.0 +height = 0.54 + [node name="Level3D" type="Level3D"] game_mode_prototype = SubResource("TunnelsGameMode_hnap3") @@ -54,3 +71,15 @@ transform = Transform3D(-0.925514, 0, -0.378713, 0, 1, 0, 0.378713, 0, -0.925514 [node name="PlayerCharacter" parent="." instance=ExtResource("4_22npn")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.12963, 0.125, -3.38872) + +[node name="GoalMarker" type="GoalMarker" parent="."] +goal = SubResource("Goal_jou2o") +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.0795, 0, -6.23068) +collision_layer = 8 +collision_mask = 0 + +[node name="CollisionShape3D" type="CollisionShape3D" parent="GoalMarker"] +shape = SubResource("CylinderShape3D_hp1wp") + +[node name="MeshInstance3D" type="MeshInstance3D" parent="GoalMarker"] +mesh = SubResource("CylinderMesh_oaw6a") diff --git a/src/character_actor.cpp b/src/character_actor.cpp index 1917176..cb34e93 100644 --- a/src/character_actor.cpp +++ b/src/character_actor.cpp @@ -16,8 +16,8 @@ void CharacterActor::_bind_methods() { #define CLASSNAME CharacterActor GDPROPERTY_HINTED(rotation_speed_curve, Variant::OBJECT, PROPERTY_HINT_RESOURCE_TYPE, "Curve"); GDFUNCTION_ARGS(set_velocity_target, "value"); + GDPROPERTY_HINTED(target, Variant::OBJECT, PROPERTY_HINT_NODE_TYPE, "Node"); GDFUNCTION(get_is_near_player); - GDFUNCTION(get_player_character); } void CharacterActor::_enter_tree() { GDGAMEONLY(); @@ -84,6 +84,10 @@ void CharacterActor::shoot_at(Vector3 at) { this->set_firing(true); } +void CharacterActor::force_update_action() { + this->set_state(this->planner->get_next_state()); +} + void CharacterActor::set_firing(bool firing) { this->firing = firing; } @@ -131,12 +135,27 @@ Vector3 CharacterActor::get_velocity_target() const { } bool CharacterActor::get_is_near_player() const { - return this->get_player_character()->get_global_position().distance_to(this->get_global_position()) < 5.f; + return Ref(GameRoot::get_singleton()->get_game_mode()) + ->get_player_instance() + ->get_character() + ->get_global_position().distance_to(this->get_global_position()) < 5.f; } -CharacterActor *CharacterActor::get_player_character() const { - Ref game_mode = GameRoot::get_singleton()->get_game_mode(); - return game_mode->get_player_instance()->get_character(); +bool CharacterActor::get_is_near_target() const { + Node3D *target_node3d = Object::cast_to(this->target); + return target_node3d ? target_node3d->get_global_position().distance_to(this->get_global_position()) < 5.f : false; +} + +goap::Planner *CharacterActor::get_planner() const { + return this->planner; +} + +void CharacterActor::set_target(Node *target) { + this->target = target; +} + +Node *CharacterActor::get_target() const { + return this->target; } void CharacterActor::set_state(goap::State state) { diff --git a/src/character_actor.hpp b/src/character_actor.hpp index 1d83bce..d4d72a2 100644 --- a/src/character_actor.hpp +++ b/src/character_actor.hpp @@ -12,6 +12,7 @@ namespace godot { class NavigationAgent3D; class TunnelsPlayer; class AnimationPlayer; + namespace goap { class Planner; }; @@ -36,20 +37,27 @@ public: // fire weapon at a target position // calls aim(at) and set_firing(true) void shoot_at(Vector3 at); + // refresh the current action, ending the previous one + void force_update_action(); // getter-setters void set_firing(bool firing); void set_manual_mode(bool value); void set_rotation_speed_curve(Ref curve); Ref get_rotation_speed_curve() const; + virtual Health *get_health() override; virtual Health const *get_health() const override; + void set_character_data(Ref data); void set_weapon_muzzle(Node3D *node); void set_velocity_target(Vector3 value); Vector3 get_velocity_target() const; bool get_is_near_player() const; - CharacterActor *get_player_character() const; + bool get_is_near_target() const; + goap::Planner *get_planner() const; + void set_target(Node *target); + Node *get_target() const; void set_state(goap::State state); protected: void process_behaviour(double delta_time); diff --git a/src/global_world_state.cpp b/src/global_world_state.cpp index 3a29828..4eb6cf6 100644 --- a/src/global_world_state.cpp +++ b/src/global_world_state.cpp @@ -42,7 +42,6 @@ CharacterActor *GlobalWorldState::get_player_character() const { } Variant GlobalWorldState::get_world_property(StringName prop_key) { - UtilityFunctions::print("fetching: ", prop_key); // check if prop key corresponds to a global key if(!prop_key.begins_with("g_")) return nullptr; @@ -56,9 +55,12 @@ Variant GlobalWorldState::get_world_property(StringName prop_key) { // cache and return this->global_state_cache.insert(prop_key, result); return result; + } else { +#ifdef DEBUG_ENABLED_ENABLED + abort(); +#endif + return nullptr; } - abort(); - return nullptr; } GlobalWorldState *GlobalWorldState::singleton_instance{nullptr}; diff --git a/src/goal_marker.cpp b/src/goal_marker.cpp new file mode 100644 index 0000000..4a85d52 --- /dev/null +++ b/src/goal_marker.cpp @@ -0,0 +1,19 @@ +#include "goal_marker.hpp" +#include "godot_cpp/classes/global_constants.hpp" +#include "planner.hpp" +#include "utils/godot_macros.h" + +namespace godot { +void GoalMarker::_bind_methods() { +#define CLASSNAME GoalMarker + GDPROPERTY_HINTED(goal, Variant::OBJECT, PROPERTY_HINT_RESOURCE_TYPE, "Goal"); +} + +Ref GoalMarker::get_goal() const { + return this->goal; +} + +void GoalMarker::set_goal(Ref goal) { + this->goal = goal; +} +} diff --git a/src/goal_marker.hpp b/src/goal_marker.hpp new file mode 100644 index 0000000..de37d6c --- /dev/null +++ b/src/goal_marker.hpp @@ -0,0 +1,24 @@ +#ifndef GOAL_MARKER_HPP +#define GOAL_MARKER_HPP + +#include + +namespace godot { +class CharacterActor; +namespace goap { + class Planner; + class Goal; +} + +class GoalMarker : public Area3D { + GDCLASS(GoalMarker, Area3D); + static void _bind_methods(); +public: + Ref get_goal() const; + void set_goal(Ref goal); +private: + Ref goal{nullptr}; +}; +} + +#endif // !GOAL_MARKER_HPP diff --git a/src/planner.cpp b/src/planner.cpp index c079c1a..4a9b4e9 100644 --- a/src/planner.cpp +++ b/src/planner.cpp @@ -66,7 +66,6 @@ void Planner::_bind_methods() { void Planner::_enter_tree() { this->global_world_state = GlobalWorldState::get_singleton(); - UtilityFunctions::print("global world state cached: ", this->global_world_state); this->actor = Object::cast_to(this->get_parent()); } @@ -83,9 +82,12 @@ static Vector> trace_path(FromMap &map, PlannerNode &end) { Vector> Planner::make_plan() { // clear cache every planning phase this->cached_world_state.clear(); + // select the most desirable goal available Ref goal = this->select_goal(); - if(!goal.is_valid()) - return {}; + if(!goal.is_valid()) { + this->plan = {}; + return this->plan; + } // ordered list of all nodes still being considered Vector open{PlannerNode::goal_node(goal->goal_state)}; PlannerNode first = open.get(0); @@ -99,8 +101,10 @@ Vector> Planner::make_plan() { // current is the top of the ordered list current = open.get(0); // check if we've reached the goal - if(current.open_requirements.is_empty()) - return trace_path(from, current); + if(current.open_requirements.is_empty()) { + this->plan = trace_path(from, current); + return this->plan; + } // current is no longer considered as it cannot be the end open.erase(current); // find all neighbours of this state @@ -119,19 +123,15 @@ Vector> Planner::make_plan() { } } } - return {}; + UtilityFunctions::push_warning("Failed to find a path satisfying goal"); + this->plan = {}; + return this->plan; } Ref Planner::select_goal() { for(Ref const &goal : this->goals) { - bool can_try{true}; - for(WorldProperty const &prop : goal->prerequisites) { - if(prop.value != this->get_world_property(prop.key)) { - can_try = false; - break; - } - } - if(can_try) return goal; + if(this->can_do(goal)) + return goal; } return {}; } @@ -149,10 +149,16 @@ Variant Planner::get_world_property(StringName prop_key) { } bool Planner::can_do(Ref action) { - for(WorldProperty &prop : action->context_prerequisites) { + for(WorldProperty &prop : action->context_prerequisites) + if(this->get_world_property(prop.key) != prop.value) + return false; + return true; +} + +bool Planner::can_do(Ref goal) { + for(WorldProperty const &prop : goal->prerequisites) if(this->get_world_property(prop.key) != prop.value) return false; - } return true; } @@ -235,4 +241,14 @@ Array Planner::get_goals() const { } return array; } + +bool Planner::add_goal(Ref goal) { + bool can_do = this->can_do(goal); + this->goals.insert(0, goal); + return can_do; +} + +void Planner::remove_goal(Ref goal) { + this->goals.erase(goal); +} } diff --git a/src/planner.hpp b/src/planner.hpp index 399a1cb..fee995c 100644 --- a/src/planner.hpp +++ b/src/planner.hpp @@ -2,6 +2,7 @@ #define GOAP_PLANNER_HPP #include "action.hpp" +#include "goal_marker.hpp" #include "godot_cpp/variant/variant.hpp" #include #include @@ -52,6 +53,7 @@ public: Variant get_world_property(StringName prop_key); bool can_do(Ref action); + bool can_do(Ref goal); Vector find_neighbours_of(PlannerNode &node); Vector> find_actions_satisfying(WorldState requirements); @@ -62,6 +64,8 @@ public: Array get_actions() const; void set_goals(Array goals); Array get_goals() const; + bool add_goal(Ref goal); + void remove_goal(Ref goal); private: CharacterActor *actor{nullptr}; // the parent actor of this planner WorldState cached_world_state{}; // the cached worldstate, cleared for every make_plan call diff --git a/src/register_types.cpp b/src/register_types.cpp index 8a05f53..f3ff105 100644 --- a/src/register_types.cpp +++ b/src/register_types.cpp @@ -4,6 +4,7 @@ #include "character_data.hpp" #include "enemy.hpp" #include "global_world_state.hpp" +#include "goal_marker.hpp" #include "health.hpp" #include "pellet_projectile.hpp" #include "planner.hpp" @@ -64,6 +65,7 @@ void initialize_gdextension_types(ModuleInitializationLevel p_level) ClassDB::register_class(); ClassDB::register_class(); ClassDB::register_class(); + ClassDB::register_class(); } extern "C" diff --git a/src/tunnels_game_mode.cpp b/src/tunnels_game_mode.cpp index 905d914..0d279c0 100644 --- a/src/tunnels_game_mode.cpp +++ b/src/tunnels_game_mode.cpp @@ -33,7 +33,19 @@ void TunnelsGameMode::register_player_character(CharacterActor *actor) { } } +void TunnelsGameMode::set_manual_character(CharacterActor *actor) { + if(!this->player_characters.has(actor)) + this->register_player_character(actor); + this->manual_character = actor; +} + void TunnelsGameMode::on_character_destroyed(CharacterActor *actor) { this->player_characters.erase(actor); + if(this->manual_character == actor) + this->manual_character = nullptr; +} + +Vector const &TunnelsGameMode::get_player_characters() const { + return this->player_characters; } } diff --git a/src/tunnels_game_mode.hpp b/src/tunnels_game_mode.hpp index e6a882d..ea20832 100644 --- a/src/tunnels_game_mode.hpp +++ b/src/tunnels_game_mode.hpp @@ -18,6 +18,7 @@ public: void register_player_character(CharacterActor *actor); void set_manual_character(CharacterActor *actor); void on_character_destroyed(CharacterActor *actor); + Vector const &get_player_characters() const; private: TunnelsPlayer *player{nullptr}; CharacterActor *manual_character{nullptr}; diff --git a/src/tunnels_player.cpp b/src/tunnels_player.cpp index 9949061..e750505 100644 --- a/src/tunnels_player.cpp +++ b/src/tunnels_player.cpp @@ -1,15 +1,22 @@ #include "tunnels_player.hpp" #include "character_actor.hpp" #include "character_data.hpp" +#include "goal_marker.hpp" +#include "godot_cpp/variant/utility_functions.hpp" +#include "planner.hpp" +#include "tunnels_game_mode.hpp" #include "tunnels_game_state.hpp" #include "utils/game_root.hpp" #include "utils/godot_macros.h" #include "utils/player_input.hpp" #include #include +#include +#include #include #include #include +#include #include #include @@ -19,6 +26,7 @@ void TunnelsPlayer::_bind_methods() { GDFUNCTION_ARGS(horizontal_move_input, "event", "value"); GDFUNCTION_ARGS(vertical_move_input, "event", "value"); GDFUNCTION_ARGS(fire_pressed, "event", "value"); + GDFUNCTION_ARGS(mode_switch_input, "event", "value"); GDPROPERTY_HINTED(camera_rotation_ramp, Variant::OBJECT, PROPERTY_HINT_RESOURCE_TYPE, "Curve"); } @@ -55,10 +63,11 @@ void TunnelsPlayer::_process(double delta_time) { GDGAMEONLY(); this->set_global_position(this->character->get_global_position()); break; case State::Tactics: + // move camera along with the input + this->set_global_position(this->get_global_position() + this->get_world_move_input().normalized() * + delta_time * TunnelsPlayer::TACTICS_MOVEMENT_SPEED); break; case State::Overview: - // move camera along with the input - this->set_global_position(this->get_global_position() + this->get_world_move_input().normalized()); break; } } @@ -101,6 +110,7 @@ void TunnelsPlayer::setup_player_input(PlayerInput *input) { input->listen_to(PlayerInput::Listener("move_left", "move_right", this, "horizontal_move_input")); input->listen_to(PlayerInput::Listener("move_forward", "move_backward", this, "vertical_move_input")); input->listen_to(PlayerInput::Listener("fire", this, "fire_pressed")); + input->listen_to(PlayerInput::Listener("tactics_mode", this, "mode_switch_input")); } Node *TunnelsPlayer::to_node() { @@ -115,8 +125,70 @@ void TunnelsPlayer::vertical_move_input(Ref event, float value) { this->move_input.y = value; } +void TunnelsPlayer::mode_switch_input(Ref event, float value) { + if(value != 0.f) + this->state = this->state == State::Tactics ? State::ManualControl : State::Tactics; +} + void TunnelsPlayer::fire_pressed(Ref event, float value) { - this->character->set_firing(value != 0); + switch(this->state) { + case State::ManualControl: + this->character->set_firing(value != 0); + break; + case State::Tactics: + if(value == 1.f) + this->try_select_marker(); + break; + case State::Overview: + break; + } +} + +void TunnelsPlayer::try_select_marker() { + UtilityFunctions::print("TunnelsPlayer::try_select_marker()"); + Transform3D const &camera_trans{this->camera->get_global_transform()}; + // prepare raycast query + Ref params{PhysicsRayQueryParameters3D::create(camera_trans.origin, camera_trans.origin + this->mouse_world_ray_normal * 1000.f)}; + params->set_collision_mask(1u << 3u); + params->set_collide_with_areas(true); + // fetch current physics state and cast ray + PhysicsDirectSpaceState3D *state = this->get_world_3d()->get_direct_space_state(); + Dictionary dict{state->intersect_ray(params)}; + // fail if nothing was hit + if(dict.is_empty()) + return; + // attempt to cast hit node to a marker + GoalMarker *marker{Object::cast_to(dict["collider"])}; + // fail if hit object is not a marker + if(marker == nullptr) + return; + UtilityFunctions::print("Hit: ", marker->get_path()); + CharacterActor *target_character{nullptr}; + for(CharacterActor *loop_character : Ref(GameRoot::get_singleton()->get_game_mode())->get_player_characters()) { + if(loop_character != this->character) { + target_character = loop_character; + break; + } + } + // no non-player ally was found + if(target_character == nullptr) + return; + // cache planner component + goap::Planner *planner{target_character->get_planner()}; + // cache previous target in case planning fails + Node *previous_target{target_character->get_target()}; + // attempt to find a plan to marker's goal + target_character->set_target(marker); + if(planner->can_do(marker->get_goal())) { + planner->add_goal(marker->get_goal()); + planner->make_plan(); + target_character->force_update_action(); + UtilityFunctions::print("Made plan for character ", target_character->get_path()); + } else { + // reset character to the state it was in before attempts to change goal + UtilityFunctions::push_warning("Failed to make plan for ", marker->get_goal()->get_path()); + target_character->set_target(previous_target); + } } void TunnelsPlayer::initialize_character() { @@ -135,6 +207,7 @@ void TunnelsPlayer::initialize_character() { this->character->set_character_data(game_state->get_characters()[0]); // disable navmesh navigation and start using player input this->character->set_manual_mode(true); + Ref(GameRoot::get_singleton()->get_game_mode())->set_manual_character(this->character); } Vector3 TunnelsPlayer::get_world_move_input() const { @@ -178,4 +251,5 @@ CharacterActor *TunnelsPlayer::get_character() const { float const TunnelsPlayer::ROTATION_SPEED{0.5f}; float const TunnelsPlayer::ROTATION_Y_MIN_INFLUENCE{7.f}; float const TunnelsPlayer::ROTATION_MARGIN{0.4f}; +float const TunnelsPlayer::TACTICS_MOVEMENT_SPEED{20.f}; } diff --git a/src/tunnels_player.hpp b/src/tunnels_player.hpp index f5c6364..e0dae94 100644 --- a/src/tunnels_player.hpp +++ b/src/tunnels_player.hpp @@ -35,8 +35,10 @@ public: void horizontal_move_input(Ref event, float value); void vertical_move_input(Ref event, float value); + void mode_switch_input(Ref event, float value); void fire_pressed(Ref event, float value); + void try_select_marker(); void initialize_character(); Vector3 get_world_move_input() const; @@ -61,6 +63,7 @@ private: static float const ROTATION_SPEED; static float const ROTATION_Y_MIN_INFLUENCE; static float const ROTATION_MARGIN; + static float const TACTICS_MOVEMENT_SPEED; }; }