feat: implemented last bits required to create a 'follow player' goal

This commit is contained in:
Sara 2024-04-01 23:26:28 +02:00
parent 540d91ddcf
commit ea730d61b4
7 changed files with 128 additions and 58 deletions

View file

@ -1,4 +1,4 @@
[gd_scene load_steps=12 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,38 +18,21 @@ albedo_color = Color(0.94902, 0.909804, 0, 1)
[sub_resource type="BoxMesh" id="BoxMesh_f5yvh"]
size = Vector3(0.125, 0.14, 0.94)
[sub_resource type="MoveStateArgs" id="MoveStateArgs_752r2"]
argument_property = &"target_position"
[sub_resource type="MoveStateArgs" id="MoveStateArgs_ibmkn"]
argument_property = &"player_character"
[sub_resource type="Action" id="Action_ksl64"]
[sub_resource type="Action" id="Action_gtisq"]
effects = {
"prereq": true
"is_near_player": true
}
apply_state = SubResource("MoveStateArgs_752r2")
[sub_resource type="Action" id="Action_mdru6"]
effects = {
"not_goal": 1
}
[sub_resource type="AnimateStateArgs" id="AnimateStateArgs_tlart"]
argument_property = &"fire_weapon"
[sub_resource type="Action" id="Action_7v1i5"]
prerequisites = {
"prereq": true
}
effects = {
"goal": true
}
apply_state = SubResource("AnimateStateArgs_tlart")
apply_state = SubResource("MoveStateArgs_ibmkn")
[sub_resource type="Goal" id="Goal_sqtwb"]
goal_state = {
"target_dead": true
"is_near_player": true
}
prerequisites = {
"has_target": true
"is_near_player": false
}
[node name="PlayerCharacter" type="CharacterActor"]
@ -83,5 +66,5 @@ surface_material_override/0 = SubResource("StandardMaterial3D_scmx3")
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")]
actions = [SubResource("Action_gtisq")]
goals = [SubResource("Goal_sqtwb")]

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,12 +26,14 @@ 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) {
this->try_fire_weapon();
@ -83,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) {
@ -119,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

@ -3,14 +3,18 @@
#include "character_data.hpp"
#include "health.hpp"
#include "state.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 {
@ -38,18 +42,18 @@ public:
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:
@ -65,19 +69,21 @@ private:
float fire_timer{0.f};
// the origin point for projectiles
Node3D *weapon_muzzle{nullptr};
// whatever the AI is currently targetting
// something that the AI wants to target
Node *target{nullptr};
// the current state of the actor
goap::State current_state{}; // the current state
goap::State current_state{goap::State::new_invalid()};
AnimationPlayer *anim_player{nullptr};
Health *health{nullptr};
ProjectilePool *primary_weapon_pool{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 the current weapon's rpm
float fire_interval{0.f}; // derived from 1 / the current weapon's rps
static float const ACCELERATION;
static float const WALK_SPEED;

View file

@ -2,6 +2,7 @@
#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>
@ -61,7 +62,6 @@ 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));
GDFUNCTION_ARGS(gdscript_make_plan, "goal");
}
void Planner::_ready() {
@ -79,21 +79,12 @@ static Vector<Ref<Action>> trace_path(FromMap &map, PlannerNode &end) {
return edges;
}
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) {
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);
@ -130,6 +121,20 @@ Vector<Ref<Action>> Planner::make_plan(Ref<Goal> goal) {
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);
@ -172,13 +177,28 @@ Vector<Ref<Action>> Planner::find_actions_satisfying(WorldState requirements) {
for(WorldProperty &prop : requirements) {
if(act->effects.has(prop.key)
&& act->effects.get(prop.key) == prop.value
&& this->can_do(act))
&& 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());

View file

@ -46,8 +46,8 @@ class Planner : public Node {
public:
virtual void _ready() override;
Array gdscript_make_plan(Ref<Goal> goal);
Vector<Ref<Action>> make_plan(Ref<Goal> goal);
Vector<Ref<Action>> make_plan();
Ref<Goal> select_goal();
Variant get_world_property(StringName prop_key);
@ -55,9 +55,11 @@ public:
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:
@ -67,6 +69,7 @@ private:
// configured settings
Vector<Ref<Action>> actions{}; // available actions
Vector<Ref<Goal>> goals{}; // available goals
Vector<Ref<Action>> plan{};
};
struct PlannerNodeHasher {

View file

@ -29,6 +29,10 @@ State State::new_activate(Node *node) {
};
}
State State::new_invalid() {
return { .type = State::Type::STATE_TYPE_MAX };
}
bool State::is_complete(CharacterActor *context) const {
switch(this->type) {
default:

View file

@ -14,6 +14,7 @@ struct 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;