feat: first functioning state of GOAP implementation

This commit is contained in:
Sara 2024-03-27 01:22:31 +01:00
parent dfe6349c76
commit fdfde706e0
9 changed files with 218 additions and 44 deletions

View file

@ -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
View file

@ -0,0 +1,6 @@
[gd_resource type="Goal" format=3 uid="uid://ogtaubr23l5x"]
[resource]
goal_state = {
"goal": true
}

View file

@ -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")

View file

@ -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(), ")");
}
}
}

View file

@ -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:

View file

@ -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;

View file

@ -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);
}
}

View file

@ -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);
}
}
}

View file

@ -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"