diff --git a/Engine/source/CMakeLists.txt b/Engine/source/CMakeLists.txt index 35d9d81d7..34991cce7 100644 --- a/Engine/source/CMakeLists.txt +++ b/Engine/source/CMakeLists.txt @@ -58,7 +58,7 @@ torqueAddSourceDirectories("platform" "platform/threads" "platform/async" torqueAddSourceDirectories("platform/nativeDialogs") # Handle T3D -torqueAddSourceDirectories( "T3D" "T3D/assets" "T3D/decal" "T3D/examples" "T3D/fps" "T3D/fx" +torqueAddSourceDirectories( "T3D" "T3D/AI" "T3D/assets" "T3D/decal" "T3D/examples" "T3D/fps" "T3D/fx" "T3D/gameBase" "T3D/gameBase/std" "T3D/lighting" "T3D/physics" diff --git a/Engine/source/T3D/AI/AIAimTarget.cpp b/Engine/source/T3D/AI/AIAimTarget.cpp new file mode 100644 index 000000000..35c7d927e --- /dev/null +++ b/Engine/source/T3D/AI/AIAimTarget.cpp @@ -0,0 +1,226 @@ +//----------------------------------------------------------------------------- +// 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 "AIAimTarget.h" +#include "AIController.h" + +static U32 sAILoSMask = TerrainObjectType | StaticShapeObjectType | StaticObjectType; + +F32 AIAimTarget::getTargetDistance(SceneObject* target, bool _checkEnabled) +{ + if (!target) + { + target = mObj.getPointer(); + if (!target) + return F32_MAX; + } + + if (_checkEnabled) + { + if (target->getTypeMask() & ShapeBaseObjectType) + { + ShapeBase* shapeBaseCheck = static_cast(target); + if (shapeBaseCheck) + if (shapeBaseCheck->getDamageState() != ShapeBase::Enabled) return false; + } + else + return F32_MAX; + } + + return (getPosition() - target->getPosition()).len(); +} + +bool AIAimTarget::checkInLos(SceneObject* target, bool _useMuzzle, bool _checkEnabled) +{ + ShapeBase* sbo = dynamic_cast(getCtrl()->getAIInfo()->mObj.getPointer()); + if (!target) + { + target = dynamic_cast(mObj.getPointer()); + if (!target) + return false; + } + if (_checkEnabled) + { + if (target->getTypeMask() & ShapeBaseObjectType) + { + ShapeBase* shapeBaseCheck = static_cast(target); + if (shapeBaseCheck) + if (shapeBaseCheck->getDamageState() != ShapeBase::Enabled) return false; + } + else + return false; + } + + RayInfo ri; + + sbo->disableCollision(); + + S32 mountCount = target->getMountedObjectCount(); + for (S32 i = 0; i < mountCount; i++) + { + target->getMountedObject(i)->disableCollision(); + } + + Point3F checkPoint; + if (_useMuzzle) + sbo->getMuzzlePoint(0, &checkPoint); + else + { + MatrixF eyeMat; + sbo->getEyeTransform(&eyeMat); + eyeMat.getColumn(3, &checkPoint); + } + + bool hit = !gServerContainer.castRay(checkPoint, target->getBoxCenter(), sAILoSMask, &ri); + sbo->enableCollision(); + + for (S32 i = 0; i < mountCount; i++) + { + target->getMountedObject(i)->enableCollision(); + } + return hit; +} + +bool AIAimTarget::checkInFoV(SceneObject* target, F32 camFov, bool _checkEnabled) +{ + ShapeBase* sbo = dynamic_cast(getCtrl()->getAIInfo()->mObj.getPointer()); + if (!target) + { + target = dynamic_cast(mObj.getPointer()); + if (!target) + return false; + } + if (_checkEnabled) + { + if (target->getTypeMask() & ShapeBaseObjectType) + { + ShapeBase* shapeBaseCheck = static_cast(target); + if (shapeBaseCheck) + if (shapeBaseCheck->getDamageState() != ShapeBase::Enabled) return false; + } + else + return false; + } + + MatrixF cam = sbo->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(AIController, 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->setAim(target); +} + +DefineEngineMethod(AIController, 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->getAim()->getPosition(); +} + +DefineEngineMethod(AIController, 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 + SceneObject* targetObject; + if (Sim::findObject(objName, targetObject)) + { + + object->setAim(targetObject, 0.0f, offset); + } + else + object->setAim(0, 0.0f, offset); +} + +DefineEngineMethod(AIController, clearAim, void, (), , "clears the bot's target.") +{ + object->clearAim(); +} + +DefineEngineMethod(AIController, 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") +{ + SceneObject* obj = dynamic_cast(object->getAim()->mObj.getPointer()); + return obj ? obj->getId() : -1; +} + + +DefineEngineMethod(AIController, getTargetDistance, F32, (SceneObject* obj, bool checkEnabled), (nullAsType(), false), + "@brief The distance to a given target.\n" + "@obj Object to check. (If blank, it will check the current target).\n" + "@checkEnabled check whether the object can take damage and if so is still alive.(Defaults to false)\n") +{ + return object->getAim()->getTargetDistance(obj, checkEnabled); +} + +DefineEngineMethod(AIController, checkInLos, bool, (SceneObject* obj, bool useMuzzle, bool checkEnabled), (nullAsType(), 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->getAim()->checkInLos(obj, useMuzzle, checkEnabled); +} + +DefineEngineMethod(AIController, checkInFoV, bool, (SceneObject* obj, F32 fov, bool checkEnabled), (nullAsType(), 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->getAim()->checkInFoV(obj, fov, checkEnabled); +} diff --git a/Engine/source/T3D/AI/AIAimTarget.h b/Engine/source/T3D/AI/AIAimTarget.h new file mode 100644 index 000000000..4c609af27 --- /dev/null +++ b/Engine/source/T3D/AI/AIAimTarget.h @@ -0,0 +1,41 @@ +//----------------------------------------------------------------------------- +// 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. +//----------------------------------------------------------------------------- +#ifndef _AIAIMTARGET_H_ +#define _AIAIMTARGET_H_ + +#include "AIInfo.h" +struct AIAimTarget : public AIInfo +{ + typedef AIInfo Parent; + Point3F mAimOffset; + bool mTargetInLOS; // Is target object visible? + 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); + AIAimTarget() = delete; + AIAimTarget(AIController* controller) : Parent(controller) { mTargetInLOS = false; }; + AIAimTarget(AIController* controller, SimObjectPtr objIn, F32 radIn) : Parent(controller, objIn, radIn) { mTargetInLOS = false; }; + AIAimTarget(AIController* controller, Point3F pointIn, F32 radIn) : Parent(controller, pointIn, radIn) { mTargetInLOS = false; }; +}; + +#endif diff --git a/Engine/source/T3D/AI/AIController.cpp b/Engine/source/T3D/AI/AIController.cpp new file mode 100644 index 000000000..e54b173bb --- /dev/null +++ b/Engine/source/T3D/AI/AIController.cpp @@ -0,0 +1,916 @@ +//----------------------------------------------------------------------------- +// 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 "AIController.h" +#include "T3D/player.h" +#include "T3D/rigidShape.h" +#include "T3D/vehicles/wheeledVehicle.h" +#include "T3D/vehicles/flyingVehicle.h" + + +IMPLEMENT_CONOBJECT(AIController); + +//----------------------------------------------------------------------------- +void AIController::throwCallback(const char* name) +{ + //Con::warnf("throwCallback: %s", name); + Con::executef(mControllerData, name, getIdString()); //controller data callbacks + + GameBase* gbo = dynamic_cast(getAIInfo()->mObj.getPointer()); + if (!gbo) return; + Con::executef(gbo->getDataBlock(), name, getAIInfo()->mObj->getIdString()); //legacy support for object db callbacks +} + +void AIController::initPersistFields() +{ + addProtectedField("ControllerData", TYPEID< AIControllerData >(), Offset(mControllerData, AIController), + &setControllerDataProperty, &defaultProtectedGetFn, + "Script datablock used for game objects."); + addFieldV("MoveSpeed", TypeRangedF32, Offset(mMovement.mMoveSpeed, AIController), &CommonValidators::PositiveFloat, + "@brief default move sepeed."); +} + +bool AIController::setControllerDataProperty(void* obj, const char* index, const char* db) +{ + if (db == NULL || !db[0]) + { + Con::errorf("AIController::setControllerDataProperty - Can't unset ControllerData on AIController objects"); + return false; + } + + AIController* object = static_cast(obj); + AIControllerData* data; + if (Sim::findObject(db, data)) + { + object->mControllerData = data; + return true; + } + Con::errorf("AIController::setControllerDataProperty - Could not find ControllerData \"%s\"", db); + return false; +} + +void AIController::setGoal(AIInfo* targ) +{ + if (mGoal) { delete(mGoal); mGoal = NULL; } + + if (targ->mObj.isValid()) + { + delete(mGoal); + mGoal = new AIGoal(this, targ->mObj, targ->mRadius); + } + else if (targ->mPosSet) + { + delete(mGoal); + mGoal = new AIGoal(this, targ->mPosition, targ->mRadius); + } +} + +void AIController::setGoal(Point3F loc, F32 rad) +{ + if (mGoal) delete(mGoal); + mGoal = new AIGoal(this, loc, rad); +} + +void AIController::setGoal(SimObjectPtr objIn, F32 rad) +{ + if (mGoal) delete(mGoal); + mGoal = new AIGoal(this, objIn, rad); +} + +void AIController::setAim(Point3F loc, F32 rad, Point3F offset) +{ + if (mAimTarget) delete(mAimTarget); + mAimTarget = new AIAimTarget(this, loc, rad); + mAimTarget->mAimOffset = offset; +} + +void AIController::setAim(SimObjectPtr objIn, F32 rad, Point3F offset) +{ + if (mAimTarget) delete(mAimTarget); + mAimTarget = new AIAimTarget(this, objIn, rad); + mAimTarget->mAimOffset = offset; +} + +bool AIController::getAIMove(Move* movePtr) +{ + *movePtr = NullMove; + ShapeBase* sbo = dynamic_cast(getAIInfo()->mObj.getPointer()); + if (!sbo) return false; + + // Use the eye as the current position. + MatrixF eye; + sbo->getEyeTransform(&eye); + Point3F location = eye.getPosition(); + Point3F rotation = sbo->getTransform().toEuler(); + + // 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 (getAim() && getAim()->mObj) + { + GameBase* gbo = dynamic_cast(getAIInfo()->mObj.getPointer()); + if (getAim()->checkInLos(gbo)) + { + if (!getAim()->mTargetInLOS) + { + throwCallback("onTargetEnterLOS"); + getAim()->mTargetInLOS = true; + } + } + else if (getAim()->mTargetInLOS) + { + throwCallback("onTargetExitLOS"); + getAim()->mTargetInLOS = false; + } + } + + if (sbo->getDamageState() == ShapeBase::Enabled && getGoal()) + { +#ifdef TORQUE_NAVIGATION_ENABLED + if (mMovement.mMoveState != ModeStop) + getNav()->updateNavMesh(); + + if (getNav()->mPathData.path.isNull()) + { + if (getGoal()->getDist() > mControllerData->mFollowTolerance) + { + if (getGoal()->mObj.isValid()) + getNav()->followObject(getGoal()->mObj, mControllerData->mFollowTolerance); + else if (getGoal()->mPosSet) + getNav()->setPathDestination(getGoal()->getPosition(true)); + } + } + else + { + if (getGoal()->getDist() > mControllerData->mFollowTolerance) + { + SceneObject* obj = getAIInfo()->mObj->getObjectMount(); + if (!obj) + { + obj = getAIInfo()->mObj; + } + RayInfo info; + if (obj->getContainer()->castRay(obj->getPosition(), obj->getPosition() - Point3F(0, 0, mControllerData->mHeightTolerance), StaticShapeObjectType, &info)) + { + getNav()->repath(); + } + getGoal()->mInRange = false; + } + if (getGoal()->getDist() < mControllerData->mFollowTolerance ) + { + getNav()->clearPath(); + mMovement.mMoveState = ModeStop; + + if (!getGoal()->mInRange) + { + getGoal()->mInRange = true; + throwCallback("onTargetInRange"); + } + else getGoal()->mInRange = false; + } + else + { + if (getGoal()->getDist() < mControllerData->mAttackRadius ) + { + if (!getGoal()->mInFiringRange) + { + getGoal()->mInFiringRange = true; + throwCallback("onTargetInFiringRange"); + } + } + else getGoal()->mInFiringRange = false; + } + } +#else + if (getGoal()->getDist() > mControllerData->mFollowTolerance) + { + if (getGoal()->mObj.isValid()) + getNav()->followObject(getGoal()->mObj, mControllerData->mFollowTolerance); + else if (getGoal()->mPosSet) + getNav()->setPathDestination(getGoal()->getPosition(true)); + + getGoal()->mInRange = false; + } + if (getGoal()->getDist() < mControllerData->mFollowTolerance) + { + mMovement.mMoveState = ModeStop; + + if (!getGoal()->mInRange) + { + getGoal()->mInRange = true; + throwCallback("onTargetInRange"); + } + else getGoal()->mInRange = false; + } + else + { + if (getGoal()->getDist() < mControllerData->mAttackRadius) + { + if (!getGoal()->mInFiringRange) + { + getGoal()->mInFiringRange = true; + throwCallback("onTargetInFiringRange"); + } + } + else getGoal()->mInFiringRange = false; + } +#endif // TORQUE_NAVIGATION_ENABLED + } + // Orient towards the aim point, aim object, or towards + // our destination. + if (getAim() || mMovement.mMoveState != ModeStop) + { + // Update the aim position if we're aiming for an object or explicit position + if (getAim()) + mMovement.mAimLocation = getAim()->getPosition(); + else + mMovement.mAimLocation = getNav()->getMoveDestination(); + + mControllerData->resolveYawPtr(this, location, movePtr); + mControllerData->resolvePitchPtr(this, location, movePtr); + mControllerData->resolveRollPtr(this, location, movePtr); + + if (mMovement.mMoveState != AIController::ModeStop) + { + F32 xDiff = getNav()->getMoveDestination().x - location.x; + F32 yDiff = getNav()->getMoveDestination().y - location.y; + if (mFabs(xDiff) < mControllerData->mMoveTolerance && mFabs(yDiff) < mControllerData->mMoveTolerance) + { + getNav()->onReachDestination(); + } + else + { + mControllerData->resolveSpeedPtr(this, location, movePtr); + mControllerData->resolveStuckPtr(this); + } + } + } + + mControllerData->resolveTriggerStatePtr(this, movePtr); + + getAIInfo()->mLastPos = getAIInfo()->getPosition(); + return true; +} + +void AIController::clearCover() +{ + // Notify cover that we are no longer on our way. + if (getCover() && !getCover()->mCoverPoint.isNull()) + getCover()->mCoverPoint->setOccupied(false); + SAFE_DELETE(mCover); +} + +void AIController::Movement::stopMove() +{ + mMoveState = ModeStop; +#ifdef TORQUE_NAVIGATION_ENABLED + getCtrl()->getNav()->clearPath(); + getCtrl()->clearCover(); + getCtrl()->getNav()->clearFollow(); +#endif +} +void AIController::Movement::onStuck() +{ + mMoveState = AIController::ModeStuck; + getCtrl()->throwCallback("onMoveStuck"); +#ifdef TORQUE_NAVIGATION_ENABLED + if (!getCtrl()->getNav()->getPath().isNull()) + getCtrl()->getNav()->repath(); +#endif +} + +DefineEngineMethod(AIController, 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 AIController controlled object's base movement rates (as defined in " + "its PlayerData datablock)\n\n" + + "@see getMoveDestination()\n") +{ + object->mMovement.setMoveSpeed(speed); +} + +DefineEngineMethod(AIController, 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->mMovement.getMoveSpeed(); +} + +DefineEngineMethod(AIController, stop, void, (), , + "@brief Tells the AIController controlled object 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); +void AIControllerData::resolveYaw(AIController* obj, Point3F location, Move* move) +{ + F32 xDiff = obj->mMovement.mAimLocation.x - location.x; + F32 yDiff = obj->mMovement.mAimLocation.y - location.y; + Point3F rotation = obj->getAIInfo()->mObj->getTransform().toEuler(); + + 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; + + move->yaw = yawDiff; + } +} + + +void AIControllerData::resolveRoll(AIController* obj, Point3F location, Move* movePtr) +{ +} + +void AIControllerData::resolveSpeed(AIController* obj, Point3F location, Move* movePtr) +{ + F32 xDiff = obj->getNav()->getMoveDestination().x - location.x; + F32 yDiff = obj->getNav()->getMoveDestination().y - location.y; + Point3F rotation = obj->getAIInfo()->mObj->getTransform().toEuler(); + + // Build move direction in world space + if (mIsZero(xDiff)) + movePtr->y = (location.y > obj->getNav()->getMoveDestination().y) ? -1.0f : 1.0f; + else + { + if (mIsZero(yDiff)) + movePtr->x = (location.x > obj->getNav()->getMoveDestination().x) ? -1.0f : 1.0f; + else + { + if (mFabs(xDiff) > mFabs(yDiff)) + { + F32 value = mFabs(yDiff / xDiff); + movePtr->y = (location.y > obj->getNav()->getMoveDestination().y) ? -value : value; + movePtr->x = (location.x > obj->getNav()->getMoveDestination().x) ? -1.0f : 1.0f; + } + else + { + F32 value = mFabs(xDiff / yDiff); + movePtr->x = (location.x > obj->getNav()->getMoveDestination().x) ? -value : value; + movePtr->y = (location.y > obj->getNav()->getMoveDestination().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 (obj->mMovement.mMoveSlowdown) + { + F32 speed = obj->mMovement.mMoveSpeed; + F32 dist = mSqrt(xDiff * xDiff + yDiff * yDiff); + F32 maxDist = mMoveTolerance * 2; + if (dist < maxDist) + speed *= dist / maxDist; + movePtr->x *= speed; + movePtr->y *= speed; + } + else + { + movePtr->x *= obj->mMovement.mMoveSpeed; + movePtr->y *= obj->mMovement.mMoveSpeed; + } +} + +void AIControllerData::resolveTriggerState(AIController* obj, Move* movePtr) +{ + //check for scripted overides + for (U32 slot = 0; slot < MaxTriggerKeys; slot++) + { + movePtr->trigger[slot] = obj->mTriggerState.mMoveTriggers[slot]; + } +} + +void AIControllerData::resolveStuck(AIController* obj) +{ + if (obj->mMovement.mMoveState < AIController::ModeSlowing) return; + if (!obj->getGoal()) return; + ShapeBase* sbo = dynamic_cast(obj->getAIInfo()->mObj.getPointer()); + // Don't check for ai stuckness if animation during + // an anim-clip effect override. + if (sbo->getDamageState() == ShapeBase::Enabled && !(sbo->anim_clip_flags & ShapeBase::ANIM_OVERRIDDEN) && !sbo->isAnimationLocked()) + { + // We should check to see if we are stuck... + F32 locationDelta = (obj->getAIInfo()->getPosition() - obj->getAIInfo()->mLastPos).len(); + if (locationDelta < mMoveStuckTolerance) + { + if (obj->mMovement.mMoveStuckTestCountdown > 0) + --obj->mMovement.mMoveStuckTestCountdown; + else + { + // 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 (obj->mMovement.mMoveState != AIController::ModeSlowing || locationDelta == 0) + { + obj->mMovement.onStuck(); + obj->mMovement.mMoveStuckTestCountdown = obj->mControllerData->mMoveStuckTestDelay; + } + } + } + } +} + +AIControllerData::AIControllerData() +{ + mMoveTolerance = 0.25f; + mAttackRadius = 2.0f; + mMoveStuckTolerance = 0.01f; + mMoveStuckTestDelay = 30; + mHeightTolerance = 0.001f; + mFollowTolerance = 1.0f; + +#ifdef TORQUE_NAVIGATION_ENABLED + mLinkTypes = LinkData(AllFlags); + mNavSize = AINavigation::Regular; + mFlocking.mChance = 90; + mFlocking.mMin = 1.0f; + mFlocking.mMax = 3.0f; + mFlocking.mSideStep = 0.01f; +#endif + resolveYawPtr.bind(this, &AIControllerData::resolveYaw); + resolvePitchPtr.bind(this, &AIControllerData::resolvePitch); + resolveRollPtr.bind(this, &AIControllerData::resolveRoll); + resolveSpeedPtr.bind(this, &AIControllerData::resolveSpeed); + resolveTriggerStatePtr.bind(this, &AIControllerData::resolveTriggerState); + resolveStuckPtr.bind(this, &AIControllerData::resolveStuck); +} + +AIControllerData::AIControllerData(const AIControllerData& other, bool temp_clone) : SimDataBlock(other, temp_clone) +{ + mMoveTolerance = other.mMoveTolerance; + mAttackRadius = other.mAttackRadius; + mMoveStuckTolerance = other.mMoveStuckTolerance; + mMoveStuckTestDelay = other.mMoveStuckTestDelay; + mHeightTolerance = other.mHeightTolerance; + mFollowTolerance = other.mFollowTolerance; + +#ifdef TORQUE_NAVIGATION_ENABLED + mLinkTypes = other.mLinkTypes; + mNavSize = other.mNavSize; + mFlocking.mChance = other.mFlocking.mChance; + mFlocking.mMin = other.mFlocking.mMin; + mFlocking.mMax = other.mFlocking.mMax; + mFlocking.mSideStep = other.mFlocking.mSideStep; +#endif + + resolveYawPtr.bind(this, &AIControllerData::resolveYaw); + resolvePitchPtr.bind(this, &AIControllerData::resolvePitch); + resolveRollPtr.bind(this, &AIControllerData::resolveRoll); + resolveSpeedPtr.bind(this, &AIControllerData::resolveSpeed); + resolveTriggerStatePtr.bind(this, &AIControllerData::resolveTriggerState); + resolveStuckPtr.bind(this, &AIControllerData::resolveStuck); +} + +void AIControllerData::initPersistFields() +{ + docsURL; + addGroup("AI"); + + addFieldV("moveTolerance", TypeRangedF32, Offset(mMoveTolerance, AIControllerData), &CommonValidators::PositiveFloat, + "@brief Distance from destination before stopping.\n\n" + "When the AIController controlled object 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 AIController controlled object 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"); + + addFieldV("followTolerance", TypeRangedF32, Offset(mFollowTolerance, AIControllerData), &CommonValidators::PositiveFloat, + "@brief Distance from destination before stopping.\n\n" + "When the AIController controlled object 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 AIController controlled object 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"); + + addFieldV("AttackRadius", TypeRangedF32, Offset(mAttackRadius, AIControllerData), &CommonValidators::PositiveFloat, + "@brief Distance considered in firing range for callback purposes."); + + addFieldV("moveStuckTolerance", TypeRangedF32, Offset(mMoveStuckTolerance, AIControllerData), &CommonValidators::PositiveFloat, + "@brief Distance tolerance on stuck check.\n\n" + "When the AIController controlled object controlled object is moving to a given destination, if it ever moves less than " + "this tolerance during a single tick, the AIController controlled object is considered stuck. At this point " + "the onMoveStuck() callback is called on the datablock.\n"); + + addFieldV("HeightTolerance", TypeRangedF32, Offset(mHeightTolerance, AIControllerData), &CommonValidators::PositiveFloat, + "@brief Distance from destination before stopping.\n\n" + "When the AIController controlled object 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 AIController controlled object 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"); + + addFieldV("moveStuckTestDelay", TypeRangedS32, Offset(mMoveStuckTestDelay, AIControllerData), &CommonValidators::PositiveInt, + "@brief The number of ticks to wait before testing if the AIController controlled object is stuck.\n\n" + "When the AIController controlled object is asked to move, this property is the number of ticks to wait " + "before the AIController controlled object starts to check if it is stuck. This delay allows the AIController controlled object " + "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"); + +#ifdef TORQUE_NAVIGATION_ENABLED + addGroup("Pathfinding"); + addFieldV("FlockChance", TypeRangedS32, Offset(mFlocking.mChance, AIControllerData), &CommonValidators::S32Percent, + "@brief chance of flocking."); + addFieldV("FlockMin", TypeRangedF32, Offset(mFlocking.mMin, AIControllerData), &CommonValidators::PositiveFloat, + "@brief min flocking separation distance."); + addFieldV("FlockMax", TypeRangedF32, Offset(mFlocking.mMax, AIControllerData), &CommonValidators::PositiveFloat, + "@brief max flocking clustering distance."); + addFieldV("FlockSideStep", TypeRangedF32, Offset(mFlocking.mSideStep, AIControllerData), &CommonValidators::PositiveFloat, + "@brief Distance from destination before we stop moving out of the way."); + + addField("allowWalk", TypeBool, Offset(mLinkTypes.walk, AIControllerData), + "Allow the character to walk on dry land."); + addField("allowJump", TypeBool, Offset(mLinkTypes.jump, AIControllerData), + "Allow the character to use jump links."); + addField("allowDrop", TypeBool, Offset(mLinkTypes.drop, AIControllerData), + "Allow the character to use drop links."); + addField("allowSwim", TypeBool, Offset(mLinkTypes.swim, AIControllerData), + "Allow the character to move in water."); + addField("allowLedge", TypeBool, Offset(mLinkTypes.ledge, AIControllerData), + "Allow the character to jump ledges."); + addField("allowClimb", TypeBool, Offset(mLinkTypes.climb, AIControllerData), + "Allow the character to use climb links."); + addField("allowTeleport", TypeBool, Offset(mLinkTypes.teleport, AIControllerData), + "Allow the character to use teleporters."); + + endGroup("Pathfinding"); +#endif // TORQUE_NAVIGATION_ENABLED + + Parent::initPersistFields(); +} + +void AIControllerData::packData(BitStream* stream) +{ + Parent::packData(stream); + stream->write(mMoveTolerance); + stream->write(mAttackRadius); + stream->write(mMoveStuckTolerance); + stream->write(mMoveStuckTestDelay); + stream->write(mHeightTolerance); + stream->write(mFollowTolerance); + +#ifdef TORQUE_NAVIGATION_ENABLED + //enums + stream->write(mLinkTypes.getFlags()); + stream->write((U32)mNavSize); + // end enums + stream->write(mFlocking.mChance); + stream->write(mFlocking.mMin); + stream->write(mFlocking.mMax); + stream->write(mFlocking.mSideStep); +#endif // TORQUE_NAVIGATION_ENABLED +}; + +void AIControllerData::unpackData(BitStream* stream) +{ + Parent::unpackData(stream); + + stream->read(&mMoveTolerance); + stream->read(&mAttackRadius); + stream->read(&mMoveStuckTolerance); + stream->read(&mMoveStuckTestDelay); + stream->read(&mHeightTolerance); + stream->read(&mFollowTolerance); + +#ifdef TORQUE_NAVIGATION_ENABLED + //enums + U16 linkFlags; + stream->read(&linkFlags); + mLinkTypes = LinkData(linkFlags); + U32 navSize; + stream->read(&navSize); + mNavSize = (AINavigation::NavSize)(navSize); + // end enums + stream->read(&(mFlocking.mChance)); + stream->read(&(mFlocking.mMin)); + stream->read(&(mFlocking.mMax)); + stream->read(&(mFlocking.mSideStep)); +#endif // TORQUE_NAVIGATION_ENABLED +}; +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +IMPLEMENT_CO_DATABLOCK_V1(AIPlayerControllerData); +void AIPlayerControllerData::resolvePitch(AIController* obj, Point3F location, Move* movePtr) +{ + Player* po = dynamic_cast(obj->getAIInfo()->mObj.getPointer()); + if (!po) return;//not a player + + if (obj->getAim() || obj->mMovement.mMoveState != AIController::ModeStop) + { + // Next do pitch. + if (!obj->getAim()) + { + // Level out if were just looking at our next way point. + Point3F headRotation = po->getHeadRotation(); + movePtr->pitch = -headRotation.x; + } + else + { + F32 xDiff = obj->mMovement.mAimLocation.x - location.x; + F32 yDiff = obj->mMovement.mAimLocation.y - location.y; + // 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 = obj->mMovement.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 = po->getHeadRotation(); + movePtr->pitch = newPitch - headRotation.x; + } + } + } + else + { + // Level out if we're not doing anything else + Point3F headRotation = po->getHeadRotation(); + movePtr->pitch = -headRotation.x; + } +} + +void AIPlayerControllerData::resolveTriggerState(AIController* obj, Move* movePtr) +{ + Parent::resolveTriggerState(obj, movePtr); +#ifdef TORQUE_NAVIGATION_ENABLED + if (obj->getNav()->mJump == AINavigation::Now) + { + movePtr->trigger[2] = true; + obj->getNav()->mJump = AINavigation::None; + } + else if (obj->getNav()->mJump == AINavigation::Ledge) + { + // If we're not touching the ground, jump! + RayInfo info; + if (!obj->getAIInfo()->mObj->getContainer()->castRay(obj->getAIInfo()->getPosition(), obj->getAIInfo()->getPosition() - Point3F(0, 0, 0.4f), StaticShapeObjectType, &info)) + { + movePtr->trigger[2] = true; + obj->getNav()->mJump = AINavigation::None; + } + } +#endif // TORQUE_NAVIGATION_ENABLED +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +IMPLEMENT_CO_DATABLOCK_V1(AIWheeledVehicleControllerData); + +void AIWheeledVehicleControllerData::resolveYaw(AIController* obj, Point3F location, Move* movePtr) +{ + if (obj->mMovement.mMoveState < AIController::ModeSlowing) return; + WheeledVehicle* wvo = dynamic_cast(obj->getAIInfo()->mObj.getPointer()); + if (!wvo) + { + //cover the case of a connection controling an object in turn controlling another + if (obj->getAIInfo()->mObj->getObjectMount()) + wvo = dynamic_cast(obj->getAIInfo()->mObj->getObjectMount()); + } + if (!wvo) return;//not a WheeledVehicle + + F32 lastYaw = wvo->getSteering().x; + + Point3F right = wvo->getTransform().getRightVector(); + right.normalize(); + Point3F aimLoc = obj->mMovement.mAimLocation; + + // Get the AI to Target vector and normalize it. + Point3F toTarg = (location + wvo->getVelocity() * TickSec) - aimLoc; + toTarg.normalize(); + + F32 dotYaw = -mDot(right, toTarg); + movePtr->yaw = -lastYaw; + + VehicleData* vd = (VehicleData*)(wvo->getDataBlock()); + F32 maxSteeringAngle = vd->maxSteeringAngle; + + if (mFabs(dotYaw) > maxSteeringAngle*1.5f) + dotYaw *= -1.0f; + + if (dotYaw > maxSteeringAngle) dotYaw = maxSteeringAngle; + if (dotYaw < -maxSteeringAngle) dotYaw = -maxSteeringAngle; + + if (mFabs(dotYaw) > 0.05f) + movePtr->yaw = dotYaw - lastYaw; +}; + +void AIWheeledVehicleControllerData::resolveSpeed(AIController* obj, Point3F location, Move* movePtr) +{ + if (obj->mMovement.mMoveState < AIController::ModeSlowing) return; + WheeledVehicle* wvo = dynamic_cast(obj->getAIInfo()->mObj.getPointer()); + if (!wvo) + { + //cover the case of a connection controling an object in turn controlling another + if (obj->getAIInfo()->mObj->getObjectMount()) + wvo = dynamic_cast(obj->getAIInfo()->mObj->getObjectMount()); + } + if (!wvo) return;//not a WheeledVehicle + + Parent::resolveSpeed(obj, location, movePtr); + + VehicleData* db = static_cast(wvo->getDataBlock()); + movePtr->x = 0; + movePtr->y *= mMax((db->maxSteeringAngle-mFabs(movePtr->yaw) / db->maxSteeringAngle),0.75f); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +IMPLEMENT_CO_DATABLOCK_V1(AIFlyingVehicleControllerData); + +void AIFlyingVehicleControllerData::initPersistFields() +{ + docsURL; + addGroup("AI"); + + addFieldV("FlightFloor", TypeRangedF32, Offset(mFlightFloor, AIFlyingVehicleControllerData), &CommonValidators::F32Range, + "@brief Min height we can target."); + + addFieldV("FlightCeiling", TypeRangedF32, Offset(mFlightCeiling, AIFlyingVehicleControllerData), &CommonValidators::F32Range, + "@brief Max height we can target."); + + endGroup("AI"); + + Parent::initPersistFields(); +} + +AIFlyingVehicleControllerData::AIFlyingVehicleControllerData(const AIFlyingVehicleControllerData& other, bool temp_clone) : AIControllerData(other, temp_clone) +{ + mFlightCeiling = other.mFlightCeiling; + mFlightFloor = other.mFlightFloor; + resolveYawPtr.bind(this, &AIFlyingVehicleControllerData::resolveYaw); + resolvePitchPtr.bind(this, &AIFlyingVehicleControllerData::resolvePitch); + resolveSpeedPtr.bind(this, &AIFlyingVehicleControllerData::resolveSpeed); +} + +void AIFlyingVehicleControllerData::resolveYaw(AIController* obj, Point3F location, Move* movePtr) +{ + if (obj->mMovement.mMoveState < AIController::ModeSlowing) return; + FlyingVehicle* fvo = dynamic_cast(obj->getAIInfo()->mObj.getPointer()); + if (!fvo) + { + //cover the case of a connection controling an object in turn controlling another + if (obj->getAIInfo()->mObj->getObjectMount()) + fvo = dynamic_cast(obj->getAIInfo()->mObj->getObjectMount()); + } + if (!fvo) return;//not a FlyingVehicle + + Point3F right = fvo->getTransform().getRightVector(); + right.normalize(); + + // Get the Target to AI vector and normalize it. + Point3F aimLoc = obj->mMovement.mAimLocation; + aimLoc.z = mClampF(aimLoc.z, mFlightFloor, mFlightCeiling); + Point3F toTarg = (location + fvo->getVelocity() * TickSec) - aimLoc; + toTarg.normalize(); + + movePtr->yaw = 0; + F32 dotYaw = -mDot(right, toTarg); + if (mFabs(dotYaw) > 0.05f) + movePtr->yaw = dotYaw; +}; + +void AIFlyingVehicleControllerData::resolvePitch(AIController* obj, Point3F location, Move* movePtr) +{ + if (obj->mMovement.mMoveState < AIController::ModeSlowing) return; + FlyingVehicle* fvo = dynamic_cast(obj->getAIInfo()->mObj.getPointer()); + if (!fvo) + { + //cover the case of a connection controling an object in turn controlling another + if (obj->getAIInfo()->mObj->getObjectMount()) + fvo = dynamic_cast(obj->getAIInfo()->mObj->getObjectMount()); + } + if (!fvo) return;//not a FlyingVehicle + + Point3F up = fvo->getTransform().getUpVector(); + up.normalize(); + + + // Get the Target to AI vector and normalize it. + Point3F aimLoc = obj->mMovement.mAimLocation; + aimLoc.z = mClampF(aimLoc.z, mFlightFloor, mFlightCeiling); + Point3F toTarg = (location + fvo->getVelocity() * TickSec) - aimLoc; + toTarg.normalize(); + + movePtr->pitch = 0.0f; + F32 dotPitch = mDot(up, toTarg); + if (mFabs(dotPitch) > 0.05f) + movePtr->pitch = dotPitch * M_2PI_F; + +} + +void AIFlyingVehicleControllerData::resolveSpeed(AIController* obj, Point3F location, Move* movePtr) +{ + if (obj->mMovement.mMoveState < AIController::ModeSlowing) return; + FlyingVehicle* fvo = dynamic_cast(obj->getAIInfo()->mObj.getPointer()); + if (!fvo) + { + //cover the case of a connection controling an object in turn controlling another + if (obj->getAIInfo()->mObj->getObjectMount()) + fvo = dynamic_cast(obj->getAIInfo()->mObj->getObjectMount()); + } + if (!fvo) return;//not a FlyingVehicle + + movePtr->x = 0; + movePtr->y = obj->mMovement.mMoveSpeed; +} diff --git a/Engine/source/T3D/AI/AIController.h b/Engine/source/T3D/AI/AIController.h new file mode 100644 index 000000000..18d95e210 --- /dev/null +++ b/Engine/source/T3D/AI/AIController.h @@ -0,0 +1,247 @@ +//----------------------------------------------------------------------------- +// 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. +//----------------------------------------------------------------------------- +#ifndef _AICONTROLLER_H_ +#define _AICONTROLLER_H_ +#include "navigation/coverPoint.h" +#include "AIInfo.h" +#include "AIGoal.h" +#include "AIAimTarget.h" +#include "AICover.h" +#include "AINavigation.h" +class AIControllerData; + +//----------------------------------------------------------------------------- +class AIController : public SimObject { + + typedef SimObject Parent; + +public: + AIControllerData* mControllerData; +protected: + static bool setControllerDataProperty(void* object, const char* index, const char* data); +public: + enum MoveState { + ModeStop, // AI has stopped moving. + ModeStuck, // AI is stuck, but wants to move. + ModeSlowing, // AI is slowing down as it reaches it's destination. + ModeMove, // AI is currently moving. + ModeReverse // AI is reversing + }; + +private: + AIInfo*mAIInfo; +public: + void setAIInfo(SimObjectPtr objIn, F32 rad = 0.0f) { delete(mAIInfo); mAIInfo = new AIInfo(this, objIn, rad); } + AIInfo* getAIInfo() { return mAIInfo; } +private: + AIGoal* mGoal; +public: + void setGoal(AIInfo* targ); + void setGoal(Point3F loc, F32 rad = 0.0f); + void setGoal(SimObjectPtr objIn, F32 rad = 0.0f); + AIGoal* getGoal() { return mGoal; } + void clearGoal() { SAFE_DELETE(mGoal); } +private: + AIAimTarget* mAimTarget; +public: + void setAim(Point3F loc, F32 rad = 0.0f, Point3F offset = Point3F(0.0f, 0.0f, 0.0f)); + void setAim(SimObjectPtr objIn, F32 rad = 0.0f, Point3F offset = Point3F(0.0f, 0.0f, 0.0f)); + AIAimTarget* getAim() { return mAimTarget; } + void clearAim() { SAFE_DELETE(mAimTarget); } +private: + AICover* mCover; +public: + void setCover(Point3F loc, F32 rad = 0.0f) { delete(mCover); mCover = new AICover(this, loc, rad); } + void setCover(SimObjectPtr objIn, F32 rad = 0.0f) { delete(mCover); mCover = new AICover(this, objIn, rad); } + AICover* getCover() { return mCover; } + bool findCover(const Point3F& from, F32 radius); + void clearCover(); + + // Utility Methods + void throwCallback(const char* name); + AINavigation* mNav; + AINavigation* getNav() { return mNav; }; + struct Movement + { + AIController* mControllerRef; + AIController* getCtrl() { return mControllerRef; }; + MoveState mMoveState; + F32 mMoveSpeed = 1.0; + void setMoveSpeed(F32 speed) { mMoveSpeed = speed; }; + F32 getMoveSpeed() { return mMoveSpeed; }; + bool mMoveSlowdown; // Slowdown as we near the destination + Point3F mLastLocation; // For stuck check + S32 mMoveStuckTestCountdown; // The current countdown until at AI starts to check if it is stuck + Point3F mAimLocation; + // move triggers + bool mMoveTriggers[MaxTriggerKeys]; + void stopMove(); + void onStuck(); + } mMovement; + + struct TriggerState + { + AIController* mControllerRef; + AIController* getCtrl() { return mControllerRef; }; + bool mMoveTriggers[MaxTriggerKeys]; + // Trigger sets/gets + void setMoveTrigger(U32 slot, const bool isSet = true); + bool getMoveTrigger(U32 slot) const; + void clearMoveTriggers(); + } mTriggerState; + bool getAIMove(Move* move); + + 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); + mNav = new AINavigation(this); + mGoal = NULL; + mAimTarget = NULL; + mCover = NULL; + mMovement.mMoveState = ModeStop; + }; + ~AIController() + { + SAFE_DELETE(mAIInfo); + SAFE_DELETE(mNav); + clearGoal(); + clearAim(); + clearCover(); + } + DECLARE_CONOBJECT(AIController); +}; + +//----------------------------------------------------------------------------- +class AIControllerData : public SimDataBlock { + + typedef SimDataBlock Parent; + +public: + + AIControllerData(); + AIControllerData(const AIControllerData&, bool = false); + ~AIControllerData() {}; + void packData(BitStream* stream) override; + void unpackData(BitStream* stream) override; + static void initPersistFields(); + DECLARE_CONOBJECT(AIControllerData); + + F32 mMoveTolerance; // Distance from destination point before we stop + F32 mFollowTolerance; // Distance from destination object before we stop + F32 mAttackRadius; // Distance to trigger weaponry calcs + S32 mMoveStuckTestDelay; // The number of ticks to wait before checking if the AI is stuck + F32 mMoveStuckTolerance; // Distance tolerance on stuck check + F32 mHeightTolerance; // how high above the navmesh are we before we stop trying to repath +#ifdef TORQUE_NAVIGATION_ENABLED + struct Flocking { + U32 mChance; // chance of flocking + F32 mMin; // min flocking separation distance + F32 mMax; // max flocking clustering distance + F32 mSideStep; // Distance from destination before we stop moving out of the way + } mFlocking; + + /// Types of link we can use. + LinkData mLinkTypes; + AINavigation::NavSize mNavSize; +#endif + Delegate resolveYawPtr; + void resolveYaw(AIController* obj, Point3F location, Move* movePtr); + + Delegate resolvePitchPtr; + void resolvePitch(AIController* obj, Point3F location, Move* movePtr) {}; + + Delegate resolveRollPtr; + void resolveRoll(AIController* obj, Point3F location, Move* movePtr); + + Delegate resolveSpeedPtr; + void resolveSpeed(AIController* obj, Point3F location, Move* movePtr); + + Delegate resolveTriggerStatePtr; + void resolveTriggerState(AIController* obj, Move* movePtr); + + Delegate resolveStuckPtr; + void resolveStuck(AIController* obj); +}; + +class AIPlayerControllerData : public AIControllerData +{ + typedef AIControllerData Parent; + +public: + AIPlayerControllerData() + { + resolvePitchPtr.bind(this, &AIPlayerControllerData::resolvePitch); + resolveTriggerStatePtr.bind(this, &AIPlayerControllerData::resolveTriggerState); + } + void resolvePitch(AIController* obj, Point3F location, Move* movePtr); + void resolveTriggerState(AIController* obj, Move* movePtr); + DECLARE_CONOBJECT(AIPlayerControllerData); +}; + +class AIWheeledVehicleControllerData : public AIControllerData +{ + typedef AIControllerData Parent; + +public: + AIWheeledVehicleControllerData() + { + resolveYawPtr.bind(this, &AIWheeledVehicleControllerData::resolveYaw); + resolveSpeedPtr.bind(this, &AIWheeledVehicleControllerData::resolveSpeed); + mHeightTolerance = 2.0f; + } + void resolveYaw(AIController* obj, Point3F location, Move* movePtr); + void resolveSpeed(AIController* obj, Point3F location, Move* movePtr); + DECLARE_CONOBJECT(AIWheeledVehicleControllerData); +}; + +class AIFlyingVehicleControllerData : public AIControllerData +{ + typedef AIControllerData Parent; + + F32 mFlightFloor; + F32 mFlightCeiling; +public: + AIFlyingVehicleControllerData() + { + resolveYawPtr.bind(this, &AIFlyingVehicleControllerData::resolveYaw); + resolvePitchPtr.bind(this, &AIFlyingVehicleControllerData::resolvePitch); + resolveSpeedPtr.bind(this, &AIFlyingVehicleControllerData::resolveSpeed); + mHeightTolerance = 200.0f; + mFlightCeiling = 200.0f; + mFlightFloor = 1.0; + } + AIFlyingVehicleControllerData(const AIFlyingVehicleControllerData&, bool = false); + static void initPersistFields(); + void resolveYaw(AIController* obj, Point3F location, Move* movePtr); + void resolveSpeed(AIController* obj, Point3F location, Move* movePtr); + void resolvePitch(AIController* obj, Point3F location, Move* movePtr); + + DECLARE_CONOBJECT(AIFlyingVehicleControllerData); +}; +#endif //_AICONTROLLER_H_ diff --git a/Engine/source/T3D/AI/AICover.cpp b/Engine/source/T3D/AI/AICover.cpp new file mode 100644 index 000000000..97cf82a39 --- /dev/null +++ b/Engine/source/T3D/AI/AICover.cpp @@ -0,0 +1,108 @@ +//----------------------------------------------------------------------------- +// 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 "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(obj); + if (!p || p->isOccupied()) + return; + CoverSearch* s = static_cast(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; + } +} diff --git a/Engine/source/T3D/AI/AICover.h b/Engine/source/T3D/AI/AICover.h new file mode 100644 index 000000000..e18863bcf --- /dev/null +++ b/Engine/source/T3D/AI/AICover.h @@ -0,0 +1,41 @@ +//----------------------------------------------------------------------------- +// 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. +//----------------------------------------------------------------------------- +#ifndef _AICOVER_H_ +#define _AICOVER_H_ + +#include "AIInfo.h" +#include "navigation/coverPoint.h" + + + +struct AICover : public AIInfo +{ + typedef AIInfo Parent; + /// Pointer to a cover point. + SimObjectPtr mCoverPoint; + AICover() = delete; + AICover(AIController* controller) : Parent(controller) {}; + AICover(AIController* controller, SimObjectPtr objIn, F32 radIn) : Parent(controller, objIn, radIn) { mCoverPoint = dynamic_cast(objIn.getPointer());}; + AICover(AIController* controller, Point3F pointIn, F32 radIn) : Parent(controller, pointIn, radIn) {}; +}; + +#endif diff --git a/Engine/source/T3D/AI/AIGoal.cpp b/Engine/source/T3D/AI/AIGoal.cpp new file mode 100644 index 000000000..e94fdd8ee --- /dev/null +++ b/Engine/source/T3D/AI/AIGoal.cpp @@ -0,0 +1,24 @@ +//----------------------------------------------------------------------------- +// 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 "AIGoal.h" +#include "AIController.h" diff --git a/Engine/source/T3D/AI/AIGoal.h b/Engine/source/T3D/AI/AIGoal.h new file mode 100644 index 000000000..e8a22b873 --- /dev/null +++ b/Engine/source/T3D/AI/AIGoal.h @@ -0,0 +1,36 @@ +//----------------------------------------------------------------------------- +// 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. +//----------------------------------------------------------------------------- +#ifndef _AIGOAL_H_ +#define _AIGOAL_H_ + +#include "AIInfo.h" + +struct AIGoal : public AIInfo +{ + typedef AIInfo Parent; + bool mInRange, mInFiringRange; + AIGoal() = delete; + AIGoal(AIController* controller) : Parent(controller) { mInRange = mInFiringRange = false; }; + AIGoal(AIController* controller, SimObjectPtr objIn, F32 radIn) : Parent(controller, objIn, radIn) { mInRange = mInFiringRange = false; }; + AIGoal(AIController* controller, Point3F pointIn, F32 radIn) : Parent(controller, pointIn, radIn) { mInRange = mInFiringRange = false; }; +}; +#endif diff --git a/Engine/source/T3D/AI/AIInfo.cpp b/Engine/source/T3D/AI/AIInfo.cpp new file mode 100644 index 000000000..f4a02e47f --- /dev/null +++ b/Engine/source/T3D/AI/AIInfo.cpp @@ -0,0 +1,81 @@ +//----------------------------------------------------------------------------- +// 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 "AIInfo.h" +#include "AIController.h" + +AIInfo::AIInfo(AIController* controller) +{ + mControllerRef = controller; + mObj = NULL; + mPosition = mLastPos = Point3F(0.0f, 0.0f, 0.0f); + mRadius = 0.0f; + mPosSet = false; +}; + +AIInfo::AIInfo(AIController* controller, SimObjectPtr objIn, F32 radIn) +{ + mControllerRef = controller; + mObj = objIn; + mPosition = mLastPos = objIn->getPosition(); + mRadius = radIn; + mPosSet = false; + if (radIn == 0.0f) + mRadius = mMax(objIn->getObjBox().len_x(), objIn->getObjBox().len_y()) * 0.5; +}; + +AIInfo::AIInfo(AIController* controller, Point3F pointIn, F32 radIn) +{ + mControllerRef = controller; + mObj = NULL; + mPosition = mLastPos = pointIn; + mRadius = radIn; + mPosSet = true; +}; + +Point3F AIInfo::getPosition(bool doCastray) +{ + Point3F pos = (mObj.isValid()) ? mObj->getPosition() : mPosition; + if (doCastray) + { + RayInfo info; + if (gServerContainer.castRay(pos, pos - Point3F(0, 0, getCtrl()->mControllerData->mHeightTolerance), StaticShapeObjectType, &info)) + { + pos = info.point; + } + } + + return pos; +} + +F32 AIInfo::getDist() +{ + AIInfo* controlObj = getCtrl()->getAIInfo(); + Point3F targPos = getPosition(); + + if (mFabs(targPos.z - controlObj->getPosition().z) < getCtrl()->mControllerData->mHeightTolerance) + targPos.z = controlObj->getPosition().z; + + F32 ret = VectorF(controlObj->getPosition() - targPos).len(); + ret -= controlObj->mRadius + mRadius; + return ret; +} diff --git a/Engine/source/T3D/AI/AIInfo.h b/Engine/source/T3D/AI/AIInfo.h new file mode 100644 index 000000000..8aede0cae --- /dev/null +++ b/Engine/source/T3D/AI/AIInfo.h @@ -0,0 +1,46 @@ +//----------------------------------------------------------------------------- +// 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. +//----------------------------------------------------------------------------- +#ifndef _AIINFO_H_ +#define _AIINFO_H_ + +#ifndef _SCENEOBJECT_H_ +#include "scene/sceneObject.h" +#endif + +class AIController; +struct AIInfo +{ + AIController* mControllerRef; + AIController* getCtrl() { return mControllerRef; }; + void setCtrl(AIController* controller) { mControllerRef = controller; }; + SimObjectPtr mObj; + Point3F mPosition, mLastPos; + bool mPosSet; + F32 mRadius; + Point3F getPosition(bool doCastray = false); + F32 getDist(); + AIInfo() = delete; + AIInfo(AIController* controller); + AIInfo(AIController* controller, SimObjectPtr objIn, F32 radIn = 0.0f); + AIInfo(AIController* controller,Point3F pointIn, F32 radIn = 0.0f); +}; +#endif diff --git a/Engine/source/T3D/AI/AINavigation.cpp b/Engine/source/T3D/AI/AINavigation.cpp new file mode 100644 index 000000000..258939f06 --- /dev/null +++ b/Engine/source/T3D/AI/AINavigation.cpp @@ -0,0 +1,574 @@ +//----------------------------------------------------------------------------- +// 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 "AINavigation.h" +#include "AIController.h" +#include "T3D/shapeBase.h" + +static U32 sAILoSMask = TerrainObjectType | StaticShapeObjectType | StaticObjectType | AIObjectType; + +AINavigation::AINavigation(AIController* controller) +{ + mControllerRef = controller; +#ifdef TORQUE_NAVIGATION_ENABLED + mJump = None; + mNavSize = Regular; +#endif +} + +AINavigation::~AINavigation() +{ +#ifdef TORQUE_NAVIGATION_ENABLED + clearPath(); + clearFollow(); +#endif +} + +void AINavigation::setMoveDestination(const Point3F& location, bool slowdown) +{ + mMoveDestination = location; + getCtrl()->mMovement.mMoveState = AIController::ModeMove; + getCtrl()->mMovement.mMoveSlowdown = slowdown; + getCtrl()->mMovement.mMoveStuckTestCountdown = getCtrl()->mControllerData->mMoveStuckTestDelay; +} + +bool AINavigation::setPathDestination(const Point3F& pos, bool replace) +{ +#ifdef TORQUE_NAVIGATION_ENABLED + if (replace) + getCtrl()->setGoal(pos, getCtrl()->mControllerData->mMoveTolerance); + + if (!mNavMesh) + updateNavMesh(); + + // If we can't find a mesh, just move regularly. + if (!mNavMesh) + { + //setMoveDestination(pos); + getCtrl()->throwCallback("onPathFailed"); + return false; + } + + // Create a new path. + NavPath* path = new NavPath(); + + path->mMesh = mNavMesh; + path->mFrom = getCtrl()->getAIInfo()->getPosition(true); + path->mTo = getCtrl()->getGoal()->getPosition(true); + path->mFromSet = path->mToSet = true; + path->mAlwaysRender = true; + path->mLinkTypes = getCtrl()->mControllerData->mLinkTypes; + path->mXray = true; + // Paths plan automatically upon being registered. + if (!path->registerObject()) + { + delete path; + return false; + } + + if (path->success()) + { + // Clear any current path we might have. + clearPath(); + getCtrl()->clearCover(); + // Store new path. + mPathData.path = path; + mPathData.owned = true; + // Skip node 0, which we are currently standing on. + moveToNode(1); + getCtrl()->throwCallback("onPathSuccess"); + return true; + } + else + { + // Just move normally if we can't path. + //setMoveDestination(pos, true); + //return; + getCtrl()->throwCallback("onPathFailed"); + path->deleteObject(); + return false; + } +#else + setMoveDestination(pos, false); + return true; +#endif +} + +Point3F AINavigation::getPathDestination() const +{ +#ifdef TORQUE_NAVIGATION_ENABLED + if (!mPathData.path.isNull()) + return mPathData.path->mTo; + return Point3F(0, 0, 0); +#else + return getMoveDestination(); +#endif +} +void AINavigation::onReachDestination() +{ + +#ifdef TORQUE_NAVIGATION_ENABLED + if (!getPath().isNull()) + { + if (mPathData.index == getPath()->size() - 1) + { + // Handle looping paths. + if (getPath()->mIsLooping) + moveToNode(0); + // Otherwise end path. + else + { + clearPath(); + getCtrl()->throwCallback("onReachDestination"); + } + } + else + { + moveToNode(mPathData.index + 1); + // Throw callback every time if we're on a looping path. + //if(mPathData.path->mIsLooping) + //throwCallback("onReachDestination"); + } + } + else +#endif + { + getCtrl()->throwCallback("onReachDestination"); + getCtrl()->mMovement.mMoveState = AIController::ModeStop; + } +} + +void AINavigation::followObject() +{ + if (getCtrl()->getGoal()->getDist() < getCtrl()->mControllerData->mMoveTolerance) + return; + + if (setPathDestination(getCtrl()->getGoal()->getPosition(true))) + { +#ifdef TORQUE_NAVIGATION_ENABLED + getCtrl()->clearCover(); +#endif + } +} + +void AINavigation::followObject(SceneObject* obj, F32 radius) +{ + getCtrl()->setGoal(obj, radius); + followObject(); +} + +void AINavigation::clearFollow() +{ + getCtrl()->clearGoal(); +} + +DefineEngineMethod(AIController, 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->getNav()->setMoveDestination(goal, slowDown); +} + + +DefineEngineMethod(AIController, 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->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, true); +} + + +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, 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; +#ifdef TORQUE_NAVIGATION_ENABLED + object->getNav()->clearPath(); + object->clearCover(); +#endif + object->getNav()->clearFollow(); + + if (Sim::findObject(obj, follow)) + object->getNav()->followObject(follow, radius); +} + +#ifdef TORQUE_NAVIGATION_ENABLED +NavMesh* AINavigation::findNavMesh() const +{ + GameBase* gbo = dynamic_cast(mControllerRef->getAIInfo()->mObj.getPointer()); + // Search for NavMeshes that contain us entirely with the smallest possible + // volume. + NavMesh* mesh = NULL; + SimSet* set = NavMesh::getServerSet(); + for (U32 i = 0; i < set->size(); i++) + { + NavMesh* m = static_cast(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; + } + } + return mesh; +} + +void AINavigation::updateNavMesh() +{ + GameBase* gbo = dynamic_cast(mControllerRef->getAIInfo()->mObj.getPointer()); + NavMesh* old = mNavMesh; + if (mNavMesh.isNull()) + mNavMesh = findNavMesh(); + else + { + if (!mNavMesh->getWorldBox().isContained(gbo->getWorldBox())) + mNavMesh = findNavMesh(); + } + // See if we need to update our path. + if (mNavMesh != old && !mPathData.path.isNull()) + { + setPathDestination(mPathData.path->mTo); + } +} + +void AINavigation::moveToNode(S32 node) +{ + if (mPathData.path.isNull()) + return; + + // -1 is shorthand for 'last path node'. + if (node == -1) + node = mPathData.path->size() - 1; + + // Consider slowing down on the last path node. + setMoveDestination(mPathData.path->getNode(node), false); + + // Check flags for this segment. + if (mPathData.index) + { + U16 flags = mPathData.path->getFlags(node - 1); + // Jump if we must. + if (flags & LedgeFlag) + mJump = Ledge; + else if (flags & JumpFlag) + mJump = Now; + else + // Catch pathing errors. + mJump = None; + } + + // Store current index. + mPathData.index = node; +} + +void AINavigation::repath() +{ + // Ineffectual if we don't have a path, or are using someone else's. + if (mPathData.path.isNull() || !mPathData.owned) + return; + + if (mRandI(0, 100) < getCtrl()->mControllerData->mFlocking.mChance && flock()) + { + mPathData.path->mTo = mMoveDestination; + } + else + { + // If we're following, get their position. + mPathData.path->mTo = getCtrl()->getGoal()->getPosition(true); + } + + // Update from position and replan. + mPathData.path->mFrom = getCtrl()->getAIInfo()->getPosition(true); + mPathData.path->plan(); + + // Move to first node (skip start pos). + moveToNode(1); +} + + +void AINavigation::followNavPath(NavPath* path) +{ + // Get rid of our current path. + clearPath(); + getCtrl()->clearCover(); + + // Follow new path. + mPathData.path = path; + mPathData.owned = false; + // Start from 0 since we might not already be there. + moveToNode(0); +} + +void AINavigation::clearPath() +{ + // Only delete if we own the path. + if (!mPathData.path.isNull() && mPathData.owned) + mPathData.path->deleteObject(); + // Reset path data. + mPathData = PathData(); +} + +bool AINavigation::flock() +{ + AIControllerData::Flocking flockingData = getCtrl()->mControllerData->mFlocking; + SimObjectPtr obj = getCtrl()->getAIInfo()->mObj; + + 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; + bool flocking = false; + U32 found = 0; + if (getCtrl()->getGoal()) + { + Point3F dest = mMoveDestination; + + if (getCtrl()->mMovement.mMoveState == AIController::ModeStuck) + { + Point3F shuffle = Point3F(mRandF() - 0.5, mRandF() - 0.5, 0); + shuffle.normalize(); + dest += shuffle * flockingData.mMin; + } + + dest.z = pos.z; + if ((pos - dest).len() > flockingData.mSideStep) + { + //find closest object + SimpleQueryList sql; + Box3F queryBox = Box3F(pos - searchArea, pos + searchArea); + obj->getContainer()->findObjects(queryBox, AIObjectType, SimpleQueryList::insertionCallback, &sql); + sql.mList.remove(obj); + + Point3F avoidanceOffset = Point3F::Zero; + + //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(sql.mList[i]); + Point3F objectCenter = other->getBoxCenter(); + + F32 sumRad = flockingData.mMin + other->getAIController()->mControllerData->mFlocking.mMin; + F32 separation = getCtrl()->getAIInfo()->mRadius + other->getAIController()->getAIInfo()->mRadius; + sumRad += separation; + + Point3F offset = (pos - objectCenter); + F32 offsetLensq = offset.lenSquared(); //square roots are expensive, so use squared val compares + if ((flockingData.mMin > 0) && (offsetLensq < (sumRad * sumRad))) + { + other->disableCollision(); + if (!obj->getContainer()->castRay(pos, other->getBoxCenter(), sAILoSMask, &info)) + { + found++; + offset.normalizeSafe(); + offset *= sumRad + separation; + avoidanceOffset += offset; //accumulate total group, move away from that + } + 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) + { + for (U32 i = 0; i < sql.mList.size(); i++) + { + ShapeBase* other = static_cast(sql.mList[i]); + Point3F objectCenter = other->getBoxCenter(); + + F32 sumRad = flockingData.mMin + other->getAIController()->mControllerData->mFlocking.mMin; + F32 separation = getCtrl()->getAIInfo()->mRadius + other->getAIController()->getAIInfo()->mRadius; + sumRad += separation; + + Point3F offset = (pos - objectCenter); + if ((flockingData.mMin > 0) && ((sumRad * sumRad) < (maxFlocksq))) + { + other->disableCollision(); + if (!obj->getContainer()->castRay(pos, other->getBoxCenter(), sAILoSMask, &info)) + { + found++; + offset.normalizeSafe(); + offset *= sumRad + separation; + avoidanceOffset -= offset; // subtract total group, move toward it + } + other->enableCollision(); + } + } + } + if (found > 0) + { + 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)) + { + dest += 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; + } + } + } + } + } + } + obj->enableCollision(); + return flocking; +} + + +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, 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 ""; +} +#endif diff --git a/Engine/source/T3D/AI/AINavigation.h b/Engine/source/T3D/AI/AINavigation.h new file mode 100644 index 000000000..4d4f1b966 --- /dev/null +++ b/Engine/source/T3D/AI/AINavigation.h @@ -0,0 +1,105 @@ +//----------------------------------------------------------------------------- +// 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. +//----------------------------------------------------------------------------- +#ifndef _AINAVIGATION_H_ +#define _AINAVIGATION_H_ + +#include "AIInfo.h" + +#include "navigation/navPath.h" +#include "navigation/navMesh.h" + +class AIController; +struct AINavigation +{ + AIController* mControllerRef; + AIController* getCtrl() { return mControllerRef; }; + + AINavigation() = delete; + AINavigation(AIController* controller); + ~AINavigation(); + Point3F mMoveDestination; + void setMoveDestination(const Point3F& location, bool slowdown); + Point3F getMoveDestination() const { return mMoveDestination; }; + bool setPathDestination(const Point3F& pos, bool replace = false); + Point3F getPathDestination() const; + + void onReachDestination(); + + void followObject(); + void followObject(SceneObject* obj, F32 radius); + void clearFollow(); + +#ifdef TORQUE_NAVIGATION_ENABLED + /// Stores information about a path. + struct PathData { + /// Pointer to path object. + SimObjectPtr path; + /// Do we own our path? If so, we will delete it when finished. + bool owned; + /// Path node we're at. + U32 index; + /// Default constructor. + PathData() : path(NULL) + { + owned = false; + index = 0; + } + }; + + /// Should we jump? + enum JumpStates { + None, ///< No, don't jump. + Now, ///< Jump immediately. + 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; } + + /// NavMesh we pathfind on. + SimObjectPtr mNavMesh; + NavMesh* findNavMesh() const; + void updateNavMesh(); + NavMesh* getNavMesh() const { return mNavMesh; } + PathData mPathData; + JumpStates mJump; + + /// Clear out the current path. + void clearPath(); + void repath(); + + /// Get the current path we're following. + SimObjectPtr getPath() { return mPathData.path; }; + void followNavPath(NavPath* path); + + /// Move to the specified node in the current path. + void moveToNode(S32 node); + bool flock(); +#endif +}; + +#endif diff --git a/Engine/source/T3D/gameFunctions.cpp b/Engine/source/T3D/gameFunctions.cpp index c40e2f081..24487c5ec 100644 --- a/Engine/source/T3D/gameFunctions.cpp +++ b/Engine/source/T3D/gameFunctions.cpp @@ -669,6 +669,7 @@ static void RegisterGameFunctions() Con::setIntVariable("$TypeMasks::PathShapeObjectType", PathShapeObjectType); // PATHSHAPE END Con::setIntVariable("$TypeMasks::TurretObjectType", TurretObjectType); + Con::setIntVariable("$TypeMasks::AIObjectType", AIObjectType); Con::addVariable("Ease::InOut", TypeS32, &gEaseInOut, "InOut ease for curve movement.\n" diff --git a/Engine/source/T3D/objectTypes.h b/Engine/source/T3D/objectTypes.h index c6b05df74..47fb2edee 100644 --- a/Engine/source/T3D/objectTypes.h +++ b/Engine/source/T3D/objectTypes.h @@ -174,7 +174,7 @@ enum SceneObjectTypes /// @see TurretShape TurretObjectType = BIT(29), N_A_31 = BIT(30), - N_A_32 = BIT(31), + AIObjectType = BIT(31), /// @} }; diff --git a/Engine/source/T3D/player.cpp b/Engine/source/T3D/player.cpp index fe560e739..c736ff022 100644 --- a/Engine/source/T3D/player.cpp +++ b/Engine/source/T3D/player.cpp @@ -461,7 +461,6 @@ PlayerData::PlayerData() physicsPlayerType = StringTable->EmptyString(); mControlMap = StringTable->EmptyString(); - dMemset( actionList, 0, sizeof(actionList) ); } @@ -740,7 +739,7 @@ void PlayerData::initPersistFields() endGroup( "Camera" ); addGroup( "Movement" ); - addField("controlMap", TypeString, Offset(mControlMap, PlayerData), + addField("controlMap", TypeString, Offset(mControlMap, PlayerData), "@brief movemap used by these types of objects.\n\n"); addFieldV( "maxStepHeight", TypeRangedF32, Offset(maxStepHeight, PlayerData), &CommonValidators::PositiveFloat, @@ -1643,7 +1642,6 @@ Player::Player() mLastAbsoluteYaw = 0.0f; mLastAbsolutePitch = 0.0f; mLastAbsoluteRoll = 0.0f; - afx_init(); } @@ -1741,7 +1739,6 @@ bool Player::onAdd() world ); mPhysicsRep->setTransform( getTransform() ); } - return true; } @@ -2258,12 +2255,6 @@ void Player::advanceTime(F32 dt) } } } - -bool Player::getAIMove(Move* move) -{ - return false; -} - void Player::setState(ActionState state, U32 recoverTicks) { if (state != mState) { diff --git a/Engine/source/T3D/player.h b/Engine/source/T3D/player.h index 0b8f44df2..f1f10e29a 100644 --- a/Engine/source/T3D/player.h +++ b/Engine/source/T3D/player.h @@ -50,6 +50,12 @@ class Player; class OpenVRTrackedObject; #endif +#ifdef TORQUE_NAVIGATION_ENABLED +#include "navigation/navPath.h" +#include "navigation/navMesh.h" +#include "navigation/coverPoint.h" +#endif // TORQUE_NAVIGATION_ENABLED + //---------------------------------------------------------------------------- struct PlayerData: public ShapeBaseData { @@ -758,8 +764,6 @@ public: Point3F getMomentum() const override; void setMomentum(const Point3F &momentum) override; bool displaceObject(const Point3F& displaceVector) override; - virtual bool getAIMove(Move*); - bool checkDismountPosition(const MatrixF& oldPos, const MatrixF& newPos); ///< Is it safe to dismount here? // diff --git a/Engine/source/T3D/shapeBase.cpp b/Engine/source/T3D/shapeBase.cpp index b3d2e1db1..92f5f4da6 100644 --- a/Engine/source/T3D/shapeBase.cpp +++ b/Engine/source/T3D/shapeBase.cpp @@ -69,6 +69,7 @@ #include "core/stream/fileStream.h" #include "T3D/accumulationVolume.h" #include "console/persistenceManager.h" +#include "AI/AIController.h" IMPLEMENT_CO_DATABLOCK_V1(ShapeBaseData); @@ -197,7 +198,8 @@ ShapeBaseData::ShapeBaseData() useEyePoint( false ), isInvincible( false ), renderWhenDestroyed( true ), - inheritEnergyFromMount( false ) + inheritEnergyFromMount( false ), + mAIControllData(NULL) { INIT_ASSET(Shape); INIT_ASSET(DebrisShape); @@ -548,6 +550,10 @@ void ShapeBaseData::initPersistFields() addField("silentBBoxValidation", TypeBool, Offset(silent_bbox_check, ShapeBaseData)); INITPERSISTFIELD_SHAPEASSET(DebrisShape, ShapeBaseData, "The shape asset to use for auto-generated breakups via blowup(). @note may not be functional."); endGroup( "Shapes" ); + addGroup("Movement"); + addField("aiControllerData", TYPEID< AIControllerData >(), Offset(mAIControllData, ShapeBaseData), + "@brief ai controller used by these types of objects.\n\n"); + endGroup("Movement"); addGroup("Particle Effects"); addField( "explosion", TYPEID< ExplosionData >(), Offset(explosion, ShapeBaseData), @@ -999,7 +1005,8 @@ ShapeBase::ShapeBase() mCameraFov( 90.0f ), mIsControlled( false ), mLastRenderFrame( 0 ), - mLastRenderDistance( 0.0f ) + mLastRenderDistance( 0.0f ), + mAIController(NULL) { mTypeMask |= ShapeBaseObjectType | LightObjectType; @@ -1050,6 +1057,7 @@ ShapeBase::~ShapeBase() cur->next = sFreeTimeoutList; sFreeTimeoutList = cur; } + if (mAIController) mAIController->deleteObject(); } void ShapeBase::initPersistFields() @@ -5467,3 +5475,44 @@ DefineEngineMethod(ShapeBase, getNodePoint, Point3F, (const char* nodeName), , return pos; } + +bool ShapeBase::setAIController(SimObjectId controller) +{ + if (Sim::findObject(controller, mAIController) && mAIController->mControllerData) + { + mAIController->setAIInfo(this); + mTypeMask |= AIObjectType; + return true; + } + Con::errorf("unable to find AIController : %i", controller); + mAIController = NULL; + mTypeMask |= ~AIObjectType; + return false; +} + +bool ShapeBase::getAIMove(Move* move) +{ + if (!isServerObject()) return false; + if (isControlled()) return false; //something else is steering us, so use that one's controller + if (!(mTypeMask & VehicleObjectType || mTypeMask & PlayerObjectType)) return false; //only support players and vehicles for now + if (mAIController) + { + mAIController->getAIMove(move); //actual result + mTypeMask |= AIObjectType; + return true; + } + mAIController = NULL; + mTypeMask &= ~AIObjectType; + return false; +} + + +DefineEngineMethod(ShapeBase, setAIController, bool, (S32 controller), , "") +{ + return object->setAIController(controller); +} + +DefineEngineMethod(ShapeBase, getAIController, AIController*, (), , "") +{ + return object->getAIController(); +} diff --git a/Engine/source/T3D/shapeBase.h b/Engine/source/T3D/shapeBase.h index b34e544bf..46f9a3270 100644 --- a/Engine/source/T3D/shapeBase.h +++ b/Engine/source/T3D/shapeBase.h @@ -88,6 +88,8 @@ class ShapeBase; class SFXSource; class SFXTrack; class SFXProfile; +struct AIController; +struct AIControllerData; typedef void* Light; @@ -560,6 +562,7 @@ public: U32 cubeDescId; ReflectorDesc *reflectorDesc; + AIControllerData* mAIControllData; /// @name Destruction /// /// Everyone likes to blow things up! @@ -1761,6 +1764,11 @@ public: /// Returns true if this object is controlling by something bool isControlled() { return(mIsControlled); } + AIController* mAIController; + bool setAIController(SimObjectId controller); + AIController* getAIController() { return mAIController; }; + virtual bool getAIMove(Move* move); + /// Returns true if this object is being used as a camera in first person bool isFirstPerson() const; @@ -1902,7 +1910,6 @@ public: void registerCollisionCallback(CollisionEventCallback*); void unregisterCollisionCallback(CollisionEventCallback*); -protected: enum { ANIM_OVERRIDDEN = BIT(0), BLOCK_USER_CONTROL = BIT(1), @@ -1910,6 +1917,8 @@ protected: BAD_ANIM_ID = 999999999, BLENDED_CLIP = 0x80000000, }; + U8 anim_clip_flags; +protected: struct BlendThread { TSThread* thread; @@ -1917,7 +1926,6 @@ protected: }; Vector blend_clips; static U32 unique_anim_tag_counter; - U8 anim_clip_flags; S32 last_anim_id; U32 last_anim_tag; U32 last_anim_lock_tag; diff --git a/Engine/source/T3D/vehicles/vehicle.cpp b/Engine/source/T3D/vehicles/vehicle.cpp index aa191be29..9ad0abdbf 100644 --- a/Engine/source/T3D/vehicles/vehicle.cpp +++ b/Engine/source/T3D/vehicles/vehicle.cpp @@ -148,6 +148,7 @@ VehicleData::VehicleData() collDamageThresholdVel = 20; collDamageMultiplier = 0.05f; enablePhysicsRep = true; + mControlMap = StringTable->EmptyString(); } @@ -321,6 +322,11 @@ void VehicleData::initPersistFields() "velocity).\n\nCurrently unused." ); endGroup("Collision"); + addGroup("Movement"); + addField("controlMap", TypeString, Offset(mControlMap, VehicleData), + "@brief movemap used by these types of objects.\n\n"); + endGroup("Movement"); + addGroup("Steering"); addField("controlMap", TypeString, Offset(mControlMap, VehicleData), "@brief movemap used by these types of objects.\n\n"); @@ -474,7 +480,6 @@ bool Vehicle::onAdd() void Vehicle::onRemove() { SAFE_DELETE(mPhysicsRep); - U32 i=0; for( i=0; i mDamageEmitterList[VehicleData::VC_NUM_DAMAGE_EMITTERS]; @@ -147,6 +148,9 @@ public: bool onAdd() override; void onRemove() override; + Point2F getSteering() { return mSteering; }; + F32 getThrottle() { return mThrottle;}; + /// Interpolates between move ticks @see processTick /// @param dt Change in time between the last call and this call to the function void advanceTime(F32 dt) override; diff --git a/Engine/source/console/simObject.cpp b/Engine/source/console/simObject.cpp index 1f6e28af5..6039393ef 100644 --- a/Engine/source/console/simObject.cpp +++ b/Engine/source/console/simObject.cpp @@ -82,7 +82,7 @@ ImplementBitfieldType(GameTypeMasksType, { SceneObjectTypes::PathShapeObjectType, "$TypeMasks::PathShapeObjectType", "Path-following Objects.\n" }, { SceneObjectTypes::TurretObjectType, "$TypeMasks::TurretObjectType", "Turret Objects.\n" }, { SceneObjectTypes::N_A_31, "$TypeMasks::N_A_31", "unused 31st bit.\n" }, -{ SceneObjectTypes::N_A_32, "$TypeMasks::N_A_32", "unused 32nd bit.\n" }, +{ SceneObjectTypes::AIObjectType, "$TypeMasks::AIObjectType", "AIObjectType.\n" }, EndImplementBitfieldType; diff --git a/Engine/source/navigation/guiNavEditorCtrl.cpp b/Engine/source/navigation/guiNavEditorCtrl.cpp index bf164bc4b..eae204447 100644 --- a/Engine/source/navigation/guiNavEditorCtrl.cpp +++ b/Engine/source/navigation/guiNavEditorCtrl.cpp @@ -37,6 +37,7 @@ #include "gui/buttons/guiButtonCtrl.h" #include "gui/worldEditor/undoActions.h" #include "T3D/gameBase/gameConnection.h" +#include "T3D/AI/AIController.h" IMPLEMENT_CONOBJECT(GuiNavEditorCtrl); @@ -225,8 +226,29 @@ void GuiNavEditorCtrl::spawnPlayer(const Point3F &pos) SimGroup* missionCleanup = dynamic_cast(cleanup); missionCleanup->addObject(obj); } - mPlayer = static_cast(obj); - Con::executef(this, "onPlayerSelected", Con::getIntArg(mPlayer->mLinkTypes.getFlags())); + mPlayer = obj; +#ifdef TORQUE_NAVIGATION_ENABLED + AIPlayer* asAIPlayer = dynamic_cast(obj); + if (asAIPlayer) //try direct + { + Con::executef(this, "onPlayerSelected", Con::getIntArg(asAIPlayer->mLinkTypes.getFlags())); + } + else + { + ShapeBase* sbo = dynamic_cast(obj); + if (sbo->getAIController()) + { + if (sbo->getAIController()->mControllerData) + Con::executef(this, "onPlayerSelected", Con::getIntArg(sbo->getAIController()->mControllerData->mLinkTypes.getFlags())); + } + else + { +#endif + Con::executef(this, "onPlayerSelected"); +#ifdef TORQUE_NAVIGATION_ENABLED + } + } +#endif } } @@ -383,16 +405,56 @@ 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(ri.object)) + if(ri.object) { - mPlayer = dynamic_cast(ri.object); - Con::executef(this, "onPlayerSelected", Con::getIntArg(mPlayer->mLinkTypes.getFlags())); + mPlayer = ri.object; +#ifdef TORQUE_NAVIGATION_ENABLED + AIPlayer* asAIPlayer = dynamic_cast(mPlayer.getPointer()); + if (asAIPlayer) //try direct + { + Con::executef(this, "onPlayerSelected", Con::getIntArg(asAIPlayer->mLinkTypes.getFlags())); + } + else + { + ShapeBase* sbo = dynamic_cast(mPlayer.getPointer()); + if (sbo->getAIController()) + { + if (sbo->getAIController()->mControllerData) + Con::executef(this, "onPlayerSelected", Con::getIntArg(sbo->getAIController()->mControllerData->mLinkTypes.getFlags())); + } + else + { +#endif + Con::executef(this, "onPlayerSelected"); + } +#ifdef TORQUE_NAVIGATION_ENABLED + } + } +#endif + } + else if (!mPlayer.isNull() && gServerContainer.castRay(startPnt, endPnt, StaticObjectType, &ri)) + { + AIPlayer* asAIPlayer = dynamic_cast(mPlayer.getPointer()); + if (asAIPlayer) //try direct + { +#ifdef TORQUE_NAVIGATION_ENABLED + asAIPlayer->setPathDestination(ri.point); +#else + asAIPlayer->setMoveDestination(ri.point,false); +#endif + } + else + { + ShapeBase* sbo = dynamic_cast(mPlayer.getPointer()); + if (sbo->getAIController()) + { + if (sbo->getAIController()->mControllerData) + sbo->getAIController()->getNav()->setPathDestination(ri.point, true); + } } } - else if(!mPlayer.isNull() && gServerContainer.castRay(startPnt, endPnt, StaticObjectType, &ri)) - mPlayer->setPathDestination(ri.point); } } } @@ -455,8 +517,8 @@ void GuiNavEditorCtrl::on3DMouseMove(const Gui3DMouseEvent & event) if(mMode == mTestMode) { - if(gServerContainer.castRay(startPnt, endPnt, PlayerObjectType, &ri)) - mCurPlayer = dynamic_cast(ri.object); + if(gServerContainer.castRay(startPnt, endPnt, PlayerObjectType | VehicleObjectType, &ri)) + mCurPlayer = ri.object; else mCurPlayer = NULL; } diff --git a/Engine/source/navigation/guiNavEditorCtrl.h b/Engine/source/navigation/guiNavEditorCtrl.h index 6e0b34f5e..c5d2f1b5a 100644 --- a/Engine/source/navigation/guiNavEditorCtrl.h +++ b/Engine/source/navigation/guiNavEditorCtrl.h @@ -155,8 +155,8 @@ protected: /// @name Test mode /// @{ - SimObjectPtr mPlayer; - SimObjectPtr mCurPlayer; + SimObjectPtr mPlayer; + SimObjectPtr mCurPlayer; /// @} diff --git a/Templates/BaseGame/game/core/gameObjects/datablocks/defaultDatablocks.tscript b/Templates/BaseGame/game/core/gameObjects/datablocks/defaultDatablocks.tscript index 1035f2d9a..512acd273 100644 --- a/Templates/BaseGame/game/core/gameObjects/datablocks/defaultDatablocks.tscript +++ b/Templates/BaseGame/game/core/gameObjects/datablocks/defaultDatablocks.tscript @@ -169,3 +169,20 @@ datablock LightAnimData( SpinLightAnim ) rotKeys[2] = "az"; rotSmooth[2] = true; }; + +datablock AIPlayerControllerData( aiPlayerControl ) +{ + moveTolerance = 0.25; followTolerance = 1.0; mAttackRadius = 2; +}; + +datablock AIWheeledVehicleControllerData( aiCarControl ) +{ + moveTolerance = 1.0; followTolerance = 2.0; mAttackRadius = 5.0; +}; + +datablock AIFlyingVehicleControllerData( aiPlaneControl ) +{ + moveTolerance = 2.0; followTolerance = 5.0; mAttackRadius = 10.0; + FlightFloor = 15; FlightCeiling = 150; +}; + diff --git a/Templates/BaseGame/game/tools/navEditor/NavEditorGui.gui b/Templates/BaseGame/game/tools/navEditor/NavEditorGui.gui index 92505f4e7..18646fe1a 100644 --- a/Templates/BaseGame/game/tools/navEditor/NavEditorGui.gui +++ b/Templates/BaseGame/game/tools/navEditor/NavEditorGui.gui @@ -485,7 +485,7 @@ $guiContent = new GuiNavEditorCtrl(NavEditorGui, EditorGuiGroup) { VertSizing = "bottom"; Extent = "90 18"; text = "Stop"; - command = "NavEditorGui.getPlayer().stop();"; + command = "NavEditorGui.stop();"; }; }; }; diff --git a/Templates/BaseGame/game/tools/navEditor/main.tscript b/Templates/BaseGame/game/tools/navEditor/main.tscript index 56739e3a6..3c2ef7aab 100644 --- a/Templates/BaseGame/game/tools/navEditor/main.tscript +++ b/Templates/BaseGame/game/tools/navEditor/main.tscript @@ -87,7 +87,7 @@ function NavEditorPlugin::onWorldEditorStartup(%this) // Add ourselves to the Editor Settings window. exec("./NavEditorSettingsTab.gui"); - //ESettingsWindow.addTabPage(ENavEditorSettingsPage); + ESettingsWindow.addTabPage(ENavEditorSettingsPage); ENavEditorSettingsPage.init(); // Add items to World Editor Creator @@ -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(); } diff --git a/Templates/BaseGame/game/tools/navEditor/navEditor.tscript b/Templates/BaseGame/game/tools/navEditor/navEditor.tscript index 8e2234646..7f7b01804 100644 --- a/Templates/BaseGame/game/tools/navEditor/navEditor.tscript +++ b/Templates/BaseGame/game/tools/navEditor/navEditor.tscript @@ -459,6 +459,14 @@ function NavEditorGui::onLinkSelected(%this, %flags) function NavEditorGui::onPlayerSelected(%this, %flags) { + if (!isObject(%this.getPlayer().aiController) && (!(%this.getPlayer().isMemberOfClass("AIPlayer")))) + { + %this.getPlayer().aiController = new AIController(){ ControllerData = %this.getPlayer().getDatablock().aiControllerData; }; + %this.getPlayer().setAIController(%this.getPlayer().aiController); + } + NavMeshIgnore(%this.getPlayer(), true); + %this.getPlayer().setDamageState("Enabled"); + updateLinkData(NavEditorOptionsWindow-->TestProperties, %flags); } @@ -526,7 +534,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,10 +555,19 @@ 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()); } } +function NavEditorGui::stop(%this) +{ + if (isObject(%this.getPlayer().aiController)) + %this.getPlayer().aiController.stop(); + else + { + NavEditorGui.getPlayer().stop(); + } +} function NavInspector::inspect(%this, %obj) { %name = "";