add follow logic

select follow target and toggle follow for a specific object. Only way to unfollow is to move the following bot to an arbitrary location
This commit is contained in:
marauder2k7 2025-07-27 17:25:09 +01:00
parent 0b96579ada
commit 3946017556
6 changed files with 155 additions and 55 deletions

View file

@ -168,22 +168,20 @@ bool AIController::getAIMove(Move* movePtr)
{
obj = getAIInfo()->mObj;
}
bool adjusted = false;
if (getNav()->avoidObstacles())
{
adjusted = true;
}
else if (mRandI(0, 100) < mControllerData->mFlocking.mChance && getNav()->flock())
{
adjusted = true;
}
Point3F start = obj->getPosition();
Point3F end = start;
start.z = obj->getBoxCenter().z;
end.z -= mControllerData->mHeightTolerance;
obj->disableCollision();
// Only repath if not already adjusted and on risky ground
RayInfo info;
if (!adjusted && obj->getContainer()->castRay(obj->getPosition(), obj->getPosition() - Point3F(0, 0, mControllerData->mHeightTolerance), StaticShapeObjectType, &info))
if (obj->getContainer()->castRay(start, end, StaticShapeObjectType, &info))
{
getNav()->repath();
}
obj->enableCollision();
getGoal()->mInRange = false;
}
if (getGoal()->getDist() < mControllerData->mFollowTolerance )
@ -541,7 +539,7 @@ AIControllerData::AIControllerData()
mAttackRadius = 2.0f;
mMoveStuckTolerance = 0.01f;
mMoveStuckTestDelay = 30;
mHeightTolerance = 0.001f;
mHeightTolerance = 0.1f;
mFollowTolerance = 1.0f;
#ifdef TORQUE_NAVIGATION_ENABLED

View file

@ -23,7 +23,7 @@
#include "AIController.h"
#include "T3D/shapeBase.h"
static U32 sAILoSMask = TerrainObjectType | StaticShapeObjectType | StaticObjectType | AIObjectType;
static U32 sAILoSMask = TerrainObjectType | StaticShapeObjectType | StaticObjectType;
AINavigation::AINavigation(AIController* controller)
{
@ -339,11 +339,11 @@ void AINavigation::repath()
if (mPathData.path.isNull() || !mPathData.owned)
return;
if (mRandI(0, 100) < getCtrl()->mControllerData->mFlocking.mChance && flock())
if (avoidObstacles())
{
mPathData.path->mTo = mMoveDestination;
}
else if (avoidObstacles())
else if (mRandI(0, 100) < getCtrl()->mControllerData->mFlocking.mChance && flock())
{
mPathData.path->mTo = mMoveDestination;
}
@ -401,7 +401,7 @@ bool AINavigation::avoidObstacles()
leftDir.normalizeSafe();
rightDir.normalizeSafe();
F32 rayLength = getCtrl()->mMovement.getMoveSpeed();
F32 rayLength = obj->getVelocity().lenSquared() * TickSec * 2 + getCtrl()->getAIInfo()->mRadius;
Point3F directions[3] = {
forward,
leftDir,
@ -445,9 +445,10 @@ bool AINavigation::flock()
obj->disableCollision();
Point3F pos = obj->getBoxCenter();
Point3F searchArea = Point3F(flockingData.mMin / 2, flockingData.mMax / 2, getCtrl()->getAIInfo()->mObj->getObjBox().maxExtents.z / 2);
F32 maxFlocksq = flockingData.mMax * flockingData.mMax;
Point3F searchArea = Point3F(maxFlocksq, maxFlocksq, getCtrl()->getAIInfo()->mObj->getObjBox().maxExtents.z / 2);
bool flocking = false;
U32 found = 0;
if (getCtrl()->getGoal())
@ -471,41 +472,35 @@ bool AINavigation::flock()
sql.mList.remove(obj);
Point3F avoidanceOffset = Point3F::Zero;
F32 avoidanceAmtSq = 0;
//avoid objects in the way
RayInfo info;
if (obj->getContainer()->castRay(pos, dest + Point3F(0, 0, obj->getObjBox().len_z() / 2), sAILoSMask, &info))
{
Point3F blockerOffset = (info.point - dest);
blockerOffset.z = 0;
avoidanceOffset += blockerOffset;
}
//avoid bots that are too close
for (U32 i = 0; i < sql.mList.size(); i++)
{
ShapeBase* other = dynamic_cast<ShapeBase*>(sql.mList[i]);
Point3F objectCenter = other->getBoxCenter();
F32 sumRad = flockingData.mMin + other->getAIController()->mControllerData->mFlocking.mMin;
F32 sumMinRad = flockingData.mMin + other->getAIController()->mControllerData->mFlocking.mMin;
F32 separation = getCtrl()->getAIInfo()->mRadius + other->getAIController()->getAIInfo()->mRadius;
sumRad += separation;
separation += sumMinRad;
Point3F offset = (pos - objectCenter);
F32 offsetLensq = offset.lenSquared(); //square roots are expensive, so use squared val compares
if ((flockingData.mMin > 0) && (offsetLensq < (sumRad * sumRad)))
if ((flockingData.mMin > 0) && (offsetLensq < (sumMinRad * sumMinRad)))
{
other->disableCollision();
if (!obj->getContainer()->castRay(pos, other->getBoxCenter(), sAILoSMask, &info))
if (!obj->getContainer()->castRay(pos, other->getBoxCenter(), sAILoSMask | AIObjectType, &info))
{
found++;
offset.normalizeSafe();
offset *= sumRad + separation;
offset *= separation;
avoidanceOffset += offset; //accumulate total group, move away from that
avoidanceAmtSq += offsetLensq;
}
other->enableCollision();
}
}
//if we don't have to worry about bumping into one another (nothing found lower than minFLock), see about grouping up
if (found == 0)
{
@ -514,20 +509,20 @@ bool AINavigation::flock()
ShapeBase* other = static_cast<ShapeBase*>(sql.mList[i]);
Point3F objectCenter = other->getBoxCenter();
F32 sumRad = flockingData.mMin + other->getAIController()->mControllerData->mFlocking.mMin;
F32 sumMaxRad = flockingData.mMax + other->getAIController()->mControllerData->mFlocking.mMax;
F32 separation = getCtrl()->getAIInfo()->mRadius + other->getAIController()->getAIInfo()->mRadius;
sumRad += separation;
separation += sumMaxRad;
Point3F offset = (pos - objectCenter);
if ((flockingData.mMin > 0) && ((sumRad * sumRad) < (maxFlocksq)))
F32 offsetLensq = offset.lenSquared(); //square roots are expensive, so use squared val compares
if ((flockingData.mMax > 0) && (offsetLensq < (sumMaxRad * sumMaxRad)))
{
other->disableCollision();
if (!obj->getContainer()->castRay(pos, other->getBoxCenter(), sAILoSMask, &info))
if (!obj->getContainer()->castRay(pos, other->getBoxCenter(), sAILoSMask | AIObjectType, &info))
{
found++;
offset.normalizeSafe();
offset *= sumRad + separation;
avoidanceOffset -= offset; // subtract total group, move toward it
avoidanceAmtSq -= offsetLensq;
}
other->enableCollision();
}
@ -535,27 +530,36 @@ bool AINavigation::flock()
}
if (found > 0)
{
//ephasize the *side* portion of sidestep to better avoid clumps
if (avoidanceOffset.x < avoidanceOffset.y)
avoidanceOffset.x *= 2.0;
else
avoidanceOffset.y *= 2.0;
//add fuzz to sidestepping
avoidanceOffset.z = 0;
avoidanceOffset.x = (mRandF() * avoidanceOffset.x) * 0.5 + avoidanceOffset.x * 0.75;
avoidanceOffset.y = (mRandF() * avoidanceOffset.y) * 0.5 + avoidanceOffset.y * 0.75;
if (avoidanceOffset.lenSquared() < (maxFlocksq))
avoidanceOffset.normalizeSafe();
avoidanceOffset *= avoidanceAmtSq;
if ((avoidanceAmtSq) > flockingData.mMin * flockingData.mMin)
{
dest += avoidanceOffset;
dest = obj->getPosition()+avoidanceOffset;
}
//if we're not jumping...
if (mJump == None)
{
dest.z = obj->getPosition().z;
//make sure we don't run off a cliff
Point3F zlen(0, 0, getCtrl()->mControllerData->mHeightTolerance);
if (obj->getContainer()->castRay(dest + zlen, dest - zlen, TerrainObjectType | StaticShapeObjectType | StaticObjectType, &info))
{
if ((mMoveDestination - dest).len() > getCtrl()->mControllerData->mMoveTolerance)
{
mMoveDestination = dest;
flocking = true;
}
mMoveDestination = dest;
flocking = true;
}
}
}

View file

@ -112,6 +112,9 @@ NavMeshTestTool::NavMeshTestTool()
mPlayer = NULL;
mCurPlayer = NULL;
mFollowObject = NULL;
mCurFollowObject = NULL;
mPathStart = Point3F::Max;
mPathEnd = Point3F::Max;
@ -120,10 +123,12 @@ NavMeshTestTool::NavMeshTestTool()
mLinkTypes = LinkData(AllFlags);
mFilter.setIncludeFlags(mLinkTypes.getFlags());
mFilter.setExcludeFlags(0);
mSelectFollow = false;
}
void NavMeshTestTool::onActivated(const Gui3DMouseEvent& evt)
{
mSelectFollow = false;
Con::executef(this, "onActivated");
}
@ -140,6 +145,8 @@ void NavMeshTestTool::onDeactivated()
Con::executef(this, "onPlayerDeselected");
}
mSelectFollow = false;
Con::executef(this, "onDeactivated");
}
@ -175,8 +182,17 @@ void NavMeshTestTool::on3DMouseDown(const Gui3DMouseEvent& evt)
{
if (!ri.object)
return;
mPlayer = ri.object;
if (mSelectFollow)
{
mFollowObject = ri.object;
Con::executef(this, "onFollowSelected");
mSelectFollow = false;
return;
}
else
{
mPlayer = ri.object;
}
#ifdef TORQUE_NAVIGATION_ENABLED
AIPlayer* asAIPlayer = dynamic_cast<AIPlayer*>(mPlayer.getPointer());
@ -277,10 +293,17 @@ void NavMeshTestTool::on3DMouseMove(const Gui3DMouseEvent& evt)
RayInfo ri;
if (gServerContainer.castRay(startPnt, endPnt, PlayerObjectType | VehicleObjectType, &ri))
mCurPlayer = ri.object;
{
if (mSelectFollow)
mCurFollowObject = ri.object;
else
mCurPlayer = ri.object;
}
else
{
mCurFollowObject = NULL;
mCurPlayer = NULL;
}
}
void NavMeshTestTool::onRender3D()
@ -310,10 +333,15 @@ void NavMeshTestTool::onRender3D()
dd.immediateRender();
if (!mCurFollowObject.isNull())
renderBoxOutline(mCurFollowObject->getWorldBox(), ColorI::LIGHT);
if (!mCurPlayer.isNull())
renderBoxOutline(mCurPlayer->getWorldBox(), ColorI::BLUE);
if (!mPlayer.isNull())
renderBoxOutline(mPlayer->getWorldBox(), ColorI::GREEN);
if (!mFollowObject.isNull())
renderBoxOutline(mFollowObject->getWorldBox(), ColorI::WHITE);
}
bool NavMeshTestTool::updateGuiInfo()
@ -326,10 +354,25 @@ bool NavMeshTestTool::updateGuiInfo()
String text;
if (mPlayer)
text = "LMB To Select move Destination. LSHIFT+LMB To Deselect Current Bot.";
if (mCurPlayer != NULL && mCurPlayer != mPlayer)
text = "LMB To select Bot.";
if (mPlayer == NULL)
{
text = "LMB To place start/end for test path.";
}
if (mSpawnClass != String::EmptyString && mSpawnDatablock != String::EmptyString)
text += " CTRL+LMB To spawn a new Bot.";
if (statusbar)
Con::executef(statusbar, "setInfo", text.c_str());
text = "";
if (mPlayer)
text = String::ToString("Bot Selected: %d", mPlayer->getId());
if (selectionBar)
selectionBar->setText(text);
@ -342,12 +385,24 @@ S32 NavMeshTestTool::getPlayerId()
return mPlayer.isNull() ? 0 : mPlayer->getId();
}
S32 NavMeshTestTool::getFollowObjectId()
{
return mFollowObject.isNull() ? 0 : mFollowObject->getId();
}
DefineEngineMethod(NavMeshTestTool, getPlayer, S32, (), ,
"@brief Return the current player id.")
{
return object->getPlayerId();
}
DefineEngineMethod(NavMeshTestTool, getFollowObject, S32, (), ,
"@brief Return the current follow object id.")
{
return object->getFollowObjectId();
}
DefineEngineMethod(NavMeshTestTool, setSpawnClass, void, (String className), , "")
{
object->setSpawnClass(className);
@ -358,3 +413,9 @@ DefineEngineMethod(NavMeshTestTool, setSpawnDatablock, void, (String dbName), ,
object->setSpawnDatablock(dbName);
}
DefineEngineMethod(NavMeshTestTool, followSelectMode, void, (), ,
"@brief Set NavMeshTool to select a follow object.")
{
return object->followSelectMode();
}

View file

@ -18,11 +18,15 @@ protected:
String mSpawnDatablock;
SimObjectPtr<SceneObject> mPlayer;
SimObjectPtr<SceneObject> mCurPlayer;
SimObjectPtr<SceneObject> mFollowObject;
SimObjectPtr<SceneObject> mCurFollowObject;
Point3F mPathStart;
Point3F mPathEnd;
NavPath* mTestPath;
LinkData mLinkTypes;
dtQueryFilter mFilter;
bool mSelectFollow;
public:
DECLARE_CONOBJECT(NavMeshTestTool);
@ -44,9 +48,11 @@ public:
bool updateGuiInfo() override;
S32 getPlayerId();
S32 getFollowObjectId();
void setSpawnClass(String className) { mSpawnClass = className; }
void setSpawnDatablock(String dbName) { mSpawnDatablock = dbName; }
void followSelectMode() { mSelectFollow = true; }
};

View file

@ -294,7 +294,7 @@ $guiContent = new GuiNavEditorCtrl(NavEditorGui, EditorGuiGroup) {
{
internalName = "SelectActions";
position = "7 21";
extent = "190 64";
extent = "190 136";
new GuiButtonCtrl() {
Profile = "ToolsGuiButtonProfile";
@ -370,7 +370,7 @@ $guiContent = new GuiNavEditorCtrl(NavEditorGui, EditorGuiGroup) {
{
internalName = "LinkActions";
position = "7 21";
extent = "190 64";
extent = "190 136";
new GuiButtonCtrl() {
Profile = "ToolsGuiButtonProfile";
@ -386,7 +386,7 @@ $guiContent = new GuiNavEditorCtrl(NavEditorGui, EditorGuiGroup) {
{
internalName = "CoverActions";
position = "7 21";
extent = "190 64";
extent = "190 136";
new GuiButtonCtrl() {
Profile = "ToolsGuiButtonProfile";
@ -411,7 +411,7 @@ $guiContent = new GuiNavEditorCtrl(NavEditorGui, EditorGuiGroup) {
{
internalName = "TileActions";
position = "7 21";
extent = "190 64";
extent = "190 136";
new GuiButtonCtrl() {
Profile = "ToolsGuiButtonProfile";
@ -427,7 +427,7 @@ $guiContent = new GuiNavEditorCtrl(NavEditorGui, EditorGuiGroup) {
{
internalName = "TestActions";
position = "7 21";
extent = "190 64";
extent = "190 136";
new GuiControl() {
profile = "GuiDefaultProfile";
Extent = "190 20";
@ -481,7 +481,7 @@ $guiContent = new GuiNavEditorCtrl(NavEditorGui, EditorGuiGroup) {
Extent = "90 18";
text = "Delete";
tooltipProfile = "GuiToolTipProfile";
tooltip = "Delete Selected.";
tooltip = "Delete Selected Bot.";
command = "NavMeshTools->TestTool.getPlayer().delete();NavInspector.inspect();";
};
new GuiButtonCtrl() {
@ -505,7 +505,7 @@ $guiContent = new GuiNavEditorCtrl(NavEditorGui, EditorGuiGroup) {
HorizSizing = "right";
VertSizing = "bottom";
Extent = "90 18";
text = "Follow";
text = "Select Follow";
command = "NavMeshTools->TestTool.followObject();";
};
new GuiButtonCtrl() {
@ -519,6 +519,20 @@ $guiContent = new GuiNavEditorCtrl(NavEditorGui, EditorGuiGroup) {
command = "NavMeshTools->TestTool.stop();";
};
};
new GuiControl() {
profile = "GuiDefaultProfile";
Extent = "190 18";
new GuiButtonCtrl() {
Profile = "ToolsGuiButtonProfile";
buttonType = "PushButton";
HorizSizing = "right";
VertSizing = "bottom";
Extent = "90 18";
text = "Toggle Follow";
command = "NavMeshTools->TestTool.toggleFollow();";
};
};
};
};
new GuiContainer(NavEditorInspector){

View file

@ -431,7 +431,7 @@ function NavMeshTestTool::onActivated(%this)
%properties->TestProperties.setVisible(false);
%classList = enumerateConsoleClasses("Player") TAB enumerateConsoleClasses("Vehicle");
echo(%classList);
//echo(%classList);
SpawnClassSelector.clear();
foreach$(%class in %classList)
@ -498,6 +498,23 @@ function NavMeshTestTool::stop(%this)
}
}
function NavMeshTestTool::toggleFollow(%this)
{
if(isObject(%this.getFollowObject()) && isObject(%this.getPlayer()))
{
if(%this.getPlayer().isMemberOfClass("AIPlayer"))
%this.getPlayer().followObject(%this.getFollowObject(), "2.0");
else
%this.getPlayer().getAIController().followObject(%this.getFollowObject(), %this.getPlayer().getDatablock().aiControllerData.mFollowTolerance);
}
}
function NavMeshTestTool::followObject(%this)
{
%this.followSelectMode();
}
function SpawnClassSelector::onSelect(%this, %id)
{
%className = %this.getTextById(%id);