Torque3D/Engine/source/T3D/turret/aiTurretShape.cpp
2020-05-11 15:30:21 -05:00

1390 lines
43 KiB
C++

//-----------------------------------------------------------------------------
// 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<ShapeBase*>(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<const ShapeBase* const*>(a));
const ShapeBase* s2 = (*reinterpret_cast<const ShapeBase* const*>(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<AITurretShapeData*>(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<ShapeBase*>( *iter );
obj->disableCollision();
}
gServerContainer.findObjects( mTransformedScanBox, sScanTypeMask, _scanCallback, (void*)this );
for ( SimSetIterator iter(&mIgnoreObjects); *iter; ++iter )
{
ShapeBase* obj = static_cast<ShapeBase*>( *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, &currentPos);
// 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<ObjectRenderInst>();
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();
}