Game Development Community

Damage/Text popups on objects.

by Stephen Lujan · 08/14/2007 (9:42 am) · 14 comments

Below is the file guiShapeDamageHud.cc
Just add it to your engine, I recommend in the gui/game/ directory. Pay attention to the console method toward the bottom, that's the only way to add new text popups. I'll provide an example that prints damage values later anyways.
//-----------------------------------------------------------------------------
// gui/game/guiShapeDamageHud.cc
// by Stephen Lujan
//-----------------------------------------------------------------------------
// Torque Game Engine
// Copyright (C) GarageGames.com, Inc.
//-----------------------------------------------------------------------------

#include "console/console.h"
#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"

//----------------------------------------------------------------------------
/// Temporarily displays damage or other text above shape objects.
///
/// 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 GuiShapeDamageHud : public GuiControl 
{
   typedef GuiControl Parent;
public:
	struct damagePopup
	{
		ShapeBase*			mShape;
		S32					mTime;
		S32					mStartTime;
		ColorF				mTextColor;
		StringTableEntry	mText;
	};

	Vector<damagePopup> mPopups;

protected:
	// field data
   ColorF   mFillColor;
   ColorF   mFrameColor;
   F32      mStartVerticalOffset;
	F32      mEndVerticalOffset;
   F32      mDistanceFade;
   bool     mShowFrame;
   bool     mShowFill;
	U32		mLastTime;

   void drawDamage( Point2I offset, damagePopup *popup, F32 opacity);

public:
   GuiShapeDamageHud();

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

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


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

IMPLEMENT_CONOBJECT(GuiShapeDamageHud);

/// Default distance for object's information to be displayed.
static const F32 cDefaultVisibleDistance = 500.0f;

GuiShapeDamageHud::GuiShapeDamageHud()
{
   mFillColor.set( 0.25, 0.25, 0.25, 0.25 );
   mFrameColor.set( 0, 1, 0, 1 );
   mShowFrame = mShowFill = true;
   mStartVerticalOffset = 1;
	mEndVerticalOffset = 40;
   mDistanceFade = 0.1;
	mLastTime = Platform::getVirtualMilliseconds();
}

void GuiShapeDamageHud::initPersistFields()
{
   Parent::initPersistFields();
   addGroup("Colors");
   addField( "fillColor",  TypeColorF, Offset( mFillColor, GuiShapeDamageHud ) );
   addField( "frameColor", TypeColorF, Offset( mFrameColor, GuiShapeDamageHud ) );
   endGroup("Colors");

   addGroup("Misc");
   addField( "showFill",   TypeBool, Offset( mShowFill, GuiShapeDamageHud ) );
   addField( "showFrame",  TypeBool, Offset( mShowFrame, GuiShapeDamageHud ) );
   addField( "startingVerticalOffset", TypeF32, Offset( mStartVerticalOffset, GuiShapeDamageHud ) );
   addField( "distanceFade", TypeF32, Offset( mDistanceFade, GuiShapeDamageHud ) );
   endGroup("Misc");
}

//----------------------------------------------------------------------------
/// Core rendering method for this control.
///
///
/// 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 GuiShapeDamageHud::onRender( Point2I, const RectI &updateRect)
{
	U32 time = Platform::getVirtualMilliseconds();
	S32 deltaTime= time - mLastTime;
	//Con::errorf("%d = %d - %d", deltaTime, time, mLastTime);
	mLastTime = time;

	//no impossibilities extending life of popups
	if (deltaTime < 0)
		deltaTime = 0;
	//if something is stuck lets not delete popups all at once
	if (deltaTime > 500)
		deltaTime = 500;

   // 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;
	// the next line is optional just to widen the viewcone a little bit for when 40% of a player is visible
   // but their center isn't. Increase the viewcone by 5 degrees:
   camFov += 0.08; //mDegToRad(5) / 2;
   F32 cosCamFov = mCos(camFov);

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

	Vector<damagePopup>::iterator i;
   for(i = mPopups.begin(); i != mPopups.end(); i++)
   {
      damagePopup *current = &(*i);
      if( !current )
		{
			//Con::errorf("Damage popup doesn't exist!");
			continue;
		}

		if (current->mTime < 0)
		{
			//Con::errorf("deleting expired popup at time %d", time);
			mPopups.erase(i);
			i--;
			continue;
		}

		if( !current->mShape )
		{	
			//Con::errorf("Damage popup has a shape that doesn't exist.");
			//AssertFatal(current->mShape, "this shape doesn't exist");
			continue;
		}

		if( !(current->mShape->getType() & ShapeBaseObjectType))
		{	
			Con::errorf("Damage popup has a shape that isn't derived from shapeBase.");
			continue;
		}
		
		//Con::errorf("decreasing popup life %d - %d", current->mTime, deltaTime);
		current->mTime -= deltaTime;
		// 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 (current->mShape->getType() & PlayerObjectType) 
		{
			MatrixF eye;

			// Use the render eye transform, otherwise we'll see jittering
			// Stephen: why always access violations here? grrrrrr
			current->mShape->getRenderEyeTransform(&eye);
			eye.getColumn(3, &shapePos);
		}
		else 
		{
			 // Use the render transform instead of the box center
			 // otherwise it'll jitter.
			MatrixF srtMat = current->mShape->getRenderTransform();
			srtMat.getColumn(3, &shapePos);
		}
		VectorF shapeDir = shapePos - camPos;

		// test early to see if it's behind us.
      // no need to normalize shapeDir for this,
      // we'll normalize it later if needed.
      F32 dot = mDot(shapeDir, camDir);
      if (dot < 0)
		{
			//Con::errorf("Damage popup was behind camera.");
         continue;
		}

      // Test to see if it's in range
      F32 shapeDist = shapeDir.lenSquared();
      if (shapeDist == 0 || shapeDist > visDistanceSqr)
		{
			//Con::errorf("Damage popup was not in range.");
         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.
      dot /= shapeDist;
      if (dot < cosCamFov)
		{
			//Con::errorf("Damage popup was not in viewcone.");
         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;
      current->mShape->disableCollision();
      ShapeBase *mount = current->mShape->getObjectMount();

      if (mount)
         mount->disableCollision();

      bool los = !gClientContainer.castRay(camPos, shapePos,losMask, &info);
      current->mShape->enableCollision();

      if (mount)
         mount->enableCollision();

      if (!los)
		{
			//Con::errorf("Damage popup did not have line of sight.");
         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;

		//perhaps instantiating these as guimembers instead of every message every frame would be better
		F32 heightMultiplier= ((F32)current->mTime / (F32)current->mStartTime);
		F32 pixelHeight= ((1.0-heightMultiplier)*mEndVerticalOffset) + ((heightMultiplier)*mStartVerticalOffset);
		//Con::errorf("before projPnt.y = %.2f", projPnt.y);
		
      if (!parent->project(shapePos, &projPnt))
		{
			//Con::errorf("Damage popup could not project to screen coordinates.");
         continue;
		}
		projPnt.y -= pixelHeight;
		if (projPnt.y < 1)
			projPnt.y = 1;
		/*
		projPnt.x -= 0.6*pixelHeight;
		if (projPnt.x < 1)
			projPnt.x = 1;
		*/
		//Con::errorf("heightMultiplier = %.2f  pixelHeight = %.2f after projPnt.y = %.2f", heightMultiplier, pixelHeight, projPnt.y);
      F32 opacity = (shapeDist < fadeDistance)? 1.0:
         1.0 - (shapeDist - fadeDistance) / (visDistance - fadeDistance);
		//spend last half of life fading out
		F32 ageOpacity = (2.0*(F32)current->mTime / (F32)current->mStartTime);
		if (ageOpacity < opacity)
			opacity= ageOpacity;
      // Render the text
      drawDamage(Point2I((S32)projPnt.x, (S32)projPnt.y),current,opacity);
   }

   // Restore control object collision
   control->enableCollision();

}

//----------------------------------------------------------------------------
/// Render popup text.
///
/// Helper function for GuiShapeDamageHud::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   popup	the damage popup struct
/// @param   opacity Opacity of name (a fraction).
void GuiShapeDamageHud::drawDamage(Point2I offset, damagePopup *popup, F32 opacity)
{
	//Con::errorf("executing GuiShapeDamageHud::drawDamage() at shape %d at time %d",popup->mShape->getId(), Platform::getVirtualMilliseconds());
   // Center the name
   offset.x -= mProfile->mFont->getStrWidth((const UTF8 *)popup->mText) / 2;
   offset.y -= mProfile->mFont->getHeight();

   // Deal with opacity and draw.
   popup->mTextColor.alpha = opacity;
   dglSetBitmapModulation(popup->mTextColor);
   dglDrawText(mProfile->mFont, offset, popup->mText);
   dglClearBitmapModulation();
}
//New popups are trigger by the scripts.
ConsoleMethod( GuiShapeDamageHud, addPopup, bool, 6, 6, "int time, ColorF color, int object, string text")
{
	GameConnection* conn = GameConnection::getConnectionToServer();
   if (!conn)
      return false;

	// Allocate a new popup.
	GuiShapeDamageHud::damagePopup newPopup;
	// PARSE SHIZZLE
	newPopup.mTime = dAtoi(argv[2]);
	newPopup.mStartTime = newPopup.mTime;
	dSscanf(argv[3], "%f %f %f", &newPopup.mTextColor.red, &newPopup.mTextColor.green, &newPopup.mTextColor.blue);
	newPopup.mShape = static_cast<ShapeBase*> (Sim::findObject(argv[4])); //(conn->findObject(argv[4]));
	if (!newPopup.mShape)
	{
		Con::errorf("New damage popup couldn't find the shape below! Check guiShapeDamageHud.cc");
		Con::errorf(argv[4]);
		return false;
	}
	newPopup.mText= StringTable->insert(argv[5]);

	// Store it in the list at the back to get rendered last
	object->mPopups.push_back(newPopup);
	//for now i'll assume it worked
	return true;
}
Just need to add the gui to our main gui when we're playing a game. With starter.fps here's how. Open up "example/starter.fps/client/ui/playgui.gui". Then add the block of code below around line 121 so its just below the GuiShapeNameHud which it is based off of.
new GuiShapeDamageHud(DamageHud) {
      profile = "GuiDefaultProfile";
      horizSizing = "width";
      vertSizing = "height";
      position = "0 0";
      extent = "653 485";
      minExtent = "8 8";
      visible = "1";
      helpTag = "0";
      fillColor = "0.000000 0.000000 0.000000 0.250000";
      frameColor = "0.000000 1.000000 0.000000 1.000000";
      showFill = "0";
      showFrame = "0";
      verticalOffset = "0.2";
      distanceFade = "0.1";
		//this is in guiCrossHairHud, not used here!
      //   damageFrameColor = "1.000000 0.600000 0.000000 1.000000";
      //   damageFillColor = "0.000000 1.000000 0.000000 1.000000";
      //   damageRect = "30 4";
   };
Good now let's put it to use by printing damage values. First open "server\scripts\shapeBase.cs", then add the code below to the bottom of function ShapeBase::damage
// Inform the clients to print damage message
   for( %clientIndex = 0; %clientIndex < ClientGroup.getCount(); %clientIndex++ )
   {
      %cl = ClientGroup.getObject( %clientIndex );
      commandToClient(%cl, 'NewDamagePopup', %this, %damage, %this.getDamageLevel());
   }
Now we take care of the client side. Just add the function below to the bottom of "client/scripts/client.cs" or create a new file for it or whatever you want.
function clientCmdNewDamagePopup(%shape,%damage,%damageLevel)
{
	//echo("creating popup ",%damage," for object ",%shape);
	%damageLevel /= 100.0;
	%red = %damageLevel;
	%green = 1.0 - %damageLevel;
	DamageHud.addPopup(2000,%red@" "@%green@" 0.0",%shape,%damage@" damage");
}
That's it. It should look great in so test it out. I think it could use some optimizations both for network usage and processing speed. Part of what concerns me is the Tvector the messages are stored in. I'm not sure how much faster linked lists would be for this job, but if there's a significant difference it should be used. Better line of sight occlusion would be nice too.

#1
08/14/2007 (5:26 pm)
Well done! Thanks for the resource.
#2
09/01/2007 (5:19 pm)
This works extremely well I'm sure I'll be making good use of this resource. Thank you so much.

Edit:
I got a couple of crashes and traced it to an invalid mShape reference.

My problems was that my enemies were dying too quickly for the popups to disappear and the ShapeBase was becoming invalid.

To fix this, rather than store the ShapeBase pointer. Store the id of the object and query for it each onRender.

Change mShape to an S32 mShapeId.
In addPopup after Sim::findObject call set mShapeId to the objects id returned by getId().
In onRender in the loop use Sim::findObject to lookup the shape and cancel out if it isn't found.
#3
09/04/2007 (4:52 am)
In case anyone is interested..

Change:
struct damagePopup
	{
		ShapeBase*			mShape;
		S32					mTime;
                ...

To:
struct damagePopup
	{
		S32                                     mShapeId;
		S32					mTime;
                ...

Then around line 346 change:
newPopup.mShape = static_cast<ShapeBase*> (Sim::findObject(argv[4])); //(conn->findObject(argv[4]));
	if (!newPopup.mShape)
	{
		Con::errorf("New damage popup couldn't find the shape below! Check guiShapeDamageHud.cc");
		Con::errorf(argv[4]);
		return false;
	}
to:
ShapeBase* shape;
   shape = static_cast<ShapeBase*> (Sim::findObject(argv[4])); //(conn->findObject(argv[4]));
	if (!shape)
	{
		Con::errorf("New damage popup couldn't find the shape below! Check guiShapeDamageHud.cc");
		Con::errorf(argv[4]);
		return false;
	}
   newPopup.mShapeId = shape->getId();

Around line 183 change
if( !current->mShape )
		{	
			//Con::errorf("Damage popup has a shape that doesn't exist.");
			//AssertFatal(current->mShape, "this shape doesn't exist");
			continue;
		}

to:
ShapeBase* shape;
      shape = static_cast<ShapeBase*> (Sim::findObject(current->mShapeId));
   	if (!shape)
		{	
			//Con::errorf("Damage popup has a shape that doesn't exist.");
			//AssertFatal(current->mShape, "this shape doesn't exist");
			mPopups.erase(i);
			i--;
			continue;
		}

Last but not least change all references of
current->mShape
to
shape
.
#4
10/09/2007 (10:27 am)
Great resource, Thanks! One more thing, how could i change pop up position to pop above player not the target?
#5
10/14/2007 (10:44 am)
Wonderful resource Stephen and thanks to Brian for the fix. Easily ported to TGEA and working as expected. (To use in TGEA, change the dgl functions to their GFX-> counterparts)
#6
11/07/2007 (3:53 am)
Lol, that fix was done EARLY in my Torque development. In actuality there is a much simpler fix. I haven't tested this but I suspect it will work without a hitch.

Just change:
struct damagePopup
	{
		ShapeBase*			mShape;
		S32					mTime;
                ...

to

struct damagePopup
	{
		SimObjectPtr<ShapeBase>	mShape;
		S32					mTime;
                ...
#7
11/17/2007 (1:13 am)
@ Danny: sorry to ask but I'm mainly focused on art and really not good at programming and mainly I implement code as I see it... so, can you show the changes you made to implement it in TGEA?

Sorry,
Tnx :)
#8
01/11/2008 (6:10 am)
It might be extremly useful.
It can't be thought it is possible to play without such as this resource.

Thank you. :-)
#9
03/01/2008 (12:47 am)
Great resource works great except for one problem I'm having with it over a network connection. Works fine when you're the server but when your the client you see "Damage popup has a shape that isn't derived from shapeBase." in the console and the text will not display. I'm working on it now and I have a theory that I've tracked it down to this line:
commandToClient(%cl, 'NewDamagePopup', %this, %damage, %this.getDamageLevel());

On the server when I do a %this.dumpClassHierarchy(); I get
AIPlayer ->Player ->ShapeBase ->GameBase ->SceneObject ->NetObject ->SimObject ->
Like it should be, however on the client I receive:
GuiControlProfile ->SimObject-> which turns out to be a scroll bar control on the chat window.

I think its a problem with sending %this over the network since its probably just a pointer and not an actual copy of the object right?
#10
03/01/2008 (1:29 am)
Ok I got it figured out. There was a problem with the server's object ID not matching the clients. Here's the fix.

Open "server\scripts\shapeBase.cs" and replace
commandToClient(%cl, 'NewDamagePopup', %this, %damage, %this.getDamageLevel());
with this:
commandToClient(%cl, 'NewDamagePopup', %cl.getGhostId(%this), %damage, %this.getDamageLevel());

Then replace the client command... in whatever file you put it in...
replace this:
function clientCmdNewDamagePopup(%shape,%damage,%damageLevel)
{
	//echo("creating popup ",%damage," for object ",%shape);
	%damageLevel /= 100.0;
	%red = %damageLevel;
	%green = 1.0 - %damageLevel;
	DamageHud.addPopup(2000,%red@" "@%green@" 0.0",%shape,%damage@" damage");
}
with this:
function clientCmdNewDamagePopup(%shape,%damage,%damageLevel)
{
	%damageLevel /= 100.0;
	%red = %damageLevel;
	%green = 1.0 - %damageLevel;
	%realShape = ServerConnection.resolveGhostID(%shape);
	DamageHud.addPopup(2000,%red@" "@%green@" 0.0",%realShape,%damage@" damage");
}

And it should now work for your clients and server.
#11
03/26/2008 (3:07 pm)
Killer resource, working great in TGE 1.5.
#12
02/25/2009 (8:18 pm)
Has anyone tried this in TGEA 1.8.1?
#13
02/25/2009 (9:12 pm)
yeah, except I still get crashes from this:
// Use the render eye transform, otherwise we'll see jittering
			// Stephen: why always access violations here? grrrrrr
			current->mShape->getRenderEyeTransform(&eye);
			eye.getColumn(3, &shapePos);
#14
03/04/2009 (3:27 pm)
Is there a way to fix it?