feat: fully implemented click-to-move

This commit is contained in:
Sara 2024-06-11 09:08:49 +02:00
parent 1ddfa17659
commit 8cfbbcce27
24 changed files with 684 additions and 90 deletions

View file

@ -9,8 +9,8 @@
#define GOAP_ACTION(Name_) \
public: \
_FORCE_INLINE_ static gd::String get_static_class() { return #Name_; }\
_FORCE_INLINE_ virtual gd::String get_class() const override { return #Name_; }\
_FORCE_INLINE_ static gd::StringName get_static_class() { return #Name_; }\
_FORCE_INLINE_ virtual gd::StringName get_class() const override { return #Name_; }\
private:
namespace goap {
@ -23,11 +23,11 @@ typedef int ActionID;
class Action {
friend class ActionDB;
public:
static gd::String get_static_class() { return "Action"; }
virtual gd::String get_class() const { return "Action"; }
static gd::StringName get_static_class() { return "Action"; }
virtual gd::StringName get_class() const { return "Action"; }
virtual ~Action();
virtual State *get_apply_state() const = 0;
virtual State *get_apply_state(ActorWorldState *context) const = 0;
bool is_completed(ActorWorldState *context) const;
bool is_possible(ActorWorldState *context) const;
@ -38,7 +38,8 @@ public:
ActionID get_id() const;
protected:
Action() = default;
template<class TState>
TState *create_state() const;
virtual bool procedural_is_possible(ActorWorldState *context) const;
virtual bool procedural_is_completed(ActorWorldState *context) const;
protected:
@ -49,6 +50,13 @@ protected:
private:
ActionID id{-1};
};
template <class TState>
TState *Action::create_state() const {
TState *state = memnew(TState);
state->action = this;
return state;
}
}
#endif // !GOAP_ACTION_HPP

View file

@ -1,5 +1,6 @@
#include "planner.hpp"
#include "action_db.hpp"
#include "godot_cpp/classes/engine.hpp"
#include "godot_cpp/classes/global_constants.hpp"
#include "godot_cpp/templates/hashfuncs.hpp"
#include "utils/godot_macros.hpp"
@ -106,49 +107,58 @@ Plan Planner::plan_for_goal(gd::Ref<Goal> goal) {
return {};
}
gd::Vector<WorldStateNode> open{goal_node};
NodeMap from{};
NodeScoreMap best_path_cost{};
NodeScoreMap heuristic_cost{};
NodeMap from{}; // mapping nodes to the next node in the plan
NodeScoreMap best_path_cost{}; // map nodes to the cost of the best path found so far
NodeScoreMap heuristic_cost{}; // map nodes to their approximate cost to get to their goal
// insert with the goal node into the cost maps,
// path scored 0 as it's the goal
best_path_cost.insert(goal_node, 0.f);
heuristic_cost.insert(goal_node, goal_node.requirements_unmet());
WorldStateNode current{goal_node};
// repeat until current has no traversible edges
while(!open.is_empty()) {
current = open.get(0);
// return found path when all requirements are met
if(current.requirements_unmet() == 0)
return this->unroll_plan(current, from);
open.remove_at(0);
open.remove_at(0); // pop current off the top of the open set
// get all actions that contribute to the open requirements of `current`
gd::Vector<Action const *> edges{this->get_neighbours(current)};
gd::UtilityFunctions::print("found ", edges.size(), " possible actions");
for(Action const * action : edges) {
// construct the node produced by traversing edge
WorldStateNode node_through{current, action};
// calculate the cost of getting here from current through action.
float const path_cost{best_path_cost.get(current) + 1.f};
gd::UtilityFunctions::print("hashes ", WorldStateNodeHasher::hash(node_through), " ; ", WorldStateNodeHasher::hash(current));
gd::UtilityFunctions::print("equiv: ", current == node_through);
// store only if path through `current` represents a new fastest way to get to the new node
if(!best_path_cost.has(node_through) || best_path_cost.get(node_through) >= path_cost) {
gd::UtilityFunctions::print("node through action ", action->get_class(), " added to open set");
// update cheapest route found
best_path_cost[node_through] = path_cost;
// approximate cost to goal
heuristic_cost[node_through] = path_cost + node_through.requirements_unmet();
// store path to get to this point
from[node_through] = current;
open.erase(node_through);
// (re-) insert node according to priority based on unmet requirements
if(open.has(node_through))
open.erase(node_through);
open.ordered_insert(node_through);
} else {
gd::UtilityFunctions::print("node through action ", action->get_class(), " scores worse than previous at same state. ", path_cost, " vs ", best_path_cost[node_through]);
}
}
}
gd::UtilityFunctions::print("failed to find plan for goal ", goal->get_path());
gd::UtilityFunctions::push_warning("failed to find plan for goal ", goal->get_path());
return {};
}
void Planner::set_actions(gd::Array array) {
this->actions.clear();
for(size_t i = 0; i < array.size(); ++i) {
Action const *action = ActionDB::get_action(array[i]);
Action const *action = int(array[i]) < 0 ? nullptr : ActionDB::get_action(array[i]);
if(action != nullptr && !this->actions.has(action)) {
this->actions.push_back(action);
} else if(gd::Engine::get_singleton()->is_editor_hint() && !this->actions.has(nullptr)) {
this->actions.push_back(nullptr);
}
}
}
@ -156,7 +166,7 @@ void Planner::set_actions(gd::Array array) {
gd::Array Planner::get_actions() const {
gd::Array array{};
for(Action const *action : this->actions)
array.push_back(action->get_id());
array.push_back(action == nullptr ? -1 : action->get_id());
return array;
}

View file

@ -7,7 +7,6 @@
#include <cstdint>
#include <godot_cpp/classes/node.hpp>
#include <godot_cpp/templates/vector.hpp>
#include <godot_cpp/variant/packed_int32_array.hpp>
namespace gd = godot;
@ -25,7 +24,7 @@ struct WorldStateNode {
WorldStateNode(WorldStateNode const &last_state, Action const *last_action);
~WorldStateNode() = default;
int requirements_unmet() const;
WorldState state{};
WorldState state{}; // the only part considered in hashing
WorldState open_requirements{};
Action const *last_action{nullptr};
ActorWorldState *context;

View file

@ -1,32 +1,33 @@
#include "state.hpp"
#include "goap/action.hpp"
#include "goap/actor_world_state.hpp"
#include "utils/godot_macros.hpp"
namespace goap {
void State::_bind_methods() {}
void MoveTo::_bind_methods() {}
void MoveTo::_enter_tree() {
this->agent = this->get_node<gd::NavigationAgent3D>("../NavigationAgent3D");
this->agent->set_target_position(this->target_node->get_global_position());
this->parent_node3d = Object::cast_to<gd::Node3D>(this->get_parent());
void State::_bind_methods() {
#define CLASSNAME State
GDSIGNAL("end_state");
}
void MoveTo::_exit_tree() {
this->agent->set_target_position(this->parent_node3d->get_global_position());
void State::_enter_tree() {
this->world_state = this->get_node<ActorWorldState>("../ActorWorldState");
}
void MoveTo::_process(double delta_time) {
gd::Vector3 const pos = this->parent_node3d->get_global_position();
gd::Vector3 const target = this->agent->get_next_path_position();
gd::Vector3 const direction = (target - pos).normalized();
this->parent_node3d->set_global_position(direction * delta_time);
this->parent_node3d->look_at(pos - gd::Vector3{direction.x, 0.f, direction.z});
if(this->agent->is_navigation_finished())
this->queue_free();
void State::_process(double delta_time) {
if(this->action->is_completed(this->world_state)) {
this->end_state();
}
}
void Activate::_bind_methods() {}
void Animate::_bind_methods() {}
Action const *State::get_action() const {
return this->action;
}
void State::_end_state() {}
void State::end_state() {
this->_end_state();
this->queue_free();
this->emit_signal("end_state");
}
}

View file

@ -1,47 +1,29 @@
#ifndef GOAP_STATE_HPP
#define GOAP_STATE_HPP
#include "godot_cpp/classes/navigation_agent3d.hpp"
#include "goap/actor_world_state.hpp"
#include <godot_cpp/classes/node.hpp>
#include <godot_cpp/classes/node3d.hpp>
namespace gd = godot;
namespace goap {
class State : public gd::Node {
GDCLASS(State, gd::Node);
static void _bind_methods();
};
class Action;
/*! Uses navigation to chase the desired target node.
*/
class MoveTo : public State {
GDCLASS(MoveTo, State);
class State : public gd::Node {
friend class Action;
GDCLASS(State, gd::Node);
static void _bind_methods();
public:
virtual void _enter_tree() override;
virtual void _exit_tree() override;
virtual void _process(double delta_time) override;
public:
gd::Node3D *target_node{nullptr};
protected:
Action const *get_action() const;
virtual void _end_state();
void end_state();
private:
gd::Node3D *parent_node3d{nullptr};
gd::NavigationAgent3D *agent{nullptr};
};
class Activate : public State {
GDCLASS(Activate, State);
static void _bind_methods();
public:
gd::Node3D *target_node{nullptr};
gd::String animation{};
};
class Animate : public State {
GDCLASS(Animate, State);
static void _bind_methods();
public:
gd::String animation{};
ActorWorldState *world_state{nullptr};
Action const *action{nullptr};
};
}