Merge remote-tracking branch 'origin/goap-planner'
This commit is contained in:
commit
ba6bc4fd6d
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
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=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
78
src/action.cpp
Normal 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
43
src/action.hpp
Normal 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
|
|
@ -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())
|
||||
|
|
|
@ -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;
|
||||
|
|
60
src/global_world_state.cpp
Normal file
60
src/global_world_state.cpp
Normal 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};
|
||||
}
|
||||
|
31
src/global_world_state.hpp
Normal file
31
src/global_world_state.hpp
Normal 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
237
src/planner.cpp
Normal 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
109
src/planner.hpp
Normal 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
|
|
@ -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
80
src/state.cpp
Normal 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
66
src/state.hpp
Normal 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
|
Loading…
Reference in a new issue