//----------------------------------------------------------------------------- // Copyright (c) 2012 GarageGames, LLC // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to // deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or // sell copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. //----------------------------------------------------------------------------- #include "platform/platform.h" #include "T3D/aiPlayer.h" #include "console/consoleInternal.h" #include "math/mMatrix.h" #include "T3D/gameBase/moveManager.h" #include "console/engineAPI.h" static U32 sAIPlayerLoSMask = TerrainObjectType | StaticShapeObjectType | StaticObjectType; IMPLEMENT_CO_NETOBJECT_V1(AIPlayer); ConsoleDocClass( AIPlayer, "@brief A Player object not controlled by conventional input, but by an AI engine.\n\n" "The AIPlayer provides a Player object that may be controlled from script. You control " "where the player moves and how fast. You may also set where the AIPlayer is aiming at " "-- either a location or another game object.\n\n" "The AIPlayer class does not have a datablock of its own. It makes use of the PlayerData " "datablock to define how it looks, etc. As the AIPlayer is an extension of the Player class " "it can mount objects and fire weapons, or mount vehicles and drive them.\n\n" "While the PlayerData datablock is used, there are a number of additional callbacks that are " "implemented by AIPlayer on the datablock. These are listed here:\n\n" "void onReachDestination(AIPlayer obj) \n" "Called when the player has reached its set destination using the setMoveDestination() method. " "The actual point at which this callback is called is when the AIPlayer is within the mMoveTolerance " "of the defined destination.\n\n" "void onMoveStuck(AIPlayer obj) \n" "While in motion, if an AIPlayer has moved less than moveStuckTolerance within a single tick, this " "callback is called. From here you could choose an alternate destination to get the AIPlayer moving " "again.\n\n" "void onTargetEnterLOS(AIPlayer obj) \n" "When an object is being aimed at (following a call to setAimObject()) and the targeted object enters " "the AIPlayer's line of sight, this callback is called. The LOS test is a ray from the AIPlayer's eye " "position to the center of the target's bounding box. The LOS ray test only checks against interiors, " "statis shapes, and terrain.\n\n" "void onTargetExitLOS(AIPlayer obj) \n" "When an object is being aimed at (following a call to setAimObject()) and the targeted object leaves " "the AIPlayer's line of sight, this callback is called. The LOS test is a ray from the AIPlayer's eye " "position to the center of the target's bounding box. The LOS ray test only checks against interiors, " "statis shapes, and terrain.\n\n" "@tsexample\n" "// Create the demo player object\n" "%player = new AiPlayer()\n" "{\n" " dataBlock = DemoPlayer;\n" " path = \"\";\n" "};\n" "@endtsexample\n\n" "@see Player for a list of all inherited functions, variables, and base description\n" "@ingroup AI\n" "@ingroup gameObjects\n"); /** * Constructor */ AIPlayer::AIPlayer() { mMoveDestination.set( 0.0f, 0.0f, 0.0f ); mMoveSpeed = 1.0f; mMoveTolerance = 0.25f; mMoveStuckTolerance = 0.01f; mMoveStuckTestDelay = 30; mMoveStuckTestCountdown = 0; mMoveSlowdown = true; mMoveState = ModeStop; mAimObject = 0; mAimLocationSet = false; mTargetInLOS = false; mAimOffset = Point3F(0.0f, 0.0f, 0.0f); mIsAiControlled = true; } /** * Destructor */ AIPlayer::~AIPlayer() { } void AIPlayer::initPersistFields() { addGroup( "AI" ); addField( "mMoveTolerance", TypeF32, Offset( mMoveTolerance, AIPlayer ), "@brief Distance from destination before stopping.\n\n" "When the AIPlayer is moving to a given destination it will move to within " "this distance of the destination and then stop. By providing this tolerance " "it helps the AIPlayer from never reaching its destination due to minor obstacles, " "rounding errors on its position calculation, etc. By default it is set to 0.25.\n"); addField( "moveStuckTolerance", TypeF32, Offset( mMoveStuckTolerance, AIPlayer ), "@brief Distance tolerance on stuck check.\n\n" "When the AIPlayer is moving to a given destination, if it ever moves less than " "this tolerance during a single tick, the AIPlayer is considered stuck. At this point " "the onMoveStuck() callback is called on the datablock.\n"); addField( "moveStuckTestDelay", TypeS32, Offset( mMoveStuckTestDelay, AIPlayer ), "@brief The number of ticks to wait before testing if the AIPlayer is stuck.\n\n" "When the AIPlayer is asked to move, this property is the number of ticks to wait " "before the AIPlayer starts to check if it is stuck. This delay allows the AIPlayer " "to accelerate to full speed without its initial slow start being considered as stuck.\n" "@note Set to zero to have the stuck test start immediately.\n"); endGroup( "AI" ); Parent::initPersistFields(); } bool AIPlayer::onAdd() { if (!Parent::onAdd()) return false; // Use the eye as the current position (see getAIMove) MatrixF eye; getEyeTransform(&eye); mLastLocation = eye.getPosition(); return true; } /** * Sets the speed at which this AI moves * * @param speed Speed to move, default player was 10 */ void AIPlayer::setMoveSpeed( F32 speed ) { mMoveSpeed = getMax(0.0f, getMin( 1.0f, speed )); } /** * Stops movement for this AI */ void AIPlayer::stopMove() { mMoveState = ModeStop; } /** * Sets how far away from the move location is considered * "on target" * * @param tolerance Movement tolerance for error */ void AIPlayer::setMoveTolerance( const F32 tolerance ) { mMoveTolerance = getMax( 0.1f, tolerance ); } /** * Sets the location for the bot to run to * * @param location Point to run to */ void AIPlayer::setMoveDestination( const Point3F &location, bool slowdown ) { mMoveDestination = location; mMoveState = ModeMove; mMoveSlowdown = slowdown; mMoveStuckTestCountdown = mMoveStuckTestDelay; } /** * Sets the object the bot is targeting * * @param targetObject The object to target */ void AIPlayer::setAimObject( GameBase *targetObject ) { mAimObject = targetObject; mTargetInLOS = false; mAimOffset = Point3F(0.0f, 0.0f, 0.0f); } /** * Sets the object the bot is targeting and an offset to add to target location * * @param targetObject The object to target * @param offset The offest from the target location to aim at */ void AIPlayer::setAimObject( GameBase *targetObject, Point3F offset ) { mAimObject = targetObject; mTargetInLOS = false; mAimOffset = offset; } /** * Sets the location for the bot to aim at * * @param location Point to aim at */ void AIPlayer::setAimLocation( const Point3F &location ) { mAimObject = 0; mAimLocationSet = true; mAimLocation = location; mAimOffset = Point3F(0.0f, 0.0f, 0.0f); } /** * Clears the aim location and sets it to the bot's * current destination so he looks where he's going */ void AIPlayer::clearAim() { mAimObject = 0; mAimLocationSet = false; mAimOffset = Point3F(0.0f, 0.0f, 0.0f); } /** * This method calculates the moves for the AI player * * @param movePtr Pointer to move the move list into */ bool AIPlayer::getAIMove(Move *movePtr) { *movePtr = NullMove; // Use the eye as the current position. MatrixF eye; getEyeTransform(&eye); Point3F location = eye.getPosition(); Point3F rotation = getRotation(); // Orient towards the aim point, aim object, or towards // our destination. if (mAimObject || mAimLocationSet || mMoveState != ModeStop) { // Update the aim position if we're aiming for an object if (mAimObject) mAimLocation = mAimObject->getPosition() + mAimOffset; else if (!mAimLocationSet) mAimLocation = mMoveDestination; F32 xDiff = mAimLocation.x - location.x; F32 yDiff = mAimLocation.y - location.y; if (!mIsZero(xDiff) || !mIsZero(yDiff)) { // First do Yaw // use the cur yaw between -Pi and Pi F32 curYaw = rotation.z; while (curYaw > M_2PI_F) curYaw -= M_2PI_F; while (curYaw < -M_2PI_F) curYaw += M_2PI_F; // find the yaw offset F32 newYaw = mAtan2( xDiff, yDiff ); F32 yawDiff = newYaw - curYaw; // make it between 0 and 2PI if( yawDiff < 0.0f ) yawDiff += M_2PI_F; else if( yawDiff >= M_2PI_F ) yawDiff -= M_2PI_F; // now make sure we take the short way around the circle if( yawDiff > M_PI_F ) yawDiff -= M_2PI_F; else if( yawDiff < -M_PI_F ) yawDiff += M_2PI_F; movePtr->yaw = yawDiff; // Next do pitch. if (!mAimObject && !mAimLocationSet) { // Level out if were just looking at our next way point. Point3F headRotation = getHeadRotation(); movePtr->pitch = -headRotation.x; } else { // This should be adjusted to run from the // eye point to the object's center position. Though this // works well enough for now. F32 vertDist = mAimLocation.z - location.z; F32 horzDist = mSqrt(xDiff * xDiff + yDiff * yDiff); F32 newPitch = mAtan2( horzDist, vertDist ) - ( M_PI_F / 2.0f ); if (mFabs(newPitch) > 0.01f) { Point3F headRotation = getHeadRotation(); movePtr->pitch = newPitch - headRotation.x; } } } } else { // Level out if we're not doing anything else Point3F headRotation = getHeadRotation(); movePtr->pitch = -headRotation.x; } // Move towards the destination if (mMoveState != ModeStop) { F32 xDiff = mMoveDestination.x - location.x; F32 yDiff = mMoveDestination.y - location.y; // Check if we should mMove, or if we are 'close enough' if (mFabs(xDiff) < mMoveTolerance && mFabs(yDiff) < mMoveTolerance) { mMoveState = ModeStop; throwCallback("onReachDestination"); } else { // Build move direction in world space if (mIsZero(xDiff)) movePtr->y = (location.y > mMoveDestination.y) ? -1.0f : 1.0f; else if (mIsZero(yDiff)) movePtr->x = (location.x > mMoveDestination.x) ? -1.0f : 1.0f; else if (mFabs(xDiff) > mFabs(yDiff)) { F32 value = mFabs(yDiff / xDiff); movePtr->y = (location.y > mMoveDestination.y) ? -value : value; movePtr->x = (location.x > mMoveDestination.x) ? -1.0f : 1.0f; } else { F32 value = mFabs(xDiff / yDiff); movePtr->x = (location.x > mMoveDestination.x) ? -value : value; movePtr->y = (location.y > mMoveDestination.y) ? -1.0f : 1.0f; } // Rotate the move into object space (this really only needs // a 2D matrix) Point3F newMove; MatrixF moveMatrix; moveMatrix.set(EulerF(0.0f, 0.0f, -(rotation.z + movePtr->yaw))); moveMatrix.mulV( Point3F( movePtr->x, movePtr->y, 0.0f ), &newMove ); movePtr->x = newMove.x; movePtr->y = newMove.y; // Set movement speed. We'll slow down once we get close // to try and stop on the spot... if (mMoveSlowdown) { F32 speed = mMoveSpeed; F32 dist = mSqrt(xDiff*xDiff + yDiff*yDiff); F32 maxDist = 5.0f; if (dist < maxDist) speed *= dist / maxDist; movePtr->x *= speed; movePtr->y *= speed; mMoveState = ModeSlowing; } else { movePtr->x *= mMoveSpeed; movePtr->y *= mMoveSpeed; mMoveState = ModeMove; } if (mMoveStuckTestCountdown > 0) --mMoveStuckTestCountdown; else { // We should check to see if we are stuck... F32 locationDelta = (location - mLastLocation).len(); if (locationDelta < mMoveStuckTolerance && mDamageState == Enabled) { // If we are slowing down, then it's likely that our location delta will be less than // our move stuck tolerance. Because we can be both slowing and stuck // we should TRY to check if we've moved. This could use better detection. if ( mMoveState != ModeSlowing || locationDelta == 0 ) { mMoveState = ModeStuck; throwCallback("onMoveStuck"); } } } } } // Test for target location in sight if it's an object. The LOS is // run from the eye position to the center of the object's bounding, // which is not very accurate. if (mAimObject) { if (checkInLos(mAimObject.getPointer())) { if (!mTargetInLOS) { throwCallback("onTargetEnterLOS"); mTargetInLOS = true; } } else if (mTargetInLOS) { throwCallback("onTargetExitLOS"); mTargetInLOS = false; } } // Replicate the trigger state into the move so that // triggers can be controlled from scripts. for( U32 i = 0; i < MaxMountedImages; i++ ) movePtr->trigger[i] = getImageTriggerState(i); mLastLocation = location; return true; } /** * Utility function to throw callbacks. Callbacks always occure * on the datablock class. * * @param name Name of script function to call */ void AIPlayer::throwCallback( const char *name ) { Con::executef(getDataBlock(), name, getIdString()); } // -------------------------------------------------------------------------------------------- // Console Functions // -------------------------------------------------------------------------------------------- DefineEngineMethod( AIPlayer, stop, void, ( ),, "@brief Tells the AIPlayer to stop moving.\n\n") { object->stopMove(); } DefineEngineMethod( AIPlayer, clearAim, void, ( ),, "@brief Use this to stop aiming at an object or a point.\n\n" "@see setAimLocation()\n" "@see setAimObject()\n") { object->clearAim(); } DefineEngineMethod( AIPlayer, setMoveSpeed, void, ( F32 speed ),, "@brief Sets the move speed for an AI object.\n\n" "@param speed A speed multiplier between 0.0 and 1.0. " "This is multiplied by the AIPlayer's base movement rates (as defined in " "its PlayerData datablock)\n\n" "@see getMoveDestination()\n") { object->setMoveSpeed(speed); } DefineEngineMethod( AIPlayer, getMoveSpeed, F32, ( ),, "@brief Gets the move speed of an AI object.\n\n" "@return A speed multiplier between 0.0 and 1.0.\n\n" "@see setMoveSpeed()\n") { return object->getMoveSpeed(); } DefineEngineMethod( AIPlayer, setMoveDestination, void, ( Point3F goal, bool slowDown ), ( true ), "@brief Tells the AI to move to the location provided\n\n" "@param goal Coordinates in world space representing location to move to.\n" "@param slowDown A boolean value. If set to true, the bot will slow down " "when it gets within 5-meters of its move destination. If false, the bot " "will stop abruptly when it reaches the move destination. By default, this is true.\n\n" "@note Upon reaching a move destination, the bot will clear its move destination and " "calls to getMoveDestination will return \"0 0 0\"." "@see getMoveDestination()\n") { object->setMoveDestination( goal, slowDown); } DefineEngineMethod( AIPlayer, getMoveDestination, Point3F, (),, "@brief Get the AIPlayer's current destination.\n\n" "@return Returns a point containing the \"x y z\" position " "of the AIPlayer's current move destination. If no move destination " "has yet been set, this returns \"0 0 0\"." "@see setMoveDestination()\n") { return object->getMoveDestination(); } DefineEngineMethod( AIPlayer, setAimLocation, void, ( Point3F target ),, "@brief Tells the AIPlayer to aim at the location provided.\n\n" "@param target An \"x y z\" position in the game world to target.\n\n" "@see getAimLocation()\n") { object->setAimLocation(target); } DefineEngineMethod( AIPlayer, getAimLocation, Point3F, (),, "@brief Returns the point the AIPlayer is aiming at.\n\n" "This will reflect the position set by setAimLocation(), " "or the position of the object that the bot is now aiming at. " "If the bot is not aiming at anything, this value will " "change to whatever point the bot's current line-of-sight intercepts." "@return World space coordinates of the object AI is aiming at. Formatted as \"X Y Z\".\n\n" "@see setAimLocation()\n" "@see setAimObject()\n") { return object->getAimLocation(); } ConsoleDocFragment _setAimObject( "@brief Sets the AIPlayer's target object. May optionally set an offset from target location\n\n" "@param targetObject The object to target\n" "@param offset Optional three-element offset vector which will be added to the position of the aim object.\n\n" "@tsexample\n" "// Without an offset\n" "%ai.setAimObject(%target);\n\n" "// With an offset\n" "// Cause our AI object to aim at the target\n" "// offset (0, 0, 1) so you don't aim at the target's feet\n" "%ai.setAimObject(%target, \"0 0 1\");\n" "@endtsexample\n\n" "@see getAimLocation()\n" "@see getAimObject()\n" "@see clearAim()\n", "AIPlayer", "void setAimObject(GameBase targetObject, Point3F offset);" ); DefineConsoleMethod( AIPlayer, setAimObject, void, ( const char * objName, Point3F offset ), (Point3F::Zero), "( GameBase obj, [Point3F offset] )" "Sets the bot's target object. Optionally set an offset from target location." "@hide") { // Find the target GameBase *targetObject; if( Sim::findObject( objName, targetObject ) ) { object->setAimObject( targetObject, offset ); } else object->setAimObject( 0, offset ); } DefineEngineMethod( AIPlayer, getAimObject, S32, (),, "@brief Gets the object the AIPlayer is targeting.\n\n" "@return Returns -1 if no object is being aimed at, " "or the SimObjectID of the object the AIPlayer is aiming at.\n\n" "@see setAimObject()\n") { GameBase* obj = object->getAimObject(); return obj? obj->getId(): -1; } bool AIPlayer::checkInLos(GameBase* target, bool _useMuzzle, bool _checkEnabled) { if (!isServerObject()) return false; if (!target) { target = mAimObject.getPointer(); if (!target) return false; } if (_checkEnabled) { if (target->getTypeMask() & ShapeBaseObjectType) { ShapeBase *shapeBaseCheck = static_cast(target); if (shapeBaseCheck) if (shapeBaseCheck->getDamageState() != Enabled) return false; } else return false; } RayInfo ri; disableCollision(); S32 mountCount = target->getMountedObjectCount(); for (S32 i = 0; i < mountCount; i++) { target->getMountedObject(i)->disableCollision(); } Point3F checkPoint ; if (_useMuzzle) getMuzzlePointAI(0, &checkPoint ); else { MatrixF eyeMat; getEyeTransform(&eyeMat); eyeMat.getColumn(3, &checkPoint ); } bool hit = !gServerContainer.castRay(checkPoint, target->getBoxCenter(), sAIPlayerLoSMask, &ri); enableCollision(); for (S32 i = 0; i < mountCount; i++) { target->getMountedObject(i)->enableCollision(); } return hit; } DefineEngineMethod(AIPlayer, checkInLos, bool, (ShapeBase* obj, bool useMuzzle, bool checkEnabled),(NULL, false, false), "@brief Check whether an object is in line of sight.\n" "@obj Object to check. (If blank, it will check the current target).\n" "@useMuzzle Use muzzle position. Otherwise use eye position. (defaults to false).\n" "@checkEnabled check whether the object can take damage and if so is still alive.(Defaults to false)\n") { return object->checkInLos(obj, useMuzzle, checkEnabled); } bool AIPlayer::checkInFoV(GameBase* target, F32 camFov, bool _checkEnabled) { if (!isServerObject()) return false; if (!target) { target = mAimObject.getPointer(); if (!target) return false; } if (_checkEnabled) { if (target->getTypeMask() & ShapeBaseObjectType) { ShapeBase *shapeBaseCheck = static_cast(target); if (shapeBaseCheck) if (shapeBaseCheck->getDamageState() != Enabled) return false; } else return false; } MatrixF cam = getTransform(); Point3F camPos; VectorF camDir; cam.getColumn(3, &camPos); cam.getColumn(1, &camDir); camFov = mDegToRad(camFov) / 2; Point3F shapePos = target->getBoxCenter(); VectorF shapeDir = shapePos - camPos; // Test to see if it's within our viewcone, this test doesn't // actually match the viewport very well, should consider // projection and box test. shapeDir.normalize(); F32 dot = mDot(shapeDir, camDir); return (dot > mCos(camFov)); } DefineEngineMethod(AIPlayer, checkInFoV, bool, (ShapeBase* obj, F32 fov, bool checkEnabled), (NULL, 45.0f, false), "@brief Check whether an object is within a specified veiw cone.\n" "@obj Object to check. (If blank, it will check the current target).\n" "@fov view angle in degrees.(Defaults to 45)\n" "@checkEnabled check whether the object can take damage and if so is still alive.(Defaults to false)\n") { return object->checkInFoV(obj, fov, checkEnabled); }