From 1e9454208568ccefcc2e09d250f884e6ac058fbc Mon Sep 17 00:00:00 2001 From: Sara Date: Sun, 18 Jan 2026 23:26:41 +0100 Subject: [PATCH] feat: npc follow logic --- modules/authority/character.cpp | 27 ++++++-- modules/authority/character.h | 2 + modules/authority/nav_marker.cpp | 13 ++++ modules/authority/nav_marker.h | 19 +++++ modules/authority/party_member_states.cpp | 84 +++++++++++++++++++++++ modules/authority/party_member_states.h | 22 ++++++ modules/authority/player_camera.h | 8 +++ modules/authority/player_character.cpp | 36 ++++++++++ modules/authority/player_character.h | 21 ++++++ modules/authority/player_states.cpp | 2 +- modules/authority/register_types.cpp | 6 ++ project/objects/player_character.tscn | 21 +++++- project/project.godot | 1 - project/scenes/test_world.tscn | 17 ++++- 14 files changed, 268 insertions(+), 11 deletions(-) create mode 100644 modules/authority/nav_marker.cpp create mode 100644 modules/authority/nav_marker.h create mode 100644 modules/authority/party_member_states.cpp create mode 100644 modules/authority/party_member_states.h create mode 100644 modules/authority/player_camera.h create mode 100644 modules/authority/player_character.cpp create mode 100644 modules/authority/player_character.h diff --git a/modules/authority/character.cpp b/modules/authority/character.cpp index 0dfdb3b8..6b738ed7 100644 --- a/modules/authority/character.cpp +++ b/modules/authority/character.cpp @@ -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(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(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)); diff --git a/modules/authority/character.h b/modules/authority/character.h index b523b0d1..db259213 100644 --- a/modules/authority/character.h +++ b/modules/authority/character.h @@ -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); diff --git a/modules/authority/nav_marker.cpp b/modules/authority/nav_marker.cpp new file mode 100644 index 00000000..12690b04 --- /dev/null +++ b/modules/authority/nav_marker.cpp @@ -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; + } +} diff --git a/modules/authority/nav_marker.h b/modules/authority/nav_marker.h new file mode 100644 index 00000000..8e06579b --- /dev/null +++ b/modules/authority/nav_marker.h @@ -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); +}; diff --git a/modules/authority/party_member_states.cpp b/modules/authority/party_member_states.cpp new file mode 100644 index 00000000..99774610 --- /dev/null +++ b/modules/authority/party_member_states.cpp @@ -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(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(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 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); + } +} diff --git a/modules/authority/party_member_states.h b/modules/authority/party_member_states.h new file mode 100644 index 00000000..85faa988 --- /dev/null +++ b/modules/authority/party_member_states.h @@ -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 }; +}; diff --git a/modules/authority/player_camera.h b/modules/authority/player_camera.h new file mode 100644 index 00000000..a16c13a6 --- /dev/null +++ b/modules/authority/player_camera.h @@ -0,0 +1,8 @@ +#pragma once + +#include "scene/3d/camera_3d.h" + +class PlayerCamera : public Camera3D { + GDCLASS(PlayerCamera, Camera3D); + static void _bind_methods(); +}; diff --git a/modules/authority/player_character.cpp b/modules/authority/player_character.cpp new file mode 100644 index 00000000..34610c5d --- /dev/null +++ b/modules/authority/player_character.cpp @@ -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(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; +} diff --git a/modules/authority/player_character.h b/modules/authority/player_character.h new file mode 100644 index 00000000..48f7cc67 --- /dev/null +++ b/modules/authority/player_character.h @@ -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 party_follow_markers{}; + static PlayerCharacter *instance; + +public: + static PlayerCharacter *get_singleton(); + GET_SET_FNS(Vector const &, party_follow_markers); +}; diff --git a/modules/authority/player_states.cpp b/modules/authority/player_states.cpp index 593a790f..393730d8 100644 --- a/modules/authority/player_states.cpp +++ b/modules/authority/player_states.cpp @@ -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) { diff --git a/modules/authority/register_types.cpp b/modules/authority/register_types.cpp index d0dab1c2..d2ad6d36 100644 --- a/modules/authority/register_types.cpp +++ b/modules/authority/register_types.cpp @@ -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(); ClassDB::register_class(); ClassDB::register_class(); + ClassDB::register_class(); ClassDB::register_class(); ClassDB::register_class(); + ClassDB::register_class(); + ClassDB::register_class(); } void uninitialize_authority_module(ModuleInitializationLevel p_level) { diff --git a/project/objects/player_character.tscn b/project/objects/player_character.tscn index f6736957..d94c6468 100644 --- a/project/objects/player_character.tscn +++ b/project/objects/player_character.tscn @@ -6,7 +6,7 @@ [sub_resource type="CylinderMesh" id="CylinderMesh_5kd2n"] -[node name="PlayerCharacter" type="Character" unique_id=1694717421] +[node name="PlayerCharacter" type="PlayerCharacter" unique_id=159035892] data = ExtResource("1_jy05a") [node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=511026275] @@ -16,10 +16,27 @@ shape = SubResource("CapsuleShape3D_vcg8s") mesh = SubResource("CylinderMesh_5kd2n") [node name="Camera3D" type="Camera3D" parent="." unique_id=932811285] -transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, 9.536743e-07, 18.920525, -0.37265897) +transform = Transform3D(-1, 5.660465e-08, -6.662324e-08, 0, 0.762081, 0.64748174, 8.742278e-08, 0.64748174, -0.762081, 9.536743e-07, 5.8584056, -4.4809494) current = true +far = 1000.0 [node name="PlayerInputState" type="PlayerInputState" parent="." unique_id=1290843255] start_active = true [node name="PlayerMovementState" type="PlayerMovementState" parent="." unique_id=71639209] + +[node name="NavMarker" type="NavMarker" parent="." unique_id=2076207950] +transform = Transform3D(-1, 0, 8.742278e-08, 0, 1, 0, -8.742278e-08, 0, -1, 2.0248106, 0, -1.4610382) +gizmo_extents = 3.0 + +[node name="NavMarker2" type="NavMarker" parent="." unique_id=786944405] +transform = Transform3D(-1, 0, 8.742278e-08, 0, 1, 0, -8.742278e-08, 0, -1, -0.54701775, 9.536743e-07, -2.7108493) +gizmo_extents = 3.0 + +[node name="NavMarker4" type="NavMarker" parent="." unique_id=1781147686] +transform = Transform3D(-1, 0, 8.742278e-08, 0, 1, 0, -8.742278e-08, 0, -1, 0.8027056, 9.536743e-07, -2.7077246) +gizmo_extents = 3.0 + +[node name="NavMarker3" type="NavMarker" parent="." unique_id=430426412] +transform = Transform3D(-1, 0, 8.742278e-08, 0, 1, 0, -8.742278e-08, 0, -1, -2.0046833, 9.536743e-07, -1.4623514) +gizmo_extents = 3.0 diff --git a/project/project.godot b/project/project.godot index 52af6690..c4de7177 100644 --- a/project/project.godot +++ b/project/project.godot @@ -23,7 +23,6 @@ config/icon="res://icon.svg" window/size/viewport_width=1920 window/size/viewport_height=1080 -window/size/mode=3 [input] diff --git a/project/scenes/test_world.tscn b/project/scenes/test_world.tscn index f974d9d4..663f3d0d 100644 --- a/project/scenes/test_world.tscn +++ b/project/scenes/test_world.tscn @@ -1,6 +1,7 @@ [gd_scene format=3 uid="uid://cv0ub3llm3jew"] [ext_resource type="PackedScene" uid="uid://dcqd0wo5y5a1g" path="res://objects/player_character.tscn" id="1_kyfjp"] +[ext_resource type="PackedScene" uid="uid://dfbdn64i7vfuc" path="res://objects/party_member.tscn" id="2_amxg5"] [sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_kyfjp"] sky_horizon_color = Color(0.66224277, 0.6717428, 0.6867428, 1) @@ -15,6 +16,10 @@ sky = SubResource("Sky_amxg5") tonemap_mode = 2 glow_enabled = true +[sub_resource type="NavigationMesh" id="NavigationMesh_amxg5"] +vertices = PackedVector3Array(-8.5, 0.5, -9, -6.5, 0.5, -9, -6.5, 0.5, -49.5, -49.5, 0.5, -7.5, -8.75, 0.5, -7.25, -49.5, 0.5, -49.5, -4.25, 0.5, -9, -4.25, 0.5, -49.5, -2, 0.5, -8.75, 49.5, 0.5, -6.5, 49.5, 0.5, -49.5, -2, 0.5, -6.5, -7.75, 2.5, -8, -7.75, 2.5, -5.25, -3, 2.5, -5.25, -3, 2.5, -8, -49.5, 0.5, -5.75, -8.75, 0.5, -6, -2, 0.5, -4.5, -3.75, 0.5, -4.25, -3.5, 0.5, 49.5, 49.5, 0.5, 49.5, -8.75, 0.5, -4.5, -7, 0.5, -4.25, -49.5, 0.5, 49.5, -7.25, 0.5, 49.5) +polygons = [PackedInt32Array(2, 1, 0), PackedInt32Array(4, 3, 0), PackedInt32Array(0, 3, 5), PackedInt32Array(0, 5, 2), PackedInt32Array(2, 7, 1), PackedInt32Array(1, 7, 6), PackedInt32Array(6, 7, 8), PackedInt32Array(8, 7, 10), PackedInt32Array(8, 10, 9), PackedInt32Array(9, 11, 8), PackedInt32Array(15, 14, 12), PackedInt32Array(12, 14, 13), PackedInt32Array(17, 16, 4), PackedInt32Array(4, 16, 3), PackedInt32Array(18, 11, 9), PackedInt32Array(18, 9, 19), PackedInt32Array(19, 9, 20), PackedInt32Array(20, 9, 21), PackedInt32Array(16, 17, 22), PackedInt32Array(22, 23, 16), PackedInt32Array(16, 23, 25), PackedInt32Array(16, 25, 24), PackedInt32Array(23, 19, 25), PackedInt32Array(25, 19, 20)] + [node name="TestWorld" type="Node3D" unique_id=262419127] [node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=1185961481] @@ -27,13 +32,19 @@ shadow_enabled = true [node name="PlayerCharacter" parent="." unique_id=1435471129 instance=ExtResource("1_kyfjp")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0) -[node name="CSGCombiner3D" type="CSGCombiner3D" parent="." unique_id=885387983] +[node name="PartyMember" parent="." unique_id=2124931928 instance=ExtResource("2_amxg5")] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 2, 1, 5) + +[node name="NavigationRegion3D" type="NavigationRegion3D" parent="." unique_id=357996274] +navigation_mesh = SubResource("NavigationMesh_amxg5") + +[node name="CSGCombiner3D" type="CSGCombiner3D" parent="NavigationRegion3D" unique_id=885387983] use_collision = true -[node name="CSGBox3D" type="CSGBox3D" parent="CSGCombiner3D" unique_id=1853081325] +[node name="CSGBox3D" type="CSGBox3D" parent="NavigationRegion3D/CSGCombiner3D" unique_id=1853081325] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.5, 0) size = Vector3(100, 1, 100) -[node name="CSGBox3D2" type="CSGBox3D" parent="CSGCombiner3D" unique_id=40055740] +[node name="CSGBox3D2" type="CSGBox3D" parent="NavigationRegion3D/CSGCombiner3D" unique_id=40055740] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -5.293318, 1.1054688, -6.6544046) size = Vector3(5.5302734, 2.2109375, 3.6298828)