feat: first functioning state of GOAP implementation
This commit is contained in:
parent
dfe6349c76
commit
fdfde706e0
2
Makefile
2
Makefile
|
@ -1,5 +1,5 @@
|
|||
debug:
|
||||
/usr/bin/scons debug_symbols=yes
|
||||
/usr/bin/scons debug_symbols=yes optimize=none
|
||||
|
||||
develop:
|
||||
/usr/bin/scons
|
||||
|
|
6
godot/new_goal.tres
Normal file
6
godot/new_goal.tres
Normal file
|
@ -0,0 +1,6 @@
|
|||
[gd_resource type="Goal" format=3 uid="uid://ogtaubr23l5x"]
|
||||
|
||||
[resource]
|
||||
goal_state = {
|
||||
"goal": true
|
||||
}
|
|
@ -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")
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
#include "action.hpp"
|
||||
#include "utils/godot_macros.h"
|
||||
#include <godot_cpp/variant/utility_functions.hpp>
|
||||
|
||||
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(), ")");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
#ifndef GOAP_ACTION_HPP
|
||||
#define GOAP_ACTION_HPP
|
||||
|
||||
#include <cstddef>
|
||||
#include <godot_cpp/classes/object.hpp>
|
||||
#include <godot_cpp/classes/ref_counted.hpp>
|
||||
#include <godot_cpp/classes/resource.hpp>
|
||||
|
@ -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:
|
||||
|
|
|
@ -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;
|
||||
|
|
110
src/planner.cpp
110
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 <cassert>
|
||||
#include <cstdlib>
|
||||
#include <godot_cpp/templates/pair.hpp>
|
||||
#include <godot_cpp/variant/utility_functions.hpp>
|
||||
#include <godot_cpp/templates/hash_set.hpp>
|
||||
#include <godot_cpp/templates/hash_map.hpp>
|
||||
#include <godot_cpp/templates/hashfuncs.hpp>
|
||||
#include <godot_cpp/variant/vector3.hpp>
|
||||
|
||||
namespace godot::goap {
|
||||
typedef HashMap<PlannerNode, PlannerNode, PlannerNodeHasher> FromMap;
|
||||
typedef HashMap<PlannerNode, float, PlannerNodeHasher> ScoreMap;
|
||||
typedef HashSet<PlannerNode, PlannerNodeHasher> 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<State> Planner::make_plan(Goal *goal) {
|
||||
HashMap<PlannerNode, int, PlannerNodeHasher> open{};
|
||||
open.insert(PlannerNode{{}, goal->goal_state, nullptr}, 0);
|
||||
HashMap<PlannerNode, Ref<Action>, PlannerNodeHasher> from{};
|
||||
HashMap<PlannerNode, float, PlannerNodeHasher> score_to_start{};
|
||||
HashMap<PlannerNode, float, PlannerNodeHasher> heuristic_score{};
|
||||
static Vector<Ref<Action>> trace_path(FromMap &map, PlannerNode &end) {
|
||||
Vector<Ref<Action>> 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> goal) {
|
||||
Vector<Ref<Action>> plan = this->make_plan(goal);
|
||||
Array out{};
|
||||
int i{0};
|
||||
UtilityFunctions::print("plan len: ", plan.size());
|
||||
for(Ref<Action> const &action : plan) {
|
||||
out.push_back(action);
|
||||
UtilityFunctions::print("plan[", i++, "]: ", this->actions.find(action));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
Vector<Ref<Action>> Planner::make_plan(Ref<Goal> goal) {
|
||||
UtilityFunctions::print("run");
|
||||
Vector<PlannerNode> 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<PlannerNode, int> &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<PlannerNode> 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> action) {
|
|||
return true;
|
||||
}
|
||||
|
||||
Vector<PlannerNode> Planner::find_neighbours_of(PlannerNode &node) {
|
||||
Vector<PlannerNode> neighbours{};
|
||||
for(Ref<Action> 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<Ref<Action>> Planner::find_actions_satisfying(WorldState requirements) {
|
||||
Vector<Ref<Action>> found_actions{};
|
||||
for(Ref<Action> &act : this->actions) {
|
||||
|
@ -78,10 +153,11 @@ Vector<Ref<Action>> 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<Action> act = value[i];
|
||||
if(act.is_valid())
|
||||
this->actions.push_back(value[i]);
|
||||
this->actions.set(i, act);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
#define GOAP_PLANNER_HPP
|
||||
|
||||
#include "action.hpp"
|
||||
#include "godot_cpp/classes/object.hpp"
|
||||
#include "godot_cpp/variant/variant.hpp"
|
||||
#include <godot_cpp/classes/node.hpp>
|
||||
#include <godot_cpp/classes/resource.hpp>
|
||||
|
@ -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<Action> last_edge{};
|
||||
|
||||
PlannerNode() = default;
|
||||
PlannerNode(PlannerNode const &src) = default;
|
||||
|
||||
static PlannerNode goal_node(WorldState goal) {
|
||||
return PlannerNode{
|
||||
goal, {}, {}
|
||||
};
|
||||
}
|
||||
|
||||
PlannerNode new_node_along(Ref<Action> 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<State> make_plan(Goal *goal);
|
||||
Array gdscript_make_plan(Ref<Goal> goal);
|
||||
Vector<Ref<Action>> make_plan(Ref<Goal> goal);
|
||||
|
||||
Variant get_world_property(StringName prop_key);
|
||||
|
||||
bool can_do(Ref<Action> action);
|
||||
Vector<PlannerNode> find_neighbours_of(PlannerNode &node);
|
||||
Vector<Ref<Action>> find_actions_satisfying(WorldState requirements);
|
||||
|
||||
void set_actions(Array actions);
|
||||
|
@ -64,27 +96,34 @@ private:
|
|||
Vector<Ref<Action>> actions{};
|
||||
};
|
||||
|
||||
struct PlannerNode {
|
||||
WorldState state;
|
||||
WorldState open_requirements;
|
||||
Ref<Action> last_edge;
|
||||
|
||||
int heuristic_score(Ref<Action> 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<StringName, Variant> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Projectile>();
|
||||
ClassDB::register_class<PelletProjectile>();
|
||||
|
||||
ClassDB::register_class<goap::Action>();
|
||||
ClassDB::register_class<goap::GlobalWorldState>();
|
||||
ClassDB::register_class<goap::Goal>();
|
||||
ClassDB::register_class<goap::Planner>();
|
||||
}
|
||||
|
||||
extern "C"
|
||||
|
|
Loading…
Reference in a new issue