Merge branch 'aiwander' into 'master'

Build checkpoints based path for wandering actors (#8433)

Closes #8433

See merge request OpenMW/openmw!4652
This commit is contained in:
Alexei Kotov 2025-07-28 00:48:17 +03:00
commit 0a8373987d
26 changed files with 645 additions and 429 deletions

View File

@ -139,7 +139,7 @@ namespace
TEST_F(DetourNavigatorNavigatorTest, find_path_for_empty_should_return_empty) TEST_F(DetourNavigatorNavigatorTest, find_path_for_empty_should_return_empty)
{ {
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut),
Status::NavMeshNotFound); Status::NavMeshNotFound);
EXPECT_EQ(mPath, std::deque<osg::Vec3f>()); EXPECT_EQ(mPath, std::deque<osg::Vec3f>());
} }
@ -147,7 +147,7 @@ namespace
TEST_F(DetourNavigatorNavigatorTest, find_path_for_existing_agent_with_no_navmesh_should_throw_exception) TEST_F(DetourNavigatorNavigatorTest, find_path_for_existing_agent_with_no_navmesh_should_throw_exception)
{ {
ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); ASSERT_TRUE(mNavigator->addAgent(mAgentBounds));
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut),
Status::StartPolygonNotFound); Status::StartPolygonNotFound);
} }
@ -156,7 +156,7 @@ namespace
ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); ASSERT_TRUE(mNavigator->addAgent(mAgentBounds));
ASSERT_TRUE(mNavigator->addAgent(mAgentBounds)); ASSERT_TRUE(mNavigator->addAgent(mAgentBounds));
mNavigator->removeAgent(mAgentBounds); mNavigator->removeAgent(mAgentBounds);
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut),
Status::StartPolygonNotFound); Status::StartPolygonNotFound);
} }
@ -172,7 +172,7 @@ namespace
updateGuard.reset(); updateGuard.reset();
mNavigator->wait(WaitConditionType::requiredTilesPresent, &mListener); mNavigator->wait(WaitConditionType::requiredTilesPresent, &mListener);
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut),
Status::Success); Status::Success);
EXPECT_THAT(mPath, EXPECT_THAT(mPath,
@ -194,7 +194,7 @@ namespace
updateGuard.reset(); updateGuard.reset();
mNavigator->wait(WaitConditionType::requiredTilesPresent, &mListener); mNavigator->wait(WaitConditionType::requiredTilesPresent, &mListener);
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mStart, Flag_walk, mAreaCosts, mEndTolerance, mOut), EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mStart, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut),
Status::Success); Status::Success);
EXPECT_THAT(mPath, ElementsAre(Vec3fEq(56.66666412353515625, 460, 1.99998295307159423828125))) << mPath; EXPECT_THAT(mPath, ElementsAre(Vec3fEq(56.66666412353515625, 460, 1.99998295307159423828125))) << mPath;
@ -218,7 +218,7 @@ namespace
mNavigator->update(mPlayerPosition, nullptr); mNavigator->update(mPlayerPosition, nullptr);
mNavigator->wait(WaitConditionType::allJobsDone, &mListener); mNavigator->wait(WaitConditionType::allJobsDone, &mListener);
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut),
Status::Success); Status::Success);
EXPECT_THAT(mPath, EXPECT_THAT(mPath,
@ -237,7 +237,7 @@ namespace
mPath.clear(); mPath.clear();
mOut = std::back_inserter(mPath); mOut = std::back_inserter(mPath);
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut),
Status::Success); Status::Success);
EXPECT_THAT(mPath, EXPECT_THAT(mPath,
@ -265,7 +265,7 @@ namespace
mNavigator->update(mPlayerPosition, nullptr); mNavigator->update(mPlayerPosition, nullptr);
mNavigator->wait(WaitConditionType::allJobsDone, &mListener); mNavigator->wait(WaitConditionType::allJobsDone, &mListener);
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut),
Status::Success); Status::Success);
EXPECT_THAT(mPath, EXPECT_THAT(mPath,
@ -285,7 +285,7 @@ namespace
mPath.clear(); mPath.clear();
mOut = std::back_inserter(mPath); mOut = std::back_inserter(mPath);
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut),
Status::Success); Status::Success);
EXPECT_THAT(mPath, EXPECT_THAT(mPath,
@ -318,7 +318,7 @@ namespace
mNavigator->update(mPlayerPosition, nullptr); mNavigator->update(mPlayerPosition, nullptr);
mNavigator->wait(WaitConditionType::allJobsDone, &mListener); mNavigator->wait(WaitConditionType::allJobsDone, &mListener);
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut),
Status::Success); Status::Success);
EXPECT_THAT(mPath, EXPECT_THAT(mPath,
@ -386,7 +386,7 @@ namespace
mNavigator->update(mPlayerPosition, nullptr); mNavigator->update(mPlayerPosition, nullptr);
mNavigator->wait(WaitConditionType::allJobsDone, &mListener); mNavigator->wait(WaitConditionType::allJobsDone, &mListener);
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut),
Status::Success); Status::Success);
EXPECT_THAT(mPath, EXPECT_THAT(mPath,
@ -421,7 +421,7 @@ namespace
mEnd.x() = 256; mEnd.x() = 256;
mEnd.z() = 300; mEnd.z() = 300;
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_swim, mAreaCosts, mEndTolerance, mOut), EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_swim, mAreaCosts, mEndTolerance, {}, mOut),
Status::Success); Status::Success);
EXPECT_THAT(mPath, EXPECT_THAT(mPath,
@ -453,8 +453,8 @@ namespace
mStart.x() = 256; mStart.x() = 256;
mEnd.x() = 256; mEnd.x() = 256;
EXPECT_EQ( EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_swim | Flag_walk, mAreaCosts, mEndTolerance,
findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_swim | Flag_walk, mAreaCosts, mEndTolerance, mOut), {}, mOut),
Status::Success); Status::Success);
EXPECT_THAT(mPath, EXPECT_THAT(mPath,
@ -487,8 +487,8 @@ namespace
mStart.x() = 256; mStart.x() = 256;
mEnd.x() = 256; mEnd.x() = 256;
EXPECT_EQ( EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_swim | Flag_walk, mAreaCosts, mEndTolerance,
findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_swim | Flag_walk, mAreaCosts, mEndTolerance, mOut), {}, mOut),
Status::Success); Status::Success);
EXPECT_THAT(mPath, EXPECT_THAT(mPath,
@ -520,7 +520,7 @@ namespace
mStart.x() = 256; mStart.x() = 256;
mEnd.x() = 256; mEnd.x() = 256;
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut),
Status::Success); Status::Success);
EXPECT_THAT(mPath, EXPECT_THAT(mPath,
@ -549,7 +549,7 @@ namespace
mNavigator->update(mPlayerPosition, nullptr); mNavigator->update(mPlayerPosition, nullptr);
mNavigator->wait(WaitConditionType::allJobsDone, &mListener); mNavigator->wait(WaitConditionType::allJobsDone, &mListener);
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut),
Status::Success); Status::Success);
EXPECT_THAT(mPath, EXPECT_THAT(mPath,
@ -577,7 +577,7 @@ namespace
mNavigator->update(mPlayerPosition, nullptr); mNavigator->update(mPlayerPosition, nullptr);
mNavigator->wait(WaitConditionType::allJobsDone, &mListener); mNavigator->wait(WaitConditionType::allJobsDone, &mListener);
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut),
Status::Success); Status::Success);
EXPECT_THAT(mPath, EXPECT_THAT(mPath,
@ -658,7 +658,7 @@ namespace
mNavigator->update(mPlayerPosition, nullptr); mNavigator->update(mPlayerPosition, nullptr);
mNavigator->wait(WaitConditionType::allJobsDone, &mListener); mNavigator->wait(WaitConditionType::allJobsDone, &mListener);
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut),
Status::Success); Status::Success);
EXPECT_THAT(mPath, EXPECT_THAT(mPath,
@ -781,7 +781,7 @@ namespace
mNavigator->update(mPlayerPosition, nullptr); mNavigator->update(mPlayerPosition, nullptr);
mNavigator->wait(WaitConditionType::requiredTilesPresent, &mListener); mNavigator->wait(WaitConditionType::requiredTilesPresent, &mListener);
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut),
Status::Success); Status::Success);
EXPECT_THAT(mPath, EXPECT_THAT(mPath,
@ -806,7 +806,7 @@ namespace
mNavigator->update(mPlayerPosition, nullptr); mNavigator->update(mPlayerPosition, nullptr);
mNavigator->wait(WaitConditionType::allJobsDone, &mListener); mNavigator->wait(WaitConditionType::allJobsDone, &mListener);
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, mOut), EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut),
Status::PartialPath); Status::PartialPath);
EXPECT_THAT(mPath, EXPECT_THAT(mPath,
@ -834,7 +834,7 @@ namespace
const float endTolerance = 1000.0f; const float endTolerance = 1000.0f;
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, endTolerance, mOut), EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_walk, mAreaCosts, endTolerance, {}, mOut),
Status::Success); Status::Success);
EXPECT_THAT(mPath, EXPECT_THAT(mPath,
@ -979,6 +979,146 @@ namespace
EXPECT_EQ(usedNavMeshTiles, 854); EXPECT_EQ(usedNavMeshTiles, 854);
} }
TEST_F(DetourNavigatorNavigatorTest, find_path_should_return_path_around_steep_mountains)
{
const std::array<float, 5 * 5> heightfieldData{ {
0, 0, 0, 0, 0, // row 0
0, 0, 0, 0, 0, // row 1
0, 0, 1000, 0, 0, // row 2
0, 0, 0, 0, 0, // row 3
0, 0, 0, 0, 0, // row 4
} };
const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData);
const int cellSize = heightfieldTileSize * static_cast<int>(surface.mSize - 1);
ASSERT_TRUE(mNavigator->addAgent(mAgentBounds));
mNavigator->addHeightfield(mCellPosition, cellSize, surface, nullptr);
mNavigator->update(mPlayerPosition, nullptr);
mNavigator->wait(WaitConditionType::allJobsDone, &mListener);
const osg::Vec3f start(56, 56, 12);
const osg::Vec3f end(464, 464, 12);
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, start, end, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut),
Status::Success);
EXPECT_THAT(mPath,
ElementsAre( //
Vec3fEq(56.66664886474609375, 56.66664886474609375, 11.33333301544189453125),
Vec3fEq(396.666656494140625, 79.33331298828125, 11.33333301544189453125),
Vec3fEq(430.666656494140625, 113.33331298828125, 11.33333301544189453125),
Vec3fEq(463.999969482421875, 463.999969482421875, 11.33333301544189453125)))
<< mPath;
}
TEST_F(DetourNavigatorNavigatorTest, find_path_should_return_path_around_steep_cliffs)
{
const std::array<float, 5 * 5> heightfieldData{ {
0, 0, 0, 0, 0, // row 0
0, 0, 0, 0, 0, // row 1
0, 0, -1000, 0, 0, // row 2
0, 0, 0, 0, 0, // row 3
0, 0, 0, 0, 0, // row 4
} };
const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData);
const int cellSize = heightfieldTileSize * static_cast<int>(surface.mSize - 1);
ASSERT_TRUE(mNavigator->addAgent(mAgentBounds));
mNavigator->addHeightfield(mCellPosition, cellSize, surface, nullptr);
mNavigator->update(mPlayerPosition, nullptr);
mNavigator->wait(WaitConditionType::allJobsDone, &mListener);
const osg::Vec3f start(56, 56, 12);
const osg::Vec3f end(464, 464, 12);
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, start, end, Flag_walk, mAreaCosts, mEndTolerance, {}, mOut),
Status::Success);
EXPECT_THAT(mPath,
ElementsAre( //
Vec3fEq(56.66664886474609375, 56.66664886474609375, 8.66659259796142578125),
Vec3fEq(385.33331298828125, 79.33331298828125, 8.66659259796142578125),
Vec3fEq(430.666656494140625, 124.66664886474609375, 8.66659259796142578125),
Vec3fEq(463.999969482421875, 463.999969482421875, 8.66659259796142578125)))
<< mPath;
}
TEST_F(DetourNavigatorNavigatorTest, find_path_should_return_path_with_checkpoints)
{
const std::array<float, 5 * 5> heightfieldData{ {
0, 0, 0, 0, 0, // row 0
0, 0, 0, 0, 0, // row 1
0, 0, 1000, 0, 0, // row 2
0, 0, 0, 0, 0, // row 3
0, 0, 0, 0, 0, // row 4
} };
const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData);
const int cellSize = heightfieldTileSize * static_cast<int>(surface.mSize - 1);
ASSERT_TRUE(mNavigator->addAgent(mAgentBounds));
mNavigator->addHeightfield(mCellPosition, cellSize, surface, nullptr);
mNavigator->update(mPlayerPosition, nullptr);
mNavigator->wait(WaitConditionType::allJobsDone, &mListener);
const std::vector<osg::Vec3f> checkpoints = {
osg::Vec3f(400, 70, 12),
};
const osg::Vec3f start(56, 56, 12);
const osg::Vec3f end(464, 464, 12);
EXPECT_EQ(
findPath(*mNavigator, mAgentBounds, start, end, Flag_walk, mAreaCosts, mEndTolerance, checkpoints, mOut),
Status::Success);
EXPECT_THAT(mPath,
ElementsAre( //
Vec3fEq(56.66664886474609375, 56.66664886474609375, 11.33333301544189453125),
Vec3fEq(400, 70, 11.33333301544189453125),
Vec3fEq(430.666656494140625, 113.33331298828125, 11.33333301544189453125),
Vec3fEq(463.999969482421875, 463.999969482421875, 11.33333301544189453125)))
<< mPath;
}
TEST_F(DetourNavigatorNavigatorTest, find_path_should_skip_unreachable_checkpoints)
{
const std::array<float, 5 * 5> heightfieldData{ {
0, 0, 0, 0, 0, // row 0
0, 0, 0, 0, 0, // row 1
0, 0, 1000, 0, 0, // row 2
0, 0, 0, 0, 0, // row 3
0, 0, 0, 0, 0, // row 4
} };
const HeightfieldSurface surface = makeSquareHeightfieldSurface(heightfieldData);
const int cellSize = heightfieldTileSize * static_cast<int>(surface.mSize - 1);
ASSERT_TRUE(mNavigator->addAgent(mAgentBounds));
mNavigator->addHeightfield(mCellPosition, cellSize, surface, nullptr);
mNavigator->update(mPlayerPosition, nullptr);
mNavigator->wait(WaitConditionType::allJobsDone, &mListener);
const std::vector<osg::Vec3f> checkpoints = {
osg::Vec3f(400, 70, 10000),
osg::Vec3f(256, 256, 1000),
osg::Vec3f(-1000, -1000, 0),
};
const osg::Vec3f start(56, 56, 12);
const osg::Vec3f end(464, 464, 12);
EXPECT_EQ(
findPath(*mNavigator, mAgentBounds, start, end, Flag_walk, mAreaCosts, mEndTolerance, checkpoints, mOut),
Status::Success);
EXPECT_THAT(mPath,
ElementsAre( //
Vec3fEq(56.66664886474609375, 56.66664886474609375, 11.33333301544189453125),
Vec3fEq(396.666656494140625, 79.33331298828125, 11.33333301544189453125),
Vec3fEq(430.666656494140625, 113.33331298828125, 11.33333301544189453125),
Vec3fEq(463.999969482421875, 463.999969482421875, 11.33333301544189453125)))
<< mPath;
}
struct DetourNavigatorNavigatorNotSupportedAgentBoundsTest : TestWithParam<AgentBounds> struct DetourNavigatorNavigatorNotSupportedAgentBoundsTest : TestWithParam<AgentBounds>
{ {
}; };

View File

@ -589,8 +589,7 @@ namespace MWBase
virtual bool hasCollisionWithDoor( virtual bool hasCollisionWithDoor(
const MWWorld::ConstPtr& door, const osg::Vec3f& position, const osg::Vec3f& destination) const = 0; const MWWorld::ConstPtr& door, const osg::Vec3f& position, const osg::Vec3f& destination) const = 0;
virtual bool isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, virtual bool isAreaOccupiedByOtherActor(const MWWorld::ConstPtr& actor, const osg::Vec3f& position) const = 0;
std::span<const MWWorld::ConstPtr> ignore, std::vector<MWWorld::Ptr>* occupyingActors = nullptr) const = 0;
virtual void reportStats(unsigned int frameNumber, osg::Stats& stats) const = 0; virtual void reportStats(unsigned int frameNumber, osg::Stats& stats) const = 0;

View File

@ -233,6 +233,7 @@ namespace MWLua
DetourNavigator::Flags includeFlags = defaultIncludeFlags; DetourNavigator::Flags includeFlags = defaultIncludeFlags;
DetourNavigator::AreaCosts areaCosts{}; DetourNavigator::AreaCosts areaCosts{};
float destinationTolerance = 1; float destinationTolerance = 1;
std::vector<osg::Vec3f> checkpoints;
if (options.has_value()) if (options.has_value())
{ {
@ -258,13 +259,24 @@ namespace MWLua
} }
if (const auto& v = options->get<sol::optional<float>>("destinationTolerance")) if (const auto& v = options->get<sol::optional<float>>("destinationTolerance"))
destinationTolerance = *v; destinationTolerance = *v;
if (const auto& t = options->get<sol::optional<sol::table>>("checkpoints"))
{
for (const auto& [k, v] : *t)
{
const int index = k.as<int>();
const osg::Vec3f position = v.as<osg::Vec3f>();
if (index != static_cast<int>(checkpoints.size() + 1))
throw std::runtime_error("checkpoints is not an array");
checkpoints.push_back(position);
}
}
} }
std::vector<osg::Vec3f> path; std::vector<osg::Vec3f> path;
const DetourNavigator::Status status const DetourNavigator::Status status = DetourNavigator::findPath(
= DetourNavigator::findPath(*MWBase::Environment::get().getWorld()->getNavigator(), agentBounds, *MWBase::Environment::get().getWorld()->getNavigator(), agentBounds, source, destination,
source, destination, includeFlags, areaCosts, destinationTolerance, std::back_inserter(path)); includeFlags, areaCosts, destinationTolerance, checkpoints, std::back_inserter(path));
sol::table result(lua, sol::create); sol::table result(lua, sol::create);
LuaUtil::copyVectorToTable(path, result); LuaUtil::copyVectorToTable(path, result);

View File

@ -1,13 +1,11 @@
#include "aicombat.hpp" #include "aicombat.hpp"
#include <components/misc/coordinateconverter.hpp>
#include <components/misc/rng.hpp>
#include <components/esm3/aisequence.hpp>
#include <components/misc/mathutil.hpp>
#include <components/detournavigator/navigatorutils.hpp> #include <components/detournavigator/navigatorutils.hpp>
#include <components/esm3/aisequence.hpp>
#include <components/misc/coordinateconverter.hpp>
#include <components/misc/mathutil.hpp>
#include <components/misc/pathgridutils.hpp>
#include <components/misc/rng.hpp>
#include <components/sceneutil/positionattitudetransform.hpp> #include <components/sceneutil/positionattitudetransform.hpp>
#include "../mwphysics/raycasting.hpp" #include "../mwphysics/raycasting.hpp"
@ -303,8 +301,8 @@ namespace MWMechanics
const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags); const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags);
const ESM::Pathgrid* pathgrid = world->getStore().get<ESM::Pathgrid>().search(*actor.getCell()->getCell()); const ESM::Pathgrid* pathgrid = world->getStore().get<ESM::Pathgrid>().search(*actor.getCell()->getCell());
const auto& pathGridGraph = getPathGridGraph(pathgrid); const auto& pathGridGraph = getPathGridGraph(pathgrid);
mPathFinder.buildPath(actor, vActorPos, vTargetPos, actor.getCell(), pathGridGraph, agentBounds, mPathFinder.buildPath(actor, vActorPos, vTargetPos, pathGridGraph, agentBounds, navigatorFlags, areaCosts,
navigatorFlags, areaCosts, storage.mAttackRange, PathType::Full); storage.mAttackRange, PathType::Full);
if (!mPathFinder.isPathConstructed()) if (!mPathFinder.isPathConstructed())
{ {
@ -317,8 +315,8 @@ namespace MWMechanics
if (hit.has_value() && (*hit - vTargetPos).length() <= rangeAttack) if (hit.has_value() && (*hit - vTargetPos).length() <= rangeAttack)
{ {
// If the point is close enough, try to find a path to that point. // If the point is close enough, try to find a path to that point.
mPathFinder.buildPath(actor, vActorPos, *hit, actor.getCell(), pathGridGraph, agentBounds, mPathFinder.buildPath(actor, vActorPos, *hit, pathGridGraph, agentBounds, navigatorFlags, areaCosts,
navigatorFlags, areaCosts, storage.mAttackRange, PathType::Full); storage.mAttackRange, PathType::Full);
if (mPathFinder.isPathConstructed()) if (mPathFinder.isPathConstructed())
{ {
// If path to that point is found use it as custom destination. // If path to that point is found use it as custom destination.
@ -394,8 +392,8 @@ namespace MWMechanics
osg::Vec3f localPos = actor.getRefData().getPosition().asVec3(); osg::Vec3f localPos = actor.getRefData().getPosition().asVec3();
coords.toLocal(localPos); coords.toLocal(localPos);
size_t closestPointIndex = PathFinder::getClosestPoint(pathgrid, localPos); const std::size_t closestPointIndex = Misc::getClosestPoint(*pathgrid, localPos);
for (size_t i = 0; i < pathgrid->mPoints.size(); i++) for (std::size_t i = 0; i < pathgrid->mPoints.size(); i++)
{ {
if (i != closestPointIndex if (i != closestPointIndex
&& getPathGridGraph(pathgrid).isPointConnected(closestPointIndex, i)) && getPathGridGraph(pathgrid).isPointConnected(closestPointIndex, i))
@ -456,7 +454,8 @@ namespace MWMechanics
float dist float dist
= (actor.getRefData().getPosition().asVec3() - target.getRefData().getPosition().asVec3()).length(); = (actor.getRefData().getPosition().asVec3() - target.getRefData().getPosition().asVec3()).length();
if ((dist > fFleeDistance && !storage.mLOS) if ((dist > fFleeDistance && !storage.mLOS)
|| pathTo(actor, PathFinder::makeOsgVec3(storage.mFleeDest), duration, supportedMovementDirections)) || pathTo(
actor, Misc::Convert::makeOsgVec3f(storage.mFleeDest), duration, supportedMovementDirections))
{ {
state = AiCombatStorage::FleeState_Idle; state = AiCombatStorage::FleeState_Idle;
} }

View File

@ -2,12 +2,13 @@
#define GAME_MWMECHANICS_AICOMBAT_H #define GAME_MWMECHANICS_AICOMBAT_H
#include "aitemporarybase.hpp" #include "aitemporarybase.hpp"
#include "aitimer.hpp"
#include "movement.hpp"
#include "typedaipackage.hpp" #include "typedaipackage.hpp"
#include "../mwworld/cellstore.hpp" // for Doors #include "../mwworld/cellstore.hpp" // for Doors
#include "aitimer.hpp" #include <components/esm3/loadpgrd.hpp>
#include "movement.hpp"
namespace ESM namespace ESM
{ {

View File

@ -180,8 +180,8 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const osg::Vec3f&
= world->getStore().get<ESM::Pathgrid>().search(*actor.getCell()->getCell()); = world->getStore().get<ESM::Pathgrid>().search(*actor.getCell()->getCell());
const DetourNavigator::Flags navigatorFlags = getNavigatorFlags(actor); const DetourNavigator::Flags navigatorFlags = getNavigatorFlags(actor);
const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags); const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags);
mPathFinder.buildLimitedPath(actor, position, dest, actor.getCell(), getPathGridGraph(pathgrid), mPathFinder.buildLimitedPath(actor, position, dest, getPathGridGraph(pathgrid), agentBounds,
agentBounds, navigatorFlags, areaCosts, endTolerance, pathType); navigatorFlags, areaCosts, endTolerance, pathType);
mRotateOnTheRunChecks = 3; mRotateOnTheRunChecks = 3;
// give priority to go directly on target if there is minimal opportunity // give priority to go directly on target if there is minimal opportunity
@ -501,7 +501,11 @@ DetourNavigator::Flags MWMechanics::AiPackage::getNavigatorFlags(const MWWorld::
result |= DetourNavigator::Flag_swim; result |= DetourNavigator::Flag_swim;
if (actorClass.canWalk(actor) && actor.getClass().getWalkSpeed(actor) > 0) if (actorClass.canWalk(actor) && actor.getClass().getWalkSpeed(actor) > 0)
result |= DetourNavigator::Flag_walk | DetourNavigator::Flag_usePathgrid; {
result |= DetourNavigator::Flag_walk;
if (getTypeId() != AiPackageTypeId::Wander)
result |= DetourNavigator::Flag_usePathgrid;
}
if (canOpenDoors(actor) && getTypeId() != AiPackageTypeId::Wander) if (canOpenDoors(actor) && getTypeId() != AiPackageTypeId::Wander)
result |= DetourNavigator::Flag_openDoor; result |= DetourNavigator::Flag_openDoor;

View File

@ -16,6 +16,8 @@
namespace ESM namespace ESM
{ {
struct Cell; struct Cell;
struct Pathgrid;
namespace AiSequence namespace AiSequence
{ {
struct AiSequence; struct AiSequence;

View File

@ -8,6 +8,7 @@
#include <components/detournavigator/navigatorutils.hpp> #include <components/detournavigator/navigatorutils.hpp>
#include <components/esm3/aisequence.hpp> #include <components/esm3/aisequence.hpp>
#include <components/misc/coordinateconverter.hpp> #include <components/misc/coordinateconverter.hpp>
#include <components/misc/pathgridutils.hpp>
#include <components/misc/rng.hpp> #include <components/misc/rng.hpp>
#include "../mwbase/environment.hpp" #include "../mwbase/environment.hpp"
@ -29,17 +30,6 @@
namespace MWMechanics namespace MWMechanics
{ {
static const int COUNT_BEFORE_RESET = 10;
static const float IDLE_POSITION_CHECK_INTERVAL = 1.5f;
// to prevent overcrowding
static const int DESTINATION_TOLERANCE = 64;
// distance must be long enough that NPC will need to move to get there.
static const int MINIMUM_WANDER_DISTANCE = DESTINATION_TOLERANCE * 2;
static const std::size_t MAX_IDLE_SIZE = 8;
const std::string_view AiWander::sIdleSelectToGroupName[GroupIndex_MaxIdle - GroupIndex_MinIdle + 1] = { const std::string_view AiWander::sIdleSelectToGroupName[GroupIndex_MaxIdle - GroupIndex_MinIdle + 1] = {
"idle2", "idle2",
"idle3", "idle3",
@ -53,11 +43,22 @@ namespace MWMechanics
namespace namespace
{ {
constexpr int countBeforeReset = 10;
constexpr float idlePositionCheckInterval = 1.5f;
// to prevent overcrowding
constexpr unsigned destinationTolerance = 64;
// distance must be long enough that NPC will need to move to get there.
constexpr unsigned minimumWanderDistance = destinationTolerance * 2;
constexpr std::size_t maxIdleSize = 8;
inline int getCountBeforeReset(const MWWorld::ConstPtr& actor) inline int getCountBeforeReset(const MWWorld::ConstPtr& actor)
{ {
if (actor.getClass().isPureWaterCreature(actor) || actor.getClass().isPureFlyingCreature(actor)) if (actor.getClass().isPureWaterCreature(actor) || actor.getClass().isPureFlyingCreature(actor))
return 1; return 1;
return COUNT_BEFORE_RESET; return countBeforeReset;
} }
osg::Vec3f getRandomPointAround(const osg::Vec3f& position, const float distance) osg::Vec3f getRandomPointAround(const osg::Vec3f& position, const float distance)
@ -99,16 +100,42 @@ namespace MWMechanics
std::vector<unsigned char> getInitialIdle(const std::vector<unsigned char>& idle) std::vector<unsigned char> getInitialIdle(const std::vector<unsigned char>& idle)
{ {
std::vector<unsigned char> result(MAX_IDLE_SIZE, 0); std::vector<unsigned char> result(maxIdleSize, 0);
std::copy_n(idle.begin(), std::min(MAX_IDLE_SIZE, idle.size()), result.begin()); std::copy_n(idle.begin(), std::min(maxIdleSize, idle.size()), result.begin());
return result; return result;
} }
std::vector<unsigned char> getInitialIdle(const unsigned char (&idle)[MAX_IDLE_SIZE]) std::vector<unsigned char> getInitialIdle(const unsigned char (&idle)[maxIdleSize])
{ {
return std::vector<unsigned char>(std::begin(idle), std::end(idle)); return std::vector<unsigned char>(std::begin(idle), std::end(idle));
} }
void trimAllowedPositions(const std::deque<osg::Vec3f>& path, std::vector<osg::Vec3f>& allowedPositions)
{
// TODO: how to add these back in once the door opens?
// Idea: keep a list of detected closed doors (see aicombat.cpp)
// Every now and then check whether one of the doors is opened. (maybe
// at the end of playing idle?) If the door is opened then re-calculate
// allowed positions starting from the spawn point.
std::vector<osg::Vec3f> points(path.begin(), path.end());
while (points.size() >= 2)
{
const osg::Vec3f point = points.back();
for (std::size_t j = 0; j < allowedPositions.size(); j++)
{
// FIXME: doesn't handle a door with the same X/Y
// coordinates but with a different Z
if (std::abs(allowedPositions[j].x() - point.x()) <= 0.5
&& std::abs(allowedPositions[j].y() - point.y()) <= 0.5)
{
allowedPositions.erase(allowedPositions.begin() + j);
break;
}
}
points.pop_back();
}
}
} }
AiWanderStorage::AiWanderStorage() AiWanderStorage::AiWanderStorage()
@ -118,9 +145,9 @@ namespace MWMechanics
, mCanWanderAlongPathGrid(true) , mCanWanderAlongPathGrid(true)
, mIdleAnimation(0) , mIdleAnimation(0)
, mBadIdles() , mBadIdles()
, mPopulateAvailableNodes(true) , mPopulateAvailablePositions(true)
, mAllowedNodes() , mAllowedPositions()
, mTrimCurrentNode(false) , mTrimCurrentPosition(false)
, mCheckIdlePositionTimer(0) , mCheckIdlePositionTimer(0)
, mStuckCount(0) , mStuckCount(0)
{ {
@ -128,8 +155,8 @@ namespace MWMechanics
AiWander::AiWander(int distance, int duration, int timeOfDay, const std::vector<unsigned char>& idle, bool repeat) AiWander::AiWander(int distance, int duration, int timeOfDay, const std::vector<unsigned char>& idle, bool repeat)
: TypedAiPackage<AiWander>(repeat) : TypedAiPackage<AiWander>(repeat)
, mDistance(std::max(0, distance)) , mDistance(static_cast<unsigned>(std::max(0, distance)))
, mDuration(std::max(0, duration)) , mDuration(static_cast<unsigned>(std::max(0, duration)))
, mRemainingDuration(duration) , mRemainingDuration(duration)
, mTimeOfDay(timeOfDay) , mTimeOfDay(timeOfDay)
, mIdle(getInitialIdle(idle)) , mIdle(getInitialIdle(idle))
@ -215,20 +242,12 @@ namespace MWMechanics
{ {
const ESM::Pathgrid* pathgrid const ESM::Pathgrid* pathgrid
= MWBase::Environment::get().getESMStore()->get<ESM::Pathgrid>().search(*actor.getCell()->getCell()); = MWBase::Environment::get().getESMStore()->get<ESM::Pathgrid>().search(*actor.getCell()->getCell());
if (mUsePathgrid)
{
mPathFinder.buildPathByPathgrid(
pos.asVec3(), mDestination, actor.getCell(), getPathGridGraph(pathgrid));
}
else
{
const auto agentBounds = MWBase::Environment::get().getWorld()->getPathfindingAgentBounds(actor); const auto agentBounds = MWBase::Environment::get().getWorld()->getPathfindingAgentBounds(actor);
constexpr float endTolerance = 0; constexpr float endTolerance = 0;
const DetourNavigator::Flags navigatorFlags = getNavigatorFlags(actor); const DetourNavigator::Flags navigatorFlags = getNavigatorFlags(actor);
const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags); const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags);
mPathFinder.buildPath(actor, pos.asVec3(), mDestination, actor.getCell(), getPathGridGraph(pathgrid), mPathFinder.buildPath(actor, pos.asVec3(), mDestination, getPathGridGraph(pathgrid), agentBounds,
agentBounds, navigatorFlags, areaCosts, endTolerance, PathType::Full); navigatorFlags, areaCosts, endTolerance, PathType::Full);
}
if (mPathFinder.isPathConstructed()) if (mPathFinder.isPathConstructed())
storage.setState(AiWanderStorage::Wander_Walking, !mUsePathgrid); storage.setState(AiWanderStorage::Wander_Walking, !mUsePathgrid);
@ -259,9 +278,6 @@ namespace MWMechanics
bool AiWander::reactionTimeActions(const MWWorld::Ptr& actor, AiWanderStorage& storage, ESM::Position& pos) bool AiWander::reactionTimeActions(const MWWorld::Ptr& actor, AiWanderStorage& storage, ESM::Position& pos)
{ {
if (mDistance <= 0)
storage.mCanWanderAlongPathGrid = false;
if (isPackageCompleted()) if (isPackageCompleted())
{ {
stopWalking(actor); stopWalking(actor);
@ -276,13 +292,15 @@ namespace MWMechanics
mStoredInitialActorPosition = true; mStoredInitialActorPosition = true;
} }
// Initialization to discover & store allowed node points for this actor. // Initialization to discover & store allowed positions points for this actor.
if (storage.mPopulateAvailableNodes) if (storage.mPopulateAvailablePositions)
{ {
getAllowedNodes(actor, storage); fillAllowedPositions(actor, storage);
} }
auto& prng = MWBase::Environment::get().getWorld()->getPrng(); MWBase::World& world = *MWBase::Environment::get().getWorld();
auto& prng = world.getPrng();
if (canActorMoveByZAxis(actor) && mDistance > 0) if (canActorMoveByZAxis(actor) && mDistance > 0)
{ {
// Typically want to idle for a short time before the next wander // Typically want to idle for a short time before the next wander
@ -295,7 +313,7 @@ namespace MWMechanics
} }
// If the package has a wander distance but no pathgrid is available, // If the package has a wander distance but no pathgrid is available,
// randomly idle or wander near spawn point // randomly idle or wander near spawn point
else if (storage.mAllowedNodes.empty() && mDistance > 0 && !storage.mIsWanderingManually) else if (storage.mAllowedPositions.empty() && mDistance > 0 && !storage.mIsWanderingManually)
{ {
// Typically want to idle for a short time before the next wander // Typically want to idle for a short time before the next wander
if (Misc::Rng::rollDice(100, prng) >= 96) if (Misc::Rng::rollDice(100, prng) >= 96)
@ -307,7 +325,7 @@ namespace MWMechanics
storage.setState(AiWanderStorage::Wander_IdleNow); storage.setState(AiWanderStorage::Wander_IdleNow);
} }
} }
else if (storage.mAllowedNodes.empty() && !storage.mIsWanderingManually) else if (storage.mAllowedPositions.empty() && !storage.mIsWanderingManually)
{ {
storage.mCanWanderAlongPathGrid = false; storage.mCanWanderAlongPathGrid = false;
} }
@ -323,9 +341,9 @@ namespace MWMechanics
// Construct a new path if there isn't one // Construct a new path if there isn't one
if (!mPathFinder.isPathConstructed()) if (!mPathFinder.isPathConstructed())
{ {
if (!storage.mAllowedNodes.empty()) if (!storage.mAllowedPositions.empty())
{ {
setPathToAnAllowedNode(actor, storage, pos); setPathToAnAllowedPosition(actor, storage, pos);
} }
} }
} }
@ -336,7 +354,7 @@ namespace MWMechanics
if (storage.mIsWanderingManually && storage.mState == AiWanderStorage::Wander_Walking if (storage.mIsWanderingManually && storage.mState == AiWanderStorage::Wander_Walking
&& (mPathFinder.getPathSize() == 0 || isDestinationHidden(actor, mPathFinder.getPath().back()) && (mPathFinder.getPathSize() == 0 || isDestinationHidden(actor, mPathFinder.getPath().back())
|| isAreaOccupiedByOtherActor(actor, mPathFinder.getPath().back()))) || world.isAreaOccupiedByOtherActor(actor, mPathFinder.getPath().back())))
completeManualWalking(actor, storage); completeManualWalking(actor, storage);
return false; // AiWander package not yet completed return false; // AiWander package not yet completed
@ -366,12 +384,12 @@ namespace MWMechanics
std::size_t attempts = 10; // If a unit can't wander out of water, don't want to hang here std::size_t attempts = 10; // If a unit can't wander out of water, don't want to hang here
const bool isWaterCreature = actor.getClass().isPureWaterCreature(actor); const bool isWaterCreature = actor.getClass().isPureWaterCreature(actor);
const bool isFlyingCreature = actor.getClass().isPureFlyingCreature(actor); const bool isFlyingCreature = actor.getClass().isPureFlyingCreature(actor);
const auto world = MWBase::Environment::get().getWorld(); MWBase::World& world = *MWBase::Environment::get().getWorld();
const auto agentBounds = world->getPathfindingAgentBounds(actor); const auto agentBounds = world.getPathfindingAgentBounds(actor);
const auto navigator = world->getNavigator(); const auto navigator = world.getNavigator();
const DetourNavigator::Flags navigatorFlags = getNavigatorFlags(actor); const DetourNavigator::Flags navigatorFlags = getNavigatorFlags(actor);
const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags); const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags);
auto& prng = MWBase::Environment::get().getWorld()->getPrng(); Misc::Rng::Generator& prng = world.getPrng();
do do
{ {
@ -411,7 +429,7 @@ namespace MWMechanics
if (isDestinationHidden(actor, mDestination)) if (isDestinationHidden(actor, mDestination))
continue; continue;
if (isAreaOccupiedByOtherActor(actor, mDestination)) if (world.isAreaOccupiedByOtherActor(actor, mDestination))
continue; continue;
constexpr float endTolerance = 0; constexpr float endTolerance = 0;
@ -491,17 +509,17 @@ namespace MWMechanics
void AiWander::onIdleStatePerFrameActions(const MWWorld::Ptr& actor, float duration, AiWanderStorage& storage) void AiWander::onIdleStatePerFrameActions(const MWWorld::Ptr& actor, float duration, AiWanderStorage& storage)
{ {
// Check if an idle actor is too far from all allowed nodes or too close to a door - if so start walking. // Check if an idle actor is too far from all allowed positions or too close to a door - if so start walking.
storage.mCheckIdlePositionTimer += duration; storage.mCheckIdlePositionTimer += duration;
if (storage.mCheckIdlePositionTimer >= IDLE_POSITION_CHECK_INTERVAL && !isStationary()) if (storage.mCheckIdlePositionTimer >= idlePositionCheckInterval && !isStationary())
{ {
storage.mCheckIdlePositionTimer = 0; // restart timer storage.mCheckIdlePositionTimer = 0; // restart timer
static float distance = MWBase::Environment::get().getWorld()->getMaxActivationDistance() * 1.6f; static float distance = MWBase::Environment::get().getWorld()->getMaxActivationDistance() * 1.6f;
if (proximityToDoor(actor, distance) || !isNearAllowedNode(actor, storage, distance)) if (proximityToDoor(actor, distance) || !isNearAllowedPosition(actor, storage, distance))
{ {
storage.setState(AiWanderStorage::Wander_MoveNow); storage.setState(AiWanderStorage::Wander_MoveNow);
storage.mTrimCurrentNode = false; // just in case storage.mTrimCurrentPosition = false; // just in case
return; return;
} }
} }
@ -517,16 +535,14 @@ namespace MWMechanics
} }
} }
bool AiWander::isNearAllowedNode(const MWWorld::Ptr& actor, const AiWanderStorage& storage, float distance) const bool AiWander::isNearAllowedPosition(
const MWWorld::Ptr& actor, const AiWanderStorage& storage, float distance) const
{ {
const osg::Vec3f actorPos = actor.getRefData().getPosition().asVec3(); const osg::Vec3f actorPos = actor.getRefData().getPosition().asVec3();
for (const ESM::Pathgrid::Point& node : storage.mAllowedNodes) const float squaredDistance = distance * distance;
{ return std::ranges::find_if(storage.mAllowedPositions, [&](const osg::Vec3& v) {
osg::Vec3f point(node.mX, node.mY, node.mZ); return (actorPos - v).length2() < squaredDistance;
if ((actorPos - point).length2() < distance * distance) }) != storage.mAllowedPositions.end();
return true;
}
return false;
} }
void AiWander::onWalkingStatePerFrameActions(const MWWorld::Ptr& actor, float duration, void AiWander::onWalkingStatePerFrameActions(const MWWorld::Ptr& actor, float duration,
@ -535,7 +551,7 @@ namespace MWMechanics
// Is there no destination or are we there yet? // Is there no destination or are we there yet?
if ((!mPathFinder.isPathConstructed()) if ((!mPathFinder.isPathConstructed())
|| pathTo(actor, osg::Vec3f(mPathFinder.getPath().back()), duration, supportedMovementDirections, || pathTo(actor, osg::Vec3f(mPathFinder.getPath().back()), duration, supportedMovementDirections,
DESTINATION_TOLERANCE)) destinationTolerance))
{ {
stopWalking(actor); stopWalking(actor);
storage.setState(AiWanderStorage::Wander_ChooseAction); storage.setState(AiWanderStorage::Wander_ChooseAction);
@ -586,8 +602,8 @@ namespace MWMechanics
if (proximityToDoor(actor, distance)) if (proximityToDoor(actor, distance))
{ {
// remove allowed points then select another random destination // remove allowed points then select another random destination
storage.mTrimCurrentNode = true; storage.mTrimCurrentPosition = true;
trimAllowedNodes(storage.mAllowedNodes, mPathFinder); trimAllowedPositions(mPathFinder.getPath(), storage.mAllowedPositions);
mObstacleCheck.clear(); mObstacleCheck.clear();
stopWalking(actor); stopWalking(actor);
storage.setState(AiWanderStorage::Wander_MoveNow); storage.setState(AiWanderStorage::Wander_MoveNow);
@ -606,68 +622,68 @@ namespace MWMechanics
} }
} }
void AiWander::setPathToAnAllowedNode( void AiWander::setPathToAnAllowedPosition(
const MWWorld::Ptr& actor, AiWanderStorage& storage, const ESM::Position& actorPos) const MWWorld::Ptr& actor, AiWanderStorage& storage, const ESM::Position& actorPos)
{ {
auto world = MWBase::Environment::get().getWorld(); MWBase::World& world = *MWBase::Environment::get().getWorld();
auto& prng = world->getPrng(); Misc::Rng::Generator& prng = world.getPrng();
unsigned int randNode = Misc::Rng::rollDice(storage.mAllowedNodes.size(), prng); const std::size_t randomAllowedPositionIndex
const ESM::Pathgrid::Point& dest = storage.mAllowedNodes[randNode]; = static_cast<std::size_t>(Misc::Rng::rollDice(storage.mAllowedPositions.size(), prng));
const osg::Vec3f randomAllowedPosition = storage.mAllowedPositions[randomAllowedPositionIndex];
const osg::Vec3f start = actorPos.asVec3(); const osg::Vec3f start = actorPos.asVec3();
// don't take shortcuts for wandering const MWWorld::Cell& cell = *actor.getCell()->getCell();
const ESM::Pathgrid* pathgrid = world->getStore().get<ESM::Pathgrid>().search(*actor.getCell()->getCell()); const ESM::Pathgrid* pathgrid = world.getStore().get<ESM::Pathgrid>().search(cell);
const osg::Vec3f destVec3f = PathFinder::makeOsgVec3(dest); const PathgridGraph& pathgridGraph = getPathGridGraph(pathgrid);
mPathFinder.buildPathByPathgrid(start, destVec3f, actor.getCell(), getPathGridGraph(pathgrid));
if (mPathFinder.isPathConstructed()) const Misc::CoordinateConverter converter = Misc::makeCoordinateConverter(cell);
std::deque<ESM::Pathgrid::Point> path
= pathgridGraph.aStarSearch(Misc::getClosestPoint(*pathgrid, converter.toLocalVec3(start)),
Misc::getClosestPoint(*pathgrid, converter.toLocalVec3(randomAllowedPosition)));
// Choose a different position and delete this one from possible positions because it is uncreachable:
if (path.empty())
{ {
mDestination = destVec3f; storage.mAllowedPositions.erase(storage.mAllowedPositions.begin() + randomAllowedPositionIndex);
return;
}
// Drop nearest pathgrid point.
path.pop_front();
std::vector<osg::Vec3f> checkpoints(path.size());
for (std::size_t i = 0; i < path.size(); ++i)
checkpoints[i] = Misc::Convert::makeOsgVec3f(converter.toWorldPoint(path[i]));
const DetourNavigator::AgentBounds agentBounds = world.getPathfindingAgentBounds(actor);
const DetourNavigator::Flags flags = getNavigatorFlags(actor);
const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, flags);
constexpr float endTolerance = 0;
mPathFinder.buildPath(actor, start, randomAllowedPosition, pathgridGraph, agentBounds, flags, areaCosts,
endTolerance, PathType::Full, checkpoints);
if (!mPathFinder.isPathConstructed())
{
storage.mAllowedPositions.erase(storage.mAllowedPositions.begin() + randomAllowedPositionIndex);
return;
}
mDestination = randomAllowedPosition;
mHasDestination = true; mHasDestination = true;
mUsePathgrid = true; mUsePathgrid = true;
// Remove this node as an option and add back the previously used node (stops NPC from picking the same // Remove this position as an option and add back the previously used position (stops NPC from picking the
// node): // same position):
ESM::Pathgrid::Point temp = storage.mAllowedNodes[randNode]; storage.mAllowedPositions.erase(storage.mAllowedPositions.begin() + randomAllowedPositionIndex);
storage.mAllowedNodes.erase(storage.mAllowedNodes.begin() + randNode); // check if mCurrentPosition was taken out of mAllowedPositions
// check if mCurrentNode was taken out of mAllowedNodes if (storage.mTrimCurrentPosition && storage.mAllowedPositions.size() > 1)
if (storage.mTrimCurrentNode && storage.mAllowedNodes.size() > 1) storage.mTrimCurrentPosition = false;
storage.mTrimCurrentNode = false;
else else
storage.mAllowedNodes.push_back(storage.mCurrentNode); storage.mAllowedPositions.push_back(storage.mCurrentPosition);
storage.mCurrentNode = temp; storage.mCurrentPosition = randomAllowedPosition;
storage.setState(AiWanderStorage::Wander_Walking); storage.setState(AiWanderStorage::Wander_Walking);
} }
// Choose a different node and delete this one from possible nodes because it is uncreachable:
else
storage.mAllowedNodes.erase(storage.mAllowedNodes.begin() + randNode);
}
void AiWander::trimAllowedNodes(std::vector<ESM::Pathgrid::Point>& nodes, const PathFinder& pathfinder)
{
// TODO: how to add these back in once the door opens?
// Idea: keep a list of detected closed doors (see aicombat.cpp)
// Every now and then check whether one of the doors is opened. (maybe
// at the end of playing idle?) If the door is opened then re-calculate
// allowed nodes starting from the spawn point.
auto paths = pathfinder.getPath();
while (paths.size() >= 2)
{
const auto pt = paths.back();
for (unsigned int j = 0; j < nodes.size(); j++)
{
// FIXME: doesn't handle a door with the same X/Y
// coordinates but with a different Z
if (std::abs(nodes[j].mX - pt.x()) <= 0.5 && std::abs(nodes[j].mY - pt.y()) <= 0.5)
{
nodes.erase(nodes.begin() + j);
break;
}
}
paths.pop_back();
}
}
void AiWander::stopWalking(const MWWorld::Ptr& actor) void AiWander::stopWalking(const MWWorld::Ptr& actor)
{ {
@ -742,20 +758,20 @@ namespace MWMechanics
return; return;
AiWanderStorage& storage = state.get<AiWanderStorage>(); AiWanderStorage& storage = state.get<AiWanderStorage>();
if (storage.mPopulateAvailableNodes) if (storage.mPopulateAvailablePositions)
getAllowedNodes(actor, storage); fillAllowedPositions(actor, storage);
if (storage.mAllowedNodes.empty()) if (storage.mAllowedPositions.empty())
return; return;
auto& prng = MWBase::Environment::get().getWorld()->getPrng(); auto& prng = MWBase::Environment::get().getWorld()->getPrng();
int index = Misc::Rng::rollDice(storage.mAllowedNodes.size(), prng); int index = Misc::Rng::rollDice(storage.mAllowedPositions.size(), prng);
ESM::Pathgrid::Point worldDest = storage.mAllowedNodes[index]; const osg::Vec3f worldDest = storage.mAllowedPositions[index];
const Misc::CoordinateConverter converter = Misc::makeCoordinateConverter(*actor.getCell()->getCell()); const Misc::CoordinateConverter converter = Misc::makeCoordinateConverter(*actor.getCell()->getCell());
ESM::Pathgrid::Point dest = converter.toLocalPoint(worldDest); osg::Vec3f dest = converter.toLocalVec3(worldDest);
bool isPathGridOccupied = MWBase::Environment::get().getMechanicsManager()->isAnyActorInRange( const bool isPathGridOccupied
PathFinder::makeOsgVec3(worldDest), 60); = MWBase::Environment::get().getMechanicsManager()->isAnyActorInRange(worldDest, 60);
// add offset only if the selected pathgrid is occupied by another actor // add offset only if the selected pathgrid is occupied by another actor
if (isPathGridOccupied) if (isPathGridOccupied)
@ -775,19 +791,17 @@ namespace MWMechanics
const ESM::Pathgrid::Point& connDest = points[randomIndex]; const ESM::Pathgrid::Point& connDest = points[randomIndex];
// add an offset towards random neighboring node // add an offset towards random neighboring node
osg::Vec3f dir = PathFinder::makeOsgVec3(connDest) - PathFinder::makeOsgVec3(dest); osg::Vec3f dir = Misc::Convert::makeOsgVec3f(connDest) - dest;
float length = dir.length(); const float length = dir.length();
dir.normalize(); dir.normalize();
for (int j = 1; j <= 3; j++) for (int j = 1; j <= 3; j++)
{ {
// move for 5-15% towards random neighboring node // move for 5-15% towards random neighboring node
dest dest = dest + dir * (j * 5 * length / 100.f);
= PathFinder::makePathgridPoint(PathFinder::makeOsgVec3(dest) + dir * (j * 5 * length / 100.f));
worldDest = converter.toWorldPoint(dest);
isOccupied = MWBase::Environment::get().getMechanicsManager()->isAnyActorInRange( isOccupied = MWBase::Environment::get().getMechanicsManager()->isAnyActorInRange(
PathFinder::makeOsgVec3(worldDest), 60); converter.toWorldVec3(dest), 60);
if (!isOccupied) if (!isOccupied)
break; break;
@ -807,19 +821,18 @@ namespace MWMechanics
// place above to prevent moving inside objects, e.g. stairs, because a vector between pathgrids can be // place above to prevent moving inside objects, e.g. stairs, because a vector between pathgrids can be
// underground. Adding 20 in adjustPosition() is not enough. // underground. Adding 20 in adjustPosition() is not enough.
dest.mZ += 60; dest.z() += 60;
converter.toWorld(dest); converter.toWorld(dest);
state.reset(); state.reset();
osg::Vec3f pos(static_cast<float>(dest.mX), static_cast<float>(dest.mY), static_cast<float>(dest.mZ)); MWBase::Environment::get().getWorld()->moveObject(actor, dest);
MWBase::Environment::get().getWorld()->moveObject(actor, pos);
actor.getClass().adjustPosition(actor, false); actor.getClass().adjustPosition(actor, false);
} }
void AiWander::getNeighbouringNodes( void AiWander::getNeighbouringNodes(
ESM::Pathgrid::Point dest, const MWWorld::CellStore* currentCell, ESM::Pathgrid::PointList& points) const osg::Vec3f& dest, const MWWorld::CellStore* currentCell, ESM::Pathgrid::PointList& points)
{ {
const ESM::Pathgrid* pathgrid const ESM::Pathgrid* pathgrid
= MWBase::Environment::get().getESMStore()->get<ESM::Pathgrid>().search(*currentCell->getCell()); = MWBase::Environment::get().getESMStore()->get<ESM::Pathgrid>().search(*currentCell->getCell());
@ -827,19 +840,19 @@ namespace MWMechanics
if (pathgrid == nullptr || pathgrid->mPoints.empty()) if (pathgrid == nullptr || pathgrid->mPoints.empty())
return; return;
size_t index = PathFinder::getClosestPoint(pathgrid, PathFinder::makeOsgVec3(dest)); const size_t index = Misc::getClosestPoint(*pathgrid, dest);
getPathGridGraph(pathgrid).getNeighbouringPoints(index, points); getPathGridGraph(pathgrid).getNeighbouringPoints(index, points);
} }
void AiWander::getAllowedNodes(const MWWorld::Ptr& actor, AiWanderStorage& storage) void AiWander::fillAllowedPositions(const MWWorld::Ptr& actor, AiWanderStorage& storage)
{ {
// infrequently used, therefore no benefit in caching it as a member // infrequently used, therefore no benefit in caching it as a member
const MWWorld::CellStore* cellStore = actor.getCell(); const MWWorld::CellStore* cellStore = actor.getCell();
const ESM::Pathgrid* pathgrid const ESM::Pathgrid* pathgrid
= MWBase::Environment::get().getESMStore()->get<ESM::Pathgrid>().search(*cellStore->getCell()); = MWBase::Environment::get().getESMStore()->get<ESM::Pathgrid>().search(*cellStore->getCell());
storage.mAllowedNodes.clear(); storage.mAllowedPositions.clear();
// If there is no path this actor doesn't go anywhere. See: // If there is no path this actor doesn't go anywhere. See:
// https://forum.openmw.org/viewtopic.php?t=1556 // https://forum.openmw.org/viewtopic.php?t=1556
@ -860,34 +873,35 @@ namespace MWMechanics
const osg::Vec3f npcPos = converter.toLocalVec3(mInitialActorPosition); const osg::Vec3f npcPos = converter.toLocalVec3(mInitialActorPosition);
// Find closest pathgrid point // Find closest pathgrid point
size_t closestPointIndex = PathFinder::getClosestPoint(pathgrid, npcPos); const std::size_t closestPointIndex = Misc::getClosestPoint(*pathgrid, npcPos);
// mAllowedNodes for this actor with pathgrid point indexes based on mDistance // mAllowedPositions for this actor with pathgrid point indexes based on mDistance
// and if the point is connected to the closest current point // and if the point is connected to the closest current point
// NOTE: mPoints is in local coordinates // NOTE: mPoints is in local coordinates
size_t pointIndex = 0; size_t pointIndex = 0;
for (size_t counter = 0; counter < pathgrid->mPoints.size(); counter++) for (size_t counter = 0; counter < pathgrid->mPoints.size(); counter++)
{ {
osg::Vec3f nodePos(PathFinder::makeOsgVec3(pathgrid->mPoints[counter])); const osg::Vec3f nodePos = Misc::Convert::makeOsgVec3f(pathgrid->mPoints[counter]);
if ((npcPos - nodePos).length2() <= mDistance * mDistance if ((npcPos - nodePos).length2() <= mDistance * mDistance
&& getPathGridGraph(pathgrid).isPointConnected(closestPointIndex, counter)) && getPathGridGraph(pathgrid).isPointConnected(closestPointIndex, counter))
{ {
storage.mAllowedNodes.push_back(converter.toWorldPoint(pathgrid->mPoints[counter])); storage.mAllowedPositions.push_back(
Misc::Convert::makeOsgVec3f(converter.toWorldPoint(pathgrid->mPoints[counter])));
pointIndex = counter; pointIndex = counter;
} }
} }
if (storage.mAllowedNodes.size() == 1) if (storage.mAllowedPositions.size() == 1)
{ {
storage.mAllowedNodes.push_back(PathFinder::makePathgridPoint(mInitialActorPosition)); storage.mAllowedPositions.push_back(mInitialActorPosition);
addNonPathGridAllowedPoints(pathgrid, pointIndex, storage, converter); addNonPathGridAllowedPoints(pathgrid, pointIndex, storage, converter);
} }
if (!storage.mAllowedNodes.empty()) if (!storage.mAllowedPositions.empty())
{ {
setCurrentNodeToClosestAllowedNode(storage); setCurrentPositionToClosestAllowedPosition(storage);
} }
} }
storage.mPopulateAvailableNodes = false; storage.mPopulateAvailablePositions = false;
} }
// When only one path grid point in wander distance, // When only one path grid point in wander distance,
@ -901,44 +915,44 @@ namespace MWMechanics
{ {
if (edge.mV0 == pointIndex) if (edge.mV0 == pointIndex)
{ {
AddPointBetweenPathGridPoints(converter.toWorldPoint(pathGrid->mPoints[edge.mV0]), addPositionBetweenPathgridPoints(converter.toWorldPoint(pathGrid->mPoints[edge.mV0]),
converter.toWorldPoint(pathGrid->mPoints[edge.mV1]), storage); converter.toWorldPoint(pathGrid->mPoints[edge.mV1]), storage);
} }
} }
} }
void AiWander::AddPointBetweenPathGridPoints( void AiWander::addPositionBetweenPathgridPoints(
const ESM::Pathgrid::Point& start, const ESM::Pathgrid::Point& end, AiWanderStorage& storage) const ESM::Pathgrid::Point& start, const ESM::Pathgrid::Point& end, AiWanderStorage& storage)
{ {
osg::Vec3f vectorStart = PathFinder::makeOsgVec3(start); osg::Vec3f vectorStart = Misc::Convert::makeOsgVec3f(start);
osg::Vec3f delta = PathFinder::makeOsgVec3(end) - vectorStart; osg::Vec3f delta = Misc::Convert::makeOsgVec3f(end) - vectorStart;
float length = delta.length(); float length = delta.length();
delta.normalize(); delta.normalize();
int distance = std::max(mDistance / 2, MINIMUM_WANDER_DISTANCE); unsigned distance = std::max(mDistance / 2, minimumWanderDistance);
// must not travel longer than distance between waypoints or NPC goes past waypoint // must not travel longer than distance between waypoints or NPC goes past waypoint
distance = std::min(distance, static_cast<int>(length)); distance = std::min(distance, static_cast<unsigned>(length));
delta *= distance; delta *= distance;
storage.mAllowedNodes.push_back(PathFinder::makePathgridPoint(vectorStart + delta)); storage.mAllowedPositions.push_back(vectorStart + delta);
} }
void AiWander::setCurrentNodeToClosestAllowedNode(AiWanderStorage& storage) void AiWander::setCurrentPositionToClosestAllowedPosition(AiWanderStorage& storage)
{ {
float distanceToClosestNode = std::numeric_limits<float>::max(); float distanceToClosestPosition = std::numeric_limits<float>::max();
size_t index = 0; size_t index = 0;
for (size_t i = 0; i < storage.mAllowedNodes.size(); ++i) for (size_t i = 0; i < storage.mAllowedPositions.size(); ++i)
{ {
osg::Vec3f nodePos(PathFinder::makeOsgVec3(storage.mAllowedNodes[i])); const osg::Vec3f position = storage.mAllowedPositions[i];
float tempDist = (mInitialActorPosition - nodePos).length2(); const float tempDist = (mInitialActorPosition - position).length2();
if (tempDist < distanceToClosestNode) if (tempDist < distanceToClosestPosition)
{ {
index = i; index = i;
distanceToClosestNode = tempDist; distanceToClosestPosition = tempDist;
} }
} }
storage.mCurrentNode = storage.mAllowedNodes[index]; storage.mCurrentPosition = storage.mAllowedPositions[index];
storage.mAllowedNodes.erase(storage.mAllowedNodes.begin() + index); storage.mAllowedPositions.erase(storage.mAllowedPositions.begin() + index);
} }
void AiWander::writeState(ESM::AiSequence::AiSequence& sequence) const void AiWander::writeState(ESM::AiSequence::AiSequence& sequence) const
@ -970,8 +984,8 @@ namespace MWMechanics
AiWander::AiWander(const ESM::AiSequence::AiWander* wander) AiWander::AiWander(const ESM::AiSequence::AiWander* wander)
: TypedAiPackage<AiWander>(makeDefaultOptions().withRepeat(wander->mData.mShouldRepeat != 0)) : TypedAiPackage<AiWander>(makeDefaultOptions().withRepeat(wander->mData.mShouldRepeat != 0))
, mDistance(std::max(static_cast<short>(0), wander->mData.mDistance)) , mDistance(static_cast<unsigned>(std::max(static_cast<short>(0), wander->mData.mDistance)))
, mDuration(std::max(static_cast<short>(0), wander->mData.mDuration)) , mDuration(static_cast<unsigned>(std::max(static_cast<short>(0), wander->mData.mDuration)))
, mRemainingDuration(wander->mDurationData.mRemainingDuration) , mRemainingDuration(wander->mDurationData.mRemainingDuration)
, mTimeOfDay(wander->mData.mTimeOfDay) , mTimeOfDay(wander->mData.mTimeOfDay)
, mIdle(getInitialIdle(wander->mData.mIdle)) , mIdle(getInitialIdle(wander->mData.mIdle))

View File

@ -1,14 +1,15 @@
#ifndef GAME_MWMECHANICS_AIWANDER_H #ifndef GAME_MWMECHANICS_AIWANDER_H
#define GAME_MWMECHANICS_AIWANDER_H #define GAME_MWMECHANICS_AIWANDER_H
#include "typedaipackage.hpp"
#include <string_view>
#include <vector>
#include "aitemporarybase.hpp" #include "aitemporarybase.hpp"
#include "aitimer.hpp" #include "aitimer.hpp"
#include "pathfinding.hpp" #include "pathfinding.hpp"
#include "typedaipackage.hpp"
#include <components/esm3/loadpgrd.hpp>
#include <string_view>
#include <vector>
namespace ESM namespace ESM
{ {
@ -51,14 +52,13 @@ namespace MWMechanics
unsigned short mIdleAnimation; unsigned short mIdleAnimation;
std::vector<unsigned short> mBadIdles; // Idle animations that when called cause errors std::vector<unsigned short> mBadIdles; // Idle animations that when called cause errors
// do we need to calculate allowed nodes based on mDistance bool mPopulateAvailablePositions;
bool mPopulateAvailableNodes;
// allowed pathgrid nodes based on mDistance from the spawn point // allowed destination positions based on mDistance from the spawn point
std::vector<ESM::Pathgrid::Point> mAllowedNodes; std::vector<osg::Vec3f> mAllowedPositions;
ESM::Pathgrid::Point mCurrentNode; osg::Vec3f mCurrentPosition;
bool mTrimCurrentNode; bool mTrimCurrentPosition;
float mCheckIdlePositionTimer; float mCheckIdlePositionTimer;
int mStuckCount; int mStuckCount;
@ -132,7 +132,8 @@ namespace MWMechanics
bool playIdle(const MWWorld::Ptr& actor, unsigned short idleSelect); bool playIdle(const MWWorld::Ptr& actor, unsigned short idleSelect);
bool checkIdle(const MWWorld::Ptr& actor, unsigned short idleSelect); bool checkIdle(const MWWorld::Ptr& actor, unsigned short idleSelect);
int getRandomIdle() const; int getRandomIdle() const;
void setPathToAnAllowedNode(const MWWorld::Ptr& actor, AiWanderStorage& storage, const ESM::Position& actorPos); void setPathToAnAllowedPosition(
const MWWorld::Ptr& actor, AiWanderStorage& storage, const ESM::Position& actorPos);
void evadeObstacles(const MWWorld::Ptr& actor, AiWanderStorage& storage); void evadeObstacles(const MWWorld::Ptr& actor, AiWanderStorage& storage);
void doPerFrameActionsForState(const MWWorld::Ptr& actor, float duration, void doPerFrameActionsForState(const MWWorld::Ptr& actor, float duration,
MWWorld::MovementDirectionFlags supportedMovementDirections, AiWanderStorage& storage); MWWorld::MovementDirectionFlags supportedMovementDirections, AiWanderStorage& storage);
@ -145,28 +146,27 @@ namespace MWMechanics
void wanderNearStart(const MWWorld::Ptr& actor, AiWanderStorage& storage, int wanderDistance); void wanderNearStart(const MWWorld::Ptr& actor, AiWanderStorage& storage, int wanderDistance);
bool destinationIsAtWater(const MWWorld::Ptr& actor, const osg::Vec3f& destination); bool destinationIsAtWater(const MWWorld::Ptr& actor, const osg::Vec3f& destination);
void completeManualWalking(const MWWorld::Ptr& actor, AiWanderStorage& storage); void completeManualWalking(const MWWorld::Ptr& actor, AiWanderStorage& storage);
bool isNearAllowedNode(const MWWorld::Ptr& actor, const AiWanderStorage& storage, float distance) const; bool isNearAllowedPosition(const MWWorld::Ptr& actor, const AiWanderStorage& storage, float distance) const;
const int mDistance; // how far the actor can wander from the spawn point // how far the actor can wander from the spawn point
const int mDuration; const unsigned mDistance;
const unsigned mDuration;
float mRemainingDuration; float mRemainingDuration;
const int mTimeOfDay; const int mTimeOfDay;
const std::vector<unsigned char> mIdle; const std::vector<unsigned char> mIdle;
bool mStoredInitialActorPosition; bool mStoredInitialActorPosition;
osg::Vec3f // Note: an original engine does not reset coordinates even when actor changes a cell
mInitialActorPosition; // Note: an original engine does not reset coordinates even when actor changes a cell osg::Vec3f mInitialActorPosition;
bool mHasDestination; bool mHasDestination;
osg::Vec3f mDestination; osg::Vec3f mDestination;
bool mUsePathgrid; bool mUsePathgrid;
void getNeighbouringNodes( void getNeighbouringNodes(
ESM::Pathgrid::Point dest, const MWWorld::CellStore* currentCell, ESM::Pathgrid::PointList& points); const osg::Vec3f& dest, const MWWorld::CellStore* currentCell, ESM::Pathgrid::PointList& points);
void getAllowedNodes(const MWWorld::Ptr& actor, AiWanderStorage& storage); void fillAllowedPositions(const MWWorld::Ptr& actor, AiWanderStorage& storage);
void trimAllowedNodes(std::vector<ESM::Pathgrid::Point>& nodes, const PathFinder& pathfinder);
// constants for converting idleSelect values into groupNames // constants for converting idleSelect values into groupNames
enum GroupIndex enum GroupIndex
@ -175,12 +175,12 @@ namespace MWMechanics
GroupIndex_MaxIdle = 9 GroupIndex_MaxIdle = 9
}; };
void setCurrentNodeToClosestAllowedNode(AiWanderStorage& storage); void setCurrentPositionToClosestAllowedPosition(AiWanderStorage& storage);
void addNonPathGridAllowedPoints(const ESM::Pathgrid* pathGrid, size_t pointIndex, AiWanderStorage& storage, void addNonPathGridAllowedPoints(const ESM::Pathgrid* pathGrid, size_t pointIndex, AiWanderStorage& storage,
const Misc::CoordinateConverter& converter); const Misc::CoordinateConverter& converter);
void AddPointBetweenPathGridPoints( void addPositionBetweenPathgridPoints(
const ESM::Pathgrid::Point& start, const ESM::Pathgrid::Point& end, AiWanderStorage& storage); const ESM::Pathgrid::Point& start, const ESM::Pathgrid::Point& end, AiWanderStorage& storage);
/// lookup table for converting idleSelect value to groupName /// lookup table for converting idleSelect value to groupName

View File

@ -106,21 +106,6 @@ namespace MWMechanics
return visitor.mResult; return visitor.mResult;
} }
bool isAreaOccupiedByOtherActor(const MWWorld::ConstPtr& actor, const osg::Vec3f& destination, bool ignorePlayer,
std::vector<MWWorld::Ptr>* occupyingActors)
{
const auto world = MWBase::Environment::get().getWorld();
const osg::Vec3f halfExtents = world->getPathfindingAgentBounds(actor).mHalfExtents;
const auto maxHalfExtent = std::max(halfExtents.x(), std::max(halfExtents.y(), halfExtents.z()));
if (ignorePlayer)
{
const std::array ignore{ actor, world->getPlayerConstPtr() };
return world->isAreaOccupiedByOtherActor(destination, 2 * maxHalfExtent, ignore, occupyingActors);
}
const std::array ignore{ actor };
return world->isAreaOccupiedByOtherActor(destination, 2 * maxHalfExtent, ignore, occupyingActors);
}
ObstacleCheck::ObstacleCheck() ObstacleCheck::ObstacleCheck()
: mEvadeDirectionIndex(std::size(evadeDirections) - 1) : mEvadeDirectionIndex(std::size(evadeDirections) - 1)
{ {

View File

@ -5,8 +5,6 @@
#include <osg/Vec3f> #include <osg/Vec3f>
#include <vector>
namespace MWWorld namespace MWWorld
{ {
class Ptr; class Ptr;
@ -24,9 +22,6 @@ namespace MWMechanics
/** \return Pointer to the door, or empty pointer if none exists **/ /** \return Pointer to the door, or empty pointer if none exists **/
const MWWorld::Ptr getNearbyDoor(const MWWorld::Ptr& actor, float minDist); const MWWorld::Ptr getNearbyDoor(const MWWorld::Ptr& actor, float minDist);
bool isAreaOccupiedByOtherActor(const MWWorld::ConstPtr& actor, const osg::Vec3f& destination,
bool ignorePlayer = false, std::vector<MWWorld::Ptr>* occupyingActors = nullptr);
class ObstacleCheck class ObstacleCheck
{ {
public: public:

View File

@ -10,6 +10,7 @@
#include <components/detournavigator/navigatorutils.hpp> #include <components/detournavigator/navigatorutils.hpp>
#include <components/misc/coordinateconverter.hpp> #include <components/misc/coordinateconverter.hpp>
#include <components/misc/math.hpp> #include <components/misc/math.hpp>
#include <components/misc/pathgridutils.hpp>
#include "../mwbase/environment.hpp" #include "../mwbase/environment.hpp"
#include "../mwbase/world.hpp" #include "../mwbase/world.hpp"
@ -38,7 +39,7 @@ namespace
// points to a quadtree may help // points to a quadtree may help
for (size_t counter = 0; counter < grid->mPoints.size(); counter++) for (size_t counter = 0; counter < grid->mPoints.size(); counter++)
{ {
float potentialDistBetween = MWMechanics::PathFinder::distanceSquared(grid->mPoints[counter], pos); float potentialDistBetween = Misc::distanceSquared(grid->mPoints[counter], pos);
if (potentialDistBetween < closestDistanceReachable) if (potentialDistBetween < closestDistanceReachable)
{ {
// found a closer one // found a closer one
@ -197,7 +198,7 @@ namespace MWMechanics
// point right behind the wall that is closer than any pathgrid // point right behind the wall that is closer than any pathgrid
// point outside the wall // point outside the wall
osg::Vec3f startPointInLocalCoords(converter.toLocalVec3(startPoint)); osg::Vec3f startPointInLocalCoords(converter.toLocalVec3(startPoint));
size_t startNode = getClosestPoint(pathgrid, startPointInLocalCoords); const size_t startNode = Misc::getClosestPoint(*pathgrid, startPointInLocalCoords);
osg::Vec3f endPointInLocalCoords(converter.toLocalVec3(endPoint)); osg::Vec3f endPointInLocalCoords(converter.toLocalVec3(endPoint));
std::pair<size_t, bool> endNode std::pair<size_t, bool> endNode
@ -206,8 +207,8 @@ namespace MWMechanics
// if it's shorter for actor to travel from start to end, than to travel from either // if it's shorter for actor to travel from start to end, than to travel from either
// start or end to nearest pathgrid point, just travel from start to end. // start or end to nearest pathgrid point, just travel from start to end.
float startToEndLength2 = (endPointInLocalCoords - startPointInLocalCoords).length2(); float startToEndLength2 = (endPointInLocalCoords - startPointInLocalCoords).length2();
float endTolastNodeLength2 = distanceSquared(pathgrid->mPoints[endNode.first], endPointInLocalCoords); float endTolastNodeLength2 = Misc::distanceSquared(pathgrid->mPoints[endNode.first], endPointInLocalCoords);
float startTo1stNodeLength2 = distanceSquared(pathgrid->mPoints[startNode], startPointInLocalCoords); float startTo1stNodeLength2 = Misc::distanceSquared(pathgrid->mPoints[startNode], startPointInLocalCoords);
if ((startToEndLength2 < startTo1stNodeLength2) || (startToEndLength2 < endTolastNodeLength2)) if ((startToEndLength2 < startTo1stNodeLength2) || (startToEndLength2 < endTolastNodeLength2))
{ {
*out++ = endPoint; *out++ = endPoint;
@ -223,7 +224,7 @@ namespace MWMechanics
{ {
ESM::Pathgrid::Point temp(pathgrid->mPoints[startNode]); ESM::Pathgrid::Point temp(pathgrid->mPoints[startNode]);
converter.toWorld(temp); converter.toWorld(temp);
*out++ = makeOsgVec3(temp); *out++ = Misc::Convert::makeOsgVec3f(temp);
} }
else else
{ {
@ -234,8 +235,8 @@ namespace MWMechanics
if (path.size() > 1) if (path.size() > 1)
{ {
ESM::Pathgrid::Point secondNode = *(++path.begin()); ESM::Pathgrid::Point secondNode = *(++path.begin());
osg::Vec3f firstNodeVec3f = makeOsgVec3(pathgrid->mPoints[startNode]); osg::Vec3f firstNodeVec3f = Misc::Convert::makeOsgVec3f(pathgrid->mPoints[startNode]);
osg::Vec3f secondNodeVec3f = makeOsgVec3(secondNode); osg::Vec3f secondNodeVec3f = Misc::Convert::makeOsgVec3f(secondNode);
osg::Vec3f toSecondNodeVec3f = secondNodeVec3f - firstNodeVec3f; osg::Vec3f toSecondNodeVec3f = secondNodeVec3f - firstNodeVec3f;
osg::Vec3f toStartPointVec3f = startPointInLocalCoords - firstNodeVec3f; osg::Vec3f toStartPointVec3f = startPointInLocalCoords - firstNodeVec3f;
if (toSecondNodeVec3f * toStartPointVec3f > 0) if (toSecondNodeVec3f * toStartPointVec3f > 0)
@ -259,7 +260,7 @@ namespace MWMechanics
// convert supplied path to world coordinates // convert supplied path to world coordinates
std::transform(path.begin(), path.end(), out, [&](ESM::Pathgrid::Point& point) { std::transform(path.begin(), path.end(), out, [&](ESM::Pathgrid::Point& point) {
converter.toWorld(point); converter.toWorld(point);
return makeOsgVec3(point); return Misc::Convert::makeOsgVec3f(point);
}); });
} }
@ -359,26 +360,16 @@ namespace MWMechanics
mConstructed = true; mConstructed = true;
} }
void PathFinder::buildPathByPathgrid(const osg::Vec3f& startPoint, const osg::Vec3f& endPoint,
const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph)
{
mPath.clear();
mCell = cell;
buildPathByPathgridImpl(startPoint, endPoint, pathgridGraph, std::back_inserter(mPath));
mConstructed = !mPath.empty();
}
void PathFinder::buildPathByNavMesh(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, void PathFinder::buildPathByNavMesh(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint,
const osg::Vec3f& endPoint, const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, const osg::Vec3f& endPoint, const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags,
const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType) const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType,
std::span<const osg::Vec3f> checkpoints)
{ {
mPath.clear(); mPath.clear();
// If it's not possible to build path over navmesh due to disabled navmesh generation fallback to straight path // If it's not possible to build path over navmesh due to disabled navmesh generation fallback to straight path
DetourNavigator::Status status = buildPathByNavigatorImpl(actor, startPoint, endPoint, agentBounds, flags, DetourNavigator::Status status = buildPathByNavigatorImpl(actor, startPoint, endPoint, agentBounds, flags,
areaCosts, endTolerance, pathType, std::back_inserter(mPath)); areaCosts, endTolerance, pathType, checkpoints, std::back_inserter(mPath));
if (status != DetourNavigator::Status::Success) if (status != DetourNavigator::Status::Success)
mPath.clear(); mPath.clear();
@ -390,19 +381,19 @@ namespace MWMechanics
} }
void PathFinder::buildPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, void PathFinder::buildPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint,
const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph, const PathgridGraph& pathgridGraph, const DetourNavigator::AgentBounds& agentBounds,
const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance,
const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType) PathType pathType, std::span<const osg::Vec3f> checkpoints)
{ {
mPath.clear(); mPath.clear();
mCell = cell; mCell = actor.getCell();
DetourNavigator::Status status = DetourNavigator::Status::NavMeshNotFound; DetourNavigator::Status status = DetourNavigator::Status::NavMeshNotFound;
if (!actor.getClass().isPureWaterCreature(actor) && !actor.getClass().isPureFlyingCreature(actor)) if (!actor.getClass().isPureWaterCreature(actor) && !actor.getClass().isPureFlyingCreature(actor))
{ {
status = buildPathByNavigatorImpl(actor, startPoint, endPoint, agentBounds, flags, areaCosts, endTolerance, status = buildPathByNavigatorImpl(actor, startPoint, endPoint, agentBounds, flags, areaCosts, endTolerance,
pathType, std::back_inserter(mPath)); pathType, checkpoints, std::back_inserter(mPath));
if (status != DetourNavigator::Status::Success) if (status != DetourNavigator::Status::Success)
mPath.clear(); mPath.clear();
} }
@ -411,7 +402,7 @@ namespace MWMechanics
&& (flags & DetourNavigator::Flag_usePathgrid) == 0) && (flags & DetourNavigator::Flag_usePathgrid) == 0)
{ {
status = buildPathByNavigatorImpl(actor, startPoint, endPoint, agentBounds, status = buildPathByNavigatorImpl(actor, startPoint, endPoint, agentBounds,
flags | DetourNavigator::Flag_usePathgrid, areaCosts, endTolerance, pathType, flags | DetourNavigator::Flag_usePathgrid, areaCosts, endTolerance, pathType, checkpoints,
std::back_inserter(mPath)); std::back_inserter(mPath));
if (status != DetourNavigator::Status::Success) if (status != DetourNavigator::Status::Success)
mPath.clear(); mPath.clear();
@ -429,12 +420,13 @@ namespace MWMechanics
DetourNavigator::Status PathFinder::buildPathByNavigatorImpl(const MWWorld::ConstPtr& actor, DetourNavigator::Status PathFinder::buildPathByNavigatorImpl(const MWWorld::ConstPtr& actor,
const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const DetourNavigator::AgentBounds& agentBounds, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const DetourNavigator::AgentBounds& agentBounds,
const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance, const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance,
PathType pathType, std::back_insert_iterator<std::deque<osg::Vec3f>> out) PathType pathType, std::span<const osg::Vec3f> checkpoints,
std::back_insert_iterator<std::deque<osg::Vec3f>> out)
{ {
const auto world = MWBase::Environment::get().getWorld(); const MWBase::World& world = *MWBase::Environment::get().getWorld();
const auto navigator = world->getNavigator(); const DetourNavigator::Navigator& navigator = *world.getNavigator();
const auto status = DetourNavigator::findPath( const DetourNavigator::Status status = DetourNavigator::findPath(
*navigator, agentBounds, startPoint, endPoint, flags, areaCosts, endTolerance, out); navigator, agentBounds, startPoint, endPoint, flags, areaCosts, endTolerance, checkpoints, out);
if (pathType == PathType::Partial && status == DetourNavigator::Status::PartialPath) if (pathType == PathType::Partial && status == DetourNavigator::Status::PartialPath)
return DetourNavigator::Status::Success; return DetourNavigator::Status::Success;
@ -451,9 +443,9 @@ namespace MWMechanics
} }
void PathFinder::buildLimitedPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, void PathFinder::buildLimitedPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint,
const osg::Vec3f& endPoint, const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph, const osg::Vec3f& endPoint, const PathgridGraph& pathgridGraph, const DetourNavigator::AgentBounds& agentBounds,
const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance,
const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType) PathType pathType)
{ {
const auto navigator = MWBase::Environment::get().getWorld()->getNavigator(); const auto navigator = MWBase::Environment::get().getWorld()->getNavigator();
const auto maxDistance const auto maxDistance
@ -461,9 +453,9 @@ namespace MWMechanics
const auto startToEnd = endPoint - startPoint; const auto startToEnd = endPoint - startPoint;
const auto distance = startToEnd.length(); const auto distance = startToEnd.length();
if (distance <= maxDistance) if (distance <= maxDistance)
return buildPath(actor, startPoint, endPoint, cell, pathgridGraph, agentBounds, flags, areaCosts, return buildPath(
endTolerance, pathType); actor, startPoint, endPoint, pathgridGraph, agentBounds, flags, areaCosts, endTolerance, pathType);
const auto end = startPoint + startToEnd * maxDistance / distance; const auto end = startPoint + startToEnd * maxDistance / distance;
buildPath(actor, startPoint, end, cell, pathgridGraph, agentBounds, flags, areaCosts, endTolerance, pathType); buildPath(actor, startPoint, end, pathgridGraph, agentBounds, flags, areaCosts, endTolerance, pathType);
} }
} }

View File

@ -4,12 +4,13 @@
#include <cassert> #include <cassert>
#include <deque> #include <deque>
#include <iterator> #include <iterator>
#include <span>
#include <osg/Vec3f>
#include <components/detournavigator/areatype.hpp> #include <components/detournavigator/areatype.hpp>
#include <components/detournavigator/flags.hpp> #include <components/detournavigator/flags.hpp>
#include <components/detournavigator/status.hpp> #include <components/detournavigator/status.hpp>
#include <components/esm/position.hpp>
#include <components/esm3/loadpgrd.hpp>
namespace MWWorld namespace MWWorld
{ {
@ -102,23 +103,20 @@ namespace MWMechanics
void buildStraightPath(const osg::Vec3f& endPoint); void buildStraightPath(const osg::Vec3f& endPoint);
void buildPathByPathgrid(const osg::Vec3f& startPoint, const osg::Vec3f& endPoint,
const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph);
void buildPathByNavMesh(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, void buildPathByNavMesh(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint,
const osg::Vec3f& endPoint, const DetourNavigator::AgentBounds& agentBounds, const osg::Vec3f& endPoint, const DetourNavigator::AgentBounds& agentBounds,
const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance, const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance,
PathType pathType); PathType pathType, std::span<const osg::Vec3f> checkpoints = {});
void buildPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, void buildPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint,
const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph, const PathgridGraph& pathgridGraph, const DetourNavigator::AgentBounds& agentBounds,
const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance,
const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType); PathType pathType, std::span<const osg::Vec3f> checkpoints = {});
void buildLimitedPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, void buildLimitedPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint,
const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph, const PathgridGraph& pathgridGraph, const DetourNavigator::AgentBounds& agentBounds,
const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags, const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance,
const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType); PathType pathType);
/// Remove front point if exist and within tolerance /// Remove front point if exist and within tolerance
void update(const osg::Vec3f& position, float pointTolerance, float destinationTolerance, void update(const osg::Vec3f& position, float pointTolerance, float destinationTolerance,
@ -145,61 +143,6 @@ namespace MWMechanics
mPath.push_back(point); mPath.push_back(point);
} }
/// utility function to convert a osg::Vec3f to a Pathgrid::Point
static ESM::Pathgrid::Point makePathgridPoint(const osg::Vec3f& v)
{
return ESM::Pathgrid::Point(static_cast<int>(v[0]), static_cast<int>(v[1]), static_cast<int>(v[2]));
}
/// utility function to convert an ESM::Position to a Pathgrid::Point
static ESM::Pathgrid::Point makePathgridPoint(const ESM::Position& p)
{
return ESM::Pathgrid::Point(
static_cast<int>(p.pos[0]), static_cast<int>(p.pos[1]), static_cast<int>(p.pos[2]));
}
static osg::Vec3f makeOsgVec3(const ESM::Pathgrid::Point& p)
{
return osg::Vec3f(static_cast<float>(p.mX), static_cast<float>(p.mY), static_cast<float>(p.mZ));
}
// Slightly cheaper version for comparisons.
// Caller needs to be careful for very short distances (i.e. less than 1)
// or when accumuating the results i.e. (a + b)^2 != a^2 + b^2
//
static float distanceSquared(const ESM::Pathgrid::Point& point, const osg::Vec3f& pos)
{
return (MWMechanics::PathFinder::makeOsgVec3(point) - pos).length2();
}
// Return the closest pathgrid point index from the specified position
// coordinates. NOTE: Does not check if there is a sensible way to get there
// (e.g. a cliff in front).
//
// NOTE: pos is expected to be in local coordinates, as is grid->mPoints
//
static size_t getClosestPoint(const ESM::Pathgrid* grid, const osg::Vec3f& pos)
{
assert(grid && !grid->mPoints.empty());
float distanceBetween = distanceSquared(grid->mPoints[0], pos);
size_t closestIndex = 0;
// TODO: if this full scan causes performance problems mapping pathgrid
// points to a quadtree may help
for (size_t counter = 1; counter < grid->mPoints.size(); counter++)
{
float potentialDistBetween = distanceSquared(grid->mPoints[counter], pos);
if (potentialDistBetween < distanceBetween)
{
distanceBetween = potentialDistBetween;
closestIndex = counter;
}
}
return closestIndex;
}
private: private:
bool mConstructed = false; bool mConstructed = false;
std::deque<osg::Vec3f> mPath; std::deque<osg::Vec3f> mPath;
@ -211,7 +154,8 @@ namespace MWMechanics
[[nodiscard]] DetourNavigator::Status buildPathByNavigatorImpl(const MWWorld::ConstPtr& actor, [[nodiscard]] DetourNavigator::Status buildPathByNavigatorImpl(const MWWorld::ConstPtr& actor,
const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const DetourNavigator::AgentBounds& agentBounds, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const DetourNavigator::AgentBounds& agentBounds,
const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance, const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance,
PathType pathType, std::back_insert_iterator<std::deque<osg::Vec3f>> out); PathType pathType, std::span<const osg::Vec3f> checkpoints,
std::back_insert_iterator<std::deque<osg::Vec3f>> out);
}; };
} }

View File

@ -10,7 +10,7 @@
namespace MWPhysics namespace MWPhysics
{ {
// https://developer.mozilla.org/en-US/docs/Games/Techniques/3D_collision_detection // https://developer.mozilla.org/en-US/docs/Games/Techniques/3D_collision_detection
bool testAabbAgainstSphere( inline bool testAabbAgainstSphere(
const btVector3& aabbMin, const btVector3& aabbMax, const btVector3& position, const btScalar radius) const btVector3& aabbMin, const btVector3& aabbMax, const btVector3& position, const btScalar radius)
{ {
const btVector3 nearest(std::clamp(position.x(), aabbMin.x(), aabbMax.x()), const btVector3 nearest(std::clamp(position.x(), aabbMin.x(), aabbMax.x()),
@ -18,35 +18,28 @@ namespace MWPhysics
return nearest.distance(position) < radius; return nearest.distance(position) < radius;
} }
template <class Ignore, class OnCollision>
class HasSphereCollisionCallback final : public btBroadphaseAabbCallback class HasSphereCollisionCallback final : public btBroadphaseAabbCallback
{ {
public: public:
HasSphereCollisionCallback(const btVector3& position, const btScalar radius, const int mask, const int group, explicit HasSphereCollisionCallback(const btVector3& position, const btScalar radius, const int mask,
const Ignore& ignore, OnCollision* onCollision) const int group, const btCollisionObject* ignore)
: mPosition(position) : mPosition(position)
, mRadius(radius) , mRadius(radius)
, mIgnore(ignore) , mIgnore(ignore)
, mCollisionFilterMask(mask) , mCollisionFilterMask(mask)
, mCollisionFilterGroup(group) , mCollisionFilterGroup(group)
, mOnCollision(onCollision)
{ {
} }
bool process(const btBroadphaseProxy* proxy) override bool process(const btBroadphaseProxy* proxy) override
{ {
if (mResult && mOnCollision == nullptr) if (mResult)
return false; return false;
const auto collisionObject = static_cast<btCollisionObject*>(proxy->m_clientObject); const auto collisionObject = static_cast<btCollisionObject*>(proxy->m_clientObject);
if (mIgnore(collisionObject) || !needsCollision(*proxy) if (mIgnore == collisionObject || !needsCollision(*proxy)
|| !testAabbAgainstSphere(proxy->m_aabbMin, proxy->m_aabbMax, mPosition, mRadius)) || !testAabbAgainstSphere(proxy->m_aabbMin, proxy->m_aabbMax, mPosition, mRadius))
return true; return true;
mResult = true; mResult = true;
if (mOnCollision != nullptr)
{
(*mOnCollision)(collisionObject);
return true;
}
return !mResult; return !mResult;
} }
@ -55,10 +48,9 @@ namespace MWPhysics
private: private:
btVector3 mPosition; btVector3 mPosition;
btScalar mRadius; btScalar mRadius;
Ignore mIgnore; const btCollisionObject* mIgnore;
int mCollisionFilterMask; int mCollisionFilterMask;
int mCollisionFilterGroup; int mCollisionFilterGroup;
OnCollision* mOnCollision;
bool mResult = false; bool mResult = false;
bool needsCollision(const btBroadphaseProxy& proxy) const bool needsCollision(const btBroadphaseProxy& proxy) const

View File

@ -849,36 +849,18 @@ namespace MWPhysics
mWaterCollisionObject.get(), CollisionType_Water, CollisionType_Actor | CollisionType_Projectile); mWaterCollisionObject.get(), CollisionType_Water, CollisionType_Actor | CollisionType_Projectile);
} }
bool PhysicsSystem::isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, bool PhysicsSystem::isAreaOccupiedByOtherActor(
std::span<const MWWorld::ConstPtr> ignore, std::vector<MWWorld::Ptr>* occupyingActors) const const MWWorld::LiveCellRefBase* actor, const osg::Vec3f& position, const float radius) const
{ {
std::vector<const btCollisionObject*> ignoredObjects; const btCollisionObject* ignoredObject = nullptr;
ignoredObjects.reserve(ignore.size()); if (const auto it = mActors.find(actor); it != mActors.end())
for (const auto& v : ignore) ignoredObject = it->second->getCollisionObject();
if (const auto it = mActors.find(v.mRef); it != mActors.end()) const btVector3 bulletPosition = Misc::Convert::toBullet(position);
ignoredObjects.push_back(it->second->getCollisionObject()); const btVector3 aabbMin = bulletPosition - btVector3(radius, radius, radius);
std::sort(ignoredObjects.begin(), ignoredObjects.end()); const btVector3 aabbMax = bulletPosition + btVector3(radius, radius, radius);
ignoredObjects.erase(std::unique(ignoredObjects.begin(), ignoredObjects.end()), ignoredObjects.end());
const auto ignoreFilter = [&](const btCollisionObject* v) {
return std::binary_search(ignoredObjects.begin(), ignoredObjects.end(), v);
};
const auto bulletPosition = Misc::Convert::toBullet(position);
const auto aabbMin = bulletPosition - btVector3(radius, radius, radius);
const auto aabbMax = bulletPosition + btVector3(radius, radius, radius);
const int mask = MWPhysics::CollisionType_Actor; const int mask = MWPhysics::CollisionType_Actor;
const int group = MWPhysics::CollisionType_AnyPhysical; const int group = MWPhysics::CollisionType_AnyPhysical;
if (occupyingActors == nullptr) HasSphereCollisionCallback callback(bulletPosition, radius, mask, group, ignoredObject);
{
HasSphereCollisionCallback callback(bulletPosition, radius, mask, group, ignoreFilter,
static_cast<void (*)(const btCollisionObject*)>(nullptr));
mTaskScheduler->aabbTest(aabbMin, aabbMax, callback);
return callback.getResult();
}
const auto onCollision = [&](const btCollisionObject* object) {
if (PtrHolder* holder = static_cast<PtrHolder*>(object->getUserPointer()))
occupyingActors->push_back(holder->getPtr());
};
HasSphereCollisionCallback callback(bulletPosition, radius, mask, group, ignoreFilter, &onCollision);
mTaskScheduler->aabbTest(aabbMin, aabbMax, callback); mTaskScheduler->aabbTest(aabbMin, aabbMax, callback);
return callback.getResult(); return callback.getResult();
} }

View File

@ -281,8 +281,8 @@ namespace MWPhysics
std::for_each(mAnimatedObjects.begin(), mAnimatedObjects.end(), function); std::for_each(mAnimatedObjects.begin(), mAnimatedObjects.end(), function);
} }
bool isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, bool isAreaOccupiedByOtherActor(
std::span<const MWWorld::ConstPtr> ignore, std::vector<MWWorld::Ptr>* occupyingActors) const; const MWWorld::LiveCellRefBase* actor, const osg::Vec3f& position, float radius) const;
void reportStats(unsigned int frameNumber, osg::Stats& stats) const; void reportStats(unsigned int frameNumber, osg::Stats& stats) const;
void reportCollision(const btVector3& position, const btVector3& normal); void reportCollision(const btVector3& position, const btVector3& normal);

View File

@ -3871,10 +3871,11 @@ namespace MWWorld
return btRayAabb(localFrom, localTo, aabbMin, aabbMax, hitDistance, hitNormal); return btRayAabb(localFrom, localTo, aabbMin, aabbMax, hitDistance, hitNormal);
} }
bool World::isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, bool World::isAreaOccupiedByOtherActor(const MWWorld::ConstPtr& actor, const osg::Vec3f& position) const
std::span<const MWWorld::ConstPtr> ignore, std::vector<MWWorld::Ptr>* occupyingActors) const
{ {
return mPhysics->isAreaOccupiedByOtherActor(position, radius, ignore, occupyingActors); const osg::Vec3f halfExtents = getPathfindingAgentBounds(actor).mHalfExtents;
const float maxHalfExtent = std::max(halfExtents.x(), std::max(halfExtents.y(), halfExtents.z()));
return mPhysics->isAreaOccupiedByOtherActor(actor.mRef, position, 2 * maxHalfExtent);
} }
void World::reportStats(unsigned int frameNumber, osg::Stats& stats) const void World::reportStats(unsigned int frameNumber, osg::Stats& stats) const

View File

@ -664,8 +664,7 @@ namespace MWWorld
bool hasCollisionWithDoor( bool hasCollisionWithDoor(
const MWWorld::ConstPtr& door, const osg::Vec3f& position, const osg::Vec3f& destination) const override; const MWWorld::ConstPtr& door, const osg::Vec3f& position, const osg::Vec3f& destination) const override;
bool isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius, bool isAreaOccupiedByOtherActor(const MWWorld::ConstPtr& actor, const osg::Vec3f& position) const override;
std::span<const MWWorld::ConstPtr> ignore, std::vector<MWWorld::Ptr>* occupyingActors) const override;
void reportStats(unsigned int frameNumber, osg::Stats& stats) const override; void reportStats(unsigned int frameNumber, osg::Stats& stats) const override;

View File

@ -13,7 +13,6 @@
#include <osg/Vec3f> #include <osg/Vec3f>
#include <array>
#include <cassert> #include <cassert>
#include <functional> #include <functional>
#include <iterator> #include <iterator>
@ -64,6 +63,25 @@ namespace DetourNavigator
std::reference_wrapper<const RecastSettings> mSettings; std::reference_wrapper<const RecastSettings> mSettings;
}; };
template <class T>
class ToNavMeshCoordinatesSpan
{
public:
explicit ToNavMeshCoordinatesSpan(std::span<T> span, const RecastSettings& settings)
: mSpan(span)
, mSettings(settings)
{
}
std::size_t size() const noexcept { return mSpan.size(); }
T operator[](std::size_t i) const noexcept { return toNavMeshCoordinates(mSettings, mSpan[i]); }
private:
std::span<T> mSpan;
const RecastSettings& mSettings;
};
inline std::optional<std::size_t> findPolygonPath(const dtNavMeshQuery& navMeshQuery, const dtPolyRef startRef, inline std::optional<std::size_t> findPolygonPath(const dtNavMeshQuery& navMeshQuery, const dtPolyRef startRef,
const dtPolyRef endRef, const osg::Vec3f& startPos, const osg::Vec3f& endPos, const dtQueryFilter& queryFilter, const dtPolyRef endRef, const osg::Vec3f& startPos, const osg::Vec3f& endPos, const dtQueryFilter& queryFilter,
std::span<dtPolyRef> pathBuffer) std::span<dtPolyRef> pathBuffer)
@ -79,7 +97,7 @@ namespace DetourNavigator
} }
Status makeSmoothPath(const dtNavMeshQuery& navMeshQuery, const osg::Vec3f& start, const osg::Vec3f& end, Status makeSmoothPath(const dtNavMeshQuery& navMeshQuery, const osg::Vec3f& start, const osg::Vec3f& end,
std::span<dtPolyRef> polygonPath, std::size_t polygonPathSize, std::size_t maxSmoothPathSize, std::span<dtPolyRef> polygonPath, std::size_t polygonPathSize, std::size_t maxSmoothPathSize, bool skipFirst,
std::output_iterator<osg::Vec3f> auto& out) std::output_iterator<osg::Vec3f> auto& out)
{ {
assert(polygonPathSize <= polygonPath.size()); assert(polygonPathSize <= polygonPath.size());
@ -95,7 +113,7 @@ namespace DetourNavigator
dtStatusFailed(status)) dtStatusFailed(status))
return Status::FindStraightPathFailed; return Status::FindStraightPathFailed;
for (int i = 0; i < cornersCount; ++i) for (int i = skipFirst ? 1 : 0; i < cornersCount; ++i)
*out++ = Misc::Convert::makeOsgVec3f(&cornerVertsBuffer[static_cast<std::size_t>(i) * 3]); *out++ = Misc::Convert::makeOsgVec3f(&cornerVertsBuffer[static_cast<std::size_t>(i) * 3]);
return Status::Success; return Status::Success;
@ -103,7 +121,8 @@ namespace DetourNavigator
Status findSmoothPath(const dtNavMeshQuery& navMeshQuery, const osg::Vec3f& halfExtents, const osg::Vec3f& start, Status findSmoothPath(const dtNavMeshQuery& navMeshQuery, const osg::Vec3f& halfExtents, const osg::Vec3f& start,
const osg::Vec3f& end, const Flags includeFlags, const AreaCosts& areaCosts, const DetourSettings& settings, const osg::Vec3f& end, const Flags includeFlags, const AreaCosts& areaCosts, const DetourSettings& settings,
float endTolerance, std::output_iterator<osg::Vec3f> auto out) float endTolerance, const ToNavMeshCoordinatesSpan<const osg::Vec3f>& checkpoints,
std::output_iterator<osg::Vec3f> auto out)
{ {
dtQueryFilter queryFilter; dtQueryFilter queryFilter;
queryFilter.setIncludeFlags(includeFlags); queryFilter.setIncludeFlags(includeFlags);
@ -131,29 +150,66 @@ namespace DetourNavigator
return Status::EndPolygonNotFound; return Status::EndPolygonNotFound;
std::vector<dtPolyRef> polygonPath(settings.mMaxPolygonPathSize); std::vector<dtPolyRef> polygonPath(settings.mMaxPolygonPathSize);
const auto polygonPathSize std::span<dtPolyRef> polygonPathBuffer = polygonPath;
= findPolygonPath(navMeshQuery, startRef, endRef, startNavMeshPos, endNavMeshPos, queryFilter, polygonPath); dtPolyRef currentRef = startRef;
osg::Vec3f currentNavMeshPos = startNavMeshPos;
bool skipFirst = false;
if (!polygonPathSize.has_value()) for (std::size_t i = 0; i < checkpoints.size(); ++i)
return Status::FindPathOverPolygonsFailed; {
const osg::Vec3f checkpointPos = checkpoints[i];
osg::Vec3f checkpointNavMeshPos;
dtPolyRef checkpointRef;
if (const dtStatus status = navMeshQuery.findNearestPoly(checkpointPos.ptr(), polyHalfExtents.ptr(),
&queryFilter, &checkpointRef, checkpointNavMeshPos.ptr());
dtStatusFailed(status) || checkpointRef == 0)
continue;
if (*polygonPathSize == 0) const std::optional<std::size_t> toCheckpointPathSize = findPolygonPath(navMeshQuery, currentRef,
return Status::Success; checkpointRef, currentNavMeshPos, checkpointNavMeshPos, queryFilter, polygonPath);
osg::Vec3f targetNavMeshPos; if (!toCheckpointPathSize.has_value())
if (const dtStatus status = navMeshQuery.closestPointOnPoly( continue;
polygonPath[*polygonPathSize - 1], end.ptr(), targetNavMeshPos.ptr(), nullptr);
dtStatusFailed(status))
return Status::TargetPolygonNotFound;
const bool partialPath = polygonPath[*polygonPathSize - 1] != endRef; if (*toCheckpointPathSize == 0)
const Status smoothStatus = makeSmoothPath(navMeshQuery, startNavMeshPos, targetNavMeshPos, polygonPath, continue;
*polygonPathSize, settings.mMaxSmoothPathSize, out);
if (polygonPath[*toCheckpointPathSize - 1] != checkpointRef)
continue;
const Status smoothStatus = makeSmoothPath(navMeshQuery, currentNavMeshPos, checkpointNavMeshPos,
polygonPath, *toCheckpointPathSize, settings.mMaxSmoothPathSize, skipFirst, out);
if (smoothStatus != Status::Success) if (smoothStatus != Status::Success)
return smoothStatus; return smoothStatus;
return partialPath ? Status::PartialPath : Status::Success; currentRef = checkpointRef;
currentNavMeshPos = checkpointNavMeshPos;
skipFirst = true;
}
const std::optional<std::size_t> toEndPathSize = findPolygonPath(
navMeshQuery, currentRef, endRef, currentNavMeshPos, endNavMeshPos, queryFilter, polygonPathBuffer);
if (!toEndPathSize.has_value())
return Status::FindPathOverPolygonsFailed;
if (*toEndPathSize == 0)
return currentRef == endRef ? Status::Success : Status::PartialPath;
osg::Vec3f targetNavMeshPos;
if (const dtStatus status = navMeshQuery.closestPointOnPoly(
polygonPath[*toEndPathSize - 1], end.ptr(), targetNavMeshPos.ptr(), nullptr);
dtStatusFailed(status))
return Status::TargetPolygonNotFound;
const Status smoothStatus = makeSmoothPath(navMeshQuery, currentNavMeshPos, targetNavMeshPos, polygonPath,
*toEndPathSize, settings.mMaxSmoothPathSize, skipFirst, out);
if (smoothStatus != Status::Success)
return smoothStatus;
return polygonPath[*toEndPathSize - 1] == endRef ? Status::Success : Status::PartialPath;
} }
} }

View File

@ -11,6 +11,7 @@
#include <iterator> #include <iterator>
#include <optional> #include <optional>
#include <span>
namespace DetourNavigator namespace DetourNavigator
{ {
@ -21,13 +22,13 @@ namespace DetourNavigator
* @param end path at given point. * @param end path at given point.
* @param includeFlags setup allowed navmesh areas. * @param includeFlags setup allowed navmesh areas.
* @param out the beginning of the destination range. * @param out the beginning of the destination range.
* @param endTolerance defines maximum allowed distance to end path point in addition to agentHalfExtents * @param endTolerance defines maximum allowed distance to end path point in addition to agentHalfExtents.
* @param checkpoints is a sequence of positions the path should go over if possible.
* @return Status. * @return Status.
* Equal to out if no path is found.
*/ */
inline Status findPath(const Navigator& navigator, const AgentBounds& agentBounds, const osg::Vec3f& start, inline Status findPath(const Navigator& navigator, const AgentBounds& agentBounds, const osg::Vec3f& start,
const osg::Vec3f& end, const Flags includeFlags, const AreaCosts& areaCosts, float endTolerance, const osg::Vec3f& end, const Flags includeFlags, const AreaCosts& areaCosts, float endTolerance,
std::output_iterator<osg::Vec3f> auto out) std::span<const osg::Vec3f> checkpoints, std::output_iterator<osg::Vec3f> auto out)
{ {
const auto navMesh = navigator.getNavMesh(agentBounds); const auto navMesh = navigator.getNavMesh(agentBounds);
if (navMesh == nullptr) if (navMesh == nullptr)
@ -37,7 +38,8 @@ namespace DetourNavigator
const auto locked = navMesh->lock(); const auto locked = navMesh->lock();
return findSmoothPath(locked->getQuery(), toNavMeshCoordinates(settings.mRecast, agentBounds.mHalfExtents), return findSmoothPath(locked->getQuery(), toNavMeshCoordinates(settings.mRecast, agentBounds.mHalfExtents),
toNavMeshCoordinates(settings.mRecast, start), toNavMeshCoordinates(settings.mRecast, end), includeFlags, toNavMeshCoordinates(settings.mRecast, start), toNavMeshCoordinates(settings.mRecast, end), includeFlags,
areaCosts, settings.mDetour, endTolerance, outTransform); areaCosts, settings.mDetour, endTolerance, ToNavMeshCoordinatesSpan(checkpoints, settings.mRecast),
outTransform);
} }
/** /**

View File

@ -59,10 +59,18 @@ namespace Misc
point.y() -= static_cast<float>(mCellY); point.y() -= static_cast<float>(mCellY);
} }
osg::Vec3f toWorldVec3(const osg::Vec3f& point) const
{
osg::Vec3f result = point;
toWorld(result);
return result;
}
osg::Vec3f toLocalVec3(const osg::Vec3f& point) const osg::Vec3f toLocalVec3(const osg::Vec3f& point) const
{ {
return osg::Vec3f( osg::Vec3f result = point;
point.x() - static_cast<float>(mCellX), point.y() - static_cast<float>(mCellY), point.z()); toLocal(result);
return result;
} }
private: private:

View File

@ -0,0 +1,53 @@
#ifndef OPENMW_COMPONENTS_MISC_PATHGRIDUTILS_H
#define OPENMW_COMPONENTS_MISC_PATHGRIDUTILS_H
#include "convert.hpp"
#include <components/esm3/loadpgrd.hpp>
#include <osg/Vec3f>
#include <stdexcept>
namespace Misc
{
// Slightly cheaper version for comparisons.
// Caller needs to be careful for very short distances (i.e. less than 1)
// or when accumuating the results i.e. (a + b)^2 != a^2 + b^2
//
inline float distanceSquared(const ESM::Pathgrid::Point& point, const osg::Vec3f& pos)
{
return (Misc::Convert::makeOsgVec3f(point) - pos).length2();
}
// Return the closest pathgrid point index from the specified position
// coordinates. NOTE: Does not check if there is a sensible way to get there
// (e.g. a cliff in front).
//
// NOTE: pos is expected to be in local coordinates, as is grid->mPoints
//
inline std::size_t getClosestPoint(const ESM::Pathgrid& grid, const osg::Vec3f& pos)
{
if (grid.mPoints.empty())
throw std::invalid_argument("Pathgrid has no points");
float minDistance = distanceSquared(grid.mPoints[0], pos);
std::size_t closestIndex = 0;
// TODO: if this full scan causes performance problems mapping pathgrid
// points to a quadtree may help
for (std::size_t i = 1; i < grid.mPoints.size(); ++i)
{
const float distance = distanceSquared(grid.mPoints[i], pos);
if (minDistance > distance)
{
minDistance = distance;
closestIndex = i;
}
}
return closestIndex;
}
}
#endif

View File

@ -180,6 +180,7 @@
-- @field [parent=#FindPathOptions] #AreaCosts areaCosts a table defining relative cost for each type of area. -- @field [parent=#FindPathOptions] #AreaCosts areaCosts a table defining relative cost for each type of area.
-- @field [parent=#FindPathOptions] #number destinationTolerance a floating point number representing maximum allowed -- @field [parent=#FindPathOptions] #number destinationTolerance a floating point number representing maximum allowed
-- distance between destination and a nearest point on the navigation mesh in addition to agent size (default: 1). -- distance between destination and a nearest point on the navigation mesh in addition to agent size (default: 1).
-- @field [parent=#FindPathOptions] #table checkpoints an array of positions to build path over if possible.
--- ---
-- A table of parameters for @{#nearby.findRandomPointAroundCircle} and @{#nearby.castNavigationRay} -- A table of parameters for @{#nearby.findRandomPointAroundCircle} and @{#nearby.castNavigationRay}

View File

@ -315,6 +315,7 @@ registerPlayerTest('player rotation')
registerPlayerTest('player forward running') registerPlayerTest('player forward running')
registerPlayerTest('player diagonal walking') registerPlayerTest('player diagonal walking')
registerPlayerTest('findPath') registerPlayerTest('findPath')
registerPlayerTest('findPath with checkpoints')
registerPlayerTest('findRandomPointAroundCircle') registerPlayerTest('findRandomPointAroundCircle')
registerPlayerTest('castNavigationRay') registerPlayerTest('castNavigationRay')
registerPlayerTest('findNearestNavMeshPosition') registerPlayerTest('findNearestNavMeshPosition')

View File

@ -82,6 +82,7 @@ registerGlobalTest('player rotation', 'rotating player should not lead to nan ro
registerGlobalTest('player forward running') registerGlobalTest('player forward running')
registerGlobalTest('player diagonal walking') registerGlobalTest('player diagonal walking')
registerGlobalTest('findPath') registerGlobalTest('findPath')
registerGlobalTest('findPath with checkpoints')
registerGlobalTest('findRandomPointAroundCircle') registerGlobalTest('findRandomPointAroundCircle')
registerGlobalTest('castNavigationRay') registerGlobalTest('castNavigationRay')
registerGlobalTest('findNearestNavMeshPosition') registerGlobalTest('findNearestNavMeshPosition')

View File

@ -179,6 +179,39 @@ testing.registerLocalTest('findPath',
testing.expectEqual(status, nearby.FIND_PATH_STATUS.Success, 'Status') testing.expectEqual(status, nearby.FIND_PATH_STATUS.Success, 'Status')
testing.expectLessOrEqual((path[#path] - dst):length(), 1, testing.expectLessOrEqual((path[#path] - dst):length(), 1,
'Last path point ' .. testing.formatActualExpected(path[#path], dst)) 'Last path point ' .. testing.formatActualExpected(path[#path], dst))
testing.expectThat(path, matchers.equalTo({
matchers.closeToVector(util.vector3(4096, 4096, 1746.27099609375), 1e-1),
matchers.closeToVector(util.vector3(4500, 4500, 1745.95263671875), 1e-1),
}))
end)
testing.registerLocalTest('findPath with checkpoints',
function()
local src = util.vector3(4096, 4096, 1745)
local dst = util.vector3(4500, 4500, 1745.95263671875)
local options = {
agentBounds = types.Actor.getPathfindingAgentBounds(self),
includeFlags = nearby.NAVIGATOR_FLAGS.Walk + nearby.NAVIGATOR_FLAGS.Swim,
areaCosts = {
water = 1,
door = 2,
ground = 1,
pathgrid = 1,
},
destinationTolerance = 1,
checkpoints = {
util.vector3(4200, 4100, 1750),
},
}
local status, path = nearby.findPath(src, dst, options)
testing.expectEqual(status, nearby.FIND_PATH_STATUS.Success, 'Status')
testing.expectLessOrEqual((path[#path] - dst):length(), 1,
'Last path point ' .. testing.formatActualExpected(path[#path], dst))
testing.expectThat(path, matchers.equalTo({
matchers.closeToVector(util.vector3(4096, 4096, 1746.27099609375), 1e-1),
matchers.closeToVector(util.vector3(4200, 4100, 1749.5076904296875), 1e-1),
matchers.closeToVector(util.vector3(4500, 4500, 1745.95263671875), 1e-1),
}))
end) end)
testing.registerLocalTest('findRandomPointAroundCircle', testing.registerLocalTest('findRandomPointAroundCircle',