Steve_Yorkshire Posted March 10, 2023 Share Posted March 10, 2023 Third Person Free Camera With ScreenSpace Movement And Lock On Target, that works in Torque 4 devhead as of today. This is the FINAL (hopefully) version of this resource, I have now fixed my client-server predication issues. Needless to say, I had been looking the wrong place, and it was pretty much a 1 line fix. Player.h //in protected //... SimObjectPtr<ShapeBase> mControlObject; ///< Controlling object SimObjectPtr<ShapeBase> mLockOn;//yorks - gamebase/shapebase object id we are locked on to, this keeps the camera pointing at this object /// @name Animation threads & data //... //down in public //... ShapeBase* getControlObject(); //yorks block F32 getLockOnHorizontal(ShapeBase* obj); F32 getLockOnVertical(ShapeBase* obj); void setLockOn(ShapeBase* obj); ShapeBase* getLockOn(); void clearLockOn();//yorks end // void updateWorkingCollisionSet(); //... Player.cpp //... Player::Player() { mTypeMask |= PlayerObjectType | DynamicShapeObjectType; //... dMemset( mSplashEmitter, 0, sizeof( mSplashEmitter ) ); mLockOn = NULL;// 0;//yorks for locking on a shapeBase/gameBase id mUseHeadZCalc = true; //.. And into the player's main player::updateMove where the action happens. void Player::updateMove(const Move* move) { struct Move my_move; //... setImageAltTriggerState( 0, move->trigger[sImageTrigger1] ); } //yorks >>>>>>>>>>>>>>>>>>>>>>>>>>>> F32 camZ = 0.0f; GameConnection* con = getControllingClient(); bool thirdP = false; if (con && !con->isFirstPerson()) thirdP = true;//yorks end <<<<<<<<<<<<<<<<<<<<<<<<<<<< // Update current orientation if (mDamageState == Enabled) { F32 prevZRot = mRot.z; mDelta.headVec = mHead; bool doStandardMove = true; bool absoluteDelta = false; //GameConnection* con = getControllingClient();//yorks moved above now #ifdef TORQUE_EXTENDED_MOVE //... Now find the doStandardMove and replace it. if(doStandardMove) { //yorks new F32 p = move->pitch; F32 y = move->yaw; //yorks >>>>>>>>>>>>>>>>>>>>>>> moved below for first person only /*F32 p = move->pitch * (mPose == SprintPose ? mDataBlock->sprintPitchScale : 1.0f); if (p > M_PI_F) p -= M_2PI_F; mHead.x = mClampF(mHead.x + p,mDataBlock->minLookAngle, mDataBlock->maxLookAngle); F32 y = move->yaw * (mPose == SprintPose ? mDataBlock->sprintYawScale : 1.0f); if (y > M_PI_F) y -= M_2PI_F;*/ /*if (move->freeLook && ((isMounted() && getMountNode() == 0) || (con && !con->isFirstPerson()))) { mHead.z = mClampF(mHead.z + y, -mDataBlock->maxFreelookAngle, mDataBlock->maxFreelookAngle); } else { mRot.z += y; // Rotate the head back to the front, center horizontal // as well if we're controlling another object. mHead.z *= 0.5f; if (mControlObject) mHead.x *= 0.5f; }//yorks end of moved below for first person <<<<<<<<<<<<<<<<<<<<<<<<<<<<<< */ // yorks start >>>>>>>>>>>>>>>>>>>>>>>>>> //if (((move->freeLook && isMounted() && getMountNode() == 0) || (con && !con->isFirstPerson())))//out no freelook now if (thirdP)//if not default to first person stock code below { if (!mLockOn.isNull()) { //think we might need a check to see if mLockOn is a real thing? if (!mLockOn->isProperlyAdded()) setLockOn(NULL); } else { //yorks moved down here mHead.x = mClampF(mHead.x + (p * 0.5f), -1.4f, 0.9f);//direct values now//yorksmDataBlock->minLookAngle, mDataBlock->maxLookAngle);//yorks added 0.5f to slow p down a bit //let us run the camera around in a big old never ending circle F32 camRot = mHead.z + (y * 0.5f);//slow y down a bit, techincally you should do this input script mouse sensitivity, also see p above //constrain the range of camRot within pi*2 /*if (camRot > M_PI_F) camRot -= M_2PI_F; else if (camRot < -M_PI_F) camRot += M_2PI_F;*/ camRot = mWrapF(camRot, 0.0f, M_2PI_F);//yorks use the new method mHead.z = camRot; // mHead.z = mClampF(mHead.z + y, // -mDataBlock->maxFreelookAngle, // mDataBlock->maxFreelookAngle);//yorks end //Con::printf("camRot/mHead.z %f", camRot); } } else { //yorks original code moved down here p *= mPose == SprintPose ? mDataBlock->sprintPitchScale : 1.0f; if (p > M_PI_F) p -= M_2PI_F; mHead.x = mClampF(mHead.x + p, mDataBlock->minLookAngle, mDataBlock->maxLookAngle); y *= mPose == SprintPose ? mDataBlock->sprintYawScale : 1.0f; if (y > M_PI_F) y -= M_2PI_F; mRot.z += y; // Rotate the head back to the front, center horizontal // as well if we're controlling another object. mHead.z *= 0.5f; if (mControlObject) mHead.x *= 0.5f; } //}//yorks end <<<<<<<<<<<<<<<<<<<<<< // constrain the range of mRot.z while (mRot.z < 0.0f) mRot.z += M_2PI_F; while (mRot.z > M_2PI_F) mRot.z -= M_2PI_F; } mDelta.rot = mRot; //... This is good for both first person view, our new third person camera, and AiPlayers. Now replace the whole of; if ((mState == MoveState || (mState == RecoverState && mDataBlock->recoverRunForceScale > 0.0f)) && mDamageState == Enabled && !isAnimationLocked()) //... if ((mState == MoveState || (mState == RecoverState && mDataBlock->recoverRunForceScale > 0.0f)) && mDamageState == Enabled && !isAnimationLocked()) { F32 yawDiff = 0.0f;//yorks start if (thirdP) { GameBase* cam = con->getCameraObject(); if (cam) { //get the camera's transform F32 pos = 1; MatrixF camTrans; cam->getCameraTransform(&pos, &camTrans); //flatten the transform to X&Y (so when the camera //is facing up/down the move remains horizontal) camTrans[9] = 0; camTrans.normalize(); //create a move vector and multiply by the camera transform VectorF temp(move->x, move->y, 0); camTrans.mulV(temp, &moveVec); // get the z rotation of the camera for working with right stick aiming - don't need this here but have it for debugging below camZ = camTrans.toEuler().z; if (camZ < 0.0f) camZ += M_2PI_F; // Constrain the range of camZ within pi*2 camZ = mWrapF(camZ, 0.0f, M_2PI_F);//yorks use the new method if ((!mIsZero(move->y)) || (!mIsZero(move->x))) { // This is make the player turn towards the direction that they are moving Point3F transVec = getTransform().getForwardVector(); //getVelocity(); F32 tran = mAtan2(transVec.x, transVec.y); if (tran < 0.0f) tran += M_2PI_F; mWrapF(tran, 0.0f, M_2PI_F); F32 moveDir = mAtan2(getVelocity().x, getVelocity().y); if (moveDir < 0.0f) moveDir += M_2PI_F; yawDiff = moveDir - tran; //ignore very small rotations if (mFabs(yawDiff) < 0.005f && mFabs(yawDiff) > -0.005f)//yorks now less than a third of 1 degree//0.001f)// yawDiff = 0.0f; if (!mIsZero(yawDiff)) { // now make sure we take the short way around the circle if (yawDiff > M_PI_F) yawDiff -= M_2PI_F; else if (yawDiff < -M_PI_F) yawDiff += M_2PI_F; F32 slower;//slower exists to slow the turn down slower = 0.15f; mRot.z += yawDiff * slower; mHead.z -= yawDiff * slower;//the camera is still attached to the player so we need to compensate for the player's new rotation } } if (!mLockOn.isNull()) { F32 testVal = 0.0f; testVal = getLockOnHorizontal(mLockOn); mHead.z += testVal; //---------------------- F32 headV = 0.0f; headV = getLockOnVertical(mLockOn); mHead.x += headV; //yorks this is what we forgot before - to update the headVec for a second time because we have altered it mDelta.headVec -= Point3F(headV, 0.0f, testVal); for (U32 i = 0; i < 3; ++i)//yorks safety { if (mDelta.headVec[i] > M_PI_F) mDelta.headVec[i] -= M_2PI_F; else if (mDelta.headVec[i] < -M_PI_F) mDelta.headVec[i] += M_2PI_F; } } } F32 dirSpeed = 0.0f; if (yawDiff < 0.785f)//0-45deg { if (mSwimming) dirSpeed = mDataBlock->maxUnderwaterForwardSpeed; else if (mPose == CrouchPose) dirSpeed = mDataBlock->maxCrouchForwardSpeed; else if (mPose == SprintPose) dirSpeed = mDataBlock->maxSprintForwardSpeed; else dirSpeed = mDataBlock->maxForwardSpeed; } else if (yawDiff > 5.497f)//315-360deg { if (mSwimming) dirSpeed = mDataBlock->maxUnderwaterForwardSpeed; else if (mPose == CrouchPose) dirSpeed = mDataBlock->maxCrouchForwardSpeed; else if (mPose == SprintPose) dirSpeed = mDataBlock->maxSprintForwardSpeed; else dirSpeed = mDataBlock->maxForwardSpeed; } else if (yawDiff > 0.784f && yawDiff < 2.356f) { if (mSwimming) dirSpeed = mDataBlock->maxUnderwaterSideSpeed; else if (mPose == CrouchPose) dirSpeed = mDataBlock->maxCrouchSideSpeed; else if (mPose == SprintPose) dirSpeed = mDataBlock->maxSprintSideSpeed; else dirSpeed = mDataBlock->maxSideSpeed; } else if (yawDiff < 5.496f && yawDiff > 3.926f) { if (mSwimming) dirSpeed = mDataBlock->maxUnderwaterSideSpeed; else if (mPose == CrouchPose) dirSpeed = mDataBlock->maxCrouchSideSpeed; else if (mPose == SprintPose) dirSpeed = mDataBlock->maxSprintSideSpeed; else dirSpeed = mDataBlock->maxSideSpeed; } else { if (mSwimming) dirSpeed = mDataBlock->maxUnderwaterBackwardSpeed; else if (mPose == CrouchPose) dirSpeed = mDataBlock->maxCrouchBackwardSpeed; else if (mPose == SprintPose) dirSpeed = mDataBlock->maxSprintBackwardSpeed; else dirSpeed = mDataBlock->maxBackwardSpeed; } moveSpeed = dirSpeed; } else {//yorks original movement code below zRot.getColumn(0, &moveVec); moveVec *= (move->x * (mPose == SprintPose ? mDataBlock->sprintStrafeScale : 1.0f)); VectorF tv; zRot.getColumn(1, &tv); moveVec += tv * move->y; // Clamp water movement if (move->y > 0.0f) { if (mSwimming) moveSpeed = getMax(mDataBlock->maxUnderwaterForwardSpeed * move->y, mDataBlock->maxUnderwaterSideSpeed * mFabs(move->x)); else if (mPose == PronePose) moveSpeed = getMax(mDataBlock->maxProneForwardSpeed * move->y, mDataBlock->maxProneSideSpeed * mFabs(move->x)); else if (mPose == CrouchPose) moveSpeed = getMax(mDataBlock->maxCrouchForwardSpeed * move->y, mDataBlock->maxCrouchSideSpeed * mFabs(move->x)); else if (mPose == SprintPose) moveSpeed = getMax(mDataBlock->maxSprintForwardSpeed * move->y, mDataBlock->maxSprintSideSpeed * mFabs(move->x)); else // StandPose moveSpeed = getMax(mDataBlock->maxForwardSpeed * move->y, mDataBlock->maxSideSpeed * mFabs(move->x)); } else { if (mSwimming) moveSpeed = getMax(mDataBlock->maxUnderwaterBackwardSpeed * mFabs(move->y), mDataBlock->maxUnderwaterSideSpeed * mFabs(move->x)); else if (mPose == PronePose) moveSpeed = getMax(mDataBlock->maxProneBackwardSpeed * mFabs(move->y), mDataBlock->maxProneSideSpeed * mFabs(move->x)); else if (mPose == CrouchPose) moveSpeed = getMax(mDataBlock->maxCrouchBackwardSpeed * mFabs(move->y), mDataBlock->maxCrouchSideSpeed * mFabs(move->x)); else if (mPose == SprintPose) moveSpeed = getMax(mDataBlock->maxSprintBackwardSpeed * mFabs(move->y), mDataBlock->maxSprintSideSpeed * mFabs(move->x)); else // StandPose moveSpeed = getMax(mDataBlock->maxBackwardSpeed * mFabs(move->y), mDataBlock->maxSideSpeed * mFabs(move->x)); } }//yorks added to segregate original code from freecam // Cancel any script driven animations if we are going to move. if (moveVec.x + moveVec.y + moveVec.z != 0.0f && (mActionAnimation.action >= PlayerData::NumTableActionAnims || mActionAnimation.action == PlayerData::LandAnim)) mActionAnimation.action = PlayerData::NullAnimation; }//this is all stock code down here else { moveVec.set(0.0f, 0.0f, 0.0f); moveSpeed = 0.0f; } //... Write and Read the packets to send our lockOn target's ghost over net. void Player::writePacketData(GameConnection *connection, BitStream *stream) { Parent::writePacketData(connection, stream); //... //yorks, at the bottom if (!mLockOn.isNull()) { S32 ghost = connection->getGhostIndex(mLockOn); if (stream->writeFlag(ghost != -1)) stream->writeRangedU32(ghost, 0, NetConnection::MaxGhostCount); } else stream->writeFlag(false);//yorks end } void Player::readPacketData(GameConnection *connection, BitStream *stream) { Parent::readPacketData(connection, stream); //... //yorks at the bottom if (stream->readFlag()) { S32 ghost = stream->readRangedU32(0, NetConnection::MaxGhostCount); ShapeBase* lock = static_cast<ShapeBase*>(connection->resolveGhost(ghost));//static_cast as we have a check for shapebase object when we assign it mLockOn = lock;// setLockOn(lock); } else mLockOn = NULL;// setLockOn(NULL); } At the bottom of the file add the maths and functions for the lockOn target. /yorks start to end of file F32 Player::getLockOnHorizontal(ShapeBase* obj) { //face to look at the object's center - no look at it's base to keep the camera higher Point3F centerLock = mLockOn->getPosition(); //get the vector from us to the target and break it down to x (up/down) and z horizontal rotation Point3F ourCenter = getBoxCenter();// use our center to their base for additional vert rotation VectorF lookAtVec = centerLock - ourCenter; lookAtVec.normalize();//just in case //get the angle in radians and then invert it as we want to be at the far side of our player --- apparently doesn't need inverting F32 viewRad = 0.0f; viewRad = mAtan2(lookAtVec.x, lookAtVec.y); viewRad = mWrapF(viewRad, 0.0f, M_2PI_F);//yorks use the new method F32 totalRad = mHead.z + mRot.z; totalRad = mWrapF(totalRad, 0.0f, M_2PI_F);//yorks use the new method F32 testVal = 0.0f; testVal = viewRad - totalRad; //ignore very small rotations if (mFabs(testVal) < 0.001f && mFabs(testVal) > -0.001f)//yorks //0.017f = 1 degree; 0.005f = 0.2864789 of 1 degree//0.001f testVal = 0.0f; if (!mIsZero(testVal)) { // now make sure we take the short way around the circle while (testVal > M_PI_F) testVal -= M_2PI_F; while (testVal < -M_PI_F) testVal += M_2PI_F; //testVal = mWrapF(testVal, 0.0f, M_2PI_F);//this is not the fast turn above!!!! Must not have this for interlope if (testVal > 0.05f)//stagger the turn if large testVal = 0.05f; else if (testVal < -0.05f) testVal = -0.05f; } return testVal; } F32 Player::getLockOnVertical(ShapeBase* obj) { //face to look at the object's center - no look at it's base to keep the camera higher Point3F centerLock = mLockOn->getPosition(); //get the vector from us to the target and break it down to x (up/down) and z horizontal rotation Point3F ourCenter = getBoxCenter(); // use our center to their base for additional vert rotation VectorF lookAtVec = centerLock - ourCenter; lookAtVec.normalize();//just in case ;) //mHead.x = mWrapF(mHead.x, -M_HALFPI_F, M_HALFPI_F);lock it between half -Pi (-1.57) and half +Pi (+1.57)think it works better without F32 vertZ; F32 headV = 0.0f; F32 lenTo = lookAtVec.len();// len() or lenSquared() ? vertZ = mAtan2(mFabs(lookAtVec.z), mFabs(lenTo)); if (ourCenter.z < centerLock.z) vertZ *= -1; vertZ = mWrapF(vertZ, -M_HALFPI_F, M_HALFPI_F);//yorks use the new method - lock it between half -Pi (-1.57) and half +Pi (+1.57) F32 totalRad = mHead.x + mRot.x;//mRot.x should be zero totalRad = mWrapF(totalRad, -M_HALFPI_F, M_HALFPI_F); headV = vertZ - totalRad; //ignore very small rotations if (mFabs(headV) < 0.001f && mFabs(headV) > -0.001f)//yorks //0.017f = 1 degree; 0.005f = 0.2864789 of 1 degree//0.001f headV = 0.0f; if (!mIsZero(headV)) { // now make sure we take the short way around the circle while (headV > M_HALFPI_F) headV -= M_PI_F; while (headV < -M_HALFPI_F) headV += M_PI_F; //headV = mWrapF(headV, -M_HALFPI_F, M_HALFPI_F);//<<<<<<<<<<<this is not the above fast turn!!!! Must not have this for interlope if (headV > 0.05f)//stagger the turn if large headV = 0.05f; else if (headV < -0.05f) headV = -0.05f; } return headV; } void Player::setLockOn(ShapeBase* obj) { if (!obj) { mLockOn = NULL; Con::printf("setLockOn no obj or not shapebase"); } else if (!obj->isProperlyAdded()) { mLockOn = NULL; Con::printf("setLockOn obj not properly added"); } else if (obj->getDamageStateName() != "Enabled") { mLockOn = NULL; Con::printf("setLockOn obj dead"); } else { if (bool(mLockOn))//Az thought this might help { clearProcessAfter(); clearNotify(mLockOn); } mLockOn = obj;//set the lockOn object if (bool(mLockOn)) // Az thought this might help { processAfter(mLockOn); deleteNotify(mLockOn); } } } ShapeBase* Player::getLockOn() { return mLockOn; } void Player::clearLockOn() { mLockOn = NULL; } And finally after that, the script calls. DefineEngineMethod(Player, setLockOnTarget, void, (ShapeBase* obj), , "set lockOn target object") { object->setLockOn(obj); } DefineEngineMethod(Player, getLockOnTarget, ShapeBase*, (), , "@brief Get player's lock on object. return 0 if no lockOn.") { return object->getLockOn(); } DefineEngineMethod(Player, clearLockOnTarget, void, (), , "@brief Clear player's lock on object.") { object->clearLockOn(); } And there you have it, a third person free camera with a movement to screenSpace with a target lock on system. Now you too can be Sleeping Torquedogs, Torque Ring or Grand Theft Kork 6! Quote Link to comment Share on other sites More sharing options...
Steve_Yorkshire Posted April 12, 2023 Author Share Posted April 12, 2023 (edited) Minor Fix: I noticed that when the player runs into something, they turn away from the point of collision. This is because the player's movement is in X and Y velocity, which is world space. When the velocity changes due to an obstruction, then the forward vector will rotate. To get around this we need to swap X and Y velocity for the player's input move->x/y. Player move input is in screenspace, but by simply adding the camera Z transform, which we already have, we can get the relevant world space value. In player.cpp, inside our above code in Player::UpdateMove, find where we are using the x/y velocity. F32 moveDir = mAtan2(getVelocity().x, getVelocity().y); We want to replace that with this: //... //yorks out now, this will change due to collisions and turn the player when we don't want him to //F32 moveDir = mAtan2(getVelocity().x, getVelocity().y); F32 moveDir = mAtan2(move->x, move->y);//player input move is screenSpaced//<<<<<<<<<<<<<<<<<<<<<< new! moveDir += camZ;//add the camera Z axis view to make screenSpace into worldSpace//<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< new! if (moveDir < 0.0f) moveDir += M_2PI_F; //... And that's it, 1 line out and 2 lines in. Don't think about using the mWrapF as it can cause the player object to choose the wrong direction to rotate on movement changes, stick with the += 2PI for both "moveDir" and the forwardVector transform "tran" above it. Now the player will bravely faceplant into walls rather gutlessly turn away! Edited April 12, 2023 by Steve_Yorkshire Quote Link to comment Share on other sites More sharing options...
Steve_Yorkshire Posted July 24 Author Share Posted July 24 (edited) Minor update, I noticed whilst locked on to a moving target causes jitter. As a smallest effort possible hotfix I have the client look 1 tick ahead of the target's velocity and heading for it's renderPosition. Changes to Player::lockOnHorizontal function F32 Player::getLockOnHorizontal(ShapeBase* obj) { //face to look at the object's center - no look at it's base to keep the camera higher if (!obj) return 0.0f; Point3F centerLock = getLockOnPredict(obj);//obj->getPosition();//yorks change //get the vector from us to the target and break it down to x (up/down) and z horizontal rotation Point3F ourCenter = getBoxCenter();// use our center to their base for additional vert rotation //... Changes to Player::lockOnVertical function F32 Player::getLockOnVertical(ShapeBase* obj) { //face to look at the object's center - no look at it's base to keep the camera higher if (!obj) return 0.0f; Point3F centerLock = getLockOnPredict(obj);//obj->getPosition();//yorks change! //get the vector from us to the target and break it down to x (up/down) and z horizontal rotation Point3F ourCenter = getBoxCenter();// use our center to their base for additional horiz rotation //... And the new Player::lockOnPredict function Point3F Player::getLockOnPredict(ShapeBase* obj) { //if the player is moving we need to get the move ahead Point3F pos = obj->getRenderPosition();// getPosition(); Point3F vel = obj->getVelocity(); Point3F finPos = pos;//give it a value for safety if (vel.isZero())// == VectorF::Zero) { //player is static we can return his position return pos; } else { vel *= 0.064f;//32 ticks a second and a tick behind finPos = vel + pos; if (isGhost()) return finPos; else return pos; } } If the target is static it defaults to the original way, though now uses getRenderPosition rather than getPosition to help it synchronize better with what the player is actually seeing on screen. Jitter bug Ai movement before above code (looks a lot worse in 1080p): And after the world's fastest hack since somebody messed around with the Windows kernal Edited July 24 by Steve_Yorkshire Quote Link to comment Share on other sites More sharing options...
Recommended Posts
Join the conversation
You can post now and register later. If you have an account, sign in now to post with your account.