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)
{
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);
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)
{
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);
}
@ -156,7 +156,7 @@ namespace
ASSERT_TRUE(mNavigator->addAgent(mAgentBounds));
ASSERT_TRUE(mNavigator->addAgent(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);
}
@ -172,7 +172,7 @@ namespace
updateGuard.reset();
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);
EXPECT_THAT(mPath,
@ -194,7 +194,7 @@ namespace
updateGuard.reset();
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);
EXPECT_THAT(mPath, ElementsAre(Vec3fEq(56.66666412353515625, 460, 1.99998295307159423828125))) << mPath;
@ -218,7 +218,7 @@ namespace
mNavigator->update(mPlayerPosition, nullptr);
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);
EXPECT_THAT(mPath,
@ -237,7 +237,7 @@ namespace
mPath.clear();
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);
EXPECT_THAT(mPath,
@ -265,7 +265,7 @@ namespace
mNavigator->update(mPlayerPosition, nullptr);
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);
EXPECT_THAT(mPath,
@ -285,7 +285,7 @@ namespace
mPath.clear();
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);
EXPECT_THAT(mPath,
@ -318,7 +318,7 @@ namespace
mNavigator->update(mPlayerPosition, nullptr);
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);
EXPECT_THAT(mPath,
@ -386,7 +386,7 @@ namespace
mNavigator->update(mPlayerPosition, nullptr);
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);
EXPECT_THAT(mPath,
@ -421,7 +421,7 @@ namespace
mEnd.x() = 256;
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);
EXPECT_THAT(mPath,
@ -453,8 +453,8 @@ namespace
mStart.x() = 256;
mEnd.x() = 256;
EXPECT_EQ(
findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_swim | Flag_walk, mAreaCosts, mEndTolerance, mOut),
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_swim | Flag_walk, mAreaCosts, mEndTolerance,
{}, mOut),
Status::Success);
EXPECT_THAT(mPath,
@ -487,8 +487,8 @@ namespace
mStart.x() = 256;
mEnd.x() = 256;
EXPECT_EQ(
findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_swim | Flag_walk, mAreaCosts, mEndTolerance, mOut),
EXPECT_EQ(findPath(*mNavigator, mAgentBounds, mStart, mEnd, Flag_swim | Flag_walk, mAreaCosts, mEndTolerance,
{}, mOut),
Status::Success);
EXPECT_THAT(mPath,
@ -520,7 +520,7 @@ namespace
mStart.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);
EXPECT_THAT(mPath,
@ -549,7 +549,7 @@ namespace
mNavigator->update(mPlayerPosition, nullptr);
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);
EXPECT_THAT(mPath,
@ -577,7 +577,7 @@ namespace
mNavigator->update(mPlayerPosition, nullptr);
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);
EXPECT_THAT(mPath,
@ -658,7 +658,7 @@ namespace
mNavigator->update(mPlayerPosition, nullptr);
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);
EXPECT_THAT(mPath,
@ -781,7 +781,7 @@ namespace
mNavigator->update(mPlayerPosition, nullptr);
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);
EXPECT_THAT(mPath,
@ -806,7 +806,7 @@ namespace
mNavigator->update(mPlayerPosition, nullptr);
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);
EXPECT_THAT(mPath,
@ -834,7 +834,7 @@ namespace
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);
EXPECT_THAT(mPath,
@ -979,6 +979,146 @@ namespace
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>
{
};

View File

@ -589,8 +589,7 @@ namespace MWBase
virtual bool hasCollisionWithDoor(
const MWWorld::ConstPtr& door, const osg::Vec3f& position, const osg::Vec3f& destination) const = 0;
virtual bool isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius,
std::span<const MWWorld::ConstPtr> ignore, std::vector<MWWorld::Ptr>* occupyingActors = nullptr) const = 0;
virtual bool isAreaOccupiedByOtherActor(const MWWorld::ConstPtr& actor, const osg::Vec3f& position) 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::AreaCosts areaCosts{};
float destinationTolerance = 1;
std::vector<osg::Vec3f> checkpoints;
if (options.has_value())
{
@ -258,13 +259,24 @@ namespace MWLua
}
if (const auto& v = options->get<sol::optional<float>>("destinationTolerance"))
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;
const DetourNavigator::Status status
= DetourNavigator::findPath(*MWBase::Environment::get().getWorld()->getNavigator(), agentBounds,
source, destination, includeFlags, areaCosts, destinationTolerance, std::back_inserter(path));
const DetourNavigator::Status status = DetourNavigator::findPath(
*MWBase::Environment::get().getWorld()->getNavigator(), agentBounds, source, destination,
includeFlags, areaCosts, destinationTolerance, checkpoints, std::back_inserter(path));
sol::table result(lua, sol::create);
LuaUtil::copyVectorToTable(path, result);

View File

@ -1,13 +1,11 @@
#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/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 "../mwphysics/raycasting.hpp"
@ -303,8 +301,8 @@ namespace MWMechanics
const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags);
const ESM::Pathgrid* pathgrid = world->getStore().get<ESM::Pathgrid>().search(*actor.getCell()->getCell());
const auto& pathGridGraph = getPathGridGraph(pathgrid);
mPathFinder.buildPath(actor, vActorPos, vTargetPos, actor.getCell(), pathGridGraph, agentBounds,
navigatorFlags, areaCosts, storage.mAttackRange, PathType::Full);
mPathFinder.buildPath(actor, vActorPos, vTargetPos, pathGridGraph, agentBounds, navigatorFlags, areaCosts,
storage.mAttackRange, PathType::Full);
if (!mPathFinder.isPathConstructed())
{
@ -317,8 +315,8 @@ namespace MWMechanics
if (hit.has_value() && (*hit - vTargetPos).length() <= rangeAttack)
{
// If the point is close enough, try to find a path to that point.
mPathFinder.buildPath(actor, vActorPos, *hit, actor.getCell(), pathGridGraph, agentBounds,
navigatorFlags, areaCosts, storage.mAttackRange, PathType::Full);
mPathFinder.buildPath(actor, vActorPos, *hit, pathGridGraph, agentBounds, navigatorFlags, areaCosts,
storage.mAttackRange, PathType::Full);
if (mPathFinder.isPathConstructed())
{
// 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();
coords.toLocal(localPos);
size_t closestPointIndex = PathFinder::getClosestPoint(pathgrid, localPos);
for (size_t i = 0; i < pathgrid->mPoints.size(); i++)
const std::size_t closestPointIndex = Misc::getClosestPoint(*pathgrid, localPos);
for (std::size_t i = 0; i < pathgrid->mPoints.size(); i++)
{
if (i != closestPointIndex
&& getPathGridGraph(pathgrid).isPointConnected(closestPointIndex, i))
@ -456,7 +454,8 @@ namespace MWMechanics
float dist
= (actor.getRefData().getPosition().asVec3() - target.getRefData().getPosition().asVec3()).length();
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;
}

View File

@ -2,12 +2,13 @@
#define GAME_MWMECHANICS_AICOMBAT_H
#include "aitemporarybase.hpp"
#include "aitimer.hpp"
#include "movement.hpp"
#include "typedaipackage.hpp"
#include "../mwworld/cellstore.hpp" // for Doors
#include "aitimer.hpp"
#include "movement.hpp"
#include <components/esm3/loadpgrd.hpp>
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());
const DetourNavigator::Flags navigatorFlags = getNavigatorFlags(actor);
const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags);
mPathFinder.buildLimitedPath(actor, position, dest, actor.getCell(), getPathGridGraph(pathgrid),
agentBounds, navigatorFlags, areaCosts, endTolerance, pathType);
mPathFinder.buildLimitedPath(actor, position, dest, getPathGridGraph(pathgrid), agentBounds,
navigatorFlags, areaCosts, endTolerance, pathType);
mRotateOnTheRunChecks = 3;
// 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;
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)
result |= DetourNavigator::Flag_openDoor;

View File

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

View File

@ -8,6 +8,7 @@
#include <components/detournavigator/navigatorutils.hpp>
#include <components/esm3/aisequence.hpp>
#include <components/misc/coordinateconverter.hpp>
#include <components/misc/pathgridutils.hpp>
#include <components/misc/rng.hpp>
#include "../mwbase/environment.hpp"
@ -29,17 +30,6 @@
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] = {
"idle2",
"idle3",
@ -53,11 +43,22 @@ namespace MWMechanics
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)
{
if (actor.getClass().isPureWaterCreature(actor) || actor.getClass().isPureFlyingCreature(actor))
return 1;
return COUNT_BEFORE_RESET;
return countBeforeReset;
}
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> result(MAX_IDLE_SIZE, 0);
std::copy_n(idle.begin(), std::min(MAX_IDLE_SIZE, idle.size()), result.begin());
std::vector<unsigned char> result(maxIdleSize, 0);
std::copy_n(idle.begin(), std::min(maxIdleSize, idle.size()), result.begin());
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));
}
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()
@ -118,9 +145,9 @@ namespace MWMechanics
, mCanWanderAlongPathGrid(true)
, mIdleAnimation(0)
, mBadIdles()
, mPopulateAvailableNodes(true)
, mAllowedNodes()
, mTrimCurrentNode(false)
, mPopulateAvailablePositions(true)
, mAllowedPositions()
, mTrimCurrentPosition(false)
, mCheckIdlePositionTimer(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)
: TypedAiPackage<AiWander>(repeat)
, mDistance(std::max(0, distance))
, mDuration(std::max(0, duration))
, mDistance(static_cast<unsigned>(std::max(0, distance)))
, mDuration(static_cast<unsigned>(std::max(0, duration)))
, mRemainingDuration(duration)
, mTimeOfDay(timeOfDay)
, mIdle(getInitialIdle(idle))
@ -215,20 +242,12 @@ namespace MWMechanics
{
const ESM::Pathgrid* pathgrid
= 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);
constexpr float endTolerance = 0;
const DetourNavigator::Flags navigatorFlags = getNavigatorFlags(actor);
const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags);
mPathFinder.buildPath(actor, pos.asVec3(), mDestination, actor.getCell(), getPathGridGraph(pathgrid),
agentBounds, navigatorFlags, areaCosts, endTolerance, PathType::Full);
}
const auto agentBounds = MWBase::Environment::get().getWorld()->getPathfindingAgentBounds(actor);
constexpr float endTolerance = 0;
const DetourNavigator::Flags navigatorFlags = getNavigatorFlags(actor);
const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags);
mPathFinder.buildPath(actor, pos.asVec3(), mDestination, getPathGridGraph(pathgrid), agentBounds,
navigatorFlags, areaCosts, endTolerance, PathType::Full);
if (mPathFinder.isPathConstructed())
storage.setState(AiWanderStorage::Wander_Walking, !mUsePathgrid);
@ -259,9 +278,6 @@ namespace MWMechanics
bool AiWander::reactionTimeActions(const MWWorld::Ptr& actor, AiWanderStorage& storage, ESM::Position& pos)
{
if (mDistance <= 0)
storage.mCanWanderAlongPathGrid = false;
if (isPackageCompleted())
{
stopWalking(actor);
@ -276,13 +292,15 @@ namespace MWMechanics
mStoredInitialActorPosition = true;
}
// Initialization to discover & store allowed node points for this actor.
if (storage.mPopulateAvailableNodes)
// Initialization to discover & store allowed positions points for this actor.
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)
{
// 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,
// 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
if (Misc::Rng::rollDice(100, prng) >= 96)
@ -307,7 +325,7 @@ namespace MWMechanics
storage.setState(AiWanderStorage::Wander_IdleNow);
}
}
else if (storage.mAllowedNodes.empty() && !storage.mIsWanderingManually)
else if (storage.mAllowedPositions.empty() && !storage.mIsWanderingManually)
{
storage.mCanWanderAlongPathGrid = false;
}
@ -323,9 +341,9 @@ namespace MWMechanics
// Construct a new path if there isn't one
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
&& (mPathFinder.getPathSize() == 0 || isDestinationHidden(actor, mPathFinder.getPath().back())
|| isAreaOccupiedByOtherActor(actor, mPathFinder.getPath().back())))
|| world.isAreaOccupiedByOtherActor(actor, mPathFinder.getPath().back())))
completeManualWalking(actor, storage);
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
const bool isWaterCreature = actor.getClass().isPureWaterCreature(actor);
const bool isFlyingCreature = actor.getClass().isPureFlyingCreature(actor);
const auto world = MWBase::Environment::get().getWorld();
const auto agentBounds = world->getPathfindingAgentBounds(actor);
const auto navigator = world->getNavigator();
MWBase::World& world = *MWBase::Environment::get().getWorld();
const auto agentBounds = world.getPathfindingAgentBounds(actor);
const auto navigator = world.getNavigator();
const DetourNavigator::Flags navigatorFlags = getNavigatorFlags(actor);
const DetourNavigator::AreaCosts areaCosts = getAreaCosts(actor, navigatorFlags);
auto& prng = MWBase::Environment::get().getWorld()->getPrng();
Misc::Rng::Generator& prng = world.getPrng();
do
{
@ -411,7 +429,7 @@ namespace MWMechanics
if (isDestinationHidden(actor, mDestination))
continue;
if (isAreaOccupiedByOtherActor(actor, mDestination))
if (world.isAreaOccupiedByOtherActor(actor, mDestination))
continue;
constexpr float endTolerance = 0;
@ -491,17 +509,17 @@ namespace MWMechanics
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;
if (storage.mCheckIdlePositionTimer >= IDLE_POSITION_CHECK_INTERVAL && !isStationary())
if (storage.mCheckIdlePositionTimer >= idlePositionCheckInterval && !isStationary())
{
storage.mCheckIdlePositionTimer = 0; // restart timer
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.mTrimCurrentNode = false; // just in case
storage.mTrimCurrentPosition = false; // just in case
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();
for (const ESM::Pathgrid::Point& node : storage.mAllowedNodes)
{
osg::Vec3f point(node.mX, node.mY, node.mZ);
if ((actorPos - point).length2() < distance * distance)
return true;
}
return false;
const float squaredDistance = distance * distance;
return std::ranges::find_if(storage.mAllowedPositions, [&](const osg::Vec3& v) {
return (actorPos - v).length2() < squaredDistance;
}) != storage.mAllowedPositions.end();
}
void AiWander::onWalkingStatePerFrameActions(const MWWorld::Ptr& actor, float duration,
@ -535,7 +551,7 @@ namespace MWMechanics
// Is there no destination or are we there yet?
if ((!mPathFinder.isPathConstructed())
|| pathTo(actor, osg::Vec3f(mPathFinder.getPath().back()), duration, supportedMovementDirections,
DESTINATION_TOLERANCE))
destinationTolerance))
{
stopWalking(actor);
storage.setState(AiWanderStorage::Wander_ChooseAction);
@ -586,8 +602,8 @@ namespace MWMechanics
if (proximityToDoor(actor, distance))
{
// remove allowed points then select another random destination
storage.mTrimCurrentNode = true;
trimAllowedNodes(storage.mAllowedNodes, mPathFinder);
storage.mTrimCurrentPosition = true;
trimAllowedPositions(mPathFinder.getPath(), storage.mAllowedPositions);
mObstacleCheck.clear();
stopWalking(actor);
storage.setState(AiWanderStorage::Wander_MoveNow);
@ -606,67 +622,67 @@ namespace MWMechanics
}
}
void AiWander::setPathToAnAllowedNode(
void AiWander::setPathToAnAllowedPosition(
const MWWorld::Ptr& actor, AiWanderStorage& storage, const ESM::Position& actorPos)
{
auto world = MWBase::Environment::get().getWorld();
auto& prng = world->getPrng();
unsigned int randNode = Misc::Rng::rollDice(storage.mAllowedNodes.size(), prng);
const ESM::Pathgrid::Point& dest = storage.mAllowedNodes[randNode];
MWBase::World& world = *MWBase::Environment::get().getWorld();
Misc::Rng::Generator& prng = world.getPrng();
const std::size_t randomAllowedPositionIndex
= 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();
// don't take shortcuts for wandering
const ESM::Pathgrid* pathgrid = world->getStore().get<ESM::Pathgrid>().search(*actor.getCell()->getCell());
const osg::Vec3f destVec3f = PathFinder::makeOsgVec3(dest);
mPathFinder.buildPathByPathgrid(start, destVec3f, actor.getCell(), getPathGridGraph(pathgrid));
const MWWorld::Cell& cell = *actor.getCell()->getCell();
const ESM::Pathgrid* pathgrid = world.getStore().get<ESM::Pathgrid>().search(cell);
const PathgridGraph& pathgridGraph = 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;
mHasDestination = true;
mUsePathgrid = true;
// Remove this node as an option and add back the previously used node (stops NPC from picking the same
// node):
ESM::Pathgrid::Point temp = storage.mAllowedNodes[randNode];
storage.mAllowedNodes.erase(storage.mAllowedNodes.begin() + randNode);
// check if mCurrentNode was taken out of mAllowedNodes
if (storage.mTrimCurrentNode && storage.mAllowedNodes.size() > 1)
storage.mTrimCurrentNode = false;
else
storage.mAllowedNodes.push_back(storage.mCurrentNode);
storage.mCurrentNode = temp;
storage.setState(AiWanderStorage::Wander_Walking);
storage.mAllowedPositions.erase(storage.mAllowedPositions.begin() + randomAllowedPositionIndex);
return;
}
// Choose a different node and delete this one from possible nodes because it is uncreachable:
// 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;
mUsePathgrid = true;
// Remove this position as an option and add back the previously used position (stops NPC from picking the
// same position):
storage.mAllowedPositions.erase(storage.mAllowedPositions.begin() + randomAllowedPositionIndex);
// check if mCurrentPosition was taken out of mAllowedPositions
if (storage.mTrimCurrentPosition && storage.mAllowedPositions.size() > 1)
storage.mTrimCurrentPosition = false;
else
storage.mAllowedNodes.erase(storage.mAllowedNodes.begin() + randNode);
}
storage.mAllowedPositions.push_back(storage.mCurrentPosition);
storage.mCurrentPosition = randomAllowedPosition;
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();
}
storage.setState(AiWanderStorage::Wander_Walking);
}
void AiWander::stopWalking(const MWWorld::Ptr& actor)
@ -742,20 +758,20 @@ namespace MWMechanics
return;
AiWanderStorage& storage = state.get<AiWanderStorage>();
if (storage.mPopulateAvailableNodes)
getAllowedNodes(actor, storage);
if (storage.mPopulateAvailablePositions)
fillAllowedPositions(actor, storage);
if (storage.mAllowedNodes.empty())
if (storage.mAllowedPositions.empty())
return;
auto& prng = MWBase::Environment::get().getWorld()->getPrng();
int index = Misc::Rng::rollDice(storage.mAllowedNodes.size(), prng);
ESM::Pathgrid::Point worldDest = storage.mAllowedNodes[index];
int index = Misc::Rng::rollDice(storage.mAllowedPositions.size(), prng);
const osg::Vec3f worldDest = storage.mAllowedPositions[index];
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(
PathFinder::makeOsgVec3(worldDest), 60);
const bool isPathGridOccupied
= MWBase::Environment::get().getMechanicsManager()->isAnyActorInRange(worldDest, 60);
// add offset only if the selected pathgrid is occupied by another actor
if (isPathGridOccupied)
@ -775,19 +791,17 @@ namespace MWMechanics
const ESM::Pathgrid::Point& connDest = points[randomIndex];
// add an offset towards random neighboring node
osg::Vec3f dir = PathFinder::makeOsgVec3(connDest) - PathFinder::makeOsgVec3(dest);
float length = dir.length();
osg::Vec3f dir = Misc::Convert::makeOsgVec3f(connDest) - dest;
const float length = dir.length();
dir.normalize();
for (int j = 1; j <= 3; j++)
{
// move for 5-15% towards random neighboring node
dest
= PathFinder::makePathgridPoint(PathFinder::makeOsgVec3(dest) + dir * (j * 5 * length / 100.f));
worldDest = converter.toWorldPoint(dest);
dest = dest + dir * (j * 5 * length / 100.f);
isOccupied = MWBase::Environment::get().getMechanicsManager()->isAnyActorInRange(
PathFinder::makeOsgVec3(worldDest), 60);
converter.toWorldVec3(dest), 60);
if (!isOccupied)
break;
@ -807,19 +821,18 @@ namespace MWMechanics
// 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.
dest.mZ += 60;
dest.z() += 60;
converter.toWorld(dest);
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, pos);
MWBase::Environment::get().getWorld()->moveObject(actor, dest);
actor.getClass().adjustPosition(actor, false);
}
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
= MWBase::Environment::get().getESMStore()->get<ESM::Pathgrid>().search(*currentCell->getCell());
@ -827,19 +840,19 @@ namespace MWMechanics
if (pathgrid == nullptr || pathgrid->mPoints.empty())
return;
size_t index = PathFinder::getClosestPoint(pathgrid, PathFinder::makeOsgVec3(dest));
const size_t index = Misc::getClosestPoint(*pathgrid, dest);
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
const MWWorld::CellStore* cellStore = actor.getCell();
const ESM::Pathgrid* pathgrid
= 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:
// https://forum.openmw.org/viewtopic.php?t=1556
@ -860,34 +873,35 @@ namespace MWMechanics
const osg::Vec3f npcPos = converter.toLocalVec3(mInitialActorPosition);
// 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
// NOTE: mPoints is in local coordinates
size_t pointIndex = 0;
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
&& 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;
}
}
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);
}
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,
@ -901,44 +915,44 @@ namespace MWMechanics
{
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);
}
}
}
void AiWander::AddPointBetweenPathGridPoints(
void AiWander::addPositionBetweenPathgridPoints(
const ESM::Pathgrid::Point& start, const ESM::Pathgrid::Point& end, AiWanderStorage& storage)
{
osg::Vec3f vectorStart = PathFinder::makeOsgVec3(start);
osg::Vec3f delta = PathFinder::makeOsgVec3(end) - vectorStart;
osg::Vec3f vectorStart = Misc::Convert::makeOsgVec3f(start);
osg::Vec3f delta = Misc::Convert::makeOsgVec3f(end) - vectorStart;
float length = delta.length();
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
distance = std::min(distance, static_cast<int>(length));
distance = std::min(distance, static_cast<unsigned>(length));
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;
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]));
float tempDist = (mInitialActorPosition - nodePos).length2();
if (tempDist < distanceToClosestNode)
const osg::Vec3f position = storage.mAllowedPositions[i];
const float tempDist = (mInitialActorPosition - position).length2();
if (tempDist < distanceToClosestPosition)
{
index = i;
distanceToClosestNode = tempDist;
distanceToClosestPosition = tempDist;
}
}
storage.mCurrentNode = storage.mAllowedNodes[index];
storage.mAllowedNodes.erase(storage.mAllowedNodes.begin() + index);
storage.mCurrentPosition = storage.mAllowedPositions[index];
storage.mAllowedPositions.erase(storage.mAllowedPositions.begin() + index);
}
void AiWander::writeState(ESM::AiSequence::AiSequence& sequence) const
@ -970,8 +984,8 @@ namespace MWMechanics
AiWander::AiWander(const ESM::AiSequence::AiWander* wander)
: TypedAiPackage<AiWander>(makeDefaultOptions().withRepeat(wander->mData.mShouldRepeat != 0))
, mDistance(std::max(static_cast<short>(0), wander->mData.mDistance))
, mDuration(std::max(static_cast<short>(0), wander->mData.mDuration))
, mDistance(static_cast<unsigned>(std::max(static_cast<short>(0), wander->mData.mDistance)))
, mDuration(static_cast<unsigned>(std::max(static_cast<short>(0), wander->mData.mDuration)))
, mRemainingDuration(wander->mDurationData.mRemainingDuration)
, mTimeOfDay(wander->mData.mTimeOfDay)
, mIdle(getInitialIdle(wander->mData.mIdle))

View File

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

View File

@ -106,21 +106,6 @@ namespace MWMechanics
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()
: mEvadeDirectionIndex(std::size(evadeDirections) - 1)
{

View File

@ -5,8 +5,6 @@
#include <osg/Vec3f>
#include <vector>
namespace MWWorld
{
class Ptr;
@ -24,9 +22,6 @@ namespace MWMechanics
/** \return Pointer to the door, or empty pointer if none exists **/
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
{
public:

View File

@ -10,6 +10,7 @@
#include <components/detournavigator/navigatorutils.hpp>
#include <components/misc/coordinateconverter.hpp>
#include <components/misc/math.hpp>
#include <components/misc/pathgridutils.hpp>
#include "../mwbase/environment.hpp"
#include "../mwbase/world.hpp"
@ -38,7 +39,7 @@ namespace
// points to a quadtree may help
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)
{
// found a closer one
@ -197,7 +198,7 @@ namespace MWMechanics
// point right behind the wall that is closer than any pathgrid
// point outside the wall
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));
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
// start or end to nearest pathgrid point, just travel from start to end.
float startToEndLength2 = (endPointInLocalCoords - startPointInLocalCoords).length2();
float endTolastNodeLength2 = distanceSquared(pathgrid->mPoints[endNode.first], endPointInLocalCoords);
float startTo1stNodeLength2 = distanceSquared(pathgrid->mPoints[startNode], startPointInLocalCoords);
float endTolastNodeLength2 = Misc::distanceSquared(pathgrid->mPoints[endNode.first], endPointInLocalCoords);
float startTo1stNodeLength2 = Misc::distanceSquared(pathgrid->mPoints[startNode], startPointInLocalCoords);
if ((startToEndLength2 < startTo1stNodeLength2) || (startToEndLength2 < endTolastNodeLength2))
{
*out++ = endPoint;
@ -223,7 +224,7 @@ namespace MWMechanics
{
ESM::Pathgrid::Point temp(pathgrid->mPoints[startNode]);
converter.toWorld(temp);
*out++ = makeOsgVec3(temp);
*out++ = Misc::Convert::makeOsgVec3f(temp);
}
else
{
@ -234,8 +235,8 @@ namespace MWMechanics
if (path.size() > 1)
{
ESM::Pathgrid::Point secondNode = *(++path.begin());
osg::Vec3f firstNodeVec3f = makeOsgVec3(pathgrid->mPoints[startNode]);
osg::Vec3f secondNodeVec3f = makeOsgVec3(secondNode);
osg::Vec3f firstNodeVec3f = Misc::Convert::makeOsgVec3f(pathgrid->mPoints[startNode]);
osg::Vec3f secondNodeVec3f = Misc::Convert::makeOsgVec3f(secondNode);
osg::Vec3f toSecondNodeVec3f = secondNodeVec3f - firstNodeVec3f;
osg::Vec3f toStartPointVec3f = startPointInLocalCoords - firstNodeVec3f;
if (toSecondNodeVec3f * toStartPointVec3f > 0)
@ -259,7 +260,7 @@ namespace MWMechanics
// convert supplied path to world coordinates
std::transform(path.begin(), path.end(), out, [&](ESM::Pathgrid::Point& point) {
converter.toWorld(point);
return makeOsgVec3(point);
return Misc::Convert::makeOsgVec3f(point);
});
}
@ -359,26 +360,16 @@ namespace MWMechanics
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,
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();
// 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,
areaCosts, endTolerance, pathType, std::back_inserter(mPath));
areaCosts, endTolerance, pathType, checkpoints, std::back_inserter(mPath));
if (status != DetourNavigator::Status::Success)
mPath.clear();
@ -390,19 +381,19 @@ namespace MWMechanics
}
void PathFinder::buildPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint,
const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph,
const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags,
const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType)
const PathgridGraph& pathgridGraph, const DetourNavigator::AgentBounds& agentBounds,
const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance,
PathType pathType, std::span<const osg::Vec3f> checkpoints)
{
mPath.clear();
mCell = cell;
mCell = actor.getCell();
DetourNavigator::Status status = DetourNavigator::Status::NavMeshNotFound;
if (!actor.getClass().isPureWaterCreature(actor) && !actor.getClass().isPureFlyingCreature(actor))
{
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)
mPath.clear();
}
@ -411,7 +402,7 @@ namespace MWMechanics
&& (flags & DetourNavigator::Flag_usePathgrid) == 0)
{
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));
if (status != DetourNavigator::Status::Success)
mPath.clear();
@ -429,12 +420,13 @@ namespace MWMechanics
DetourNavigator::Status PathFinder::buildPathByNavigatorImpl(const MWWorld::ConstPtr& actor,
const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const DetourNavigator::AgentBounds& agentBounds,
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 auto navigator = world->getNavigator();
const auto status = DetourNavigator::findPath(
*navigator, agentBounds, startPoint, endPoint, flags, areaCosts, endTolerance, out);
const MWBase::World& world = *MWBase::Environment::get().getWorld();
const DetourNavigator::Navigator& navigator = *world.getNavigator();
const DetourNavigator::Status status = DetourNavigator::findPath(
navigator, agentBounds, startPoint, endPoint, flags, areaCosts, endTolerance, checkpoints, out);
if (pathType == PathType::Partial && status == DetourNavigator::Status::PartialPath)
return DetourNavigator::Status::Success;
@ -451,9 +443,9 @@ namespace MWMechanics
}
void PathFinder::buildLimitedPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint,
const osg::Vec3f& endPoint, const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph,
const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags,
const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType)
const osg::Vec3f& endPoint, const PathgridGraph& pathgridGraph, const DetourNavigator::AgentBounds& agentBounds,
const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance,
PathType pathType)
{
const auto navigator = MWBase::Environment::get().getWorld()->getNavigator();
const auto maxDistance
@ -461,9 +453,9 @@ namespace MWMechanics
const auto startToEnd = endPoint - startPoint;
const auto distance = startToEnd.length();
if (distance <= maxDistance)
return buildPath(actor, startPoint, endPoint, cell, pathgridGraph, agentBounds, flags, areaCosts,
endTolerance, pathType);
return buildPath(
actor, startPoint, endPoint, pathgridGraph, agentBounds, flags, areaCosts, endTolerance, pathType);
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 <deque>
#include <iterator>
#include <span>
#include <osg/Vec3f>
#include <components/detournavigator/areatype.hpp>
#include <components/detournavigator/flags.hpp>
#include <components/detournavigator/status.hpp>
#include <components/esm/position.hpp>
#include <components/esm3/loadpgrd.hpp>
namespace MWWorld
{
@ -102,23 +103,20 @@ namespace MWMechanics
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,
const osg::Vec3f& endPoint, const DetourNavigator::AgentBounds& agentBounds,
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,
const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph,
const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags,
const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType);
const PathgridGraph& pathgridGraph, const DetourNavigator::AgentBounds& agentBounds,
const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance,
PathType pathType, std::span<const osg::Vec3f> checkpoints = {});
void buildLimitedPath(const MWWorld::ConstPtr& actor, const osg::Vec3f& startPoint, const osg::Vec3f& endPoint,
const MWWorld::CellStore* cell, const PathgridGraph& pathgridGraph,
const DetourNavigator::AgentBounds& agentBounds, const DetourNavigator::Flags flags,
const DetourNavigator::AreaCosts& areaCosts, float endTolerance, PathType pathType);
const PathgridGraph& pathgridGraph, const DetourNavigator::AgentBounds& agentBounds,
const DetourNavigator::Flags flags, const DetourNavigator::AreaCosts& areaCosts, float endTolerance,
PathType pathType);
/// Remove front point if exist and within tolerance
void update(const osg::Vec3f& position, float pointTolerance, float destinationTolerance,
@ -145,61 +143,6 @@ namespace MWMechanics
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:
bool mConstructed = false;
std::deque<osg::Vec3f> mPath;
@ -211,7 +154,8 @@ namespace MWMechanics
[[nodiscard]] DetourNavigator::Status buildPathByNavigatorImpl(const MWWorld::ConstPtr& actor,
const osg::Vec3f& startPoint, const osg::Vec3f& endPoint, const DetourNavigator::AgentBounds& agentBounds,
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
{
// 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 nearest(std::clamp(position.x(), aabbMin.x(), aabbMax.x()),
@ -18,35 +18,28 @@ namespace MWPhysics
return nearest.distance(position) < radius;
}
template <class Ignore, class OnCollision>
class HasSphereCollisionCallback final : public btBroadphaseAabbCallback
{
public:
HasSphereCollisionCallback(const btVector3& position, const btScalar radius, const int mask, const int group,
const Ignore& ignore, OnCollision* onCollision)
explicit HasSphereCollisionCallback(const btVector3& position, const btScalar radius, const int mask,
const int group, const btCollisionObject* ignore)
: mPosition(position)
, mRadius(radius)
, mIgnore(ignore)
, mCollisionFilterMask(mask)
, mCollisionFilterGroup(group)
, mOnCollision(onCollision)
{
}
bool process(const btBroadphaseProxy* proxy) override
{
if (mResult && mOnCollision == nullptr)
if (mResult)
return false;
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))
return true;
mResult = true;
if (mOnCollision != nullptr)
{
(*mOnCollision)(collisionObject);
return true;
}
return !mResult;
}
@ -55,10 +48,9 @@ namespace MWPhysics
private:
btVector3 mPosition;
btScalar mRadius;
Ignore mIgnore;
const btCollisionObject* mIgnore;
int mCollisionFilterMask;
int mCollisionFilterGroup;
OnCollision* mOnCollision;
bool mResult = false;
bool needsCollision(const btBroadphaseProxy& proxy) const

View File

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

View File

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

View File

@ -3871,10 +3871,11 @@ namespace MWWorld
return btRayAabb(localFrom, localTo, aabbMin, aabbMax, hitDistance, hitNormal);
}
bool World::isAreaOccupiedByOtherActor(const osg::Vec3f& position, const float radius,
std::span<const MWWorld::ConstPtr> ignore, std::vector<MWWorld::Ptr>* occupyingActors) const
bool World::isAreaOccupiedByOtherActor(const MWWorld::ConstPtr& actor, const osg::Vec3f& position) 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

View File

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

View File

@ -13,7 +13,6 @@
#include <osg/Vec3f>
#include <array>
#include <cassert>
#include <functional>
#include <iterator>
@ -64,6 +63,25 @@ namespace DetourNavigator
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,
const dtPolyRef endRef, const osg::Vec3f& startPos, const osg::Vec3f& endPos, const dtQueryFilter& queryFilter,
std::span<dtPolyRef> pathBuffer)
@ -79,7 +97,7 @@ namespace DetourNavigator
}
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)
{
assert(polygonPathSize <= polygonPath.size());
@ -95,7 +113,7 @@ namespace DetourNavigator
dtStatusFailed(status))
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]);
return Status::Success;
@ -103,7 +121,8 @@ namespace DetourNavigator
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,
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;
queryFilter.setIncludeFlags(includeFlags);
@ -131,29 +150,66 @@ namespace DetourNavigator
return Status::EndPolygonNotFound;
std::vector<dtPolyRef> polygonPath(settings.mMaxPolygonPathSize);
const auto polygonPathSize
= findPolygonPath(navMeshQuery, startRef, endRef, startNavMeshPos, endNavMeshPos, queryFilter, polygonPath);
std::span<dtPolyRef> polygonPathBuffer = 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)
{
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;
const std::optional<std::size_t> toCheckpointPathSize = findPolygonPath(navMeshQuery, currentRef,
checkpointRef, currentNavMeshPos, checkpointNavMeshPos, queryFilter, polygonPath);
if (!toCheckpointPathSize.has_value())
continue;
if (*toCheckpointPathSize == 0)
continue;
if (polygonPath[*toCheckpointPathSize - 1] != checkpointRef)
continue;
const Status smoothStatus = makeSmoothPath(navMeshQuery, currentNavMeshPos, checkpointNavMeshPos,
polygonPath, *toCheckpointPathSize, settings.mMaxSmoothPathSize, skipFirst, out);
if (smoothStatus != Status::Success)
return smoothStatus;
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 (*polygonPathSize == 0)
return Status::Success;
if (*toEndPathSize == 0)
return currentRef == endRef ? Status::Success : Status::PartialPath;
osg::Vec3f targetNavMeshPos;
if (const dtStatus status = navMeshQuery.closestPointOnPoly(
polygonPath[*polygonPathSize - 1], end.ptr(), targetNavMeshPos.ptr(), nullptr);
polygonPath[*toEndPathSize - 1], end.ptr(), targetNavMeshPos.ptr(), nullptr);
dtStatusFailed(status))
return Status::TargetPolygonNotFound;
const bool partialPath = polygonPath[*polygonPathSize - 1] != endRef;
const Status smoothStatus = makeSmoothPath(navMeshQuery, startNavMeshPos, targetNavMeshPos, polygonPath,
*polygonPathSize, settings.mMaxSmoothPathSize, out);
const Status smoothStatus = makeSmoothPath(navMeshQuery, currentNavMeshPos, targetNavMeshPos, polygonPath,
*toEndPathSize, settings.mMaxSmoothPathSize, skipFirst, out);
if (smoothStatus != Status::Success)
return smoothStatus;
return partialPath ? Status::PartialPath : Status::Success;
return polygonPath[*toEndPathSize - 1] == endRef ? Status::Success : Status::PartialPath;
}
}

View File

@ -11,6 +11,7 @@
#include <iterator>
#include <optional>
#include <span>
namespace DetourNavigator
{
@ -21,13 +22,13 @@ namespace DetourNavigator
* @param end path at given point.
* @param includeFlags setup allowed navmesh areas.
* @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.
* Equal to out if no path is found.
*/
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,
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);
if (navMesh == nullptr)
@ -37,7 +38,8 @@ namespace DetourNavigator
const auto locked = navMesh->lock();
return findSmoothPath(locked->getQuery(), toNavMeshCoordinates(settings.mRecast, agentBounds.mHalfExtents),
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);
}
osg::Vec3f toWorldVec3(const osg::Vec3f& point) const
{
osg::Vec3f result = point;
toWorld(result);
return result;
}
osg::Vec3f toLocalVec3(const osg::Vec3f& point) const
{
return osg::Vec3f(
point.x() - static_cast<float>(mCellX), point.y() - static_cast<float>(mCellY), point.z());
osg::Vec3f result = point;
toLocal(result);
return result;
}
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] #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).
-- @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}

View File

@ -315,6 +315,7 @@ registerPlayerTest('player rotation')
registerPlayerTest('player forward running')
registerPlayerTest('player diagonal walking')
registerPlayerTest('findPath')
registerPlayerTest('findPath with checkpoints')
registerPlayerTest('findRandomPointAroundCircle')
registerPlayerTest('castNavigationRay')
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 diagonal walking')
registerGlobalTest('findPath')
registerGlobalTest('findPath with checkpoints')
registerGlobalTest('findRandomPointAroundCircle')
registerGlobalTest('castNavigationRay')
registerGlobalTest('findNearestNavMeshPosition')

View File

@ -179,6 +179,39 @@ testing.registerLocalTest('findPath',
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(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)
testing.registerLocalTest('findRandomPointAroundCircle',