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.
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.About the author
#2
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.
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
Change:
To:
Then around line 346 change:
Around line 183 change
to:
Last but not least change all references of
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->mShapeto
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
Just change:
to
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
Sorry,
Tnx :)
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
It can't be thought it is possible to play without such as this resource.
Thank you. :-)
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
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?
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
Open "server\scripts\shapeBase.cs" and replace
Then replace the client command... in whatever file you put it in...
replace this:
And it should now work for your clients and server.
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?
Torque Owner Trenton Shaffer