feat: implemented goal oriented action planning
This commit is contained in:
parent
092ea840dc
commit
1ddfa17659
|
@ -1,11 +1,24 @@
|
||||||
#include "action.hpp"
|
#include "action.hpp"
|
||||||
|
|
||||||
namespace goap {
|
namespace goap {
|
||||||
Action::Action(WorldState &requires, WorldState &effects)
|
Action::~Action() {}
|
||||||
: requires{requires}, effects{effects} {}
|
|
||||||
|
bool Action::is_possible(ActorWorldState *context) const {
|
||||||
|
return this->procedural_is_possible(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Action::is_completed(ActorWorldState *context) const {
|
||||||
|
if(!only_proc_is_completed) {
|
||||||
|
for(WorldProperty const &prop : this->effects) {
|
||||||
|
if(!context->check_property_match(prop))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this->procedural_is_completed(context);
|
||||||
|
}
|
||||||
|
|
||||||
WorldState const &Action::get_required() const {
|
WorldState const &Action::get_required() const {
|
||||||
return this->requires;
|
return this->required;
|
||||||
}
|
}
|
||||||
|
|
||||||
WorldState const &Action::get_effects() const {
|
WorldState const &Action::get_effects() const {
|
||||||
|
@ -16,22 +29,11 @@ ActionID Action::get_id() const {
|
||||||
return this->id;
|
return this->id;
|
||||||
}
|
}
|
||||||
|
|
||||||
ActionData::ActionData(Action *action)
|
bool Action::procedural_is_possible(ActorWorldState *context) const {
|
||||||
: data{action} {}
|
|
||||||
|
|
||||||
ActionData::~ActionData() {
|
|
||||||
if(this->data != nullptr)
|
|
||||||
delete this->data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool TestAction::is_done_for(goap::ActorWorldState *) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
bool TestAction::is_possible_for(goap::ActorWorldState *) {
|
|
||||||
|
bool Action::procedural_is_completed(ActorWorldState *context) const {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
goap::State *TestAction::get_apply_state() const {
|
|
||||||
return nullptr;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
#define GOAP_ACTION(Name_) \
|
#define GOAP_ACTION(Name_) \
|
||||||
public: \
|
public: \
|
||||||
_FORCE_INLINE_ static gd::String get_static_class() { return #Name_; }\
|
_FORCE_INLINE_ static gd::String get_static_class() { return #Name_; }\
|
||||||
_FORCE_INLINE_ gd::String get_class() const { return #Name_; }\
|
_FORCE_INLINE_ virtual gd::String get_class() const override { return #Name_; }\
|
||||||
private:
|
private:
|
||||||
|
|
||||||
namespace goap {
|
namespace goap {
|
||||||
|
@ -22,44 +22,33 @@ typedef int ActionID;
|
||||||
*/
|
*/
|
||||||
class Action {
|
class Action {
|
||||||
friend class ActionDB;
|
friend class ActionDB;
|
||||||
GOAP_ACTION(Action);
|
|
||||||
public:
|
public:
|
||||||
virtual ~Action() = default;
|
static gd::String get_static_class() { return "Action"; }
|
||||||
|
virtual gd::String get_class() const { return "Action"; }
|
||||||
|
|
||||||
virtual bool is_possible_for(ActorWorldState *state) = 0;
|
virtual ~Action();
|
||||||
virtual bool is_done_for(ActorWorldState *state) = 0;
|
|
||||||
virtual State *get_apply_state() const = 0;
|
virtual State *get_apply_state() const = 0;
|
||||||
|
|
||||||
|
bool is_completed(ActorWorldState *context) const;
|
||||||
|
bool is_possible(ActorWorldState *context) const;
|
||||||
|
|
||||||
WorldState const &get_required() const;
|
WorldState const &get_required() const;
|
||||||
WorldState const &get_effects() const;
|
WorldState const &get_effects() const;
|
||||||
|
|
||||||
ActionID get_id() const;
|
ActionID get_id() const;
|
||||||
protected:
|
protected:
|
||||||
Action() = default;
|
Action() = default;
|
||||||
Action(WorldState &requires, WorldState &effects);
|
|
||||||
private:
|
|
||||||
WorldState requires{};
|
|
||||||
WorldState effects{};
|
|
||||||
ActionID id{-1};
|
|
||||||
};
|
|
||||||
|
|
||||||
/* Storage wrapper for managed action class.
|
virtual bool procedural_is_possible(ActorWorldState *context) const;
|
||||||
*/
|
virtual bool procedural_is_completed(ActorWorldState *context) const;
|
||||||
struct ActionData {
|
protected:
|
||||||
ActionData() = default;
|
WorldState blocking_required{};
|
||||||
ActionData(Action *action);
|
WorldState required{};
|
||||||
~ActionData();
|
WorldState effects{};
|
||||||
Action *data{nullptr};
|
bool only_proc_is_completed{false};
|
||||||
|
private:
|
||||||
|
ActionID id{-1};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
#endif // !GOAP_ACTION_HPP
|
||||||
|
|
|
@ -1,36 +1,42 @@
|
||||||
#include "action_db.hpp"
|
#include "action_db.hpp"
|
||||||
|
#include "godot_cpp/variant/utility_functions.hpp"
|
||||||
|
|
||||||
namespace goap {
|
namespace goap {
|
||||||
StaticString::~StaticString() {
|
ActionDB::StaticData::~StaticData() {
|
||||||
if(this->ptr != nullptr)
|
if(this->hint != nullptr)
|
||||||
delete this->ptr;
|
delete this->hint;
|
||||||
|
for(Action *action : this->actions)
|
||||||
|
delete action;
|
||||||
}
|
}
|
||||||
|
|
||||||
gd::String &StaticString::get() {
|
gd::String &ActionDB::StaticData::get_hint() {
|
||||||
if(this->ptr == nullptr)
|
if(this->hint == nullptr)
|
||||||
this->ptr = new gd::String();
|
this->hint = new gd::String();
|
||||||
return *this->ptr;
|
return *this->hint;
|
||||||
}
|
}
|
||||||
|
|
||||||
ActionID ActionDB::register_action(Action *instance, gd::String name) {
|
ActionID ActionDB::register_action(Action *instance, gd::String name) {
|
||||||
instance->id = ActionDB::actions.size();
|
instance->id = ActionDB::data.actions.size();
|
||||||
if(!ActionDB::hint.get().is_empty())
|
if(!ActionDB::data.get_hint().is_empty())
|
||||||
ActionDB::hint.get() += ",";
|
ActionDB::data.get_hint() += ",";
|
||||||
ActionDB::hint.get() += name;
|
ActionDB::data.get_hint() += name;
|
||||||
ActionDB::actions.push_back(ActionData(instance));
|
// defer intialization until after emplacement to avoid deleting instance
|
||||||
|
ActionDB::data.actions.push_back(instance);
|
||||||
|
gd::UtilityFunctions::print("registered action ", name);
|
||||||
return instance->get_id();
|
return instance->get_id();
|
||||||
}
|
}
|
||||||
|
|
||||||
Action const *ActionDB::get_action(ActionID index) {
|
Action const *ActionDB::get_action(ActionID index) {
|
||||||
if(ActionDB::actions.size() >= index || index < 0)
|
if(ActionDB::data.actions.size() <= index || index < 0) {
|
||||||
|
gd::UtilityFunctions::push_warning("Attempted to get pointer to non-existent Action by ActionID ", index);
|
||||||
return nullptr;
|
return nullptr;
|
||||||
return ActionDB::actions[index].data;
|
}
|
||||||
|
return ActionDB::data.actions[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
gd::String const &ActionDB::get_enum_hint() {
|
gd::String const &ActionDB::get_enum_hint() {
|
||||||
return ActionDB::hint.get();
|
return ActionDB::data.get_hint();
|
||||||
}
|
}
|
||||||
|
|
||||||
gd::Vector<ActionData> ActionDB::actions{gd::Vector<ActionData>()};
|
ActionDB::StaticData ActionDB::data{};
|
||||||
StaticString ActionDB::hint{};
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,15 +4,6 @@
|
||||||
#include "action.hpp"
|
#include "action.hpp"
|
||||||
|
|
||||||
namespace goap {
|
namespace goap {
|
||||||
struct StaticString {
|
|
||||||
private:
|
|
||||||
gd::String *ptr{nullptr};
|
|
||||||
public:
|
|
||||||
StaticString() = default;
|
|
||||||
~StaticString();
|
|
||||||
gd::String &get();
|
|
||||||
};
|
|
||||||
|
|
||||||
/*! Global access to all (registered) Action types.
|
/*! Global access to all (registered) Action types.
|
||||||
*
|
*
|
||||||
* Register them with the `register_action` function from `initialize_gdextension_types()`.
|
* Register them with the `register_action` function from `initialize_gdextension_types()`.
|
||||||
|
@ -29,7 +20,13 @@ public:
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
class ActionDB {
|
class ActionDB {
|
||||||
protected:
|
struct StaticData {
|
||||||
|
StaticData() = default;
|
||||||
|
~StaticData();
|
||||||
|
gd::String &get_hint();
|
||||||
|
gd::String *hint{nullptr};
|
||||||
|
gd::Vector<Action *> actions{};
|
||||||
|
};
|
||||||
static ActionID register_action(Action *instance, gd::String name);
|
static ActionID register_action(Action *instance, gd::String name);
|
||||||
public:
|
public:
|
||||||
/*! Get action by ID.
|
/*! Get action by ID.
|
||||||
|
@ -47,8 +44,7 @@ public:
|
||||||
return ActionDB::register_action(new TAction(), TAction::get_static_class());
|
return ActionDB::register_action(new TAction(), TAction::get_static_class());
|
||||||
}
|
}
|
||||||
private:
|
private:
|
||||||
static gd::Vector<ActionData> actions;
|
static StaticData data;
|
||||||
static StaticString hint;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,4 +4,12 @@ namespace goap {
|
||||||
void ActorWorldState::_bind_methods() {
|
void ActorWorldState::_bind_methods() {
|
||||||
#define CLASSNAME ActorWorldState
|
#define CLASSNAME ActorWorldState
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool ActorWorldState::check_property_match(WorldProperty const &property) {
|
||||||
|
return this->get_world_property(property.key) == property.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
gd::Variant ActorWorldState::get_world_property(gd::String world_prop_name) {
|
||||||
|
return this->call("get_" + world_prop_name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,9 @@ typedef gd::KeyValue<gd::StringName, gd::Variant> WorldProperty;
|
||||||
class ActorWorldState : public gd::Node {
|
class ActorWorldState : public gd::Node {
|
||||||
GDCLASS(ActorWorldState, gd::Node);
|
GDCLASS(ActorWorldState, gd::Node);
|
||||||
static void _bind_methods();
|
static void _bind_methods();
|
||||||
|
public:
|
||||||
|
bool check_property_match(WorldProperty const &property);
|
||||||
|
gd::Variant get_world_property(gd::String world_prop_name);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
27
src/goap/goal.cpp
Normal file
27
src/goap/goal.cpp
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
#include "goal.hpp"
|
||||||
|
#include "utils/godot_macros.hpp"
|
||||||
|
#include "utils/util_functions.hpp"
|
||||||
|
|
||||||
|
namespace goap {
|
||||||
|
void Goal::_bind_methods() {
|
||||||
|
#define CLASSNAME Goal
|
||||||
|
GDPROPERTY(requirements_dict, gd::Variant::DICTIONARY);
|
||||||
|
GDPROPERTY(desired_state_dict, gd::Variant::DICTIONARY);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Goal::set_requirements_dict(gd::Dictionary dict) {
|
||||||
|
this->requirements = utils::dictionary_to_hashmap<gd::StringName, gd::Variant>(dict);
|
||||||
|
}
|
||||||
|
|
||||||
|
gd::Dictionary Goal::get_requirements_dict() const {
|
||||||
|
return utils::hashmap_to_dictionary(this->requirements);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Goal::set_desired_state_dict(gd::Dictionary dict) {
|
||||||
|
this->desired_state = utils::dictionary_to_hashmap<gd::StringName, gd::Variant>(dict);
|
||||||
|
}
|
||||||
|
|
||||||
|
gd::Dictionary Goal::get_desired_state_dict() const {
|
||||||
|
return utils::hashmap_to_dictionary(this->desired_state);
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,9 +11,11 @@ class Goal : public gd::Resource {
|
||||||
GDCLASS(Goal, gd::Resource);
|
GDCLASS(Goal, gd::Resource);
|
||||||
static void _bind_methods();
|
static void _bind_methods();
|
||||||
public:
|
public:
|
||||||
gd::Dictionary get_desired_state() const;
|
void set_requirements_dict(gd::Dictionary dict);
|
||||||
void set_desired_state(gd::Dictionary dict);
|
gd::Dictionary get_requirements_dict() const;
|
||||||
private:
|
void set_desired_state_dict(gd::Dictionary dict);
|
||||||
|
gd::Dictionary get_desired_state_dict() const;
|
||||||
|
public:
|
||||||
//! Do not select this goal unless all requirements are met.
|
//! Do not select this goal unless all requirements are met.
|
||||||
WorldState requirements;
|
WorldState requirements;
|
||||||
//! The state to find a path to.
|
//! The state to find a path to.
|
||||||
|
|
|
@ -1,34 +1,207 @@
|
||||||
#include "planner.hpp"
|
#include "planner.hpp"
|
||||||
#include "goap/action_db.hpp"
|
#include "action_db.hpp"
|
||||||
|
#include "godot_cpp/classes/global_constants.hpp"
|
||||||
|
#include "godot_cpp/templates/hashfuncs.hpp"
|
||||||
#include "utils/godot_macros.hpp"
|
#include "utils/godot_macros.hpp"
|
||||||
|
#include <cstdint>
|
||||||
|
#include <godot_cpp/variant/utility_functions.hpp>
|
||||||
|
|
||||||
namespace goap {
|
namespace goap {
|
||||||
|
WorldStateNode::WorldStateNode(WorldState const &goal, ActorWorldState *context)
|
||||||
|
: state{}, open_requirements{goal}, context{context} {}
|
||||||
|
|
||||||
|
WorldStateNode::WorldStateNode(WorldStateNode const &last_state, Action const *last_action)
|
||||||
|
: state{last_state.state}
|
||||||
|
, open_requirements{last_state.open_requirements}
|
||||||
|
, last_action{last_action}
|
||||||
|
, context{last_state.context} {
|
||||||
|
for(WorldProperty const &req : last_action->get_required())
|
||||||
|
this->open_requirements[req.key] = req.value;
|
||||||
|
for(WorldProperty const &effect : last_action->get_effects())
|
||||||
|
if(this->open_requirements.has(effect.key))
|
||||||
|
this->state[effect.key] = effect.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
int WorldStateNode::requirements_unmet() const {
|
||||||
|
int requirements = this->open_requirements.size();
|
||||||
|
for(WorldProperty const &req : this->open_requirements) {
|
||||||
|
if(this->state.has(req.key) && this->state[req.key] == req.value) {
|
||||||
|
--requirements;
|
||||||
|
} else if(this->context->check_property_match(req)) {
|
||||||
|
--requirements;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return requirements;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t WorldStateNodeHasher::hash(goap::WorldStateNode const &state) {
|
||||||
|
uint32_t hash{1};
|
||||||
|
for(goap::WorldProperty const &prop : state.state) {
|
||||||
|
hash = gd::hash_murmur3_one_32(prop.key.hash(), hash);
|
||||||
|
hash = gd::hash_murmur3_one_32(prop.value.hash(), hash);
|
||||||
|
}
|
||||||
|
return gd::hash_fmix32(hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool operator==(goap::WorldStateNode const &lhs, goap::WorldStateNode const &rhs) {
|
||||||
|
if(lhs.state.size() != rhs.state.size())
|
||||||
|
return false;
|
||||||
|
for(goap::WorldProperty const &prop_lhs : lhs.state) {
|
||||||
|
if((!rhs.state.has(prop_lhs.key)) || rhs.state[prop_lhs.key] != prop_lhs.value)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for(goap::WorldProperty const &prop_rhs : rhs.state) {
|
||||||
|
if((!lhs.state.has(prop_rhs.key)) || lhs.state[prop_rhs.key] != prop_rhs.value)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool operator!=(goap::WorldStateNode const &lhs, goap::WorldStateNode const &rhs) {
|
||||||
|
return !(lhs == rhs);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool operator<(goap::WorldStateNode const &lhs, goap::WorldStateNode const &rhs) {
|
||||||
|
return lhs.state.size() < rhs.state.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool operator<=(goap::WorldStateNode const &lhs, goap::WorldStateNode const &rhs) {
|
||||||
|
return lhs.state.size() <= rhs.state.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool operator>(goap::WorldStateNode const &lhs, goap::WorldStateNode const &rhs) {
|
||||||
|
return lhs.state.size() > rhs.state.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool operator>=(goap::WorldStateNode const &lhs, goap::WorldStateNode const &rhs) {
|
||||||
|
return lhs.state.size() >= rhs.state.size();
|
||||||
|
}
|
||||||
|
|
||||||
void Planner::_bind_methods() {
|
void Planner::_bind_methods() {
|
||||||
#define CLASSNAME Planner
|
#define CLASSNAME Planner
|
||||||
GDPROPERTY_HINTED(actions, gd::Variant::PACKED_INT32_ARRAY, gd::PROPERTY_HINT_ENUM, ActionDB::get_enum_hint());
|
GDPROPERTY_HINTED(actions, gd::Variant::ARRAY, gd::PROPERTY_HINT_ARRAY_TYPE, gd::vformat("%s/%s:%s", gd::Variant::INT, gd::PROPERTY_HINT_ENUM, ActionDB::get_enum_hint()));
|
||||||
|
GDPROPERTY_HINTED(test_goal, gd::Variant::OBJECT, gd::PROPERTY_HINT_RESOURCE_TYPE, "Goal");
|
||||||
}
|
}
|
||||||
|
|
||||||
void Planner::_enter_tree() {
|
void Planner::_enter_tree() { GDGAMEONLY();
|
||||||
this->world_state = this->get_node<ActorWorldState>("../ActorWorldState");
|
this->world_state = this->get_node<ActorWorldState>("../ActorWorldState");
|
||||||
}
|
if(test_goal.is_valid()) {
|
||||||
|
Plan plan{this->plan_for_goal(test_goal)};
|
||||||
Plan Planner::plan_for_goal(gd::Ref<Goal> goal) {
|
gd::UtilityFunctions::print("plan for ", this->test_goal->get_path(), ":");
|
||||||
return {};
|
for(Action const *action : plan)
|
||||||
}
|
gd::UtilityFunctions::print(" ", action->get_class());
|
||||||
|
|
||||||
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 {
|
Plan Planner::plan_for_goal(gd::Ref<Goal> goal) {
|
||||||
gd::PackedInt32Array array{};
|
// exit if goal reference is invalid or goal is already completed
|
||||||
|
if(!goal.is_valid()) {
|
||||||
|
gd::UtilityFunctions::print("goal not valid");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
// The node representing the start of the search and the goal of the plan.
|
||||||
|
WorldStateNode goal_node{goal->desired_state, this->world_state};
|
||||||
|
if(goal_node.requirements_unmet() == 0) {
|
||||||
|
gd::UtilityFunctions::print("goal ", goal->get_path(), " already met");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
gd::Vector<WorldStateNode> open{goal_node};
|
||||||
|
NodeMap from{};
|
||||||
|
NodeScoreMap best_path_cost{};
|
||||||
|
NodeScoreMap heuristic_cost{};
|
||||||
|
|
||||||
|
best_path_cost.insert(goal_node, 0.f);
|
||||||
|
heuristic_cost.insert(goal_node, goal_node.requirements_unmet());
|
||||||
|
|
||||||
|
WorldStateNode current{goal_node};
|
||||||
|
while(!open.is_empty()) {
|
||||||
|
current = open.get(0);
|
||||||
|
if(current.requirements_unmet() == 0)
|
||||||
|
return this->unroll_plan(current, from);
|
||||||
|
open.remove_at(0);
|
||||||
|
gd::Vector<Action const *> edges{this->get_neighbours(current)};
|
||||||
|
gd::UtilityFunctions::print("found ", edges.size(), " possible actions");
|
||||||
|
for(Action const * action : edges) {
|
||||||
|
WorldStateNode node_through{current, action};
|
||||||
|
float const path_cost{best_path_cost.get(current) + 1.f};
|
||||||
|
gd::UtilityFunctions::print("hashes ", WorldStateNodeHasher::hash(node_through), " ; ", WorldStateNodeHasher::hash(current));
|
||||||
|
gd::UtilityFunctions::print("equiv: ", current == node_through);
|
||||||
|
if(!best_path_cost.has(node_through) || best_path_cost.get(node_through) >= path_cost) {
|
||||||
|
gd::UtilityFunctions::print("node through action ", action->get_class(), " added to open set");
|
||||||
|
best_path_cost[node_through] = path_cost;
|
||||||
|
heuristic_cost[node_through] = path_cost + node_through.requirements_unmet();
|
||||||
|
from[node_through] = current;
|
||||||
|
open.erase(node_through);
|
||||||
|
open.ordered_insert(node_through);
|
||||||
|
} else {
|
||||||
|
gd::UtilityFunctions::print("node through action ", action->get_class(), " scores worse than previous at same state. ", path_cost, " vs ", best_path_cost[node_through]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gd::UtilityFunctions::print("failed to find plan for goal ", goal->get_path());
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
void Planner::set_actions(gd::Array array) {
|
||||||
|
this->actions.clear();
|
||||||
|
for(size_t i = 0; i < array.size(); ++i) {
|
||||||
|
Action const *action = ActionDB::get_action(array[i]);
|
||||||
|
if(action != nullptr && !this->actions.has(action)) {
|
||||||
|
this->actions.push_back(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gd::Array Planner::get_actions() const {
|
||||||
|
gd::Array array{};
|
||||||
for(Action const *action : this->actions)
|
for(Action const *action : this->actions)
|
||||||
action->get_id();
|
array.push_back(action->get_id());
|
||||||
return array;
|
return array;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Planner::set_test_goal(gd::Ref<Goal> goal) {
|
||||||
|
this->test_goal = goal;
|
||||||
|
}
|
||||||
|
|
||||||
|
gd::Ref<Goal> Planner::get_test_goal() const {
|
||||||
|
return this->test_goal;
|
||||||
|
}
|
||||||
|
|
||||||
|
gd::Vector<Action const *> Planner::get_neighbours(WorldStateNode const &from) const {
|
||||||
|
gd::Vector<Action const *> retval{};
|
||||||
|
for(Action const *action : this->actions) {
|
||||||
|
if(!action->is_possible(from.context)) {
|
||||||
|
gd::UtilityFunctions::print("action ", action->get_class(), " is not possible");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if(!this->does_action_contribute(action, from)) {
|
||||||
|
gd::UtilityFunctions::print("action ", action->get_class(), " does not contribute");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
retval.push_back(action);
|
||||||
|
}
|
||||||
|
return retval;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Planner::does_action_contribute(Action const *action, WorldStateNode const &node) const {
|
||||||
|
WorldState const &effects = action->get_effects();
|
||||||
|
for(WorldProperty const &goal : node.open_requirements) {
|
||||||
|
if(node.state.has(goal.key) && goal.value == node.state.get(goal.key))
|
||||||
|
continue; // requirement already met, don't count as requiring contribution
|
||||||
|
if(effects.has(goal.key) && effects[goal.key] == goal.value)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Plan Planner::unroll_plan(WorldStateNode const &start_node, NodeMap const &from) {
|
||||||
|
Plan plan{};
|
||||||
|
WorldStateNode node{start_node};
|
||||||
|
while(node.last_action != nullptr) {
|
||||||
|
plan.push_back(node.last_action);
|
||||||
|
node = from.get(node);
|
||||||
|
}
|
||||||
|
return plan;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,26 +4,74 @@
|
||||||
#include "action.hpp"
|
#include "action.hpp"
|
||||||
#include "actor_world_state.hpp"
|
#include "actor_world_state.hpp"
|
||||||
#include "goal.hpp"
|
#include "goal.hpp"
|
||||||
#include "godot_cpp/variant/packed_int32_array.hpp"
|
#include <cstdint>
|
||||||
#include <godot_cpp/templates/vector.hpp>
|
|
||||||
#include <godot_cpp/classes/node.hpp>
|
#include <godot_cpp/classes/node.hpp>
|
||||||
|
#include <godot_cpp/templates/vector.hpp>
|
||||||
|
#include <godot_cpp/variant/packed_int32_array.hpp>
|
||||||
|
|
||||||
namespace gd = godot;
|
namespace gd = godot;
|
||||||
|
|
||||||
namespace goap {
|
namespace goap {
|
||||||
typedef gd::Vector<Action> Plan;
|
typedef gd::Vector<Action const *> Plan;
|
||||||
|
|
||||||
|
/*! Wrapper for WorldState to be used to represent nodes in the A* implementation.
|
||||||
|
* Mainly required to let a HashMap of HashMaps work.
|
||||||
|
*/
|
||||||
|
struct WorldStateNode {
|
||||||
|
WorldStateNode() = default;
|
||||||
|
//! Root node constructor
|
||||||
|
WorldStateNode(WorldState const &goal, ActorWorldState *context);
|
||||||
|
//! Path node constructor
|
||||||
|
WorldStateNode(WorldStateNode const &last_state, Action const *last_action);
|
||||||
|
~WorldStateNode() = default;
|
||||||
|
int requirements_unmet() const;
|
||||||
|
WorldState state{};
|
||||||
|
WorldState open_requirements{};
|
||||||
|
Action const *last_action{nullptr};
|
||||||
|
ActorWorldState *context;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct WorldStateNodeHasher {
|
||||||
|
static uint32_t hash(WorldStateNode const &state);
|
||||||
|
};
|
||||||
|
|
||||||
|
extern bool operator==(WorldStateNode const &lhs, WorldStateNode const &rhs);
|
||||||
|
extern bool operator!=(WorldStateNode const &lhs, WorldStateNode const &rhs);
|
||||||
|
extern bool operator<(WorldStateNode const &lhs, WorldStateNode const &rhs);
|
||||||
|
extern bool operator<=(WorldStateNode const &lhs, WorldStateNode const &rhs);
|
||||||
|
extern bool operator>(WorldStateNode const &lhs, WorldStateNode const &rhs);
|
||||||
|
extern bool operator>=(WorldStateNode const &lhs, WorldStateNode const &rhs);
|
||||||
|
|
||||||
|
typedef gd::HashMap<WorldStateNode, WorldStateNode, WorldStateNodeHasher> NodeMap;
|
||||||
|
typedef gd::HashMap<WorldStateNode, float, WorldStateNodeHasher> NodeScoreMap;
|
||||||
|
|
||||||
|
/*! Uses A* to compute `Action` sequences for an actor to achieve `Goal`s.
|
||||||
|
*
|
||||||
|
* Set up available actions from the editor and call plan_for_goal with a desired goal.
|
||||||
|
* Requires a sibling node inheriting from ActorWorldState called "ActorWorldState".
|
||||||
|
*/
|
||||||
class Planner : public gd::Node {
|
class Planner : public gd::Node {
|
||||||
GDCLASS(Planner, gd::Node);
|
GDCLASS(Planner, gd::Node);
|
||||||
static void _bind_methods();
|
static void _bind_methods();
|
||||||
public:
|
public:
|
||||||
virtual void _enter_tree() override;
|
virtual void _enter_tree() override;
|
||||||
|
|
||||||
|
//! Compute a plan to achieve the passed `Goal`.
|
||||||
Plan plan_for_goal(gd::Ref<Goal> goal);
|
Plan plan_for_goal(gd::Ref<Goal> goal);
|
||||||
|
|
||||||
void set_actions(gd::PackedInt32Array array);
|
void set_actions(gd::Array array);
|
||||||
gd::PackedInt32Array get_actions() const;
|
gd::Array get_actions() const;
|
||||||
|
void set_test_goal(gd::Ref<Goal> goal);
|
||||||
|
gd::Ref<Goal> get_test_goal() const;
|
||||||
private:
|
private:
|
||||||
|
//! \returns A vector of all actions that satisfy any of the requirements in `unsatisfied`
|
||||||
|
gd::Vector<Action const *> get_neighbours(WorldStateNode const &from) const;
|
||||||
|
//! \returns True if the passed action contributes to any of the open requirements in `node`.
|
||||||
|
bool does_action_contribute(Action const *action, WorldStateNode const &node) const;
|
||||||
|
//! \returns A plan starting with `start_node` traced backwards through the `from` map.
|
||||||
|
Plan unroll_plan(WorldStateNode const &start_node, NodeMap const &from);
|
||||||
|
private:
|
||||||
|
gd::Ref<Goal> test_goal{};
|
||||||
ActorWorldState *world_state{nullptr};
|
ActorWorldState *world_state{nullptr};
|
||||||
gd::Vector<Action const *> actions;
|
gd::Vector<Action const *> actions;
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
#include "goap/action.hpp"
|
#include "goap/action.hpp"
|
||||||
#include "goap/action_db.hpp"
|
#include "goap/action_db.hpp"
|
||||||
#include "goap/actor_world_state.hpp"
|
#include "goap/actor_world_state.hpp"
|
||||||
|
#include "goap/goal.hpp"
|
||||||
#include "goap/planner.hpp"
|
#include "goap/planner.hpp"
|
||||||
#include "rts_game_mode.hpp"
|
#include "rts_game_mode.hpp"
|
||||||
#include "rts_player.hpp"
|
#include "rts_player.hpp"
|
||||||
|
@ -20,12 +21,17 @@ void initialize_gdextension_types(ModuleInitializationLevel p_level)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
utils::godot_cpp_utils_register_types();
|
utils::godot_cpp_utils_register_types();
|
||||||
|
|
||||||
|
// always register actions before classes,
|
||||||
|
// so that ActionDB::get_enum_hint is complete before _bind_methods
|
||||||
|
|
||||||
ClassDB::register_class<RTSPlayer>();
|
ClassDB::register_class<RTSPlayer>();
|
||||||
ClassDB::register_class<Unit>();
|
ClassDB::register_class<Unit>();
|
||||||
ClassDB::register_class<RTSGameMode>();
|
ClassDB::register_class<RTSGameMode>();
|
||||||
|
|
||||||
ClassDB::register_class<goap::ActorWorldState>();
|
ClassDB::register_class<goap::ActorWorldState>();
|
||||||
|
ClassDB::register_class<goap::Goal>();
|
||||||
ClassDB::register_class<goap::Planner>();
|
ClassDB::register_class<goap::Planner>();
|
||||||
goap::ActionDB::register_action<TestAction>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C"
|
extern "C"
|
||||||
|
|
Loading…
Reference in a new issue