V12 Documentation Repository

About | Create Article | Log In | Create Account

Internal
V12
-Graphics (0)
 -Models (2)
 -Textures (0)
-Level Design (0)
-Programming (4)
 -AI (0)
 -Audio (0)
 -DirectX (0)
 -Networking (0)
 -OpenGL (0)
 -Physics (0)
-Quick Hacks (2)
-Scripting (2)
 -Items (0)

External

Garage Games
V12 Powered
V12 Mod Central
Ultima Group

Dynamic Third-Person Camera (Part 1)

Disclaimer
There are usually many ways to implement a feature into a complex code-base like V12. I tried a few different approaches (some of which are detailed in the Background Research section) and this one seemed to work best. As I continue to work with the V12 Engine and surrounding community, a better way to implement this camera may be revealed - at which time, I will most likely update our code and this tutorial.

Requirements
The camera manipulation code is all handled in the V12 Engine itself. Therefore, you will need to have the ability to compile and build the V12 Engine in order to complete this tutorial. This tutorial is based on version 1.0 of the V12 SDK.

How To Read This Document
The instructions presented here assume you have some basic knowledge of C++ programming. There are code samples associated with each set of instructions to help explain the specific procedure(s). Since many people will be modifying their own version of the V12 Engine, their code may be different than what is presented here - that is why I tried to write the textual instructions as informatively as possible.

The code block examples (backed in gray) show the general section of code where you can make edits to accomplish the particular instruction(s). These are not mandates, but rather guidelines to follow. The blue text represents additions to that code and will correspond to the instruction being given.

Introduction
Our company, 21-6 Productions, is building a game called Myrmidon which will employ a rather sophisticated third-person camera, similar to that found in Asheron's Call. Using this camera, the gamer can simultaneously control the movement of the camera and the player it is pointing at. The camera is centered on the player model and can be rotated both vertically and horizontally in a spherical path (aka "orbit"). Of course, the gamer can also zoom the camera in or out.

Background Research
There is already an "orbit camera" in the V12 engine. It's not hooked up by default, but with a little scripting, I was able to get it working. It seemed to work fine except for one important fact - the existing "orbit camera" is actually a separate physical entity in the 3D environment. The way that the V12 Engine is coded - only one physical entity in the world can be controlled by the keyboard at any time. In other words, two physical entities in the world (the player and the camera, in this case) couldn't be controlled from the same input device at the same time without a great deal of enhancements to the engine.

So, I went down another track. The third-person camera support already built into the V12 Engine (different than the "orbit" camera mentioned above), positions the camera directly behind the "eye" of the player model. The position of the camera behind the player is static and cannot be changed (until now). It's also worthy to note that the implementation of this third-person camera does not require another physical entity in the 3D environment. It is simply the "view" from the player model in question. This allows us to have the keyboard control both the player and camera movements simultaneously.

What's Next
There are many features of the third-person camera that Myrmidon will require. These include features like: floating (lag) camera, mouse-look, mouse-orbit, and auto-orbit (keeping another object in view at all times). I named this tutorial "Part 1" because I plan to implement these other features and write follow-up tutorials as appropriate.


Step 1 - Default to the "third-person" camera

Myrmidon is primarily a third-person game and therefore we want the engine to start up in third-person camera mode. This is an optional step.

  • Open "/engine/game/GameConnection.cc" in the "V12 Engine" project.
  • Change the initialization of the "GameConnection::mFirstPerson" variable to "false".

    IMPLEMENT_CONOBJECT(GameConnection);
    bool GameConnection::mFirstPerson = false;
    S32 GameConnection::mLagThresholdMS = 0;
    S32 GameConnection::smVoiceConnections[MaxClients];


Step 2 - Add variables to store position, orientation, and velocity of the camera

For this step, I have decided to follow the approach already implemented for keeping track of camera state in the engine. We will use the "GameConnection::mFirstPerson" variable as our model. Therefore, we need to add six static variables to the GameConnection class.

  • Open "/engine/game/GameConnection.h" in the "V12 Engine" project.
  • Declare the following six static member variables in the GameConnection class: mCameraPitch, mCameraYaw, mCameraZoom, mCameraPitchSpeed, mCameraYawSpeed, and mCameraZoomSpeed.

    private:
      S32 mDataBlockModifiedKey;
      S32 mMaxDataBlockModifiedKey;

      // Client side first/third person
      static bool mFirstPerson; // Currently first person or not
      static F32 mCameraPitch;
      static F32 mCameraYaw;
      static F32 mCameraZoom;
      static F32 mCameraPitchSpeed;
      static F32 mCameraYawSpeed;
      static F32 mCameraZoomSpeed;

      bool mUpdateCameraFov; // set to notify server of camera FOV change
      F32 mCameraFov; // current camera fov (in degrees)
      F32 mCameraPos; // Current camera pos (0-1)
      F32 mCameraSpeed; // Camera in/out speed

  • Open "/engine/game/GameConnection.cc" in the "V12 Engine" project.
  • Initialize the six static member variables. These particular settings will position the camera directly behind the eye of the player model, far enough back that you can see the entire model. Adjust these values for your game as necessary. Note that the camera is not initially moving, hence the "speed" variables are all set to zero.

    IMPLEMENT_CONOBJECT(GameConnection);
    bool GameConnection::mFirstPerson = true;
    F32 GameConnection::mCameraPitch = -0.2;
    F32 GameConnection::mCameraYaw = M_PI;
    F32 GameConnection::mCameraZoom = 4;
    F32 GameConnection::mCameraPitchSpeed = 0;
    F32 GameConnection::mCameraYawSpeed = 0;
    F32 GameConnection::mCameraZoomSpeed = 0;
    S32 GameConnection::mLagThresholdMS = 0;
    S32 GameConnection::smVoiceConnections[MaxClients];

  • Now, we need to persist these new variables so search for the "writeDemoStartBlock()" method and add some code to write the variables to the supplied stream.

    stream->writeFlag(false);
    stream->write(mFirstPerson);
    stream->write(mCameraPitch);
    stream->write(mCameraYaw);
    stream->write(mCameraZoom);
    stream->write(mCameraPitchSpeed);
    stream->write(mCameraYawSpeed);
    stream->write(mCameraZoomSpeed);
    stream->write(mCameraPos);
    stream->write(mCameraSpeed);

  • Of course, we now need to put code in that reads these variables from a supplied stream so search for the "readDemoStartBlock()" method and add the appropriate code.

    while(stream->readFlag())
    {
      SimDataBlockEvent evt;
      evt.unpack(this, stream);
      evt.process(this);
    }
    stream->read(&mFirstPerson);
    stream->read(&mCameraPitch);
    stream->read(&mCameraYaw);
    stream->read(&mCameraZoom);
    stream->read(&mCameraPitchSpeed);
    stream->read(&mCameraYawSpeed);
    stream->read(&mCameraZoomSpeed);
    stream->read(&mCameraPos);
    stream->read(&mCameraSpeed);


Step 3 - Expose new variables to the console

The six static variables we created in step 2 need to be exposed to the scripts so they can be modified with the keyboard. Again, we are going to follow the implementation of mFirstPerson in our solution.

  • Open "/engine/game/GameConnection.cc" in the "V12 Engine" project.
  • Search for the "GameConnection::consoleInit" method and add calls to "Con::addVariable" for each of the six static camera variables.

    void GameConnection::consoleInit()
    {
      Con::addVariable("firstPerson", TypeBool, &mFirstPerson);
      Con::addVariable("cameraPitch", TypeF32, &mCameraPitch);
      Con::addVariable("cameraYaw", TypeF32, &mCameraYaw);
      Con::addVariable("cameraZoom", TypeF32, &mCameraZoom);
      Con::addVariable("cameraPitchSpeed", TypeF32, &mCameraPitchSpeed);
      Con::addVariable("cameraYawSpeed", TypeF32, &mCameraYawSpeed);
      Con::addVariable("cameraZoomSpeed", TypeF32, &mCameraZoomSpeed);
      Con::addVariable("pref::Net::lagThreshold", TypeS32, &mLagThresholdMS);


Step 4 - Animate the position and orientation of the camera

Every time the engine is about to render the 3D environment, it calls the GameConnection::getControlCameraTransform() method to get the "camera transformation matrix" for the "control object". The "control object" is the object in the 3D environment which the player currently is controlling with their keyboard/mouse - this is usually the character model. A "camera transformation matrix" contains the position and orientation information for the camera, in real world coordinates.

In order to keep the camera movement smooth, we need to consider time as we animate the movement of the camera through space. The "getControlCameraTransform()" method is called by the engine as fast as possible and can therefore be affected by the frame rate on the client machine. This means we have to implement our own time tracking mechanism for smooth animation of the camera movements. To do this, we need to add an instance variable to the GameConnection class that stores the last clock tick in which the "getControlCameraTransform()" method was called.

  • Open "/engine/game/GameConnection.h" in the "V12 Engine" project.
  • Declare the mLastCameraUpdate variable in the GameConnection class:

    private:
      S32 mDataBlockModifiedKey;
      S32 mMaxDataBlockModifiedKey;
      F32 mLastCameraUpdate;

      // Client side first/third person
      static bool mFirstPerson; // Currently first person or not

  • Open "/engine/game/GameConnection.cc" in the "V12 Engine" project.
  • Initialize mLastCameraUpdate in the GameConnection class constuctor.

    GameConnection::GameConnection(bool ghostFrom, bool ghostTo, bool sendEvents)
      : NetConnection(ghostFrom, ghostTo, sendEvents)
    {
      mLastCameraUpdate = -1;
      mControlObject = NULL;
      mLastMoveAck = 0;
Now, lets add in the code which animates the camera position and orientation. Note that we will be replacing the existing code to animate the transition between first and third person camera modes. In a future release, this will be added back in using a different approach.
  • Open "/engine/game/GameConnection.cc" in the "V12 Engine" project.
  • Search for the "GameConnection::getControlCameraTransform()" method and replace this code:

    if (dt) {
      if (mFirstPerson || obj->onlyFirstPerson()) {
        if (mCameraPos > 0)
          if ((mCameraPos -= mCameraSpeed * dt) <= 0)
            mCameraPos = 0;
      }
      else {
        if (mCameraPos < 1)
          if ((mCameraPos += mCameraSpeed * dt) > 1)
            mCameraPos = 1;
      }
    }

    with this code:

    // animate the camera properties based on the amount of time that
    // has passed since the last call to this method
    F32 curTime = (F32)Platform::getRealMilliseconds() / 1000.0f;

    F32 elapsedTime = 0;
    if(mLastCameraUpdate != -1)
      elapsedTime = curTime - mLastCameraUpdate;
    mLastCameraUpdate = curTime;

    mCameraZoom += mCameraZoomSpeed * elapsedTime;
    if(mCameraZoom < 1) mCameraZoom = 1;

    mCameraYaw += mCameraYawSpeed * elapsedTime;

    mCameraPitch += mCameraPitchSpeed * elapsedTime;
    F32 temp = M_PI/2 - 0.2;
    if(mCameraPitch > temp) mCameraPitch = temp;
    if(mCameraPitch < -temp) mCameraPitch = -temp;
In replacing the code mentioned above, we have essentially removed the need for the mCameraPos variable. It isn't used much throughout the GameConnection class and could be easily removed, however it is not essential for the completion of this tutorial. There is one case, however, where we need to make a code change in relation to mCameraPos.
  • Open "/engine/game/GameConnection.h" in the "V12 Engine" project.
  • Search for the "bool isFirstPerson()" method and change it to return the value of the mFirstPerson variable.

    bool isFirstPerson() {return mFirstPerson;}


Step 5 - Calculate the camera transformation matrix

The ShapeBase class is used to represent any 3D object that the player can control. This is where we will implement the new code to calculate the transformation matrix used to position and orient the camera in relation to the player model. The method on the ShapeBase class that we are interested in is called "getCameraTransform()".

The way this method works in the base SDK is that you pass in a "position" variable (mCameraPos was passed in from the GameConnection class) which defined what percentage of the maximum camera distance the camera was from the player model. Since we have extended the functionality of the camera system, we have a few more variables to pass into this method. Therefore, we need to do change the signature of this method to take a few new parameters and also change any other code that depends on this method.

  • Open "/engine/game/ShapeBase.h" in the "V12 Engine" project.
  • Search for the "void getCameraTransform" method and modify the parameter list to look like this:

    virtual void getCameraTransform(bool firstPerson, F32 cameraPitch, F32 cameraYaw, F32 cameraZoom, MatrixF* mat);
Because this is a virtual method, we need to update any classes that implemented an override method. The only class in the engine that does this is the Camera class, which we don't really need anymore but in order to get the project to compile, we need to make the following updates.
  • Open "/engine/game/Camera.h" in the "V12 Engine" project.
  • Search for the "void getCameraTransform" method and modify the parameter list to look like this:

    void getCameraTransform(bool firstPerson, F32 cameraPitch, F32 cameraYaw, F32 cameraZoom, MatrixF* mat);

  • Open "/engine/game/Camera.cc" in the "V12 Engine" project.
  • Search for the "Camera::getCameraTransform" method and modify the parameter list to look like this:

    void Camera::getCameraTransform(bool firstPerson, F32 cameraPitch, F32 cameraYaw, F32 cameraZoom, MatrixF* mat)

  • Search for "obj->getCameraTransform" method and modify the parameter list to look like this:

    obj->getCameraTransform(firstPerson, cameraPitch, cameraYaw, cameraZoom, mat);

Now that we have updated the signature of the getCameraTransform() method, we need to pass in the appropriate parameters (we recently created) in the GameConnection class.
  • Open "/engine/game/GameConnection.cc" in the "V12 Engine" project.
  • Search for the "bool getControlCameraTransform" method and update the two calls to getCameraTransform() to use the new static member variables we created in the beginning of this tutorial.

    if (!sChaseQueueSize || mFirstPerson || obj->onlyFirstPerson())
      obj->getCameraTransform(mFirstPerson, mCameraPitch, mCameraYaw, mCameraZoom, mat);
    else {
      MatrixF& hm = sChaseQueue[sChaseQueueHead];
      MatrixF& tm = sChaseQueue[sChaseQueueTail];
      obj->getCameraTransform(mFirstPerson, mCameraPitch, mCameraYaw, mCameraZoom,&hm);
      *mat = tm;
      if (dt) {
        if ((sChaseQueueHead += 1) >= sChaseQueueSize)
          sChaseQueueHead = 0;
        if (sChaseQueueHead == sChaseQueueTail)
          if ((sChaseQueueTail += 1) >= sChaseQueueSize)
            sChaseQueueTail = 0;
      }
    }
Finally, we can add the actual code that calculates the camera transformation matrix based on these new parameters. This is done back in the ShapeBase class in the getCameraTransform() method.
  • Open "/engine/game/ShapeBase.cc" in the "V12 Engine" project.
  • Search for the "ShapeBase::getCameraTransform" method and replace the entire method with the following code:

    void ShapeBase::getCameraTransform(bool firstPerson, F32 cameraPitch, F32 cameraYaw, F32 cameraZoom, MatrixF* mat)
    {
      // Returns camera to world space transform
      // Handles first person / third person camera position

      if (isServerObject() && mShapeInstance)
        mShapeInstance->animateNodeSubtrees(true);

      // if first-person view, use eye vector of model
      if (firstPerson)
      {
        getRenderEyeTransform(mat);
      }

      // else if third person view, figure out where camera is
      else
      {
        // determine the transform matrix for the eye
        MatrixF eye;
        getRenderEyeTransform(&eye);

        // Use the eye transform to orient the camera
        VectorF vp(0, cameraZoom, 0);

        // rotate around x and z axis
        MatrixF xRot, zRot;
        xRot.set(EulerF(cameraPitch, 0, 0));
        zRot.set(EulerF(0, 0, cameraYaw));
        MatrixF temp;
        temp.mul(zRot, xRot);
        Point3F rotPoint;
        temp.mulV(vp, &rotPoint);
        vp = rotPoint;

        // determine the vector where the camera should go, in local coordinates
        VectorF cameraVec;
        eye.mulV(vp, &cameraVec);

        // center the camera on the eye of the model, in world coordinates
        Point3F lookAtPos = eye.getPosition();
        Point3F osp;
        if(mDataBlock->cameraNode != -1)
        {
          mShapeInstance->mNodeTransforms[mDataBlock->cameraNode].getColumn(3, &osp);
          getRenderTransform().mulP(osp, &lookAtPos);
        }
        else
          getRenderTransform().getColumn(3, &lookAtPos);

        // Make sure we don't extend the camera into anything solid
        Point3F cameraPos = lookAtPos + cameraVec;

        // disable collision system for this test (not sure why at this point)
        disableCollision();
        if (isMounted())
          getObjectMount()->disableCollision();

        // cast a ray between the two points and clip if necessary
        RayInfo collision;
        if (mContainer->castRay(lookAtPos, cameraPos,
          (0xFFFFFFFF & ~(WaterObjectType |
          ForceFieldObjectType |
          GameBaseObjectType |
          DefaultObjectType)),
          &collision) == true)
        {
          F32 veclen = cameraVec.len();
          F32 adj = (-mDot(cameraVec, collision.normal) / veclen) * 0.1;
          F32 newPos = getMax(0.0f, collision.t - adj);
          if (newPos == 0.0f)
            eye.getColumn(3, &cameraPos);
          else
            cameraPos = lookAtPos + (cameraVec * newPos);
        }

        // re-orient the transform matrix to point at the lookAtPos from the cameraPos
        vp = lookAtPos - cameraPos;
        vp.normalize();
        *mat = MathUtils::createOrientFromDir(vp);
        mat->setColumn(3, cameraPos);

        // enable the collision system again
        if (isMounted())
          getObjectMount()->enableCollision();
        enableCollision();
      }
    }


Step 6 - Build the engine

We have finished all the engine changes for this tutorial. At this time, it would be a good idea to rebuild the V12 Engine project and resolve any compiler/linker errors. Note that I have had problems with making header file changes and doing an incremental compile of the project. Therefore, I suggest doing a "Rebuild All" on the V12 Engine project.

You can continue with the rest of the tutorial while the engine is building, if you want.


Step 7 - Add "key binds" to client scripts

It's time to give the player access to this new camera system. To do this, we need to add code to the scripts that transform key presses into changes in the six console variables we defined in Step 5.

  • Open "/example/client/scripts/default.bind.cs"
  • Search for the "Helper Functions" section of the file and add the following script functions to it. These functions will act as our keyboard handlers.

    // Zoom the camera in by setting the $cameraZoomSpeed
    // to a positive number
    function zoomCameraIn(%val)
    {
      if($firstPerson) return;
      if(%val)
        $cameraZoomSpeed += $pref::Player::cameraZoomSpeed;
      else
        $cameraZoomSpeed -= $pref::Player::cameraZoomSpeed;
    }

    // Zoom the camera in by setting the $cameraZoomSpeed
    // to a negative number
    function zoomCameraOut(%val)
    {
      if($firstPerson) return;
      if(%val)
        $cameraZoomSpeed -= $pref::Player::cameraZoomSpeed;
      else
        $cameraZoomSpeed += $pref::Player::cameraZoomSpeed;
    }

    // Rotate the camera horizontally around the player
    // in a clockwise motion by setting the $cameraYawSpeed
    // to a positive number
    function rotateCameraLeft(%val)
    {
      if($firstPerson) return;
      if(%val)
        $cameraYawSpeed -= $pref::Player::cameraRotateSpeed;
      else
        $cameraYawSpeed += $pref::Player::cameraRotateSpeed;
    }

    // Rotate the camera horizontally around the player
    // in a counter-clockwise motion by setting the $cameraYawSpeed
    // to a negative number
    function rotateCameraRight(%val)
    {
      if($firstPerson) return;
      if(%val)
        $cameraYawSpeed += $pref::Player::cameraRotateSpeed;
      else
        $cameraYawSpeed -= $pref::Player::cameraRotateSpeed;
    }

    // Rotate the camera vertically around the player
    // in an upward motion by setting the $cameraPitchSpeed
    // to a positive number
    function rotateCameraUp(%val)
    {
      if($firstPerson) return;
      if(%val)
        $cameraPitchSpeed -= $pref::Player::cameraRotateSpeed;
      else
        $cameraPitchSpeed += $pref::Player::cameraRotateSpeed;
    }

    // Rotate the camera vertically around the player
    // in a downward motion by setting the $cameraPitchSpeed
    // to a negative number
    function rotateCameraDown(%val)
    {
      if($firstPerson) return;
      if(%val)
        $cameraPitchSpeed += $pref::Player::cameraRotateSpeed;
      else
        $cameraPitchSpeed -= $pref::Player::cameraRotateSpeed;
    }

    // Reset the camera position to the "home" position
    function resetCamera(%val)
    {
      if($firstPerson) return;
      $cameraPitch = -0.2;
      $cameraYaw = 3.14159265358979323846;
      $cameraZoom = 4;
    }


  • Just under this code, add the actual keyboard bindings that will control the camera. Of course, you can set these to anything you want. These bindings are just an example.

    moveMap.bind(keyboard, "numpadadd", zoomCameraOut);
    moveMap.bind(keyboard, "numpadminus", zoomCameraIn);
    moveMap.bind(keyboard, "right", rotateCameraLeft);
    moveMap.bind(keyboard, "left", rotateCameraRight);
    moveMap.bind(keyboard, "up", rotateCameraUp);
    moveMap.bind(keyboard, "down", rotateCameraDown);
    moveMap.bind(keyboard, "insert", resetCamera);


Step 8 - Add client preferences

You might have caught the use of $pref::Player::cameraRotateSpeed and $pref::Player::cameraZoomSpeed in the last set of script functions. These are preference variables that the player can use (possibly through a nice options dialog) to control the speed at which the camera zooms and rotates, respectively.

  • Open "/example/base/scripts/clientDefaults.cs"
  • Search for the "$pref::Player" variables and add our two new camera preference variables to that namespace. Again, you can set these defaults to values that work for your particular game.

    $pref::Player::Count = 0;
    $pref::Player::Current = 0;
    $pref::Player::defaultFov = 90;
    $pref::Player::zoomSpeed = 0;
    $pref::Player::Name = "Test Guy";
    $pref::Player::cameraZoomSpeed = 8;
    $pref::Player::cameraRotateSpeed = 2;


Congratulations! You have completed the tutorial. You should now be able to run your application and use the NUMPAD keys to rotate the camera around the player model.

This is the first tutorial I have written and I am interested in getting some feedback on content or presentation. Of course, if something in this tutorial doesn't work, please don't hesitate to write me either. Feedback can be sent to justin.mette@21-6.com.