diff --git a/Engine/source/T3D/aiPlayer.cpp b/Engine/source/T3D/aiPlayer.cpp index 38a3133e6..6e1c39328 100644 --- a/Engine/source/T3D/aiPlayer.cpp +++ b/Engine/source/T3D/aiPlayer.cpp @@ -20,6 +20,11 @@ // IN THE SOFTWARE. //----------------------------------------------------------------------------- +//~~~~~~~~~~~~~~~~~~~~//~~~~~~~~~~~~~~~~~~~~//~~~~~~~~~~~~~~~~~~~~//~~~~~~~~~~~~~~~~~~~~~// +// Arcane-FX for MIT Licensed Open Source version of Torque 3D from GarageGames +// Copyright (C) 2015 Faust Logic, Inc. +//~~~~~~~~~~~~~~~~~~~~//~~~~~~~~~~~~~~~~~~~~//~~~~~~~~~~~~~~~~~~~~//~~~~~~~~~~~~~~~~~~~~~// + #include "platform/platform.h" #include "T3D/aiPlayer.h" @@ -97,6 +102,9 @@ AIPlayer::AIPlayer() mMoveSlowdown = true; mMoveState = ModeStop; + // This new member saves the movement state of the AI so that + // it can be restored after a substituted animation is finished. + mMoveState_saved = -1; mAimObject = 0; mAimLocationSet = false; mTargetInLOS = false; @@ -547,23 +555,27 @@ bool AIPlayer::getAIMove(Move *movePtr) mMoveState = ModeMove; } - if (mMoveStuckTestCountdown > 0) - --mMoveStuckTestCountdown; - else - { - // We should check to see if we are stuck... - F32 locationDelta = (location - mLastLocation).len(); + // Don't check for ai stuckness if animation during + // an anim-clip effect override. + if (mDamageState == Enabled && !(anim_clip_flags & ANIM_OVERRIDDEN) && !isAnimationLocked()) { + if (mMoveStuckTestCountdown > 0) + --mMoveStuckTestCountdown; + else + { + // We should check to see if we are stuck... + F32 locationDelta = (location - mLastLocation).len(); if (locationDelta < mMoveStuckTolerance && mDamageState == Enabled) { // If we are slowing down, then it's likely that our location delta will be less than // our move stuck tolerance. Because we can be both slowing and stuck // we should TRY to check if we've moved. This could use better detection. if ( mMoveState != ModeSlowing || locationDelta == 0 ) - { - mMoveState = ModeStuck; - onStuck(); - } - } + { + mMoveState = ModeStuck; + onStuck(); + } + } + } } } } @@ -626,6 +638,7 @@ bool AIPlayer::getAIMove(Move *movePtr) } #endif // TORQUE_NAVIGATION_ENABLED + if (!(anim_clip_flags & ANIM_OVERRIDDEN) && !isAnimationLocked()) mLastLocation = location; return true; @@ -1415,6 +1428,47 @@ DefineEngineMethod( AIPlayer, clearMoveTriggers, void, ( ),, object->clearMoveTriggers(); } +// These changes coordinate with anim-clip mods to parent class, Player. + +// New method, restartMove(), restores the AIPlayer to its normal move-state +// following animation overrides from AFX. The tag argument is used to match +// the latest override and prevents interruption of overlapping animation +// overrides. See related anim-clip changes in Player.[h,cc]. +void AIPlayer::restartMove(U32 tag) +{ + if (tag != 0 && tag == last_anim_tag) + { + if (mMoveState_saved != -1) + { + mMoveState = (MoveState) mMoveState_saved; + mMoveState_saved = -1; + } + + bool is_death_anim = ((anim_clip_flags & IS_DEATH_ANIM) != 0); + + last_anim_tag = 0; + anim_clip_flags &= ~(ANIM_OVERRIDDEN | IS_DEATH_ANIM); + + if (mDamageState != Enabled) + { + if (!is_death_anim) + { + // this is a bit hardwired and desperate, + // but if he's dead he needs to look like it. + setActionThread("death10", false, false, false); + } + } + } +} + +// New method, saveMoveState(), stores the current movement state +// so that it can be restored when restartMove() is called. +void AIPlayer::saveMoveState() +{ + if (mMoveState_saved == -1) + mMoveState_saved = (S32) mMoveState; +} + F32 AIPlayer::getTargetDistance(GameBase* target, bool _checkEnabled) { if (!isServerObject()) return false; diff --git a/Engine/source/T3D/aiPlayer.h b/Engine/source/T3D/aiPlayer.h index a8430575c..524b0ba16 100644 --- a/Engine/source/T3D/aiPlayer.h +++ b/Engine/source/T3D/aiPlayer.h @@ -20,6 +20,11 @@ // IN THE SOFTWARE. //----------------------------------------------------------------------------- +//~~~~~~~~~~~~~~~~~~~~//~~~~~~~~~~~~~~~~~~~~//~~~~~~~~~~~~~~~~~~~~//~~~~~~~~~~~~~~~~~~~~~// +// Arcane-FX for MIT Licensed Open Source version of Torque 3D from GarageGames +// Copyright (C) 2015 Faust Logic, Inc. +//~~~~~~~~~~~~~~~~~~~~//~~~~~~~~~~~~~~~~~~~~//~~~~~~~~~~~~~~~~~~~~//~~~~~~~~~~~~~~~~~~~~~// + #ifndef _AIPLAYER_H_ #define _AIPLAYER_H_ @@ -225,6 +230,18 @@ public: /// @} #endif // TORQUE_NAVIGATION_ENABLED + // New method, restartMove(), restores the AIPlayer to its normal move-state + // following animation overrides from AFX. The tag argument is used to match + // the latest override and prevents interruption of overlapping animation + // overrides. + // New method, saveMoveState(), stores the current movement state + // so that it can be restored when restartMove() is called. + // See related anim-clip changes in Player.[h,cc]. +private: + S32 mMoveState_saved; +public: + void restartMove(U32 tag); + void saveMoveState(); }; #endif diff --git a/Engine/source/T3D/player.cpp b/Engine/source/T3D/player.cpp index 6b4d4ae0d..41cf55c03 100644 --- a/Engine/source/T3D/player.cpp +++ b/Engine/source/T3D/player.cpp @@ -2746,7 +2746,12 @@ void Player::updateMove(const Move* move) // Desired move direction & speed VectorF moveVec; F32 moveSpeed; - if ((mState == MoveState || (mState == RecoverState && mDataBlock->recoverRunForceScale > 0.0f)) && mDamageState == Enabled) + // If BLOCK_USER_CONTROL is set in anim_clip_flags, the user won't be able to + // resume control over the player character. This generally happens for + // short periods of time synchronized with script driven animation at places + // where it makes sense that user motion is prohibited, such as when the + // player is lifted off the ground or knocked down. + if ((mState == MoveState || (mState == RecoverState && mDataBlock->recoverRunForceScale > 0.0f)) && mDamageState == Enabled && !isAnimationLocked()) { zRot.getColumn(0,&moveVec); moveVec *= (move->x * (mPose == SprintPose ? mDataBlock->sprintStrafeScale : 1.0f)); @@ -3031,7 +3036,9 @@ void Player::updateMove(const Move* move) mContactTimer++; // Acceleration from Jumping - if (move->trigger[sJumpTrigger] && canJump())// !isMounted() && + // While BLOCK_USER_CONTROL is set in anim_clip_flags, the user won't be able to + // make the player character jump. + if (move->trigger[sJumpTrigger] && canJump() && !isAnimationLocked()) { // Scale the jump impulse base on maxJumpSpeed F32 zSpeedScale = mVelocity.z; @@ -3539,6 +3546,8 @@ void Player::updateLookAnimation(F32 dt) bool Player::inDeathAnim() { + if ((anim_clip_flags & ANIM_OVERRIDDEN) != 0 && (anim_clip_flags & IS_DEATH_ANIM) == 0) + return false; if (mActionAnimation.thread && mActionAnimation.action >= 0) if (mActionAnimation.action < mDataBlock->actionCount) return mDataBlock->actionList[mActionAnimation.action].death; @@ -3748,6 +3757,8 @@ bool Player::setArmThread(U32 action) bool Player::setActionThread(const char* sequence,bool hold,bool wait,bool fsp) { + if (anim_clip_flags & ANIM_OVERRIDDEN) + return false; for (U32 i = 1; i < mDataBlock->actionCount; i++) { PlayerData::ActionAnimation &anim = mDataBlock->actionList[i]; @@ -3947,8 +3958,10 @@ void Player::updateActionThread() pickActionAnimation(); } + // prevent scaling of AFX picked actions if ( (mActionAnimation.action != PlayerData::LandAnim) && - (mActionAnimation.action != PlayerData::NullAnimation) ) + (mActionAnimation.action != PlayerData::NullAnimation) && + !(anim_clip_flags & ANIM_OVERRIDDEN)) { // Update action animation time scale to match ground velocity PlayerData::ActionAnimation &anim = @@ -4566,6 +4579,10 @@ void Player::updateAnimation(F32 dt) if (mImageStateThread) mShapeInstance->advanceTime(dt,mImageStateThread); + // update any active blend clips + if (isGhost()) + for (S32 i = 0; i < blend_clips.size(); i++) + mShapeInstance->advanceTime(dt, blend_clips[i].thread); // If we are the client's player on this machine, then we need // to make sure the transforms are up to date as they are used // to setup the camera. @@ -4579,6 +4596,11 @@ void Player::updateAnimation(F32 dt) else { updateAnimationTree(false); + // This addition forces recently visible players to animate their + // skeleton now rather than in pre-render so that constrained effects + // get up-to-date node transforms. + if (didRenderLastRender()) + mShapeInstance->animate(); } } } @@ -7249,6 +7271,165 @@ void Player::afx_unpackUpdate(NetConnection* con, BitStream* stream) mark_fx_c_triggers = mask; } } + +// Code for overriding player's animation with sequences selected by the +// anim-clip component effect. + +void Player::restoreAnimation(U32 tag) +{ + // check if this is a blended clip + if ((tag & BLENDED_CLIP) != 0) + { + restoreBlendAnimation(tag); + return; + } + + if (tag != 0 && tag == last_anim_tag) + { + bool is_death_anim = ((anim_clip_flags & IS_DEATH_ANIM) != 0); + + anim_clip_flags &= ~(ANIM_OVERRIDDEN | IS_DEATH_ANIM); + + if (isClientObject()) + { + if (mDamageState != Enabled) + { + if (!is_death_anim) + { + // this is a bit hardwired and desperate, + // but if he's dead he needs to look like it. + setActionThread("death10", false, false, false); + } + } + else if (mState != MoveState) + { + // not sure what happens here + } + else + { + pickActionAnimation(); + } + } + + last_anim_tag = 0; + last_anim_id = -1; + } +} + +U32 Player::getAnimationID(const char* name) +{ + for (U32 i = 0; i < mDataBlock->actionCount; i++) + { + PlayerData::ActionAnimation &anim = mDataBlock->actionList[i]; + if (dStricmp(anim.name, name) == 0) + return i; + } + + Con::errorf("Player::getAnimationID() -- Player does not contain a sequence that matches the name, %s.", name); + return BAD_ANIM_ID; +} + +U32 Player::playAnimationByID(U32 anim_id, F32 pos, F32 rate, F32 trans, bool hold, bool wait, bool is_death_anim) +{ + if (anim_id == BAD_ANIM_ID) + return 0; + + S32 seq_id = mDataBlock->actionList[anim_id].sequence; + if (seq_id == -1) + { + Con::errorf("Player::playAnimation() problem. BAD_SEQ_ID"); + return 0; + } + + if (mShapeInstance->getShape()->sequences[seq_id].isBlend()) + return playBlendAnimation(seq_id, pos, rate); + + if (isClientObject()) + { + PlayerData::ActionAnimation &anim = mDataBlock->actionList[anim_id]; + if (anim.sequence != -1) + { + mActionAnimation.action = anim_id; + mActionAnimation.forward = (rate >= 0); + mActionAnimation.firstPerson = false; + mActionAnimation.holdAtEnd = hold; + mActionAnimation.waitForEnd = hold? true: wait; + mActionAnimation.animateOnServer = false; + mActionAnimation.atEnd = false; + mActionAnimation.delayTicks = (S32)sNewAnimationTickTime; + + F32 transTime = (trans < 0) ? sAnimationTransitionTime : trans; + + mShapeInstance->setTimeScale(mActionAnimation.thread, rate); + mShapeInstance->transitionToSequence(mActionAnimation.thread,anim.sequence, + pos, transTime, true); + } + } + + if (is_death_anim) + anim_clip_flags |= IS_DEATH_ANIM; + else + anim_clip_flags &= ~IS_DEATH_ANIM; + + anim_clip_flags |= ANIM_OVERRIDDEN; + last_anim_tag = unique_anim_tag_counter++; + last_anim_id = anim_id; + + return last_anim_tag; +} + +F32 Player::getAnimationDurationByID(U32 anim_id) +{ + if (anim_id == BAD_ANIM_ID) + return 0.0f; + S32 seq_id = mDataBlock->actionList[anim_id].sequence; + if (seq_id >= 0 && seq_id < mDataBlock->mShape->sequences.size()) + return mDataBlock->mShape->sequences[seq_id].duration; + + return 0.0f; +} + +bool Player::isBlendAnimation(const char* name) +{ + U32 anim_id = getAnimationID(name); + if (anim_id == BAD_ANIM_ID) + return false; + + S32 seq_id = mDataBlock->actionList[anim_id].sequence; + if (seq_id >= 0 && seq_id < mDataBlock->mShape->sequences.size()) + return mDataBlock->mShape->sequences[seq_id].isBlend(); + + return false; +} + +const char* Player::getLastClipName(U32 clip_tag) +{ + if (clip_tag != last_anim_tag || last_anim_id >= PlayerData::NumActionAnims) + return ""; + + return mDataBlock->actionList[last_anim_id].name; +} + +void Player::unlockAnimation(U32 tag, bool force) +{ + if ((tag != 0 && tag == last_anim_lock_tag) || force) + anim_clip_flags &= ~BLOCK_USER_CONTROL; +} + +U32 Player::lockAnimation() +{ + anim_clip_flags |= BLOCK_USER_CONTROL; + last_anim_lock_tag = unique_anim_tag_counter++; + + return last_anim_lock_tag; +} + +ConsoleMethod(Player, isAnimationLocked, bool, 2, 2, "isAnimationLocked()") +{ + return object->isAnimationLocked(); +} + + #ifdef TORQUE_OPENVR void Player::setControllers(Vector controllerList) { diff --git a/Engine/source/T3D/player.h b/Engine/source/T3D/player.h index 367536058..ec9bf3569 100644 --- a/Engine/source/T3D/player.h +++ b/Engine/source/T3D/player.h @@ -793,6 +793,17 @@ private: private: static bool sCorpsesHiddenFromRayCast; +public: + virtual void restoreAnimation(U32 tag); + virtual U32 getAnimationID(const char* name); + virtual U32 playAnimationByID(U32 anim_id, F32 pos, F32 rate, F32 trans, bool hold, bool wait, bool is_death_anim); + virtual F32 getAnimationDurationByID(U32 anim_id); + virtual bool isBlendAnimation(const char* name); + virtual const char* getLastClipName(U32 clip_tag); + virtual void unlockAnimation(U32 tag, bool force=false); + virtual U32 lockAnimation(); + virtual bool isAnimationLocked() const { return ((anim_clip_flags & BLOCK_USER_CONTROL) != 0); } + }; typedef Player::Pose PlayerPose; diff --git a/Engine/source/T3D/shapeBase.cpp b/Engine/source/T3D/shapeBase.cpp index 1aebe6755..53f4509ed 100644 --- a/Engine/source/T3D/shapeBase.cpp +++ b/Engine/source/T3D/shapeBase.cpp @@ -1045,6 +1045,13 @@ ShapeBase::ShapeBase() for (i = 0; i < MaxTriggerKeys; i++) mTrigger[i] = false; + anim_clip_flags = 0; + last_anim_id = -1; + last_anim_tag = 0; + last_anim_lock_tag = 0; + saved_seq_id = -1; + saved_pos = 0.0f; + saved_rate = 1.0f; } @@ -1183,6 +1190,16 @@ void ShapeBase::onSceneRemove() bool ShapeBase::onNewDataBlock( GameBaseData *dptr, bool reload ) { + // need to destroy blend-clips or we crash + if (isGhost()) + { + for (S32 i = 0; i < blend_clips.size(); i++) + { + if (blend_clips[i].thread) + mShapeInstance->destroyThread(blend_clips[i].thread); + blend_clips.erase_fast(i); + } + } ShapeBaseData *prevDB = dynamic_cast( mDataBlock ); bool isInitialDataBlock = ( mDataBlock == 0 ); @@ -5101,6 +5118,186 @@ DefineEngineMethod( ShapeBase, getModelFile, const char *, (),, return datablock->getDataField( fieldName, NULL ); } + +U32 ShapeBase::unique_anim_tag_counter = 1; + +U32 ShapeBase::playBlendAnimation(S32 seq_id, F32 pos, F32 rate) +{ + BlendThread blend_clip; + blend_clip.tag = ((unique_anim_tag_counter++) | BLENDED_CLIP); + blend_clip.thread = 0; + + if (isClientObject()) + { + blend_clip.thread = mShapeInstance->addThread(); + mShapeInstance->setSequence(blend_clip.thread, seq_id, pos); + mShapeInstance->setTimeScale(blend_clip.thread, rate); + } + + blend_clips.push_back(blend_clip); + + return blend_clip.tag; +} + +void ShapeBase::restoreBlendAnimation(U32 tag) +{ + for (S32 i = 0; i < blend_clips.size(); i++) + { + if (blend_clips[i].tag == tag) + { + if (blend_clips[i].thread) + { + mShapeInstance->destroyThread(blend_clips[i].thread); + } + blend_clips.erase_fast(i); + break; + } + } +} + +// + +void ShapeBase::restoreAnimation(U32 tag) +{ + if (!isClientObject()) + return; + + // check if this is a blended clip + if ((tag & BLENDED_CLIP) != 0) + { + restoreBlendAnimation(tag); + return; + } + + if (tag != 0 && tag == last_anim_tag) + { + anim_clip_flags &= ~(ANIM_OVERRIDDEN | IS_DEATH_ANIM); + + stopThread(0); + + if (saved_seq_id != -1) + { + setThreadSequence(0, saved_seq_id); + setThreadPosition(0, saved_pos); + setThreadTimeScale(0, saved_rate); + setThreadDir(0, (saved_rate >= 0)); + playThread(0); + + saved_seq_id = -1; + saved_pos = 0.0f; + saved_rate = 1.0f; + } + + last_anim_tag = 0; + last_anim_id = -1; + } +} + +U32 ShapeBase::getAnimationID(const char* name) +{ + const TSShape* ts_shape = getShape(); + S32 seq_id = (ts_shape) ? ts_shape->findSequence(name) : -1; + return (seq_id >= 0) ? (U32) seq_id : BAD_ANIM_ID; +} + +U32 ShapeBase::playAnimationByID(U32 anim_id, F32 pos, F32 rate, F32 trans, bool hold, bool wait, bool is_death_anim) +{ + if (!isClientObject()) + return 0; + + if (anim_id == BAD_ANIM_ID) + return 0; + + const TSShape* ts_shape = getShape(); + if (!ts_shape) + return 0; + + S32 seq_id = (S32) anim_id; + if (mShapeInstance->getShape()->sequences[seq_id].isBlend()) + return playBlendAnimation(seq_id, pos, rate); + + if (last_anim_tag == 0) + { + // try to save state of playing animation + Thread& st = mScriptThread[0]; + if (st.sequence != -1) + { + saved_seq_id = st.sequence; + saved_pos = st.position; + saved_rate = st.timescale; + } + } + + // START OR TRANSITION TO SEQUENCE HERE + setThreadSequence(0, seq_id); + setThreadPosition(0, pos); + setThreadTimeScale(0, rate); + setThreadDir(0, (rate >= 0)); + playThread(0); + + if (is_death_anim) + anim_clip_flags |= IS_DEATH_ANIM; + else + anim_clip_flags &= ~IS_DEATH_ANIM; + + anim_clip_flags |= ANIM_OVERRIDDEN; + last_anim_tag = unique_anim_tag_counter++; + last_anim_id = anim_id; + + return last_anim_tag; +} + +F32 ShapeBase::getAnimationDurationByID(U32 anim_id) +{ + if (anim_id == BAD_ANIM_ID) + return 0.0f; + + S32 seq_id = (S32) anim_id; + if (seq_id >= 0 && seq_id < mDataBlock->mShape->sequences.size()) + return mDataBlock->mShape->sequences[seq_id].duration; + + return 0.0f; +} + +bool ShapeBase::isBlendAnimation(const char* name) +{ + U32 anim_id = getAnimationID(name); + if (anim_id == BAD_ANIM_ID) + return false; + + S32 seq_id = (S32) anim_id; + if (seq_id >= 0 && seq_id < mDataBlock->mShape->sequences.size()) + return mDataBlock->mShape->sequences[seq_id].isBlend(); + + return false; +} + +const char* ShapeBase::getLastClipName(U32 clip_tag) +{ + if (clip_tag != last_anim_tag) + return ""; + + S32 seq_id = (S32) last_anim_id; + + S32 idx = mDataBlock->mShape->sequences[seq_id].nameIndex; + if (idx < 0 || idx >= mDataBlock->mShape->names.size()) + return 0; + + return mDataBlock->mShape->names[idx]; +} + +// + +U32 ShapeBase::playAnimation(const char* name, F32 pos, F32 rate, F32 trans, bool hold, bool wait, bool is_death_anim) +{ + return playAnimationByID(getAnimationID(name), pos, rate, trans, hold, wait, is_death_anim); +} + +F32 ShapeBase::getAnimationDuration(const char* name) +{ + return getAnimationDurationByID(getAnimationID(name)); +} + void ShapeBase::setSelectionFlags(U8 flags) { Parent::setSelectionFlags(flags); diff --git a/Engine/source/T3D/shapeBase.h b/Engine/source/T3D/shapeBase.h index 163fa956b..9276cfb98 100644 --- a/Engine/source/T3D/shapeBase.h +++ b/Engine/source/T3D/shapeBase.h @@ -1861,6 +1861,43 @@ public: protected: DECLARE_CALLBACK( F32, validateCameraFov, (F32 fov) ); +protected: + enum { + ANIM_OVERRIDDEN = BIT(0), + BLOCK_USER_CONTROL = BIT(1), + IS_DEATH_ANIM = BIT(2), + BAD_ANIM_ID = 999999999, + BLENDED_CLIP = 0x80000000, + }; + struct BlendThread + { + TSThread* thread; + U32 tag; + }; + 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; + S32 saved_seq_id; + F32 saved_pos; + F32 saved_rate; + U32 playBlendAnimation(S32 seq_id, F32 pos, F32 rate); + void restoreBlendAnimation(U32 tag); +public: + U32 playAnimation(const char* name, F32 pos, F32 rate, F32 trans, bool hold, bool wait, bool is_death_anim); + F32 getAnimationDuration(const char* name); + + virtual void restoreAnimation(U32 tag); + virtual U32 getAnimationID(const char* name); + virtual U32 playAnimationByID(U32 anim_id, F32 pos, F32 rate, F32 trans, bool hold, bool wait, bool is_death_anim); + virtual F32 getAnimationDurationByID(U32 anim_id); + virtual bool isBlendAnimation(const char* name); + virtual const char* getLastClipName(U32 clip_tag); + virtual void unlockAnimation(U32 tag, bool force=false) { } + virtual U32 lockAnimation() { return 0; } + virtual bool isAnimationLocked() const { return false; } virtual void setSelectionFlags(U8 flags); };