From 97609fcb5e655d65410091541ce450712bc638ac Mon Sep 17 00:00:00 2001 From: Daniel Evans Date: Sun, 18 Nov 2018 12:58:12 +0000 Subject: [PATCH 1/3] HitTest class for use in area scanning --- rwengine/CMakeLists.txt | 2 + rwengine/src/dynamics/HitTest.cpp | 64 ++++++++++++++++++++++ rwengine/src/dynamics/HitTest.hpp | 39 +++++++++++++ tests/CMakeLists.txt | 1 + tests/test_HitTest.cpp | 91 +++++++++++++++++++++++++++++++ 5 files changed, 197 insertions(+) create mode 100644 rwengine/src/dynamics/HitTest.cpp create mode 100644 rwengine/src/dynamics/HitTest.hpp create mode 100644 tests/test_HitTest.cpp 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() From 00240c41251ba35c744a2822e03f540bdf0663b1 Mon Sep 17 00:00:00 2001 From: Daniel Evans Date: Sat, 24 Nov 2018 02:24:39 +0000 Subject: [PATCH 2/3] Logic and Debug Vis for vehicle path checking --- rwengine/src/objects/VehicleObject.cpp | 12 +++++++ rwengine/src/objects/VehicleObject.hpp | 5 +++ rwengine/src/render/DebugDraw.hpp | 6 ++++ rwgame/RWGame.cpp | 49 +++++++++++++++++++++----- 4 files changed, 64 insertions(+), 8 deletions(-) diff --git a/rwengine/src/objects/VehicleObject.cpp b/rwengine/src/objects/VehicleObject.cpp index 07b117dd..502eb32e 100644 --- a/rwengine/src/objects/VehicleObject.cpp +++ b/rwengine/src/objects/VehicleObject.cpp @@ -1083,3 +1083,15 @@ float VehicleObject::isOnSide(const glm::vec3& point) { return distance; } +std::tuple VehicleObject::obstacleCheckVolume() const { + const auto& dim = info->handling.dimensions; + const auto kMaxDistance = 20.f; + const auto velocity = getVelocity() / info->handling.maxVelocity; + const auto lookDistance = glm::clamp(kMaxDistance * velocity, 0.f, kMaxDistance); + const glm::vec3 areaSize{dim.x * 0.6f, 1.0f + lookDistance, 1.0f}; + return { + {0.f, dim.y * 0.5f + areaSize.y, 0.f}, + areaSize, + }; +} + diff --git a/rwengine/src/objects/VehicleObject.hpp b/rwengine/src/objects/VehicleObject.hpp index dc1e29e9..c36cdb1a 100644 --- a/rwengine/src/objects/VehicleObject.hpp +++ b/rwengine/src/objects/VehicleObject.hpp @@ -246,6 +246,11 @@ public: void grantOccupantRewards(CharacterObject* character); + /** + * @return The position, and size of the area that must be free for the vehicle to continue. + */ + std::tuple obstacleCheckVolume() const; + private: void setupModel(); void registerPart(ModelFrame* mf); diff --git a/rwengine/src/render/DebugDraw.hpp b/rwengine/src/render/DebugDraw.hpp index 5ad84b32..7631b3e6 100644 --- a/rwengine/src/render/DebugDraw.hpp +++ b/rwengine/src/render/DebugDraw.hpp @@ -30,6 +30,12 @@ public: void drawLine(const btVector3 &from, const btVector3 &to, const btVector3 &color) override; + void drawLine(const glm::vec3 &from, const glm::vec3 &to, + const glm::vec3 &color) { + drawLine(btVector3{from.x, from.y, from.z}, + btVector3{to.x, to.y, to.z}, + btVector3{color.r, color.g, color.b}); + } void drawContactPoint(const btVector3 &pointOnB, const btVector3 &normalOnB, btScalar distance, int lifeTime, const btVector3 &color) override; diff --git a/rwgame/RWGame.cpp b/rwgame/RWGame.cpp index 21e09d9a..ba913145 100644 --- a/rwgame/RWGame.cpp +++ b/rwgame/RWGame.cpp @@ -24,6 +24,7 @@ #include #include #include +#include namespace { static constexpr std::array< @@ -775,17 +776,49 @@ void RWGame::renderDebugPaths(float time) { for (auto& p : world->pedestrianPool.objects) { auto v = static_cast(p.second.get()); - static const btVector3 color(1.f, 1.f, 0.f); + static const glm::vec3 color(1.f, 1.f, 0.f); - if (v->controller->targetNode && v->getCurrentVehicle()) { - const glm::vec3 pos1 = v->getPosition(); - const glm::vec3 pos2 = v->controller->targetNode->position; + if (auto vehicle = v->getCurrentVehicle(); vehicle) + { + if (v->controller->targetNode) { + debug.drawLine(v->getPosition(), v->controller->targetNode->position, color); + } - btVector3 position1(pos1.x, pos1.y, pos1.z); - btVector3 position2(pos2.x, pos2.y, pos2.z); + auto [center, halfSize] = vehicle->obstacleCheckVolume(); + std::array corners { { + glm::vec3{- halfSize.x, - halfSize.y, - halfSize.z}, + glm::vec3{+ halfSize.x, - halfSize.y, - halfSize.z}, + glm::vec3{- halfSize.x, - halfSize.y, + halfSize.z}, + glm::vec3{+ halfSize.x, - halfSize.y, + halfSize.z}, + glm::vec3{- halfSize.x, + halfSize.y, - halfSize.z}, + glm::vec3{+ halfSize.x, + halfSize.y, - halfSize.z}, + glm::vec3{- halfSize.x, + halfSize.y, + halfSize.z}, + glm::vec3{+ halfSize.x, + halfSize.y, + halfSize.z}, + } + }; + const auto iRotation = (vehicle->getRotation()); + const auto rCenter = iRotation * center; + std::transform(corners.begin(), corners.end(), corners.begin(), + [&](const auto& p) -> glm::vec3 { + return vehicle->getPosition() + rCenter + iRotation * p; + }); - debug.drawLine(position1, position2, color); - } + static const glm::vec3 color2(1.f, 0.f, 0.f); + debug.drawLine(corners[0], corners[1], color2); + debug.drawLine(corners[0], corners[2], color2); + debug.drawLine(corners[3], corners[1], color2); + debug.drawLine(corners[3], corners[2], color2); + + debug.drawLine(corners[0], corners[4], color2); + debug.drawLine(corners[1], corners[5], color2); + debug.drawLine(corners[2], corners[6], color2); + debug.drawLine(corners[3], corners[7], color2); + + debug.drawLine(corners[4], corners[5], color2); + debug.drawLine(corners[4], corners[6], color2); + debug.drawLine(corners[7], corners[5], color2); + debug.drawLine(corners[7], corners[6], color2); + } } debug.flush(renderer); From 0bf99fade6b677aa752b3b4fcaf7e7d9b2e62240 Mon Sep 17 00:00:00 2001 From: Daniel Evans Date: Sun, 25 Nov 2018 23:18:43 +0000 Subject: [PATCH 3/3] Use hittests for AI driver behaviour --- rwengine/src/ai/CharacterController.cpp | 99 +++++-------------------- 1 file changed, 19 insertions(+), 80 deletions(-) diff --git a/rwengine/src/ai/CharacterController.cpp b/rwengine/src/ai/CharacterController.cpp index bdeab1ec..cd1cf7c2 100644 --- a/rwengine/src/ai/CharacterController.cpp +++ b/rwengine/src/ai/CharacterController.cpp @@ -19,6 +19,7 @@ #include #include +#include #include "data/WeaponData.hpp" #include "engine/Animator.hpp" @@ -193,91 +194,25 @@ void CharacterController::steerTo(const glm::vec3 &target) { vehicle->setSteeringAngle(steeringAngle, true); } -// @todo replace this by raytest/raycast logic -bool CharacterController::checkForObstacles() -{ +bool CharacterController::checkForObstacles() { // We can't drive without a vehicle - VehicleObject* vehicle = character->getCurrentVehicle(); + VehicleObject *vehicle = character->getCurrentVehicle(); if (vehicle == nullptr) { return false; } - // The minimal distance we test for objects - static constexpr float minColDist = 20.f; + HitTest test{*vehicle->engine->dynamicsWorld}; - // Try to stop before pedestrians - for (const auto &obj : character->engine->pedestrianPool.objects) { - // Verify that the character isn't the driver and is walking - if (obj.second.get() != character && - static_cast(obj.second.get())->getCurrentVehicle() == - nullptr) { - // Only check characters that are near our vehicle - if (glm::distance(vehicle->getPosition(), - obj.second->getPosition()) <= minColDist) { - // Check if the character is in front of us and in our way - if (vehicle->isInFront(obj.second->getPosition()) > -3.f && - vehicle->isInFront(obj.second->getPosition()) < 10.f && - glm::abs(vehicle->isOnSide(obj.second->getPosition())) < - 3.f) { - return true; - } - } - } - } + auto[center, halfSize] = vehicle->obstacleCheckVolume(); + const auto rotation = vehicle->getRotation(); + center = vehicle->getPosition() + rotation * center; + auto results = test.boxTest(center, halfSize, vehicle->getRotation()); - // Brake when a car is in front of us and change lanes when possible - for (const auto &obj : character->engine->vehiclePool.objects) { - // Verify that the vehicle isn't our vehicle - if (obj.second.get() != vehicle) { - // Only check vehicles that are near our vehicle - if (glm::distance(vehicle->getPosition(), - obj.second->getPosition()) <= minColDist) { - // Check if the vehicle is in front of us and in our way - if (vehicle->isInFront(obj.second->getPosition()) > 0.f && - vehicle->isInFront(obj.second->getPosition()) < 10.f && - glm::abs(vehicle->isOnSide(obj.second->getPosition())) < - 2.5f) { - // Check if the road has more than one lane - // @todo we don't know the direction of the road, so for - // now, choose the bigger value - int maxLanes = - targetNode->rightLanes > targetNode->leftLanes - ? targetNode->rightLanes - : targetNode->leftLanes; - if (maxLanes > 1) { - // Change the lane, firstly check if there is an - // occupant - if (static_cast(obj.second.get()) - ->getDriver() != nullptr) { - // @todo for now we don't know the lane where the - // player is currently driving so just slow down, in - // the future calculate the lane - if (static_cast(obj.second.get()) - ->getDriver() - ->isPlayer()) { - return true; - } else { - int avoidLane = - static_cast(obj.second.get()) - ->getDriver() - ->controller->getLane(); - - // @todo for now just two lanes - if (avoidLane == 1) - character->controller->setLane(2); - else - character->controller->setLane(1); - } - } - } else { - return true; - } - } - } - } - } - - return false; + return any_of(results.begin(), results.end(), + [&](const auto &hit) { + return !(hit.object == vehicle || + hit.body->isStaticObject()); + }); } bool Activities::DriveTo::update(CharacterObject *character, @@ -384,12 +319,16 @@ bool Activities::DriveTo::update(CharacterObject *character, } // Check whether a pedestrian or vehicle is in our way - if (controller->checkForObstacles() == true) { + if (controller->checkForObstacles()) { currentSpeed = 0.f; } + if (std::fabs(currentSpeed) < 0.1f) { + vehicle->setHandbraking(true); + vehicle->setThrottle(0.f); + } // Is the vehicle slower than it should be - if (vehicle->getVelocity() < currentSpeed) { + else if (vehicle->getVelocity() < currentSpeed) { vehicle->setHandbraking(false); // The vehicle is driving backwards, accelerate