From e0261a033e797b448895f42f9590450482a6e6e3 Mon Sep 17 00:00:00 2001 From: Sara Date: Mon, 25 Mar 2024 21:58:20 +0100 Subject: [PATCH 01/25] feat: defined action with prereqs and effects --- src/action.cpp | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/action.hpp | 36 ++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 src/action.cpp create mode 100644 src/action.hpp diff --git a/src/action.cpp b/src/action.cpp new file mode 100644 index 0000000..b99c5dd --- /dev/null +++ b/src/action.cpp @@ -0,0 +1,53 @@ +#include "action.hpp" +#include "utils/godot_macros.h" + +namespace godot::goap { +void Action::_bind_methods() { +#define CLASSNAME Action + GDPROPERTY(context_prerequisites, Variant::DICTIONARY); + GDPROPERTY(prerequisites, Variant::DICTIONARY); + GDPROPERTY(effects, Variant::DICTIONARY); +} + +void Action::set_context_prerequisites(Dictionary dict) { + Action::dict_to_property_map(dict, this->context_prerequisites); +} + +Dictionary Action::get_context_prerequisites() const { + return Action::property_map_to_dict(this->context_prerequisites); +} + +void Action::set_prerequisites(Dictionary dict) { + Action::dict_to_property_map(dict, this->prerequisites); +} + +Dictionary Action::get_prerequisites() const { + return Action::property_map_to_dict(this->prerequisites); +} + +void Action::set_effects(Dictionary dict) { + Action::dict_to_property_map(dict, this->effects); +} + +Dictionary Action::get_effects() const { + return Action::property_map_to_dict(this->effects); +} + +Dictionary Action::property_map_to_dict(WorldState const &props) { + Dictionary out{}; + for(KeyValue const &prop : props) { + out[prop.key] = prop.value; + } + return out; +} + +void Action::dict_to_property_map(Dictionary const &dict, WorldState &out) { + out.clear(); + Array keys = dict.keys(); + Array vals = dict.values(); + for(size_t i{0}; i < keys.size(); ++i) { + if(keys[i].get_type() == Variant::STRING_NAME) + out.insert(keys[i], vals[i]); + } +} +} diff --git a/src/action.hpp b/src/action.hpp new file mode 100644 index 0000000..1a1b2e2 --- /dev/null +++ b/src/action.hpp @@ -0,0 +1,36 @@ +#ifndef GOAP_ACTION_HPP +#define GOAP_ACTION_HPP + +#include +#include +#include +#include +#include +#include +#include + +namespace godot::goap { +typedef HashMap WorldState; +typedef KeyValue WorldProperty; + +class Action : public Resource { + GDCLASS(Action, Resource); + static void _bind_methods(); +public: + void set_context_prerequisites(Dictionary dict); + Dictionary get_context_prerequisites() const; + void set_prerequisites(Dictionary dict); + Dictionary get_prerequisites() const; + void set_effects(Dictionary dict); + Dictionary get_effects() const; +protected: + static Dictionary property_map_to_dict(WorldState const &props); + static void dict_to_property_map(Dictionary const &dict, WorldState &out); +public: + WorldState context_prerequisites{}; + WorldState prerequisites{}; + WorldState effects{}; +}; +} + +#endif //!GOAP_ACTION_HPP From 6e55e5293f67c29071f6d0340f919b8cef5facf3 Mon Sep 17 00:00:00 2001 From: Sara Date: Mon, 25 Mar 2024 21:58:30 +0100 Subject: [PATCH 02/25] feat: created global world state singleton --- src/global_world_state.cpp | 56 ++++++++++++++++++++++++++++++++++++++ src/global_world_state.hpp | 31 +++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 src/global_world_state.cpp create mode 100644 src/global_world_state.hpp diff --git a/src/global_world_state.cpp b/src/global_world_state.cpp new file mode 100644 index 0000000..b22257b --- /dev/null +++ b/src/global_world_state.cpp @@ -0,0 +1,56 @@ +#include "global_world_state.hpp" +#include "character_actor.hpp" +#include "utils/game_root.hpp" + +namespace godot::goap { +void GlobalWorldState::_bind_methods() { +#define CLASSNAME GlobalWorldState +} + +bool GlobalWorldState::has_singleton() { + return GlobalWorldState::singleton_instance != nullptr; +} + +void GlobalWorldState::_enter_tree() { + if(GlobalWorldState::singleton_instance == nullptr) + GlobalWorldState::singleton_instance = this; +} + +void GlobalWorldState::_ready() { + this->game_mode = GameRoot::get_singleton()->get_game_mode(); +} + +void GlobalWorldState::_exit_tree() { + if(GlobalWorldState::singleton_instance == this) + GlobalWorldState::singleton_instance = nullptr; +} + +void GlobalWorldState::_process(double delta_time) { + global_state_cache.clear(); // invalidate cache +} + +Vector3 GlobalWorldState::get_player_position() { + return this->game_mode->get_player_instance()->get_character()->get_global_position(); +} + +Variant GlobalWorldState::get_world_property(StringName prop_key) { + // check if prop key corresponds to a global key + if(!prop_key.begins_with("g_")) + return nullptr; + // check if the key is cached for this frame + else if(global_state_cache.has(prop_key)) + return global_state_cache[prop_key]; + // fetch by function name + StringName const fn = "get_" + prop_key.right(prop_key.length() - 2); + if(this->has_method(fn)) { + Variant result = this->call(fn); + // cache and return + this->global_state_cache[prop_key] = result; + return result; + } + return nullptr; +} + +GlobalWorldState *GlobalWorldState::singleton_instance{nullptr}; +} + diff --git a/src/global_world_state.hpp b/src/global_world_state.hpp new file mode 100644 index 0000000..a8f1eed --- /dev/null +++ b/src/global_world_state.hpp @@ -0,0 +1,31 @@ +#ifndef GOAP_GLOBAL_WORLD_STATE_HPP +#define GOAP_GLOBAL_WORLD_STATE_HPP + +#include "action.hpp" +#include "tunnels_game_mode.hpp" +#include + +namespace godot::goap { +class GlobalWorldState : public Node { + GDCLASS(GlobalWorldState, Node); + static void _bind_methods(); +public: + static bool has_singleton(); + static GlobalWorldState *get_singleton(); + + virtual void _enter_tree() override; + virtual void _ready() override; + virtual void _exit_tree() override; + virtual void _process(double delta_time) override; + + Vector3 get_player_position(); + + Variant get_world_property(StringName prop_key); +private: + WorldState global_state_cache{}; + Ref game_mode{}; + static GlobalWorldState *singleton_instance; +}; +} + +#endif // !GOAP_GLOBAL_WORLD_STATE_HPP From dfe6349c76fa46218c3a5544218ed40fb7108d3e Mon Sep 17 00:00:00 2001 From: Sara Date: Mon, 25 Mar 2024 21:58:48 +0100 Subject: [PATCH 03/25] feat: outlined planner, started implementation --- src/planner.cpp | 95 +++++++++++++++++++++++++++++++++++++++++++++++++ src/planner.hpp | 93 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 188 insertions(+) create mode 100644 src/planner.cpp create mode 100644 src/planner.hpp diff --git a/src/planner.cpp b/src/planner.cpp new file mode 100644 index 0000000..3ad233c --- /dev/null +++ b/src/planner.cpp @@ -0,0 +1,95 @@ +#include "planner.hpp" +#include "action.hpp" +#include "global_world_state.hpp" +#include "godot_cpp/templates/pair.hpp" +#include "utils/godot_macros.h" +#include +#include +#include + +namespace godot::goap { +void Planner::_bind_methods() { +#define CLASSNAME Planner + GDPROPERTY_HINTED(actions, Variant::OBJECT, PROPERTY_HINT_ARRAY_TYPE, "Action"); +} + +void Planner::_ready() { + this->global_world_state = GlobalWorldState::get_singleton(); +} + +void Planner::_process(double delta_time) { + this->cached_world_state.clear(); +} + +Vector Planner::make_plan(Goal *goal) { + HashMap open{}; + open.insert(PlannerNode{{}, goal->goal_state, nullptr}, 0); + HashMap, PlannerNodeHasher> from{}; + HashMap score_to_start{}; + HashMap heuristic_score{}; + + PlannerNode current; + while(!open.is_empty()) { + int current_weight = 0; + for(KeyValue &kvp : open) { + if(kvp.value > current_weight) { + current = kvp.key; + current_weight = kvp.value; + } + } + } +} + +Variant Planner::get_world_property(StringName prop_key) { + if(world_state_override.has(prop_key)) + return world_state_override.get(prop_key); + if(prop_key.begins_with("g_")) + return this->global_world_state->get_world_property(prop_key); + if(this->cached_world_state.has(prop_key)) + return this->cached_world_state[prop_key]; + if(this->has_method("get_" + prop_key)) { + Variant val = this->call(prop_key); + this->cached_world_state[prop_key] = val; + return val; + } + return nullptr; +} + +bool Planner::can_do(Ref action) { + for(WorldProperty &prop : action->context_prerequisites) { + if(this->get_world_property(prop.key) != prop.value) + return false; + } + return true; +} + +Vector> Planner::find_actions_satisfying(WorldState requirements) { + Vector> found_actions{}; + for(Ref &act : this->actions) { + for(WorldProperty &prop : requirements) { + if(act->effects.has(prop.key) + && act->effects.get(prop.key) == prop.value + && this->can_do(act)) + found_actions.push_back(act); + } + } + return found_actions; +} + +void Planner::set_actions(Array value) { + this->actions.clear(); + for(size_t i{0}; i < value.size(); ++i) { + Ref act = value[i]; + if(act.is_valid()) + this->actions.push_back(value[i]); + } +} + +Array Planner::get_actions() const { + Array array{}; + for(Ref const &act : this->actions) { + array.push_back(act); + } + return array; +} +} diff --git a/src/planner.hpp b/src/planner.hpp new file mode 100644 index 0000000..13b6c23 --- /dev/null +++ b/src/planner.hpp @@ -0,0 +1,93 @@ +#ifndef GOAP_PLANNER_HPP +#define GOAP_PLANNER_HPP + +#include "action.hpp" +#include "godot_cpp/classes/object.hpp" +#include "godot_cpp/variant/variant.hpp" +#include +#include +#include +#include + +namespace godot { +class CharacterActor; + +namespace goap { +class GlobalWorldState; + +struct State { + static State new_move_to(Vector3 location); + static State new_animate(StringName animation); + static State new_activate(Node *node); + enum Type { + STATE_MOVE_TO, + STATE_ANIMATE, + STATE_ACTIVATE + }; + State::Type type; + union { + Vector3 move_to; + StringName animate; + Node *activate; + }; +}; + +class Goal : public Resource { + GDCLASS(Goal, Resource); + static void _bind_methods(); +public: + WorldState goal_state{}; +}; + +class Planner : public Node { + GDCLASS(Planner, Node); + static void _bind_methods(); +public: + virtual void _ready() override; + virtual void _process(double delta_time) override; + + Vector make_plan(Goal *goal); + + Variant get_world_property(StringName prop_key); + + bool can_do(Ref action); + Vector> find_actions_satisfying(WorldState requirements); + + void set_actions(Array actions); + Array get_actions() const; +private: + CharacterActor *actor{nullptr}; + WorldState world_state_override{}; + WorldState cached_world_state{}; + GlobalWorldState *global_world_state{nullptr}; + + Vector> actions{}; +}; + +struct PlannerNode { + WorldState state; + WorldState open_requirements; + Ref last_edge; + + int heuristic_score(Ref action) { + int score{0}; + return score; + } +}; + +struct PlannerNodeHasher { + static _FORCE_INLINE_ uint32_t hash(godot::goap::PlannerNode const &node) { + VariantHasher variant_hasher{}; + Variant a{1}; + variant_hasher.hash(a); + } +}; + +static _FORCE_INLINE_ bool operator==(PlannerNode const& lhs, PlannerNode const& rhs) { + return true; +} +} +} + + +#endif // !GOAP_PLANNER_HPP From fdfde706e0ea83413d638c5791c2fa3e964ba7ab Mon Sep 17 00:00:00 2001 From: Sara Date: Wed, 27 Mar 2024 01:22:31 +0100 Subject: [PATCH 04/25] feat: first functioning state of GOAP implementation --- Makefile | 2 +- godot/new_goal.tres | 6 ++ godot/player_character.tscn | 43 +++++++++++++- src/action.cpp | 7 ++- src/action.hpp | 3 +- src/global_world_state.cpp | 8 ++- src/planner.cpp | 110 ++++++++++++++++++++++++++++++------ src/planner.hpp | 75 ++++++++++++++++++------ src/register_types.cpp | 8 +++ 9 files changed, 218 insertions(+), 44 deletions(-) create mode 100644 godot/new_goal.tres diff --git a/Makefile b/Makefile index cfbd053..fd31b3d 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ debug: - /usr/bin/scons debug_symbols=yes + /usr/bin/scons debug_symbols=yes optimize=none develop: /usr/bin/scons diff --git a/godot/new_goal.tres b/godot/new_goal.tres new file mode 100644 index 0000000..d54c01a --- /dev/null +++ b/godot/new_goal.tres @@ -0,0 +1,6 @@ +[gd_resource type="Goal" format=3 uid="uid://ogtaubr23l5x"] + +[resource] +goal_state = { +"goal": true +} diff --git a/godot/player_character.tscn b/godot/player_character.tscn index f4cd098..32b6ea6 100644 --- a/godot/player_character.tscn +++ b/godot/player_character.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=6 format=3 uid="uid://dpda341t6ipiv"] +[gd_scene load_steps=10 format=3 uid="uid://dpda341t6ipiv"] [sub_resource type="Curve" id="Curve_7rmf4"] min_value = 0.2 @@ -18,7 +18,42 @@ albedo_color = Color(0.94902, 0.909804, 0, 1) [sub_resource type="BoxMesh" id="BoxMesh_f5yvh"] size = Vector3(0.125, 0.14, 0.94) -[node name="PlayerCharacter" type="PlayerCharacter"] +[sub_resource type="Action" id="Action_ksl64"] +effects = { +"prereq": true +} + +[sub_resource type="Action" id="Action_mdru6"] +effects = { +"not_goal": 1 +} + +[sub_resource type="Action" id="Action_7v1i5"] +prerequisites = { +"prereq": true +} +effects = { +"goal": true +} + +[sub_resource type="GDScript" id="GDScript_ncqfl"] +script/source = "extends Planner + +func get_goal() -> bool: + return false + +func get_prereq() -> bool: + return false + +func get_not_goal() -> int: + return 0 + +func _ready(): + var goal : Goal = ResourceLoader.load(\"res://new_goal.tres\") + var plan : Array = self.gdscript_make_plan(goal) +" + +[node name="PlayerCharacter" type="CharacterActor"] rotation_speed_curve = SubResource("Curve_7rmf4") collision_layer = 7 @@ -47,3 +82,7 @@ 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_ksl64"), SubResource("Action_mdru6"), SubResource("Action_7v1i5")] +script = SubResource("GDScript_ncqfl") diff --git a/src/action.cpp b/src/action.cpp index b99c5dd..7d8e515 100644 --- a/src/action.cpp +++ b/src/action.cpp @@ -1,5 +1,6 @@ #include "action.hpp" #include "utils/godot_macros.h" +#include namespace godot::goap { void Action::_bind_methods() { @@ -46,8 +47,10 @@ void Action::dict_to_property_map(Dictionary const &dict, WorldState &out) { Array keys = dict.keys(); Array vals = dict.values(); for(size_t i{0}; i < keys.size(); ++i) { - if(keys[i].get_type() == Variant::STRING_NAME) - out.insert(keys[i], vals[i]); + if(keys[i].get_type() == Variant::STRING_NAME || keys[i].get_type() == Variant::STRING) + out.insert(keys[i], dict[keys[i]]); + else + UtilityFunctions::push_error("WorldProperty keys have to be string names (expected ", Variant::STRING_NAME, " is ", keys[i].get_type(), ")"); } } } diff --git a/src/action.hpp b/src/action.hpp index 1a1b2e2..eac8d58 100644 --- a/src/action.hpp +++ b/src/action.hpp @@ -1,7 +1,6 @@ #ifndef GOAP_ACTION_HPP #define GOAP_ACTION_HPP -#include #include #include #include @@ -23,7 +22,7 @@ public: Dictionary get_prerequisites() const; void set_effects(Dictionary dict); Dictionary get_effects() const; -protected: + static Dictionary property_map_to_dict(WorldState const &props); static void dict_to_property_map(Dictionary const &dict, WorldState &out); public: diff --git a/src/global_world_state.cpp b/src/global_world_state.cpp index b22257b..3152b07 100644 --- a/src/global_world_state.cpp +++ b/src/global_world_state.cpp @@ -11,6 +11,10 @@ bool GlobalWorldState::has_singleton() { return GlobalWorldState::singleton_instance != nullptr; } +GlobalWorldState *GlobalWorldState::get_singleton() { + return GlobalWorldState::singleton_instance; +} + void GlobalWorldState::_enter_tree() { if(GlobalWorldState::singleton_instance == nullptr) GlobalWorldState::singleton_instance = this; @@ -39,13 +43,13 @@ Variant GlobalWorldState::get_world_property(StringName prop_key) { return nullptr; // check if the key is cached for this frame else if(global_state_cache.has(prop_key)) - return global_state_cache[prop_key]; + return global_state_cache.get(prop_key); // fetch by function name StringName const fn = "get_" + prop_key.right(prop_key.length() - 2); if(this->has_method(fn)) { Variant result = this->call(fn); // cache and return - this->global_state_cache[prop_key] = result; + this->global_state_cache.insert(prop_key, result); return result; } return nullptr; diff --git a/src/planner.cpp b/src/planner.cpp index 3ad233c..6029b53 100644 --- a/src/planner.cpp +++ b/src/planner.cpp @@ -1,16 +1,40 @@ #include "planner.hpp" #include "action.hpp" #include "global_world_state.hpp" -#include "godot_cpp/templates/pair.hpp" #include "utils/godot_macros.h" +#include +#include +#include +#include #include +#include #include #include namespace godot::goap { +typedef HashMap FromMap; +typedef HashMap ScoreMap; +typedef HashSet NodeSet; + +void Goal::_bind_methods() { +#define CLASSNAME Goal + GDPROPERTY(goal_state, Variant::DICTIONARY); +} + +void Goal::set_goal_state(Dictionary dict) { + Action::dict_to_property_map(dict, this->goal_state); +} + +Dictionary Goal::get_goal_state() const { + return Action::property_map_to_dict(this->goal_state); +} + +#undef CLASSNAME // !Goal + void Planner::_bind_methods() { #define CLASSNAME Planner - GDPROPERTY_HINTED(actions, Variant::OBJECT, PROPERTY_HINT_ARRAY_TYPE, "Action"); + GDPROPERTY_HINTED(actions, Variant::ARRAY, PROPERTY_HINT_ARRAY_TYPE, GDRESOURCETYPE(Action)); + GDFUNCTION_ARGS(gdscript_make_plan, "goal"); } void Planner::_ready() { @@ -21,23 +45,58 @@ void Planner::_process(double delta_time) { this->cached_world_state.clear(); } -Vector Planner::make_plan(Goal *goal) { - HashMap open{}; - open.insert(PlannerNode{{}, goal->goal_state, nullptr}, 0); - HashMap, PlannerNodeHasher> from{}; - HashMap score_to_start{}; - HashMap heuristic_score{}; +static Vector> trace_path(FromMap &map, PlannerNode &end) { + Vector> edges{}; + PlannerNode node{end}; + while(node.last_edge.is_valid()) { + edges.push_back(node.last_edge); + node = map.get(node); + } + return edges; +} - PlannerNode current; +Array Planner::gdscript_make_plan(Ref goal) { + Vector> plan = this->make_plan(goal); + Array out{}; + int i{0}; + UtilityFunctions::print("plan len: ", plan.size()); + for(Ref const &action : plan) { + out.push_back(action); + UtilityFunctions::print("plan[", i++, "]: ", this->actions.find(action)); + } + return out; +} + +Vector> Planner::make_plan(Ref goal) { + UtilityFunctions::print("run"); + Vector open{PlannerNode::goal_node(goal->goal_state)}; + PlannerNode first = open.get(0); + FromMap from{}; + ScoreMap dist_traveled{}; + ScoreMap best_guess{}; + dist_traveled.insert(first, 0); + best_guess.insert(first, first.open_requirements.size()); + PlannerNode current{}; while(!open.is_empty()) { - int current_weight = 0; - for(KeyValue &kvp : open) { - if(kvp.value > current_weight) { - current = kvp.key; - current_weight = kvp.value; + current = open.get(0); + if(current.open_requirements.is_empty()) + return trace_path(from, current); + open.erase(current); + Vector neighbours = this->find_neighbours_of(current); + for(PlannerNode const& node : neighbours) { + float const new_dist = dist_traveled.get(current) + 1.f; + if(!dist_traveled.has(node) || new_dist < dist_traveled.get(node)) { + dist_traveled[node] = new_dist; + best_guess[node] = new_dist + node.open_requirements.size(); + from[node] = current; + int i = open.find(node); + if(i != -1) + open.remove_at(i); + open.ordered_insert(node); } } } + return {}; } Variant Planner::get_world_property(StringName prop_key) { @@ -46,10 +105,10 @@ Variant Planner::get_world_property(StringName prop_key) { if(prop_key.begins_with("g_")) return this->global_world_state->get_world_property(prop_key); if(this->cached_world_state.has(prop_key)) - return this->cached_world_state[prop_key]; + return this->cached_world_state.get(prop_key); if(this->has_method("get_" + prop_key)) { Variant val = this->call(prop_key); - this->cached_world_state[prop_key] = val; + this->cached_world_state.insert(prop_key, val); return val; } return nullptr; @@ -63,6 +122,22 @@ bool Planner::can_do(Ref action) { return true; } +Vector Planner::find_neighbours_of(PlannerNode &node) { + Vector neighbours{}; + for(Ref const &action : this->find_actions_satisfying(node.open_requirements)) { + PlannerNode new_node = node.new_node_along(action); + // remove all satisfied requirements + for(WorldProperty const &delta : action->effects) { + if(new_node.open_requirements.has(delta.key) + && new_node.open_requirements.get(delta.key) == delta.value) { + new_node.open_requirements.erase(delta.key); + } + } + neighbours.push_back(new_node); + } + return neighbours; +} + Vector> Planner::find_actions_satisfying(WorldState requirements) { Vector> found_actions{}; for(Ref &act : this->actions) { @@ -78,10 +153,11 @@ Vector> Planner::find_actions_satisfying(WorldState requirements) { void Planner::set_actions(Array value) { this->actions.clear(); + this->actions.resize(value.size()); for(size_t i{0}; i < value.size(); ++i) { Ref act = value[i]; if(act.is_valid()) - this->actions.push_back(value[i]); + this->actions.set(i, act); } } diff --git a/src/planner.hpp b/src/planner.hpp index 13b6c23..6e84864 100644 --- a/src/planner.hpp +++ b/src/planner.hpp @@ -2,7 +2,6 @@ #define GOAP_PLANNER_HPP #include "action.hpp" -#include "godot_cpp/classes/object.hpp" #include "godot_cpp/variant/variant.hpp" #include #include @@ -35,10 +34,41 @@ struct State { class Goal : public Resource { GDCLASS(Goal, Resource); static void _bind_methods(); + + void set_goal_state(Dictionary dict); + Dictionary get_goal_state() const; public: WorldState goal_state{}; }; +struct PlannerNode { + WorldState open_requirements{}; + WorldState state{}; + Ref last_edge{}; + + PlannerNode() = default; + PlannerNode(PlannerNode const &src) = default; + + static PlannerNode goal_node(WorldState goal) { + return PlannerNode{ + goal, {}, {} + }; + } + + PlannerNode new_node_along(Ref action) const { + PlannerNode new_node{ + action->prerequisites, + action->effects, + action, + }; + for(WorldProperty const &prop : this->state) + new_node.state.insert(prop.key, prop.value); + for(WorldProperty const &prop : this->open_requirements) + new_node.open_requirements.insert(prop.key, prop.value); + return new_node; + } +}; + class Planner : public Node { GDCLASS(Planner, Node); static void _bind_methods(); @@ -46,11 +76,13 @@ public: virtual void _ready() override; virtual void _process(double delta_time) override; - Vector make_plan(Goal *goal); + Array gdscript_make_plan(Ref goal); + Vector> make_plan(Ref goal); Variant get_world_property(StringName prop_key); bool can_do(Ref action); + Vector find_neighbours_of(PlannerNode &node); Vector> find_actions_satisfying(WorldState requirements); void set_actions(Array actions); @@ -64,27 +96,34 @@ private: Vector> actions{}; }; -struct PlannerNode { - WorldState state; - WorldState open_requirements; - Ref last_edge; - - int heuristic_score(Ref action) { - int score{0}; - return score; - } -}; - struct PlannerNodeHasher { static _FORCE_INLINE_ uint32_t hash(godot::goap::PlannerNode const &node) { - VariantHasher variant_hasher{}; - Variant a{1}; - variant_hasher.hash(a); + uint32_t hash{1}; + for(KeyValue const &kvp : node.state) { + hash = hash_murmur3_one_32(kvp.key.hash(), hash); + hash = hash_murmur3_one_32(kvp.value.hash(), hash); + } + return hash_fmix32(hash); } }; -static _FORCE_INLINE_ bool operator==(PlannerNode const& lhs, PlannerNode const& rhs) { - return true; +static _FORCE_INLINE_ bool operator==(PlannerNode const &lhs, PlannerNode const &rhs) { + return PlannerNodeHasher::hash(lhs) == PlannerNodeHasher::hash(rhs); +} +static _FORCE_INLINE_ bool operator!=(PlannerNode const &lhs, PlannerNode const &rhs) { + return !(lhs == rhs); +} +static _FORCE_INLINE_ bool operator<(PlannerNode const &lhs, PlannerNode const &rhs) { + return lhs.open_requirements.size() < rhs.open_requirements.size(); +} +static _FORCE_INLINE_ bool operator>=(PlannerNode const &lhs, PlannerNode const &rhs) { + return !(lhs < rhs); +} +static _FORCE_INLINE_ bool operator>(PlannerNode const &lhs, PlannerNode const &rhs) { + return lhs.open_requirements.size() > rhs.open_requirements.size(); +} +static _FORCE_INLINE_ bool operator<=(PlannerNode const &lhs, PlannerNode const &rhs) { + return !(lhs > rhs); } } } diff --git a/src/register_types.cpp b/src/register_types.cpp index d2ce3f3..701e673 100644 --- a/src/register_types.cpp +++ b/src/register_types.cpp @@ -1,9 +1,12 @@ #include "register_types.h" +#include "action.hpp" #include "character_data.hpp" #include "character_actor.hpp" #include "enemy.hpp" +#include "global_world_state.hpp" #include "health.hpp" #include "pellet_projectile.hpp" +#include "planner.hpp" #include "projectile_pool.hpp" #include "tunnels_game_mode.hpp" #include "tunnels_game_state.hpp" @@ -51,6 +54,11 @@ void initialize_gdextension_types(ModuleInitializationLevel p_level) ClassDB::register_class(); ClassDB::register_class(); + + ClassDB::register_class(); + ClassDB::register_class(); + ClassDB::register_class(); + ClassDB::register_class(); } extern "C" From 971c2dd2ff82c88934b914de1e56d71cc520a14e Mon Sep 17 00:00:00 2001 From: Sara Date: Wed, 27 Mar 2024 23:11:47 +0100 Subject: [PATCH 05/25] feat: put PlannerNode member fns in planner.cpp --- src/planner.cpp | 19 +++++++++++++++++++ src/planner.hpp | 19 ++----------------- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/planner.cpp b/src/planner.cpp index 6029b53..0940b95 100644 --- a/src/planner.cpp +++ b/src/planner.cpp @@ -31,6 +31,25 @@ Dictionary Goal::get_goal_state() const { #undef CLASSNAME // !Goal +PlannerNode PlannerNode::goal_node(WorldState const &goal) { + return PlannerNode{ + goal, {}, {} + }; +} + +PlannerNode PlannerNode::new_node_along(Ref action) const { + PlannerNode new_node{ + action->prerequisites, + action->effects, + action, + }; + for(WorldProperty const &prop : this->state) + new_node.state.insert(prop.key, prop.value); + for(WorldProperty const &prop : this->open_requirements) + new_node.open_requirements.insert(prop.key, prop.value); + return new_node; +} + void Planner::_bind_methods() { #define CLASSNAME Planner GDPROPERTY_HINTED(actions, Variant::ARRAY, PROPERTY_HINT_ARRAY_TYPE, GDRESOURCETYPE(Action)); diff --git a/src/planner.hpp b/src/planner.hpp index 6e84864..6fa4824 100644 --- a/src/planner.hpp +++ b/src/planner.hpp @@ -49,24 +49,9 @@ struct PlannerNode { PlannerNode() = default; PlannerNode(PlannerNode const &src) = default; - static PlannerNode goal_node(WorldState goal) { - return PlannerNode{ - goal, {}, {} - }; - } + static PlannerNode goal_node(WorldState const &goal); - PlannerNode new_node_along(Ref action) const { - PlannerNode new_node{ - action->prerequisites, - action->effects, - action, - }; - for(WorldProperty const &prop : this->state) - new_node.state.insert(prop.key, prop.value); - for(WorldProperty const &prop : this->open_requirements) - new_node.open_requirements.insert(prop.key, prop.value); - return new_node; - } + PlannerNode new_node_along(Ref action) const; }; class Planner : public Node { From 4c51a6053aa76288cdec07e3f1add2ef56b8b014 Mon Sep 17 00:00:00 2001 From: Sara Date: Wed, 27 Mar 2024 23:12:05 +0100 Subject: [PATCH 06/25] chore(docs): commenting pass on Planner::make_plan --- src/planner.cpp | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/planner.cpp b/src/planner.cpp index 0940b95..5520c65 100644 --- a/src/planner.cpp +++ b/src/planner.cpp @@ -87,24 +87,29 @@ Array Planner::gdscript_make_plan(Ref goal) { } Vector> Planner::make_plan(Ref goal) { - UtilityFunctions::print("run"); + // ordered list of all nodes still being considered Vector open{PlannerNode::goal_node(goal->goal_state)}; PlannerNode first = open.get(0); - FromMap from{}; - ScoreMap dist_traveled{}; - ScoreMap best_guess{}; + FromMap from{}; // mapping states to the previous in the path + ScoreMap dist_traveled{}; // mapping states to the shortest found distance from start dist_traveled.insert(first, 0); + ScoreMap best_guess{}; // mapping states to the best guess of the distance to the goal best_guess.insert(first, first.open_requirements.size()); - PlannerNode current{}; + PlannerNode current{}; // state we're checking for neighbours or completion while(!open.is_empty()) { + // 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); + // current is no longer considered as it cannot be the end open.erase(current); + // find all neighbours of this state Vector neighbours = this->find_neighbours_of(current); for(PlannerNode const& node : neighbours) { - float const new_dist = dist_traveled.get(current) + 1.f; + float const new_dist = dist_traveled.get(current) + 1.f; // unweighed distance traveled to neighbour if(!dist_traveled.has(node) || new_dist < dist_traveled.get(node)) { + // store distances dist_traveled[node] = new_dist; best_guess[node] = new_dist + node.open_requirements.size(); from[node] = current; From ad3292aafdb841e93e96fbe9605eba9a3fa1386f Mon Sep 17 00:00:00 2001 From: Sara Date: Wed, 27 Mar 2024 23:12:27 +0100 Subject: [PATCH 07/25] feat: removed optimize=none from debug make rule --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index fd31b3d..cfbd053 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ debug: - /usr/bin/scons debug_symbols=yes optimize=none + /usr/bin/scons debug_symbols=yes develop: /usr/bin/scons From bb4f870dae29f43cac248c65438b213e49e9700a Mon Sep 17 00:00:00 2001 From: Sara Date: Sat, 30 Mar 2024 22:59:57 +0100 Subject: [PATCH 08/25] feat: added the apply state to Action --- src/action.cpp | 22 ++++++++++++++++++++++ src/action.hpp | 8 ++++++++ 2 files changed, 30 insertions(+) diff --git a/src/action.cpp b/src/action.cpp index 7d8e515..909c3cc 100644 --- a/src/action.cpp +++ b/src/action.cpp @@ -1,5 +1,8 @@ #include "action.hpp" +#include "character_actor.hpp" +#include "global_world_state.hpp" #include "utils/godot_macros.h" +#include #include namespace godot::goap { @@ -8,6 +11,7 @@ void Action::_bind_methods() { GDPROPERTY(context_prerequisites, Variant::DICTIONARY); GDPROPERTY(prerequisites, Variant::DICTIONARY); GDPROPERTY(effects, Variant::DICTIONARY); + GDPROPERTY_HINTED(apply_state, Variant::OBJECT, PROPERTY_HINT_RESOURCE_TYPE, "StateArgs"); } void Action::set_context_prerequisites(Dictionary dict) { @@ -34,6 +38,24 @@ Dictionary Action::get_effects() const { return Action::property_map_to_dict(this->effects); } +void Action::set_apply_state(Ref state) { + this->apply_state = state; +} + +Ref Action::get_apply_state() const { + return this->apply_state; +} + +bool Action::get_is_completed(CharacterActor *context) { + GlobalWorldState *state = GlobalWorldState::get_singleton(); + for(WorldProperty const &prop : this->effects) { + return (prop.key.begins_with("g_") + ? state->get_world_property(prop.key) + : context->call("get_" + prop.key)) == prop.value; + } + return true; +} + Dictionary Action::property_map_to_dict(WorldState const &props) { Dictionary out{}; for(KeyValue const &prop : props) { diff --git a/src/action.hpp b/src/action.hpp index eac8d58..582c342 100644 --- a/src/action.hpp +++ b/src/action.hpp @@ -1,6 +1,7 @@ #ifndef GOAP_ACTION_HPP #define GOAP_ACTION_HPP +#include "state.hpp" #include #include #include @@ -8,6 +9,8 @@ #include #include +namespace godot { class CharacterActor; } + namespace godot::goap { typedef HashMap WorldState; typedef KeyValue WorldProperty; @@ -22,6 +25,10 @@ public: Dictionary get_prerequisites() const; void set_effects(Dictionary dict); Dictionary get_effects() const; + void set_apply_state(Ref args); + Ref get_apply_state() const; + + bool get_is_completed(CharacterActor *context); static Dictionary property_map_to_dict(WorldState const &props); static void dict_to_property_map(Dictionary const &dict, WorldState &out); @@ -29,6 +36,7 @@ public: WorldState context_prerequisites{}; WorldState prerequisites{}; WorldState effects{}; + Ref apply_state{}; }; } From b12cef310dea1c62ae82425a83950b0fccb86efa Mon Sep 17 00:00:00 2001 From: Sara Date: Sat, 30 Mar 2024 23:00:58 +0100 Subject: [PATCH 09/25] feat: moved State into it's own file --- src/planner.cpp | 1 - src/planner.hpp | 17 ---------- src/register_types.cpp | 7 +++- src/state.cpp | 76 ++++++++++++++++++++++++++++++++++++++++++ src/state.hpp | 65 ++++++++++++++++++++++++++++++++++++ 5 files changed, 147 insertions(+), 19 deletions(-) create mode 100644 src/state.cpp create mode 100644 src/state.hpp diff --git a/src/planner.cpp b/src/planner.cpp index 5520c65..4fe0f8c 100644 --- a/src/planner.cpp +++ b/src/planner.cpp @@ -2,7 +2,6 @@ #include "action.hpp" #include "global_world_state.hpp" #include "utils/godot_macros.h" -#include #include #include #include diff --git a/src/planner.hpp b/src/planner.hpp index 6fa4824..b197812 100644 --- a/src/planner.hpp +++ b/src/planner.hpp @@ -14,23 +14,6 @@ class CharacterActor; namespace goap { class GlobalWorldState; -struct State { - static State new_move_to(Vector3 location); - static State new_animate(StringName animation); - static State new_activate(Node *node); - enum Type { - STATE_MOVE_TO, - STATE_ANIMATE, - STATE_ACTIVATE - }; - State::Type type; - union { - Vector3 move_to; - StringName animate; - Node *activate; - }; -}; - class Goal : public Resource { GDCLASS(Goal, Resource); static void _bind_methods(); diff --git a/src/register_types.cpp b/src/register_types.cpp index 701e673..8a05f53 100644 --- a/src/register_types.cpp +++ b/src/register_types.cpp @@ -1,13 +1,14 @@ #include "register_types.h" #include "action.hpp" -#include "character_data.hpp" #include "character_actor.hpp" +#include "character_data.hpp" #include "enemy.hpp" #include "global_world_state.hpp" #include "health.hpp" #include "pellet_projectile.hpp" #include "planner.hpp" #include "projectile_pool.hpp" +#include "state.hpp" #include "tunnels_game_mode.hpp" #include "tunnels_game_state.hpp" #include "tunnels_player.hpp" @@ -57,6 +58,10 @@ void initialize_gdextension_types(ModuleInitializationLevel p_level) ClassDB::register_class(); ClassDB::register_class(); + ClassDB::register_abstract_class(); + ClassDB::register_class(); + ClassDB::register_class(); + ClassDB::register_class(); ClassDB::register_class(); ClassDB::register_class(); } diff --git a/src/state.cpp b/src/state.cpp new file mode 100644 index 0000000..165c3e7 --- /dev/null +++ b/src/state.cpp @@ -0,0 +1,76 @@ +#include "state.hpp" +#include "character_actor.hpp" +#include "utils/godot_macros.h" + +namespace godot::goap { +State::~State() { + if(unlikely(this->type == STATE_ANIMATE)) + delete this->animate; +} + +State State::new_move_to(Node3D *node) { + return { + .type = State::Type::STATE_MOVE_TO, + .move_to = node + }; +} + +State State::new_animate(StringName animation) { + return { + .type = State::Type::STATE_ANIMATE, + .animate = new StringName(animation) + }; +} + +State State::new_activate(Node *node) { + return { + .type = State::Type::STATE_ACTIVATE, + .activate = node + }; +} + +bool State::is_complete(CharacterActor *context) const { + switch(this->type) { + default: + return true; + case STATE_MOVE_TO: + return context->get_global_position().is_equal_approx(this->move_to->get_global_position()); + case STATE_ANIMATE: + return false; // TODO: replace this with checks for animation completion + case STATE_ACTIVATE: + return false; // TODO: replace this with checks for object activation + } +} + +void StateArgs::_bind_methods() { +#define CLASSNAME StateArgs + GDPROPERTY(argument_property, Variant::STRING_NAME); +} + +State StateArgs::construct(Node *context) const { + return { .type = State::STATE_TYPE_MAX }; +} + +void StateArgs::set_argument_property(StringName var) { this->argument_property = var; } +StringName StateArgs::get_argument_property() const { return this->argument_property; } + +void MoveStateArgs::_bind_methods() {} + +State MoveStateArgs::construct(Node *context) const { + Node3D *node = Object::cast_to(context->call("get_" + this->argument_property)); + return State::new_move_to(node); +} + +void AnimateStateArgs::_bind_methods() {} + +State AnimateStateArgs::construct(Node *context) const { + return State::new_animate(context->call("get_" + this->argument_property)); +} + +void ActivateStateArgs::_bind_methods() {} + +State ActivateStateArgs::construct(Node *context) const { + Node *node = Object::cast_to(context->call("get_" + this->argument_property)); + return State::new_activate(node); +} +} diff --git a/src/state.hpp b/src/state.hpp new file mode 100644 index 0000000..8a7e528 --- /dev/null +++ b/src/state.hpp @@ -0,0 +1,65 @@ +#ifndef GOAP_STATE_HPP +#define GOAP_STATE_HPP + +#include +#include +#include +#include +#include + +namespace godot { class CharacterActor; } +namespace godot::goap { +struct State { + ~State(); + static State new_move_to(Node3D *location); + static State new_animate(StringName animation); + static State new_activate(Node *node); + + bool is_complete(CharacterActor *context) const; + + enum Type { + STATE_MOVE_TO, + STATE_ANIMATE, + STATE_ACTIVATE, + STATE_TYPE_MAX, + }; + State::Type type{STATE_TYPE_MAX}; + union { + Node3D* move_to; + StringName *animate; + Node *activate; + }; +}; + +class StateArgs : public Resource { + GDCLASS(StateArgs, Resource); + static void _bind_methods(); +public: + virtual State construct(Node *context) const; + void set_argument_property(StringName name); + StringName get_argument_property() const; + StringName argument_property; +}; + +class MoveStateArgs : public StateArgs { + GDCLASS(MoveStateArgs, StateArgs); + static void _bind_methods(); + virtual State construct(Node *context) const override; +}; + +class AnimateStateArgs : public StateArgs { + GDCLASS(AnimateStateArgs, StateArgs); + static void _bind_methods(); +public: + virtual State construct(Node *context) const override; +}; + +class ActivateStateArgs : public StateArgs { + GDCLASS(ActivateStateArgs, StateArgs); + static void _bind_methods(); +public: + virtual State construct(Node *context) const override; +}; +}; + +#endif // !GOAP_STATE_HPP From 325afa7f116b334dfd917737b66f3fbc1898f5bb Mon Sep 17 00:00:00 2001 From: Sara Date: Sat, 30 Mar 2024 23:01:48 +0100 Subject: [PATCH 10/25] feat: added Goal prerequisites --- src/planner.cpp | 10 ++++++++++ src/planner.hpp | 3 +++ 2 files changed, 13 insertions(+) diff --git a/src/planner.cpp b/src/planner.cpp index 4fe0f8c..e09ab87 100644 --- a/src/planner.cpp +++ b/src/planner.cpp @@ -1,5 +1,6 @@ #include "planner.hpp" #include "action.hpp" +#include "character_actor.hpp" #include "global_world_state.hpp" #include "utils/godot_macros.h" #include @@ -18,6 +19,7 @@ typedef HashSet NodeSet; void Goal::_bind_methods() { #define CLASSNAME Goal GDPROPERTY(goal_state, Variant::DICTIONARY); + GDPROPERTY(prerequisites, Variant::DICTIONARY); } void Goal::set_goal_state(Dictionary dict) { @@ -28,6 +30,14 @@ Dictionary Goal::get_goal_state() const { return Action::property_map_to_dict(this->goal_state); } +void Goal::set_prerequisites(Dictionary dict) { + Action::dict_to_property_map(dict, this->prerequisites); +} + +Dictionary Goal::get_prerequisites() const { + return Action::property_map_to_dict(this->prerequisites); +} + #undef CLASSNAME // !Goal PlannerNode PlannerNode::goal_node(WorldState const &goal) { diff --git a/src/planner.hpp b/src/planner.hpp index b197812..ea3f4f7 100644 --- a/src/planner.hpp +++ b/src/planner.hpp @@ -20,8 +20,11 @@ class Goal : public Resource { void set_goal_state(Dictionary dict); Dictionary get_goal_state() const; + void set_prerequisites(Dictionary dict); + Dictionary get_prerequisites() const; public: WorldState goal_state{}; + WorldState prerequisites{}; }; struct PlannerNode { From 87d2cbd72757be49b5d974dd96058374c5afed55 Mon Sep 17 00:00:00 2001 From: Sara Date: Sat, 30 Mar 2024 23:02:07 +0100 Subject: [PATCH 11/25] chore: simplified PlannerNode::goal_node --- src/planner.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/planner.cpp b/src/planner.cpp index e09ab87..26d3ab3 100644 --- a/src/planner.cpp +++ b/src/planner.cpp @@ -41,9 +41,7 @@ Dictionary Goal::get_prerequisites() const { #undef CLASSNAME // !Goal PlannerNode PlannerNode::goal_node(WorldState const &goal) { - return PlannerNode{ - goal, {}, {} - }; + return PlannerNode{goal}; } PlannerNode PlannerNode::new_node_along(Ref action) const { From 60df329d1b10c4428fdba46cf47693e9fc7be569 Mon Sep 17 00:00:00 2001 From: Sara Date: Sat, 30 Mar 2024 23:03:12 +0100 Subject: [PATCH 12/25] chore: added comments, removed unused world_state_override --- src/planner.cpp | 2 -- src/planner.hpp | 12 ++++++------ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/planner.cpp b/src/planner.cpp index 26d3ab3..7bd7f1f 100644 --- a/src/planner.cpp +++ b/src/planner.cpp @@ -131,8 +131,6 @@ Vector> Planner::make_plan(Ref goal) { } Variant Planner::get_world_property(StringName prop_key) { - if(world_state_override.has(prop_key)) - return world_state_override.get(prop_key); if(prop_key.begins_with("g_")) return this->global_world_state->get_world_property(prop_key); if(this->cached_world_state.has(prop_key)) diff --git a/src/planner.hpp b/src/planner.hpp index ea3f4f7..01b93d5 100644 --- a/src/planner.hpp +++ b/src/planner.hpp @@ -58,13 +58,13 @@ public: void set_actions(Array actions); Array get_actions() const; -private: - CharacterActor *actor{nullptr}; - WorldState world_state_override{}; - WorldState cached_world_state{}; - GlobalWorldState *global_world_state{nullptr}; - Vector> actions{}; +private: + CharacterActor *actor{nullptr}; // the parent actor of this planner + WorldState cached_world_state{}; // the cached worldstate, cleared for every make_plan call + GlobalWorldState *global_world_state{nullptr}; // cached singleton instance + // configured settings + Vector> actions{}; // available actions }; struct PlannerNodeHasher { From 4847d82533619a782b0e4549627b858f47dac728 Mon Sep 17 00:00:00 2001 From: Sara Date: Sat, 30 Mar 2024 23:03:39 +0100 Subject: [PATCH 13/25] chore(formatting): put newline after _FORCE_INLINE_ for PlannerNodeHasher::hash --- src/planner.hpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/planner.hpp b/src/planner.hpp index 01b93d5..c79a888 100644 --- a/src/planner.hpp +++ b/src/planner.hpp @@ -68,7 +68,8 @@ private: }; struct PlannerNodeHasher { - static _FORCE_INLINE_ uint32_t hash(godot::goap::PlannerNode const &node) { + static _FORCE_INLINE_ + uint32_t hash(godot::goap::PlannerNode const &node) { uint32_t hash{1}; for(KeyValue const &kvp : node.state) { hash = hash_murmur3_one_32(kvp.key.hash(), hash); From 662ede04e94b4b811698ed3e2d28b1507e97a1c2 Mon Sep 17 00:00:00 2001 From: Sara Date: Sat, 30 Mar 2024 23:03:51 +0100 Subject: [PATCH 14/25] feat: added goals to Planner --- src/planner.hpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/planner.hpp b/src/planner.hpp index c79a888..f3d91a3 100644 --- a/src/planner.hpp +++ b/src/planner.hpp @@ -59,12 +59,15 @@ public: void set_actions(Array actions); Array get_actions() const; + void set_goals(Array goals); + Array get_goals() const; private: CharacterActor *actor{nullptr}; // the parent actor of this planner WorldState cached_world_state{}; // the cached worldstate, cleared for every make_plan call GlobalWorldState *global_world_state{nullptr}; // cached singleton instance // configured settings Vector> actions{}; // available actions + Vector> goals{}; // available goals }; struct PlannerNodeHasher { From e1fd4b8e8d2938a17a90c0901cd26ad90c0446d3 Mon Sep 17 00:00:00 2001 From: Sara Date: Sat, 30 Mar 2024 23:04:16 +0100 Subject: [PATCH 15/25] feat: removed Planner::_process --- src/planner.cpp | 5 +---- src/planner.hpp | 1 - 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/planner.cpp b/src/planner.cpp index 7bd7f1f..6c87542 100644 --- a/src/planner.cpp +++ b/src/planner.cpp @@ -65,10 +65,7 @@ void Planner::_bind_methods() { void Planner::_ready() { this->global_world_state = GlobalWorldState::get_singleton(); -} - -void Planner::_process(double delta_time) { - this->cached_world_state.clear(); + this->actor = Object::cast_to(this->get_parent()); } static Vector> trace_path(FromMap &map, PlannerNode &end) { diff --git a/src/planner.hpp b/src/planner.hpp index f3d91a3..25add97 100644 --- a/src/planner.hpp +++ b/src/planner.hpp @@ -45,7 +45,6 @@ class Planner : public Node { static void _bind_methods(); public: virtual void _ready() override; - virtual void _process(double delta_time) override; Array gdscript_make_plan(Ref goal); Vector> make_plan(Ref goal); From 8017194b0c024cd926fd12405b255ab0f4b21924 Mon Sep 17 00:00:00 2001 From: Sara Date: Sat, 30 Mar 2024 23:12:24 +0100 Subject: [PATCH 16/25] feat: implemented set/get_goals for Planner --- src/planner.cpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/planner.cpp b/src/planner.cpp index 6c87542..43b3955 100644 --- a/src/planner.cpp +++ b/src/planner.cpp @@ -194,4 +194,22 @@ Array Planner::get_actions() const { } return array; } + +void Planner::set_goals(Array value) { + this->goals.clear(); + this->goals.resize(value.size()); + for(size_t i{0}; i < value.size(); ++i) { + Ref goal = value[i]; + if(goal.is_valid()) + this->goals.set(i, goal); + } +} + +Array Planner::get_goals() const { + Array array{}; + for(Ref const &goal : this->goals) { + array.push_back(goal); + } + return array; +} } From 4dbeb77ad4070c360127e1a48196ce583248cb4e Mon Sep 17 00:00:00 2001 From: Sara Date: Sat, 30 Mar 2024 23:32:52 +0100 Subject: [PATCH 17/25] feat: Planner::cached_world_state now clears when make_plan is called --- src/planner.cpp | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/planner.cpp b/src/planner.cpp index 43b3955..dbf4340 100644 --- a/src/planner.cpp +++ b/src/planner.cpp @@ -91,6 +91,8 @@ Array Planner::gdscript_make_plan(Ref goal) { } Vector> Planner::make_plan(Ref goal) { + // clear cache every planning phase + this->cached_world_state.clear(); // ordered list of all nodes still being considered Vector open{PlannerNode::goal_node(goal->goal_state)}; PlannerNode first = open.get(0); @@ -128,16 +130,15 @@ Vector> Planner::make_plan(Ref goal) { } Variant Planner::get_world_property(StringName prop_key) { - if(prop_key.begins_with("g_")) + if(prop_key.begins_with("g_")) { return this->global_world_state->get_world_property(prop_key); - if(this->cached_world_state.has(prop_key)) + } else if(this->cached_world_state.has(prop_key)) { return this->cached_world_state.get(prop_key); - if(this->has_method("get_" + prop_key)) { - Variant val = this->call(prop_key); + } else if(this->actor->has_method("get_" + prop_key)) { + Variant val = this->actor->call("get_" + prop_key); this->cached_world_state.insert(prop_key, val); return val; - } - return nullptr; + } else return nullptr; } bool Planner::can_do(Ref action) { From 206f8a6e705ba58a30689096b1c95ab6babc0438 Mon Sep 17 00:00:00 2001 From: Sara Date: Sat, 30 Mar 2024 23:33:09 +0100 Subject: [PATCH 18/25] feat: added godot property for Planner::goals --- src/planner.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/planner.cpp b/src/planner.cpp index dbf4340..3e6808c 100644 --- a/src/planner.cpp +++ b/src/planner.cpp @@ -60,6 +60,7 @@ PlannerNode PlannerNode::new_node_along(Ref action) const { void Planner::_bind_methods() { #define CLASSNAME Planner GDPROPERTY_HINTED(actions, Variant::ARRAY, PROPERTY_HINT_ARRAY_TYPE, GDRESOURCETYPE(Action)); + GDPROPERTY_HINTED(goals, Variant::ARRAY, PROPERTY_HINT_ARRAY_TYPE, GDRESOURCETYPE(Goal)); GDFUNCTION_ARGS(gdscript_make_plan, "goal"); } From a1ee9f01608aca3b504fa8ad2362225a64d97913 Mon Sep 17 00:00:00 2001 From: Sara Date: Sat, 30 Mar 2024 23:33:47 +0100 Subject: [PATCH 19/25] feat: updated design document --- design/design-document.svg | 638 ++++++++++++++++++------------------- 1 file changed, 315 insertions(+), 323 deletions(-) diff --git a/design/design-document.svg b/design/design-document.svg index 91f41fa..ea79e93 100644 --- a/design/design-document.svg +++ b/design/design-document.svg @@ -24,10 +24,11 @@ inkscape:deskcolor="#d1d1d1" inkscape:document-units="mm" inkscape:zoom="0.845329" - inkscape:cx="7581.6634" - inkscape:cy="458.99289" + inkscape:cx="5903.0271" + inkscape:cy="437.6994" inkscape:current-layer="layer1" - showgrid="false"> + showgrid="false" + inkscape:export-bgcolor="#ffffffff"> + bleed="0" + inkscape:export-filename="../../../../Downloads/design-document.pdf" + inkscape:export-xdpi="96" + inkscape:export-ydpi="96" /> Modern civilisation has been wiped out. Most methods of long-distance communication have been destroyed or become unavailable. Factories have Modern civilisation has been wiped out. Most methods of long-distance communication have been destroyed or become unavailable. Factories have stopped running, and there is no government to take the lead. Those that remain have formed communes and towns in the ruins. + id="tspan4">stopped running, and there is no government to take the lead. Those that remain have formed communes and towns in the ruins. + id="tspan6"> In this particular city, people survived by sheltering in the underground subways. Now, several years after the end, they've become braver, and In this particular city, people survived by sheltering in the underground subways. Now, several years after the end, they've become braver, and started rebuilding. Their first order of business: reconnect the stations. By enabling the flow of resources and people between the various groups, started rebuilding. Their first order of business: reconnect the stations. By enabling the flow of resources and people between the various groups, they hope to improve the chances survivors have of making it through the end. + id="tspan24">they hope to improve the chances survivors have of making it through the end. + id="tspan26"> Player:Player: + id="tspan29"> A "combat railway engineer" who can maintain, drive, and repair trains and railway materiel. Because of the dangers lurking in the underground, A "combat railway engineer" who can maintain, drive, and repair trains and railway materiel. Because of the dangers lurking in the underground, these engineers are supported and trained as and by military personell. + id="tspan38">these engineers are supported and trained as and by military personell. + id="tspan40"> Objectives:Objectives: + id="tspan43"> The re-opening of the underground has various obstacles. The combat railway engineers are tasked with solving these. No matter what kind. The re-opening of the underground has various obstacles. The combat railway engineers are tasked with solving these. No matter what kind. Obstacles include: monsters in the tunnels, trains broken down, no trains available on a given line, broken rails, broken signals, and many more. Obstacles include: monsters in the tunnels, trains broken down, no trains available on a given line, broken rails, broken signals, and many more. Solving these problems will always require some combination of on-site problem solving and combat against the creatures that have spilled in from Solving these problems will always require some combination of on-site problem solving and combat against the creatures that have spilled in from the city surface. + id="tspan51">the city surface. + id="tspan53"> Resources:Resources: + id="tspan56"> Railway engineers and soldiers need extensive training, when one goes KIA that means one less person to fight in the next operation. Since the Railway engineers and soldiers need extensive training, when one goes KIA that means one less person to fight in the next operation. Since the apocalypse, weapons and spare parts are in short supply, the player will have to decide where to use what. + id="tspan60">apocalypse, weapons and spare parts are in short supply, the player will have to decide where to use what. + id="tspan62"> Mechanics:Mechanics: + id="tspan65"> Engineers are sent into the tunnels in squads, they'll have to communicate well and manage risks. The player and squad-members will each be Engineers are sent into the tunnels in squads, they'll have to communicate well and manage risks. The player and squad-members will each be carrying different tools, parts, and weapons. The player can shoot, and interact with things themselves, or order AI teammates to do those things carrying different tools, parts, and weapons. The player can shoot, and interact with things themselves, or order AI teammates to do those things instead, as long as the teammate has the right tools. + id="tspan71">instead, as long as the teammate has the right tools. + id="tspan73"> Dynamics:Dynamics: + id="tspan76"> During missions, the player will have to manage their time and resources well. You don't want to run out of ammunition while in the tunnels. This During missions, the player will have to manage their time and resources well. You don't want to run out of ammunition while in the tunnels. This means that the player will have to decide which teammember is the best fit for any given task. Whether to it themselves or to order someone else. means that the player will have to decide which teammember is the best fit for any given task. Whether to it themselves or to order someone else. Who can be missed, who has the right tools. Who has the right skills. + id="tspan82">Who can be missed, who has the right tools. Who has the right skills. + id="tspan84"> Conflict: + id="tspan86">Conflict: In the tunnels, there are many obstacles for the trains to start or continue running. There may be monsters, broken trains, broken rails, signals, or In the tunnels, there are many obstacles for the trains to start or continue running. There may be monsters, broken trains, broken rails, signals, or perhaps there are people who pull heists that need to be stopped. + id="tspan90">perhaps there are people who pull heists that need to be stopped. + id="tspan92"> Boundaries: + id="tspan94">Boundaries: The player is limited in their ability to explore the tunnels by resources and damage output. Deeper into the network the danger increases. And any The player is limited in their ability to explore the tunnels by resources and damage output. Deeper into the network the danger increases. And any lost team members are lost forever. + id="tspan98">lost team members are lost forever. + id="tspan100"> Outcome: + id="tspan102">Outcome: The player's team will be able to open up new routes, expanding the reach of the railway, as well as being able to find new recruits at newly The player's team will be able to open up new routes, expanding the reach of the railway, as well as being able to find new recruits at newly connected settlements. If the player dies in a tunnel, they will wake up at the nearest station, having been rescued. The tunnel they were in will connected settlements. If the player dies in a tunnel, they will wake up at the nearest station, having been rescued. The tunnel they were in will have been weakened and they will be able to retry. Though their own resources will have been lessened as well. + id="tspan113">have been weakened and they will be able to retry. Though their own resources will have been lessened as well. Mechanics + id="tspan115">Mechanics + id="tspan117"> The metro + id="tspan119">The metro As the metro expands, and trains start running, the player will have to navigate it. As the metro expands, and trains start running, the player will have to navigate it. Using a railway map annotated with connected, discovered, and unavailable Using a railway map annotated with connected, discovered, and unavailable tunnels and stations. The player can fast travel only along active lines. + id="tspan125">tunnels and stations. The player can fast travel only along active lines. + id="tspan127"> While travelling on the 'outer' lines (those close to not-yet-cleared tunnels). The While travelling on the 'outer' lines (those close to not-yet-cleared tunnels). The train may be attacked by monsters. + id="tspan131">train may be attacked by monsters. + id="tspan133"> Combat + id="tspan135">Combat The player and team are equipped with small arms and optionally explosives. The player and team are equipped with small arms and optionally explosives. Combat is on the slow side, with moving forward being dangerous and ill-Combat is on the slow side, with moving forward being dangerous and ill-adviced. The player use stations, side-tunnels and engineering tunnels to their adviced. The player use stations, side-tunnels and engineering tunnels to their advantage to take up a position. The players can use scarce consumable advantage to take up a position. The players can use scarce consumable equipment to create opportunities from such positions. + id="tspan145">equipment to create opportunities from such positions. + id="tspan147"> Excursions + id="tspan149">Excursions When moving into new territory, the player will first have to find out what might When moving into new territory, the player will first have to find out what might stop the trains from running through there. To do this, an engineering vehicle can stop the trains from running through there. To do this, an engineering vehicle can be taken into the tunnel to serve as a base of operations. The player will have to be taken into the tunnel to serve as a base of operations. The player will have to clear obstacles stopping the engineering vehicle from advancing. + id="tspan157">clear obstacles stopping the engineering vehicle from advancing. Dynamics + id="tspan159">Dynamics + id="tspan160"> The player will have to become familiar with the metro system. Always keeping The player will have to become familiar with the metro system. Always keeping track of where the active lines, outer lines and edge stations are. + id="tspan163">track of where the active lines, outer lines and edge stations are. + id="tspan164"> The player's ability to navigate the metro quickly is vital to their ability to both The player's ability to navigate the metro quickly is vital to their ability to both expand the network, and respond to incidents quickly. + id="tspan166">expand the network, and respond to incidents quickly. + id="tspan167"> Excursions are fights of endurance, monsters will be more numerous, and the Excursions are fights of endurance, monsters will be more numerous, and the advance slow. The engineering vehicle enables regular resupplies and allows the advance slow. The engineering vehicle enables regular resupplies and allows the player to specialize their squard more by letting them switch equipment more often. + id="tspan170">player to specialize their squard more by letting them switch equipment more often. + id="tspan177"> Scouting encourages speed and minimizing combat. Once trains have been brought Scouting encourages speed and minimizing combat. Once trains have been brought to the closest active station, the player will be able to prepare an excursion to clear to the closest active station, the player will be able to prepare an excursion to clear out the tunnel. Equipment for these missions will have to be general, as it is out the tunnel. Equipment for these missions will have to be general, as it is unknown what kinds of challenges may block the player's advance, with no way of unknown what kinds of challenges may block the player's advance, with no way of swapping equipment midway through. + id="tspan185">swapping equipment midway through. + id="tspan186"> By limiting outpost range based on transfers, the player is encouraged to place By limiting outpost range based on transfers, the player is encouraged to place them at intersections and transfers. Simultaneously the player will want to place them at intersections and transfers. Simultaneously the player will want to place them close to service stations (where the engineering train can stop). This tension them close to service stations (where the engineering train can stop). This tension can be exploited through level design. + id="tspan190">can be exploited through level design. + id="tspan191"> Early on, incidents will force the player to spread their own attention around. When Early on, incidents will force the player to spread their own attention around. When the player gains the ability to place more outposts, they will have to focus less of the player gains the ability to place more outposts, they will have to focus less of their own attention to this. Moving it from a micro task to a macro task. + id="tspan194">their own attention to this. Moving it from a micro task to a macro task. Such as broken rails or monsters. When some waypoint is reached (such as a station) the regular train's line extends. Allowing fast travel from other Such as broken rails or monsters. When some waypoint is reached (such as a station) the regular train's line extends. Allowing fast travel from other stations on the same line to that station. + id="tspan197">stations on the same line to that station. + id="tspan198"> Scouting + id="tspan199">Scouting When a new line is discovered, often the trains are not available. The player will then have to go into the tunnels on-foot to find a service station, When a new line is discovered, often the trains are not available. The player will then have to go into the tunnels on-foot to find a service station, repair a train there, and bring it back to an 'active' station. + id="tspan201">repair a train there, and bring it back to an 'active' station. + id="tspan202"> Incidents + id="tspan203">Incidents On the 'outer' lines, incidents can occur. Forcing a certain line out of service until the player can restore it. If the player cannot restore it, the line may On the 'outer' lines, incidents can occur. Forcing a certain line out of service until the player can restore it. If the player cannot restore it, the line may be lost again. + id="tspan205">be lost again. + id="tspan206"> Outposts + id="tspan207">Outposts Each line can have one outpost. The player can pick which station to put it on. An outpost's range is defined in number of transfers. An outpost can Each line can have one outpost. The player can pick which station to put it on. An outpost's range is defined in number of transfers. An outpost can respond to incidents within it's range. Outposts are also where the player can find new recruits. + id="tspan210">respond to incidents within it's range. Outposts are also where the player can find new recruits. + id="tspan212"> Engineering train + id="tspan213">Engineering train The train used for excursions. This can be driven by the player and will always be given right of way by other trains on the network. The player can The train used for excursions. This can be driven by the player and will always be given right of way by other trains on the network. The player can equip it and store materials in it. The engineering train can only be exited while stopped in un-cleared tunnels and service stations. + id="tspan220">equip it and store materials in it. The engineering train can only be exited while stopped in un-cleared tunnels and service stations. + id="tspan221"> Repair crews and trains + id="tspan222">Repair crews and trains Some places on the network are so severely damaged that the player's squad cannot fix it. To fix these the player gains the ability to dispatch repair Some places on the network are so severely damaged that the player's squad cannot fix it. To fix these the player gains the ability to dispatch repair crews to certain locations to repair extreme damage. These trains will however interupt service on the line they're repairing for a while. + id="tspan224">crews to certain locations to repair extreme damage. These trains will however interupt service on the line they're repairing for a while. Resources + id="tspan225">Resources + id="tspan226"> + id="tspan228"> + id="tspan230"> + id="tspan231"> + id="tspan232"> + id="tspan234"> + id="tspan239"> + id="tspan240"> + id="tspan243"> The number of different resource types the player has to manage increase over the course of the game. + id="tspan244">The number of different resource types the player has to manage increase over the course of the game. + id="tspan245"> To begin with, the player has to manage their squad's health (and consumables) and ammunition for various weapons. This includes having to To begin with, the player has to manage their squad's health (and consumables) and ammunition for various weapons. This includes having to manage squad members when they are injured or die. + id="tspan247">manage squad members when they are injured or die. + id="tspan248"> Next the player will gain the ability to assign engineers to outposts. The engineer's skills will impact how many recruits the outpost attracts, how Next the player will gain the ability to assign engineers to outposts. The engineer's skills will impact how many recruits the outpost attracts, how well trained they will be when they join, the range of the outpost, and the response speed. + id="tspan250">well trained they will be when they join, the range of the outpost, and the response speed. + id="tspan251"> While expanding, the player will come across a service station with maintainance trains. Which the player will then be able to assign to clear rubble While expanding, the player will come across a service station with maintainance trains. Which the player will then be able to assign to clear rubble and repair more extensive damage to the metro's systems. + id="tspan253">and repair more extensive damage to the metro's systems. + id="tspan254"> The player's primary method of acquiring new equipment and resources is through scrapping and crafting. The player can scrap specific parts from The player's primary method of acquiring new equipment and resources is through scrapping and crafting. The player can scrap specific parts from broken items, and if they can find the right parts they can combine them into some new item. The items are specific to the level of "assault rifle broken items, and if they can find the right parts they can combine them into some new item. The items are specific to the level of "assault rifle firing mechanism". So not "AK47 firing mechanism" nor "gun part". + id="tspan257">firing mechanism". So not "AK47 firing mechanism" nor "gun part". + id="tspan258"> Trains will inevitably break down. And without a consistent source of replacement parts, the player will have to venture into the tunnels to find Trains will inevitably break down. And without a consistent source of replacement parts, the player will have to venture into the tunnels to find them instead. + id="tspan260">them instead. + id="tspan261"> List of resources: + id="tspan262">List of resources: - Health and ammo: these are mainly important for on-the-ground combat. They are both ticking down to an excursion's failure. + id="tspan263">- Health and ammo: these are mainly important for on-the-ground combat. They are both ticking down to an excursion's failure. - People: this is the big one the player needs to balance. Assigning engineers to increasingly varied posts. + id="tspan264">- People: this is the big one the player needs to balance. Assigning engineers to increasingly varied posts. - Maintainance trains: these will be in short supply, only ever ticking up to at most 3 in late game. + id="tspan265">- Maintainance trains: these will be in short supply, only ever ticking up to at most 3 in late game. - Regular trains: these will be unlocked shortly after finding a new line (see: scouting) + id="tspan266">- Regular trains: these will be unlocked shortly after finding a new line (see: scouting) - Weapon/Equipment parts. + id="tspan267">- Weapon/Equipment parts. - Train repair parts. + id="tspan268">- Train repair parts. Objectives + id="tspan269">Objectives + id="tspan270"> The player's and settlement's resources are always running out. And there's no way of producing more at the rate required for self-The player's and settlement's resources are always running out. And there's no way of producing more at the rate required for self-sustainability. The player needs to expand the network to survive. But as the network grows, the resource pressure grows as well. + id="tspan272">sustainability. The player needs to expand the network to survive. But as the network grows, the resource pressure grows as well. + id="tspan273"> To get more resources, the player will have to explore the tunnels and increase the number of stations connected. The player can also go into To get more resources, the player will have to explore the tunnels and increase the number of stations connected. The player can also go into "dark zones" areas of the network that cannot be connected to the main network due to large caveins and failed tracks. These areas will be "dark zones" areas of the network that cannot be connected to the main network due to large caveins and failed tracks. These areas will be more dangerous, but also have more resources. + id="tspan276">more dangerous, but also have more resources. + id="tspan277"> + id="tspan278"> Conflict + id="tspan279">Conflict + id="tspan280"> In the tunnels, managing resources is vital. The primary danger to those living there is running out of vital supplies like food, medicine, and In the tunnels, managing resources is vital. The primary danger to those living there is running out of vital supplies like food, medicine, and water. Without trains to run the metro, or tracks to carry them, the people of the metro are left with dwindling supplies and no fast way of water. Without trains to run the metro, or tracks to carry them, the people of the metro are left with dwindling supplies and no fast way of replenishing them. + id="tspan283">replenishing them. + id="tspan284"> Repairing the tracks is dangerous work, even just transporting replacement parts can bring unwanted attention. Attacks are common, and Repairing the tracks is dangerous work, even just transporting replacement parts can bring unwanted attention. Attacks are common, and repairing one line requires an extended battle of attrition to get to all the broken parts of the railway and repair them. + id="tspan286">repairing one line requires an extended battle of attrition to get to all the broken parts of the railway and repair them. + id="tspan287"> Some parts of the railway are rendered permanently inaccessible. This is where the most danger, and the majority of the remaining resources, Some parts of the railway are rendered permanently inaccessible. This is where the most danger, and the majority of the remaining resources, can be found. Because these parts of the metro are difficult to get to, very few people have managed to get into them yet, and thus a lot of can be found. Because these parts of the metro are difficult to get to, very few people have managed to get into them yet, and thus a lot of materials are left ungathered. This is also where it may be possible to find spare train parts, as the trains there are of no use to the metro materials are left ungathered. This is also where it may be possible to find spare train parts, as the trains there are of no use to the metro without connections. + id="tspan291">without connections. Core Ideas and Pillars + id="tspan292">Core Ideas and Pillars + id="tspan294"> + id="tspan296">Collectively making something from the scraps ... + id="tspan298"> Of a broken world, of the metro, of society, etc. Collectively making something from the scraps ... + id="tspan302"> Of a broken world, of the metro, of society, etc. + id="tspan304">This phrase should be the driving force. People working together to build something new from what remains in the metro. + id="tspan306"> This phrase should be the driving force. People working together to build something new from what remains in the metro. + id="tspan308">Everyone working together to survive + id="tspan310">This means no economy, no human enemies. In general, all humans should be trying to help eachother survive. Everyone working together to survive -It does not mean there can't be distrust or conflict between humans, but it does mean that those should be storylines that can be resolved with This means no economy, no human enemies. In general, all humans should be trying to help eachother survive. + id="tspan314">collaboration and gaining trust It does not mean there can't be distrust or conflict between humans, but it does mean that those should be storylines that can be resolved with In gameplay it means that fucking people over can *never* be the optimal solution to a problem. +collaboration and gaining trust + id="tspan318"> In gameplay it means that fucking people over can *never* be the optimal solution to a problem. + id="tspan320">Something new -Not just a return to form, but an attempt at something new. In early stages, when the player and people of the metro are not yet "safe" this should still Something new + id="tspan324">be far off. But it should be the "end goal" of the game, to not just repair what was lost, but make something new. Not just a return to form, but an attempt at something new. In early stages, when the player and people of the metro are not yet "safe" this should still +be far off. But it should be the "end goal" of the game, to not just repair what was lost, but make something new. + id="tspan328">Anarchy -The aftermath of the apocalypse has no government, only a shared will to survive. The player will be a leader of sorts, but not in any official fashion. Anarchy + id="tspan332">The player character is a respected expert who's opinion is valued enough to let them make calls on the organization of the metro. The aftermath of the apocalypse has no government, only a shared will to survive. The player will be a leader of sorts, but not in any official fashion. +The player character is a respected expert who's opinion is valued enough to let them make calls on the organization of the metro. + id="tspan336">ludonarative progression -ludonarative progression -Surviving off what is left -> Repairing -> Rebuilding -> Trying the same thing in a changed environment fails -> Making something new + id="tspan338">Surviving off what is left -> Repairing -> Rebuilding -> Trying the same thing in a changed environment fails -> Making something new Team member personality + id="tspan340">Team member personality Engineers have their own personalities that inform their behaviour in combat situations. This informs how they react to stress, and how they Engineers have their own personalities that inform their behaviour in combat situations. This informs how they react to stress, and how they approach combat situations. + id="tspan342">approach combat situations. + id="tspan343"> Team member skills + id="tspan344">Team member skills Some engineers are more adept at certain tasks than others. This might impact things like accuracy with a weapon type. + id="tspan345">Some engineers are more adept at certain tasks than others. This might impact things like accuracy with a weapon type. + id="tspan346"> Team member tension/stress + id="tspan347">Team member tension/stress As a combat situation worsens your team will become more stressed. Different engineers will react to stress in different ways according to their As a combat situation worsens your team will become more stressed. Different engineers will react to stress in different ways according to their personality. + id="tspan349">personality. From d3b9e54b0ba16a32b4f124dd4d0ef4a1a9a5a7b2 Mon Sep 17 00:00:00 2001 From: Sara Date: Sat, 30 Mar 2024 23:34:16 +0100 Subject: [PATCH 20/25] feat: removed script from player_character.tscn --- godot/player_character.tscn | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/godot/player_character.tscn b/godot/player_character.tscn index 32b6ea6..e43c534 100644 --- a/godot/player_character.tscn +++ b/godot/player_character.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=10 format=3 uid="uid://dpda341t6ipiv"] +[gd_scene load_steps=12 format=3 uid="uid://dpda341t6ipiv"] [sub_resource type="Curve" id="Curve_7rmf4"] min_value = 0.2 @@ -18,16 +18,23 @@ 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_752r2"] +argument_property = &"target_position" + [sub_resource type="Action" id="Action_ksl64"] effects = { "prereq": true } +apply_state = SubResource("MoveStateArgs_752r2") [sub_resource type="Action" id="Action_mdru6"] effects = { "not_goal": 1 } +[sub_resource type="AnimateStateArgs" id="AnimateStateArgs_tlart"] +argument_property = &"fire_weapon" + [sub_resource type="Action" id="Action_7v1i5"] prerequisites = { "prereq": true @@ -35,23 +42,15 @@ prerequisites = { effects = { "goal": true } +apply_state = SubResource("AnimateStateArgs_tlart") -[sub_resource type="GDScript" id="GDScript_ncqfl"] -script/source = "extends Planner - -func get_goal() -> bool: - return false - -func get_prereq() -> bool: - return false - -func get_not_goal() -> int: - return 0 - -func _ready(): - var goal : Goal = ResourceLoader.load(\"res://new_goal.tres\") - var plan : Array = self.gdscript_make_plan(goal) -" +[sub_resource type="Goal" id="Goal_sqtwb"] +goal_state = { +"target_dead": true +} +prerequisites = { +"has_target": true +} [node name="PlayerCharacter" type="CharacterActor"] rotation_speed_curve = SubResource("Curve_7rmf4") @@ -85,4 +84,4 @@ 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_ksl64"), SubResource("Action_mdru6"), SubResource("Action_7v1i5")] -script = SubResource("GDScript_ncqfl") +goals = [SubResource("Goal_sqtwb")] From 56f0e3bc87c16038a184353d787cf7c62c174fce Mon Sep 17 00:00:00 2001 From: Sara Date: Sun, 31 Mar 2024 18:15:47 +0200 Subject: [PATCH 21/25] feat: character actor velocity is now applied only when on the floor --- src/character_actor.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/character_actor.cpp b/src/character_actor.cpp index e22fb60..211e40b 100644 --- a/src/character_actor.cpp +++ b/src/character_actor.cpp @@ -34,9 +34,9 @@ void CharacterActor::_process(double delta_time) { GDGAMEONLY(); void CharacterActor::_physics_process(double delta_time) { GDGAMEONLY(); // accelerate towards velocity target Vector3 const new_velocity = this->get_velocity().move_toward(this->velocity_target, delta_time * CharacterActor::ACCELERATION); - // only apply velocity if not grounded - Vector3 const gravity{this->is_on_floor() ? Vector3() : Vector3{0.f, this->get_velocity().y - 9.8f, 0.f}}; - this->set_velocity(new_velocity + gravity); + Vector3 const gravity{Vector3{0.f, this->get_velocity().y - 9.8f, 0.f}}; + // apply either gravity or walking velocity depending on results + this->set_velocity(this->is_on_floor() ? new_velocity : this->get_velocity() + gravity); // update position this->move_and_slide(); } From 317b7f877ae97e73602bdee0701c3dcae298dff0 Mon Sep 17 00:00:00 2001 From: Sara Date: Sun, 31 Mar 2024 18:16:05 +0200 Subject: [PATCH 22/25] feat: no longer checking for proximity in CharacterActor::move_to --- src/character_actor.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/character_actor.cpp b/src/character_actor.cpp index 211e40b..035c88a 100644 --- a/src/character_actor.cpp +++ b/src/character_actor.cpp @@ -65,9 +65,7 @@ void CharacterActor::aim_direction(Vector3 direction) { void CharacterActor::move_to(Vector3 to, float target_distance) { this->nav_agent->set_target_desired_distance(target_distance); - this->nav_agent->set_target_position(this->get_global_position().distance_squared_to(to) < target_distance * target_distance - ? this->get_global_position() - : to); + this->nav_agent->set_target_position(to); } void CharacterActor::shoot_at(Vector3 at) { From dc56c7bc52b94041015c566ce8e80785a7b317eb Mon Sep 17 00:00:00 2001 From: Sara Date: Sun, 31 Mar 2024 18:16:32 +0200 Subject: [PATCH 23/25] chore(formatting): added brackets for consistency --- src/character_actor.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/character_actor.cpp b/src/character_actor.cpp index 035c88a..8da7733 100644 --- a/src/character_actor.cpp +++ b/src/character_actor.cpp @@ -27,8 +27,9 @@ void CharacterActor::_process(double delta_time) { GDGAMEONLY(); if(!this->mode_manual) { this->process_ai(delta_time); } - if(this->firing) + if(this->firing) { this->try_fire_weapon(); + } } void CharacterActor::_physics_process(double delta_time) { GDGAMEONLY(); From 540d91ddcf5ea783c82f35c92c9ab087e161c136 Mon Sep 17 00:00:00 2001 From: Sara Date: Sun, 31 Mar 2024 18:17:29 +0200 Subject: [PATCH 24/25] feat: commenting an cleanup pass character_actor.hpp --- src/character_actor.hpp | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/src/character_actor.hpp b/src/character_actor.hpp index 686964b..9ee2f77 100644 --- a/src/character_actor.hpp +++ b/src/character_actor.hpp @@ -3,6 +3,7 @@ #include "character_data.hpp" #include "health.hpp" +#include "state.hpp" #include "projectile_pool.hpp" #include #include @@ -19,11 +20,19 @@ public: virtual void _enter_tree() override; virtual void _process(double delta_time) override; virtual void _physics_process(double delta_time) override; + // manually set target_velocity void move(Vector3 world_vector); + // manually aim at a target position + // calls aim_direction with the flattened direction to 'at' void aim(Vector3 at); + // manually set the forward vector of target_rotation void aim_direction(Vector3 direction); + // set a movement target to navigate towards void move_to(Vector3 to, float target_distance = 0.5f); + // fire weapon at a target position + // calls aim(at) and set_firing(true) void shoot_at(Vector3 at); + // getter-setters void set_firing(bool firing); void set_manual_mode(bool value); @@ -44,19 +53,31 @@ protected: void process_rotation(double delta_time); void try_fire_weapon(); private: + // desired velocity, accelerated towards each frame Vector3 velocity_target{0.f,0.f,0.f}; + // target rotation, slerped towards each frame Basis target_rotation{}; - NavigationAgent3D *nav_agent{nullptr}; + // ignore any ai planning or navigation bool mode_manual{false}; + // fire weapon at whatever we're aiming at + bool firing{false}; + // the next timestamp at which a weapon can be fired + float fire_timer{0.f}; + // the origin point for projectiles + Node3D *weapon_muzzle{nullptr}; + // whatever the AI is currently targetting + Node *target{nullptr}; + // the current state of the actor + goap::State current_state{}; // the current state + Health *health{nullptr}; ProjectilePool *primary_weapon_pool{nullptr}; - Ref data; - float fire_interval{0.f}; - bool firing{false}; - float fire_timer{0.f}; - Node3D *weapon_muzzle{nullptr}; + NavigationAgent3D *nav_agent{nullptr}; Ref rotation_speed_curve{}; + // character data assigned when spawned + Ref data; + float fire_interval{0.f}; // derived from the current weapon's rpm static float const ACCELERATION; static float const WALK_SPEED; From ea730d61b45803735779efedf3a57fca3dfdc453 Mon Sep 17 00:00:00 2001 From: Sara Date: Mon, 1 Apr 2024 23:26:28 +0200 Subject: [PATCH 25/25] feat: implemented last bits required to create a 'follow player' goal --- godot/player_character.tscn | 35 ++++++--------------- src/character_actor.cpp | 63 ++++++++++++++++++++++++++++++++++--- src/character_actor.hpp | 24 ++++++++------ src/planner.cpp | 50 ++++++++++++++++++++--------- src/planner.hpp | 9 ++++-- src/state.cpp | 4 +++ src/state.hpp | 1 + 7 files changed, 128 insertions(+), 58 deletions(-) diff --git a/godot/player_character.tscn b/godot/player_character.tscn index e43c534..924af26 100644 --- a/godot/player_character.tscn +++ b/godot/player_character.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=12 format=3 uid="uid://dpda341t6ipiv"] +[gd_scene load_steps=9 format=3 uid="uid://dpda341t6ipiv"] [sub_resource type="Curve" id="Curve_7rmf4"] min_value = 0.2 @@ -18,38 +18,21 @@ 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_752r2"] -argument_property = &"target_position" +[sub_resource type="MoveStateArgs" id="MoveStateArgs_ibmkn"] +argument_property = &"player_character" -[sub_resource type="Action" id="Action_ksl64"] +[sub_resource type="Action" id="Action_gtisq"] effects = { -"prereq": true +"is_near_player": true } -apply_state = SubResource("MoveStateArgs_752r2") - -[sub_resource type="Action" id="Action_mdru6"] -effects = { -"not_goal": 1 -} - -[sub_resource type="AnimateStateArgs" id="AnimateStateArgs_tlart"] -argument_property = &"fire_weapon" - -[sub_resource type="Action" id="Action_7v1i5"] -prerequisites = { -"prereq": true -} -effects = { -"goal": true -} -apply_state = SubResource("AnimateStateArgs_tlart") +apply_state = SubResource("MoveStateArgs_ibmkn") [sub_resource type="Goal" id="Goal_sqtwb"] goal_state = { -"target_dead": true +"is_near_player": true } prerequisites = { -"has_target": true +"is_near_player": false } [node name="PlayerCharacter" type="CharacterActor"] @@ -83,5 +66,5 @@ surface_material_override/0 = SubResource("StandardMaterial3D_scmx3") 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_ksl64"), SubResource("Action_mdru6"), SubResource("Action_7v1i5")] +actions = [SubResource("Action_gtisq")] goals = [SubResource("Goal_sqtwb")] diff --git a/src/character_actor.cpp b/src/character_actor.cpp index 8da7733..525b294 100644 --- a/src/character_actor.cpp +++ b/src/character_actor.cpp @@ -1,5 +1,9 @@ #include "character_actor.hpp" +#include "planner.hpp" #include "projectile_pool.hpp" +#include "state.hpp" +#include "tunnels_game_mode.hpp" +#include "utils/game_root.hpp" #include "utils/godot_macros.h" #include #include @@ -12,6 +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"); + GDFUNCTION(get_is_near_player); + GDFUNCTION(get_player_character); } void CharacterActor::_enter_tree() { GDGAMEONLY(); @@ -20,12 +26,14 @@ void CharacterActor::_enter_tree() { GDGAMEONLY(); this->target_rotation = this->get_global_transform().get_basis().get_quaternion(); this->health = this->get_node("Health"); this->primary_weapon_pool = this->get_node("ProjectilePool"); + this->planner = this->get_node("Planner"); } void CharacterActor::_process(double delta_time) { GDGAMEONLY(); this->process_rotation(delta_time); if(!this->mode_manual) { - this->process_ai(delta_time); + this->process_behaviour(delta_time); + this->process_navigation(delta_time); } if(this->firing) { this->try_fire_weapon(); @@ -83,6 +91,7 @@ void CharacterActor::set_manual_mode(bool value) { ProcessMode const mode = value ? ProcessMode::PROCESS_MODE_DISABLED : ProcessMode::PROCESS_MODE_PAUSABLE; //this->nav_agent->set_process_mode(mode); this->nav_agent->set_avoidance_priority(value ? 1.f : 0.9f); + this->set_state(goap::State::new_invalid()); } void CharacterActor::set_rotation_speed_curve(Ref curve) { @@ -119,10 +128,54 @@ Vector3 CharacterActor::get_velocity_target() const { return this->velocity_target; } -void CharacterActor::process_ai(double delta_time) { - float const distance = this->nav_agent->get_target_position().distance_squared_to(this->get_global_position()); - float const target_distance_sqr = std::pow(this->nav_agent->get_target_desired_distance(), 2.f); - if(!this->nav_agent->is_navigation_finished() && distance >= target_distance_sqr) { +bool CharacterActor::get_is_near_player() const { + return this->get_player_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(); +} + +void CharacterActor::set_state(goap::State state) { + switch(this->current_state.type) { + default: + break; + case goap::State::STATE_MOVE_TO: + this->nav_agent->set_target_position(this->get_global_position()); + break; + } + this->current_state = state; + switch(state.type) { + default: + break; + case goap::State::STATE_MOVE_TO: + this->move_to(state.move_to->get_global_position()); + break; + } +} + +void CharacterActor::process_behaviour(double delta_time) { + if(this->current_state.is_complete(this) || this->planner->is_action_complete()) + this->set_state(this->planner->get_next_state()); + switch(this->current_state.type) { + default: + break; + case goap::State::STATE_MOVE_TO: + if(this->nav_agent->get_target_position().distance_to(this->current_state.move_to->get_global_position()) > 2.f) + this->nav_agent->set_target_position(this->current_state.move_to->get_global_position()); + break; + case goap::State::STATE_ACTIVATE: + break; + case goap::State::STATE_ANIMATE: + break; + } +} + +void CharacterActor::process_navigation(double delta_time) { + float const distance_sqr = this->nav_agent->get_target_position().distance_squared_to(this->get_global_position()); + float const distance_target_sqr = std::pow(this->nav_agent->get_target_desired_distance(), 2.f); + if(!this->nav_agent->is_navigation_finished() && distance_sqr >= distance_target_sqr) { Vector3 const target_position = this->nav_agent->get_next_path_position(); Vector3 const direction = (target_position - this->get_global_position()).normalized(); if(this->nav_agent->get_avoidance_enabled()) diff --git a/src/character_actor.hpp b/src/character_actor.hpp index 9ee2f77..1d83bce 100644 --- a/src/character_actor.hpp +++ b/src/character_actor.hpp @@ -3,14 +3,18 @@ #include "character_data.hpp" #include "health.hpp" -#include "state.hpp" #include "projectile_pool.hpp" +#include "state.hpp" #include #include namespace godot { class NavigationAgent3D; class TunnelsPlayer; +class AnimationPlayer; +namespace goap { + class Planner; +}; class CharacterActor : public CharacterBody3D, public IHealthEntity { @@ -38,18 +42,18 @@ public: 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; + void set_state(goap::State state); protected: - void process_ai(double delta_time); + void process_behaviour(double delta_time); + void process_navigation(double delta_time); void process_rotation(double delta_time); void try_fire_weapon(); private: @@ -65,19 +69,21 @@ private: float fire_timer{0.f}; // the origin point for projectiles Node3D *weapon_muzzle{nullptr}; - // whatever the AI is currently targetting + // something that the AI wants to target Node *target{nullptr}; // the current state of the actor - goap::State current_state{}; // the current state + goap::State current_state{goap::State::new_invalid()}; + AnimationPlayer *anim_player{nullptr}; Health *health{nullptr}; ProjectilePool *primary_weapon_pool{nullptr}; NavigationAgent3D *nav_agent{nullptr}; + goap::Planner *planner{nullptr}; Ref rotation_speed_curve{}; // character data assigned when spawned Ref data; - float fire_interval{0.f}; // derived from the current weapon's rpm + float fire_interval{0.f}; // derived from 1 / the current weapon's rps static float const ACCELERATION; static float const WALK_SPEED; diff --git a/src/planner.cpp b/src/planner.cpp index 3e6808c..cdd1235 100644 --- a/src/planner.cpp +++ b/src/planner.cpp @@ -2,6 +2,7 @@ #include "action.hpp" #include "character_actor.hpp" #include "global_world_state.hpp" +#include "state.hpp" #include "utils/godot_macros.h" #include #include @@ -61,7 +62,6 @@ void Planner::_bind_methods() { #define CLASSNAME Planner GDPROPERTY_HINTED(actions, Variant::ARRAY, PROPERTY_HINT_ARRAY_TYPE, GDRESOURCETYPE(Action)); GDPROPERTY_HINTED(goals, Variant::ARRAY, PROPERTY_HINT_ARRAY_TYPE, GDRESOURCETYPE(Goal)); - GDFUNCTION_ARGS(gdscript_make_plan, "goal"); } void Planner::_ready() { @@ -79,21 +79,12 @@ static Vector> trace_path(FromMap &map, PlannerNode &end) { return edges; } -Array Planner::gdscript_make_plan(Ref goal) { - Vector> plan = this->make_plan(goal); - Array out{}; - int i{0}; - UtilityFunctions::print("plan len: ", plan.size()); - for(Ref const &action : plan) { - out.push_back(action); - UtilityFunctions::print("plan[", i++, "]: ", this->actions.find(action)); - } - return out; -} - -Vector> Planner::make_plan(Ref goal) { +Vector> Planner::make_plan() { // clear cache every planning phase this->cached_world_state.clear(); + Ref goal = this->select_goal(); + if(!goal.is_valid()) + return {}; // ordered list of all nodes still being considered Vector open{PlannerNode::goal_node(goal->goal_state)}; PlannerNode first = open.get(0); @@ -130,6 +121,20 @@ Vector> Planner::make_plan(Ref goal) { return {}; } +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; + } + return {}; +} + Variant Planner::get_world_property(StringName prop_key) { if(prop_key.begins_with("g_")) { return this->global_world_state->get_world_property(prop_key); @@ -172,13 +177,28 @@ Vector> Planner::find_actions_satisfying(WorldState requirements) { for(WorldProperty &prop : requirements) { if(act->effects.has(prop.key) && act->effects.get(prop.key) == prop.value - && this->can_do(act)) + && this->can_do(act)) { found_actions.push_back(act); + } } } return found_actions; } +bool Planner::is_action_complete() { + return this->plan.get(0)->get_is_completed(this->actor); +} + +State Planner::get_next_state() { + if(!this->plan.is_empty()) + this->plan.remove_at(0); + if(this->plan.is_empty()) + this->plan = this->make_plan(); + if(this->plan.is_empty()) + return State::new_invalid(); + return this->plan.get(0)->apply_state->construct(this->actor); +} + void Planner::set_actions(Array value) { this->actions.clear(); this->actions.resize(value.size()); diff --git a/src/planner.hpp b/src/planner.hpp index 25add97..9bf9e73 100644 --- a/src/planner.hpp +++ b/src/planner.hpp @@ -46,8 +46,8 @@ class Planner : public Node { public: virtual void _ready() override; - Array gdscript_make_plan(Ref goal); - Vector> make_plan(Ref goal); + Vector> make_plan(); + Ref select_goal(); Variant get_world_property(StringName prop_key); @@ -55,9 +55,11 @@ public: Vector find_neighbours_of(PlannerNode &node); Vector> find_actions_satisfying(WorldState requirements); + bool is_action_complete(); + State get_next_state(); + void set_actions(Array actions); Array get_actions() const; - void set_goals(Array goals); Array get_goals() const; private: @@ -67,6 +69,7 @@ private: // configured settings Vector> actions{}; // available actions Vector> goals{}; // available goals + Vector> plan{}; }; struct PlannerNodeHasher { diff --git a/src/state.cpp b/src/state.cpp index 165c3e7..bdb1900 100644 --- a/src/state.cpp +++ b/src/state.cpp @@ -29,6 +29,10 @@ State State::new_activate(Node *node) { }; } +State State::new_invalid() { + return { .type = State::Type::STATE_TYPE_MAX }; +} + bool State::is_complete(CharacterActor *context) const { switch(this->type) { default: diff --git a/src/state.hpp b/src/state.hpp index 8a7e528..1e9986c 100644 --- a/src/state.hpp +++ b/src/state.hpp @@ -14,6 +14,7 @@ struct State { static State new_move_to(Node3D *location); static State new_animate(StringName animation); static State new_activate(Node *node); + static State new_invalid(); bool is_complete(CharacterActor *context) const;