Jump to content

Lock-On Target Camera With ScreenSpace Oriented Movement


Steve_Yorkshire

Recommended Posts

This is a continuation of the Elden Ring/GTA/Sleeping Dogs (the only one I have actually played) screenSpace movement resource that I added here.

The camera locks on a target and then follows it, the code presented here is my initial test case and moves slower than the player movement so you can see that the camera orientates correctly, this makes it look janky as hell.

Warning: this code was written at some god-forsaken time when my brain had died and is fugly as hell, feel free to pretty it up, also there is no X (vertical) axis.

Code: Player.cpp and Player.h

Player.h

Somewhere under 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
     //...

And then way down under public ...

//...
   ShapeBase* getControlObject();

   //yorks
   ShapeBase* getLockOn();// { return (mLockOn); }
   void setLockOn(ShapeBase *obj);
   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;
  //...
  
void Player::updateMove(const Move* move)
{
  //...
  
      if (doStandardMove)
      {
         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;

         //yorks start - this is my older camera screen space code; see link in forum post above!
         //if (((move->freeLook && isMounted() && getMountNode() == 0) || (con && !con->isFirstPerson())))
         if (thirdP)//
         {
            if (!mLockOn.isNull())//yorks - this is the new stuff!!!!!! we check to make sure it's still a good target
            {
               //think we might need a check to see if mLockOn is a real thing? Safety first!
               if (!mLockOn->isProperlyAdded())
                  setLockOn(NULL);
            }
            else
            {
               //yorks moved down here
               mHead.x = mClampF(mHead.x + (p * 0.5f), mDataBlock->minLookAngle, mDataBlock->maxLookAngle);//yorks added 0.5f to slow p down a bit
              //...
              
//in the middle of the previous screenSpace move code
              //...
                  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())//yorks this is the new lockon code <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< beware fugly sleep deprivation
            {
               //face to look at the object's center
               Point3F centerLock = mLockOn->getBoxCenter();// getWorldBox().getCenter();

               //get the vector from us to the target and break it down to x (up/down) and z horizontal rotation
               Point3F ourCenter = getBoxCenter();// getWorldBox().getCenter();
               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;
               F32 testVal = mHead.z;// 0.0f;
               totalRad = mWrapF(totalRad, 0.0f, M_2PI_F);//yorks use the new method
               //Con::printf("viewRad %f; mHead.z %f; mRot.z %f, totalRad %f", viewRad, mHead.z, mRot.z, totalRad);

               //works but god it's fugly
               if (totalRad >= viewRad)
               {
                  if ((viewRad + 0.02f) >= (totalRad))
                     testVal = mHead.z;// 0.0f;
                  else
                  {
                     if ((mHead.z - 0.02f) <= 0.0f)
                     {
                        if ((viewRad + M_PI_F) > totalRad)
                           testVal = mHead.z + 0.02f;
                        else
                           testVal = mHead.z - 0.02f;
                        testVal = mWrapF(testVal, 0.0f, M_2PI_F);//yorks use the new method
                        //Con::printf("viewRad LESS totalRad; += 0.02f; mHead.z = %f; viewRad = %f; totalRad %f;", mHead.z, viewRad, totalRad);
                     }
                     else
                     {
                        if((viewRad + M_PI_F) < totalRad)
                           testVal = mHead.z + 0.02f;
                        else
                           testVal = mHead.z - 0.02f;
                        testVal = mWrapF(testVal, 0.0f, M_2PI_F);//yorks use the new method
                        //Con::printf("viewRad LESS totalRad; -= 0.02f; mHead.z = %f; viewRad = %f; totalRad %f;", mHead.z, viewRad, totalRad);
                     }
                  }
               }
               else
               {
                  if ((viewRad - 0.02f) <= (totalRad))
                     testVal = mHead.z;// 0.0f;
                  else
                  {
                     if ((mHead.z + 0.02f) >= M_2PI_F)
                     {
                        if ((viewRad + M_PI_F) > totalRad)
                           testVal = mHead.z - 0.02f;
                        else
                           testVal = mHead.z + 0.02f;
                        testVal = mWrapF(testVal, 0.0f, M_2PI_F);//yorks use the new method
                        //Con::printf("viewRad MORE totalRad; -= 0.02f; mHead.z = %f; viewRad = %f; totalRad %f;", mHead.z, viewRad, totalRad);
                     }
                     else
                     {
                        if ((viewRad - M_PI_F) > totalRad)
                           testVal = mHead.z - 0.02f;
                        else
                           testVal = mHead.z + 0.02f;
                        testVal = mWrapF(testVal, 0.0f, M_2PI_F);//yorks use the new method
                        //Con::printf("viewRad MORE totalRad; += 0.02f; mHead.z = %f; viewRad = %f; totalRad %f;", mHead.z, viewRad, totalRad);
                     }
                  }
               }
               mHead.z = testVal;
            }
         }
      }

      if (thirdP)//yorks note; you can remove this and put it all under the above if(thirdP) from the other code snippet if you want
      {
         //get facing movement direction as yawDiff
         F32 dirSpeed = 0.0f;
         //F32 secSpeed = 0.0f;
  
         if (yawDiff < 0.785f)//0-45deg
         {
           //...
           
void Player::writePacketData(GameConnection *connection, BitStream *stream)
{
   Parent::writePacketData(connection, stream);
  //..
   if (mControlObject) {
      S32 gIndex = connection->getGhostIndex(mControlObject);
      if (stream->writeFlag(gIndex != -1)) {
         stream->writeInt(gIndex,NetConnection::GhostIdBitSize);
         mControlObject->writePacketData(connection, stream);
      }
   }
   else
      stream->writeFlag(false);

   //yorks
   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
}

oid Player::readPacketData(GameConnection *connection, BitStream *stream)
{
   Parent::readPacketData(connection, stream);
  //...
  
        obj->readPacketData(connection, stream);
   }
   else
      setControlObject(0);

   //yorks
   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);
}
        
//...
        
//right at the bottom of the file and all of this
        
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
      mLockOn = obj;
}

ShapeBase* Player::getLockOn()
{
   return mLockOn;
}

void Player::clearLockOn()
{
   mLockOn = NULL;
}

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 NULL if no lockOn.")
{
   return object->getLockOn();
}

DefineEngineMethod(Player, clearLockOnTarget, void, (), ,
   "@brief Clear player's lock on object.")
{
   object->clearLockOn();
}

So there you have it, lock-on camera with screenSpace movement (and a delay which was part of debugging). Remember you need the code for screenSpace input movement first, which can be found here.

Link to comment
Share on other sites

Added the vertical movement for the lock on camera, again the code is fugly but working. Had more trouble than expected because I forgot that the min/maxFreeLook values are negative when looking down and it really wanted the range clamping between -Pi*0.5 and +Pi*0.5 ... and there I was trying to ram Pi*2 in for most of the day 🙄.

The rotation is from the player and target worldbox centers and does not take into account the camera offset, so the player will not actual have true aim to the target, however the target should appear locked just above the player's head at all but extreme heights.

//find this line in my previous code above when the horizontal rotation is finally set
               mHead.z = testVal;

               //now do up and down <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< yorks start here with the vertical rotation code
               // minLookAngle -1.4; maxLookAngle +0.9, so can be negative ...
               //negative is look up, so move camera down, positive is look down so move camera up
               mHead.x = mWrapF(mHead.x, -M_HALFPI_F, M_HALFPI_F);//lock it between half -Pi (-1.57) and half +Pi (+1.57)
               F32 vertZ;// = ourCenter.z - centerLock.z;
               F32 headV = mHead.x;

               F32 lenTo = lookAtVec.len();// len() or lenSquared() ? toBe() ? or !toBe() ?
               vertZ = mAtan2(mFabs(lookAtVec.z), mFabs(lenTo));

               if (ourCenter.z < centerLock.z)
               {
                  vertZ *= -1;
                  //Con::printf("note we are lower inverting; vertZ pitch %f, mHead.x %f", vertZ, mHead.x);
               }
               //else
                  //Con::printf("note we are higher; vertZ pitch %f, mHead.x %f", vertZ, mHead.x);

               vertZ = mWrapF(vertZ, -M_HALFPI_F, M_HALFPI_F);//lock it between half -Pi (-1.57) and half +Pi (+1.57)

               if (vertZ < (headV - 0.02f) || vertZ > (headV + 0.02f))
               {
                     if (headV < vertZ)//want to look up, minLookAngle stock -1.4
                     {
                        headV = mHead.x + 0.02f;
                        //Con::printf("vertZ LESS %f; mHead.x %f", vertZ, mHead.x);
                     }
                     else//want to look down, maxLookAngle stock +0.9
                     {
                        headV = mHead.x - 0.02f;
                        //Con::printf("vertZ MORE %f; mHead.x %f", vertZ, mHead.x);
                     }
               }
               //else
                  //Con::printf("no move vertZ %f; headV %f", vertZ, headV);

               mHead.x = headV;
            }

Anyone who fancies cleaning this up to not look like poop on a stick, can be my guest 😇

Link to comment
Share on other sites

Fixed the rather fugly code for horizontal rotation and compressed it down from >70 to ~10 lines, oh the horrors of brain fatigue 😳

Paste over the whole of if (!mLockOn.isNull()) part of the function with this new version. This also includes the the vertical rotation now.

The "look at" position is now from the player's worldBoxCenter to the target's floor, which helps the camera look slightly down when level.

            if (!mLockOn.isNull())
            {
               //face to look at the object's center - no look at it's base to keep the camera higher
               Point3F centerLock = mLockOn->getPosition();// getBoxCenter();//

               //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.005f && mFabs(testVal) > -0.005f)//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
                  if (testVal > M_PI_F)
                     testVal -= M_2PI_F;
                  else if (testVal < -M_PI_F)
                     testVal += M_2PI_F;

                  //testVal = mWrapF(testVal, 0.0f, M_2PI_F);// <<<<<<<<<< seems to work with or without, shrug
               }

               mHead.z += testVal;
               //----------------------

               //now do up and down
               // minLookAngle -1.4; maxLookAngle +0.9, so can be negative ...
               //negative is look up, so move camera down, positive is look down so move camera up
               mHead.x = mWrapF(mHead.x, -M_HALFPI_F, M_HALFPI_F);// -1.5, 1.5f); - lock it between half -Pi (-1.57) and half +Pi (+1.57)
               F32 vertZ;// = ourCenter.z - centerLock.z;
               F32 headV = mHead.x;

               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)

               if (vertZ < (headV - 0.005f) || vertZ > (headV + 0.005f))//0.017f = 1 degree
                  headV = vertZ;

               mHead.x = headV;
            }

It is a little bit jittery, so could probably use some newton physics stuff from newton mode camera, but apart from that it works fine.

Link to comment
Share on other sites

Looks complicated, I would suggest to just pull request resources into the engine and add it to the Torque3D manual whereever it is now and a switch in the engine or in script to switch between different camera mods like there is already wiht like normal and newtonian camera etc. It would increase development speed if someone wanting to use it could just try it out wihtout having to add it himself and then eventually fix it if its broken.

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...