/*==LICENSE==* CyanWorlds.com Engine - MMOG client, server and tools Copyright (C) 2011 Cyan Worlds, Inc. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . You can contact Cyan Worlds, Inc. by email legal@cyan.com or by snail mail at: Cyan Worlds, Inc. 14617 N Newport Hwy Mead, WA 99021 *==LICENSE==*/ ///////////////////////////////////////////////////////////////////////////////////////// // // INCLUDES // ///////////////////////////////////////////////////////////////////////////////////////// // singular #include "plAvTaskSeek.h" // local #include "plAvBrainHuman.h" #include "plAGAnim.h" #include "plArmatureMod.h" #include "plAvatarMgr.h" #include "plAvCallbackAction.h" // other #include "../plMessage/plAvatarMsg.h" #include "../pnMessage/plCameraMsg.h" #include "../pnInputCore/plControlEventCodes.h" #include "../plPipeline/plDebugText.h" #include "../plStatusLog/plStatusLog.h" #include "../pnSceneObject/plCoordinateInterface.h" #include "hsTimer.h" #include "plgDispatch.h" ///////////////////////////////////////////////////////////////////////////////////////// // // PROTOTYPES // ///////////////////////////////////////////////////////////////////////////////////////// float QuatAngleDiff(const hsQuat &a, const hsQuat &b); void MakeMatrixUpright(hsMatrix44 &mat); ///////////////////////////////////////////////////////////////////////////////////////// // // DEFINES // ///////////////////////////////////////////////////////////////////////////////////////// #define kSeekTimeout 5.0f #define kRotSpeed 1.0f // normal rotation speed is 1.0 radians per second #define kFloatSpeed 3.0f #define kMaxRadiansPerSecond 1.5 #define kDefaultShuffleRange 0.5f #define kDefaultMaxSidleRange 4.0f #define kDefaultMaxSidleAngle 0.2f hsBool plAvTaskSeek::fLogProcess = false; ///////////////////////////////////////////////////////////////////////////////////////// // // IMPLEMENTATION // ///////////////////////////////////////////////////////////////////////////////////////// void plAvTaskSeek::IInitDefaults() { fSeekObject = nil; fMovingTarget = false; fAlign = kAlignHandle; fAnimName = nil; fPosGoalHit = false; fRotGoalHit = false; fStillPositioning = true; fStillRotating = true; fShuffleRange = kDefaultShuffleRange; fMaxSidleRange = kDefaultMaxSidleRange; fMaxSidleAngle = kDefaultMaxSidleAngle; fFlags = kSeekFlagForce3rdPersonOnStart; fState = kSeekRunNormal; fNotifyFinishedKey = nil; } // plAvTaskSeek ------------ // ------------- plAvTaskSeek::plAvTaskSeek() {} plAvTaskSeek::plAvTaskSeek(plAvSeekMsg *msg) { IInitDefaults(); fAlign = msg->fAlignType; fAnimName = msg->fAnimName; plKey &target = msg->fSeekPoint; if (target) SetTarget(target); else SetTarget(msg->fTargetPos, msg->fTargetLookAt); if (msg->UnForce3rdPersonOnFinish()) fFlags |= kSeekFlagUnForce3rdPersonOnFinish; else fFlags &= ~kSeekFlagUnForce3rdPersonOnFinish; if (msg->Force3rdPersonOnStart()) fFlags |= kSeekFlagForce3rdPersonOnStart; else fFlags &= ~kSeekFlagForce3rdPersonOnStart; if (msg->NoWarpOnTimeout()) fFlags |= kSeekFlagNoWarpOnTimeout; else fFlags &= ~kSeekFlagNoWarpOnTimeout; if (msg->RotationOnly()) { fFlags |= kSeekFlagRotationOnly; fStillPositioning = false; fPosGoalHit = true; } else fFlags &= ~kSeekFlagRotationOnly; fNotifyFinishedKey = msg->fFinishKey; } // plAvTaskSeek ------------------------ // ------------- plAvTaskSeek::plAvTaskSeek(plKey target) { IInitDefaults(); SetTarget(target); } // plAvTaskSeek ------------------------------------------------------------------------------------------- // ------------- plAvTaskSeek::plAvTaskSeek(plKey target, plAvAlignment align, const char *animName, bool moving) { IInitDefaults(); fMovingTarget = moving; fAlign = align; fAnimName = animName; SetTarget(target); } void plAvTaskSeek::SetTarget(plKey target) { hsAssert(target, "Bad key to seek task"); if(target) { fSeekObject = plSceneObject::ConvertNoRef(target->ObjectIsLoaded()); } else { fSeekObject = nil; } } void plAvTaskSeek::SetTarget(hsPoint3 &pos, hsPoint3 &lookAt) { fSeekPos = pos; hsVector3 up(0.f, 0.f, 1.f); hsScalar angle = hsATan2(lookAt.fY - pos.fY, lookAt.fX - pos.fX) + hsScalarPI / 2; fSeekRot.SetAngleAxis(angle, up); } // Start ----------------------------------------------------------------------------------------- // ------ hsBool plAvTaskSeek::Start(plArmatureMod *avatar, plArmatureBrain *brain, double time, hsScalar elapsed) { plAvBrainHuman *huBrain = plAvBrainHuman::ConvertNoRef(brain); hsAssert(huBrain, "Seek task only works on human brains"); plAvatarMgr::GetInstance()->GetLog()->AddLine("Starting SMART SEEK"); //controller needs to know we are seeking. prevents controller from interacting with exclusion regions if (avatar->GetController() ) avatar->GetController()->SetSeek(true); fStartTime = time; if(huBrain) { avatar->SuspendInput(); // stop accepting input from the user, but queue any messages // ...and save our current input state. ILimitPlayersInput(avatar); if (plAvOneShotTask::fForce3rdPerson && avatar->IsLocalAvatar() && (fFlags & plAvSeekMsg::kSeekFlagForce3rdPersonOnStart)) { // create message plCameraMsg* pMsg = TRACKED_NEW plCameraMsg; pMsg->SetBCastFlag(plMessage::kBCastByExactType); pMsg->SetBCastFlag(plMessage::kNetPropagate, false); pMsg->SetCmd(plCameraMsg::kResponderSetThirdPerson); plgDispatch::MsgSend( pMsg ); // whoosh... off it goes } huBrain->IdleOnly(); // Makes sure to kill jumps too. Just calling ClearInputFlags isn't enough IUpdateObjective(avatar); return true; } else { return false; } } // Process ------------------------------------------------------------------------------------------- // -------- hsBool plAvTaskSeek::Process(plArmatureMod *avatar, plArmatureBrain *brain, double time, hsScalar elapsed) { if (fState == kSeekAbort) return false; plAvBrainHuman *uBrain = plAvBrainHuman::ConvertNoRef(brain); if (uBrain) { if (fMovingTarget) { IUpdateObjective(avatar); } IAnalyze(avatar); hsBool result = IMoveTowardsGoal(avatar, uBrain, time, elapsed); if (fLogProcess) DumpToAvatarLog(avatar); return result; } return false; } // Finish --------------------------------------------------------------------------------------- // ------- void plAvTaskSeek::Finish(plArmatureMod *avatar, plArmatureBrain *brain, double time, hsScalar elapsed) { plAvBrainHuman *huBrain = plAvBrainHuman::ConvertNoRef(brain); if(huBrain) { // this will process any queued input messages so if the user pressed or released a key while we were busy, we'll note it now. avatar->ResumeInput(); IUndoLimitPlayersInput(avatar); if (plAvOneShotTask::fForce3rdPerson && avatar->IsLocalAvatar() && (fFlags & plAvSeekMsg::kSeekFlagUnForce3rdPersonOnFinish)) { // create message plCameraMsg* pMsg = TRACKED_NEW plCameraMsg; pMsg->SetBCastFlag(plMessage::kBCastByExactType); pMsg->SetBCastFlag(plMessage::kNetPropagate, false); pMsg->SetCmd(plCameraMsg::kResponderUndoThirdPerson); plgDispatch::MsgSend( pMsg ); // whoosh... off it goes } avatar->SynchIfLocal(hsTimer::GetSysSeconds(), false); } if (fNotifyFinishedKey) { plAvTaskSeekDoneMsg *msg = TRACKED_NEW plAvTaskSeekDoneMsg(avatar->GetKey(), fNotifyFinishedKey); msg->fAborted = (fState == kSeekAbort); msg->Send(); } plAvatarMgr::GetInstance()->GetLog()->AddLine("Finished SMART SEEK"); //inform controller we are done seeking if (avatar->GetController()) avatar->GetController()->SetSeek(false); } void plAvTaskSeek::LeaveAge(plArmatureMod *avatar) { fSeekObject = nil; fState = kSeekAbort; } // IAnalyze ---------------------------------------- // --------- hsBool plAvTaskSeek::IAnalyze(plArmatureMod *avatar) { avatar->GetPositionAndRotationSim(&fPosition, &fRotation); fGoalVec.Set(&(hsScalarTriple)(fSeekPos - fPosition)); hsVector3 normalizedGoalVec(fGoalVec); normalizedGoalVec.Normalize(); fDistance = hsSquareRoot(fGoalVec.fX * fGoalVec.fX + fGoalVec.fY * fGoalVec.fY); if(fDistance < 3.0f) { // we're in "near target" mode fMinFwdAngle = .5f; // walk forward if target's in 90' cone straight ahead fMaxBackAngle = -.2f; // walk backward if target's in a 144' cone behind } else { // we're in "far target" mode fMinFwdAngle = .2f; // walk forward if target's in a 144' cone ahead fMaxBackAngle = -2.0; // disable backing up if goal is far out (-1 is the minimum usable value here) } hsQuat invRot = fRotation.Conjugate(); hsPoint3 globFwd = invRot.Rotate(&kAvatarForward); hsPoint3 globRight = invRot.Rotate(&kAvatarRight); hsPoint3 locGoalVec = fRotation.Rotate(&fGoalVec); fDistForward = -(locGoalVec.fY); fDistRight = -(locGoalVec.fX); fAngForward = globFwd.InnerProduct(normalizedGoalVec); fAngRight = globRight.InnerProduct(normalizedGoalVec); return true; } // IMoveTowardsGoal -------------------------------------------------------------- // ----------------- hsBool plAvTaskSeek::IMoveTowardsGoal(plArmatureMod *avatar, plAvBrainHuman *brain, double time, hsScalar elapsed) { bool stillRunning = true; avatar->ClearInputFlags(false, false); double duration = time - fStartTime; if(duration > kSeekTimeout) { if (fFlags & kSeekFlagNoWarpOnTimeout) { fState = kSeekAbort; return false; } fSeekRot.Normalize(); avatar->SetPositionAndRotationSim(&fSeekPos, &fSeekRot); IAnalyze(avatar); // Recalcs fPosition, fDistance, etc. hsStatusMessage("Timing out on smart seek - jumping to target."); stillRunning = false; // We just set the pos/rot, so we know these are hit. fPosGoalHit = true; fRotGoalHit = true; } if (!(fDistance > fShuffleRange)) fPosGoalHit = true; if (!fPosGoalHit) { bool right = fAngRight > 0.0f; bool inSidleRange = fDistance < fMaxSidleRange; bool sidling = fabs(fDistRight) > fabs(fDistForward) && inSidleRange; if(sidling) { if(right) avatar->SetStrafeRightKeyDown(); else avatar->SetStrafeLeftKeyDown(); } else { if(fAngForward < fMaxBackAngle) avatar->SetBackwardKeyDown(); else { if(fAngForward > fMinFwdAngle) avatar->SetForwardKeyDown(); if(right) avatar->SetTurnRightKeyDown(); else avatar->SetTurnLeftKeyDown(); } } } else { if (!(QuatAngleDiff(fRotation, fSeekRot) > .1)) fRotGoalHit = true; if (!fRotGoalHit) { hsQuat invRot = fSeekRot.Conjugate(); hsPoint3 globFwd = invRot.Rotate(&kAvatarForward); globFwd = fRotation.Rotate(&globFwd); if (globFwd.fX < 0) avatar->SetTurnRightKeyDown(); else avatar->SetTurnLeftKeyDown(); } } if (fPosGoalHit && fRotGoalHit) stillRunning = ITryFinish(avatar, brain, time, elapsed); return stillRunning; } // ITRYFINISH bool plAvTaskSeek::ITryFinish(plArmatureMod *avatar, plAvBrainHuman *brain, double time, hsScalar elapsed) { hsBool animsDone = brain->IsMovementZeroBlend(); hsPoint3 newPosition = fPosition; hsQuat newRotation = fRotation; if (!(fFlags & kSeekFlagRotationOnly) && (fStillPositioning || !animsDone)) fStillPositioning = IFinishPosition(newPosition, avatar, brain, time, elapsed); if (fStillRotating || !animsDone) fStillRotating = IFinishRotation(newRotation, avatar, brain, time, elapsed); newRotation.Normalize(); if (hsCheckBits(fFlags, kSeekFlagRotationOnly)) avatar->SetPositionAndRotationSim(nil, &newRotation); else avatar->SetPositionAndRotationSim(&newPosition, &newRotation); return fStillPositioning || fStillRotating || !animsDone; } hsBool plAvTaskSeek::IFinishPosition(hsPoint3 &newPosition, plArmatureMod *avatar, plAvBrainHuman *brain, double time, hsScalar elapsed) { // While warping, we might be hovering just above the ground. Don't want that to // trigger any falling behavior. if(brain&&brain->fCallbackAction) { brain->fCallbackAction->ResetAirTime(); } // how far will we translate this frame? float thisDist = kFloatSpeed * elapsed; // what percentage of the remaining distance will we cover? float thisPct = (fDistance ? thisDist / fDistance : 1.f); if(thisPct > 0.9f) { // we're pretty much done; just hop the rest of the way newPosition = fSeekPos; return false; // we're done } else { // move incrementally toward the target position hsVector3 thisMove = fGoalVec * thisPct; newPosition = fPosition + thisMove; return true; // we're still processing } return true; } // IFinishRotation -------------------------------------- // ---------------- hsBool plAvTaskSeek::IFinishRotation(hsQuat &newRotation, plArmatureMod *avatar, plAvBrainHuman *brain, double time, hsScalar elapsed) { // we're pretty much done; just hop the rest of the way newRotation = fSeekRot; return false; } // IUpdateObjective ---------------------------------------- // ----------------- hsBool plAvTaskSeek::IUpdateObjective(plArmatureMod *avatar) { // This is an entirely valid case. It just means our goal is fixed. if (fSeekObject == nil) return true; // goal here is to express the target matrix in the avatar's PHYSICAL space hsMatrix44 targL2W = fSeekObject->GetLocalToWorld(); const plCoordinateInterface* subworldCI = nil; if (avatar->GetController()) subworldCI = avatar->GetController()->GetSubworldCI(); if (subworldCI) targL2W = subworldCI->GetWorldToLocal() * targL2W; MakeMatrixUpright(targL2W); switch(fAlign) { // match our handle to the target matrix at the end of the given animation // This case isn't currently used but will be important someday. The idea // is that you have a target point and an animation, and you want to seek // the avatar to a point where he can start playing the animation and wind // up, after the animation completes, at the target location. // Hence "AlignHandleAnimEnd" = "align the avatar so the animation will // end on the target." case kAlignHandleAnimEnd: { hsMatrix44 adjustment; plAGAnim *anim = avatar->FindCustomAnim(fAnimName); // don't need to do this every frame; the animation doesn't change. // *** cache the adjustment; GetStartToEndTransform(anim, nil, &adjustment, "Handle"); // actually getting end-to-start // ... but we do still need to multiply by the (potentially changed) target targL2W = targL2W * adjustment; } break; case kAlignHandle: // targetMat is already correct default: break; }; GetPositionAndRotation(targL2W, &fSeekPos, &fSeekRot); return true; } // DumpDebug ----------------------------------------------------------------------------------------------------- // ---------- void plAvTaskSeek::DumpDebug(const char *name, int &x, int&y, int lineHeight, char *strBuf, plDebugText &debugTxt) { sprintf(strBuf, "duration: %.2f pos: (%.3f, %.3f, %.3f) goalPos: (%.3f, %.3f, %.3f) ", hsTimer::GetSysSeconds() - fStartTime, fPosition.fX, fPosition.fY, fPosition.fZ, fSeekPos.fX, fSeekPos.fY, fSeekPos.fZ); debugTxt.DrawString(x, y, strBuf); y += lineHeight; sprintf(strBuf, "positioning: %d rotating %d goalVec: (%.3f, %.3f, %.3f) dist: %.3f angFwd: %.3f angRt: %.3f", fStillPositioning, fStillRotating, fGoalVec.fX, fGoalVec.fY, fGoalVec.fZ, fDistance, fAngForward, fAngRight); debugTxt.DrawString(x, y, strBuf); y += lineHeight; sprintf(strBuf, " distFwd: %.3f distRt: %.3f shufRange: %.3f sidAngle: %.3f sidRange: %.3f, fMinWalk: %.3f", fDistForward, fDistRight, fShuffleRange, fMaxSidleAngle, fMaxSidleRange, fMinFwdAngle); debugTxt.DrawString(x, y, strBuf); y += lineHeight; } void plAvTaskSeek::DumpToAvatarLog(plArmatureMod *avatar) { plStatusLog *log = plAvatarMgr::GetInstance()->GetLog(); char strBuf[256]; avatar->GetMoveKeyString(strBuf); log->AddLine(strBuf); sprintf(strBuf, " duration: %.2f pos: (%.3f, %.3f, %.3f) goalPos: (%.3f, %.3f, %.3f) ", hsTimer::GetSysSeconds() - fStartTime, fPosition.fX, fPosition.fY, fPosition.fZ, fSeekPos.fX, fSeekPos.fY, fSeekPos.fZ); log->AddLine(strBuf); sprintf(strBuf, " positioning: %d rotating %d goalVec: (%.3f, %.3f, %.3f) dist: %.3f angFwd: %.3f angRt: %.3f", fStillPositioning, fStillRotating, fGoalVec.fX, fGoalVec.fY, fGoalVec.fZ, fDistance, fAngForward, fAngRight); log->AddLine(strBuf); sprintf(strBuf, " distFwd: %.3f distRt: %.3f shufRange: %.3f sidAngle: %.3f sidRange: %.3f, fMinWalk: %.3f", fDistForward, fDistRight, fShuffleRange, fMaxSidleAngle, fMaxSidleRange, fMinFwdAngle); log->AddLine(strBuf); } ///////////////////////////////////////////////////////////////////////////////////////// // // LOCALS // ///////////////////////////////////////////////////////////////////////////////////////// // QuatAngleDiff ------------------------------------ // -------------- float QuatAngleDiff(const hsQuat &a, const hsQuat &b) { hsScalar theta; /* angle between A and B */ hsScalar cos_t; /* sine, cosine of theta */ /* cosine theta = dot product of A and B */ cos_t = a.Dot(b); /* if B is on opposite hemisphere from A, use -B instead */ if (cos_t < 0.0) { cos_t = -cos_t; } // Calling acos on 1.0 is returning an undefined value. Need to check for it. hsScalar epsilon = 0.00001; if (hsABS(cos_t - 1.f) < epsilon) return 0; theta = hsACosine(cos_t); return theta; } // MakeMatrixUpright ------------------------------------------- // ------------------ // ensure that the z axis of the given matrix points at the sky. // does not orthonormalize // man, I could have sworn I did this somewhere else... void MakeMatrixUpright(hsMatrix44 &mat) { mat.fMap[0][2] = 0.0f; // eliminate any z in the x axis mat.fMap[1][2] = 0.0f; // eliminate any z in the y axis mat.fMap[2][0] = 0.0f; mat.fMap[2][1] = 0.0f; mat.fMap[2][2] = 1.0f; // z axis = pure sky }