Merge remote-tracking branch 'origin/goap-planner'

This commit is contained in:
Sara 2024-04-01 23:26:54 +02:00
commit ba6bc4fd6d
14 changed files with 1164 additions and 349 deletions

File diff suppressed because it is too large Load diff

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 148 KiB

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=9 format=3 uid="uid://dpda341t6ipiv"]
[sub_resource type="Curve" id="Curve_7rmf4"]
min_value = 0.2
@ -18,7 +18,24 @@ 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="MoveStateArgs" id="MoveStateArgs_ibmkn"]
argument_property = &"player_character"
[sub_resource type="Action" id="Action_gtisq"]
effects = {
"is_near_player": true
}
apply_state = SubResource("MoveStateArgs_ibmkn")
[sub_resource type="Goal" id="Goal_sqtwb"]
goal_state = {
"is_near_player": true
}
prerequisites = {
"is_near_player": false
}
[node name="PlayerCharacter" type="CharacterActor"]
rotation_speed_curve = SubResource("Curve_7rmf4")
collision_layer = 7
@ -47,3 +64,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_gtisq")]
goals = [SubResource("Goal_sqtwb")]

78
src/action.cpp Normal file
View file

@ -0,0 +1,78 @@
#include "action.hpp"
#include "character_actor.hpp"
#include "global_world_state.hpp"
#include "utils/godot_macros.h"
#include <godot_cpp/classes/global_constants.hpp>
#include <godot_cpp/variant/utility_functions.hpp>
namespace godot::goap {
void Action::_bind_methods() {
#define CLASSNAME Action
GDPROPERTY(context_prerequisites, Variant::DICTIONARY);
GDPROPERTY(prerequisites, Variant::DICTIONARY);
GDPROPERTY(effects, Variant::DICTIONARY);
GDPROPERTY_HINTED(apply_state, Variant::OBJECT, PROPERTY_HINT_RESOURCE_TYPE, "StateArgs");
}
void Action::set_context_prerequisites(Dictionary dict) {
Action::dict_to_property_map(dict, this->context_prerequisites);
}
Dictionary Action::get_context_prerequisites() const {
return Action::property_map_to_dict(this->context_prerequisites);
}
void Action::set_prerequisites(Dictionary dict) {
Action::dict_to_property_map(dict, this->prerequisites);
}
Dictionary Action::get_prerequisites() const {
return Action::property_map_to_dict(this->prerequisites);
}
void Action::set_effects(Dictionary dict) {
Action::dict_to_property_map(dict, this->effects);
}
Dictionary Action::get_effects() const {
return Action::property_map_to_dict(this->effects);
}
void Action::set_apply_state(Ref<StateArgs> state) {
this->apply_state = state;
}
Ref<StateArgs> Action::get_apply_state() const {
return this->apply_state;
}
bool Action::get_is_completed(CharacterActor *context) {
GlobalWorldState *state = GlobalWorldState::get_singleton();
for(WorldProperty const &prop : this->effects) {
return (prop.key.begins_with("g_")
? state->get_world_property(prop.key)
: context->call("get_" + prop.key)) == prop.value;
}
return true;
}
Dictionary Action::property_map_to_dict(WorldState const &props) {
Dictionary out{};
for(KeyValue<StringName, Variant> const &prop : props) {
out[prop.key] = prop.value;
}
return out;
}
void Action::dict_to_property_map(Dictionary const &dict, WorldState &out) {
out.clear();
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 || 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(), ")");
}
}
}

43
src/action.hpp Normal file
View file

@ -0,0 +1,43 @@
#ifndef GOAP_ACTION_HPP
#define GOAP_ACTION_HPP
#include "state.hpp"
#include <godot_cpp/classes/object.hpp>
#include <godot_cpp/classes/ref_counted.hpp>
#include <godot_cpp/classes/resource.hpp>
#include <godot_cpp/templates/hash_map.hpp>
#include <godot_cpp/templates/pair.hpp>
#include <godot_cpp/variant/variant.hpp>
namespace godot { class CharacterActor; }
namespace godot::goap {
typedef HashMap<StringName, Variant> WorldState;
typedef KeyValue<StringName, Variant> WorldProperty;
class Action : public Resource {
GDCLASS(Action, Resource);
static void _bind_methods();
public:
void set_context_prerequisites(Dictionary dict);
Dictionary get_context_prerequisites() const;
void set_prerequisites(Dictionary dict);
Dictionary get_prerequisites() const;
void set_effects(Dictionary dict);
Dictionary get_effects() const;
void set_apply_state(Ref<StateArgs> args);
Ref<StateArgs> get_apply_state() const;
bool get_is_completed(CharacterActor *context);
static Dictionary property_map_to_dict(WorldState const &props);
static void dict_to_property_map(Dictionary const &dict, WorldState &out);
public:
WorldState context_prerequisites{};
WorldState prerequisites{};
WorldState effects{};
Ref<StateArgs> apply_state{};
};
}
#endif //!GOAP_ACTION_HPP

View file

@ -1,5 +1,9 @@
#include "character_actor.hpp"
#include "planner.hpp"
#include "projectile_pool.hpp"
#include "state.hpp"
#include "tunnels_game_mode.hpp"
#include "utils/game_root.hpp"
#include "utils/godot_macros.h"
#include <cmath>
#include <godot_cpp/classes/navigation_agent3d.hpp>
@ -12,6 +16,8 @@ void CharacterActor::_bind_methods() {
#define CLASSNAME CharacterActor
GDPROPERTY_HINTED(rotation_speed_curve, Variant::OBJECT, PROPERTY_HINT_RESOURCE_TYPE, "Curve");
GDFUNCTION_ARGS(set_velocity_target, "value");
GDFUNCTION(get_is_near_player);
GDFUNCTION(get_player_character);
}
void CharacterActor::_enter_tree() { GDGAMEONLY();
@ -20,23 +26,26 @@ void CharacterActor::_enter_tree() { GDGAMEONLY();
this->target_rotation = this->get_global_transform().get_basis().get_quaternion();
this->health = this->get_node<Health>("Health");
this->primary_weapon_pool = this->get_node<ProjectilePool>("ProjectilePool");
this->planner = this->get_node<goap::Planner>("Planner");
}
void CharacterActor::_process(double delta_time) { GDGAMEONLY();
this->process_rotation(delta_time);
if(!this->mode_manual) {
this->process_ai(delta_time);
this->process_behaviour(delta_time);
this->process_navigation(delta_time);
}
if(this->firing)
if(this->firing) {
this->try_fire_weapon();
}
}
void CharacterActor::_physics_process(double delta_time) { GDGAMEONLY();
// accelerate towards velocity target
Vector3 const new_velocity = this->get_velocity().move_toward(this->velocity_target, delta_time * CharacterActor::ACCELERATION);
// only apply velocity if not grounded
Vector3 const gravity{this->is_on_floor() ? Vector3() : Vector3{0.f, this->get_velocity().y - 9.8f, 0.f}};
this->set_velocity(new_velocity + gravity);
Vector3 const gravity{Vector3{0.f, this->get_velocity().y - 9.8f, 0.f}};
// apply either gravity or walking velocity depending on results
this->set_velocity(this->is_on_floor() ? new_velocity : this->get_velocity() + gravity);
// update position
this->move_and_slide();
}
@ -65,9 +74,7 @@ void CharacterActor::aim_direction(Vector3 direction) {
void CharacterActor::move_to(Vector3 to, float target_distance) {
this->nav_agent->set_target_desired_distance(target_distance);
this->nav_agent->set_target_position(this->get_global_position().distance_squared_to(to) < target_distance * target_distance
? this->get_global_position()
: to);
this->nav_agent->set_target_position(to);
}
void CharacterActor::shoot_at(Vector3 at) {
@ -84,6 +91,7 @@ void CharacterActor::set_manual_mode(bool value) {
ProcessMode const mode = value ? ProcessMode::PROCESS_MODE_DISABLED : ProcessMode::PROCESS_MODE_PAUSABLE;
//this->nav_agent->set_process_mode(mode);
this->nav_agent->set_avoidance_priority(value ? 1.f : 0.9f);
this->set_state(goap::State::new_invalid());
}
void CharacterActor::set_rotation_speed_curve(Ref<Curve> curve) {
@ -120,10 +128,54 @@ Vector3 CharacterActor::get_velocity_target() const {
return this->velocity_target;
}
void CharacterActor::process_ai(double delta_time) {
float const distance = this->nav_agent->get_target_position().distance_squared_to(this->get_global_position());
float const target_distance_sqr = std::pow(this->nav_agent->get_target_desired_distance(), 2.f);
if(!this->nav_agent->is_navigation_finished() && distance >= target_distance_sqr) {
bool CharacterActor::get_is_near_player() const {
return this->get_player_character()->get_global_position().distance_to(this->get_global_position()) < 5.f;
}
CharacterActor *CharacterActor::get_player_character() const {
Ref<TunnelsGameMode> game_mode = GameRoot::get_singleton()->get_game_mode();
return game_mode->get_player_instance()->get_character();
}
void CharacterActor::set_state(goap::State state) {
switch(this->current_state.type) {
default:
break;
case goap::State::STATE_MOVE_TO:
this->nav_agent->set_target_position(this->get_global_position());
break;
}
this->current_state = state;
switch(state.type) {
default:
break;
case goap::State::STATE_MOVE_TO:
this->move_to(state.move_to->get_global_position());
break;
}
}
void CharacterActor::process_behaviour(double delta_time) {
if(this->current_state.is_complete(this) || this->planner->is_action_complete())
this->set_state(this->planner->get_next_state());
switch(this->current_state.type) {
default:
break;
case goap::State::STATE_MOVE_TO:
if(this->nav_agent->get_target_position().distance_to(this->current_state.move_to->get_global_position()) > 2.f)
this->nav_agent->set_target_position(this->current_state.move_to->get_global_position());
break;
case goap::State::STATE_ACTIVATE:
break;
case goap::State::STATE_ANIMATE:
break;
}
}
void CharacterActor::process_navigation(double delta_time) {
float const distance_sqr = this->nav_agent->get_target_position().distance_squared_to(this->get_global_position());
float const distance_target_sqr = std::pow(this->nav_agent->get_target_desired_distance(), 2.f);
if(!this->nav_agent->is_navigation_finished() && distance_sqr >= distance_target_sqr) {
Vector3 const target_position = this->nav_agent->get_next_path_position();
Vector3 const direction = (target_position - this->get_global_position()).normalized();
if(this->nav_agent->get_avoidance_enabled())

View file

@ -4,12 +4,17 @@
#include "character_data.hpp"
#include "health.hpp"
#include "projectile_pool.hpp"
#include "state.hpp"
#include <godot_cpp/classes/character_body3d.hpp>
#include <godot_cpp/classes/curve.hpp>
namespace godot {
class NavigationAgent3D;
class TunnelsPlayer;
class AnimationPlayer;
namespace goap {
class Planner;
};
class CharacterActor : public CharacterBody3D,
public IHealthEntity {
@ -19,44 +24,66 @@ public:
virtual void _enter_tree() override;
virtual void _process(double delta_time) override;
virtual void _physics_process(double delta_time) override;
// manually set target_velocity
void move(Vector3 world_vector);
// manually aim at a target position
// calls aim_direction with the flattened direction to 'at'
void aim(Vector3 at);
// manually set the forward vector of target_rotation
void aim_direction(Vector3 direction);
// set a movement target to navigate towards
void move_to(Vector3 to, float target_distance = 0.5f);
// fire weapon at a target position
// calls aim(at) and set_firing(true)
void shoot_at(Vector3 at);
// getter-setters
void set_firing(bool firing);
void set_manual_mode(bool value);
void set_rotation_speed_curve(Ref<Curve> curve);
Ref<Curve> get_rotation_speed_curve() const;
virtual Health *get_health() override;
virtual Health const *get_health() const override;
void set_character_data(Ref<CharacterData> data);
void set_weapon_muzzle(Node3D *node);
void set_velocity_target(Vector3 value);
Vector3 get_velocity_target() const;
bool get_is_near_player() const;
CharacterActor *get_player_character() const;
void set_state(goap::State state);
protected:
void process_ai(double delta_time);
void process_behaviour(double delta_time);
void process_navigation(double delta_time);
void process_rotation(double delta_time);
void try_fire_weapon();
private:
// desired velocity, accelerated towards each frame
Vector3 velocity_target{0.f,0.f,0.f};
// target rotation, slerped towards each frame
Basis target_rotation{};
NavigationAgent3D *nav_agent{nullptr};
// ignore any ai planning or navigation
bool mode_manual{false};
// fire weapon at whatever we're aiming at
bool firing{false};
// the next timestamp at which a weapon can be fired
float fire_timer{0.f};
// the origin point for projectiles
Node3D *weapon_muzzle{nullptr};
// something that the AI wants to target
Node *target{nullptr};
// the current state of the actor
goap::State current_state{goap::State::new_invalid()};
AnimationPlayer *anim_player{nullptr};
Health *health{nullptr};
ProjectilePool *primary_weapon_pool{nullptr};
Ref<CharacterData> data;
float fire_interval{0.f};
bool firing{false};
float fire_timer{0.f};
Node3D *weapon_muzzle{nullptr};
NavigationAgent3D *nav_agent{nullptr};
goap::Planner *planner{nullptr};
Ref<Curve> rotation_speed_curve{};
// character data assigned when spawned
Ref<CharacterData> data;
float fire_interval{0.f}; // derived from 1 / the current weapon's rps
static float const ACCELERATION;
static float const WALK_SPEED;

View file

@ -0,0 +1,60 @@
#include "global_world_state.hpp"
#include "character_actor.hpp"
#include "utils/game_root.hpp"
namespace godot::goap {
void GlobalWorldState::_bind_methods() {
#define CLASSNAME GlobalWorldState
}
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;
}
void GlobalWorldState::_ready() {
this->game_mode = GameRoot::get_singleton()->get_game_mode();
}
void GlobalWorldState::_exit_tree() {
if(GlobalWorldState::singleton_instance == this)
GlobalWorldState::singleton_instance = nullptr;
}
void GlobalWorldState::_process(double delta_time) {
global_state_cache.clear(); // invalidate cache
}
Vector3 GlobalWorldState::get_player_position() {
return this->game_mode->get_player_instance()->get_character()->get_global_position();
}
Variant GlobalWorldState::get_world_property(StringName prop_key) {
// check if prop key corresponds to a global key
if(!prop_key.begins_with("g_"))
return nullptr;
// check if the key is cached for this frame
else if(global_state_cache.has(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.insert(prop_key, result);
return result;
}
return nullptr;
}
GlobalWorldState *GlobalWorldState::singleton_instance{nullptr};
}

View file

@ -0,0 +1,31 @@
#ifndef GOAP_GLOBAL_WORLD_STATE_HPP
#define GOAP_GLOBAL_WORLD_STATE_HPP
#include "action.hpp"
#include "tunnels_game_mode.hpp"
#include <godot_cpp/classes/node.hpp>
namespace godot::goap {
class GlobalWorldState : public Node {
GDCLASS(GlobalWorldState, Node);
static void _bind_methods();
public:
static bool has_singleton();
static GlobalWorldState *get_singleton();
virtual void _enter_tree() override;
virtual void _ready() override;
virtual void _exit_tree() override;
virtual void _process(double delta_time) override;
Vector3 get_player_position();
Variant get_world_property(StringName prop_key);
private:
WorldState global_state_cache{};
Ref<TunnelsGameMode> game_mode{};
static GlobalWorldState *singleton_instance;
};
}
#endif // !GOAP_GLOBAL_WORLD_STATE_HPP

237
src/planner.cpp Normal file
View file

@ -0,0 +1,237 @@
#include "planner.hpp"
#include "action.hpp"
#include "character_actor.hpp"
#include "global_world_state.hpp"
#include "state.hpp"
#include "utils/godot_macros.h"
#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);
GDPROPERTY(prerequisites, 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);
}
void Goal::set_prerequisites(Dictionary dict) {
Action::dict_to_property_map(dict, this->prerequisites);
}
Dictionary Goal::get_prerequisites() const {
return Action::property_map_to_dict(this->prerequisites);
}
#undef CLASSNAME // !Goal
PlannerNode PlannerNode::goal_node(WorldState const &goal) {
return PlannerNode{goal};
}
PlannerNode 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;
}
void Planner::_bind_methods() {
#define CLASSNAME Planner
GDPROPERTY_HINTED(actions, Variant::ARRAY, PROPERTY_HINT_ARRAY_TYPE, GDRESOURCETYPE(Action));
GDPROPERTY_HINTED(goals, Variant::ARRAY, PROPERTY_HINT_ARRAY_TYPE, GDRESOURCETYPE(Goal));
}
void Planner::_ready() {
this->global_world_state = GlobalWorldState::get_singleton();
this->actor = Object::cast_to<CharacterActor>(this->get_parent());
}
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;
}
Vector<Ref<Action>> Planner::make_plan() {
// clear cache every planning phase
this->cached_world_state.clear();
Ref<Goal> goal = this->select_goal();
if(!goal.is_valid())
return {};
// ordered list of all nodes still being considered
Vector<PlannerNode> open{PlannerNode::goal_node(goal->goal_state)};
PlannerNode first = open.get(0);
FromMap from{}; // mapping states to the previous in the path
ScoreMap dist_traveled{}; // mapping states to the shortest found distance from start
dist_traveled.insert(first, 0);
ScoreMap best_guess{}; // mapping states to the best guess of the distance to the goal
best_guess.insert(first, first.open_requirements.size());
PlannerNode current{}; // state we're checking for neighbours or completion
while(!open.is_empty()) {
// current is the top of the ordered list
current = open.get(0);
// check if we've reached the goal
if(current.open_requirements.is_empty())
return trace_path(from, current);
// current is no longer considered as it cannot be the end
open.erase(current);
// find all neighbours of this state
Vector<PlannerNode> neighbours = this->find_neighbours_of(current);
for(PlannerNode const& node : neighbours) {
float const new_dist = dist_traveled.get(current) + 1.f; // unweighed distance traveled to neighbour
if(!dist_traveled.has(node) || new_dist < dist_traveled.get(node)) {
// store distances
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 {};
}
Ref<Goal> Planner::select_goal() {
for(Ref<Goal> const &goal : this->goals) {
bool can_try{true};
for(WorldProperty const &prop : goal->prerequisites) {
if(prop.value != this->get_world_property(prop.key)) {
can_try = false;
break;
}
}
if(can_try) return goal;
}
return {};
}
Variant Planner::get_world_property(StringName prop_key) {
if(prop_key.begins_with("g_")) {
return this->global_world_state->get_world_property(prop_key);
} else if(this->cached_world_state.has(prop_key)) {
return this->cached_world_state.get(prop_key);
} else if(this->actor->has_method("get_" + prop_key)) {
Variant val = this->actor->call("get_" + prop_key);
this->cached_world_state.insert(prop_key, val);
return val;
} else return nullptr;
}
bool Planner::can_do(Ref<Action> action) {
for(WorldProperty &prop : action->context_prerequisites) {
if(this->get_world_property(prop.key) != prop.value)
return false;
}
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) {
for(WorldProperty &prop : requirements) {
if(act->effects.has(prop.key)
&& act->effects.get(prop.key) == prop.value
&& this->can_do(act)) {
found_actions.push_back(act);
}
}
}
return found_actions;
}
bool Planner::is_action_complete() {
return this->plan.get(0)->get_is_completed(this->actor);
}
State Planner::get_next_state() {
if(!this->plan.is_empty())
this->plan.remove_at(0);
if(this->plan.is_empty())
this->plan = this->make_plan();
if(this->plan.is_empty())
return State::new_invalid();
return this->plan.get(0)->apply_state->construct(this->actor);
}
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.set(i, act);
}
}
Array Planner::get_actions() const {
Array array{};
for(Ref<Action> const &act : this->actions) {
array.push_back(act);
}
return array;
}
void Planner::set_goals(Array value) {
this->goals.clear();
this->goals.resize(value.size());
for(size_t i{0}; i < value.size(); ++i) {
Ref<Goal> goal = value[i];
if(goal.is_valid())
this->goals.set(i, goal);
}
}
Array Planner::get_goals() const {
Array array{};
for(Ref<Goal> const &goal : this->goals) {
array.push_back(goal);
}
return array;
}
}

109
src/planner.hpp Normal file
View file

@ -0,0 +1,109 @@
#ifndef GOAP_PLANNER_HPP
#define GOAP_PLANNER_HPP
#include "action.hpp"
#include "godot_cpp/variant/variant.hpp"
#include <godot_cpp/classes/node.hpp>
#include <godot_cpp/classes/resource.hpp>
#include <godot_cpp/templates/vector.hpp>
#include <godot_cpp/variant/string_name.hpp>
namespace godot {
class CharacterActor;
namespace goap {
class GlobalWorldState;
class Goal : public Resource {
GDCLASS(Goal, Resource);
static void _bind_methods();
void set_goal_state(Dictionary dict);
Dictionary get_goal_state() const;
void set_prerequisites(Dictionary dict);
Dictionary get_prerequisites() const;
public:
WorldState goal_state{};
WorldState prerequisites{};
};
struct PlannerNode {
WorldState open_requirements{};
WorldState state{};
Ref<Action> last_edge{};
PlannerNode() = default;
PlannerNode(PlannerNode const &src) = default;
static PlannerNode goal_node(WorldState const &goal);
PlannerNode new_node_along(Ref<Action> action) const;
};
class Planner : public Node {
GDCLASS(Planner, Node);
static void _bind_methods();
public:
virtual void _ready() override;
Vector<Ref<Action>> make_plan();
Ref<Goal> select_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);
bool is_action_complete();
State get_next_state();
void set_actions(Array actions);
Array get_actions() const;
void set_goals(Array goals);
Array get_goals() const;
private:
CharacterActor *actor{nullptr}; // the parent actor of this planner
WorldState cached_world_state{}; // the cached worldstate, cleared for every make_plan call
GlobalWorldState *global_world_state{nullptr}; // cached singleton instance
// configured settings
Vector<Ref<Action>> actions{}; // available actions
Vector<Ref<Goal>> goals{}; // available goals
Vector<Ref<Action>> plan{};
};
struct PlannerNodeHasher {
static _FORCE_INLINE_
uint32_t hash(godot::goap::PlannerNode const &node) {
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 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);
}
}
}
#endif // !GOAP_PLANNER_HPP

View file

@ -1,10 +1,14 @@
#include "register_types.h"
#include "character_data.hpp"
#include "action.hpp"
#include "character_actor.hpp"
#include "character_data.hpp"
#include "enemy.hpp"
#include "global_world_state.hpp"
#include "health.hpp"
#include "pellet_projectile.hpp"
#include "planner.hpp"
#include "projectile_pool.hpp"
#include "state.hpp"
#include "tunnels_game_mode.hpp"
#include "tunnels_game_state.hpp"
#include "tunnels_player.hpp"
@ -51,6 +55,15 @@ 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_abstract_class<goap::StateArgs>();
ClassDB::register_class<goap::MoveStateArgs>();
ClassDB::register_class<goap::AnimateStateArgs>();
ClassDB::register_class<goap::ActivateStateArgs>();
ClassDB::register_class<goap::Goal>();
ClassDB::register_class<goap::Planner>();
}
extern "C"

80
src/state.cpp Normal file
View file

@ -0,0 +1,80 @@
#include "state.hpp"
#include "character_actor.hpp"
#include "utils/godot_macros.h"
namespace godot::goap {
State::~State() {
if(unlikely(this->type == STATE_ANIMATE))
delete this->animate;
}
State State::new_move_to(Node3D *node) {
return {
.type = State::Type::STATE_MOVE_TO,
.move_to = node
};
}
State State::new_animate(StringName animation) {
return {
.type = State::Type::STATE_ANIMATE,
.animate = new StringName(animation)
};
}
State State::new_activate(Node *node) {
return {
.type = State::Type::STATE_ACTIVATE,
.activate = node
};
}
State State::new_invalid() {
return { .type = State::Type::STATE_TYPE_MAX };
}
bool State::is_complete(CharacterActor *context) const {
switch(this->type) {
default:
return true;
case STATE_MOVE_TO:
return context->get_global_position().is_equal_approx(this->move_to->get_global_position());
case STATE_ANIMATE:
return false; // TODO: replace this with checks for animation completion
case STATE_ACTIVATE:
return false; // TODO: replace this with checks for object activation
}
}
void StateArgs::_bind_methods() {
#define CLASSNAME StateArgs
GDPROPERTY(argument_property, Variant::STRING_NAME);
}
State StateArgs::construct(Node *context) const {
return { .type = State::STATE_TYPE_MAX };
}
void StateArgs::set_argument_property(StringName var) { this->argument_property = var; }
StringName StateArgs::get_argument_property() const { return this->argument_property; }
void MoveStateArgs::_bind_methods() {}
State MoveStateArgs::construct(Node *context) const {
Node3D *node = Object::cast_to<Node3D>(context->call("get_" + this->argument_property));
return State::new_move_to(node);
}
void AnimateStateArgs::_bind_methods() {}
State AnimateStateArgs::construct(Node *context) const {
return State::new_animate(context->call("get_" + this->argument_property));
}
void ActivateStateArgs::_bind_methods() {}
State ActivateStateArgs::construct(Node *context) const {
Node *node = Object::cast_to<Node>(context->call("get_" + this->argument_property));
return State::new_activate(node);
}
}

66
src/state.hpp Normal file
View file

@ -0,0 +1,66 @@
#ifndef GOAP_STATE_HPP
#define GOAP_STATE_HPP
#include <godot_cpp/variant/vector3.hpp>
#include <godot_cpp/variant/string_name.hpp>
#include <godot_cpp/classes/node.hpp>
#include <godot_cpp/classes/node3d.hpp>
#include <godot_cpp/classes/resource.hpp>
namespace godot { class CharacterActor; }
namespace godot::goap {
struct State {
~State();
static State new_move_to(Node3D *location);
static State new_animate(StringName animation);
static State new_activate(Node *node);
static State new_invalid();
bool is_complete(CharacterActor *context) const;
enum Type {
STATE_MOVE_TO,
STATE_ANIMATE,
STATE_ACTIVATE,
STATE_TYPE_MAX,
};
State::Type type{STATE_TYPE_MAX};
union {
Node3D* move_to;
StringName *animate;
Node *activate;
};
};
class StateArgs : public Resource {
GDCLASS(StateArgs, Resource);
static void _bind_methods();
public:
virtual State construct(Node *context) const;
void set_argument_property(StringName name);
StringName get_argument_property() const;
StringName argument_property;
};
class MoveStateArgs : public StateArgs {
GDCLASS(MoveStateArgs, StateArgs);
static void _bind_methods();
virtual State construct(Node *context) const override;
};
class AnimateStateArgs : public StateArgs {
GDCLASS(AnimateStateArgs, StateArgs);
static void _bind_methods();
public:
virtual State construct(Node *context) const override;
};
class ActivateStateArgs : public StateArgs {
GDCLASS(ActivateStateArgs, StateArgs);
static void _bind_methods();
public:
virtual State construct(Node *context) const override;
};
};
#endif // !GOAP_STATE_HPP