completed list of roughly ported over scripthooks.

todo: need to figure out why followobject is only hitting the first path node. likely  amixup with goal handling
This commit is contained in:
AzaezelX 2025-04-17 01:27:08 -05:00
parent e37ae27bc0
commit 4fb92f02a3
16 changed files with 386 additions and 40 deletions

View file

@ -28,7 +28,7 @@ struct AIAimTarget : AIInfo
typedef AIInfo Parent;
Point3F mAimOffset;
bool mTargetInLOS; // Is target object visible?
Point3F getPosition() { return ((mObj) ? mObj->getPosition() : mPosition) + mAimOffset; }
Point3F getPosition() { return ((mObj.isValid()) ? mObj->getPosition() : mPosition) + mAimOffset; }
bool checkInLos(SceneObject* target = NULL, bool _useMuzzle = false, bool _checkEnabled = false);
bool checkInFoV(SceneObject* target = NULL, F32 camFov = 45.0f, bool _checkEnabled = false);
F32 getTargetDistance(SceneObject* target, bool _checkEnabled);

View file

@ -82,25 +82,25 @@ bool AIController::getAIMove(Move* movePtr)
{
if (mMovement.mMoveState != ModeStop)
getNav()->updateNavMesh();
if (!getGoal()->mObj.isNull())
if (getGoal() && !getGoal()->mObj.isNull())
{
if (getNav()->mPathData.path.isNull())
{
if (getGoal()->getDist() > mControllerData->mMoveTolerance)
getNav()->followObject(getGoal());
if (getGoal()->getDist() > mControllerData->mFollowTolerance)
getNav()->followObject(getGoal()->mObj, mControllerData->mFollowTolerance);
}
else
{
if (getGoal()->getDist() > mControllerData->mMoveTolerance)
if (getGoal()->getDist() > mControllerData->mFollowTolerance)
getNav()->repath();
if (getAim()->getDist() < mControllerData->mMoveTolerance)
if (getGoal()->getDist() < mControllerData->mFollowTolerance)
{
getNav()->clearPath();
mMovement.mMoveState = ModeStop;
throwCallback("onTargetInRange");
}
else if (getAim()->getDist() < mControllerData->mAttackRadius)
else if (getGoal()->getDist() < mControllerData->mAttackRadius)
{
throwCallback("onTargetInFiringRange");
}
@ -178,11 +178,29 @@ bool AIController::getAIMove(Move* movePtr)
void AIController::clearCover()
{
// Notify cover that we are no longer on our way.
if (!getCover()->mCoverPoint.isNull())
if (getCover() && !getCover()->mCoverPoint.isNull())
getCover()->mCoverPoint->setOccupied(false);
SAFE_DELETE(mCover);
}
void AIController::Movement::stopMove()
{
mMoveState = ModeStop;
#ifdef TORQUE_NAVIGATION_ENABLED
mControllerRef->getNav()->clearPath();
mControllerRef->clearCover();
mControllerRef->getNav()->clearFollow();
#endif
}
void AIController::Movement::onStuck()
{
mControllerRef->throwCallback("onMoveStuck");
#ifdef TORQUE_NAVIGATION_ENABLED
if (!mControllerRef->getNav()->getPath().isNull())
mControllerRef->getNav()->repath();
#endif
}
DefineEngineMethod(AIController, setMoveSpeed, void, (F32 speed), ,
"@brief Sets the move speed for an AI object.\n\n"
@ -205,6 +223,60 @@ DefineEngineMethod(AIController, getMoveSpeed, F32, (), ,
return object->mMovement.getMoveSpeed();
}
DefineEngineMethod(AIController, stop, void, (), ,
"@brief Tells the AIPlayer to stop moving.\n\n")
{
object->mMovement.stopMove();
}
/**
* Set the state of a movement trigger.
*
* @param slot The trigger slot to set
* @param isSet set/unset the trigger
*/
void AIController::TriggerState::setMoveTrigger(U32 slot, const bool isSet)
{
if (slot >= MaxTriggerKeys)
{
Con::errorf("Attempting to set an invalid trigger slot (%i)", slot);
}
else
{
mMoveTriggers[slot] = isSet; // set the trigger
mControllerRef->getAIInfo()->mObj->setMaskBits(ShapeBase::NoWarpMask); // force the client to updateMove
}
}
/**
* Get the state of a movement trigger.
*
* @param slot The trigger slot to query
* @return True if the trigger is set, false if it is not set
*/
bool AIController::TriggerState::getMoveTrigger(U32 slot) const
{
if (slot >= MaxTriggerKeys)
{
Con::errorf("Attempting to get an invalid trigger slot (%i)", slot);
return false;
}
else
{
return mMoveTriggers[slot];
}
}
/**
* Clear the trigger state for all movement triggers.
*/
void AIController::TriggerState::clearMoveTriggers()
{
for (U32 i = 0; i < MaxTriggerKeys; i++)
setMoveTrigger(i, false);
}
//-----------------------------------------------------------------------------
IMPLEMENT_CO_DATABLOCK_V1(AIControllerData);
@ -341,6 +413,7 @@ void AIControllerData::resolveStuck(AIController* obj)
if (obj->mMovement.mMoveState != AIController::ModeSlowing || locationDelta == 0)
{
obj->mMovement.mMoveState = AIController::ModeStuck;
obj->mMovement.onStuck();
obj->throwCallback("onStuck");
}
}

View file

@ -29,7 +29,6 @@
#include "AICover.h"
#include "AINavigation.h"
class AIControllerData;
class AIController;
//-----------------------------------------------------------------------------
class AIController : public SimObject {
@ -83,6 +82,7 @@ public:
AINavigation* getNav() { return mNav; };
struct Movement
{
AIController* mControllerRef;
MoveState mMoveState;
F32 mMoveSpeed = 1.0;
void setMoveSpeed(F32 speed) { mMoveSpeed = speed; };
@ -99,6 +99,8 @@ public:
struct TriggerState
{
AIController* mControllerRef;
bool mMoveTriggers[MaxTriggerKeys];
// Trigger sets/gets
void setMoveTrigger(U32 slot, const bool isSet = true);
bool getMoveTrigger(U32 slot) const;
@ -109,12 +111,18 @@ public:
static void initPersistFields();
AIController()
{
for (S32 i = 0; i < MaxTriggerKeys; i++)
mTriggerState.mMoveTriggers[i] = false;
mMovement.mControllerRef = this;
mTriggerState.mControllerRef = this;
mControllerData = NULL;
mAIInfo = new AIInfo(this);
mGoal = new AIGoal(this);
mAimTarget = new AIAimTarget(this);
mCover = new AICover(this);
mNav = new AINavigation(this);
mMovement.mMoveState = ModeStop;
};
DECLARE_CONOBJECT(AIController);
@ -127,7 +135,16 @@ class AIControllerData : public SimDataBlock {
public:
AIControllerData() { mMoveTolerance = 0.25; mFollowTolerance = 1.0; mAttackRadius = 2.0; mMoveStuckTolerance = 0.01f; mMoveStuckTestDelay = 30;};
AIControllerData()
{
mMoveTolerance = 0.25;
mFollowTolerance = 1.0;
mAttackRadius = 2.0;
mMoveStuckTolerance = 0.01f;
mMoveStuckTestDelay = 30;
mLinkTypes = LinkData(AllFlags);
mNavSize = AINavigation::Regular;
};
~AIControllerData() {};
static void initPersistFields();
@ -140,7 +157,7 @@ public:
S32 mMoveStuckTestDelay; // The number of ticks to wait before checking if the AI is stuck
/// Types of link we can use.
LinkData mLinkTypes;
AINavigation::NavSize mNavSize;
void resolveYaw(AIController* obj, Point3F location, Move* movePtr);
void resolvePitch(AIController* obj, Point3F location, Move* movePtr) {};
void resolveRoll(AIController* obj, Point3F location, Move* movePtr);

View file

@ -19,3 +19,90 @@
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
//-----------------------------------------------------------------------------
#include "AICover.h"
#include "AIController.h"
struct CoverSearch
{
Point3F loc;
Point3F from;
F32 dist;
F32 best;
CoverPoint* point;
CoverSearch() : loc(0, 0, 0), from(0, 0, 0)
{
best = -FLT_MAX;
point = NULL;
dist = FLT_MAX;
}
};
static void findCoverCallback(SceneObject* obj, void* key)
{
CoverPoint* p = dynamic_cast<CoverPoint*>(obj);
if (!p || p->isOccupied())
return;
CoverSearch* s = static_cast<CoverSearch*>(key);
Point3F dir = s->from - p->getPosition();
dir.normalizeSafe();
// Score first based on angle of cover point to enemy.
F32 score = mDot(p->getNormal(), dir);
// Score also based on distance from seeker.
score -= (p->getPosition() - s->loc).len() / s->dist;
// Finally, consider cover size.
score += (p->getSize() + 1) / CoverPoint::NumSizes;
score *= p->getQuality();
if (score > s->best)
{
s->best = score;
s->point = p;
}
}
bool AIController::findCover(const Point3F& from, F32 radius)
{
if (radius <= 0)
return false;
// Create a search state.
CoverSearch s;
s.loc = getAIInfo()->getPosition();
s.dist = radius;
// Direction we seek cover FROM.
s.from = from;
// Find cover points.
Box3F box(radius * 2.0f);
box.setCenter(getAIInfo()->getPosition());
getAIInfo()->mObj->getContainer()->findObjects(box, MarkerObjectType, findCoverCallback, &s);
// Go to cover!
if (s.point)
{
// Calling setPathDestination clears cover...
bool foundPath = getNav()->setPathDestination(s.point->getPosition());
setCover(s.point);
s.point->setOccupied(true);
return foundPath;
}
return false;
}
DefineEngineMethod(AIController, findCover, S32, (Point3F from, F32 radius), ,
"@brief Tells the AI to find cover nearby.\n\n"
"@param from Location to find cover from (i.e., enemy position).\n"
"@param radius Distance to search for cover.\n"
"@return Cover point ID if cover was found, -1 otherwise.\n\n")
{
if (object->findCover(from, radius))
{
CoverPoint* cover = object->getCover()->mCoverPoint.getObject();
return cover ? cover->getId() : -1;
}
else
{
return -1;
}
}

View file

@ -23,6 +23,9 @@
#define _AICOVER_H_
#include "AIInfo.h"
#include "navigation/coverPoint.h"
struct AICover : AIInfo
{
@ -30,7 +33,7 @@ struct AICover : AIInfo
/// Pointer to a cover point.
SimObjectPtr<CoverPoint> mCoverPoint;
AICover(AIController* controller) : Parent(controller) {};
AICover(AIController* controller, SimObjectPtr<SceneObject> objIn, F32 radIn) : Parent(controller, objIn, radIn) {};
AICover(AIController* controller, SimObjectPtr<SceneObject> objIn, F32 radIn) : Parent(controller, objIn, radIn) { mCoverPoint = dynamic_cast<CoverPoint*>(objIn.getPointer());};
AICover(AIController* controller, Point3F pointIn, F32 radIn) : Parent(controller, pointIn, radIn) {};
};

View file

@ -53,7 +53,7 @@ AIInfo::AIInfo(AIController* controller, Point3F pointIn, F32 radIn)
F32 AIInfo::getDist()
{
AIInfo* controlObj = getCtrl()->getAIInfo();
F32 ret = VectorF(controlObj->mObj->getPosition() - getPosition()).len();
F32 ret = VectorF(controlObj->getPosition() - getPosition()).len();
ret -= controlObj->mRadius + mRadius;
return ret;
}

View file

@ -36,7 +36,7 @@ struct AIInfo
Point3F mPosition, mLastPos;
bool mPosSet;
F32 mRadius;
Point3F getPosition() { return (mObj) ? mObj->getPosition() : mPosition; }
Point3F getPosition() { return (mObj.isValid()) ? mObj->getPosition() : mPosition; }
F32 getDist();
AIInfo() = delete;
AIInfo(AIController* controller);

View file

@ -26,6 +26,7 @@ AINavigation::AINavigation(AIController* controller)
{
mControllerRef = controller;
mJump = None;
mNavSize = Regular;
}
NavMesh* AINavigation::findNavMesh() const
@ -40,6 +41,19 @@ NavMesh* AINavigation::findNavMesh() const
NavMesh* m = static_cast<NavMesh*>(set->at(i));
if (m->getWorldBox().isContained(gbo->getWorldBox()))
{
// Check that mesh size is appropriate.
if (gbo->isMounted())
{
if (!m->mVehicles)
continue;
}
else
{
if ((getNavSize() == Small && !m->mSmallCharacters) ||
(getNavSize() == Regular && !m->mRegularCharacters) ||
(getNavSize() == Large && !m->mLargeCharacters))
continue;
}
if (!mesh || m->getWorldBox().getVolume() < mesh->getWorldBox().getVolume())
mesh = m;
}
@ -101,6 +115,8 @@ void AINavigation::repath()
if (mPathData.path.isNull() || !mPathData.owned)
return;
if (!mControllerRef->getGoal()) return;
// If we're following, get their position.
mPathData.path->mTo = mControllerRef->getGoal()->getPosition();
// Update from position and replan.
@ -212,24 +228,21 @@ bool AINavigation::setPathDestination(const Point3F& pos)
}
}
void AINavigation::followObject(AIInfo* targ)
void AINavigation::followObject()
{
if (!targ) return;
if (targ->getDist() < mControllerRef->mControllerData->mMoveTolerance)
if ((mControllerRef->getGoal()->mLastPos - mControllerRef->getAIInfo()->getPosition()).len() < mControllerRef->mControllerData->mMoveTolerance)
return;
if (setPathDestination(targ->getPosition()))
if (setPathDestination(mControllerRef->getGoal()->getPosition()))
{
mControllerRef->clearCover();
mControllerRef->setGoal(targ);
}
}
void AINavigation::followObject(SceneObject* obj, F32 radius)
{
mControllerRef->setGoal(obj, radius);
followObject(mControllerRef->getGoal());
followObject();
}
void AINavigation::clearFollow()
@ -289,3 +302,107 @@ DefineEngineMethod(AIController, getMoveDestination, Point3F, (), ,
return object->getNav()->getMoveDestination();
}
DefineEngineMethod(AIController, setPathDestination, bool, (Point3F goal), ,
"@brief Tells the AI to find a path to the location provided\n\n"
"@param goal Coordinates in world space representing location to move to.\n"
"@return True if a path was found.\n\n"
"@see getPathDestination()\n"
"@see setMoveDestination()\n")
{
return object->getNav()->setPathDestination(goal);
}
DefineEngineMethod(AIController, getPathDestination, Point3F, (), ,
"@brief Get the AIPlayer's current pathfinding destination.\n\n"
"@return Returns a point containing the \"x y z\" position "
"of the AIPlayer's current path destination. If no path destination "
"has yet been set, this returns \"0 0 0\"."
"@see setPathDestination()\n")
{
return object->getNav()->getPathDestination();
}
DefineEngineMethod(AIController, followNavPath, void, (SimObjectId obj), ,
"@brief Tell the AIPlayer to follow a path.\n\n"
"@param obj ID of a NavPath object for the character to follow.")
{
NavPath* path;
if (Sim::findObject(obj, path))
object->getNav()->followNavPath(path);
}
DefineEngineMethod(AIController, followObject, void, (SimObjectId obj, F32 radius), ,
"@brief Tell the AIPlayer to follow another object.\n\n"
"@param obj ID of the object to follow.\n"
"@param radius Maximum distance we let the target escape to.")
{
SceneObject* follow;
object->getNav()->clearPath();
object->clearCover();
object->getNav()->clearFollow();
if (Sim::findObject(obj, follow))
object->getNav()->followObject(follow, radius);
}
DefineEngineMethod(AIController, repath, void, (), ,
"@brief Tells the AI to re-plan its path. Does nothing if the character "
"has no path, or if it is following a mission path.\n\n")
{
object->getNav()->repath();
}
DefineEngineMethod(AIController, findNavMesh, S32, (), ,
"@brief Get the NavMesh object this AIPlayer is currently using.\n\n"
"@return The ID of the NavPath object this character is using for "
"pathfinding. This is determined by the character's location, "
"navigation type and other factors. Returns -1 if no NavMesh is "
"found.")
{
NavMesh* mesh = object->getNav()->getNavMesh();
return mesh ? mesh->getId() : -1;
}
DefineEngineMethod(AIController, getNavMesh, S32, (), ,
"@brief Return the NavMesh this AIPlayer is using to navigate.\n\n")
{
NavMesh* m = object->getNav()->getNavMesh();
return m ? m->getId() : 0;
}
DefineEngineMethod(AIController, setNavSize, void, (const char* size), ,
"@brief Set the size of NavMesh this character uses. One of \"Small\", \"Regular\" or \"Large\".")
{
if (!String::compare(size, "Small"))
object->getNav()->setNavSize(AINavigation::Small);
else if (!String::compare(size, "Regular"))
object->getNav()->setNavSize(AINavigation::Regular);
else if (!String::compare(size, "Large"))
object->getNav()->setNavSize(AINavigation::Large);
else
Con::errorf("AIPlayer::setNavSize: no such size '%s'.", size);
}
DefineEngineMethod(AIController, getNavSize, const char*, (), ,
"@brief Return the size of NavMesh this character uses for pathfinding.")
{
switch (object->getNav()->getNavSize())
{
case AINavigation::Small:
return "Small";
case AINavigation::Regular:
return "Regular";
case AINavigation::Large:
return "Large";
}
return "";
}

View file

@ -59,6 +59,14 @@ struct AINavigation
Ledge, ///< Jump when we walk off a ledge.
};
enum NavSize {
Small,
Regular,
Large
} mNavSize;
void setNavSize(NavSize size) { mNavSize = size; updateNavMesh(); }
NavSize getNavSize() const { return mNavSize; }
Point3F mMoveDestination;
void setMoveDestination(const Point3F& location, bool slowdown);
Point3F getMoveDestination() { return mMoveDestination; };
@ -68,6 +76,7 @@ struct AINavigation
SimObjectPtr<NavMesh> mNavMesh;
NavMesh* findNavMesh() const;
void updateNavMesh();
NavMesh* getNavMesh() const { return mNavMesh; }
PathData mPathData;
JumpStates mJump;
@ -81,7 +90,7 @@ struct AINavigation
SimObjectPtr<NavPath> getPath() { return mPathData.path; };
void followNavPath(NavPath* path);
void followObject(AIInfo* targ);
void followObject();
void followObject(SceneObject* obj, F32 radius);
void clearFollow();
/// Move to the specified node in the current path.

View file

@ -2259,14 +2259,14 @@ void Player::advanceTime(F32 dt)
}
}
bool Player::setAIController(S32 controller)
bool Player::setAIController(SimObjectId controller)
{
if (Sim::findObject(controller, mAIController))
{
mAIController->setAIInfo(this);
return true;
}
Con::errorf("unable to find AIController : %i", controller);
mAIController = NULL;
return false;
}

View file

@ -763,7 +763,7 @@ public:
void setMomentum(const Point3F &momentum) override;
bool displaceObject(const Point3F& displaceVector) override;
virtual bool getAIMove(Move*);
bool setAIController(S32 controller);
bool setAIController(SimObjectId controller);
AIController* getAIController() { return mAIController; };
bool checkDismountPosition(const MatrixF& oldPos, const MatrixF& newPos); ///< Is it safe to dismount here?

View file

@ -225,8 +225,18 @@ void GuiNavEditorCtrl::spawnPlayer(const Point3F &pos)
SimGroup* missionCleanup = dynamic_cast<SimGroup*>(cleanup);
missionCleanup->addObject(obj);
}
mPlayer = static_cast<AIPlayer*>(obj);
Con::executef(this, "onPlayerSelected", Con::getIntArg(mPlayer->mLinkTypes.getFlags()));
mPlayer = obj;
Player* po = dynamic_cast<Player*>(obj);
if (!po) return; //todo, more types
if (po->getAIController())
{
if (po->getAIController()->mControllerData)
Con::executef(this, "onPlayerSelected", Con::getIntArg(po->getAIController()->mControllerData->mLinkTypes.getFlags()));
}
else
{
Con::executef(this, "onPlayerSelected");
}
}
}
@ -383,16 +393,34 @@ void GuiNavEditorCtrl::on3DMouseDown(const Gui3DMouseEvent & event)
// Select/move character
else
{
if(gServerContainer.castRay(startPnt, endPnt, PlayerObjectType, &ri))
if(gServerContainer.castRay(startPnt, endPnt, PlayerObjectType | VehicleObjectType, &ri))
{
if(dynamic_cast<AIPlayer*>(ri.object))
if(ri.object)
{
mPlayer = dynamic_cast<AIPlayer*>(ri.object);
Con::executef(this, "onPlayerSelected", Con::getIntArg(mPlayer->mLinkTypes.getFlags()));
mPlayer = ri.object;
Player* po = dynamic_cast<Player*>(ri.object);
if (!po) return; //todo, more types
if (po->getAIController())
{
if (po->getAIController()->mControllerData)
Con::executef(this, "onPlayerSelected", Con::getIntArg(po->getAIController()->mControllerData->mLinkTypes.getFlags()));
}
else
{
Con::executef(this, "onPlayerSelected");
}
}
}
else if (!mPlayer.isNull() && gServerContainer.castRay(startPnt, endPnt, StaticObjectType, &ri))
{
Player* po = dynamic_cast<Player*>(mPlayer.getPointer());
if (!po) return; //todo, more types
if (po->getAIController())
{
if (po->getAIController()->mControllerData)
po->getAIController()->getNav()->setPathDestination(ri.point);
}
}
else if(!mPlayer.isNull() && gServerContainer.castRay(startPnt, endPnt, StaticObjectType, &ri))
mPlayer->setPathDestination(ri.point);
}
}
}
@ -455,8 +483,8 @@ void GuiNavEditorCtrl::on3DMouseMove(const Gui3DMouseEvent & event)
if(mMode == mTestMode)
{
if(gServerContainer.castRay(startPnt, endPnt, PlayerObjectType, &ri))
mCurPlayer = dynamic_cast<AIPlayer*>(ri.object);
if(gServerContainer.castRay(startPnt, endPnt, PlayerObjectType | VehicleObjectType, &ri))
mCurPlayer = ri.object;
else
mCurPlayer = NULL;
}

View file

@ -155,8 +155,8 @@ protected:
/// @name Test mode
/// @{
SimObjectPtr<AIPlayer> mPlayer;
SimObjectPtr<AIPlayer> mCurPlayer;
SimObjectPtr<SceneObject> mPlayer;
SimObjectPtr<SceneObject> mCurPlayer;
/// @}

View file

@ -169,3 +169,8 @@ datablock LightAnimData( SpinLightAnim )
rotKeys[2] = "az";
rotSmooth[2] = true;
};
datablock AIPlayerControllerData( aiPlayerControl )
{
};

View file

@ -104,6 +104,7 @@ function ENavEditorSettingsPage::init(%this)
{
// Initialises the settings controls in the settings dialog box.
%this-->SpawnClassOptions.clear();
%this-->SpawnClassOptions.add("Player");
%this-->SpawnClassOptions.add("AIPlayer");
%this-->SpawnClassOptions.setFirstSelected();
}
@ -240,7 +241,7 @@ function NavEditorPlugin::initSettings(%this)
{
EditorSettings.beginGroup("NavEditor", true);
EditorSettings.setDefaultValue("SpawnClass", "AIPlayer");
EditorSettings.setDefaultValue("SpawnClass", "Player");
EditorSettings.setDefaultValue("SpawnDatablock", "DefaultPlayerData");
EditorSettings.endGroup();

View file

@ -459,6 +459,12 @@ function NavEditorGui::onLinkSelected(%this, %flags)
function NavEditorGui::onPlayerSelected(%this, %flags)
{
if (!isObject(%this.getPlayer().aiController))
{
%this.getPlayer().aiController = new AIController(){ ControllerData = aiPlayerControl; };
%this.getPlayer().setAIController(%this.getPlayer().aiController);
}
NavMeshIgnore(%this.getPlayer(), true);
updateLinkData(NavEditorOptionsWindow-->TestProperties, %flags);
}
@ -526,7 +532,7 @@ function NavEditorGui::findCover(%this)
%text = NavEditorOptionsWindow-->TestProperties->CoverPosition.getText();
if(%text !$= "")
%pos = eval("return " @ %text);
%this.getPlayer().findCover(%pos, NavEditorOptionsWindow-->TestProperties->CoverRadius.getText());
%this.getPlayer().getAIController().findCover(%pos, NavEditorOptionsWindow-->TestProperties->CoverRadius.getText());
}
}
@ -547,7 +553,7 @@ function NavEditorGui::followObject(%this)
toolsMessageBoxOk("Error", "Cannot find object" SPC %text);
}
if(isObject(%obj))
%this.getPlayer().followObject(%obj, NavEditorOptionsWindow-->TestProperties->FollowRadius.getText());
%this.getPlayer().getAIController().followObject(%obj, NavEditorOptionsWindow-->TestProperties->FollowRadius.getText());
}
}