Game Development Community

Object selection in Torque (v1.2 onward)

by Dave Myers · 03/12/2005 (11:48 pm) · 241 comments

Overview

I originally wrote this tutorial back in Feb 2002 and I still receive emails on this asking for help or just saying thanks. It's very rewarding to know that in some small way this resource helps people new to the Torque Engine. This version has been updated to work with the latest release of TGE, so hopefully it will help answer many of the questions that people had with the original. -Dave

This tutorial provides one method to enable object selection in your TGE-based game. This implementation will show how to implement a bounding box cursor that will be displayed when an object is selected with the mouse. It could easily be enhanced to allow you to implement a wide variety of features.

Using this Tutorial
Bold font is used to show new code in the different code snippets throughout the tutorial. Also, code that is not important has been left out and is denoted by a comment in italics reading "irrelevant code not shown for brevity".

Step 1 - Capture the mouseDown event
First, let's quickly create a new bind and client function that will toggle the cursor. Add the following code to the bottom of the script file.

/starter.fps/client/scripts/default.bind.cs
[b]moveMap.bind(keyboard, "m",  "toggleMouseLook");

function toggleMouseLook(%val)
{
   if(%val)
   {
      if(Canvas.isCursorOn())
         CursorOff();
      else
         CursorOn();
   }
}[/b]

NOTE: Make sure and delete the current config.cs and config.cs.dso files found in /starter.fps/client. This file (config.cs) is created automatically each time you shutdown the game and is executed when starting up the game.

Second, let's capture any left button mouse click events on the client. To do that, we need to add in a method to the GameTSCtrl. This control is our canvas in the game - so clicking somewhere on this canvas (i.e. our game screen) will generate a mouseDown event, and we can capture that event and deal with it in GameTSCtrl.

I chose to add new members and "getter" methods for the point and vector to the control rather than pass the information back to the client scripts as parameters. Either way seems fine, I just chose the former.

engine/game/gameTSCtrl.h
class GameTSCtrl : public GuiTSCtrl
{
private:
   typedef GuiTSCtrl Parent;

[b]   // object selection additions
   Point3F mMouse3DVec;
   Point3F mMouse3DPos; [/b]

public:
   GameTSCtrl();

   bool processCameraQuery(CameraQuery *query);
   void renderWorld(const RectI &updateRect);

   void onMouseMove(const GuiEvent &evt);
   void onRender(Point2I offset, const RectI &updateRect);

[b]   // object selection additions
   void onMouseDown(const GuiEvent &evt); //left-mouse click
   Point3F getMouse3DVec() {return mMouse3DVec;};
   Point3F getMouse3DPos() {return mMouse3DPos;};[/b]
 
   DECLARE_CONOBJECT(GameTSCtrl);
};

Now let's add the implementation of the onMouseDown handler method. What we need to do is take the 2D screen coordinates of the mouse and create 3D screen coordinates. We then will create a vector that has the camera world coordinates as the first point and the 3D screen coordinates as the second point. Once we have calculated these two elements, we then call a client script function, which I also named onMouseDown, and let the scripts control it from there. The idea here is that later we will take the 3D screen coordinates and the vector and shoot a picking ray into our 3D world. We'll then check to see if that ray collides with an object in the world.

I believe the code is commented pretty well, so it should be easy to follow along.

engine/game/gameTSCtrl.cc
[b]//--------------------------------------------------------------------------
// object selection additions
//--------------------------------------------------------------------------
void GameTSCtrl::onMouseDown(const GuiEvent &evt)
{
   MatrixF mat;
   Point3F vel;

   if ( GameGetCameraTransform(&mat, &vel) )
   {
      //get the camera position
      Point3F pos;
      mat.getColumn(3,&pos);

      //take our mouse coordinates and create (x,y,z) screen coordinates
      Point3F screenPoint(evt.mousePoint.x, evt.mousePoint.y, -1);

      //take our screen coordinates and get the corresponding
      //world coordinates (this is what unproject does for us)
      Point3F worldPoint;

      if (unproject(screenPoint, &worldPoint))
      {
         mMouse3DPos = pos;

         //create a vector that points from our starting point (the
         //camera position) and heads towards our point we have chosen
         //in the world

         mMouse3DVec = worldPoint - pos;
         mMouse3DVec.normalizeSafe();

         //call client script handler
         Con::executef(this, 1, "onMouseDown");
      }
   }
}[/b]

I then exposed the "getter" methods to the scripting engine, also in engine/game/gameTSCtrl.cc
[b]ConsoleMethod( GameTSCtrl, getMouse3DVec, const char*, 2, 2, "()")
{
   char* retBuffer = Con::getReturnBuffer(256);
   const Point3F &vec = object->getMouse3DVec();

   dSprintf(retBuffer, 256, "%g %g %g", vec.x, vec.y, vec.z);

   return retBuffer;
}

ConsoleMethod( GameTSCtrl, getMouse3DPos, const char*, 2, 2, "()")
{
   char* retBuffer = Con::getReturnBuffer(256);
   const Point3F &pos = object->getMouse3DPos();

   dSprintf(retBuffer, 256, "%g %g %g", pos.x, pos.y, pos.z);

   return retBuffer;
}[/b]

Currently, the Torque demo has a GUI element called GuiShapeNameHud that is used to display player names and damage over the heads of the models. This control is actually almost the same size as the canvas itself, which means that any mouse events are actually handled by this control, not our GameTSCtrl. I preferred to keep the control, but modify it so that it will call to my new mouse down event handler in GameTSCtrl. To do that I inserted the following onMouseDown handler, which simply calls up to its parent - in this case, our GameTSCtrl.

engine/game/fps/guiShapeNameHud.cc
class GuiShapeNameHud : public GuiControl {
   typedef GuiControl Parent;

   // field data
   ColorF   mFillColor;
   ColorF   mFrameColor;
   ColorF   mTextColor;

   F32      mVerticalOffset;
   F32      mDistanceFade;
   bool     mShowFrame;
   bool     mShowFill;

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

public:
   GuiShapeNameHud();

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

[b]   // object selection additions
   virtual void onMouseDown(const GuiEvent &evt);[/b]

   static void initPersistFields();
   DECLARE_CONOBJECT( GuiShapeNameHud );
};

[i] // irrelevant code not shown for brevity[/i]

void GuiShapeNameHud::initPersistFields()
{
   Parent::initPersistFields();
   addField( "fillColor", TypeColorF, Offset( mFillColor, GuiShapeNameHud ) );
   addField( "frameColor", TypeColorF, Offset( mFrameColor, GuiShapeNameHud ) );
   addField( "textColor", TypeColorF, Offset( mTextColor, GuiShapeNameHud ) );

   addField( "showFill", TypeBool, Offset( mShowFill, GuiShapeNameHud ) );
   addField( "showFrame", TypeBool, Offset( mShowFrame, GuiShapeNameHud ) );
   addField( "verticalOffset", TypeF32, Offset( mVerticalOffset, GuiShapeNameHud ) );
   addField( "distanceFade", TypeF32, Offset( mDistanceFade, GuiShapeNameHud ) );
}


[b]//--------------------------------------------------------------------------
// object selection additions
//--------------------------------------------------------------------------
void GuiShapeNameHud::onMouseDown(const GuiEvent &evt)
{
   // Let's let the parent execute its event handling (if any)
   GuiTSCtrl *parent = dynamic_cast<GuiTSCtrl*>(getParent());

   if (parent)
      parent->onMouseDown(evt);
}[/b]

/**
   Core render method wich does all the work.
   This method scans through all the current client ShapeBase objects
   and if they are named, displays their name and damage value. If the
   shape is a PlayerObjectType then values are displayed offset from it's
   eye point, otherwise the bounding box center is used.
*/
void GuiShapeNameHud::onRender( Point2I, const RectI &updateRect)
{

[i] // irrelevant code not shown for brevity[/i]

Step 2 - Client handler for mouse event
We now need to add a handler to the scripts for PlayGui, which is the script name of our canvas GameTSCtrl. This new method will grab the vector and point and call to a server script function SelectObject.

example/starter.fps/client/scripts/playGui.cs
[b]//-----------------------------------------------------------------------------
// object selection additions
//-----------------------------------------------------------------------------
function PlayGui::onMouseDown(%this)
{
   // mouseVec = vector from camera point to 3d mouse coords (normalized)
   %mouseVec = %this.getMouse3DVec();

   // cameraPoint = the world position of the camera
   %cameraPoint = %this.getMouse3DPos();

   commandToServer('SelectObject', %mouseVec, %cameraPoint);
}
[/b]

Step 3 - Set up our connection to handle our selected object

Before we can actually select an object, we need to set aside a place to save this information, and provide a way for the server to update the client so that we can render (at least in this example) a bounding box cursor around our selected object. I have chosen to save this information as part of the GameConnection class.

engine/game/gameConnection.h
class GameConnection : public NetConnection
{

[i]// irrelevant code not shown for brevity[/i]

private:
   /// @name Move Packets
   /// Write/read move data to the packet.
   /// @{

   void moveWritePacket(BitStream *bstream);
   void moveReadPacket(BitStream *bstream);
   /// @}

[b]   ///object selection additions
   SimObjectPtr<ShapeBase> mSelectedObj;//pointer to our selected object
   bool mChangedSelectedObj; //flag used to determine if we have changed our selected object[/b]

public:

[b]   ///object selection additions
   void setSelectedObject(ShapeBase *so) { mSelectedObj = so; mChangedSelectedObj = true; }
   ShapeBase* getSelectedObject()  { return  mSelectedObj; }
   bool hasChangedSelectedObject() { return mChangedSelectedObj; }[/b]
     
   /// @name Protocol Versions
   ///
   /// Protocol versions are used to indicated changes in network traffic.
   /// These could be changes in how any object transmits or processes
   /// network information. You can specify backwards compatability by
   /// specifying a MinRequireProtocolVersion.  If the client
   /// protocol is >= this min value, the connection is accepted.
   ///
   /// Torque (V12) SDK 1.0 uses protocol  =  1
   ///
   /// Torque SDK 1.1 uses protocol = 2
   /// @{
   static const U32 CurrentProtocolVersion;
   static const U32 MinRequiredProtocolVersion;
   /// @}

We'll need to pass the selected object around between the server and client, so in order to do so we need to modify the readPacket and writePacket methods of our GameConnection class. For a brief description of why, take a look at Justin Mette's tutorial on node hiding, starting with Step 2 - Updating ghosts. Basically, we will be saving off the selected object on the server, but our client needs to know what object is selected so that it can render the cursor around the ghost object.

We also need to create the "getter/setter" functions so we can get and set the selected object from the server scripts.

engine/game/gameConnection.cc
GameConnection::GameConnection()
{
[i]// irrelevant code not shown for brevity[/i]
  
   //blackout vars
   mBlackOut = 0.0f;
   mBlackOutTimeMS = 0;
   mBlackOutStartTimeMS = 0;
   mFadeToBlack = false;

[b]   //object selection additions
   mSelectedObj = NULL;
   mChangedSelectedObj = false;[/b]
}

[i]// irrelevant code not shown for brevity[/i]

ConsoleMethod( GameConnection, getControlObject, S32, 2, 2, "")
{
   argv;
   SimObject* cp = object->getControlObject();
   return cp? cp->getId(): 0;
}

[b]//--------------------------------------------------------------------------
// object selection additions
//--------------------------------------------------------------------------
ConsoleMethod( GameConnection, setSelectedObject, bool, 3, 3, "(%object)")
{
   ShapeBase *sb;

   if(!Sim::findObject(argv[2], sb))
      return false;

   object->setSelectedObject(sb);

   return true;
}

//--------------------------------------------------------------------------
// object selection additions
//--------------------------------------------------------------------------
ConsoleMethod( GameConnection, getSelectedObject, S32, 2, 2, "()")
{
   SimObject* so = object->getSelectedObject();

   return so? so->getId(): 0;
}[/b]

ConsoleMethod( GameConnection, isAIControlled, bool, 2, 2, "")
{
   return object->isAIControlled();
}

void GameConnection::readPacket(BitStream *bstream)
{
   char stringBuf[256];
   stringBuf[0] = 0;
   bstream->setStringBuffer(stringBuf);

   clearCompression();
   if (isServerConnection())
   {
      mLastMoveAck = bstream->readInt(32);
      if (mLastMoveAck < mFirstMoveIndex)
         mLastMoveAck = mFirstMoveIndex;
      if(mLastMoveAck > mLastClientMove)
         mLastClientMove = mLastMoveAck;
      while(mFirstMoveIndex < mLastMoveAck)
      {
         AssertFatal(mMoveList.size(), "Popping off too many moves!");
         mMoveList.pop_front();
         mFirstMoveIndex++;
      }

[b]      // object selection additions
      // selected object - do we have a change in status?
      if (bstream->readFlag())
      { 
         // do we have a selected object now?
         bool hasSelectedObj = bstream->readFlag();

         if ((hasSelectedObj) && (bstream->readFlag()))
         {
            S32 gIndex = bstream->readInt(10);
            ShapeBase* obj = static_cast<ShapeBase*>(resolveGhost(gIndex));

            if (mSelectedObj != obj)
               setSelectedObject(obj);
         }
         else
            mSelectedObj = NULL;
      }[/b]

      mDamageFlash = 0;
      mWhiteOut = 0;
      if(bstream->readFlag())
      {
         if(bstream->readFlag())
            mDamageFlash = bstream->readFloat(7);
         if(bstream->readFlag())
            mWhiteOut = bstream->readFloat(7) * 1.5;
      }

[i]// irrelevant code not shown for brevity[/i]

}

void GameConnection::writePacket(BitStream *bstream, PacketNotify *note)
{

[i]   // irrelevant code not shown for brevity[/i]

   else
   {
      // The only time mMoveList will not be empty at this
      // point is during a change in control object.

      bstream->writeInt(mLastMoveAck - mMoveList.size(),32);

      S32 gIndex = -1;

[b]      // object selection additions
      // selected object - have we changed the status of the selected object?
      if (bstream->writeFlag(hasChangedSelectedObject()))
      {
         // do we have a selected object?
         if ((bstream->writeFlag(mSelectedObj != NULL)) && (!mSelectedObj.isNull()))
         {
            gIndex = getGhostIndex(mSelectedObj);

            if (bstream->writeFlag(gIndex != -1))
               bstream->writeInt(gIndex,10);
         }

         // reset the status of our object
         mChangedSelectedObj = false;
      }

      gIndex = -1;[/b]

      // get the ghost index of the control object, and write out
      // all the damage flash & white out

      if (!mControlObject.isNull())
      {
         gIndex = getGhostIndex(mControlObject);
     
         F32 flash = mControlObject->getDamageFlash();
         F32 whiteOut = mControlObject->getWhiteOut();
         if(bstream->writeFlag(flash != 0 || whiteOut != 0))
         {
            if(bstream->writeFlag(flash != 0))
               bstream->writeFloat(flash, 7);
            if(bstream->writeFlag(whiteOut != 0))
               bstream->writeFloat(whiteOut/1.5, 7);
         }
      }
      else
         bstream->writeFlag(false);

[i]      // irrelevant code not shown for brevity[/i]

}

Step 4 - Check if we have selected something

The server is responsible for determining if we have selected something, and if so it will save off the selected object for use later. Let's keep this simple and add our new server command SelectObject to an existing script file (I personally put this and other related server commands in a separate file). The code below is pretty well-commented, so it should be pretty easy to follow the logic there.

fps/server/scripts/commands.cs
[b]//-----------------------------------------------------------------------------
// object selection additions
//-----------------------------------------------------------------------------
function serverCmdSelectObject(%client, %mouseVec, %cameraPoint)
{
   //Determine how far should the picking ray extend into the world?
   %selectRange = 200;

   // scale mouseVec to the range the player is able to select with mouse
   %mouseScaled = VectorScale(%mouseVec, %selectRange);

   // cameraPoint = the world position of the camera
   // rangeEnd = camera point + length of selectable range
   %rangeEnd = VectorAdd(%cameraPoint, %mouseScaled);

   // Search for anything that is selectable &#8211; below are some examples
   %searchMasks = $TypeMasks::PlayerObjectType | $TypeMasks::CorpseObjectType |
                  $TypeMasks::ItemObjectType | $TypeMasks::TriggerObjectType;

   // Search for objects within the range that fit the masks above. If we are
   // in first person mode, we make sure player is not selectable by setting
   // fourth parameter (exempt  from collisions) when calling ContainerRayCast
   %player = %client.player;

   if ($firstPerson)
   {
     %scanTarg = ContainerRayCast (%cameraPoint, %rangeEnd, %searchMasks, %player);
   }
   else //3rd person - player is selectable in this case
   {
     %scanTarg = ContainerRayCast (%cameraPoint, %rangeEnd, %searchMasks);
   }

   // a target in range was found so select it
   if (%scanTarg)
   {
      %targetObject = firstWord(%scanTarg);
      %client.setSelectedObject(%targetObject);
   }
}[/b]

Step 5 - Example implementation
Your use of object selection will be specific to your game needs. However, let's go ahead and test it by making a small addition to the ShapeBase::renderObject method. There is already code there to show a bounding box if in debug mode, so let's just reuse it in the case we have something selected as a 3D cursor of sorts. First, we need to get the server connection, which is where we stored our selected object earlier. We then simply get the object id from the object and compare it to object id of the shapebase we are rendering. If they match, we go ahead and show the bounding box:

engine/game/shapeBase.cc
void ShapeBase::renderObject(SceneState* state, SceneRenderImage* image)
{
[i]      // irrelevant code not shown for brevity[/i]

   dglNPatchEnd();
  
   glMatrixMode(GL_PROJECTION);
   glPopMatrix();
   glMatrixMode(GL_MODELVIEW);
   dglSetViewport(viewport);

   uninstallLights();

[b]   // object selection additions
   // if we have been selected, then render a cursor around us
   // for now this is simply a bounding box
   GameConnection* conn = GameConnection::getServerConnection();
   ShapeBase* selectedObj = NULL;

   if (conn)
      selectedObj = conn->getSelectedObject();

   S32 selectedId = -1;

   if (selectedObj != NULL)
      selectedId = selectedObj->getId();

   // Debugging Bounding Box

   //object selection change - added || (get() == selectedId) to existing statement
   //if (!mShapeInstance || gShowBoundingBox) {
   if (!mShapeInstance || gShowBoundingBox || (getId() == selectedId)) {[/b]
           glDisable(GL_DEPTH_TEST);
           Point3F box;
           glPushMatrix();
           dglMultMatrix(&getRenderTransform());
           box = (mObjBox.min + mObjBox.max) * 0.5;
           glTranslatef(box.x,box.y,box.z);
           box = (mObjBox.max - mObjBox.min) * 0.5;
          glScalef(box.x,box.y,box.z);
          glColor3f(1, 0, 1);
          wireCube(Point3F(1,1,1),Point3F(0,0,0));
          glPopMatrix();
 }

[i]      // irrelevant code not shown for brevity[/i]

NOTE: If you want to select your own player you can hit the 'Tab' key to put yourself in third-person camera view and select him. You cannot hit F8 and drop a camera and then click on him, because in that case the game still thinks you are in first-person mode ($firstPerson = true still) and the check we made in commands.cs (serverCmdSelectObject) will filter out the player:

if ($firstPerson)
   {
     %scanTarg = ContainerRayCast (%cameraPoint, %rangeEnd, %searchMasks, %player);
   }
   else //3rd person - player is selectable in this case
   {
     %scanTarg = ContainerRayCast (%cameraPoint, %rangeEnd, %searchMasks);
   }

Conclusion
If you have any problems or suggestions with the tutorial, let's discuss them in the feedback.

Enjoy!
Dave Myers
Lumpy Games
My mad neural firings

About the author

Considerable experience developing with Torque-based technologies and produced the first third-party game using any Torque technology (Orbz). Game designer, programmer, and producer, and credits include the innovative title Orbz and the colorful BuggOut.

Page«First 7 8 9 10 11 12 13 Next»
#241
01/01/2012 (1:35 am)
Forgive me i have t3d 2.1pro Windows platform, I'm trying to implement some of this code, I don't have guiShapeNameHud.cc, and I see in the gameTSCtrl.h has:
Parent::onMouseDown(evt);
if( isMethod( "onMouseDown" ) )
makeScriptCall( "onMouseDown", evt );

when code instructions above are different. Mainly I want to transfer the mouse pointer position from mouse down and up so I can calculate a terrain xyz to create objects at. Any help would be great.
Page«First 7 8 9 10 11 12 13 Next»