From 844b905174563a63e5e0c71fc5684211230ccc43 Mon Sep 17 00:00:00 2001 From: Sara <sara@saragerretsen.nl> Date: Fri, 31 May 2024 16:06:15 +0200 Subject: [PATCH] feat: Started defining GOAP concepts --- src/goap/action.cpp | 37 +++++++++++++++++++ src/goap/action.hpp | 65 ++++++++++++++++++++++++++++++++++ src/goap/action_db.cpp | 36 +++++++++++++++++++ src/goap/action_db.hpp | 55 ++++++++++++++++++++++++++++ src/goap/actor_world_state.cpp | 7 ++++ src/goap/actor_world_state.hpp | 21 +++++++++++ src/goap/goal.hpp | 24 +++++++++++++ src/goap/planner.cpp | 34 ++++++++++++++++++ src/goap/planner.hpp | 32 +++++++++++++++++ src/goap/state.cpp | 32 +++++++++++++++++ src/goap/state.hpp | 48 +++++++++++++++++++++++++ 11 files changed, 391 insertions(+) create mode 100644 src/goap/action.cpp create mode 100644 src/goap/action.hpp create mode 100644 src/goap/action_db.cpp create mode 100644 src/goap/action_db.hpp create mode 100644 src/goap/actor_world_state.cpp create mode 100644 src/goap/actor_world_state.hpp create mode 100644 src/goap/goal.hpp create mode 100644 src/goap/planner.cpp create mode 100644 src/goap/planner.hpp create mode 100644 src/goap/state.cpp create mode 100644 src/goap/state.hpp diff --git a/src/goap/action.cpp b/src/goap/action.cpp new file mode 100644 index 0000000..7011195 --- /dev/null +++ b/src/goap/action.cpp @@ -0,0 +1,37 @@ +#include "action.hpp" + +namespace goap { +Action::Action(WorldState &requires, WorldState &effects) +: requires{requires}, effects{effects} {} + +WorldState const &Action::get_required() const { + return this->requires; +} + +WorldState const &Action::get_effects() const { + return this->effects; +} + +ActionID Action::get_id() const { + return this->id; +} + +ActionData::ActionData(Action *action) +: data{action} {} + +ActionData::~ActionData() { + if(this->data != nullptr) + delete this->data; +} +} + +bool TestAction::is_done_for(goap::ActorWorldState *) { + return true; +} +bool TestAction::is_possible_for(goap::ActorWorldState *) { + return true; +} +goap::State *TestAction::get_apply_state() const { + return nullptr; +} + diff --git a/src/goap/action.hpp b/src/goap/action.hpp new file mode 100644 index 0000000..0c6913a --- /dev/null +++ b/src/goap/action.hpp @@ -0,0 +1,65 @@ +#ifndef GOAP_ACTION_HPP +#define GOAP_ACTION_HPP + +#include "actor_world_state.hpp" +#include "state.hpp" +#include <godot_cpp/variant/string.hpp> +#include <godot_cpp/templates/vector.hpp> +#include <godot_cpp/templates/hash_map.hpp> + +#define GOAP_ACTION(Name_) \ +public: \ + _FORCE_INLINE_ static gd::String get_static_class() { return #Name_; }\ + _FORCE_INLINE_ gd::String get_class() const { return #Name_; }\ +private: + +namespace goap { +typedef int ActionID; + +/*! A goap action construct. + * + * Child classes an be registered with `ActionDB::register_action` + */ +class Action { + friend class ActionDB; + GOAP_ACTION(Action); +public: + virtual ~Action() = default; + + virtual bool is_possible_for(ActorWorldState *state) = 0; + virtual bool is_done_for(ActorWorldState *state) = 0; + virtual State *get_apply_state() const = 0; + + WorldState const &get_required() const; + WorldState const &get_effects() const; + + ActionID get_id() const; +protected: + Action() = default; + Action(WorldState &requires, WorldState &effects); +private: + WorldState requires{}; + WorldState effects{}; + ActionID id{-1}; +}; + +/* Storage wrapper for managed action class. + */ +struct ActionData { + ActionData() = default; + ActionData(Action *action); + ~ActionData(); + Action *data{nullptr}; +}; +} + +class TestAction : public goap::Action { + GOAP_ACTION(TestAction); +public: + TestAction() = default; + virtual bool is_done_for(goap::ActorWorldState *) override; + virtual bool is_possible_for(goap::ActorWorldState *) override; + virtual goap::State *get_apply_state() const override; +}; + +#endif // !GOAP_ACTION_HPP diff --git a/src/goap/action_db.cpp b/src/goap/action_db.cpp new file mode 100644 index 0000000..70b27fd --- /dev/null +++ b/src/goap/action_db.cpp @@ -0,0 +1,36 @@ +#include "action_db.hpp" + +namespace goap { +StaticString::~StaticString() { + if(this->ptr != nullptr) + delete this->ptr; +} + +gd::String &StaticString::get() { + if(this->ptr == nullptr) + this->ptr = new gd::String(); + return *this->ptr; +} + +ActionID ActionDB::register_action(Action *instance, gd::String name) { + instance->id = ActionDB::actions.size(); + if(!ActionDB::hint.get().is_empty()) + ActionDB::hint.get() += ","; + ActionDB::hint.get() += name; + ActionDB::actions.push_back(ActionData(instance)); + return instance->get_id(); +} + +Action const *ActionDB::get_action(ActionID index) { + if(ActionDB::actions.size() >= index || index < 0) + return nullptr; + return ActionDB::actions[index].data; +} + +gd::String const &ActionDB::get_enum_hint() { + return ActionDB::hint.get(); +} + +gd::Vector<ActionData> ActionDB::actions{gd::Vector<ActionData>()}; +StaticString ActionDB::hint{}; +} diff --git a/src/goap/action_db.hpp b/src/goap/action_db.hpp new file mode 100644 index 0000000..1db4012 --- /dev/null +++ b/src/goap/action_db.hpp @@ -0,0 +1,55 @@ +#ifndef GOAP_ACTION_DB_HPP +#define GOAP_ACTION_DB_HPP + +#include "action.hpp" + +namespace goap { +struct StaticString { +private: + gd::String *ptr{nullptr}; +public: + StaticString() = default; + ~StaticString(); + gd::String &get(); +}; + +/*! Global access to all (registered) Action types. + * + * Register them with the `register_action` function from `initialize_gdextension_types()`. + * To represent an Action in the editor, the getter and setter should act as if the property is an int. + * The property should then be registered as an enum with `get_enum_hint()` as hint. + * Inside the getter, `Action->get_id()` can be returned. While the setter assigns the underlying value to `ActionDB::get_action(id)`. + * ``` + * void Example::set_action(int id) { + * this->action = ActionDB::get_action(id); + * } + * int Example::get_action() const { + * return this->action->get_id(); + * } + * ``` + */ +class ActionDB { +protected: + static ActionID register_action(Action *instance, gd::String name); +public: + /*! Get action by ID. + */ + static Action const *get_action(ActionID id); + /*! Get the csv godot enum hint. + */ + static gd::String const &get_enum_hint(); + /*! Add action to database. + * + * \returns Action's assigned ID. + */ + template<class TAction> + _FORCE_INLINE_ static ActionID register_action() { + return ActionDB::register_action(new TAction(), TAction::get_static_class()); + } +private: + static gd::Vector<ActionData> actions; + static StaticString hint; +}; +} + +#endif // !GOAP_ACTION_DB_HPP diff --git a/src/goap/actor_world_state.cpp b/src/goap/actor_world_state.cpp new file mode 100644 index 0000000..de2fba1 --- /dev/null +++ b/src/goap/actor_world_state.cpp @@ -0,0 +1,7 @@ +#include "actor_world_state.hpp" + +namespace goap { +void ActorWorldState::_bind_methods() { +#define CLASSNAME ActorWorldState +} +} diff --git a/src/goap/actor_world_state.hpp b/src/goap/actor_world_state.hpp new file mode 100644 index 0000000..3b1a21f --- /dev/null +++ b/src/goap/actor_world_state.hpp @@ -0,0 +1,21 @@ +#ifndef GOAP_ACTOR_WORLD_STATE_HPP +#define GOAP_ACTOR_WORLD_STATE_HPP + +#include <godot_cpp/classes/node.hpp> +#include <godot_cpp/templates/hash_map.hpp> +#include <godot_cpp/templates/pair.hpp> +#include <godot_cpp/variant/string_name.hpp> + +namespace gd = godot; + +namespace goap { +typedef gd::HashMap<gd::StringName, gd::Variant> WorldState; +typedef gd::KeyValue<gd::StringName, gd::Variant> WorldProperty; + +class ActorWorldState : public gd::Node { + GDCLASS(ActorWorldState, gd::Node); + static void _bind_methods(); +}; +} + +#endif // !GOAP_ACTOR_WORLD_STATE_HPP diff --git a/src/goap/goal.hpp b/src/goap/goal.hpp new file mode 100644 index 0000000..3ca39e3 --- /dev/null +++ b/src/goap/goal.hpp @@ -0,0 +1,24 @@ +#ifndef GOAP_GOAL_HPP +#define GOAP_GOAL_HPP + +#include "actor_world_state.hpp" +#include <godot_cpp/classes/resource.hpp> + +namespace gd = godot; + +namespace goap { +class Goal : public gd::Resource { + GDCLASS(Goal, gd::Resource); + static void _bind_methods(); +public: + gd::Dictionary get_desired_state() const; + void set_desired_state(gd::Dictionary dict); +private: + //! Do not select this goal unless all requirements are met. + WorldState requirements; + //! The state to find a path to. + WorldState desired_state; +}; +} + +#endif // !GOAP_GOAL_HPP diff --git a/src/goap/planner.cpp b/src/goap/planner.cpp new file mode 100644 index 0000000..4bc61b6 --- /dev/null +++ b/src/goap/planner.cpp @@ -0,0 +1,34 @@ +#include "planner.hpp" +#include "goap/action_db.hpp" +#include "utils/godot_macros.hpp" + +namespace goap { +void Planner::_bind_methods() { +#define CLASSNAME Planner + GDPROPERTY_HINTED(actions, gd::Variant::PACKED_INT32_ARRAY, gd::PROPERTY_HINT_ENUM, ActionDB::get_enum_hint()); +} + +void Planner::_enter_tree() { + this->world_state = this->get_node<ActorWorldState>("../ActorWorldState"); +} + +Plan Planner::plan_for_goal(gd::Ref<Goal> goal) { + return {}; +} + +void Planner::set_actions(gd::PackedInt32Array array) { + this->actions.clear(); + for(int id : array) { + Action const *action = ActionDB::get_action(id); + if(action != nullptr && !this->actions.has(action)) + this->actions.push_back(action); + } +} + +gd::PackedInt32Array Planner::get_actions() const { + gd::PackedInt32Array array{}; + for(Action const *action : this->actions) + action->get_id(); + return array; +} +} diff --git a/src/goap/planner.hpp b/src/goap/planner.hpp new file mode 100644 index 0000000..41378ed --- /dev/null +++ b/src/goap/planner.hpp @@ -0,0 +1,32 @@ +#ifndef GOAP_PLANNER_HPP +#define GOAP_PLANNER_HPP + +#include "action.hpp" +#include "actor_world_state.hpp" +#include "goal.hpp" +#include "godot_cpp/variant/packed_int32_array.hpp" +#include <godot_cpp/templates/vector.hpp> +#include <godot_cpp/classes/node.hpp> + +namespace gd = godot; + +namespace goap { +typedef gd::Vector<Action> Plan; + +class Planner : public gd::Node { + GDCLASS(Planner, gd::Node); + static void _bind_methods(); +public: + virtual void _enter_tree() override; + + Plan plan_for_goal(gd::Ref<Goal> goal); + + void set_actions(gd::PackedInt32Array array); + gd::PackedInt32Array get_actions() const; +private: + ActorWorldState *world_state{nullptr}; + gd::Vector<Action const *> actions; +}; +} + +#endif // !GOAP_PLANNER_HPP diff --git a/src/goap/state.cpp b/src/goap/state.cpp new file mode 100644 index 0000000..65df71d --- /dev/null +++ b/src/goap/state.cpp @@ -0,0 +1,32 @@ +#include "state.hpp" + +namespace goap { +void State::_bind_methods() {} + +void MoveTo::_bind_methods() {} + +void MoveTo::_enter_tree() { + this->agent = this->get_node<gd::NavigationAgent3D>("../NavigationAgent3D"); + this->agent->set_target_position(this->target_node->get_global_position()); + this->parent_node3d = Object::cast_to<gd::Node3D>(this->get_parent()); +} + +void MoveTo::_exit_tree() { + this->agent->set_target_position(this->parent_node3d->get_global_position()); +} + +void MoveTo::_process(double delta_time) { + gd::Vector3 const pos = this->parent_node3d->get_global_position(); + gd::Vector3 const target = this->agent->get_next_path_position(); + gd::Vector3 const direction = (target - pos).normalized(); + this->parent_node3d->set_global_position(direction * delta_time); + this->parent_node3d->look_at(pos - gd::Vector3{direction.x, 0.f, direction.z}); + + if(this->agent->is_navigation_finished()) + this->queue_free(); +} + +void Activate::_bind_methods() {} + +void Animate::_bind_methods() {} +} diff --git a/src/goap/state.hpp b/src/goap/state.hpp new file mode 100644 index 0000000..2edb220 --- /dev/null +++ b/src/goap/state.hpp @@ -0,0 +1,48 @@ +#ifndef GOAP_STATE_HPP +#define GOAP_STATE_HPP + +#include "godot_cpp/classes/navigation_agent3d.hpp" +#include <godot_cpp/classes/node.hpp> +#include <godot_cpp/classes/node3d.hpp> + +namespace gd = godot; + +namespace goap { +class State : public gd::Node { + GDCLASS(State, gd::Node); + static void _bind_methods(); +}; + +/*! Uses navigation to chase the desired target node. + */ +class MoveTo : public State { + GDCLASS(MoveTo, State); + static void _bind_methods(); +public: + virtual void _enter_tree() override; + virtual void _exit_tree() override; + virtual void _process(double delta_time) override; +public: + gd::Node3D *target_node{nullptr}; +private: + gd::Node3D *parent_node3d{nullptr}; + gd::NavigationAgent3D *agent{nullptr}; +}; + +class Activate : public State { + GDCLASS(Activate, State); + static void _bind_methods(); +public: + gd::Node3D *target_node{nullptr}; + gd::String animation{}; +}; + +class Animate : public State { + GDCLASS(Animate, State); + static void _bind_methods(); +public: + gd::String animation{}; +}; +} + +#endif // !GOAP_STATE_HPP