Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add advanced NavigationServer3D tests #78667

Merged
merged 1 commit into from
Jul 21, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
281 changes: 281 additions & 0 deletions tests/servers/test_navigation_server_3d.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,38 @@
#ifndef TEST_NAVIGATION_SERVER_3D_H
#define TEST_NAVIGATION_SERVER_3D_H

#include "scene/3d/mesh_instance_3d.h"
#include "scene/resources/primitive_meshes.h"
#include "servers/navigation_server_3d.h"

#include "tests/test_macros.h"

namespace TestNavigationServer3D {

// TODO: Find a more generic way to create `Callable` mocks.
class CallableMock : public Object {
GDCLASS(CallableMock, Object);

public:
void function1(Variant arg0) {
function1_calls++;
function1_latest_arg0 = arg0;
}

unsigned function1_calls{ 0 };
Variant function1_latest_arg0{};
};

static inline Array build_array() {
return Array();
}
template <typename... Targs>
static inline Array build_array(Variant item, Targs... Fargs) {
Array a = build_array(Fargs...);
a.push_front(item);
return a;
}

TEST_SUITE("[Navigation]") {
TEST_CASE("[NavigationServer3D] Server should be empty when initialized") {
NavigationServer3D *navigation_server = NavigationServer3D::get_singleton();
Expand Down Expand Up @@ -330,6 +357,260 @@ TEST_SUITE("[Navigation]") {

navigation_server->free(region);
}

// This test case does not check precise values on purpose - to not be too sensitivte.
TEST_CASE("[NavigationServer3D] Server should move agent properly") {
NavigationServer3D *navigation_server = NavigationServer3D::get_singleton();

RID map = navigation_server->map_create();
RID agent = navigation_server->agent_create();

navigation_server->map_set_active(map, true);
navigation_server->agent_set_map(agent, map);
navigation_server->agent_set_avoidance_enabled(agent, true);
navigation_server->agent_set_velocity(agent, Vector3(1, 0, 1));
CallableMock agent_avoidance_callback_mock;
navigation_server->agent_set_avoidance_callback(agent, callable_mp(&agent_avoidance_callback_mock, &CallableMock::function1));
CHECK_EQ(agent_avoidance_callback_mock.function1_calls, 0);
navigation_server->process(0.0); // Give server some cycles to commit.
CHECK_EQ(agent_avoidance_callback_mock.function1_calls, 1);
CHECK_NE(agent_avoidance_callback_mock.function1_latest_arg0, Vector3(0, 0, 0));

navigation_server->free(agent);
navigation_server->free(map);
}

// This test case does not check precise values on purpose - to not be too sensitivte.
TEST_CASE("[NavigationServer3D] Server should make agents avoid each other when avoidance enabled") {
NavigationServer3D *navigation_server = NavigationServer3D::get_singleton();

RID map = navigation_server->map_create();
RID agent_1 = navigation_server->agent_create();
RID agent_2 = navigation_server->agent_create();

navigation_server->map_set_active(map, true);

navigation_server->agent_set_map(agent_1, map);
navigation_server->agent_set_avoidance_enabled(agent_1, true);
navigation_server->agent_set_position(agent_1, Vector3(0, 0, 0));
navigation_server->agent_set_radius(agent_1, 1);
navigation_server->agent_set_velocity(agent_1, Vector3(1, 0, 0));
CallableMock agent_1_avoidance_callback_mock;
navigation_server->agent_set_avoidance_callback(agent_1, callable_mp(&agent_1_avoidance_callback_mock, &CallableMock::function1));

navigation_server->agent_set_map(agent_2, map);
navigation_server->agent_set_avoidance_enabled(agent_2, true);
navigation_server->agent_set_position(agent_2, Vector3(2.5, 0, 0.5));
navigation_server->agent_set_radius(agent_2, 1);
navigation_server->agent_set_velocity(agent_2, Vector3(-1, 0, 0));
CallableMock agent_2_avoidance_callback_mock;
navigation_server->agent_set_avoidance_callback(agent_2, callable_mp(&agent_2_avoidance_callback_mock, &CallableMock::function1));

CHECK_EQ(agent_1_avoidance_callback_mock.function1_calls, 0);
CHECK_EQ(agent_2_avoidance_callback_mock.function1_calls, 0);
navigation_server->process(0.0); // Give server some cycles to commit.
CHECK_EQ(agent_1_avoidance_callback_mock.function1_calls, 1);
CHECK_EQ(agent_2_avoidance_callback_mock.function1_calls, 1);
Vector3 agent_1_safe_velocity = agent_1_avoidance_callback_mock.function1_latest_arg0;
Vector3 agent_2_safe_velocity = agent_2_avoidance_callback_mock.function1_latest_arg0;
CHECK_MESSAGE(agent_1_safe_velocity.x > 0, "agent 1 should move a bit along desired velocity (+X)");
CHECK_MESSAGE(agent_2_safe_velocity.x < 0, "agent 2 should move a bit along desired velocity (-X)");
CHECK_MESSAGE(agent_1_safe_velocity.z < 0, "agent 1 should move a bit to the side so that it avoids agent 2");
CHECK_MESSAGE(agent_2_safe_velocity.z > 0, "agent 2 should move a bit to the side so that it avoids agent 1");

navigation_server->free(agent_2);
navigation_server->free(agent_1);
navigation_server->free(map);
}

// This test case uses only public APIs on purpose - other test cases use simplified baking.
// FIXME: Remove once deprecated `region_bake_navigation_mesh()` is removed.
TEST_CASE("[NavigationServer3D][SceneTree][DEPRECATED] Server should be able to bake map correctly") {
NavigationServer3D *navigation_server = NavigationServer3D::get_singleton();

// Prepare scene tree with simple mesh to serve as an input geometry.
Node3D *node_3d = memnew(Node3D);
SceneTree::get_singleton()->get_root()->add_child(node_3d);
Ref<PlaneMesh> plane_mesh = memnew(PlaneMesh);
plane_mesh->set_size(Size2(10.0, 10.0));
MeshInstance3D *mesh_instance = memnew(MeshInstance3D);
mesh_instance->set_mesh(plane_mesh);
node_3d->add_child(mesh_instance);

// Prepare anything necessary to bake navigation mesh.
RID map = navigation_server->map_create();
RID region = navigation_server->region_create();
Ref<NavigationMesh> navigation_mesh = memnew(NavigationMesh);
navigation_server->map_set_active(map, true);
navigation_server->region_set_map(region, map);
navigation_server->region_set_navigation_mesh(region, navigation_mesh);
navigation_server->process(0.0); // Give server some cycles to commit.

CHECK_EQ(navigation_mesh->get_polygon_count(), 0);
CHECK_EQ(navigation_mesh->get_vertices().size(), 0);

navigation_server->region_bake_navigation_mesh(navigation_mesh, node_3d);
Scony marked this conversation as resolved.
Show resolved Hide resolved
// FIXME: The above line should trigger the update (line below) under the hood.
navigation_server->region_set_navigation_mesh(region, navigation_mesh); // Force update.
CHECK_EQ(navigation_mesh->get_polygon_count(), 2);
CHECK_EQ(navigation_mesh->get_vertices().size(), 4);

SUBCASE("Map should emit signal and take newly baked navigation mesh into account") {
SIGNAL_WATCH(navigation_server, "map_changed");
SIGNAL_CHECK_FALSE("map_changed");
navigation_server->process(0.0); // Give server some cycles to commit.
SIGNAL_CHECK("map_changed", build_array(build_array(map)));
SIGNAL_UNWATCH(navigation_server, "map_changed");
CHECK_NE(navigation_server->map_get_closest_point(map, Vector3(0, 0, 0)), Vector3(0, 0, 0));
}

navigation_server->free(region);
navigation_server->free(map);
navigation_server->process(0.0); // Give server some cycles to commit.
memdelete(mesh_instance);
memdelete(node_3d);
}

// This test case uses only public APIs on purpose - other test cases use simplified baking.
TEST_CASE("[NavigationServer3D][SceneTree] Server should be able to bake map correctly") {
NavigationServer3D *navigation_server = NavigationServer3D::get_singleton();

// Prepare scene tree with simple mesh to serve as an input geometry.
Node3D *node_3d = memnew(Node3D);
SceneTree::get_singleton()->get_root()->add_child(node_3d);
Ref<PlaneMesh> plane_mesh = memnew(PlaneMesh);
plane_mesh->set_size(Size2(10.0, 10.0));
MeshInstance3D *mesh_instance = memnew(MeshInstance3D);
mesh_instance->set_mesh(plane_mesh);
node_3d->add_child(mesh_instance);

// Prepare anything necessary to bake navigation mesh.
RID map = navigation_server->map_create();
RID region = navigation_server->region_create();
Ref<NavigationMesh> navigation_mesh = memnew(NavigationMesh);
navigation_server->map_set_active(map, true);
navigation_server->region_set_map(region, map);
navigation_server->region_set_navigation_mesh(region, navigation_mesh);
navigation_server->process(0.0); // Give server some cycles to commit.

CHECK_EQ(navigation_mesh->get_polygon_count(), 0);
CHECK_EQ(navigation_mesh->get_vertices().size(), 0);

Ref<NavigationMeshSourceGeometryData3D> source_geometry = memnew(NavigationMeshSourceGeometryData3D);
navigation_server->parse_source_geometry_data(navigation_mesh, source_geometry, node_3d);
navigation_server->bake_from_source_geometry_data(navigation_mesh, source_geometry, Callable());
// FIXME: The above line should trigger the update (line below) under the hood.
navigation_server->region_set_navigation_mesh(region, navigation_mesh); // Force update.
CHECK_EQ(navigation_mesh->get_polygon_count(), 2);
CHECK_EQ(navigation_mesh->get_vertices().size(), 4);

SUBCASE("Map should emit signal and take newly baked navigation mesh into account") {
SIGNAL_WATCH(navigation_server, "map_changed");
SIGNAL_CHECK_FALSE("map_changed");
navigation_server->process(0.0); // Give server some cycles to commit.
SIGNAL_CHECK("map_changed", build_array(build_array(map)));
SIGNAL_UNWATCH(navigation_server, "map_changed");
CHECK_NE(navigation_server->map_get_closest_point(map, Vector3(0, 0, 0)), Vector3(0, 0, 0));
}

navigation_server->free(region);
navigation_server->free(map);
navigation_server->process(0.0); // Give server some cycles to commit.
memdelete(mesh_instance);
memdelete(node_3d);
}

// This test case does not check precise values on purpose - to not be too sensitivte.
TEST_CASE("[NavigationServer3D] Server should respond to queries against valid map properly") {
NavigationServer3D *navigation_server = NavigationServer3D::get_singleton();
Ref<NavigationMesh> navigation_mesh = memnew(NavigationMesh);
Ref<NavigationMeshSourceGeometryData3D> source_geometry = memnew(NavigationMeshSourceGeometryData3D);

Array arr;
arr.resize(RS::ARRAY_MAX);
BoxMesh::create_mesh_array(arr, Vector3(10.0, 0.001, 10.0));
source_geometry->add_mesh_array(arr, Transform3D());
navigation_server->bake_from_source_geometry_data(navigation_mesh, source_geometry, Callable());
CHECK_NE(navigation_mesh->get_polygon_count(), 0);
CHECK_NE(navigation_mesh->get_vertices().size(), 0);

RID map = navigation_server->map_create();
RID region = navigation_server->region_create();
navigation_server->map_set_active(map, true);
navigation_server->region_set_map(region, map);
navigation_server->region_set_navigation_mesh(region, navigation_mesh);
navigation_server->process(0.0); // Give server some cycles to commit.

SUBCASE("Simple queries should return non-default values") {
CHECK_NE(navigation_server->map_get_closest_point(map, Vector3(0, 0, 0)), Vector3(0, 0, 0));
CHECK_NE(navigation_server->map_get_closest_point_normal(map, Vector3(0, 0, 0)), Vector3());
CHECK(navigation_server->map_get_closest_point_owner(map, Vector3(0, 0, 0)).is_valid());
// TODO: Test map_get_closest_point_to_segment() with p_use_collision=true as well.
CHECK_NE(navigation_server->map_get_closest_point_to_segment(map, Vector3(0, 0, 0), Vector3(1, 1, 1), false), Vector3());
CHECK_NE(navigation_server->map_get_path(map, Vector3(0, 0, 0), Vector3(10, 0, 10), true).size(), 0);
CHECK_NE(navigation_server->map_get_path(map, Vector3(0, 0, 0), Vector3(10, 0, 10), false).size(), 0);
}

SUBCASE("Elaborate query with 'CORRIDORFUNNEL' post-processing should yield non-empty result") {
Ref<NavigationPathQueryParameters3D> query_parameters = memnew(NavigationPathQueryParameters3D);
query_parameters->set_map(map);
query_parameters->set_start_position(Vector3(0, 0, 0));
query_parameters->set_target_position(Vector3(10, 0, 10));
query_parameters->set_path_postprocessing(NavigationPathQueryParameters3D::PATH_POSTPROCESSING_CORRIDORFUNNEL);
Ref<NavigationPathQueryResult3D> query_result = memnew(NavigationPathQueryResult3D);
navigation_server->query_path(query_parameters, query_result);
CHECK_NE(query_result->get_path().size(), 0);
CHECK_NE(query_result->get_path_types().size(), 0);
CHECK_NE(query_result->get_path_rids().size(), 0);
CHECK_NE(query_result->get_path_owner_ids().size(), 0);
}

SUBCASE("Elaborate query with 'EDGECENTERED' post-processing should yield non-empty result") {
Ref<NavigationPathQueryParameters3D> query_parameters = memnew(NavigationPathQueryParameters3D);
query_parameters->set_map(map);
query_parameters->set_start_position(Vector3(10, 0, 10));
query_parameters->set_target_position(Vector3(0, 0, 0));
query_parameters->set_path_postprocessing(NavigationPathQueryParameters3D::PATH_POSTPROCESSING_EDGECENTERED);
Ref<NavigationPathQueryResult3D> query_result = memnew(NavigationPathQueryResult3D);
navigation_server->query_path(query_parameters, query_result);
CHECK_NE(query_result->get_path().size(), 0);
CHECK_NE(query_result->get_path_types().size(), 0);
CHECK_NE(query_result->get_path_rids().size(), 0);
CHECK_NE(query_result->get_path_owner_ids().size(), 0);
}

SUBCASE("Elaborate query with non-matching navigation layer mask should yield empty result") {
Ref<NavigationPathQueryParameters3D> query_parameters = memnew(NavigationPathQueryParameters3D);
query_parameters->set_map(map);
query_parameters->set_start_position(Vector3(10, 0, 10));
query_parameters->set_target_position(Vector3(0, 0, 0));
query_parameters->set_navigation_layers(2);
Ref<NavigationPathQueryResult3D> query_result = memnew(NavigationPathQueryResult3D);
navigation_server->query_path(query_parameters, query_result);
CHECK_EQ(query_result->get_path().size(), 0);
CHECK_EQ(query_result->get_path_types().size(), 0);
CHECK_EQ(query_result->get_path_rids().size(), 0);
CHECK_EQ(query_result->get_path_owner_ids().size(), 0);
}

SUBCASE("Elaborate query without metadata flags should yield path only") {
Ref<NavigationPathQueryParameters3D> query_parameters = memnew(NavigationPathQueryParameters3D);
query_parameters->set_map(map);
query_parameters->set_start_position(Vector3(10, 0, 10));
query_parameters->set_target_position(Vector3(0, 0, 0));
query_parameters->set_metadata_flags(0);
Ref<NavigationPathQueryResult3D> query_result = memnew(NavigationPathQueryResult3D);
navigation_server->query_path(query_parameters, query_result);
CHECK_NE(query_result->get_path().size(), 0);
CHECK_EQ(query_result->get_path_types().size(), 0);
CHECK_EQ(query_result->get_path_rids().size(), 0);
CHECK_EQ(query_result->get_path_owner_ids().size(), 0);
}

navigation_server->free(region);
navigation_server->free(map);
navigation_server->process(0.0); // Give server some cycles to commit.
}
}
} //namespace TestNavigationServer3D

Expand Down