Game Development Community

Prototyping a 3rd person targeting system

by Inflight · in Torque Game Engine · 08/02/2007 (12:20 pm) · 7 replies

I'm prototyping a 3rd person targeting system using the GuiShapeNameHud. It currently shows a damage bar when you target a player. And now I am trying to add a crosshair that will follow the end point of the muzzle raycast. It will basically move around on the players body when you have a player targeted. Btw, I plan on turning this into a resource when its finished.

My issue at this point is trying to covert the world points from the raycast for gunRay into 2d points to display a bitmap crosshair in the correct location on the players body. I'm close but I seemed to be over looking
something. I'm not looking for code just some suggestions or tell me were I am going wrong. I've spent a week searching the forums trying to find something simular to what I am doing with no luck.

I am testing this with the Fluid Camera Resource. So if you would like to play around with this code I
suggest adding the Fluid camera resource to get the full effect.
www.garagegames.com/index.php?sec=mg&mod=resource&page=view&qid=8413


#include "dgl/dgl.h"

#include "dgl/gFont.h"

#include "gui/core/guiControl.h"

#include "gui/core/guiTSControl.h"

#include "console/consoleTypes.h"

#include "sceneGraph/sceneGraph.h"

#include "game/shapeBase.h"

#include "game/gameConnection.h"

#include "terrain/terrData.h"

#include "interior/interior.h"

 

//----------------------------------------------------------------------------

/// Displays name & damage above shape objects.

///

/// This control displays the name and damage value of all named

/// ShapeBase objects on the client.  The name and damage of objects

/// within the control's display area are overlayed above the object.

///

/// This GUI control must be a child of a TSControl, and a server connection

/// and control object must be present.

///

/// This is a stand-alone control and relies only on the standard base GuiControl.

class GuiShapeNameHud : public GuiControl 

{

   typedef GuiControl Parent;

 

   // field data

   ColorF   mFillColor;

   ColorF   mFrameColor;

   ColorF   mTextColor;

 

   F32      mVerticalOffset;

   F32      mDistanceFade;

   bool     mShowFrame;

   bool     mShowFill;

 

   bool     mShowDamage;

   ColorF   mDamageFillColor;

   ColorF   mDamageFrameColor;

   Point2I  mDamageRectSize;

   Point2I  mDamageOffset;


   TextureHandle  mCrosshairBmp;

 

protected: 

   void drawName( Point2I offset, const char *buf, F32 opacity);

   void drawDamage(Point2I offset, F32 damage, F32 opacity);

   void drawCrosshair(Point2I offset);

 

public:

   GuiShapeNameHud();  // GuiControl


   virtual void onRender(Point2I offset, const RectI &updateRect);

 
   static void initPersistFields();

   DECLARE_CONOBJECT( GuiShapeNameHud );

};
 

   IMPLEMENT_CONOBJECT(GuiShapeNameHud);

//-----------------------------------------------------------------------------

 

/// Default distance for object's information to be displayed.

static const F32 cDefaultVisibleDistance = 500.0f;

 

GuiShapeNameHud::GuiShapeNameHud()

{

   mFillColor.set( 0.25, 0.25, 0.25, 0.25 );

   mFrameColor.set( 0, 1, 0, 1 );

   mTextColor.set( 0, 1, 0, 1 );

   mShowFrame = mShowFill = true;

   mShowDamage = true;

   mVerticalOffset = 0.5;

   mDistanceFade = 0.1;
 

   mDamageFillColor.set( 0, 1, 0, 1 );

   mDamageFrameColor.set( 1, 0.6, 0, 1 );

   mDamageRectSize.set(50, 4);

   mDamageOffset.set(0,32);   

}

 

void GuiShapeNameHud::initPersistFields()

{

   Parent::initPersistFields();

   addGroup("Colors");

   addField( "fillColor",  TypeColorF, Offset( mFillColor, GuiShapeNameHud ) );

   addField( "frameColor", TypeColorF, Offset( mFrameColor, GuiShapeNameHud ) );

   addField( "textColor",  TypeColorF, Offset( mTextColor, GuiShapeNameHud ) );

   endGroup("Colors");

#1
08/02/2007 (12:22 pm)
More...

addGroup("Misc");

   addField( "showFill",   TypeBool, Offset( mShowFill, GuiShapeNameHud ) );

   addField( "showFrame",  TypeBool, Offset( mShowFrame, GuiShapeNameHud ) );

   addField( "verticalOffset", TypeF32, Offset( mVerticalOffset, GuiShapeNameHud ) );

   addField( "distanceFade", TypeF32, Offset( mDistanceFade, GuiShapeNameHud ) );

   endGroup("Misc");

 

   addGroup("Damage");

   addField( "showDamage",   TypeBool, Offset( mShowDamage, GuiShapeNameHud ) );          

   addField( "damageFillColor", TypeColorF, Offset( mDamageFillColor, GuiShapeNameHud) );

   addField( "damageFrameColor", TypeColorF, Offset( mDamageFrameColor, GuiShapeNameHud) );

   addField( "damageRect", TypePoint2I, Offset( mDamageRectSize, GuiShapeNameHud) );

   addField( "damageOffset", TypePoint2I, Offset( mDamageOffset, GuiShapeNameHud) );

   endGroup("Damage");        

}



//----------------------------------------------------------------------------

/// Core rendering method for this control.

///

/// This method scans through all the current client ShapeBase objects.

/// If one is named, it displays the name and damage information for it.

///

/// Information is offset from the center of the object's bounding box,

/// unless the object is a PlayerObjectType, in which case the eye point

/// is used.

///

/// @param   updateRect   Extents of control.

void GuiShapeNameHud::onRender( Point2I, const RectI &updateRect)

{

   // Background fill first

   if (mShowFill)

      dglDrawRectFill(updateRect, mFillColor);

 

   // Must be in a TS Control

   GuiTSCtrl *parent = dynamic_cast<GuiTSCtrl*>(getParent());

   if (!parent) return;

 

   // Must have a connection and control object

   GameConnection* conn = GameConnection::getConnectionToServer();

   if (!conn)

      return;

 

   ShapeBase* control = conn->getControlObject();

   if (!control)

      return;

 

   // Get control camera info

   MatrixF cam;

   Point3F camPos;

   VectorF camDir;

   conn->getControlCameraTransform(0,&cam);

   cam.getColumn(3, &camPos);

   cam.getColumn(1, &camDir);

 

   F32 camFov;

   conn->getControlCameraFov(&camFov);

   camFov = mDegToRad(camFov) / 2;

 

   // Visible distance info & name fading

   F32 visDistance = gClientSceneGraph->getVisibleDistance();

   F32 visDistanceSqr = visDistance * visDistance;

   F32 fadeDistance = visDistance * mDistanceFade;

 

   // Collision info. We're going to be running LOS tests and we

   // don't want to collide with the control object.

   static U32 losMask = TerrainObjectType | InteriorObjectType | ShapeBaseObjectType;

   control->disableCollision();
#2
08/02/2007 (12:23 pm)
More...

// All ghosted objects are added to the server connection group,

   // so we can find all the shape base objects by iterating through

   // our current connection.

   for (SimSetIterator itr(conn); *itr; ++itr) 

   {

      if ((*itr)->getType() & ShapeBaseObjectType) 

      {

         ShapeBase* shape = static_cast<ShapeBase*>(*itr);

         if (shape != control && shape->getShapeName()) 

         {

 

            // Target pos to test, if it's a player run the LOS to his eye

            // point, otherwise we'll grab the generic box center.

            Point3F shapePos;

            if (shape->getType() & PlayerObjectType) 

            {

               MatrixF eye;

 

               // Use the render eye transform, otherwise we'll see jittering

               shape->getRenderEyeTransform(&eye);

               eye.getColumn(3, &shapePos);

            }

            else 

            {

                // Use the render transform instead of the box center

                // otherwise it'll jitter.

               MatrixF srtMat = shape->getRenderTransform();

               srtMat.getColumn(3, &shapePos);

            }

 

            VectorF shapeDir = shapePos - camPos;

 

            // Test to see if it's in range

            F32 shapeDist = shapeDir.lenSquared();

            if (shapeDist == 0 || shapeDist > visDistanceSqr)

               continue;

            shapeDist = mSqrt(shapeDist);

 

            // Test to see if it's within our viewcone, this test doesn't

            // actually match the viewport very well, should consider

            // projection and box test.

            shapeDir.normalize();

            F32 dot = mDot(shapeDir, camDir);

            if (dot < camFov)

               continue;

 

            // Test to see if it's behind something, and we want to

            // ignore anything it's mounted on when we run the LOS.

            RayInfo info;

            shape->disableCollision();

            ShapeBase *mount = shape->getObjectMount();

 

            if (mount)

               mount->disableCollision();

            bool los = !gClientContainer.castRay(camPos, shapePos, losMask, &info);

            shape->enableCollision();

            if (mount)

               mount->enableCollision();

 

            if (!los)

               continue;

 

            // Project the shape pos into screen space and calculate

            // the distance opacity used to fade the labels into the

            // distance.

            Point3F projPnt;

            shapePos.z += mVerticalOffset;

            if (!parent->project(shapePos, &projPnt))

               continue;

            F32 opacity = (shapeDist < fadeDistance)? 1.0:

               1.0 - (shapeDist - fadeDistance) / (visDistance - fadeDistance);

 

            // Render the shape's name

            drawName(Point2I((S32)projPnt.x, (S32)projPnt.y),shape->getShapeName(),opacity);

         }

      }

   }

   // Restore control object collision

   control->enableCollision();

 

 

  // 3rd Person Targeting

  // do a raycast from the muzzle. if we hit a player show a damage bar over 

  // the players head and track with crosshair.

 

  // Ripped code from Max Robinson's 3rd/1st Person Super Crosshair Resource

  // This has been heavily modified.

  // http://www.garagegames.com/index.php?sec=mg&mod=resource&page=view&qid=3533
#3
08/02/2007 (12:24 pm)
More...

// are we in 3rd person?

   if(!conn->isFirstPerson())

   {

     // MUZZLE RAYCAST

     MatrixF gun;

     Point3F gunPos, gunVec;

     conn->getControlObject()->getMuzzleTransform(0,&gun);

     gun.getColumn(3, &gunPos);

 

     gun.getColumn(1, &gunVec);

     Point3F gunEndPos = gunVec;

     gunEndPos *= gClientSceneGraph->getVisibleDistance();

     gunEndPos += gunPos;

 

     // disable collisions with control object

     control->disableCollision(); 

 

     RayInfo gunRay;

     gunRay.object = 0;

     gClientContainer.castRay(gunPos, gunEndPos, losMask, &gunRay);

 

     // CAMERA LOS VERIFICATION

     Point3F camVec;

     conn->getControlCameraTransform(0,&cam);

     cam.getColumn(3, &camPos);

     cam.getColumn(1, &camVec);

 

     RayInfo camRay;

     camRay.object = 0;

     gClientContainer.castRay(camPos, gunRay.point + camVec, losMask, &camRay);


     // we are done with the raycasting

     control->enableCollision(); 


     Point3F targetPoint = gunPos;

     bool hasObject = false;

     F32 dmgLevel = 1;

     

     Point3F crossHairPnt; 

     crossHairPnt.x = gunRay.point.x;

     crossHairPnt.y = gunRay.point.y;


     Point3F dist = (camRay.point - gunRay.point);

     if (dist.len() < 1) // allow some small error

     {

       if (ShapeBase* shape = dynamic_cast<ShapeBase*>(gunRay.object))

       {

           if(shape->getShapeName())

           {

                hasObject = true;

                dmgLevel = shape->getDamageValue();

 

                MatrixF eye;

                Point3F TargetshapePos;

 

                // Use the render eye transform, 

                // otherwise we'll see jittering O.o

                shape->getRenderEyeTransform(&eye);

                eye.getColumn(3, &TargetshapePos);

 

                Point3F projPnt;

                TargetshapePos.z += mVerticalOffset;

                if (!parent->project(TargetshapePos, &projPnt)){

                     return;

                }

            

                // if we have an object, render the hud part

                if (hasObject)

                {

                    if(mShowDamage){

                        drawDamage(Point2I((S32)projPnt.x, (S32)projPnt.y), shape->getDamageValue(), 1);

                        drawCrosshair(Point2I((S32)crossHairPnt.x, (S32)crossHairPnt.y));

                    }

                }

           }

         }

         else

             return;

     }

 }

 

   // Border last

   if (mShowFrame)

      dglDrawRect(updateRect, mFrameColor);

}
#4
08/02/2007 (12:25 pm)
Last part...

//----------------------------------------------------------------------------

/// Render object names.

///

/// Helper function for GuiShapeNameHud::onRender

///

/// @param   offset  Screen coordinates to render name label. (Text is centered

///                  horizontally about this location, with bottom of text at

///                  specified y position.)

/// @param   name    String name to display.

/// @param   opacity Opacity of name (a fraction).

void GuiShapeNameHud::drawName(Point2I offset, const char *name, F32 opacity)

{

   // Center the name

   offset.x -= mProfile->mFont->getStrWidth((const UTF8 *)name) / 2;

   offset.y -= mProfile->mFont->getHeight();

 

   // Deal with opacity and draw.

   mTextColor.alpha = opacity;

   dglSetBitmapModulation(mTextColor);

   dglDrawText(mProfile->mFont, offset, name);

   dglClearBitmapModulation();

}

 

//----------------------------------------------------------------------------

// Render Damagebar.

// Ripped from GuiCrossHairHud.cc

//----------------------------------------------------------------------------

void GuiShapeNameHud::drawDamage(Point2I offset, F32 damage, F32 opacity)

{

   mDamageFillColor.alpha = mDamageFrameColor.alpha = opacity;

 

   // Damage should be 0->1 (0 being no damage,or healthy), but

   // we'll just make sure here as we flip it.

   damage = mClampF(1 - damage, 0, 1);

 

   // Center the bar

   RectI rect(offset, mDamageRectSize);

   rect.point.x -= mDamageRectSize.x / 2;

 

   // Draw the border

   dglDrawRect(rect, mDamageFrameColor);

 

   // Draw the damage % fill

   rect.point += Point2I(1, 1);

   rect.extent -= Point2I(1, 1);

   rect.extent.x = (S32)(rect.extent.x * damage);

   if (rect.extent.x == 1)

      rect.extent.x = 2;

   if (rect.extent.x > 0)

      dglDrawRectFill(rect, mDamageFillColor);

}

 

//----------------------------------------------------------------------------

// Render Crosshair.

//----------------------------------------------------------------------------

void GuiShapeNameHud::drawCrosshair(Point2I offset)

{
   // TODO: add console methods

   mCrosshairBmp = TextureHandle("starter.fps/client/ui/crossHair", BitmapTexture, true);

 

   if(mCrosshairBmp){

     TextureObject* crossHair = (TextureObject *) mCrosshairBmp; 

     dglClearBitmapModulation();

 
     dglDrawBitmap(crossHair, Point2I(offset.x, offset.y), false);

   }

}
#5
08/02/2007 (6:25 pm)
Well, I figured out what I was doing wrong and as usual I over looked the obvious. I needed to use Project() and create a pointer using the camRay.point. I really need to cut back on the coffee...

Here is the magic.

I replaced this...

Point3F crossHairPnt; 
     crossHairPnt.x = gunRay.point.x;
     crossHairPnt.y = gunRay.point.y;

With this

Point3F crossHairPnt = camRay.point;
   Point3F projCrossHair;
   if (!parent->project(crossHairPnt, &projCrossHair)){
	 return;
   }


Of course, after I figured that out I ran into another snag. When I aimed the crosshair at the sky the game locked up on me. Doh!

Heh, easy fix really, just had to add EnvironmentObjectType to the losMask. Added the WaterObjectType just in case. Lockups suck ;)

static U32 losMask = TerrainObjectType | InteriorObjectType | ShapeBaseObjectType | 
	                    EnvironmentObjectType | WaterObjectType;

Ok, time to start working on the console methods for loading that bitmap and add some high lighting color to that crosshair.
#6
08/02/2007 (6:33 pm)
This looks good, look forward to the resource, i currently using fluidcamera as well, but a more accurate crosshair would be awesome.
#7
08/02/2007 (8:38 pm)
Thanks Ian,

Yeah, the cool part about this is you can use the stock GuiCrossHairHud for first person and use this resource for 3rd person. At least thats my plan anyway.

I think I'm going to add vertical and horizontal offsets so you can adjust the crosshair as well. But, this might be over kill, we will see.

Also plan to link a video showing it in action in the resource.

If you got any suggestion or "it would be cool if it did this" let me know.