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 «Previous 1 2 3 4 Last »
    #1
    10/28/2003 (6:49 pm)
    This looks good! Haven't had a chance to try it, but i'm bookmarking it :) It looks good, and CLEAN which is important to me. Our design calls for several cameras so this will probably be of use.

    I also like this because the very clean approach has given me a better understanding of the engine in general. Thanks!
    #2
    11/12/2003 (4:28 pm)
    The download link says that the file (4720.trackingcamera.zip) doesn't exist . . .
    #3
    11/12/2003 (8:16 pm)
    Don't know what happened there. I uploaded it again and it seems to work now.
    #4
    11/14/2003 (9:51 am)
    Yeah, works now. Thanks!
    #5
    11/17/2003 (11:11 pm)
    Wow, great ressource and very useful!! Thanks Cory!
    #6
    11/17/2003 (11:29 pm)
    oups, having a problem. When the tracked object gets out of the visible distance, the engine crashes. Maybe I did something wrong?
    #7
    11/18/2003 (1:14 am)
    Okay, I tried to figure out what was happening and it seems to come from the scope. In shapeBase.cc, in ShapeBase::onCameraScopeQuery:
    if(sky)
        query->visibleDistance = sky->getVisibleDistance();
    changed to:
    if(sky)
        query->visibleDistance = (sky->getVisibleDistance() * 2);
    gives twice the distance before the engine crash and the visible distance for the objects (diffs etc.) still works like before. (the visible distance rendering the objects is not doubled)

    Now, since I'm not c++ programmer, I'm really not sure what effects this could have on other things :(. Also, in our game, the player can go infinite distance in the world, so doubling or even multiplying by 10 could result in an engine crash.

    Does anyone know if there is a better way to fix this?

    P.S. in shapeBase.cc, just a few lines below the one I mentioned above:
    // First, we are certainly in scope, and whatever we're riding is too...
       cr->objectInScope(this);

    Yeah, but what to do if we're not in scope?!?
    #8
    11/21/2003 (6:56 am)
    Cory,

    I tried out this resouce and it was really cool. Although my game don't need it, it actually thought me something. :)

    Thanks.

    Alex
    #9
    11/22/2003 (12:11 am)
    Yes I have implemented it as well. I am very thankful you wrote this! I am about to embark on either creating my own camera class for a Dungeon Seige style camera. Any suggestions, anyone? There a few ways of handling this, and I was wondering what everyone thought was best.

    I should expose the vars for speed to the console.
    I should bind the keys and mouse movements I want to use in the console bind commands.

    In those functions I can either call a rotation matrix on the object itself, or modify a variable and allow the camera to update itself on the Process Tick method.

    What do you guys think is the most "Torquest", and cleanest way to do that? I want to work with the way the engine is designed, not against it.

    Also, will I need to modify the write packet data / read packet data functions?
    #10
    12/22/2003 (2:21 pm)
    well, i am having one small issue someone may be able to help with. i have the tracking camera set up as the "contol object" in server\scripts\game.cs in the GameConnection::createPlayer function a follows:

    %this.setControlObject(%this.trackCamera);

    instead of the player as the control object (ex: %this.setControlObject(%player); )

    click to move and everything is working except i am unable to see the guihealthbarhud (and the crosshair hud) with the control object set to the trackCamera

    Anyone seen this problem before, or have any input?
    #11
    01/04/2004 (10:50 pm)
    Very nice. I found this not only useful, but educational as well. Thanks again!
    #12
    01/10/2004 (11:57 pm)
    I'm glad people have liked this. Sorry I didn't respond sooner to those who had problems/questions. The last couple of months of been very busy with the holidays and my full-time job. So I didn't get to spend any time working with Torque. Here are some responses - hopefully not too late :)

    @Eric - I haven't tested this, but I think if you modified the TrackingCamera class and had it create its own implementation of onCameraScopeQuery, you could ensure that the tracked object is always in scope as well. ShapeBase does this same thing with a mount object.

    @Cameron - I think I've already done something similar, but the code changes are spotted all over the place which makes it hard to write up. What I did was create a second MoveManager specifically for the camera. To do so, I basically just went through all the GameConnection code that dealt with MoveManager and duplicated it for a new CameraMoveManager. There are 6 files you'd have to touch - MoveManager.h, GameConnectionMoves.cc, GameConnection.h, GameConnection.cc, GameProcess.cc, and main.cc.

    With the GUI problems, just looked at guiHealthBarHud, it doesn't render when a Player object type is not set as the ControlObject:
    void GuiHealthBarHud::onRender(Point2I offset, const RectI &updateRect)
    {
       // Must have a connection and player control object
       GameConnection* conn = GameConnection::getServerConnection();
       if (!conn)
          return;
       ShapeBase* control = conn->getControlObject();
       if (!control || !(control->getType() & PlayerObjectType))
          return;
    ...
    #13
    01/11/2004 (2:22 am)
    Damn nice, the only camera resource that actually works on the new CVS! It doesnt do what I need, but at least its nice to see that you CAN change the camera.. lol
    #14
    01/11/2004 (11:40 am)
    Just updated the attached TrackingCamera class to override ShapeBase::onCameraScopeQuery so that the tracked object would always be scoped to the client:

    void TrackingCamera::onCameraScopeQuery(NetConnection *cr, CameraScopeQuery * query) {
       if (mTrackObject)
          cr->objectInScope(mTrackObject);
       Parent::onCameraScopeQuery(cr, query);
    }

    I've tested this with a different camera I'm using and it seems to solve some choppiness I had when the tracked object was inside an interior and the camera was outside. Not really something you'd want to happen in the first place, but at least its smooth now.
    #15
    01/20/2004 (3:31 am)
    Thanks Cory, I've done a mod which prevents it from crashing. I just reset the trackcam position if it reaches the max visible distance, was working great.

    in void TrackingCamera::setPosition(const Point3F& pos)
    Where there's the comment //mod it's what I did, from what I remember ;-p
    MatrixF transMat;
       ShapeBase* obj = dynamic_cast<ShapeBase*>(static_cast<SimObject*>(mTrackObject));
    
       U32 visibleDistance = 0; //mod
       Sky * sky = gClientSceneGraph->getCurrentSky(); //mod
       if(sky) //mod
    	   visibleDistance = sky->getVisibleDistance(); //mod
         
       // set rotation to point at the tracked object
       if (obj) {
          Point3F objPos;
          Point3F newPos;
    	  getLookAtPos(&objPos);
          VectorF dirVec = objPos - pos;
          if(dirVec.len() >= visibleDistance){ //mod
    		  newPos.x = objPos.x + 20; //mod
    		  newPos.y = objPos.y - 10; //mod 
    		  newPos.z = objPos.z + 35; //mod
    		  setPosition(newPos); //mod
    		  Con::printf("Reached maximum distance from camera, resetting camera position"); //mod
    		  return; //mod
          }
    ...
    #16
    01/23/2004 (3:45 pm)
    how exactly do you you implement this, do you need to call it from the console? if so what will the call look like
    #17
    01/23/2004 (3:52 pm)
    I am very new to TGE, but I have been able to implement this cool resource but how exactly do I get to use it in the demo, I mean what steps do I take to see the camera working,so far nothing happens, I know that I am doing something wrong, but I really need to use cameras that can track a player without following the palyer behind, instead they orbit to keep the player in view, sort of like a camera with no translation, but it can orbit in order to track the player.

    thanks

    Emmanuel
    #18
    01/23/2004 (10:06 pm)
    @Emmanuel

    There are a number of reasons it might not be working - you could be trying the paths for fps or starter.fps and then still running the demo game. It could be as simple as toggling out of first-person mode (tab key in the demo and fps mods). Really hard to say.

    With the way I wrote it, you should see it working as soon as you enter the mission and/or leave first-person mode.

    As far as how you want your camera to work. Use the TrackingCamera attached here and the other Camera classes in Torque as a basis for writing your own. I don't use the attached camera class in my game, but it has a lot of the same principles. I'd love to be able to post my actual camera here to show some orbitting modes, but its tied too deeply into other engine modifications I've done to be able send the camera separate Moves and the Move structures themselves have been modified to allow me to send change mode/target commands to the camera and the player.

    Cory
    #19
    01/25/2004 (11:25 am)
    thanks cory,
    that pointed me in the right direction, it was as simple changing the path for the demo mod to the starter.fps mod.
    by the way why wont't it work in the demo mod, I made the same changes in both mods.

    also if I wanted to use a separate object say like a box object as a marker location for the tracking camera to be position at, where is the best place to hook in. The idea is to use a trigger to switch from the normal 3rd mode to the tracking camera. useful for having a corridor or room camera. can the normal 3rd person camera be switched e.g when the player leaves the trigger area

    thanks

    Emmanuel
    #20
    01/26/2004 (3:44 pm)
    I haven't look too closely at the actual scripts in the demo mod, so I can't say what would need to be changed in there with much accuracy :) I'll probably go back and do some edits to the resource so it better reflects the new mods in the head.

    Here is how I would go about having the camera operate in the manner you described. First, figure out how you can add multiple cameras to the mission - right now when I add the basic camera to a mission from the editor, it gives a console error about not being able to register the object. I'm sure that can be worked around, I just haven't spent too much time with it to figure out what I'm missing. From there, add a camera that is oriented in the direction you want, it doesn't necessarily have to be the TrackingCamera, it can just be the base camera. Finally set up your trigger so that when the player enters the area in question, you simply have the script calls the setCameraObject for your camera. When you leave the area, have it call clearCameraObject - when there's no camera object set, it will revert back to the standard third person camera for the player.
    Page «Previous 1 2 3 4 Last »