feat: npc follow logic

This commit is contained in:
Sara Gerretsen 2026-01-18 23:26:41 +01:00
parent 411f1662c1
commit 1e94542085
14 changed files with 268 additions and 11 deletions

View file

@ -13,10 +13,12 @@ void Character::_bind_methods() {
void Character::physics_process(double delta) {
Vector3 const velocity{ get_velocity() };
Vector3 new_velocity{ velocity };
new_velocity.x = this->world_movement_direction.x * this->data->get_speed();
new_velocity.z = this->world_movement_direction.y * this->data->get_speed();
new_velocity.x = this->world_movement_direction.x;
new_velocity.z = this->world_movement_direction.y;
set_velocity(new_velocity);
move_and_slide();
if (!velocity.is_zero_approx()) {
move_and_slide();
}
}
void Character::_notification(int what) {
@ -35,6 +37,14 @@ void Character::_notification(int what) {
}
}
PackedStringArray Character::get_configuration_warnings() const {
PackedStringArray warnings{ super_type::get_configuration_warnings() };
if (this->data.is_null()) {
warnings.push_back("Character requires 'data' to be initialised. To avoid crashes consider adding a placeholder if you intend to programmatically initialise it.");
}
return warnings;
}
void Character::set_movement(Vector2 movement) {
this->world_movement_direction = movement;
}
@ -56,15 +66,24 @@ void CharacterState::_notification(int what) {
return;
case NOTIFICATION_ENTER_TREE:
this->character = cast_to<Character>(get_parent());
ERR_FAIL_COND_EDMSG(this->character == nullptr, "CharacterState requires parent to be of type Character");
return;
case NOTIFICATION_READY:
if (start_active) {
set_state_active(true);
callable_mp(this, &self_type::set_state_active).call_deferred(true);
}
return;
}
}
PackedStringArray CharacterState::get_configuration_warnings() const {
PackedStringArray warnings{ super_type::get_configuration_warnings() };
if (cast_to<Character>(get_parent()) == nullptr) {
warnings.push_back("CharacterState requires direct Character parent");
}
return warnings;
}
void CharacterState::switch_to_state(String value) {
if (!this->state_active) {
print_error(vformat("Attempt to switch from inactive state %s to new state %s", get_path(), value));

View file

@ -23,6 +23,7 @@ protected:
static void _bind_methods();
void physics_process(double delta);
void _notification(int what);
PackedStringArray get_configuration_warnings() const override;
public:
void set_movement(Vector2 movement);
@ -42,6 +43,7 @@ class CharacterState : public Node {
protected:
void _notification(int what);
PackedStringArray get_configuration_warnings() const override;
void switch_to_state(String state);
void stack_state_dependent(String state);
void notify_dependent_inactive(CharacterState *dependent);

View file

@ -0,0 +1,13 @@
#include "nav_marker.h"
void NavMarker::_bind_methods() {}
void NavMarker::_notification(int what) {
switch (what) {
default:
return;
case NOTIFICATION_ENTER_TREE:
this->set_gizmo_extents(3);
return;
}
}

View file

@ -0,0 +1,19 @@
#pragma once
#include "authority/macros.h"
#include "scene/3d/marker_3d.h"
class Character;
class NavMarker : public Marker3D {
GDCLASS(NavMarker, Marker3D);
static void _bind_methods();
protected:
void _notification(int what);
private:
Character *claimed{ nullptr };
public:
GET_SET_FNS(Character *, claimed);
};

View file

@ -0,0 +1,84 @@
#include "party_member_states.h"
#include "authority/nav_marker.h"
#include "authority/player_character.h"
#include "core/config/engine.h"
#include "core/error/error_macros.h"
#include "core/templates/vector.h"
void PartyMemberFollow::_bind_methods() {}
void PartyMemberFollow::process_position_target() {
Vector3 const marker_position{ this->claimed_marker->get_global_position() };
Vector3 const nav_target{ this->nav->get_target_position() };
Vector3 const global_position{ get_character()->get_global_position() };
if (global_position.distance_squared_to(marker_position) < 0.5) {
return;
}
if (nav_target.distance_squared_to(marker_position) > 0.25) {
this->nav->set_target_position(marker_position);
}
if (this->nav->is_navigation_finished()) {
return;
}
Vector3 velocity{ global_position.direction_to(this->nav->get_next_path_position()) };
velocity.y = 0;
if (this->nav->get_avoidance_enabled()) {
this->nav->set_velocity(velocity * get_character()->get_data()->get_speed());
} else {
push_movement_direction(velocity * get_character()->get_data()->get_speed());
}
}
void PartyMemberFollow::push_movement_direction(Vector3 velocity) {
get_character()->set_movement(Vector2{ velocity.x, velocity.z });
}
void PartyMemberFollow::_notification(int what) {
if (Engine::get_singleton()->is_editor_hint()) {
return;
}
switch (what) {
default:
return;
case NOTIFICATION_READY:
this->nav = cast_to<NavigationAgent3D>(get_parent()->get_node(NodePath("%NavigationAgent3D")));
ERR_FAIL_COND_EDMSG(this->nav == nullptr, "PartyMemberFollow cannot initialise without a navigation agent");
return;
case NOTIFICATION_PROCESS:
process_position_target();
return;
}
}
PackedStringArray PartyMemberFollow::get_configuration_warnings() const {
PackedStringArray warnings{ super_type::get_configuration_warnings() };
if (!get_parent()->has_node(NodePath("%NavigationAgent3D")) || !cast_to<NavigationAgent3D>(get_parent()->get_node(NodePath("%NavigationAgent3D")))) {
warnings.push_back("PartyMemberFollow expects a scene sibling of type NavigationAgent3D named with unique name '%NavigationAgent3D'");
}
return warnings;
}
void PartyMemberFollow::state_entered() {
Vector<NavMarker *> const &markers{ PlayerCharacter::get_singleton()->get_party_follow_markers() };
for (NavMarker *marker : markers) {
if (marker->get_claimed() == nullptr) {
marker->set_claimed(get_character());
this->claimed_marker = marker;
if (this->nav->get_avoidance_enabled()) {
this->nav->connect("velocity_computed", callable_mp(this, &self_type::push_movement_direction));
}
set_process(true);
return;
}
}
ERR_FAIL_EDMSG("PartyMemberFollow could not find an unclaimed player follow marker");
set_state_active(false);
}
void PartyMemberFollow::state_exited() {
if (this->claimed_marker) {
this->claimed_marker->set_claimed(nullptr);
this->nav->disconnect("velocity_computed", callable_mp(this, &self_type::push_movement_direction));
set_process(false);
}
}

View file

@ -0,0 +1,22 @@
#pragma once
#include "authority/character.h"
#include "authority/nav_marker.h"
#include "scene/3d/navigation/navigation_agent_3d.h"
class PartyMemberFollow : public CharacterState {
GDCLASS(PartyMemberFollow, CharacterState);
static void _bind_methods();
void process_position_target();
void push_movement_direction(Vector3 velocity);
protected:
void _notification(int what);
PackedStringArray get_configuration_warnings() const override;
void state_entered() override;
void state_exited() override;
private:
NavigationAgent3D *nav{ nullptr };
NavMarker *claimed_marker{ nullptr };
};

View file

@ -0,0 +1,8 @@
#pragma once
#include "scene/3d/camera_3d.h"
class PlayerCamera : public Camera3D {
GDCLASS(PlayerCamera, Camera3D);
static void _bind_methods();
};

View file

@ -0,0 +1,36 @@
#include "player_character.h"
#include "authority/nav_marker.h"
void PlayerCharacter::_bind_methods() {}
void PlayerCharacter::_notification(int what) {
if (Engine::get_singleton()->is_editor_hint()) {
return;
}
switch (what) {
default:
return;
case NOTIFICATION_ENTER_TREE:
instance = this;
return;
case NOTIFICATION_READY:
for (Variant var : find_children("*", "NavMarker")) {
if (NavMarker * marker{ cast_to<NavMarker>(var) }) {
this->party_follow_markers.push_back(marker);
}
}
ERR_FAIL_COND_EDMSG(this->party_follow_markers.size() < 4, "PlayerCharacter should have at least 4 follow NavMarkers for party members");
return;
case NOTIFICATION_EXIT_TREE:
if (instance == this) {
instance = nullptr;
}
return;
}
}
PlayerCharacter *PlayerCharacter::instance{ nullptr };
PlayerCharacter *PlayerCharacter::get_singleton() {
return instance;
}

View file

@ -0,0 +1,21 @@
#pragma once
#include "authority/character.h"
#include "authority/macros.h"
#include "authority/nav_marker.h"
class PlayerCharacter : public Character {
GDCLASS(PlayerCharacter, Character);
static void _bind_methods();
protected:
void _notification(int what);
private:
Vector<NavMarker *> party_follow_markers{};
static PlayerCharacter *instance;
public:
static PlayerCharacter *get_singleton();
GET_SET_FNS(Vector<NavMarker *> const &, party_follow_markers);
};

View file

@ -32,7 +32,7 @@ void PlayerMovementState::process(double delta) {
backward = Vector2{ basis.get_column(1).x, basis.get_column(1).z };
}
Vector2 const right{ basis.get_column(2).x, basis.get_column(2).z };
get_character()->set_movement((backward.normalized() * this->movement.x + right.normalized() * this->movement.y));
get_character()->set_movement((backward.normalized() * this->movement.x + right.normalized() * this->movement.y) * get_character()->get_data()->get_speed());
}
void PlayerMovementState::_notification(int what) {

View file

@ -1,6 +1,9 @@
#include "register_types.h"
#include "authority/character.h"
#include "authority/nav_marker.h"
#include "authority/party_member_states.h"
#include "authority/player_character.h"
#include "authority/player_states.h"
#include "core/object/class_db.h"
@ -11,8 +14,11 @@ void initialize_authority_module(ModuleInitializationLevel p_level) {
ClassDB::register_class<CharacterData>();
ClassDB::register_class<Character>();
ClassDB::register_class<CharacterState>();
ClassDB::register_class<PlayerCharacter>();
ClassDB::register_class<PlayerInputState>();
ClassDB::register_class<PlayerMovementState>();
ClassDB::register_class<NavMarker>();
ClassDB::register_class<PartyMemberFollow>();
}
void uninitialize_authority_module(ModuleInitializationLevel p_level) {