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.
#81
02/03/2005 (11:27 am)
@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.

Nathan.

UPDATE: To confirm this bug I started from scratch with a fresh installation of TGE 1.3. Installed the Advanced camera resource and was able replicate the errors listed above.
#82
02/03/2005 (10:34 pm)
@Nathan

I've tried now a few times, and no. I cannot replicate your errors with missing projectiles, shapes inside the huts or similar.

Neither in my game build or in a clean 1.3 with this resource

Has anyone else experienced this????
#83
02/16/2005 (4:22 am)
Hi everybody !!!

Excellent resource Thomas! Thank you !!
#84
02/16/2005 (1:16 pm)
Thomas,

Excellent resource! Saved me a ton of work. I am having one minor issue. When we first set the camera up in ThirdPersonMode, it flys up from (0,0,0). Is there any way to turn off the transitions?

Thanks again for the great resource!
#85
02/18/2005 (5:50 am)
I was mentioned above, but i see no response of work around for it. Once you integrate and use the AdvancedCamera you loose the free flying camera which is very useful when using the mission editor. Is there a better work around than commenting out the advanced camera code when editing?
#86
02/18/2005 (2:43 pm)
@Bill - I've noticed it too, and I think you can set the camera position to the player position (or a position near that) on startup. The engine code starts with the camera at 0,0,0 and in the first ticks it will interpolate the camera to its new position. Thats why. By setting the camera close to where you need it in game, it wont interpolate over such a long distance.

@Tom - press tab to go to 1st person, then press F8 to detach. Now you are in free fly mode
#87
02/22/2005 (8:14 am)
@Thomas - Except that doesn't work. When i do that the first person seems to change back to the advanced camera and the player name ( Freshmeat ) appears. It's like it's trying to go into free fly mode, but can't.
#88
02/22/2005 (8:23 am)
Hey Tom - it works, but order is important! I use it every day, so I know it works ;-)

1) If camera is in adv camera mode, press TAB
2) Press F8 to detach

You are now in free float mode

If you press F8 first and then TAB, then you will be stuck in free float, but still attached to the ship and you cannot move
#89
02/22/2005 (8:27 am)
@Thomas - Then there must be something else wrong then. I am familiar with the order issue, but it still doesn't work for me. Maybe it's an issue of key mappings not set properly for the advanced camera.
#90
03/01/2005 (3:53 pm)
has anyone worked out a multiplayer client-side version of this resource? would be really great! ;o)
#91
03/01/2005 (3:58 pm)
@Thomas - Still having trouble with getting the free fly camera working with the advanced camera resource integrated. I don't even see where in the advanced camera class it supports the free fly camera. Is it supported by some other Torque class? Which one? Where could i set a breakpoint to catch if it's being executed at all?
#92
03/01/2005 (11:07 pm)
@Tom - the adv camera doesnt support free fly. Thats why you have to press tab first to go into first person = the old camera system.

So if you deleted/replaced the old camera with the advanced camera, then that will explain why you cannot get free fly to work.

Just keep the old camera in there still :

function GameConnection::onClientEnterGame(%this)
{
   commandToClient(%this, 'SyncClock', $Sim::Time - $Game::StartTime);

   // Create a new camera object.
   %this.camera = new Camera() {
      dataBlock = Observer;
   };
   MissionCleanup.add( %this.camera );
   %this.camera.scopeToClient(%this);

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

The above is from server/scripts/game.cs
#93
03/01/2005 (11:17 pm)
@Thomas - Yep thats my problem. Surprisingly aside from my free fly mode issue it all works just fine including first person (this doesn't use the camera at all) with the AdvancedCamera replacing the default camera in game.cs.

Thanks for the help and the great resource.
#94
03/01/2005 (11:22 pm)
Phew - so a "natural" explaination. You almost had me sweat ;-)
#95
03/02/2005 (12:08 pm)
@Thomas:

I just got around making the orbit interpolation multiplayer compatible. It was too hackish the way it was, and wouldn't work in multiplayer. The following changes add 4 things:

- Multiplayer support;
- Rotation is done client-side;
- It's no longer framerate dependent;
- The control interfaces makes more sense now.


In AdvancedCamera::updateMovementValues, and add this right in the beginning:

//Set the absolute azimuth, declination and zoom values
	if ( dStrcmp("", Con::getVariable("$advCamera::azimuth")) )
		mAzimuth = (M_PI_F / 180) * 
                           Con::getFloatVariable( "$advCamera::azimuth",
                                             mAzimuth * 180 / M_PI_F ) ;

	if ( dStrcmp("", Con::getVariable("$advCamera::declination")) )
		mDeclination = (M_PI_F / 180) * 
                               Con::getFloatVariable( "$advCamera::declination",
                                                   mDeclination * 180 / M_PI_F );

	if ( dStrcmp("", Con::getVariable("$advCamera::zoomDistance")) )
		mZoomDistance = Con::getFloatVariable( "$advCamera::zoomDistance",
                                                        mZoomDistance );

	//Clamp the absolute values
	mDeclination = getMax( mCurrentOrbitMinMaxDeclination.x *
                               M_PI_F/180, getMin( mDeclination,
                               mCurrentOrbitMinMaxDeclination.y * M_PI_F/180 ) );
	
	mZoomDistance = getMax( mCurrentOrbitMinMaxZoom.x, 
                               getMin( mZoomDistance, mCurrentOrbitMinMaxZoom.y ) );
	
	
	//Reset the console variables that control the absolute values
	Con::setVariable( "$advCamera::azimuth", "" );
	Con::setVariable( "$advCamera::declination", "" );
	Con::setVariable( "$advCamera::zoomDistance", "" );

	//Apply the yaw, pitch and zoom increments
	mYaw   += Con::getFloatVariable( "$advCamera::Yaw", 0.0 ) * dt;
	mPitch += Con::getFloatVariable( "$advCamera::Pitch", 0.0 ) * dt; 
	mZoom  += Con::getFloatVariable( "$advCamera::Zoom", 0.0 ) * dt; 

	//Reset the increment console variables
	Con::setFloatVariable( "$advCamera::Yaw", 0.0 );
	Con::setFloatVariable( "$advCamera::Pitch", 0.0 );
	Con::setFloatVariable( "$advCamera::Zoom", 0.0 );

Now comment out these lines in AdvancedCamera::readPacketData:
mYaw   += mYawAdjust;
	mPitch += mPitchAdjust;
	mZoom  += mZoomAdjust;

Now, if you wish to cleanup, you can get rid of anything that's involved with mYawAdjust, mPitchAdjust and mZoomAdjust, since those are no longer used to rotate the camera. You can also get rid of the addYaw, addPitch, addZoom, setYaw, setPitch and setZoom functions, and their respective consoleMethod declarations. Much smaller code now, heh?

So, how do I control the camera now, you should ask? Simple, now it's like controlling the player: using console variables.

These control speed for each property:
$advCamera::Yaw
$advCamera::Pitch
$advCamera::Zoom
If you set $advCamera::Yaw to 20.0, as example, the camera will start rotating left. Set it to 0.0 to stop rotating. Instructions on integrating with mouse control comes below.

And these control absolute values for each parameter:
$advCamera::azimuth
$advCamera::declination
$advCamera::zoomDistance
If you set $advCamera::azimuth to 0, the camera azimuth will be instantly set to 0. Good to reset the camera to a pre-determinated angle. Ah, the $advCamera::azimuth and $advCamera::declination values are in degrees.

Here's a sample on how to get the camera to rotate with the mouse now, in default.binds.cs:

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

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

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

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

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

Done. I hope this helps.
#96
03/02/2005 (1:13 pm)
Excellent Manoel

Resource has been updated with your changes + I added a zoom function to the default.bind.cs
#97
03/04/2005 (1:22 am)
Ok... maybe this is a dumb question. Why does the AdvancedCamera keep it's own position and rotation state and not use the position and rotation from SceneObject? I've added AdvancedCamera into my custom Mission Editor creation menu so that i can place AdvancedCameras around my scene. To make things work as expected for tracking and static modes i added some hacky code to set mCameraPosition to the SceneObject::getPosition() and mRot to SceneObject::getRotation.

Also AdvancedCamera::getPosition() needs to be renamed. It currently blocks the call to the SceneObject:: member of the same name.
#98
03/04/2005 (1:31 am)
Very good questions Tom - there are no reasons why the resource shouldnt be more tightly integrated with TGE.

There are lots of possible optimizations to be made. I was planning a total rewrite of this resource, but I dont have time the next many month.

And in the end it all works for what I need it for. But if you got some changes and additions to make it even more useful, I'm more than happy to include any code changes and additions.
#99
03/04/2005 (1:38 am)
I'll see what i can do. I've already done quite a few additions to it... most likely stuff that isn't useful to anyone but my own project. But i have a few good general fixes to contribute. I'll post something at the latest after the crazyness of GDC is over.
#100
03/04/2005 (3:41 pm)
I am having troubles with this, :(.

In my console.log, I get an error:
starter.fps/server/scripts/game.cs Line: 341 - Syntax error.
>>> Advanced script error report.  Line 681.
>>> Some error context, with ## on sides of error halt:
  %this.advCamera.setFollowTerrainMode(false);

  %this.advCamera.setVerticalFreedomMode(false); New

  %this.##s##etCameraObject(%this.advCamera);

}


You can find my game.cs file at http://uberrealgames.com/game.cs

--Robert