Game Development Community

dev|Pro Game Development Curriculum

Using a Separate Camera Object

by Cory Osborn · 10/27/2003 (9:52 am) · 71 comments

Download Code File

For my game, I need a few different 3rd person cameras. There are a couple of other good tutorials here to show off different 3p modes (like 21-6's dynamic camera tutuorial), but in my research I found that GameConnection already has some code to use a separate camera - all it needs is some tweaking.

There are 4 parts to this:
  • exposing GameConnection::mCameraObject to the console
  • having GameConnection read/write camera packets just as it does for the control
  • having a class that operates your camera mode
  • setting script to use the new camera in the game

The first two parts of this process are required to make a separate camera work within the game. The second half is just an example of plugging a different camera into the game.

note: this code has been verified against the CVS HEAD as of 04/03/2004.


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 );


Creating a camera object
Next we need an object to use as our camera. If you look at the code in GameConnection::getControlCameraTransform, you can see how GameConnection determines the position and rotation for rendering a frame. Based upon that code, any class descending from ShapeBase could theoretically be plugged in as the connection's camera.

The standard Camera class is one option. Another option is the TrackingCamera class attached to the article. TrackingCamera will change its orientation in order to track another object in the game. Without looking at its code, you'll just need to know that it exports a console method setTrackObject that allows you to specify which object will be tracked. One warning - the TrackingCamera will suffer from Gimbal Lock.


Putting it in the game
Here is the basic areas of code to change to see the example tracking camera in starter.fps. There are other areas where you'll need to tweak the script in order to unhook the camera and return to first-person mode, but I'll leave that for you as an exercise :)

First, add a datablock for the tracking camera to /starter.fps/server/scripts/camera.cs:
...
datablock TrackingCameraData(TrackCameraData)
{
   trackAdjPos = "0 0 2";
};
...

Next, add it to the connection just like is currently done with the base camera class. Add this inside GameConnection::onClientEnterGame (/starter.fps/server/scripts/game.cs), right after %this.camera is set up:
...
   // create in-game stationary camera
   %this.trackCamera = new TrackingCamera() {
      dataBlock = TrackCameraData;
   };
   MissionCleanup.add(%this.trackCamera);
   %this.trackCamera.scopeToClient(%this);
...

We'll need to clean it up after the client leaves the game, so add this to GameConnection::onClientLeaveGame
...
   if (isObject(%this.trackCamera))
      %this.trackCamera.delete();
...

We need to tell it what to track and assign the connection's camera object, so add this to the end of GameConnection::createPlayer:
...
   // set the stationary camera to the same position
   %this.trackCamera.setPosition(%this.camera.getPosition());
   %this.trackCamera.setTrackObject(%player);
   %this.setCameraObject(%this.trackCamera);
...

And we'll want to unhook it when the player dies. Insert this at the beginning of GameConnection::onDeath:
...
   // clear connection's camera
   %this.trackCamera.clearTrackObject();
   %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.

Hope it helps. - cory

Additional Resources
  • My resource on more intuitive movement controls for a third person camara
  • Thomas Lund's work for additional camera modes
  • Thomas Lund's combination of camera flipping with mission regions

  • Edits
    1/11/2004 - Updated the attached TrackingCamera class to override ShapeBase::onCameraScopeQuery so the tracked object is always scoped to the client.
    4/3/2004 - Verified the code still works on the latest HEAD and changed the example file paths to refer to starter.fps
    4/4/2004 - Added additional resources links
    Page«First 1 2 3 4 Next»
    #61
    01/19/2008 (1:08 pm)
    I'm getting a whole bunch of linker erros:
    Error	1	error LNK2001: unresolved external symbol "protected: virtual void __thiscall ShapeBase::UpdateImageRaycastDamage(float,unsigned int)" (?UpdateImageRaycastDamage@ShapeBase@@MAEXMI@Z)	flyingVehicle.obj	
    Error	2	error LNK2001: unresolved external symbol "protected: virtual void __thiscall ShapeBase::UpdateImageRaycastDamage(float,unsigned int)" (?UpdateImageRaycastDamage@ShapeBase@@MAEXMI@Z)	hoverVehicle.obj	
    Error	3	error LNK2001: unresolved external symbol "protected: virtual void __thiscall ShapeBase::UpdateImageRaycastDamage(float,unsigned int)" (?UpdateImageRaycastDamage@ShapeBase@@MAEXMI@Z)	vehicle.obj	
    Error	4	error LNK2001: unresolved external symbol "protected: virtual void __thiscall ShapeBase::UpdateImageRaycastDamage(float,unsigned int)" (?UpdateImageRaycastDamage@ShapeBase@@MAEXMI@Z)	wheeledVehicle.obj	
    Error	5	error LNK2001: unresolved external symbol "protected: virtual void __thiscall ShapeBase::UpdateImageRaycastDamage(float,unsigned int)" (?UpdateImageRaycastDamage@ShapeBase@@MAEXMI@Z)	rigidShape.obj	
    Error	6	error LNK2001: unresolved external symbol "protected: virtual void __thiscall ShapeBase::UpdateImageRaycastDamage(float,unsigned int)" (?UpdateImageRaycastDamage@ShapeBase@@MAEXMI@Z)	scopeAlwaysShape.obj	
    Error	7	error LNK2001: unresolved external symbol "protected: virtual void __thiscall ShapeBase::UpdateImageRaycastDamage(float,unsigned int)" (?UpdateImageRaycastDamage@ShapeBase@@MAEXMI@Z)	shapeBase.obj	
    Error	8	error LNK2001: unresolved external symbol "protected: virtual void __thiscall ShapeBase::UpdateImageRaycastDamage(float,unsigned int)" (?UpdateImageRaycastDamage@ShapeBase@@MAEXMI@Z)	staticShape.obj	
    Error	9	error LNK2019: unresolved external symbol "protected: virtual void __thiscall ShapeBase::UpdateImageRaycastDamage(float,unsigned int)" (?UpdateImageRaycastDamage@ShapeBase@@MAEXMI@Z) referenced in function "public: __thiscall ItemData::ItemData(void)" (??0ItemData@@QAE@XZ)	item.obj	
    Error	10	error LNK2001: unresolved external symbol "protected: virtual void __thiscall ShapeBase::UpdateImageRaycastDamage(float,unsigned int)" (?UpdateImageRaycastDamage@ShapeBase@@MAEXMI@Z)	missionMarker.obj	
    Error	11	error LNK2001: unresolved external symbol "protected: virtual void __thiscall ShapeBase::UpdateImageRaycastDamage(float,unsigned int)" (?UpdateImageRaycastDamage@ShapeBase@@MAEXMI@Z)	pathCamera.obj	
    Error	12	error LNK2001: unresolved external symbol "protected: virtual void __thiscall ShapeBase::UpdateImageRaycastDamage(float,unsigned int)" (?UpdateImageRaycastDamage@ShapeBase@@MAEXMI@Z)	player.obj	
    Error	13	error LNK2001: unresolved external symbol "protected: virtual void __thiscall ShapeBase::UpdateImageRaycastDamage(float,unsigned int)" (?UpdateImageRaycastDamage@ShapeBase@@MAEXMI@Z)	aiPlayer.obj	
    Error	14	error LNK2001: unresolved external symbol "protected: virtual void __thiscall ShapeBase::UpdateImageRaycastDamage(float,unsigned int)" (?UpdateImageRaycastDamage@ShapeBase@@MAEXMI@Z)	aiWheeledVehicle.obj	
    Error	15	error LNK2001: unresolved external symbol "protected: virtual void __thiscall ShapeBase::UpdateImageRaycastDamage(float,unsigned int)" (?UpdateImageRaycastDamage@ShapeBase@@MAEXMI@Z)	camera.obj	
    Error	16	error LNK2001: unresolved external symbol "protected: virtual void __thiscall ShapeBase::UpdateImageRaycastDamage(float,unsigned int)" (?UpdateImageRaycastDamage@ShapeBase@@MAEXMI@Z)	trackingCamera.obj

    Any suggestions? I'll try and figure it out, but this might have me stuck...
    #62
    02/17/2008 (9:20 pm)
    For those with 1.5.2,all the code changes are already in the source. so you ONLY need to do the script related changes, and add the trackingcamera.cc file to your project.
    #63
    06/19/2008 (1:59 am)
    I am useing 1.5.2 and Ramen-sama says i don't need to to changes, only add trackingcamera.cc

    where does this go??
    #64
    07/20/2008 (4:24 pm)
    I am trying to use this in TGEA 1.7.2 and almost got it in properly. Some of the code changes above (such as the console methods) already exist in my version, so it should be overlooked. I am almost done, but I am having a couple errors when I compile:

    ..\..\..\..\..\engine\source\T3D\gameConnection.cpp(393) : error C3861: 'isServerConnection': identifier not found
    ..\..\..\..\..\engine\source\T3D\gameConnection.cpp(396) : error C3861: 'GameSetCameraFov': identifier not found

    I am sure it is just a change in the identifier, but I am not sure what it should be changed to in order to work in TGEA. Anyone?
    #65
    07/20/2008 (4:40 pm)
    Got one:
    'isServerConnection' in TGEA should be 'isConnectionToServer'

    Anyone know what to change 'GameSetCameraFov' to for TGEA?
    #66
    07/20/2008 (5:15 pm)
    'GameSetCameraFov' in TGEA should be 'setControlCameraFov'.

    My script is compiling well now, but when I try to use it with another resource, I am getting an error about mCameraObject:

    ..\..\..\..\..\engine\source\T3D\moveList.cpp(67) : error C2065: 'mCameraObject' : undeclared identifier
    #67
    01/27/2009 (8:15 am)
    Got it working perfectly on TGEA 1.7.1. Moved the code into the workspace and modified the file paths in the includes. I also had to comment out glu.h.
    #68
    02/22/2009 (6:49 pm)
    starter.fps/server/scripts/camera.cs (22): Unable to instantiate non-conobject class TrackingCameraData.

    why???
    #69
    05/02/2009 (5:57 pm)
    I check the code and is already integrated in TGE 1.52, so i you can use the script part directly...
    #70
    10/15/2010 (3:40 am)
    Has anybody tried to set an AIPlayer as a camera object? When I do this, the object seems to stop interpolating ticks (both when I'm viewing through it, and even when I tab back into the regular player and look at the AIPlayer). Anyone have any ideas?
    #71
    10/15/2010 (7:29 am)
    So, I'm not sure if anyone's interested or will want to use their camera this way. I've basically made the cameraObject completely independent of the controlObject, and it's totally possible for any number of clients to observe through the same object. To avoid dodginess, I had to comment out the parts of GameConnection::read/writePacket which call write/readPacketData on the camera object - for some reason, this was screwing up interpolation.
    Page«First 1 2 3 4 Next»