//----------------------------------------------------------------------------- // 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 "T3D/turret/aiTurretShape.h" #include "console/console.h" #include "console/consoleTypes.h" #include "console/engineAPI.h" #include "core/stream/bitStream.h" #include "math/mMath.h" #include "math/mathIO.h" #include "math/mathUtils.h" #include "gfx/gfxDrawUtil.h" #include "ts/tsShapeInstance.h" #include "math/mRandom.h" static U32 sScanTypeMask = PlayerObjectType | VehicleObjectType; static U32 sAimTypeMask = TerrainObjectType | WaterObjectType | PlayerObjectType | StaticShapeObjectType | VehicleObjectType | ItemObjectType; //---------------------------------------------------------------------------- AITurretShapeData::StateData::StateData() { name = 0; transition.rest[0] = transition.rest[1] = -1; transition.target[0] = transition.target[1] = -1; transition.activated[0] = transition.activated[1] = -1; transition.timeout = -1; waitForTimeout = true; timeoutValue = 0; fire = false; scan = false; script = 0; scaleAnimation = false; direction = false; sequence = -1; } static AITurretShapeData::StateData gDefaultStateData; //---------------------------------------------------------------------------- IMPLEMENT_CO_DATABLOCK_V1(AITurretShapeData); ConsoleDocClass( AITurretShapeData, "@brief Defines properties for an AITurretShape object.\n\n" "@see AITurretShape\n" "@see TurretShapeData\n" "@ingroup gameObjects\n" ); AITurretShapeData::AITurretShapeData() { maxScanHeading = 90; maxScanPitch = 90; maxScanDistance = 20; // Do a full scan every three ticks scanTickFrequency = 3; // Randomly add 0 to 1 ticks to the scan frequency as // chosen every scan period. scanTickFrequencyVariance = 1; trackLostTargetTime = 0; scanNode = -1; aimNode = -1; maxWeaponRange = 100; weaponLeadVelocity = 0; for (S32 i = 0; i < MaxStates; i++) { stateName[i] = 0; stateTransitionAtRest[i] = 0; stateTransitionNotAtRest[i] = 0; stateTransitionTarget[i] = 0; stateTransitionNoTarget[i] = 0; stateTransitionActivated[i] = 0; stateTransitionDeactivated[i] = 0; stateTransitionTimeout[i] = 0; stateWaitForTimeout[i] = true; stateTimeoutValue[i] = 0; stateFire[i] = false; stateScan[i] = false; stateScaleAnimation[i] = true; stateDirection[i] = true; stateSequence[i] = 0; stateScript[i] = 0; } isAnimated = false; statesLoaded = false; fireState = -1; } void AITurretShapeData::initPersistFields() { addField("maxScanHeading", TypeF32, Offset(maxScanHeading, AITurretShapeData), "@brief Maximum number of degrees to scan left and right.\n\n" "@note Maximum scan heading is 90 degrees.\n"); addField("maxScanPitch", TypeF32, Offset(maxScanPitch, AITurretShapeData), "@brief Maximum number of degrees to scan up and down.\n\n" "@note Maximum scan pitch is 90 degrees.\n"); addField("maxScanDistance", TypeF32, Offset(maxScanDistance, AITurretShapeData), "@brief Maximum distance to scan.\n\n" "When combined with maxScanHeading and maxScanPitch this forms a 3D scanning wedge used to initially " "locate a target.\n"); addField("scanTickFrequency", TypeS32, Offset(scanTickFrequency, AITurretShapeData), "@brief How often should we perform a full scan when looking for a target.\n\n" "Expressed as the number of ticks between full scans, but no less than 1.\n"); addField("scanTickFrequencyVariance", TypeS32, Offset(scanTickFrequencyVariance, AITurretShapeData), "@brief Random amount that should be added to the scan tick frequency each scan period.\n\n" "Expressed as the number of ticks to randomly add, but no less than zero.\n"); addField("trackLostTargetTime", TypeF32, Offset(trackLostTargetTime, AITurretShapeData), "@brief How long after the turret has lost the target should it still track it.\n\n" "Expressed in seconds.\n"); addField("maxWeaponRange", TypeF32, Offset(maxWeaponRange, AITurretShapeData), "@brief Maximum distance that the weapon will fire upon a target.\n\n"); addField("weaponLeadVelocity", TypeF32, Offset(weaponLeadVelocity, AITurretShapeData), "@brief Velocity used to lead target.\n\n" "If value <= 0, don't lead target.\n"); // State arrays addArray( "States", MaxStates ); addField( "stateName", TypeCaseString, Offset(stateName, AITurretShapeData), MaxStates, "Name of this state." ); addField( "stateTransitionOnAtRest", TypeString, Offset(stateTransitionAtRest, AITurretShapeData), MaxStates, "Name of the state to transition to when the turret is at rest (static)."); addField( "stateTransitionOnNotAtRest", TypeString, Offset(stateTransitionNotAtRest, AITurretShapeData), MaxStates, "Name of the state to transition to when the turret is not at rest (not static)."); addField( "stateTransitionOnTarget", TypeString, Offset(stateTransitionTarget, AITurretShapeData), MaxStates, "Name of the state to transition to when the turret gains a target." ); addField( "stateTransitionOnNoTarget", TypeString, Offset(stateTransitionNoTarget, AITurretShapeData), MaxStates, "Name of the state to transition to when the turret loses a target." ); addField( "stateTransitionOnActivated", TypeString, Offset(stateTransitionActivated, AITurretShapeData), MaxStates, "Name of the state to transition to when the turret goes from deactivated to activated."); addField( "stateTransitionOnDeactivated", TypeString, Offset(stateTransitionDeactivated, AITurretShapeData), MaxStates, "Name of the state to transition to when the turret goes from activated to deactivated"); addField( "stateTransitionOnTimeout", TypeString, Offset(stateTransitionTimeout, AITurretShapeData), MaxStates, "Name of the state to transition to when we have been in this state " "for stateTimeoutValue seconds." ); addField( "stateTimeoutValue", TypeF32, Offset(stateTimeoutValue, AITurretShapeData), MaxStates, "Time in seconds to wait before transitioning to stateTransitionOnTimeout." ); addField( "stateWaitForTimeout", TypeBool, Offset(stateWaitForTimeout, AITurretShapeData), MaxStates, "If false, this state ignores stateTimeoutValue and transitions " "immediately if other transition conditions are met." ); addField( "stateFire", TypeBool, Offset(stateFire, AITurretShapeData), MaxStates, "The first state with this set to true is the state entered by the " "client when it receives the 'fire' event." ); addField( "stateScan", TypeBool, Offset(stateScan, AITurretShapeData), MaxStates, "Indicates the turret should perform a continuous scan looking for targets." ); addField( "stateDirection", TypeBool, Offset(stateDirection, AITurretShapeData), MaxStates, "@brief Direction of the animation to play in this state.\n\n" "True is forward, false is backward." ); addField( "stateSequence", TypeString, Offset(stateSequence, AITurretShapeData), MaxStates, "Name of the sequence to play on entry to this state." ); addField( "stateScaleAnimation", TypeBool, Offset(stateScaleAnimation, AITurretShapeData), MaxStates, "If true, the timeScale of the stateSequence animation will be adjusted " "such that the sequence plays for stateTimeoutValue seconds. " ); addField( "stateScript", TypeCaseString, Offset(stateScript, AITurretShapeData), MaxStates, "@brief Method to execute on entering this state.\n\n" "Scoped to AITurretShapeData."); endArray( "States" ); Parent::initPersistFields(); } bool AITurretShapeData::onAdd() { if (!Parent::onAdd()) return false; // Copy state data from the scripting arrays into the // state structure array. If we have state data already, // we are on the client and need to leave it alone. for (U32 i = 0; i < MaxStates; i++) { StateData& s = state[i]; if (statesLoaded == false) { s.name = stateName[i]; s.transition.rest[0] = lookupState(stateTransitionNotAtRest[i]); s.transition.rest[1] = lookupState(stateTransitionAtRest[i]); s.transition.target[0] = lookupState(stateTransitionNoTarget[i]); s.transition.target[1] = lookupState(stateTransitionTarget[i]); s.transition.activated[0] = lookupState(stateTransitionDeactivated[i]); s.transition.activated[1] = lookupState(stateTransitionActivated[i]); s.transition.timeout = lookupState(stateTransitionTimeout[i]); s.waitForTimeout = stateWaitForTimeout[i]; s.timeoutValue = stateTimeoutValue[i]; s.fire = stateFire[i]; s.scan = stateScan[i]; s.scaleAnimation = stateScaleAnimation[i]; s.direction = stateDirection[i]; s.script = stateScript[i]; // Resolved at load time s.sequence = -1; } // The first state marked as "fire" is the state entered on the // client when it recieves a fire event. if (s.fire && fireState == -1) fireState = i; } // Always preload images, this is needed to avoid problems with // resolving sequences before transmission to a client. return true; } bool AITurretShapeData::preload(bool server, String &errorStr) { if (!Parent::preload(server, errorStr)) return false; // We have mShape at this point. Resolve nodes. scanNode = mShape->findNode("scanPoint"); aimNode = mShape->findNode("aimPoint"); if (scanNode == -1) scanNode = pitchNode; if (scanNode == -1) scanNode = headingNode; if (aimNode == -1) aimNode = pitchNode; if (aimNode == -1) aimNode = headingNode; // Resolve state sequence names & emitter nodes isAnimated = false; for (U32 j = 0; j < MaxStates; j++) { StateData& s = state[j]; if (stateSequence[j] && stateSequence[j][0]) s.sequence = mShape->findSequence(stateSequence[j]); if (s.sequence != -1) { // This state has an animation sequence isAnimated = true; } } return true; } void AITurretShapeData::packData(BitStream* stream) { Parent::packData(stream); stream->write(maxScanHeading); stream->write(maxScanPitch); stream->write(maxScanDistance); stream->write(maxWeaponRange); stream->write(weaponLeadVelocity); for (U32 i = 0; i < MaxStates; i++) if (stream->writeFlag(state[i].name && state[i].name[0])) { StateData& s = state[i]; // States info not needed on the client: // s.scriptNames // Transitions are inc. one to account for -1 values stream->writeString(state[i].name); stream->writeInt(s.transition.rest[0]+1,NumStateBits); stream->writeInt(s.transition.rest[1]+1,NumStateBits); stream->writeInt(s.transition.target[0]+1,NumStateBits); stream->writeInt(s.transition.target[1]+1,NumStateBits); stream->writeInt(s.transition.activated[0]+1,NumStateBits); stream->writeInt(s.transition.activated[1]+1,NumStateBits); stream->writeInt(s.transition.timeout+1,NumStateBits); if(stream->writeFlag(s.timeoutValue != gDefaultStateData.timeoutValue)) stream->write(s.timeoutValue); stream->writeFlag(s.waitForTimeout); stream->writeFlag(s.fire); stream->writeFlag(s.scan); stream->writeFlag(s.scaleAnimation); stream->writeFlag(s.direction); if(stream->writeFlag(s.sequence != gDefaultStateData.sequence)) stream->writeSignedInt(s.sequence, 16); } } void AITurretShapeData::unpackData(BitStream* stream) { Parent::unpackData(stream); stream->read(&maxScanHeading); stream->read(&maxScanPitch); stream->read(&maxScanDistance); stream->read(&maxWeaponRange); stream->read(&weaponLeadVelocity); for (U32 i = 0; i < MaxStates; i++) { if (stream->readFlag()) { StateData& s = state[i]; // States info not needed on the client: // s.scriptNames // Transitions are dec. one to restore -1 values s.name = stream->readSTString(); s.transition.rest[0] = stream->readInt(NumStateBits) - 1; s.transition.rest[1] = stream->readInt(NumStateBits) - 1; s.transition.target[0] = stream->readInt(NumStateBits) - 1; s.transition.target[1] = stream->readInt(NumStateBits) - 1; s.transition.activated[0] = stream->readInt(NumStateBits) - 1; s.transition.activated[1] = stream->readInt(NumStateBits) - 1; s.transition.timeout = stream->readInt(NumStateBits) - 1; if(stream->readFlag()) stream->read(&s.timeoutValue); else s.timeoutValue = gDefaultStateData.timeoutValue; s.waitForTimeout = stream->readFlag(); s.fire = stream->readFlag(); s.scan = stream->readFlag(); s.scaleAnimation = stream->readFlag(); s.direction = stream->readFlag(); if(stream->readFlag()) s.sequence = stream->readSignedInt(16); } } statesLoaded = true; } S32 AITurretShapeData::lookupState(const char* name) { if (!name || !name[0]) return -1; for (U32 i = 0; i < MaxStates; i++) if (stateName[i] && !dStricmp(name,stateName[i])) return i; Con::errorf(ConsoleLogEntry::General,"AITurretShapeData:: Could not resolve state \"%s\" for image \"%s\"",name,getName()); return 0; } //---------------------------------------------------------------------------- // Used to build potential target list static void _scanCallback( SceneObject* object, void* data ) { AITurretShape* turret = (AITurretShape*)data; ShapeBase* shape = dynamic_cast(object); if (shape && shape->getDamageState() == ShapeBase::Enabled) { Point3F targetPos = shape->getBoxCenter(); // Put target position into the scan node's space turret->mScanWorkspaceScanWorldMat.mulP(targetPos); // Is the target within scanning distance if (targetPos.lenSquared() > turret->getMaxScanDistanceSquared()) return; // Make sure the target is in front and within the maximum // heading range Point2F targetXY(targetPos.x, targetPos.y); targetXY.normalizeSafe(); F32 headingDot = mDot(Point2F(0, 1), targetXY); F32 heading = mAcos(headingDot); if (headingDot < 0 || heading > turret->getMaxScanHeading()) return; // Make sure the target is in front and within the maximum // pitch range Point2F targetZY(targetPos.z, targetPos.y); targetZY.normalizeSafe(); F32 pitchDot = mDot(Point2F(0, 1), targetZY); F32 pitch = mAcos(pitchDot); if (pitchDot < 0 || pitch > turret->getMaxScanPitch()) return; turret->addPotentialTarget(shape); } } // Used to sort potential target list based on distance static Point3F comparePoint; static S32 QSORT_CALLBACK _sortCallback(const void* a,const void* b) { const ShapeBase* s1 = (*reinterpret_cast(a)); const ShapeBase* s2 = (*reinterpret_cast(b)); F32 s1Len = (s1->getPosition() - comparePoint).lenSquared(); F32 s2Len = (s2->getPosition() - comparePoint).lenSquared(); return s1Len - s2Len; } IMPLEMENT_CO_NETOBJECT_V1(AITurretShape); ConsoleDocClass( AITurretShape, "@ingroup gameObjects" ); AITurretShape::AITurretShape() { mTypeMask |= VehicleObjectType | DynamicShapeObjectType; mDataBlock = 0; mScanHeading = 0; mScanPitch = 0; mScanDistance = 0; mScanDistanceSquared = 0; mScanBox = Box3F::Zero; mScanTickFrequency = 1; mScanTickFrequencyVariance = 0; mTicksToNextScan = mScanTickFrequency; mWeaponRangeSquared = 0; mWeaponLeadVelocitySquared = 0; mScanForTargets = false; mTrackTarget = false; mState = NULL; mStateDelayTime = 0; mStateActive = true; mStateAnimThread = NULL; // For the TurretShape class mSubclassTurretShapeHandlesScene = true; } AITurretShape::~AITurretShape() { _cleanupPotentialTargets(); } //---------------------------------------------------------------------------- void AITurretShape::initPersistFields() { Parent::initPersistFields(); } bool AITurretShape::onAdd() { if( !Parent::onAdd() ) return false; // Add this object to the scene addToScene(); _setScanBox(); if (isServerObject()) _initState(); if (isServerObject()) scriptOnAdd(); return true; } void AITurretShape::onRemove() { Parent::onRemove(); scriptOnRemove(); mIgnoreObjects.clear(); // Remove this object from the scene removeFromScene(); } bool AITurretShape::onNewDataBlock(GameBaseData* dptr, bool reload) { mDataBlock = dynamic_cast(dptr); if (!mDataBlock || !Parent::onNewDataBlock(dptr, reload)) return false; mScanHeading = mDegToRad(mDataBlock->maxScanHeading); if (mIsZero(mScanHeading)) mScanHeading = M_PI_F; mScanPitch = mDegToRad(mDataBlock->maxScanPitch); if (mIsZero(mScanPitch)) mScanPitch = M_PI_F; mScanDistance = mDataBlock->maxScanDistance; mScanDistanceSquared = mScanDistance * mScanDistance; mWeaponRangeSquared = mDataBlock->maxWeaponRange * mDataBlock->maxWeaponRange; mWeaponLeadVelocitySquared = (mDataBlock->weaponLeadVelocity > 0) ? (mDataBlock->weaponLeadVelocity * mDataBlock->weaponLeadVelocity) : 0; // The scan box is built such that the scanning origin is at (0,0,0) and the scanning distance // is out along the y axis. When this is transformed to the turret's location is provides a // scanning volume in front of the turret. F32 scanX = mScanDistance*mSin(mScanHeading); F32 scanY = mScanDistance; F32 scanZ = mScanDistance*mSin(mScanPitch); mScanBox.set(-scanX, 0, -scanZ, scanX, scanY, scanZ); mScanTickFrequency = mDataBlock->scanTickFrequency; if (mScanTickFrequency < 1) mScanTickFrequency = 1; mScanTickFrequencyVariance = mDataBlock->scanTickFrequencyVariance; if (mScanTickFrequencyVariance < 0) mScanTickFrequencyVariance = 0; mTicksToNextScan = mScanTickFrequency; // For states mStateAnimThread = 0; if (mDataBlock->isAnimated) { mStateAnimThread = mShapeInstance->addThread(); mShapeInstance->setTimeScale(mStateAnimThread,0); } scriptOnNewDataBlock(); return true; } //---------------------------------------------------------------------------- void AITurretShape::addToIgnoreList(ShapeBase* obj) { mIgnoreObjects.addObject(obj); } void AITurretShape::removeFromIgnoreList(ShapeBase* obj) { mIgnoreObjects.removeObject(obj); } void AITurretShape::clearIgnoreList() { mIgnoreObjects.clear(); } S32 AITurretShape::ignoreListCount() { return mIgnoreObjects.size(); } SimObject* AITurretShape::getIgnoreListObject(S32 index) { return mIgnoreObjects.at(index); } //---------------------------------------------------------------------------- void AITurretShape::_initState() { // Set the turret to its starting state. setTurretState(0, true); } void AITurretShape::setTurretStateName(const char* newState, bool force) { S32 stateVal = mDataBlock->lookupState(newState); if (stateVal >= 0) { setTurretState(stateVal, force); } } void AITurretShape::setTurretState(U32 newState, bool force) { setMaskBits(TurretStateMask); // If going back into the same state, just reset the timer // and invoke the script callback if (!force && mState == &mDataBlock->state[newState]) { mStateDelayTime = mState->timeoutValue; if (mState->script && !isGhost()) _scriptCallback(mState->script); return; } mState = &mDataBlock->state[newState]; // Reset cyclic sequences back to the first frame to turn it off // (the first key frame should be it's off state). if (mStateAnimThread && mStateAnimThread->getSequence()->isCyclic()) { mShapeInstance->setPos(mStateAnimThread,0); mShapeInstance->setTimeScale(mStateAnimThread,0); } AITurretShapeData::StateData& stateData = *mState; // Check for immediate transitions S32 ns; if ((ns = stateData.transition.rest[mAtRest]) != -1) { setTurretState(ns); return; } if ((ns = stateData.transition.target[mTarget.isValid()]) != -1) { setTurretState(ns); return; } if ((ns = stateData.transition.activated[mStateActive]) != -1) { setTurretState(ns); return; } // // Initialize the new state... // mStateDelayTime = stateData.timeoutValue; // Play animation if (mStateAnimThread && stateData.sequence != -1) { mShapeInstance->setSequence(mStateAnimThread,stateData.sequence, stateData.direction ? 0.0f : 1.0f); F32 timeScale = (stateData.scaleAnimation && stateData.timeoutValue) ? mShapeInstance->getDuration(mStateAnimThread) / stateData.timeoutValue : 1.0f; mShapeInstance->setTimeScale(mStateAnimThread, stateData.direction ? timeScale : -timeScale); } // Script callback on server if (stateData.script && stateData.script[0] && !isGhost()) _scriptCallback(stateData.script); // If there is a zero timeout, and a timeout transition, then // go ahead and transition imediately. if (!mStateDelayTime) { if ((ns = stateData.transition.timeout) != -1) { setTurretState(ns); return; } } } void AITurretShape::_updateTurretState(F32 dt) { AITurretShapeData::StateData& stateData = *mState; mStateDelayTime -= dt; // Check for transitions. On some states we must wait for the // full timeout value before moving on. if (mStateDelayTime <= 0 || !stateData.waitForTimeout) { S32 ns; if ((ns = stateData.transition.rest[mAtRest]) != -1) setTurretState(ns); else if ((ns = stateData.transition.target[mTarget.isValid()]) != -1) setTurretState(ns); else if ((ns = stateData.transition.activated[mStateActive]) != -1) setTurretState(ns); else if (mStateDelayTime <= 0 && (ns = stateData.transition.timeout) != -1) setTurretState(ns); } } void AITurretShape::_scriptCallback(const char* function) { Con::executef( mDataBlock, function, getIdString() ); } //---------------------------------------------------------------------------- void AITurretShape::_cleanupPotentialTargets() { mPotentialTargets.clear(); } void AITurretShape::_performScan() { // Only on server if (isClientObject()) return; // Are we ready for a scan? --mTicksToNextScan; if (mTicksToNextScan > 0) return; _cleanupPotentialTargets(); _setScanBox(); // Set up for the scan getScanTransform(mScanWorkspaceScanMat); mScanWorkspaceScanWorldMat = mScanWorkspaceScanMat; mScanWorkspaceScanWorldMat.affineInverse(); disableCollision(); for ( SimSetIterator iter(&mIgnoreObjects); *iter; ++iter ) { ShapeBase* obj = static_cast( *iter ); obj->disableCollision(); } gServerContainer.findObjects( mTransformedScanBox, sScanTypeMask, _scanCallback, (void*)this ); for ( SimSetIterator iter(&mIgnoreObjects); *iter; ++iter ) { ShapeBase* obj = static_cast( *iter ); obj->enableCollision(); } enableCollision(); if (mPotentialTargets.size() == 0) { // No targets in range. Clear out our current target, if necessary. _lostTarget(); } else { // Sort the targets comparePoint = getPosition(); dQsort(mPotentialTargets.address(),mPotentialTargets.size(),sizeof(SimObjectList::value_type),_sortCallback); // Go through the targets in order to find one that is not blocked from view Point3F start; mScanWorkspaceScanMat.getColumn(3, &start); S32 index = 0; bool los = false; disableCollision(); for (index=0; index < mPotentialTargets.size(); ++index) { ShapeBase* shape = (ShapeBase*)mPotentialTargets[index]; Point3F sightPoint; los = _testTargetLineOfSight(start, shape, sightPoint); // Check if we have a clear line of sight if (los) break; } enableCollision(); // If we found a valid, visible target (no hits between here and there), latch on to it if (los) { _gainedTarget((ShapeBase*)mPotentialTargets[index]); } } // Prepare for next scan period mTicksToNextScan = mScanTickFrequency; if (mScanTickFrequencyVariance > 0) { mTicksToNextScan += gRandGen.randI(0, mScanTickFrequencyVariance); } } void AITurretShape::addPotentialTarget(ShapeBase* shape) { mPotentialTargets.push_back(shape); } void AITurretShape::_lostTarget() { mTarget.target = NULL; } void AITurretShape::_gainedTarget(ShapeBase* target) { mTarget.target = target; if (target) { mTarget.hadValidTarget = true; } } void AITurretShape::_trackTarget(F32 dt) { // Only on server if (isClientObject()) return; // We can only track a target if we have one if (!mTarget.isValid()) return; Point3F targetPos = mTarget.target->getBoxCenter(); // Can we see the target? MatrixF aimMat; getAimTransform(aimMat); Point3F start; aimMat.getColumn(3, &start); RayInfo ri; Point3F sightPoint; disableCollision(); bool los = _testTargetLineOfSight(start, mTarget.target, sightPoint); enableCollision(); if (!los) { // Target is blocked. Should we try to track from its last // known position and velocity? SimTime curTime = Sim::getCurrentTime(); if ( (curTime - mTarget.lastSightTime) > (mDataBlock->trackLostTargetTime * 1000.0f) ) { // Time's up. Stop tracking. _cleanupTargetAndTurret(); return; } // Use last known information to attempt to // continue to track target for a while. targetPos = mTarget.lastPos + mTarget.lastVel * F32(curTime - mTarget.lastSightTime) / 1000.0f; } else { // Target is visible // We only track targets that are alive if (mTarget.target->getDamageState() != Enabled) { // We can't track any more _cleanupTargetAndTurret(); return; } targetPos = sightPoint; // Store latest target info mTarget.lastPos = targetPos; mTarget.lastVel = mTarget.target->getVelocity(); mTarget.lastSightTime = Sim::getCurrentTime(); } // Calculate angles to face the target, specifically the part that we can see VectorF toTarget; MatrixF mat; S32 node = mDataBlock->aimNode; if (node != -1) { // Get the current position of our node MatrixF* nodeTrans = &mShapeInstance->mNodeTransforms[node]; Point3F currentPos; nodeTrans->getColumn(3, ¤tPos); // Turn this into a matrix we can use to put the target // position into our space. MatrixF nodeMat(true); nodeMat.setColumn(3, currentPos); mat.mul(mObjToWorld, nodeMat); mat.affineInverse(); } else { mat = mWorldToObj; } mat.mulP(targetPos, &toTarget); // lead the target F32 timeToTargetSquared = (mWeaponLeadVelocitySquared > 0) ? toTarget.lenSquared() / mWeaponLeadVelocitySquared : 0; if (timeToTargetSquared > 1.0) { targetPos = targetPos + (mTarget.lastVel * mSqrt(timeToTargetSquared)); mat.mulP(targetPos, &toTarget); } F32 yaw, pitch; MathUtils::getAnglesFromVector(toTarget, yaw, pitch); if (yaw > M_PI_F) yaw = yaw - M_2PI_F; //if (pitch > M_PI_F) // pitch = -(pitch - M_2PI_F); Point3F rot(-pitch, 0.0f, yaw); // If we have a rotation rate make sure we follow it if (mHeadingRate > 0) { F32 rate = mHeadingRate * dt; F32 rateCheck = mFabs(rot.z - mRot.z); if (rateCheck > rate) { // This will clamp the new value to the rate regardless if it // is increasing or decreasing. rot.z = mClampF(rot.z, mRot.z-rate, mRot.z+rate); } } if (mPitchRate > 0) { F32 rate = mPitchRate * dt; F32 rateCheck = mFabs(rot.x - mRot.x); if (rateCheck > rate) { // This will clamp the new value to the rate regardless if it // is increasing or decreasing. rot.x = mClampF(rot.x, mRot.x-rate, mRot.x+rate); } } // Test if the rotation to the target is outside of our limits if (_outsideLimits(rot)) { // We can't track any more _cleanupTargetAndTurret(); return; } // Test if the target is out of weapons range if (toTarget.lenSquared() > mWeaponRangeSquared) { // We can't track any more _cleanupTargetAndTurret(); return; } mRot = rot; _setRotation( mRot ); setMaskBits(TurretUpdateMask); } void AITurretShape::_cleanupTargetAndTurret() { _lostTarget(); resetTarget(); } bool AITurretShape::_testTargetLineOfSight(Point3F& aimPoint, ShapeBase* target, Point3F& sightPoint) { Point3F targetCenter = target->getBoxCenter(); RayInfo ri; bool hit = false; target->disableCollision(); // First check for a clear line of sight to the target's center Point3F testPoint = targetCenter; hit = gServerContainer.castRay(aimPoint, testPoint, sAimTypeMask, &ri); if (hit) { // No clear line of sight to center, so try to the target's right. Players holding // a gun in their right hand will tend to stick their right shoulder out first if // they're peering around some cover to shoot, like a wall. Box3F targetBounds = target->getObjBox(); F32 radius = targetBounds.len_x() > targetBounds.len_y() ? targetBounds.len_x() : targetBounds.len_y(); radius *= 0.5; VectorF toTurret = aimPoint - targetCenter; toTurret.normalizeSafe(); VectorF toTurretRight = mCross(toTurret, Point3F::UnitZ); testPoint = targetCenter + toTurretRight * radius; hit = gServerContainer.castRay(aimPoint, testPoint, sAimTypeMask, &ri); if (hit) { // No clear line of sight to right, so try the target's left VectorF toTurretLeft = toTurretRight * -1.0f; testPoint = targetCenter + toTurretLeft * radius; hit = gServerContainer.castRay(aimPoint, testPoint, sAimTypeMask, &ri); } if (hit) { // No clear line of sight to left, so try the target's top testPoint = targetCenter; testPoint.z += targetBounds.len_z() * 0.5f; hit = gServerContainer.castRay(aimPoint, testPoint, sAimTypeMask, &ri); } if (hit) { // No clear line of sight to top, so try the target's bottom testPoint = targetCenter; testPoint.z -= targetBounds.len_z() * 0.5f; hit = gServerContainer.castRay(aimPoint, testPoint, sAimTypeMask, &ri); } } target->enableCollision(); if (!hit) { // Line of sight point is that last one we tested sightPoint = testPoint; } return !hit; } //---------------------------------------------------------------------------- void AITurretShape::setAllGunsFiring(bool fire) { setImageTriggerState(0,fire); setImageTriggerState(1,fire); setImageTriggerState(2,fire); setImageTriggerState(3,fire); } void AITurretShape::setGunSlotFiring(S32 slot, bool fire) { if (slot < 0 || slot > 3) return; setImageTriggerState(slot, fire); } //---------------------------------------------------------------------------- void AITurretShape::setTransform(const MatrixF& mat) { Parent::setTransform(mat); // Set the scanning box _setScanBox(); } void AITurretShape::recenterTurret() { mRot.set(0,0,0); _setRotation( mRot ); setMaskBits(TurretUpdateMask); } void AITurretShape::_setScanBox() { mTransformedScanBox = mScanBox; MatrixF mat; getScanTransform(mat); mat.mul(mTransformedScanBox); } void AITurretShape::getScanTransform(MatrixF& mat) { if (mDataBlock && mDataBlock->scanNode != -1) { if (getNodeTransform(mDataBlock->scanNode, mat)) { return; } } mat = mObjToWorld; } void AITurretShape::getAimTransform(MatrixF& mat) { if (mDataBlock && mDataBlock->aimNode != -1) { if (getNodeTransform(mDataBlock->aimNode, mat)) { return; } } mat = mObjToWorld; } //---------------------------------------------------------------------------- void AITurretShape::processTick(const Move* move) { Parent::processTick(move); if (isServerObject() && mDamageState == Enabled) { _updateTurretState(TickSec); if (mScanForTargets) { // Perform a scan for targets _performScan(); // If we found one, turn off the scan if (mTarget.isValid()) { mScanForTargets = false; } } if (mTrackTarget) { _trackTarget(TickSec); // If the target is lost, no longer track it if (!mTarget.isValid()) { mTrackTarget = false; } } } } void AITurretShape::advanceTime(F32 dt) { // If there were any ShapeBase script threads that // have played, then we need to update all code // controlled nodes. This is done before the Parent // call as script threads may play and be destroyed // before our code is called. bool updateNodes = false; for (U32 i = 0; i < MaxScriptThreads; i++) { Thread& st = mScriptThread[i]; if (st.thread) { updateNodes = true; break; } } Parent::advanceTime(dt); // Update any state thread AITurretShapeData::StateData& stateData = *mState; if (mStateAnimThread && stateData.sequence != -1) { mShapeInstance->advanceTime(dt,mStateAnimThread); updateNodes = true; } if (updateNodes) { // Update all code controlled nodes _updateNodes(mRot); } } //---------------------------------------------------------------------------- U32 AITurretShape::packUpdate(NetConnection *connection, U32 mask, BitStream *bstream) { U32 retMask = Parent::packUpdate(connection,mask,bstream); // Indicate that the transform has changed to update the scan box bstream->writeFlag(mask & (PositionMask | ExtendedInfoMask)); // Handle any state changes that need to be passed along if (bstream->writeFlag(mask & TurretStateMask)) { bstream->write(mDataBlock->lookupState(mState->name)); } return retMask; } void AITurretShape::unpackUpdate(NetConnection *connection, BitStream *bstream) { Parent::unpackUpdate(connection,bstream); // Transform has changed if (bstream->readFlag()) { _setScanBox(); } //TurretStateMask if (bstream->readFlag()) { S32 newstate; bstream->read( &newstate ); setTurretState(newstate); } } //---------------------------------------------------------------------------- void AITurretShape::prepBatchRender( SceneRenderState *state, S32 mountedImageIndex ) { Parent::prepBatchRender( state, mountedImageIndex ); if ( !gShowBoundingBox ) return; ObjectRenderInst *ri = state->getRenderPass()->allocInst(); ri->renderDelegate.bind( this, &AITurretShape::_renderScanner ); ri->type = RenderPassManager::RIT_Editor; state->getRenderPass()->addInst( ri ); } void AITurretShape::_renderScanner( ObjectRenderInst *ri, SceneRenderState *state, BaseMatInstance *overrideMat ) { GFXStateBlockDesc desc; desc.setBlend(false, GFXBlendSrcAlpha, GFXBlendInvSrcAlpha); desc.setZReadWrite(false,true); desc.fillMode = GFXFillWireframe; // Render the scan box GFX->getDrawUtil()->drawCube(desc, mTransformedScanBox.getExtents(), mTransformedScanBox.getCenter(), ColorI(255, 0, 0)); // Render a line from the scan node to the max scan distance MatrixF nodeMat; if (getNodeTransform(mDataBlock->scanNode, nodeMat)) { Point3F start; nodeMat.getColumn(3, &start); Point3F end(0.0f, mScanDistance, 0.0f); nodeMat.mulP(end); GFX->getDrawUtil()->drawLine(start, end, ColorI(255, 255, 0)); } } //---------------------------------------------------------------------------- DefineEngineMethod( AITurretShape, addToIgnoreList, void, (ShapeBase* obj),, "@brief Adds object to the turret's ignore list.\n\n" "All objects in this list will be ignored by the turret's targeting.\n" "@param obj The ShapeBase object to ignore.\n") { object->addToIgnoreList(obj); } DefineEngineMethod( AITurretShape, removeFromIgnoreList, void, (ShapeBase* obj),, "@brief Removes object from the turret's ignore list.\n\n" "All objects in this list will be ignored by the turret's targeting.\n" "@param obj The ShapeBase object to once again allow for targeting.\n") { object->removeFromIgnoreList(obj); } DefineEngineMethod( AITurretShape, clearIgnoreList, void, (),, "@brief Removes all objects from the turret's ignore list.\n\n" "All objects in this list will be ignored by the turret's targeting.\n") { object->clearIgnoreList(); } DefineEngineMethod( AITurretShape, ignoreListCount, S32, (),, "@brief Returns the number of objects in the turrets ignore list.\n\n" "All objects in this list will be ignored by the turret's targeting.\n") { return object->ignoreListCount(); } DefineEngineMethod( AITurretShape, getIgnoreListObject, SimObject*, (S32 index),, "@brief Returns the object in the ignore list at index.\n\n" "All objects in this list will be ignored by the turret's targeting.\n" "@param index The index of the object in the ignore list being retrieved.\n") { return object->getIgnoreListObject(index); } DefineEngineMethod( AITurretShape, setTurretState, void, (const char* newState, bool force), (false), "@brief Set the turret's current state.\n\n" "Normally the turret's state comes from updating the state machine but this method " "allows you to override this and jump to the requested state immediately.\n" "@param newState The name of the new state.\n" "@param force Is true then force the full processing of the new state even if it is the " "same as the current state. If false then only the time out value is reset and the state's " "script method is called, if any.\n") { object->setTurretStateName(newState, force); } DefineEngineMethod( AITurretShape, activateTurret, void, ( ),, "@brief Activate a turret from a deactive state.\n\n") { object->activateTurret(); } DefineEngineMethod( AITurretShape, deactivateTurret, void, ( ),, "@brief Deactivate a turret from an active state.\n\n") { object->deactivateTurret(); } DefineEngineMethod( AITurretShape, startScanForTargets, void, ( ),, "@brief Begin scanning for a target.\n\n") { object->startScanForTargets(); } DefineEngineMethod( AITurretShape, stopScanForTargets, void, ( ),, "@brief Stop scanning for targets.\n\n" "@note Only impacts the scanning for new targets. Does not effect a turret's current " "target lock.\n") { object->stopScanForTargets(); } DefineEngineMethod( AITurretShape, startTrackingTarget, void, ( ),, "@brief Have the turret track the current target.\n\n") { object->startTrackingTarget(); } DefineEngineMethod( AITurretShape, stopTrackingTarget, void, ( ),, "@brief Stop the turret from tracking the current target.\n\n") { object->stopTrackingTarget(); } DefineEngineMethod( AITurretShape, hasTarget, bool, (),, "@brief Indicates if the turret has a target.\n\n" "@returns True if the turret has a target.\n") { return object->hasTarget(); } DefineEngineMethod( AITurretShape, getTarget, SimObject*, (),, "@brief Get the turret's current target.\n\n" "@returns The object that is the target's current target, or 0 if no target.\n") { return object->getTarget(); } DefineEngineMethod( AITurretShape, resetTarget, void, ( ),, "@brief Resets the turret's target tracking.\n\n" "Only resets the internal target tracking. Does not modify the turret's facing.\n") { object->resetTarget(); } DefineEngineMethod( AITurretShape, setWeaponLeadVelocity, void, (F32 velocity),, "@brief Set the turret's projectile velocity to help lead the target.\n\n" "This value normally comes from AITurretShapeData::weaponLeadVelocity but this method " "allows you to override the datablock value. This can be useful if the turret changes " "ammunition, uses a different weapon than the default, is damaged, etc.\n" "@note Setting this to 0 will disable target leading.\n") { object->setWeaponLeadVelocity(velocity); } DefineEngineMethod( AITurretShape, getWeaponLeadVelocity, F32, (),, "@brief Get the turret's defined projectile velocity that helps with target leading.\n\n" "@returns The defined weapon projectile speed, or 0 if leading is disabled.\n") { return object->getWeaponLeadVelocity(); } DefineEngineMethod( AITurretShape, setAllGunsFiring, void, (bool fire),, "@brief Set the firing state of the turret's guns.\n\n" "@param fire Set to true to activate all guns. False to deactivate them.\n") { object->setAllGunsFiring(fire); } DefineEngineMethod( AITurretShape, setGunSlotFiring, void, (S32 slot, bool fire),, "@brief Set the firing state of the given gun slot.\n\n" "@param slot The gun to modify. Valid range is 0-3 that corresponds to the weapon mount point.\n" "@param fire Set to true to activate the gun. False to deactivate it.\n") { object->setGunSlotFiring(slot, fire); } DefineEngineMethod( AITurretShape, recenterTurret, void, ( ),, "@brief Recenter the turret's weapon.\n\n") { object->recenterTurret(); }