Game Development Community

dev|Pro Game Development Curriculum

Advanced Camera

by Thomas \"Man of Ice\" Lund · 04/03/2008 (11:57 am) · 480 comments

Download Code File

Description
Over time a lot of people have released camera resources, but some do not work anymore, others are hard to implement etc.

I've tried to assemble a single class to add to the engine that implements them all in one go using the same basic architecture. This enables minimum code bloat and makes it way easier to keep the code up to date.

Change log
March 3rd, 2005
Manoel made some changes to the orbit camera. Works in multiplayer now and is much nicer by using console variables

February 3rd, 2005
Fixed small big in interpolation.

February 2nd, 2005
Major changes in this one with various contributors.
* Static camera mode
* Smooth interpolation and transition between modes
* Smooth orbit camera!!!
* Vertical freedom mode when in 3rd person
* Better collision check with terrain and interiors
* Mouse control of orbit camera
* Totally reworked codebase and lots of cleanups. Much more readable now

Manoel Neto contributed the new orbit camera and the interpolation
Zik Saleeba contributed the vertical freedom mode and better collision check

Thanks a lot!!!!

I have marked changes with a New in the text below for those who upgrade

January 23rd, 2005
Minor changes. Larger update soon with new functionality
* Now takes GameBase objects as target + player
* Removed debug message in orbit camera
* Added getters for player and target object

June 25th, 2004
Updated the bindings for orbit camera. Switched left+right.

June 23rd, 2004
A big thanks to Stephen Zepp for contributing with an orbit camera mode. Its added to the resource, and is perfect for RTS games and action adventures. It allows for a camera to rotate around a user as if placed on a sphere. The user can zoom in/out, rotate and tilt the camera.

June 9th, 2004
Added getter/setter for the 3 offset values accessible from script
Added a "follow terrain" mode for the third person camera, so the camera follows the terrain slope - doesnt work perfectly

April 5th, 2004
All camera modes now use a raycast to not get hidden behind terrain or interiors

April 3rd, 2004
This release is the first release, and might not be as "advanced" as the author would like, but its time to release it and get some feedback to further enhance it down the road.

Camera Modes Implemented
The resource currently implements the following camera systems:
* Track Mode
This is the same as the tracking camera resource posted by Cory Osborn. A stationary camera tracks the player and keeps him in focus.

* New Static Mode
Camera stays in its position and rotation. Useful for e.g. scene based adventures where the camera doesnt move with the players

* Third Person Mode
The camera is placed at an offset behind the player and rotates with the player. The camera itself is not controllable

* Third Person Track Mode
The camera is placed in third person mode but rotated so it always looks at a specified object, but with the player in full view

* God View
Camera is placed at an offset from the player and does not rotate with the player. This is your typical "Diablo" kind of camera.

* New Orbit Mode
Camera is placed on a sphere looking at the player, and can rotate/tilt and zoom controlled by the player. There are bindings to the mouse when in single player game (server and client on same machine)

Movie
Here is a small movie displaying the different camera modes (3 MB)
www.codejar.com/advancedcamerademo.wmv

How to Add
First off all you need to take the attached advancedCamera.cc/h files and add to engine\game and add them to the project.

Then you need to expose the camera object in GameConnection as described in Cory's resource www.garagegames.com/index.php?sec=mg&mod=resource&page=view&qid=4720

I took the liberty to paste the relevant parts in here too

Exposing mCameraObject
The first thing you want to do is add console method's to access the GameConnection setCameraObject/getCameraObject methods. I added these to GameConnection.cc right after the console method for getControlObject:

ConsoleMethod( GameConnection, setCameraObject, bool, 3, 3, "(ShapeBase object)")
{
   ShapeBase *gb;
   if(!Sim::findObject(argv[2], gb))
      return false;

   object->setCameraObject(gb);
   return true;
}

ConsoleMethod( GameConnection, getCameraObject, S32, 2, 2, "")
{
   argv;
   SimObject* cp = object->getCameraObject();
   return cp ? cp->getId(): 0;
}

ConsoleMethod( GameConnection, clearCameraObject, void, 2, 2, "")
{
   object->setCameraObject(NULL);
}

After playing around with it, I also found you need some adjustments to the setCameraObject and setControlObject methods - otherwise the client connection can screw things up if you bounce the same object from your connection's control to camera or vice versa. Here is my GameConnection::setControlObject:

void GameConnection::setControlObject(ShapeBase *obj)
{
   if(mControlObject == obj)
      return;

   if(mControlObject && mControlObject != mCameraObject) 
      mControlObject->setControllingClient(0);
   
   if(obj)
   {
      // Nothing else is permitted to control this object.
      if (ShapeBase* coo = obj->getControllingObject())
         coo->setControlObject(0);
      if (GameConnection *con = obj->getControllingClient()) 
      {
         if (this != con) 
         {
            // was it controlled via camera or control?
            if (con->getControlObject() == obj)
               con->setControlObject(0);
            else
               con->setCameraObject(0);
         }
      }

      // We are now the controlling client of this object.
      obj->setControllingClient(this);
   }

   // Okay, set our control object.
   mControlObject = obj;
   if (mCameraObject.isNull())
      setScopeObject(mControlObject);
}


and here is my GameConnection::setCameraObject:

void GameConnection::setCameraObject(ShapeBase *obj)
{
   if(mCameraObject == obj)
      return;

   if(mCameraObject && mCameraObject != mControlObject) 
      mCameraObject->setControllingClient(0);
   
   if (obj) {
      // Nothing else is permitted to control this object.
      if (ShapeBase* coo = obj->getControllingObject())
         coo->setControlObject(0);
      if (GameConnection *con = obj->getControllingClient()) 
      {
         if (this != con) 
         {
            // was it controlled via camera or control?
            if (con->getControlObject() == obj)
               con->setControlObject(0);
            else
               con->setCameraObject(0);
         }
      }

      // We are now the controlling client of this object.
      obj->setControllingClient(this);
   }

   // Okay, set our camera object.

   mCameraObject = obj;

   if (mCameraObject.isNull()) {
      setScopeObject(mControlObject);
   } else {
      setScopeObject(mCameraObject);
      // if this is a client then set the fov and active image
      if(isServerConnection())
      {
         F32 fov = mCameraObject->getDefaultCameraFov();
         GameSetCameraFov(fov);
      }
   }
}

Camera read/write packets
None of this will work unless the client copy of the camera object gets packets updated. Here we're going to modify GameConnection::readPacket and GameConnection::writePacket.

In GameConnection::readPacket, find this block of code:

if (bstream->readFlag())
      {
         S32 gIndex = bstream->readInt(10);
         ShapeBase* obj = static_cast<ShapeBase*>(resolveGhost(gIndex));
         setCameraObject(obj);
      }
      else
         setCameraObject(0);

and change it to

if (bstream->readFlag()) 
      {
         S32 gIndex = bstream->readInt(NetConnection::GhostIdBitSize);
         ShapeBase* obj = static_cast<ShapeBase*>(resolveGhost(gIndex));
         setCameraObject(obj);
         obj->readPacketData(this, bstream);
      }
      else
         setCameraObject(0);


In GameConnection::writePacket, find this block of code:

if (!mCameraObject.isNull() && mCameraObject != mControlObject)
      {
         gIndex = getGhostIndex(mCameraObject);
         if (bstream->writeFlag(gIndex != -1))
            bstream->writeInt(gIndex, 10);
      }
      else
         bstream->writeFlag( false );

and change it to:

if (!mCameraObject.isNull() && mCameraObject != mControlObject) 
      {
         gIndex = getGhostIndex(mCameraObject);
         if (bstream->writeFlag(gIndex != -1)) {
            bstream->writeInt(gIndex, NetConnection::GhostIdBitSize);
            mCameraObject->writePacketData(this, bstream);
         }
      }
      else
         bstream->writeFlag( false );

Recompile it all and the engine is ready to go.

Script

To use the camera you need to follow some of Corys resource, but with some modifications. Datablock and naming has changed

First, add a datablock for the tracking camera to /fps/server/scripts/camera.cs:

...
datablock AdvancedCameraData(AdvCameraData)
{
   lookAtOffset = "0 0 2";
   thirdPersonOffset = "0 -3 3";
   godViewOffset = "0 -20 20";
   maxTerrainDiff = 2;
   orbitMinMaxZoom = "5 100";
   orbitMinMaxDeclination = "10 80";
   damping = 0.25;

};
...

Next, add it to the connection just like is currently done with the base camera class. Add this inside GameConnection::onClientEnterGame (/fps/server/scripts/game.cs), right after %this.camera is set up:

...
   // create advanced camera
      %this.advCamera = new AdvancedCamera() {
      dataBlock = AdvCameraData;
   };
   MissionCleanup.add(%this.advCamera);
   %this.advCamera.scopeToClient(%this);
...


We'll need to clean it up after the client leaves the game, so add this to GameConnection::onClientLeaveGame

...
   if (isObject(%this.advCamera))

      %this.advCamera.delete();
...


We need to tell it what to do when added and assign the connection's camera object, so add this to the end of GameConnection::createPlayer:

...
  // We set the camera system to run in 3rd person mode around the %player
  %this.advCamera.setPlayerObject(%player);
  %this.advCamera.setThirdPersonMode();
  %this.advCamera.setFollowTerrainMode(false);
  %this.advCamera.setVerticalFreedomMode(false);
  %this.setCameraObject(%this.advCamera);
...


And we'll want to unhook it when the player dies. Insert this at the beginning of GameConnection::onDeath:

...
   // clear connections camera
   %this.advCamera.clearPlayerObject();
   %this.advCamera.clearTargetObject();
   %this.clearCameraObject();
...

The last thing to know, is if you're in first-person mode, GameConnection automatically uses the control object to render the engine rather than the camera object. So if you don't see this working when you first enter a mission, toggle out of first-person.

Orbit mode
To use the orbit camera, one needs to add some key binds to manipulating the camera.

All you need to do, is add the following to your client\config.cs or better to your client\scripts\default.bind.cs

//------------------------------------------------------------------------------
// Advanced Camera Movement
//------------------------------------------------------------------------------

$cameraYawSpeed = -100.0;
$cameraPitchSpeed = -50.0;
$cameraZoomSpeed = -5.0;

function rotateCameraHorizontal(%val)
{
     $advCamera::Yaw = getMouseAdjustAmount(%val)*$cameraYawSpeed ;
}

function rotateCameraVertical(%val)
{
     $advCamera::Pitch = getMouseAdjustAmount(%val)*$cameraPitchSpeed;
}

function zoomCamera(%val)
{
     $advCamera::Zoom = getMouseAdjustAmount(%val)*$cameraZoomSpeed;
}

moveMap.bind( mouse, xaxis, rotateCameraHorizontal);
moveMap.bind( mouse, yaxis, rotateCameraVertical );
moveMap.bind( mouse, zaxis, zoomCamera );

Remember to comment out the mouse commands for the player movement, as these are overwritten by the orbit camera controls
//moveMap.bind( mouse, xaxis, yaw );
//moveMap.bind( mouse, yaxis, pitch );

Script API
To use the different camera modes you can use the following API
Selecting the camera mode is done with e.g.:
%this.advCamera.setTrackMode();
  %this.advCamera.setThirdPersonMode();
  %this.advCamera.setThirdPersonTargetMode();
  %this.advCamera.setGodViewMode();
  %this.advCamera.setOrbitMode();
  %this.advCamera.setStaticMode();

Prior to calling the above modes you have to set the PlayerObject using
setPlayerObject();

To use the 3rd person target mode you also need to set a TargetObject using
setTargetObject();

To use the static or tracking camera you need to set the position the camera should be placed suing
setCameraPosition(Point3F pos);

To use the follow terrain mode in 3rd person you give a bool to
setFollowTerrainMode(true/false);

To use the vertical freedom mode in 3rd person you give a bool to
setVerticalFreedomMode(true/false);
This only works if the player object is a Player, because it uses the head movement.

The offset values in the datablock can be changed for the camera object via script using
get/setLookAtOffset();
get/setThirdPersonOffset();
get/setGodViewOffset();
The setters take a Point3F as argument.

The orbit camera can be manipulated from script using
get/setOrbitMinMaxZoom()
get/setOrbitMinMaxDeclination()
The min/max take a Point2F

You can also manipulate the orbit camera directly by assigning values to the following variables
[code]
$advCamera::Yaw
$advCamera::Pitch
$advCamera::Zoom
$advCamera::azimuth
$advCamera::declination
$advCamera::zoomDistance
[code]

All camera modes coexist, so you can set the position, player object and target object once and then switch camera modes around as you see fit. Switching mode will not clear the old objects/positions.
#141
06/04/2005 (10:23 pm)
Doh! Scratch that. I know why.
#142
06/05/2005 (12:28 pm)
Can this advanced camera follow a predefined path? And how? Sorry I'm a absolute newbie.

I just searched the comments, but I didn't find the answer, so I have to trouble someones, sorry again!
#143
06/09/2005 (10:20 pm)
@Shane

I've encountered oddities with your Orbit changes. Whenever I call the statement
mRadToDeg(getWord(%player.getTransform(), 6));
it never returns any value between 240 and 360. In those cases it instead returns a value between 0 and 120 and places the camera in incorrect locations.

EDIT: Figured out a fix. Replace the original statement with
mRadToDeg( getWord(%player.getTransform(), 6) * getWord(%player.getTransform(),5) );
#144
06/09/2005 (11:12 pm)
@Nicholas: I don't think that the advanced camera has any path following support.
#145
06/10/2005 (1:02 am)
Gah! I had a nice, long winded explanation of how to synch the server and ghost Advanced camera using the same control methods that the normal camera uses, and when I click the "Post Comment" button, the web page ate it. :-(

The super short version is that there is currently no mechanism for user inputs to get back to the server. One can modify AdvancedCamera to use the same mechanisms that all of the other common control objects use: Camera, Player, Vehicle. All user inputs enter the AdvancedCamera code through the Move* argument in processTick() on both client and server, thus keeping things properly in synch.

There are a few drawbacks, though. If you use the same code path (mvRoll, mvYaw, mvPitch) for both the camera and the player, how does the server know which you are trying to control in a third person view? I suspect that we'll want a NetEvent to do it right.

Bah, this version of the post is nowhere near as cool as the first one. ;-)
#146
06/10/2005 (5:51 am)
I also used the move struct to sync client and server, like Andrew suggests above. I mention in a previous resource post what I was able to setup. However, one thing I had problem with was the orbit camera never really worked quite right, but a home brewed 3rd person cam did. The reason being, when the advanced camera processed the move struct to turn the camera, the player also turned at the same time, instead of staying still. Since it wasn't the effect I wanted and had been wrestling it for a while, I gave up on that. I was going for an "Outcast" camera, where moving the mouse turns only the camera and not the player, but when the player is moving, turning the mouse also turns the player, and the player always moves relative to the camera. I'm sure it could be worked out with some more effort, just not by me right now. :)

@Nicholas - The TGE default camera supports paths. Take a look at the TGE demo, they use path cameras when showing off the features off the engine. You can look through the scripts and get an idea for how to set one up.
#147
06/10/2005 (12:59 pm)
@Drew: Could you just disable turning in the Player unless its velocity was non-zero, or somesuch?

Oh, wait, looking at "jump" and "mousefire" in script, they are just incrementing various "$mvTriggerCount" variables, and those get sent up to the server in the "trigger[]" array in "Move". We could do the same for AdvancedCamera, using one of them as a flag. If 1, control the camera. If 0, control the player.

Alternately, one could modify the Move struct itself, but that would add bandwidth.
#148
06/10/2005 (10:34 pm)
Well, the problem I reported with Advanced Camera not giving the information needed for a valid unproject appears to not be a fault of this resource at all. I gave up, and tried it with the Torque 3rd Person Cam, and the offset there goofs up unprojects as well... And I am certain that is the case. Click on a spot in first person and the projectile hits the mark, toggle to third person and the mouse is no longer on the spot, but the projectile flies the same. Move back over the spot, and the bullet hits the wrong spot. Toggle back to first person, and the bullet is hitting the exact spot the mouse is over... (In other words the unproject is getting the 2D location of the mouse screen coords, and unprojecting from the 1st person perspective regardless, and giving 3D coords relative to that and not the camera!)

I am a little surprised that no one else has had any difficulty with this, since as soon as you move away from the Player's eye transform, unproject gives you bad world coords for your mouse. I tried playing around with it by literally moving the player eye transform around a bit, and boy it jacked up my camera view in first person, but that dang projectile still pretended it was in first person mode. Officially giving up on this one, moving on to other things. My game will just have to be first person for a while ;-).

Brent - Promising to post a fix IF I ever figger it out.
#149
06/15/2005 (12:35 am)
This is just what I've been looking for! I've spen two days tracking the camera code only to end up with less hair than when I started and only a slightly better understanding of TGE. Thanks!
#150
06/16/2005 (11:41 pm)
I compiled it all fine.. but i got the same 'unable to instanciate advcamera' blah blah msg in console.log.. i forgot to do an #include "game/advancedCamera.h" at the top of gameconnection.cc .... is that how to include it in the project??? i'm using vc7

[edit: nevermind... it wasn't saving the additional files.. it's compiling it now.. will see if it actually works!!
#151
06/17/2005 (2:43 pm)
EDIT:::.... nevermind.. it wasn't working because I am a moron, and was not using the proper coding techniques.
#152
06/30/2005 (4:14 pm)
fixed
#153
07/12/2005 (5:57 am)
[post removed at request of author]
#154
07/12/2005 (7:42 am)
fixed
#155
07/31/2005 (4:32 am)
odd, i tried applying this again to a clean torque, and it worked! great! Dunno what it was, i was trying different resources.
#156
08/06/2005 (3:51 pm)
This is a great resource and tremendously helpful. Thanks to everyone who contributed, this really helps me understand how the engine works much better.
#157
08/07/2005 (3:25 pm)
There's so many posts in here that I don't have the time to read them all. But I've totaly removed the jittering from my version of the advanced camera(that has smooth-flight interpolation) by removing the server-side movement calculations in processTick. Now my camera flys completely smooth and looks great.

Note: My version of the camera is from the original release and it's cusomized.
#158
08/07/2005 (11:41 pm)
@Josh: Where can we find your resource?
#159
08/08/2005 (6:55 pm)
There isn't one. I did send a copy of my RPG advanced camera to Thomas, but I don't think he's done anything with the code. I was just posting about how I fixed a long standing issue with my code by removing the server side Mode stuff from proccessTick.
#160
08/09/2005 (7:22 am)
Quote: @Thomas,

Just wondering if you have had chance to replicate the bugs I listed above. I've also noticed that Kork is missing from the scene. When I start the world editor I see his bounding box running around the village but the model doesn't show at all.

I've upgraded the new resource but the above problem appears, anyone has the same thing recently?