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.
#101
03/04/2005 (5:30 pm)
hi robert,
it's simple to solve:

just delete the "New" of
%this.advCamera.setVerticalFreedomMode(false); New

best regards,
Jack ;o)
#102
03/04/2005 (7:45 pm)
That was killing me before on a different part of the script.

I should have thought to check. THis is what I get for only sleeping during compiles, :(.
#103
03/07/2005 (11:27 am)
Okay, found a nasty bug.

The advanced camera server position is always ZERO, so things like items and vehicles will disappear around you if you walk too far from the world origin.

I got a small hack to solve it, and better solutions are welcome.

Go to advancedCamera::advanceTime() and move everything past updateMovementValues(dt) to advancedCamera::interpolateTick(), after Parent::interpolateTick(dt), so now yout advanceTime looks like this:

void AdvancedCamera::advanceTime(F32 dt) {

  Parent::advanceTime(dt);
  
  updateMovementValues(dt);
}

Now go to AdvancedCamera::processTick and add these at the end:

if (isServerObject())
	{
		interpolateTick(0.032f);
	}

(that magical number is how long a tick lasts - it wasn't needed really)

Compile, and now the scope problems are gone.
#104
03/07/2005 (11:58 am)
Fantastic. That bug has haunted me for a long time. I think it relates to the problems that Tom Spilman indicates with the position and rotation not using the underlying shapebase, but its own.

Will have to test both the hack and removing the redundant pos/rot. That will make things much easier to control too from editors
#105
03/07/2005 (7:41 pm)
@Manoel:
Before I put this fix in, I could no longer see Korky, items, and particle effects were not appearing. They were "there" in the Mission Editor.
#106
03/08/2005 (7:48 am)
Posted this yesterday, but for some reason it didn't show up.

This is a great resource, and thanks for the efforts.
I'm having an issue in using it that I'm hoping that someone can help with.

When returning to the regular camera, the third person camera doesn't follow anymore. First person does work though. I only have to do this because this camera is really shaky when moving fast (Same issue as Juan above, but no one had answered him).


Any ideas?

TIA
-JS
#107
03/08/2005 (12:45 pm)
@myself
Answering my own question in case anyone else has the same issue:

When returning to the original camera, use setCameraObject with the player object as the parameter and not the GameConnection's camera object.
#108
03/08/2005 (1:31 pm)
@Thomas - I successfully removed the mPosition and mRot in my copy of AdvancedCamera. They now both are pulled from mObjToWorld. You still have to add mObjToWorld to your packet read/write as those are not written by SceneObject. I have only tested this with my custom camera mode and the tracking camera, but i'll be trying third person later tonight after my GDC events are over. I will try to post my changes here then.
#109
03/10/2005 (1:09 pm)
I'm trying to use this, i implemented the code and the script changes, but when i do alt-c it lock my screen and i can't move the player model.
I can't cycle through all the views.
I did everythign up to Script API after that i got no idea what i need to change or if it needs changing. All i want to do is add godview as a extra.
#110
03/11/2005 (3:39 am)
Hello, Harold here.

We applied the Advanced Camera technique as described. We are also using the Lightning pack so the tutorial wasn't exactly matching, but anyway, it's working now. But.... now we 'd like to switch to the static camera (wich works!) and also ROTATE the camera....! Can anyone tell us how to rotate the static cam? Setting the Yaw (i.e. $advCamera::Yaw=10; ) doesn't do anything....
#111
03/11/2005 (6:43 am)
The static cam cannot be rotated as is. Thats because its.... static.

The yaw settings only apply to the camera while in orbit mode, where the camera orbits the player.

You could quite easily hack the code to take some "setStaticRotation" script method and apply to the static cam, but you would have to do that yourself.
#112
03/15/2005 (1:16 am)
Yo, mister Man Of Ice, thanks for your response. We understand that a static camera is........static! But for a movie-like-cutscene, it would be nice to set the yaw/pitch for the static camera. Static in position, but not in rotation.

How we resolved this (read as: HOW TO MAKE ROTATING POSSIBLE ON A STATIC CAM):

Search in advancedcamera.cc for this function: "AdvancedCamera::setPosition"
Search for this code:

} else {
		MatrixF xRot, zRot;
		xRot.set(EulerF(mRot.x, 0, 0));
		zRot.set(EulerF(0, 0, mRot.z));
		transMat.mul(zRot, xRot);
	}

and change it to this (=adding 2 lines):
} else {
		MatrixF xRot, zRot;
		//New lines added below for making StaticCamRotation possible
		mRot.x = mDeclination;
		mRot.z = mAzimuth;
		xRot.set(EulerF(mRot.x, 0, 0));
		zRot.set(EulerF(0, 0, mRot.z));
		transMat.mul(zRot, xRot);
	}

If you want to be able to freely place the static camera without the object collision detection,
also change in the "AdvancedCamera::advanceTime" section:

// Adjust camera position if it collides with "stuff"
	// for all modes except track mode
	if (mMode && mMode != TrackMode && playerObj != NULL) {
		cameraPosWorld = runCameraCollisionCheck(playerObj->getRenderPosition(), cameraPosWorld);
	}
into
// Adjust camera position if it collides with "stuff"
	// for all modes except track mode
	if (mMode && mMode != TrackMode && playerObj != NULL && mMode != StaticMode) {
		cameraPosWorld = runCameraCollisionCheck(playerObj->getRenderPosition(), cameraPosWorld);
	}



After these changes, recompile your .exe
For more freedom in rotation, don't forget to change the AdvCamera Datablock in your camera.cs script:

orbitMinMaxDeclination = "10 80";
change into:
orbitMinMaxDeclination = "-90 90";

Kind regards, The One and Only (Harold).
(Thanx to my dearest code-programming friends Kasper "Man of Water" Oerlemans and Jitte "Man of Steam" Hoekstra)
#113
03/23/2005 (12:49 am)
For the purposes of my game, whilst the camera is 3rd person I still only want the player to see what the character can see, so my work around for the scoping problem was to leave setScopeObject as the control object. Seems to be working fine and I can see all the relevant objects again in the fps.starter demo.

Its as simple as it sounds, though if anyone wants me to post some code let me know.
#114
03/29/2005 (12:07 am)
Uhhh... Harold here.... can you rephrase your problem? Caus this doesn't make any sense. The standard 3rd person camera already does what you want, doesn't it? How can you have a 3rd person camera showing only "what the character can see"? Don't you have a first-person cam then?

(Thanx to my dearest code-programming friends Kasper "Man of Water" Oerlemans and Jitte "Man of Steam" Hoekstra)
#115
03/29/2005 (3:04 am)
@Martin - yeah. Please explain what the initial problem was. I'm not sure I understand what you solved
#116
04/12/2005 (1:19 pm)
I would like roll the camera.Can I do this?
Tnx.
#117
04/12/2005 (11:44 pm)
Not without some extra code. But if you know simple matrix math, then it should be quite easy to add a roll to the camera object
#118
04/12/2005 (11:55 pm)
Just some info to you all

I succesfully removed the jittering I had while tracking 2 objects. It has taken me quite some experimentation, and I'm not sure its "done" yet. GOt to test it some more before releasing as a fix.

Its a combo of 2 problems.

The major contributor of jittering is the interpolation. For some reason while running in tracking mode, it makes the near object jump all over the place.

So comment out the lines
F32 tInterpolation = 0.333333f;
	tPos = pos*tInterpolation + tPos*(1.0-tInterpolation);

and put in

tPos = pos;

instead.

This solves it - almost. Next problem is that the "processAfter" is used twice. First to processAfter the player object and then processAfter the target object. This doesnt do what you expect it to. processAfter does not stack.

What we want is to process the camera _after_ the 2 objects, and if you are a bit unlucky then the camera will be processed in the middle of the 2 tracking objects - making one of them jitter

What works in my game is to comment out all the processAfter(targetObj) stuff in the camera code.

What would solve it was a processLast() command that puts the camera in the end. I'll most likely hack something up that does this. No deadline yet on that.

Feel free to post comments or observations of your own
#119
04/19/2005 (4:04 pm)
Alright, so I follow your guide to a T and i go to test it out and nothing happens. You probably get this a lot from idiots like myself, but I am so frustrated. I must have missed something. I run the Torque Demo and switch out of 1st person using TAB but then it is always the same camera no matter what. I am relatively new to Torque but I have been learning rather fast. Any help would be appreciated.
#120
04/21/2005 (12:50 pm)
@Thomas, When in Orbit Mode, setting the Azimuth to the player's facing direction does not make the camera face the same angle (or even the opposite angle which would also make sense).

I found a solution, and that is that these lines (advancedcamera.cc):
mOffsetX = mZoomDistance * sin(mDeclination) * cos(mAzimuth);
mOffsetY = mZoomDistance * sin(mDeclination) * sin(mAzimuth);
mOffsetZ = mZoomDistance * cos(mDeclination);

should be replaced with:
mOffsetX = mZoomDistance * sin(mDeclination) * sin(mAzimuth);
mOffsetY = mZoomDistance * sin(mDeclination) * cos(mAzimuth);
mOffsetZ = mZoomDistance * cos(mDeclination);

In case you're curious, I was using the following script:
$advCamera::azimuth = mRadToDeg(getWord(%player.getTransform(), 6));

This will make the camera face the opposite angle as the player, you can make the mOffsetX/Y negative if you want it to face the same angle as the player.

- Shane S. Anderson.