Entity-sensing crosshair
by Daniel Buckmaster · 12/31/2007 (2:31 pm) · 11 comments
So, this is the first part of the long-awaited (hah, there's ego for you) context-action resource. This part is simply code changes to GuiCrossHairHud to enable the crosshair to remember objects that you look at. The changes are pretty simple, since the croshair already casts rays into the scene by default - and for this reason, there shouldn't be any detectable performance hit.
Note that the way I implement these changes overwrites the existing functionality that draws a little health bar over some objects that you target (like Players). I've always thought this was a silly idea, so I was glad to get rid of it for my game. However, if you need to retain this functionality, mention it here and I'll look into it. Or, if you know how to do it, say so ;).
Anyway, on to the changes.
Back up guiCrossHairHud.cc first. Always do this. You should know that by now. Shame on you.
The first thing in the file is the definition of the GuiCrossHairHud class. Within it, find:
Beneath, add:
At the end of GuiCrossHairHud::GuiCrossHairHud, add:
Now, scroll down to void GuiCrossHairHud::onRender. This is the method that gets called every time the crosshair is rendered on the screen. The trick's in the name. Anyway, find within it:
Note: when I say I replaced something, I often leave the original code there, just commented out. This way, when I look back over the changes I made, I can easily fix errors or restore things to the way they used to be.
Now, within the same method, find:
After the commenting that explains what's going to happen, there's another if block that looks like this:
All that code which we just took so much trouble to understand, we don't need. So get rid of it any way you see fit, and replace it with:
onObjectSelected(%this,%obj)
onObjectDeselected(%this)
These are script functions that we'll define later. But basically, onObjectSelected is called whenever a new object comes under the crosshair that wasn't there before. onObjectDeselected is called whenever an object that was sensed last time we checked is no longer sensed.
Notice that I added a comment in there mentioning continuous callbacks. Take Triggers as an example. You get a callback when you enter the trigger, when you leave, and constant callbacks when you're inside it. If you want to do something similar, have a method called every continuously while the same object remaing in the crosshairs, you would do it where that comment is. However, I make no guarantee that if you did so you wouldn't get a hundred method calls a second, depending on how often the crosshair is rendered and whether callbacks do any flood control themselves. You'd have to implement a system similar to the collision notification system in ShapeBase (I think), and that's beyond the scope of this tutorial (or my abilities).
That should be everything. Recompile Torque and listen to the sound of no errors (hopefully ;)). Now we venture into script-land. It feels like being home again...
I'll use starter.fps as an example, which I guess is what it's for. Go into starter.fps/client/ui/playGui.gui and find this line:
Open up starter.fps/client/scripts/playGui.cs. Down on the bottom, add the following functions:
Anyway, so that's it. Hope you enjoyed it and see you when I get around to writing the rest of the thing!
Note that the way I implement these changes overwrites the existing functionality that draws a little health bar over some objects that you target (like Players). I've always thought this was a silly idea, so I was glad to get rid of it for my game. However, if you need to retain this functionality, mention it here and I'll look into it. Or, if you know how to do it, say so ;).
Anyway, on to the changes.
Back up guiCrossHairHud.cc first. Always do this. You should know that by now. Shame on you.
The first thing in the file is the definition of the GuiCrossHairHud class. Within it, find:
Point2I mDamageOffset;
Beneath, add:
//Sensing crosshair -> GameBase* mSelectedObject; //<- Sensing crosshairThis is the pointer to any object we find under the crosshair. Note that it's a GameBase pointer. This means the object can be a GameBase object or any object inheriting from it (ShapeBase and its derived clsses such as Player and Vehicle, as well as lots of others). This does not include objects like interiors or the terrain. This was delibrate, and if you need to sene these things, you might want to change this. I'm not sure what those things inherit from - maybe SceneObject.
At the end of GuiCrossHairHud::GuiCrossHairHud, add:
//Sensing crosshair -> mSelectedObject = NULL; //<- Sensing crosshairThis just initialises our pointer. This is very important. Someone once explained why, but it went straight over my head. Just remember that it's important.
Now, scroll down to void GuiCrossHairHud::onRender. This is the method that gets called every time the crosshair is rendered on the screen. The trick's in the name. Anyway, find within it:
endPos *= gClientSceneGraph->getVisibleDistance();The code around this line, basically, gets the camera's position and orientation, and uses this information to construct a vector that shoots out of the camera in the direction the camera is looking. The line you just found tells the line how long to be. gClientSceneGraphblahblah... basically means 'as far as the client can see'. So that means, to all intents and purposes, that the ray will appear infinitely long. If this floats your boat, then fine. I, however, wanted to put a cap on how far the player could reach. If you're like me, change that line to:
//Sensing crosshair -> endPos *= 2.0f; //Range check! //<- Sensing crosshairThe 2.0 you can change to whatever distance you like. If you're using Kork as your player model, you may want to increase this, because Kork's about 2 units tall. So he'll only just be able to pick up objects that are right at his feet. I'm using slightly shorter players, and plus, I want the player to have to crouch down to get something. Can you touch your toes without bending your knees? Really? Oh.
Note: when I say I replaced something, I often leave the original code there, just commented out. This way, when I look back over the changes I made, I can easily fix errors or restore things to the way they used to be.
Now, within the same method, find:
if (gClientContainer.castRay(camPos, endPos, losMask, &info)) {This is the meat of the thing. The function inside the if clause, gClientContainer.castRay, is the function that shoots the ray into the world to see what it can find. If it hits something, it will return true, so we enter the if clause. If it doesn't hit something, then there's no need to waste time and we skip all that.After the commenting that explains what's going to happen, there's another if block that looks like this:
if (ShapeBase* obj = dynamic_cast<ShapeBase*>(info.object))
if (obj->getShapeName()) {
offset.x = updateRect.point.x + updateRect.extent.x / 2;
offset.y = updateRect.point.y + updateRect.extent.y / 2;
drawDamage(offset + mDamageOffset, obj->getDamageValue(), 1);
}The first part basically says 'if the thing we hit is a ShapeBase object'. Then the next if clause says 'if the object has a name'. So we're looking for named ShapeBase objects. If we found one of those, then you get the code that draws a little health bar. drawDamage is the actual method that does the drawing; you can find it at the end of guiCrossHairHud.cc.All that code which we just took so much trouble to understand, we don't need. So get rid of it any way you see fit, and replace it with:
//Sensing crosshair ->
if(GameBase* obj = dynamic_cast<GameBase*>(info.object))
{
if(mSelectedObject)
{
if(obj != mSelectedObject)
{
Con::executef(this,1,"onObjectDeselected");
Con::executef(this,2,"onObjectSelected",obj->scriptThis());
mSelectedObject = obj;
}
//If you want continuous callbacks, here's your chance
}
else
{
Con::executef(this,2,"onObjectSelected",obj->scriptThis());
mSelectedObject = obj;
}
}
else
{
if(mSelectedObject)
{
Con::executef(this,1,"onObjectDeselected");
mSelectedObject = NULL;
}
}
}
else
{
if(mSelectedObject)
{
Con::executef(this,1,"onObjectDeselected");
mSelectedObject = NULL;
}
}
//<- Sensing crosshairThis lengthy (and probably un-optimised) piece of code is what fires off the script callbacks. There are two different functions that are triggered:onObjectSelected(%this,%obj)
onObjectDeselected(%this)
These are script functions that we'll define later. But basically, onObjectSelected is called whenever a new object comes under the crosshair that wasn't there before. onObjectDeselected is called whenever an object that was sensed last time we checked is no longer sensed.
Notice that I added a comment in there mentioning continuous callbacks. Take Triggers as an example. You get a callback when you enter the trigger, when you leave, and constant callbacks when you're inside it. If you want to do something similar, have a method called every continuously while the same object remaing in the crosshairs, you would do it where that comment is. However, I make no guarantee that if you did so you wouldn't get a hundred method calls a second, depending on how often the crosshair is rendered and whether callbacks do any flood control themselves. You'd have to implement a system similar to the collision notification system in ShapeBase (I think), and that's beyond the scope of this tutorial (or my abilities).
That should be everything. Recompile Torque and listen to the sound of no errors (hopefully ;)). Now we venture into script-land. It feels like being home again...
I'll use starter.fps as an example, which I guess is what it's for. Go into starter.fps/client/ui/playGui.gui and find this line:
new GuiCrossHairHud() {This creates the crosshair, which is all fine and dandy, but there's one problem. The crosshair isn't named, so there'sno way to refer to it in scripts, unless you just pick a random ID and get lucky. So to fix this, change the line to:new GuiCrossHairHud(CrossHair) {Now our crosshair's identity crisis is solved.Open up starter.fps/client/scripts/playGui.cs. Down on the bottom, add the following functions:
function CrossHair::onObjectSelected(%this,%obj)
{
echo("GuiCrossHairHud " @ %this @ ": object " @ %obj @ " selected!");
}
function CrossHair::onObjectDeselected(%this,%obj)
{
echo("GuiCrossHairHud " @ %this @ ": object deselected!");
}Look familiar? These are the functions the crosshaircalls when you select or deselect something. Inside these, instead of just a placeholding echo, you would put something useful, like changing the colour of the crosshair, printing a message to the screen, etcetera.Anyway, so that's it. Hope you enjoyed it and see you when I get around to writing the rest of the thing!
About the author
Studying mechatronic engineering and computer science at the University of Sydney. Game development is probably my most time-consuming hobby!
#2
08/09/2008 (4:57 am)
Cool...
#3
07/03/2009 (11:30 pm)
OMG this is great... stumbled upon a great resource thank you very much.
#4
07/31/2009 (3:11 pm)
One of my goals right now is to increase the size of this entity sensing crosshair. Any ideas how to go about this? I don't mean the cross hair itself but rather the radius of the area it senses.
#5
What I suggest you do is look into radius searches. I've used them in script, but I'm not sure how they work in code. Instead of casting a ray, do a radius search around your player and find the closest object within an x-degree cone in front of the player, using the dot product of the camera vector and the direction from the camera to each found object.
I'm actually working on this idea myself - if you want me to, I'll bump that to the top of my work queue and get it over with quickly ;P.
07/31/2009 (3:37 pm)
That may be tricky, depending on what you want. The crosshair uses a raycast to determine entities, and raycasts have no width.What I suggest you do is look into radius searches. I've used them in script, but I'm not sure how they work in code. Instead of casting a ray, do a radius search around your player and find the closest object within an x-degree cone in front of the player, using the dot product of the camera vector and the direction from the camera to each found object.
I'm actually working on this idea myself - if you want me to, I'll bump that to the top of my work queue and get it over with quickly ;P.
#6
08/03/2009 (12:49 pm)
Thanks for your advice, I'm trying out radius searches right now. The tricky part it seems is implementing this cone around the radius. I'll let you know how it turns out.
#7
08/04/2009 (7:53 pm)
If you've got the camera position as cameraPos, and the object position as objectPos, this should work://Vector from camera to object Point3F vector = objectPos - cameraPos; //This method probasbly doesn't exist, but you get the idea: Point3F cam = camera.getForwardVector(); //Make sure both vectors are normalized (that is to say, have a length of 1): cam.normalize(); vector.normalize(); F32 fac = mDot(cam, vector); //Now, if fac == 1, the vectors are parallel and you're looking right at the object //If fac == 0, the vectors are at right angles //If fac == -1, the vectors point in opposite directionsThat obviously needs to be optimised for looping through multiple objects, but I think you get the idea. You just make a check for a certain value of fac to determine whether the object is within a certain angle of the camera vector.
#8
Excuse me if my math is off, but could cam just be represented as endPos - camPos?
08/05/2009 (12:14 pm)
Looks cool. Excuse me if my math is off, but could cam just be represented as endPos - camPos?
#9
08/05/2009 (4:36 pm)
Yes, it could - I didn't look through the rest of the resource code before writing my little example, so I didn't remember that endPos existed. Good catch ;)
#10
08/12/2009 (2:38 pm)
Thanks for the help. I've implemented the larger crosshair, which physically works, but is sometimes causing crashes when I destroy an enemy. Have you been having any crashing problems yourself? This is how I implemented it, it finds item3: the closest shapebase. gClientContainer.initRadiusSearch(camPos, 1000, losMask);
U32 itemID;
item3 = NULL;
item2 = NULL;
F32 close = 100000;
while( itemID=gClientContainer.containerSearchNext() )
{
Point3F creature = gClientContainer.containerSearchCurrPoint();
Point3F vector = creature - camPos;
Point3F cam = endPos - camPos;
cam.normalize();
vector.normalize();
F32 fac = mDot(cam,vector);
if((gClientContainer.containerSearchCurrDist() < close) && (fac > 0.95))
{
Sim::findObject(itemID, item2);
if (ShapeBase* obj3 = dynamic_cast<ShapeBase*>(item2)){
if(obj3->getShapeName()){
close = gClientContainer.containerSearchCurrDist();
Sim::findObject(itemID, item3);
}
}
item2 = NULL;
}
}
#11
But I don't think it's possible to get a crash due to, for example, using an object handle when the object has been deleted, at least with the standard GUICrossHairHUD or my version - since the handle of the previously selected object is only used to compare with a new object, never to call methods on.
08/13/2009 (4:30 am)
I can't see any obvious flaws with your code... Are you running the engine in debug mode? If so, it should give you some information about where/why the crashes are occurring. I didn't have any problems like that, but I didn't test the resource with deleting objects.But I don't think it's possible to get a crash due to, for example, using an object handle when the object has been deleted, at least with the standard GUICrossHairHUD or my version - since the handle of the previously selected object is only used to compare with a new object, never to call methods on.

Torque Owner Trenton Shaffer