diff --git a/rwengine/CMakeLists.txt b/rwengine/CMakeLists.txt index 73fda834..87a5c99e 100644 --- a/rwengine/CMakeLists.txt +++ b/rwengine/CMakeLists.txt @@ -49,6 +49,8 @@ set(RWENGINE_SOURCES src/dynamics/CollisionInstance.cpp src/dynamics/CollisionInstance.hpp + src/dynamics/HitTest.cpp + src/dynamics/HitTest.hpp src/dynamics/RaycastCallbacks.hpp src/engine/Animator.cpp diff --git a/rwengine/src/dynamics/HitTest.cpp b/rwengine/src/dynamics/HitTest.cpp new file mode 100644 index 00000000..5934d6f0 --- /dev/null +++ b/rwengine/src/dynamics/HitTest.cpp @@ -0,0 +1,64 @@ +#include "HitTest.hpp" +#include + +#ifdef _MSC_VER +#pragma warning(disable : 4305 5033) +#endif +#include +#include + +#ifdef _MSC_VER +#pragma warning(default : 4305 5033) +#endif + +namespace { + +HitTest::TestResult HitTestWorld(btDiscreteDynamicsWorld& world, btPairCachingGhostObject& tester) +{ + world.addCollisionObject(&tester); + + HitTest::TestResult result; + result.reserve(static_cast(tester.getNumOverlappingObjects())); + + for (auto i = 0; i < tester.getNumOverlappingObjects(); ++i) + { + auto overlapping = tester.getOverlappingObject(i); + + HitTest::Hit hit{}; + hit.body = overlapping; + + if (auto object = static_cast(overlapping->getUserPointer())) + { + hit.object = object; + } + + result.push_back(hit); + } + + world.removeCollisionObject(&tester); + + return result; +} + +HitTest::TestResult HitTestWithShape(btDiscreteDynamicsWorld& world, btCollisionShape& shape, + const glm::vec3& center, const glm::quat& rotation) { + btPairCachingGhostObject ghost{}; + btTransform xform{}; + xform.setOrigin({center.x, center.y, center.z}); + xform.setRotation({rotation.x, rotation.y, rotation.z, rotation.w}); + ghost.setWorldTransform(xform); + ghost.setCollisionShape(&shape); + return HitTestWorld(world, ghost); +} + +} // namespace + +HitTest::TestResult HitTest::sphereTest(const glm::vec3& center, float radius) { + btSphereShape sphere {radius}; + return HitTestWithShape(_world, sphere, center, {}); +} + +HitTest::TestResult HitTest::boxTest(const glm::vec3 ¢er, const glm::vec3 &size, const glm::quat& rotation) { + btBoxShape box {{size.x, size.y, size.z}}; + return HitTestWithShape(_world, box, center, rotation); +} diff --git a/rwengine/src/dynamics/HitTest.hpp b/rwengine/src/dynamics/HitTest.hpp new file mode 100644 index 00000000..4f60693f --- /dev/null +++ b/rwengine/src/dynamics/HitTest.hpp @@ -0,0 +1,39 @@ +#ifndef _RWENGINE_HITTEST_HPP_ +#define _RWENGINE_HITTEST_HPP_ + +#include +#include + +#include +#include + +class btDiscreteDynamicsWorld; +class btCollisionObject; +class GameObject; + +/** + * Utility for performing collision tests against the world. + */ +class HitTest { +public: + struct Hit { + btCollisionObject* body; + GameObject* object; + }; + using TestResult = std::vector; + + explicit HitTest(btDiscreteDynamicsWorld& world) + : _world(world) + {} + + ~HitTest() = default; + + TestResult sphereTest(const glm::vec3& center, float radius); + TestResult boxTest(const glm::vec3& center, const glm::vec3& size, const glm::quat& rotation = {1.f, 0.f, 0.f, 0.f}); + +private: + btDiscreteDynamicsWorld& _world; +}; + + +#endif diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 496257c6..3db10b9c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -11,6 +11,7 @@ set(TESTS GameData GameWorld Garage + HitTest Input Items Lifetime diff --git a/tests/test_HitTest.cpp b/tests/test_HitTest.cpp new file mode 100644 index 00000000..7d227ce0 --- /dev/null +++ b/tests/test_HitTest.cpp @@ -0,0 +1,91 @@ +#include +#include "test_Globals.hpp" +#include +#include +#include +#ifdef _MSC_VER +#pragma warning(disable : 4305 5033) +#endif +#include +#include +#ifdef _MSC_VER +#pragma warning(default : 4305 5033) +#endif + +namespace { + +struct HitTestFixture { + btDefaultCollisionConfiguration collisionConfig; + btCollisionDispatcher collisionDispatcher; + btDbvtBroadphase broadphase; + btGhostPairCallback overlappingPairCallback; + btSequentialImpulseConstraintSolver solver; + btDiscreteDynamicsWorld dynamicsWorld; + HitTest hitTest; + + HitTestFixture() + : collisionDispatcher{&collisionConfig} + , dynamicsWorld{&collisionDispatcher, &broadphase, &solver, &collisionConfig} + , hitTest{dynamicsWorld} + { + broadphase.getOverlappingPairCache()->setInternalGhostPairCallback( + &overlappingPairCallback); + } +}; + +struct WithSphere : public HitTestFixture { + btSphereShape shape {0.5f}; + std::unique_ptr target; + GameObject* object {reinterpret_cast(0xDEADBEEF)}; + + WithSphere() + { + btDefaultMotionState ms; + btRigidBody::btRigidBodyConstructionInfo info {1.f, &ms, &shape}; + target = std::make_unique(info); + target->setUserPointer(object); + dynamicsWorld.addRigidBody(target.get()); + } + + ~WithSphere() { + dynamicsWorld.removeRigidBody(target.get()); + } +}; +} + +BOOST_AUTO_TEST_SUITE(HitTestTests) + +BOOST_FIXTURE_TEST_CASE(test_creation, HitTestFixture) { + HitTest test {dynamicsWorld}; +} + +BOOST_FIXTURE_TEST_CASE(sphereTest_returns_result, WithSphere) { + const auto result = hitTest.sphereTest({0.f, 0.f, 0.f}, 1.f); + BOOST_CHECK_EQUAL(result.size(), 1); +} + +BOOST_FIXTURE_TEST_CASE(boxTest_returns_result, WithSphere) { + auto sphereBoundingBoxEdge = glm::vec3{ 0.f, 0.f, shape.getRadius() }; + const auto result = hitTest.boxTest(sphereBoundingBoxEdge, {0.01f, 0.01f, 0.01f}); + BOOST_CHECK_EQUAL(result.size(), 1); +} + +BOOST_FIXTURE_TEST_CASE(non_overlapping_test_returns_nothing, WithSphere) { + auto sphereBoundingBoxEdge = glm::vec3{ shape.getRadius() }; + const auto result = hitTest.boxTest(sphereBoundingBoxEdge * 2.f, {0.01f, 0.01f, 0.01f}); + BOOST_CHECK(result.empty()); +} + +BOOST_FIXTURE_TEST_CASE(result_contains_body, WithSphere) { + const auto result = hitTest.sphereTest({0.f, 0.f, 0.f}, 1.f); + BOOST_ASSERT(result.size() == 1); + BOOST_CHECK_EQUAL(result[0].body, target.get()); +} + +BOOST_FIXTURE_TEST_CASE(test_result_contains_object, WithSphere) { + const auto result = hitTest.sphereTest({0.f, 0.f, 0.f}, 1.f); + BOOST_ASSERT(result.size() == 1); + BOOST_CHECK_EQUAL(result[0].object, object); +} + +BOOST_AUTO_TEST_SUITE_END()