Game Development Community

dev|Pro Game Development Curriculum

Multiple 3rd Person Cameras

by James Laker (BurNinG) · 04/18/2007 (1:55 pm) · 10 comments

I've whipped this up in a few minutes yesterday to start testing what camera position would be the best. I then realised that I'm actually going to leave them all in for the player to select.

At the moment your Player/Vehicle/Flyingvehicle shapes (DTS) has an Eye and a Cam node. The following code will extend on this basic concept, and add more Cam nodes (Cam1...CamN). Cam should stay in the model, and now when adding more start naming them from Cam1. I put a max of 10, as this should be enough. You can change this as you see fit.

This works with TGEA and TGE.

Let's get to it.
Open up ShapeBase.h change the following:
..
struct ShapeBaseData : public GameBaseData {
  private:
   typedef GameBaseData Parent;

public:
	//JLAKER - MultiCamPoints
   /// Various constants relating to the ShapeBaseData
   enum Constants {
      NumMountPoints = 32,
      NumMountPointBits = 5,
      MaxCollisionShapes = 8,
-      AIRepairNode = 31
+      AIRepairNode = 31,
+      NumCamPoints = 16
   };
	//JLAKER - MultiCamPoints

   bool shadowEnable;
..
NumCamPoints will specify how many cameras you will allow in the game.

A little further down still in ShapeBase.h:
..
   S32 eyeNode;                         ///< Shape's eye node index
+	//JLAKER - MultiCamPoints
-	S32 cameraNode                      ///< Shape's camera node index
+	S32 cameraNode[NumCamPoints];         ///< Shape's camera node index
+	int camLastNode;							  ///< Keep track of how many nodes there were when loaded.
+	//JLAKER - MultiCamPoints
   S32 shadowNode; 
..
Here you can see us changing the single CameraNode and turning it into an Array.

Still in ShapeBase.h change the declaration of getCameraTransform to accept the Camera Number.
..
-   virtual void getCameraTransform(F32* pos,MatrixF* mat);
+	//JLAKER - MultiCamPoints
+  virtual void getCameraTransform(F32* pos,MatrixF* mat,int camno);
+	//JLAKER - MultiCamPoints
..

We also need to know what the last camera position was, so down further add getLastExternalCam to the other camera functions:
..
   /// Sets the FOV for this object if used as a camera
   virtual void setCameraFov(F32 fov);

+	//JLAKER - MultiCamPoints
+	/// Returns the Last External Camera node
+	S32 getLastExternalCam();
+	//JLAKER - MultiCamPoints
..

Now let us move on to ShapeBase.cpp...
In the the ShapeBaseData constructor (ShapebaseData::ShapeBaseData) change the following:
..
   shadowNode = -1;
+	//JLAKER - MultiCamPoints
-   cameraNode = -1;
+	camLastNode = 0;
+	//JLAKER - MultiCamPoints
   damageSequence = -1;
..

Also a little further down, still in the contructor make all the nodes in the array -1:
..
   inheritEnergyFromMount = false;

+	//JLAKER - MultiCamPoints
+	//Default all External Camera nodes to -1
+	for (int i=0; i < NumCamPoints; i++) {
+		cameraNode[i] = -1;
+	}
+	//JLAKER - MultiCamPoints

   for(U32 j = 0; j < NumHudRenderImages; j++)
..

On to ShapeBase::preload. Here we load all the nodes from the Shape into the Array. Here you can also see that the first Cam Node that is missing will be the last Cam. It will ignore any cam after that.
..
+		//JLAKER - MultiCamPoints
-      cameraNode = shape->findNode("cam");
-      if (cameraNode == -1)
-         cameraNode = eyeNode;

+      cameraNode[0] = shape->findNode("cam");
+      if (cameraNode[0] == -1)
+         cameraNode[0] = eyeNode;

+		for (i=1; i < NumCamPoints; i++) {
+			char camName[256];
+         dSprintf(camName,sizeof(camName),"cam%d",i);
+			cameraNode[i] = shape->findNode(camName);
			
+			//Save last camera
+			if (cameraNode[i] == -1) 
+			{
+				camLastNode = i - 1;
+				break;
+			}
+		}
+		//JLAKER - MultiCamPoints
..

We have to add the method to get that last cam node:
..
void ShapeBase::setCameraFov(F32 fov)
{
   mCameraFov = mClampF(fov, mDataBlock->cameraMinFov, mDataBlock->cameraMaxFov);
}

+//JLAKER - multiCamPoints
+S32 ShapeBase::getLastExternalCam()
+{
+	return mDataBlock->camLastNode;
+}
+//JLAKER - multiCamPoints
..

We also need to change the getCameraTransform (still in ShapeBase.cpp):
..
- void ShapeBase::getCameraTransform(F32* pos,MatrixF* mat)
+ void ShapeBase::getCameraTransform(F32* pos,MatrixF* mat,int camno)
..

And down a little more still in ShapeBase::getCameraTransform, use we specify which Camera to use:
..
      // Use the camera node's pos.
      Point3F osp,sp;

+		/*JLAKER - MultiCamPoints
-      if (mDataBlock->cameraNode != -1) {
-         mShapeInstance->mNodeTransforms[mDataBlock->cameraNode].getColumn(3,&osp);

+      if (mDataBlock->cameraNode[camno] != -1) {
+         mShapeInstance->mNodeTransforms[mDataBlock->cameraNode[camno]].getColumn(3,&osp);
+		//JLAKER - MultiCamPoints

         // Scale the camera position before applying the transform
         const Point3F& scale = getScale();
..
[code]

Now we move on to the [b]GameConnection.h[/b]:
[code]
..
+	//JLAKER - MultiCamPoints
+	F32   mCamNo;				///< Decide which 3rd Person Cam to use
+	bool  mUpdateCamNo;		///< Set to notify client or server of camera number change
+	//JLAKER - MultiCamPoints
   bool  mFirstPerson;     ///< Are we currently first person or not.
   bool  mUpdateFirstPerson; ///< Set to notify client or server of first person change.
..

and add the public functions (still in GameConnection.h):
..
   void setFirstPerson(bool firstPerson);
+	//JLAKER - MultiCamPoints
+	void setCamNo(int camNo);
+	int getLastExternalCam();
+   //JLAKER - MultiCamPoints
..

On to GameConnection.cpp, change the contructor so it always defaults to the first cam node:
// first person
   mFirstPerson = true;
   mUpdateFirstPerson = false;

+	//JLAKER - MultiCamPoints
+	mCamNo = 0;
+	mUpdateCamNo = false;
+	//JLAKER - MultiCamPoints

Now in GameConnection::getControlCameraTransform we get the Cam based on the selected CamNo:
..
   if (!sChaseQueueSize || mFirstPerson || obj->onlyFirstPerson())
+		//JLAKER - MultiCamPoints
-		obj->getCameraTransform(&mCameraPos,mat);
+      obj->getCameraTransform(&mCameraPos,mat,mCamNo);
+	//JLAKER - MultiCamPoints
   else 
   {
      MatrixF& hm = sChaseQueue[sChaseQueueHead];
      MatrixF& tm = sChaseQueue[sChaseQueueTail];
+		//JLAKER - MultiCamPoints
+      obj->getCameraTransform(&mCameraPos,&hm,mCamNo);
-		obj->getCameraTransform(&mCameraPos,&hm);
+		//JLAKER - MultiCamPoints
      *mat = tm;
      if (dt) 
..

Again with the other Camera methods, add the new methods:
..
void GameConnection::setFirstPerson(bool firstPerson)
{
   mFirstPerson = firstPerson;
   mUpdateFirstPerson = true;
}

+ //JLAKER - MultiCamPoints
+int GameConnection::getLastExternalCam()
+{
+   ShapeBase* obj = getCameraObject();
+   if(!obj)
+      return 0;
+
+	return obj->getLastExternalCam();
+}
+
+void GameConnection::setCamNo(int Camno)
+{
+   mCamNo = Camno;
+	mUpdateCamNo = true;
+}
+//JLAKER - MultiCamPoints
..

On to the Networking side of things (Yep... still in GameConnection.cpp). In GameConnection::writeDemoStartBlock add the following:
..
   stream->writeFlag(false);
+	//JLAKER - MultiCamPoints
+	stream->write(mCamNo);
+	//JLAKER - MultiCamPoints
   stream->write(mFirstPerson);
   stream->write(mCameraPos);
   stream->write(mCameraSpeed);
..

We then have to do the same in GameConnection::readDemoStartBlock:
..
+	//JLAKER - MultiCamPoints
+	stream->read(&mCamNo);
+	//JLAKER - MultiCamPoints
   stream->read(&mFirstPerson);
   stream->read(&mCameraPos);
   stream->read(&mCameraSpeed);
..

When we make a change to the Camera we need to notify the client (GameConnection::writePacket):
..
      moveWritePacket(bstream);

+		//JLAKER - MultiCamPoints
+      if(bstream->writeFlag(mUpdateCamNo)) 
+      {
+			bstream->writeInt(mCamNo,8);
+         mUpdateCamNo = false;
+      }
+		//JLAKER - MultiCamPoints

      // first person changed?
      if(bstream->writeFlag(mUpdateFirstPerson)) 
      {
         bstream->writeFlag(mFirstPerson);
         mUpdateFirstPerson = false;
      }
..

And still in GameConnection:WritePacket a little further down:

..
      else
         bstream->writeFlag( false );

+		//JLAKER - MultiPointCam
+		if(bstream->writeFlag(mUpdateCamNo)) {
+			bstream->writeInt(mCamNo,8);
+         mUpdateCamNo = false;
+      }
+		//JLAKER - MultiPointCam

      // first person changed?
      if(bstream->writeFlag(mUpdateFirstPerson)) {
         bstream->writeFlag(mFirstPerson);
         mUpdateFirstPerson = false;
      }
..

Now in we also have to read those packets (GameConnection::readPacket):
..
      }
      else
         setCameraObject(0);

+		//JLAKER - MultiCamPoints
+		if(bstream->readFlag())
+		{
+			setCamNo(bstream->readInt(8));
+			mUpdateCamNo = false;
+		}
+		//JLAKER - MultiCamPoints

      // server changed first person
      if(bstream->readFlag()) {
         setFirstPerson(bstream->readFlag());
         mUpdateFirstPerson = false;
      }
..

And again a little down more in GameConnection::Readpacket:
..
      bstream->read(&mLastControlObjectChecksum);
      moveReadPacket(bstream);

+		//JLAKER - MultiCamPoints
+      // client Cam No Changed
+      if(bstream->readFlag()) {
+         setCamNo(bstream->readInt(8));
+         mUpdateCamNo = false;
+      }
+		//JLAKER - MultiCamPoints

      // client changed first person
      if(bstream->readFlag()) {
         setFirstPerson(bstream->readFlag());
         mUpdateFirstPerson = false;
      }
..

And now lastly in Gameconnection.cpp add this Method right at the bottom. Here you can see the Method returning a True or false... It will return True if the last Cam Node was reached. Then we can update the Counter in the Scripting side back to Zero:
..
+//JLAKER - MultiCamPoints
+ConsoleMethod(GameConnection, setCamNo, bool, 3, 3, "(bool SetCamNo) Change the 3rd Person cam +(camN). It returns True when last cam node is reached.")
+{
+	//Set the camera
+	if (dAtoi(argv[2]) <= object->getLastExternalCam())
+		object->setCamNo(dAtoi(argv[2]));
+
+	//Return True if we are at last number
+	if (dAtoi(argv[2]) >= object->getLastExternalCam())
+		return true;
+	else
+		return false;
+}
+//JLAKER - MultiCamPoints

On to Camera.h we need to change the Method Declaration for getCameraTransform:
..
-void Vehicle::getCameraTransform(F32* pos,MatrixF* mat)
+void Vehicle::getCameraTransform(F32* pos,MatrixF* mat,int camno)
..

And in Camera.cpp we change the function:
..
- void Camera::getCameraTransform(F32* pos, MatrixF* mat)
+ void Camera::getCameraTransform(F32* pos, MatrixF* mat,int camno)
{
   // The camera doesn't support a third person mode,
   // so we want to override the default ShapeBase behavior.
   ShapeBase * obj = dynamic_cast<ShapeBase*>(static_cast<SimObject*>(mOrbitObject));
   if(obj && static_cast<ShapeBaseData*>(obj->getDataBlock())->observeThroughObject)
-      obj->getCameraTransform(pos, mat);
+      obj->getCameraTransform(pos, mat, camno);
   else
..

If you need this for any Vehicle derived class we need to change the vehicle.h getCameraTransform declaration too:
..
-   void getCameraTransform(F32* pos, MatrixF* mat);
+	void getCameraTransform(F32* pos, MatrixF* mat,int camno);
..

And off course change the getCameraTransform method in vehicle.cpp:
..
+//JLAKER - MultiCamNo
-void Vehicle::getCameraTransform(F32* pos,MatrixF* mat)
+void Vehicle::getCameraTransform(F32* pos,MatrixF* mat,int camno)
{
+//JLAKER - MultiCamNo

   // Returns camera to world space transform
   // Handles first person / third person camera position
   if (isServerObject() && mShapeInstance)
      mShapeInstance->animateNodeSubtrees(true);
..

and this (still in getCameraTransform of Vehicle):
..
+		//JLAKER - MultiCamPoints
-		if (mDataBlock->cameraNode != -1) 
-		{
-			mShapeInstance->mNodeTransforms[mDataBlock->cameraNode].getColumn(3,&osp);
+		if (mDataBlock->cameraNode[camno] != -1) 
+		{
+			mShapeInstance->mNodeTransforms[mDataBlock->cameraNode[camno]].getColumn(3,&osp);
+		//JLAKER - MultiCamPoints
		
			getRenderTransform().mulP(osp,&sp);
		}
		else
			eye.getColumn(3,&sp);
..

That's it for Engine Changes... Do a Full Rebuild and pray there are no errors ;-)

Now for the scripting side. Open up starter.x\client\scripts\default.bind.cs and add the following:
..
+$CamNo = 0;
+function toggleCamNo(%val)
+{
+   if (%val)
+   {
+      echo("Changing $CamNo to " @ $CamNo);
+      $CamNo = $CamNo + 1;
+      if (ServerConnection.setCamNo($CamNo))
+      {
+        $CamNo = 0;
+      }
+   }
+}
+moveMap.bind(keyboard, c, toggleCamNo );
Please make sure you dont have "c" bound to something different by default.

An optional part is adding it to the Options->Keyboard settings. To do this open starter.x\client\scripts\optionsDlg.cs and scroll down to the part where you should this:
..
$RemapName[$RemapCount] = "Toggle Camera No";
$RemapCmd[$RemapCount] = "toggleCamNo";
$RemapCount++;
..

That should allow the player to bind a key to the Cam No Toggle in the Options Menu.

Now I hope this helps your game. Please rate the resource and if you have any questions drop em here.
Njoy!

Delete *.dso and run the game... press "c" to jump through the cameras!

#1
04/18/2007 (1:55 pm)
Very cool
#2
04/18/2007 (3:20 pm)
Talk about useful!!! Thanks!

And you whipped this up in a few minutes? I'm impressed!

Though you Torquers never cease to amaze me! :-)

- Ed Johnson
#3
04/19/2007 (1:21 am)
Hehe... took about 20min... One of those features you code and everything just works first time (touch wood).
#4
07/07/2007 (1:19 pm)
After I fixed all my typos I still had one problem...

Using TGE 1.5.2 on windows w/ VS 2005 Pro I get an error in rigidshape.cc in this segment

// Use the camera node as the starting position if it exists.
   Point3F osp,sp;
   //// GP - CHANGED THIS from cameraNode != -1 to an array...since it is an array.
   ////      MUST figure out how to determine the array index..
   if (mDataBlock->cameraNode[0] != -1) 
   {
      mShapeInstance->mNodeTransforms[mDataBlock->cameraNode[0]].getColumn(3,&osp);
      getRenderTransform().mulP(osp,&sp);
   }

I know that the problem is that the mDataBlock->cameraNode is not an integer but an array of S32...I'm just not sure how to figure out which index to use...so I used index 0...which should correspond to the eyeNode...it compiles w/o errors, but...

Any suggestions on the correct way to determine which index to use in this location? I suspect that mDataBlock->camLastNode might play a part here, but I really don't know.
#5
07/09/2007 (12:39 am)
Hi, I'm not 100% ure since I dont use TGE anymore and rigidshape is not included in TGEA.

What you should use though is mCamNo declared in GameConnection. I didn't create a Get Method for GameConnection, but try to use this:

Where you added this to GameConnection.h
//JLAKER - MultiCamPoints
	void setCamNo(int camNo);
	int getLastExternalCam();
   //JLAKER - MultiCamPoints
add this line:
F32 GameConnection::getCamNo()

Around line 560 in GameConnection.cpp:
F32 GameConnection::getCamNo()
{
   return mCamNo;
}

Then in Rigidshape.cpp see if the Gameconnection.h is included (at the top of the file). It should look like this:
#include "game/gameConnection.h"

Now try using this new get(ter) method to get the current camera (node) number. If you're not able to complete it, please mail me the rigidshape.cpp/.h.

Can you tell me where my typos are, so I can update the resource?
#6
07/04/2008 (11:52 am)
Aghhhh.. don't work on TGE 1.5.2
#7
07/07/2008 (1:01 am)
@David: Where are you getting an error? Have you tried stepping through the code?
#8
08/01/2008 (4:50 pm)
bro i cant see on my camera.h>> void Vehicle::getCameraTransform(F32* pos,MatrixF* mat,int camno)

i see >> void getCameraTransform(F32* pos,MatrixF* mat);

are you know it true?my engine Clean i İnstall İt New***
#9
08/01/2008 (5:08 pm)
in Tge.1.5.2 i see this error
What is This?
Error 1 error C2511: 'void Camera::getCameraTransform(F32 *,MatrixF *,int)' : overloaded member function not found in 'Camera' c:\Documents and Settings\Administrator\Desktop\TGE_1_5_2_0\engine\game\camera.cc 117
#10
08/01/2008 (5:30 pm)
@Greg i see your post that's worked Man ! Ty ;)