Game Development Community

dev|Pro Game Development Curriculum

Ultimate Compass

by Xavier "eXoDuS" Amado · 06/03/2002 (10:20 am) · 29 comments

Download Code File

Here’s a new Compass I coded from scratch. First I’ll explain you how to add it in game and then I’ll go through the code to explain it.

Download the zip attached to this resource and unzip it, place the guiCompassCtrl.cc and guiCompassCtrl.h files into your engine/gui/ directory and both dgl.cc and dgl.h to your engine/dgl/ directory. Open your project and add both files to it.
Place the three png files in a directory of choice inside your game directory, for example fps/client/ui/compass/*.png.
Compile your project, run the game and load any mission. Open the Gui Editor and add a new control of type guiCompassCtrl. You will now see a simple square in the top left part of your screen, click on it and you will be able to see the config parameters in the tab to the right. You will see many fields here, but the important ones are the last three, bitmap, OuterRingBitmap and NeedleBitmap. Feel in this fields using the paths to the three png files you unzipped before; bitmap is the inner rotating circle, OuterRingBitmap is the body of the compass and NeedleBitmap is the pointer. As an example I used fps/client/ui/compass/compass_circle.png in the bitmap field.
After supplying all the fields with values press Apply, you should now see the compass in the screen with the three bitmaps, move the compass around and place it in a comfortable position for you and close the Gui Editor.
Move around and you should see that the inner circle should rotate while the needle and body stay static.
Hope you like it.

Now for the code explanation, if you open the header file you should notice some important things, these are the mAngle float variable and the 4 new functions (setOuterBitmap and setNeedleBitmap, both of this with 1 overload function each).
You’ll also notice the two new string table entries and two texture handles, for the outer ring and the needle. There are no functions for the inner circle because I’m using the inherited bitmap functions to render the inner part (remember since this is derived from guiBitmapCtrl it has all It’s functions, including all the functions and variables to render one bitmap, I only added the functions and variables to render two more bitmaps).

Ok now into the .cc file, open it and you should first of all see the constructor for this class, there we set both new string table entries to default values.

GuiCompassCtrl::GuiCompassCtrl(void)
{
   mOuterBitmapName = StringTable->insert("");
   mNeedleBitmapName = StringTable->insert("");
}

We then move down a bit and we meet with the initPersistFields function. This function manages the linking between class variables and the configurable fields you see in the gui editor and which are saved in the .gui files. This is the way in which the engine manages data between the engine and the script files. In my code I removed some fields we don’t need (command, altcommand, wrap and autosize), please note that autoSize is a field I added in my previous resource which added the option to automatically size a guiBitmapCtrl to the size of the input image data.
In this function I also added two new fields, OuterRingBitmap and NeedleBitmap. Just in case you don’t know the Parent::initPersistFields(); line is calling the initPersistFields() function from this class’s parent, which is guiBitmapCtrl. This way it initializes all the other fields which are common to both guiCtrls, including the bitmap field in which you specify the inner circle bitmap.

void GuiCompassCtrl::initPersistFields()
{
   Parent::initPersistFields();
   addField("OuterRingBitmap",	TypeFilename, Offset(mOuterBitmapName, GuiCompassCtrl));
   addField("NeedleBitmap",		TypeFilename, Offset(mNeedleBitmapName, GuiCompassCtrl));
   removeField("wrap");
   removeField("autosize");
   removeField("command");
   removeField("altcommand");
}

Now we keep moving down and we’ll see a modified onWake() function. This function is automatically called each time the guiCtrl is shown in the screen, for example the first time you add it to the Canvas or just after mission finishes loading. In this function we make sure that the guiCompassCtrl is rendered using the already defined file paths (stored in the .gui file).
It looks for the path values stored in the member variables mBitmapName, mOuterBitmapName and mNeedleBitmapName, which are linked to the fields by the above initPersistFields function.

bool GuiCompassCtrl::onWake()
{
   if (! Parent::onWake())
      return false;
   setActive(true);
   setBitmap(mBitmapName);
   setOuterBitmap(mOuterBitmapName);
   setNeedleBitmap(mNeedleBitmapName);
   return true;
}

After this function we have the onSleep() function which does the opposite to the previous function. This one is called each time the guiCtrl isn’t viewed anymore, for example when you quit the game or delete the control from the gui. This function sets all our texture handles to NULL so that there won’t be any image data loaded. It also calls the onSleep function from its parent.

void GuiCompassCtrl::onSleep()
{
   mTextureHandle = NULL;
   mOuterTextureHandle = NULL;
   mNeedleTextureHandle = NULL;
   Parent::onSleep();
}

Now we get to see the important functions, setOuterBitmap and setNeedleBitmap. These pair of functions have one overloaded function each, you can pass these functions either a path to a file name (like “fps/data/client/ui/compass/compass_circle.png”) or you can directly pass a texture handle to them. The texture handle overloaded functions aren’t really used here, but they are there just in case.
This functions take the name passed to them (const char*), insert it in the StringTable and assign it to the member variable mOuterBitmapName (in the case of the setOuterBitmap function). It then checks if *mOuterBitmapName is non-zero, meaning that it points somewhere and in that case it creates a new texture handle and assigns it to mOuterTextureHandle. In case that *mOuterBitmapName is zero, i.e. nothing was set in the gui editor to this field, then it just sets the the mOuterTextureHandle to NULL.
The overloaded version which takes a texture handle as parameter simply assigns the member mOuterTextureHandle this passed texture handle.
The other two functions work the same but for the needle, and the inner circle code isn’t here since this is managed using the inherited from guiBitmapCtrl set of functions

void GuiCompassCtrl::setOuterBitmap(const char *name)
{
   mOuterBitmapName = StringTable->insert(name);
   if (*mOuterBitmapName)
      mOuterTextureHandle = TextureHandle(mOuterBitmapName, BitmapTexture, true);
   else
      mOuterTextureHandle = NULL;
   setUpdate();
}   

void GuiCompassCtrl::setOuterBitmap(const TextureHandle &handle)
{
   mOuterTextureHandle = handle;   
}   

void GuiCompassCtrl::setNeedleBitmap(const TextureHandle &handle)
{
   mNeedleTextureHandle = handle;   
}   

void GuiCompassCtrl::setNeedleBitmap(const char *name)
{
   mNeedleBitmapName = StringTable->insert(name);
   if (*mNeedleBitmapName)
      mNeedleTextureHandle = TextureHandle(mNeedleBitmapName, BitmapTexture, true);
   else
      mNeedleTextureHandle = NULL;
   setUpdate();
}

Now let’s jump ahead to the onRender function, which is the responsible for rendering the gui control in the screen for every tick.
You will notice that this functions is divided in 3 main blocks (these are if(mOuterTextureHandle), if (mTextureHandle) and if(mNeedleTextureHandle) ).
First we render the body bitmap, this code is very simple. Before explaining this code though I’ll explain some other things. You will see mBounds a lot, this is a RectI data structure, and it stores a rectangle. How do you describe a rectangle? With two points of course, and how do you describe the two points? With two integer values, one for x and another one for y. Along with each RectI data structure you have some functions to manage it’s data. For example you have the set() function, which can take two points, or 4 integers. Points are stored in the Point2I data structure. Note that the I stands for integer, you also have floats, RectF and Point2F, and the 2 in Point2F means it stores a point using 2 dimensions, you could also store a point in 3 dimensions using either Point3F or Point2F. Ok, back to mBounds, as I said it’s a rectangle, and it stores the render zone for the gui control you are rendering. To put it in another way it’s the black square you see around a gui control, the one you can resize, move, etc. What we might want about this rectangle are two things, the top-left point and the bottom-right point, which are mBounds.point and mBounds.extent respectively. Since these two are points you can also access each coordinate using mBounds.point.x or mBounds.point.y.
Well that was a lot of explanation, hopefully you didn’t got lost in it. Let’s go back to our rendering code. In the second line inside this first block of code you set the mBounds extent point to the size of the texture, this means that the guiCompassCtrl will always have the size of the outer body bitmap. We then define a new RectI (integer type) and set it’s two points to offset (the value passed to the onRender function, which is the same than mBounds.point) and the other one to the extents of the gui (mBounds). We now call the dgl function dglDrawBitmapStretch, which will render a bitmap (mOuterTextureHandle) to a specified rectangle. We pass both parameters, the mOuterTextureHandle texture handle and the just created rectangle outerRect.

void GuiCompassCtrl::onRender(Point2I offset, const RectI &updateRect)
{
   if(mOuterTextureHandle)
   {
	   TextureObject* texture = (TextureObject*) mOuterTextureHandle;
	   mBounds.extent.set(texture->bitmapWidth, texture->bitmapHeight);

	   RectI outerRect(offset, mBounds.extent);
	   dglDrawBitmapStretch(mOuterTextureHandle, outerRect);
   }

Now let’s look at the second block of code, this one renders the inner Circle, using the inherited mTextureHandle. The third line in this code defines a new integer point (BorderOffset), and we set it’s x component to the difference between mBounds.extent.x and texture->bitmapWidth and the y component to the difference between mBounds.extent.y and the texture’s height. We now create two new Rects, srcRect and dstRect (source Rectangle and destination Rectangle). The first, srcRect, is the rectangle of the image we want to render, meaning that it’s the rectangle we want to render, and dstRect is the rectangle on the screen to which we want to render the srcRect. In this code we want to render the complete image data, so we set srcRect’s top-left point to 0,0 and it’s bottom-right point to the texture’s width and height.
For the dstRect we use 4 integers instead of 4 points, the first one (top-left point, x component) we use the mBounds.point.x (the x component of the top-left point) minus half the BorderOffset.x component. Since we want this to be centered we use half the border on the left and the other half on the right. Now we set the y component of the dstRect which will be similar to the x component, this one is the mBounds.point.y minus the half of the BorderOffset.y component. Most of the time this value will be the same as the first one, since circles have the same height than width. We now set dstRect’s bottom right point to the dimensions of the texture (texture->bitmapWidht and texture->bitmapHeight). We use the texture’s values since this compass isn’t resizable, which you might think is bad, but I didn’t needed to resize the compass in my game, since resizing means a lose in quality my artist just made my compass images the real size I needed. A bad thing about this though is that the relative mode wont work, since it’ll always be render to the size in pixels of the texture. I might make another version of this compass which supports resizing if many people need it, since I don’t.
We then have the block of code which gets the camera matrix, extracts the rotation value from it, negates it (because there’s a bug in which the north is south and north is south) and also sets the z component to 0, since for the compass we don’t care about the height. It then sets the mAngle float variable to the value returned by the Vector2dToDegree() function. I took this function from Matt’s compass, and I don’t know if he coded it or took it from nohobody’s compass or someone elses compass. What this function does is convert a vector, in our case the camera’s rotation vector, to degrees, which we’ll use to rotate the compass inner circle. The last line in this chunk of code is the dglDrawBitmapRotated() function. This one is defined in the attached dgl.cc and dgl.h files included in the zip with this code snippet, and it was coded by Phil Carlisle. What it does is pretty simple, it renders a quad on the screen, textures it and rotates it by modifying its modelview matrix. We pass to this function several values which are, the texture handle we want to render, the rectangle in the screen to which we want to render to, the image data rectangle (srcRect) which we want to render into the dstRect, a Boolean value to flip the image or not and finally the angle amount we want the image to be rotated.

if (mTextureHandle)
   {
      dglClearBitmapModulation();
	  TextureObject* texture = (TextureObject*) mTextureHandle;

Point2I BorderOffset(mBounds.extent.x - texture->bitmapWidth, mBounds.extent.y - texture->bitmapHeight);
	  
	  RectI srcRect(0, 0, texture->bitmapWidth, texture->bitmapHeight);
	  RectI dstRect(mBounds.point.x + BorderOffset.x/2, mBounds.point.y + BorderOffset.y/2, 
          texture->bitmapWidth, texture->bitmapHeight);

	  struct CameraQuery query;
	  GameProcessCameraQuery(&query);
	  
  	  MatrixF cameraMatrix = query.cameraMatrix;
	  Point3F cameraRot;
	  cameraMatrix.getColumn(1, &cameraRot);
	  cameraRot.neg();
	  cameraRot.z = 0;

	  mAngle = Vector2dToDegree(cameraRot);

	  dglDrawBitmapRotated(mTextureHandle, dstRect, srcRect, false, -mAngle);
   }

We the have the final block of code which renders the needle, I won’t go through this code since it’s very similar to the previous render code, so it’ll be easy for you to figure out.

if(mNeedleTextureHandle)
   {
	   TextureObject* texture = (TextureObject*) mNeedleTextureHandle;
	   
	   Point2I needleOffset(mBounds.point.x + ((mBounds.extent.x - texture->bitmapWidth) / 2), 
           mBounds.point.y + ((mBounds.extent.y - texture->bitmapHeight) / 2);
	   Point2I needleExtent(texture->bitmapWidth, texture->bitmapHeight);
	   
	   RectI needleRect(needleOffset, needleExtent);
	   dglDrawBitmapStretch(mNeedleTextureHandle, needleRect);
   }

After these 3 important blocks of code we have a final if statement which renders a black border around the guiCtrl in case that the profile says to render a border or in the case that none of the texture handles are set.

if (mProfile->mBorder || !mTextureHandle && !mOuterTextureHandle && !mNeedleTextureHandle)   
   {
      RectI rect(offset.x, offset.y, mBounds.extent.x, mBounds.extent.y);
      dglDrawRect(rect, mProfile->mBorderColor);
   }

   renderChildControls(offset, updateRect);
}

That pretty much does it. Hope you understood the whole explanation, as I said I’ll try to make a resizable version if anyone feels he needs it, though I think it’s better to make the images the exact size when you make them.
If you need help with any part of this code please let me know and I’ll do my best to help you out, and If there are any bugs in it let me know too!
As I already said, but wont get tired of saying, special thanks goes to Matt Webster and all the other guys who coded different compasses, from them I took the camera rotation code and the vector to degrees function (which I’ll try to make my own soon, but I still haven’t tried cause it was already done) 

Good Luck with resource and feel free to use it in your own Torque Projects!
Regards,
eXoDuS
Page «Previous 1 2
#1
06/01/2002 (10:05 pm)
Screenshot ?
#3
06/02/2002 (7:28 am)
Oh, ok, lemme get one....

Ok, done, that's not the art included with the code snippet btw, that's from my game, the art included was quickly made by me on photoshop, it's just as an example.
#4
06/03/2002 (3:57 pm)
Nice tutorial.

The one thing I didn't like is that you are constrained to displaying the bitmaps at their real size, much like your autosize addition. My artist made them bigger than I'd like to have them in 1024x768 so it bugged me that I couldn't resize it. Also, I noticed that if you went down in resolution, it won't display correctly because now the control is too small.

So, I made a simple change to allow you to make the compass an arbitrary size. In GuiCompassCtrl::onRender():

-Replace each reference to 'texture->bitmapWidth' with 'updateRect.len_x()'
-Replace each reference to 'texture->bitmapHeight' with 'updateRect.len_y()

Edit: I had origionally posted this using the "->" opperatior on updateRect, but thats incorrect. It should use the dot as listed above.

There is one exception to this:

-The creation of srcRect needs to remain the same: 'RectI srcRect(0, 0, texture->bitmapWidth, texture->bitmapHeight);'

If you make those small changes, you can now resize the compass to make it bigger or smaller, and it will work correctly in all resolutions.

There may be a better way to do this, I'm not sure. The only problem I've seen so far is that when resizing the gui control you have to make sure you keep the proportions of the two main bitmaps correct. Thats easy tho.

Edit: I just noticed that at the bottom of the tutorial you mention that you'd work on a resizable version if anyone needs it. ;-) I didn't really read the whole thing since the code was pretty straight forward. Anyway, maybe this post will help someone out that needs to resize it.
#5
06/03/2002 (6:12 pm)
Hey, that seems like a good solution, i'm trying it at the moment, but I didnt knew about the updateRect... what does it do? or what data does it have?

Unless updateRect is magic i dont see how you are going to resize the inner circle image without losing the ratio beetween the outer and inner circles....

If you notice i used 2 pictures, on that is 150 pixels and the other one which is 120 pixels... i draw them both with a 1:1 ratio (using texture->textureWidth) and you will see both images as they should be seen... using the updateRect for both rings makes em both the same size, cause i'm guessing updateRect is the rect that describes the changed size of the gui control.. passed when it's resized... just guessing here... but anyway, using the same value for both pictures will make em both the same size... that's not what i wanted... that's why i've been fighting for hours to get it to resize and failed :\
#6
06/04/2002 (11:23 am)
That is a fine looking compass ! Good job.
#7
06/04/2002 (11:44 am)
Ok, I see what your problem is, and you're totally right. If your images aren't the same size then resizing like this won't work. That didn't effect me because both my images are the same size, and just designed to fit one inside the other, with transparency around them.

Incase anyone is interested you can see a screenshot of my compass here.

Anyway, updateRect is the RectI that is passed to GuiCompassCtrl::onRender(). From what I can tell its a RectI that is the size of the GuiCompassCtrl that should be rendered. So using the length of this rectanlge instead of the texture size lets you render it to the gui control's size, thus you can resize it. But you are totally correct that this won't work correctly if the images are not the same size. Maybe the borderoffset could be adjusted by difference between the sizes in the two images. I'll change one of my images to be smaller and see what I can do.
#8
06/05/2002 (8:58 am)
Ok, thanks for investing time on it, thought maybe i could just make the center image have a border around it... that would fix it...
Also, what's the difference beetween updateRect and mBounds then?
#9
06/05/2002 (11:39 am)
mBounds.extent is set to updateRect's lengths in this call:

mBounds.extent.set(updateRect.len_x(), updateRect.len_y());

(Note, thats how the call looks after you've made the replacments in my post above.)

Its in the first IF statment of onRender(), origionally. Which reminds me to mention another small change I made. I'm only using 2 images total with this control, instead of the 3 that it can take. I'm using an outer ring which has the N, S, W, E markings on it and spins around which is set to the "bitmap" field of the control. Then I'm using an inner ring which contains the needle and a little decoration set to the "NeedleBitmap" field of the control. I don't, atleast right now, have anything set to the "OuterBitmap" field.

So anyway, the change. The call above that sets mBounds is in the first IF, which checks for the outerbitmap. Since I don't have one, it wouldn't ever set mBounds which is used later. So I just moved that one line above outside of the IF to the very first line of the onRender() function. This really doesn't matter if you have an OuterBitmap, but it won't hurt if you do.

I haven't had a chance to work on the two different image sizes thing yet, but I'm still planning on taking a look at how to do it.
#10
06/05/2002 (1:32 pm)
Erm, you are mistaken here, mBounds isnt something i created, mBounds is the rect that describes the gui rectangle.. i did that set so that it cant be resizable, ie it sets the rect to the size of the texture... if u remove that line the gui should work as normal, ie it will be resizable again...
#11
06/05/2002 (5:29 pm)
mBounds is a member of GuiControl. I see what your saying tho... you just set mBounds.extent to the height and width of the texture, instead of leaving it alone. When I set it using updateRect, I'm basically setting it to what it already is, is that right?

I just did a build without that line and it did work fine.

So to answer your question from above: "Whats the difference between updateRect and mBounds then?", I don't really know, they very well might be the same thing. I guess I just got lucky. I don't think that solves the problem with resizing different sized images tho, as they need to be centered.

Edit: Actually, I think this does solve the problem even if your images are different sizes, as long as they are both centered in the image correctly. I made one of my images smaller, and it was simply scaled up to the correct size and worked fine.
#12
06/06/2002 (1:31 pm)
Yes scaled to the size of the gui... but not if you want to mantain the other one smaller... look at the screenshot along with my resource... the border ring is 150 pixels, the inner texture (golden) is 120 pixels.. and the needle is 40 pixels IIRC
#13
06/07/2002 (10:08 pm)
d:\after school cartoons\torque\engine\gui\guicompassctrl.cc(163) : error C2143: syntax error : missing ')' before ';'
help?
it is pointing to here in the code:
Point2I needleOffset(mBounds.point.x + ((mBounds.extent.x - texture->bitmapWidth) / 2),
							mBounds.point.y + ((mBounds.extent.y - texture->bitmapHeight) / 2);
#14
06/07/2002 (10:24 pm)
yeah Programming basics finnally paid off
Point2I needleOffset(mBounds.point.x + ((mBounds.extent.x - texture->bitmapWidth) / 2),
							mBounds.point.y + ((mBounds.extent.y - texture->bitmapHeight) / 2));
balanced now might want to change that in the zip file
#15
06/08/2002 (5:43 am)
Xavier, thanks for this cool compass and the in-depth explanation!
I only had to make some minor changes and figure out which sizes I have to use for the graphics to make a horizontal scrolling compass (you can see a first version here...
I will clean it up a bit and post it here...
EDIT: Okay, so you can grab the code here - there is a readme file provided in the zip which should explain everything ... have fun!
#16
06/09/2002 (7:28 pm)
It was working fine. . . but just added avon lady's buitmap. And now when I hit f10 I get
dgl.cc @ 165
GSurface :: drawBitmapsR::rountines
and it crashed?any idea?
#17
09/27/2002 (5:29 pm)
Sweet work. Check out the results of your hard work in my screenshot...

http://home.attbi.com/~robertbrower/screenshot_00001.png

Thanks very much.

Robert
#18
07/19/2004 (9:06 pm)
hey whats up, im having trouble getting it to work. i have to many compiling errors when i put the files in my project. there is a lot of explanation in this tutorial. is there any way you could post what you added to the dgl files in particular so i can try to just add them to my existing dgl files. thanks chris
#19
02/02/2006 (2:08 pm)
When I read these old post I always wonder if they still work in 1.4...

Well, this one does work !
#20
10/15/2006 (4:20 pm)
is the guiCompassCtrl mentioned here the same as http://www.garagegames.com/index.php?sec=mg&mod=resource&page=view&qid=2779
? do they work together?
Page «Previous 1 2