From fdfde706e0ea83413d638c5791c2fa3e964ba7ab Mon Sep 17 00:00:00 2001 From: Sara Date: Wed, 27 Mar 2024 01:22:31 +0100 Subject: [PATCH] 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"