by Victor Chelaru
The ShapeManager is a class for drawing 2D and 3D lines and shapes. This article will cover the creation of objects in the FRB.Shapes namespace and how to go about drawing these shapes. This article assumes a basic understanding of the GameMain.cs and GameData.cs files. More information on the GameMain.cs file can be found in this article.
ShapeManager Declaration and Initialization
Just like the SpriteManager stores and draws Sprites and the TextManager draws text, the ShapeManager is responsible for storing and drawing Shapes. Unlike the SpriteManager and TextManager, the ShapeManager is not part of the FRB Template in that it is not declared and defined in the GameMain.cs class. Initialization is very simple, but does require some familiarity with the GameMain.cs file. The following additions are all made in GameMain.cs until specified otherwise. First, so that we don’t have to prefix everything with FRB.Shapes, I add the following line near the top of my file:
using FRB.Shapes;
Next, I declare an instance of the ShapeManager. Since there will only be one, and I’d like to be able to access it from other classes such as my GameData class, I’ll make it public and static. Add the bold line:
public class
GameForm : Form
{
#region declaration, construction,
and events
#region Static Variables (Managers
and other FRB classes)
//here are our FRB objects, and
general variables
//these are used to set up a
game loop and input using the FRB engine.
//you will declare your
variables inside the status method
public static SpriteManager sprMan;
public static InputManager inpMan;
public static TextManager textMan;
public static GuiManager guiMan;
public static FRB.Cursor cursor;
public static Camera camera;
public static
ShapeManager shaMan;
. . .
The last step is to instantiate this object. Move to the GameForm’s InitializeFRB method and add the bold line:
. . .
// Create the GuiMManager
guiMan = new
GuiManager(sprMan, textMan, inpMan, camera, "genGfx/guiTex.png");
// The GuiManager creates
a Cursor for us.
cursor = GuiManager.cursor;
shaMan = new ShapeManager(sprMan);
// create the GameData
gameData = new
GameData();
. . .
Although I omitted the creation of the SpriteManager in the code above, be sure that the ShapeManager is created after the SpriteManager so that the ShapeManager has a valid reference to the Direct3D Device that the SpriteManager creates in its constructor. Also, I made sure to create the ShapeManager before creating the GameData so that I could reference shaMan in the Initialize method if needed. Now I move to GameData.cs. The final step is to create a reference to this in the GameData.cs class so that the ShapeManager can be easily referenced in the methods in that class as well:
public class
GameData
{
#region reference to engine managers
and data
public GameForm form;
public GuiData guiData;
public InputManager
inpMan;
public SpriteManager
sprMan;
public TextManager
textMan;
public GuiManager guiMan;
public Camera camera;
public Cursor cursor;
public Random random;
public ShapeManager shaMan;
#endregion
// declare variables of class
scope here
public void Initialize(GameForm form)
{
#region initialize engine managers
and data
this.form = form;
guiData = new
GuiData();
sprMan = GameForm.sprMan;
inpMan =
GameForm.inpMan;
textMan =
GameForm.textMan;
guiMan =
GameForm.guiMan;
camera = GameForm.camera;
cursor = GameForm.cursor;
random = GameForm.random;
shaMan =
GameForm.shaMan;
#endregion
. . .
We’re all set and can begin to work with the ShapeManager.
Choosing When to Draw Shapes Using Layers
The FRB Template assumes that Sprites should always be drawn before text, so it calls the SpriteManager’s Draw method and then the AfterFrameActivity is used to draw text. It’s not quite so clear where shapes should be drawn. In some cases, lines and shapes should be drawn on top of Sprites; if a green rectangle is used as a graphical representation of a targeting system, then it should probably be on top of all Sprites. If, however, a grid of lines is to be displayed behind the Sprites to help show the size of an object, then these lines should be behind all Sprites. The location of the ShapeManager’s DrawLayer method determines where the Shapes will be drawn. This gives us the freedom to draw shapes in front of Sprites, behind Sprites, or even both.
Layers work just like they do in the SpriteManager. Layers must be added, and each new layer will lie on top of layers added before. Layers give us the freedom of choosing which objects are drawn first and which will appear on top of others. Unlike the SpriteManager which draws all layers in one method call, we have control over which layers are drawn at which times. Therefore, we can have one layer of lines or shapes appear below Sprites while another can appear between Sprites and text, and the final one can appear on top of everything (excluding Sprites added through the SpriteManager’s AddOverlaySprite method).
If you don’t plan on using multiple layers in your application, then you don’t have to worry about creating any layers; the ShapeManager automatically creates one layer when it is initialized. All methods associated with drawing or adding lines and shapes have an overload which accepts a layer index and an overload which assumes the index will be 0. I will begin with the simplest case where only one layer is used first.
The simplest way to create a shape is to use the ShapeManager’s AddEquilateralPolygon method. The following example shows how to create a triangle, square, octogon, and circle (more or less). Add the following code in your GameData.cs file (assuming you have done the necessary initializations):
// declare variables of class
scope here
LinePolygon lp;
LinePolygon lp2;
public void Initialize(GameForm form)
{
[initialize engine managers
and data]
lp = shaMan.AddEquilateralPolygon(3, new Vector3(-6, 5, 0), 4, 1,
System.Drawing.Color.White);
lp2 = shaMan.AddEquilateralPolygon(4, new Vector3(6, 5, 0), 4, 1,
System.Drawing.Color.Blue);
shaMan.AddEquilateralPolygon(8, new Vector3(-6, -5, 0), 4, 1,
System.Drawing.Color.Green);
shaMan.AddEquilateralPolygon(24, new Vector3(6, -5, 0), 4, 1,
System.Drawing.Color.Red);
}
Although I’m not going to use them immediately, I decided to store the references to the first two LinePolygons so I can make modifications later. Let’s investigate the arguments passed to the method. The first is the number of lines to use for the shape. As we will see later, the AddEquilaterPolygon method actually creates an extra point to close the shape. The first shape is a triangle, second a quadrilateral, and so on as specified above. The second argument is the center point of the shape. The third argument is the radius of the shape. Actually, it specifies the distance from the center of the shape to each point on the shape. As the number of sides increase, the shape begins to look more like a circle and this value actually defines the circle’s radius. The fourth argument is the width of the lines used to draw the shape. The final argument is the color of the lines of shape. The shape itself is not colored in.
Keep in mind that these polygons are actually a collection of points, and that lines will connect end to end rather than outside tip to outside tip. This means that if you create a polygon with width other than 1 you will begin to see small sections missing from where lines connect as can be seen here.
So far, we’ve given the ShapeManager reference to four different shapes, but compiling the code still shows a blank screen (or perhaps just a cursor). This is because we haven’t told the ShapeManager to draw the shapes it is storing. As mentioned in the previous section, we have a choice where we want to place the DrawLayer method. The only limitation is that the call must be between the sprMan.BeginDrawing and sprMan.EndDrawing() method calls in the OnApplicationIdle call in GameMain.cs:
sprMan.CallCustomFunctions();
sprMan.UpdateDependencies();
textMan.UpdateDependencies();
sprMan.Sort();
sprMan.BeginDrawing();
// Where to insert shaMan.DrawLayer(sprMan.ca[0]); ?
//
<-- Insert here to draw before anything
sprMan.Draw(sprMan.ca[0]);
// <-- Insert here to
draw above Sprites and GUI/Cursor but below text
gameData.AfterFrameActivity();
// text is drawn here
//
<-- Insert here to draw above Sprites, GUI/Cursor, and text drawn in
AfterFrameActivity, but below overlay Sprites
sprMan.EndDrawing();
At this point, the decision isn’t that important. Since the only thing drawn on the screen is the Cursor, the decision to put the DrawLayer before or after the sprMan.Draw(sprMan.ca[0]); is the only thing that matters. Of course, if there were Sprites and text in the scene, this would be another story. For now, simply choose a place to add the DrawLayer call. Now the shapes should appear on the screen.
It is possible to move and rotate LinePolygons just like PositionedObjects, however, their behavior is a little different. Consider creating a four point polygon as was done in the example above. In this case, the polygon looks more like a diamond. Each side is angled diagonally rather than being aligned with the x and y axes. As far as the engine is concerned, this is an unrotated polygon. All equilateral polygons are drawn starting with the point directly to the right of the center and rotating counterclockwise. But most people would consider this a rotated square. Fortunately, there is a solution to this.
The LinePolygon gives access to x, y, z, rotX, rotY, and rotZ properties which behave exactly like the PositionedObject’s counterparts. Therefore, adding one to the x value will move the object to the right. Adding PI/4 to rotZ will rotate the object 45 degrees. The question arises as to how the engine determines what 0,0,0 is for rotation and position. For an equilateral polygon, the center point determines its center position, and its orientation is set to 0,0,0 for the rotation values. Therefore, to make the diamond a square, all we have to do is set the rotZ to Math.PI/4.0f (and probably cast it to a float to avoid an error).
The LinePolygon also gives us control over the center point and what to actually consider an unrotated polygon. If, for example, we wanted to take the diamond, rotate it so it is a square, then consider this unrotated, we simply have to call the SetRotation method and pass 0,0,0 as the arguments. The SetRotation and SetCenter methods allow you to change the rotX, rotY, rotZ and x, y, and z values without changing the position of any of the points on the shape.
Feel free to use the lp and lp2 references created in the previous example to play with the x, y, z, rotX, rotY, and rotZ values in the Initialize or Activity method.
Although equilateral polygons are convenient for quick shapes, the x,y,z,rotX,rotY,and rotZ values do not provide as much control as is sometimes needed. LinePolygons can be directly edited through the SetPoint and SetPoints methods.
The SetPoint method requires an index and a Microsoft.DirectX.Vector3 which holds the values for the new point. Calling SetPoint will not affect any of the other values of the LinePolygon; only the point at the particular index. SetPoints, on the other hand, will completely remove all points from the LinePolygon and set the points to the array passed. Also, the LinePolygon will attempt to set a new center for the object. It does this by averaging the position of all of the points and taking the average as the center of the object. In some cases, like circles and squares, this is convenient. This may cause problems for some objects like triangles. Keep in mind that calling SetPoints will also set the new center of the object and you may need to change this manually through the SetCenter method afterwards. The rotation values will not be changed.
The following code in the Initialize method of GameData.cs will create an axis-aligned square:
Vector3[] points = new
Vector3[5]; // need 5 points to close off the shape.
points[0] = new
Vector3(-1, 1, 0);
points[1] = new
Vector3( 1, 1, 0);
points[2] = new
Vector3( 1,-1, 0);
points[3] = new
Vector3(-1,-1, 0);
points[4] = new
Vector3(-1, 1, 0);
LinePolygon square = new
LinePolygon();
square.SetPoints(points);
square.color = System.Drawing.Color.Gainsboro; // Oh, what a common color!
shaMan.AddPolygon(square);
First I create five points. If we only created four, then the last line wouldn’t appear and the square would be “open”. Next, I create the LinePolygon and set its points. I didn’t have to set its color, but frankly, I was curious what color that was. Now we have direct control over the creation of the shape although it is a little bit more code.
Finally I’m going to return to layers, which we haven’t worked with yet but only hinted at them by the name of the DrawLayer method. Add and modify your code so it looks like the following:
Vector3[] points = new
Vector3[5]; // need 5 points to close off the shape.
points[0] = new
Vector3(-1, 1, 0);
points[1] = new
Vector3( 1, 1, 0);
points[2] = new Vector3(
1,-1, 0);
points[3] = new
Vector3(-1,-1, 0);
points[4] = new
Vector3(-1, 1, 0);
LinePolygon square = new
LinePolygon();
square.SetPoints(points);
square.color = System.Drawing.Color.Gainsboro; // Oh, what a common color!
int layerNumber =
shaMan.AddLayer(); // we know this is 1, but just
showing that AddLayer returns an index
shaMan.AddPolygon(square, layerNumber);
Now our ShapeManager has two layers. The default layer (0) and the new layer (1 or layerNumber). If the DrawLayer method is still unchanged, then the ShapeManager is only drawing layer 0 and we have to tell it to draw layer 1 as well. Breaking up LinePolygons into multiple layers allows us to draw things in a specified order and in whichever order we’d like relative to other Sprites/GUI/Cursor, and Text. Select a location in GameMain.cs between sprMan.BeginDrawing and sprMan.EndDrawing and add the following line:
shaMan.DrawLayer(sprMan.ca[0], 1);
Victor
Chelaru
Flat
Red Ball Head Developer/Lead Programmer
Email: VicChelaru@gmail.com
This
article was posted on flatredball.com: 9/01/2005
Modified: 3/01/2006
ã 2004-2006 FlatRedBall. All rights reserved.