Home.NET Game Development of .Nettrix: GDI+ and Col...
Game Development of .Nettrix: GDI+ and Collision Detection
This chapter introduces the basic concepts of GDI+, the extended library for native graphic operations on Windows systems, and discuss one of the most important aspects of game development: collision detection algorithms. (From the book Beginning .NET Game Programming in C# by David Weller, et al., Apress, 2004, ISBN: 1590593197.)
In this chapter we introduce you to the basic concepts of GDI+, the extended library for native graphic operations on Windows systems, and discuss one of the most important aspects of game development: collision detection algorithms. Although game developers use GDI+ functions to draw images on screen, collision detection algorithms are responsible for making the drawings interact with each other. This allows a program to know when an image is over another one and to take the appropriate action, such as bouncing a ball when it hits a wall.
To accomplish these goals and illustrate these concepts, we’ll show you how to create a game called .Nettrix. “Hello World” is always the first program that’s written when learning a new programming language. When learning to program games, Tetris is considered to be the best game to try first. In this simple game, you can see many basic concepts at work—for example, basic graphic routines, collision detection, and handling user input.
To begin, you’ll look at the basic GDI+ concepts and examine the idea of collision detection algorithms, so you’ll have the necessary technical background to code the sample game for this chapter (see Figure 1-1).
Figure 1-1. .Nettrix, this chapter's sample game
Basic GDI+ Concepts
GDI+ is the new .NET Framework class-based application programming interface (API) for 2-D graphics, imaging, and typography.
With some substantial improvements over the old GDI, including better performance and the capacity to run even on a 64-bit system, GDI+ is worth a look. The new features in GDI+ are discussed in the following sections.
Path Gradients
Path gradients allow programs to fill 2-D shapes with gradients with great flexibility, as shown in Figure 1-2.
Figure 1-2. Using path gradients
Alpha Blending
GDI+ works with ARGB colors, which means that each color is defined by a combination of red, green, and blue values, plus an alpha value relating to its degree of transparency. You can assign a transparency value from 0 (totally transparent) to 255 (opaque). Values between 0 and 255 make the colors partially transparent to different degrees, showing the background graphics, if any are present.
Figure 1-3 shows a rectangle with different degrees of transparency; if you had an image below it, you could see it, just like looking through glass.
Figure 1-3. Changing the alpha from 0 to 255 in a solid color bitmap
Cardinal Splines
Cardinal splines allow the creation of smooth lines joining a given set of points, as shown in Figure 1-4.
Figure 1-4. Creating a smooth curve that joins points with a spline
As you can see, the spline curve has fixed starting and ending points (in Figure 1-4, the points marked 1 and 4), and two extra points that will “attract” the curve, but won’t pass through them (points 2 and 3).
Applying Transformations to Objects Using a 3× 3 Matrix
Applying transformations (rotation, translation, or scale) is especially useful when dealing with a sequence of transformations, as they speed up performance. A sample of some transformations is shown in Figure 1-5.
Figure 1-5. Applying a rotation and scale tranformation over a figure
Antialiasing
Antialiasing is the smoothing of graphics, avoiding a stepped look when, for example, a bitmap is enlarged. An image exemplifying this is shown in Figure 1-6.
Figure 1-6. Applying antialiasing to an image
Note: In this book, we’ll show examples of the first two new GDI+ features: path gradients in this chapter and alpha blending in the next. There are many code examples for the other GDI+ features in the .NET Framework SDK.
This chapter is from Beginning .NET Game Programming in C#, by David Weller, et al., (Apress, 2004, ISBN: 1590593197). Check it out at your favorite bookstore today.
When using GDI+, the very first step always is to create a Graphics object, which will help you to perform graphics operations. The Graphics class provides methods for drawing in a specific device context.
There are four ways to attain the correct Graphics object: with the e parameter received in the Paint event, from a window handle, from an image, or from a specified handle to a device context. There’s no real difference among these different approaches; you’ll use each one depending on your program needs. For example, if you are coding your drawing functions on the Paint event of the form, you’ll use the e parameter; but if you are coding a class to draw on a form, you’ll probably want to use a window handle to create the Graphics object. We discuss each method in the sections that follow.
Creating a Graphics Object with the PaintEventArgs Parameter
In this case, all drawing code must be associated with the Paint event of the destination image object. The following code shows how to draw a simple red rectangle at the 10, 20 position (in pixels) on the screen, 7 pixels high and 13 pixels long:
Note: In these first few lines of code, you can see the event-handling features of .NET, as described here:
Every event handler in C# receives at least two parameters, the sender object, which is the object that generates the event, and an object related to the event (the EventArgs object).
The event handler procedure is now associated with the object by associating the method to the event, typically in the InitializeComponent method. The association is done with the += operator like this:
this.PicSource.Paint += new System.Windows.Forms.PaintEventHandler(this.picBackgroundPaint);
The e parameter is of the type Windows.Forms.PaintEventArgs. You will notice that everything in .NET languages is organized into managed units of code, called namespaces. In this case, you use the System.Windows.Forms namespace, which contains classes for creat ing Windows-based applications using the features of the Windows operating system. Inside this namespace, you use the PaintEventArgs class, which basically gives the Paint event access to the rectangle structure that needs to be updated (ClipRectangle property), and the Graphics object used to update it.
The Graphics and SolidBrush classes are defined in the System.Drawing namespace. This namespace has several classes that provide all the functionality you need to work with 2-D draw ings, imaging control, and typography. In the code sample, you create a SolidBrush object with red color (using the Color structure) to draw a filled rectangle using the FillRectangle method of the Graphics object.
Creating Graphics Objects from a Window Handle
In order to create any graphical images in GDI+, you must ask for a “handle” to the drawable part of a window. This handle, which is a Graphics object, can be obtained by the Graphics.FromHwnd method (Hwnd means “Handle from a window”). In the code shown here, Graphics.FromHwnd is a shortcut for the System.Drawing.Graphics.FromHwnd method, which creates a Graphics object used to draw in a specific window or control, given its handle. This code references a pictureBox control named picSource:
Note that the previous code sample will work only if you have a valid bitmap image loaded on the pictureBox control. If you try to execute it against an empty picture box or using a picture box with an indexed pixel format image loaded (such as a JPEG image), you’ll get an error and the Graphics object won’t be created.
Creating a Graphics Object from a Specified Handle to a Device Context
Similar to the previously mentioned methods, the Graphics.FromHdc method creates a Graphics object that allows the program to draw over a specific device context, given its handle. You can acquire the device handle from another Graphics object, using the GetHdc method, as shown in the next code snippet:
public void FromHdc(PaintEventArgs e) { // Get handle to device context. IntPtr hdc = e.Graphics.GetHdc(); // Create new graphics object using handle to device context. Graphics newGraphics = Graphics.FromHdc(hdc); newGraphics. FillRectangle(new SolidBrush(Color.Red), 10, 20, 13, 7); // Release handle to device context. e.Graphics.ReleaseHdc(hdc);
This chapter is from Beginning .NET Game Programming in C#, by David Weller, et al., (Apress, 2004, ISBN: 1590593197). Check it out at your favorite bookstore today.
In the previous section, you saw some code samples used to create solid red rectangles via a SolidBrush object. GDI+ allows the programmer to go beyond flat colors and create linear and path gradients, using special gradient brushes that provide very interesting effects.
GDI+ has features to create horizontal, vertical, and diagonal linear gradients. You can create linear gradients in which the colors change uniformly (the default behavior), or in a nonuniform way by using the Blend property of the gradient brush.
The sample code here shows how to create a uniform gradient brush and draw a rectangle with color changing from red to blue from the upper-left to the lower-right vertex:
Graphics graph; Drawing2D.LinearGradientBrush linGrBrush; graph = Graphics.FromHwnd(picSource.Handle); linGrBrush = new Drawing2D.LinearGradientBrush( new Point(10, 20), // Start gradient point. new Point(23, 27), // End gradient point. Color.FromArgb(255, 255, 0, 0), // Red Color.FromArgb(255, 0, 0, 255)) // Blue graph.FillRectangle(linGrBrush, 10, 20, 13, 7);
Note: The most important part of this sample code is the color definition using the FromArgb method of the Color object. As you can see, each color in GDI+ is always defined by four values: the red, green, blue (RGB) values used by the classic GDI functions, plus the alpha (A) value, which defines the transparency of the color. In the preceding example, you use an alpha value of 255 for both col ors, so they will be totally opaque. Using a value of 128, you create a 50 percent transparent color, so any graphics below are shown through the rectangle. Setting alpha to zero means that the color will be 100 percent transparent, or totally invisible. The in-between values allow different degrees of transparency.
Path gradients allow you to fill a shape using a color pattern defined by a specified path. The path can be composed of points, ellipses, and rectangles, and you can specify one color for the center of the path and a different color for each of the points in the path, allowing the creation of many different effects.
To draw an image using gradient paths, you must create a PathGradientBrush object, based on a GraphicsPath object that is defined by a sequence of lines, curves, and shapes. The code here shows how to draw the same rectangle from the previous examples, using a gradient that starts with a green color in the center of the rectangle and finishes with a blue color at the edges:
// Create a path consisting of one rectangle. graphPath = new Drawing2D.GraphicsPath(); rectSquare = new Rectangle(10, 20, 23, 27); graphPath.AddRectangle(rectSquare); brushSquare = new Drawing2D.PathGradientBrush(graphPath); brushSquare.CenterColor = Color.FromArgb(255, 0, 255, 0); brushSquare.SurroundColors = new Color(){Color.FromArgb(255, 0, 0, 255)};
// Create the rectangle from the path. Graph.FillPath(brushSquare, graphPath);
Note: We won’t go into much detail here about brushes and paths. Refer to the .NET SDK documentation for some extra examples about how to use these features. For a complete overview about this topic, look for “System.Drawing.Drawing2D Hierarchy” in the online help.
In the next section we’ll discuss collision detection, after which you’ll have an understanding of all the basic concepts you need to implement your first game.
This chapter is from Beginning .NET Game Programming in C#, by David Weller, et al., (Apress, 2004, ISBN: 1590593197). Check it out at your favorite bookstore today.
As we said at the start of the chapter, one of the most important concepts in game development is the collision detection algorithm. Some commercial games have gathered significant market shares just because their collision detection routines are faster, leaving more time for the graphics routines and allowing more responsive game play.
Just try to imagine some games without collision detection: a pinball game where the ball won’t bounce; a 3-D labyrinth where players go through the walls and the bullets don’t hit the enemy; an adventure game where the cursor doesn’t know if it’s over a specific object on screen. Without collision detection, a game loses any sense of predictability or reality.
Collision detection is a frequent research topic, and is a constant struggle between the balances of precision versus performance. The main goal here is to examine some basic concepts, so you can use them within the scope of the book and have a stepping stone to provide you with the basic tools and terms used in collision detection.
Note: For those who want to look into this topic in more detail, a simple search on the Internet will show many improved algorithms for advanced collision detection in 2-D and, mostly, in 3-D envi ronments. See Appendix A for other books and papers on collision detection.
In the next sections, you’ll see some common collision detection algorithms.
Bounding Boxes
One of the most common collision detection algorithms, the bounding boxes algorithm, uses the idea of creating boxes around objects in order to test a collision with minimum overhead and, depending on the object, an acceptable degree of precision. In Figure 1-7 you see some objects that you want to test for collisions, along with their bounding boxes.
Figure 1-7. Bounding boxes for an archer and a monster
In the game code, you must test if there’s any overlap between the boxes to test for collision, instead of testing every single pixel of the images. In Figure 1-7, for example, if the box surrounding the arrow touches the box surrounding the monster, it’s a hit.
Using bounding boxes on the sample in Figure 1-7 will probably lead to good results, although as a rule it’s better to use smaller boxes for the player. If a monster blows up when a bullet (or arrow) just misses it by a pixel, the player won’t complain; but if the situation is reversed, the player will feel cheated by the game. It’s better to create a narrower box for the archer to give the player a little more satisfaction.
You can now redefine the boxes as shown in Figure 1-8.
Figure 1-8. Revised bounding boxes for an archer and a monster
Generally speaking, the collision detection technique we’ll describe deals with Axis Aligned Bounding Boxes (AABB). These are bounding boxes that are specifically aligned with the x and y axis on a screen, which keeps all the calculations very simple. The 2-D techniques described here will generally apply to 3-D techniques as well, but the algorithms can get much more complex in three dimensions. In any case, simple 2-D collision detection isn’t really mathematically complex. An easy way to implement the AABB test is to divide the problem into two separate tests.
The first test, called the broad phase, simply tests to see if there’s a chance the two bounding boxes overlap. Imagine that you have a driving game and want to see if two cars are colliding. If one is in Seattle, and the other in New York, there’s little chance they will collide. A broad phase test gives you a sanity check on the boxes in question. If the absolute value of the distance between the centers of the two boxes is less than the sum of the extents (half the width or height of each box), then there’s a chance they overlap on that axis. If the boxes are colliding, then the broad phase test must be true for both axes. This approach is also called a proximity test, which we’ll go into in more detail later in this chapter. Let’s examine this graphically and in code.
In Figure 1-9, you see two rectangles that overlap on the x axis, but not on the y axis. Although the x axis test is true, the y axis test isn’t. Look at the code that does this test:
float Dx = Math.Abs(r2.x - r1.x); float Dy = Math.Abs(r2.y - r1.y); … if (Dx > (r1.extentX+r2.extentX) && (Dy > (r1.extentY+r2.extentY)) // The boxes do not overlap. else // The boxes overlap.
Figure 1-9. Two nonoverlapping boxes
According to the code sample, the two boxes will only overlap if both x and y coordinates of rectangle 2 are within range of rectangle 1. Looking at the diagram, you see that the distance between the two boxes in the x axis is less than their combined extents, so there’s a chance of an overlap. This means that your boxes may be colliding. But the distance in the y axis is greater than the combined extents, which means that no collision is possible.
In Figure 1-10, you do have a collision, because the distances between the boxes (on both axes) are less than the combined extents.
If your broad phase test tells you the two rectangles are in proximity, then you can begin testing for finer-grained collisions. This is done in a variety of ways, but you’ll stick to simple proximity tests for now. You can easily see where the complexity gets higher and higher when you’re dealing with hundreds, if not thousands, of these types of tests. To make it even more complex, imagine that all these bounding boxes are moving in real time. It’s pretty easy to see why complex games need faster computers and graphics cards when you think about challenges like collision testing.
Figure 1-10. Two overlapping boxes
Creating Custom Rectangle Objects
A simple improvement you can do in the algorithm is to create a custom rectangle object that stores two points of the box, the upper-left corner and the bottom-right one, so you can do the tests directly on the variables without having to perform a sum operation.
This method can be easily extended to nonrectangular objects, creating for each object a set of rectangles instead of a single rectangle. For example, for a plane, instead of using a single box (Figure 1-11), you can achieve much better precision using two overlapping boxes (Figure 1-12).
Figure 1-11. Approximating a plane shape with one box
Figure 1-12. Approximating a plane shape with two boxes
The drawback of this approach is that if you use too many boxes, the calculations will take longer, so you need to find a balance between precision and speed for each game or object. In many 3-D graphics applications, proximity tests are done to break the test down into smaller and smaller areas, until you are finally checking the intersection of the part you’re interested in. Using the preceding example, you might break the fuselage bounding box into additional boxes for the landing gear. Then you could do a collision check to see if any of the wheels were touching the runway. You can achieve greater and greater accuracy by successively doing collision checks against smaller and smaller bounding boxes.
Accuracy vs. Precision: What’s the Difference?
Most programmers get confused with issues related to accuracy versus precision, which are two very different things. Look at two examples. Imagine you’re at an archery range with your friend. Your friend shoots an arrow and hits the outside ring of her target. She was accurate, but not precise. You draw your bow and fire, hitting the bull’s-eye—on your friend’s target! Your shot was precise, but not accurate. Another example is the value of pi (p). The number 3 can represent pi, but it’s not very precise. However, it’s a better choice than 2.14159, which is precise, but not accurate.
Computers are precise and accurate with scalar (countable) values like 1, 2, 3, etc., but have challenges with the precision of real numbers. This is based on two fundamental problems in modern computer technology. First, real numbers must be stored in a binary format. While this might seem trivial, it’s a very big challenge. For instance, the simple value of 1/10 cannot be represented consistently in a computer’s floating-point format, because the numbers are stored in base 2 (1/10 in base 2 yields a repeating number). The second challenge stems from how many bits can be dedicated to representing a real number, which limits the accuracy of the number. This results in bunching, where numbers have greater accuracy near zero and for very large values, but not as much in between.
For an advanced paper on this topic, see the reference for “What Every Computer Scientist Should Know About Floating-Point Arithmetic” in Appendix A.
This chapter is from Beginning .NET Game Programming in C#, by David Weller, et al., (Apress, 2004, ISBN: 1590593197). Check it out at your favorite bookstore today.
In the previous code example, we discussed a simple method for checking the proximity of two bounding boxes. Here we’ll show you other ways to calculate proximities for circles and between circles and squares.
The basic idea behind such algorithms is to calculate the distance between the centers of two objects, and then check the value against a formula that describes approximately the objects’ shapes. This method is as precise as the formula used to approximate the object shape—for example, you can have perfect collision detection between balls, in a snooker simulator game, using the right formula.
Some of the most common formulas calculate the distances between squares, circles, and polygons.
Calculating Collision for Circle Objects
Figure 1-13. Circle proximity
When dealing with circular objects, you achieve a perfect calculation using the Pythagorean theorem, which allows you to calculate the distance between the centers (hypotenuse) using the square root of the sum of the squares of the other sides.
if (Distance > Object1.radius + Object2.radius) // => The circles do not collide. else // => The circles are overlapping.
If you just want to check the distance against a constant value, you don’t need to calculate the square root, making operations faster.
Calculating Collision between Circles and Squares
The next algorithm is actually a commonly used formula called Arvo’s Algorithm (named after Jim Arvo, who pioneered many graphics algorithms). It is based on a principal similar to the proximity check between circles, using the Pythagorean theorem once again to help you decide whether the circle and square intersect. Figure 1-14 depicts some different types of proximities that a circle and square could have.
Figure 1-14. Square/Circle proximities
Before we show you the algorithm, create a unit of code, a class, that describes what an Axis Aligned Bounding Box looks like. You’ll use a class to create multiple AABBs, which are called objects. This is the core concept of object-oriented programming, which we’ll cover in more detail as we go along. Since you should already have a beginner’s knowledge of C# syntax, you should find this class description very familiar.
public class AxisAlignedBoundingBox { private float centerX, centerY; // Coordinate centers of the box private float extentX, extentY; // Extents (width from center) of x and y // Constructor public AxisAlignedBoundingBox (float CenterX, float CenterY, float ExtentX, float ExtentY) {…}
public float MaxX { get { return centerX+extentX } } public float MinX { get { return centerX-extentX } } public float MaxY { get { return centerY+extentY } } public float MinY { get { return centerY-extentY } } … public bool CircleIntersect (float CircleCenterX, float CircleCenterY, float Radius) {…} … }
Now that you have a simple description of the class, look at the CircleIntersect method more closely.
public bool CircleIntersect (float CircleCenterX, float CircleCenterY, float Radius) { float dist = 0; // Check x axis. If Circle is outside box limits, add to distance. if (CircleCenterX < this.MinX) dist += Math.Sqr(CircleCenterX – this.MinX); else if (CircleCenterX > this.MaxX) dist += Math.Sqr(CircleCenterX – this.MaxX); // Check y axis. If Circle is outside box limits, add to distance. if (CircleCenterY < this.MinY) dist += Math.Sqr(CircleCenterY – this.MinY); else if (CircleCenterY > this.MaxY) dist += Math.Sqr(CircleCenterY – this.MaxY); // Now that distances along x and y axis are added, check if the square // of the Circle's radius is longer and return the boolean result. return (Radius*Radius) < dist; }
Figure 1-15 shows what the calculation would look like for a circle that intersects an AABB near a corner.
Figure 1-15. Square/Circle proximity algorithm in action
If you think this is too much math, this is probably the place where you should take this book back and take up something less mathematically demanding, like nuclear physics! Honestly, we can’t overemphasize how important math is when it comes to computer games. Basic algebra and geometry are essential for simple games, and very quickly in your career you will need advanced knowledge of linear algebra and physics in order to be an effective game developer. Well over 90 percent of the programming you’ll do when writing games will be related to math (Now don’t you wish you had stayed awake in algebra class??)
This chapter is from Beginning .NET Game Programming in C#, by David Weller, et al., (Apress, 2004, ISBN: 1590593197). Check it out at your favorite bookstore today.
As the number of objects in the game grows, it becomes increasingly difficult to perform all the necessary calculations, so you’ll need to find a way to speed things up. Because there’s a limit to how far you can simplify the calculations, you need to keep the number of calculations low.
The first method to consider is to only perform calculations for the objects that are currently on screen. If you really need to do calculations for off-screen objects, you’ll perform them less frequently than those for on-screen objects.
The next logical step is to attempt to determine which objects are near, and then to calculate the collisions only for those. This can be done using a zoning method. A simple approach is to break a large area down into successively smaller pieces and only check the portions that are important, refining your collision-detection algorithm and decreasing the area you’re testing as you go along. This is a very common approach in complicated games like Doom or Quake. However, if most of your objects are fixed on the screen and have the same size, you can calculate the collisions using tiled game fields (this is sometimes called zoning). This is very common with 2-D games (more about this in later chapters). In this situation, if you have many objects but need to test only one against all others (such as a bullet that may hit enemies or obstacles), you can simply divide the screen in zones and test for special collisions in a particular zone only.
We’ll discuss each of these approaches in the following sections.
Tiled Game Field
The tiled game field approach is the zone method taken to the limit; there’s only one object per area in the zone, and you use a two-dimensional array where each position on the array refers to a tile on the screen. When moving objects, all you have to do is to check the array in the given position to know if there’ll be a collision. In this chapter, you do a simple variation of this method, using a bit array where each bit maps to a tile on the screen. This approach is possible because you only want to store one piece of information—whether the tile is empty or not. If you need to store any extra data about the object (for example, an identifier about the object type), you have to create an integer array to store numbers, and create a mapping table in which each number represents a specific type of object (as you do in the next chapter). Figure 1-16 shows a tiled game where each screen object is held in an array.
Figure 1-16.In a tiled game field, you have an array that maps to screen objects.
Zoning with Bits
If you have a game with many objects but infrequent collisions, you can minimize the number of calculations dividing your screen in zones, and only calculate collisions for objects that are on the same zone. Zones are generally set up according to the number of “collision areas” you want to check, so they’re generally independent of a screen’s resolution. To divide a game field in zones, you create an array to store information about each zone’s y and x axis. So, if you divide your screen into 64 zones (8×8), you need one array with 8 elements to store information about the y axis of each zone, and another array with 8 elements to store information about the x axis of each zone. Figure 1-17 shows an example of such zoning.
If all you want to know is whether a certain zone contains an object (disregarding which one), you can use bytes (instead of arrays) to store the zone information, where each bit will represent a zone on screen; this is called zoning with bits. You can divide your screen in zones according to the number of bits on each variable used: 64 (8×8) zones with a byte, 256 (16×16) zones in an int16, 1024 (32×32) zones in an int32, and so on.
Using the zoning with bits method, at each game loop you reset the variables and, for each object, you process any movement. You then calculate the zone of each object (multiply the current position of the object by the number of zones on each axis and divide by the width or height of the screen), and set the bit corresponding to the result at the x-axis variable and at the y-axis variable, accordingly. You have to set a second bit if the sum of the position and the size of the object (width for x axis, height for y axis) lies in another zone.
Figure 1-17. Dividing a screen into 64 zones
If when checking the variables you see that the bit in both variables is already set, then there’s an object in your zone, so you check all the objects to find out which one it is. Using this method, if you have 15 objects on the screen, and only one collision, you have to do only one check against a given number of objects (14 in the worst case of this scenario), instead of 15 tests with 14 objects. This method has some drawbacks:
You don’t know which object set the bit, so you have to test all the objects looking for the collision.
Some “ghost objects” are created when crossing the bit set for the x zone by one object with the bit set for the y zone by another object, as depicted by Figure 1-18.
Figure 1-18. Using zone bits, if you have big objects (lilke the bricks), there'll be lots of "ghost objects."
This method is most useful when you want to test a group of objects against other objects (for example, bullets against enemies on screen); if you need to test all the objects against each of the others, you’d better use zoning with arrays of bits, as described in the next section.
Zoning with Arrays of Bits
If you have a limited number of objects on screen, you can use two arrays, instead of variables, to define your zones. Each object will correspond to a specific bit in the array elements, so you use byte arrays to control 8 objects, int16 arrays to control 16 objects, and so on, and create a mapping table linking each bit with a specific object. The size of each array will define the number of pixels in a zone for each dimension. For example, creating two arrays each with 10 positions in a 640×480 resolution, you’ll have zones measuring 64 pixels wide by 48 pixels high.
You use the same idea as the previous method to define the zone (or zones) in which each object may be, and then check to see if both x and y array elements aren’t empty. If they aren’t zero, and the bits set in both arrays are the same, then you know for sure that there’s another object near you (not a ghost object), and only check for collision with the one that corresponds to the bit set. An example of this is shown in Figure 1-19.
Figure 1-19. Using zone arrays, you can keep track of which objects are in each zone. The legend shows the bit set in each array element for each object.
This chapter is from Beginning .NET Game Programming in C#, by David Weller, et al., (Apress, 2004, ISBN: 1590593197). Check it out at your favorite bookstore today.
There are many advanced algorithms for 3-D collisions described on game-related sites all over the Internet. We’ll not stress the many implications on including a z axis in the collision detection algorithms; instead you just add some simple extensions to the preceding algorithms. This code sample depicts a proximity test with cube-like objects:
float Dx = Math.Abs(r2.x - r1.x); float Dy = Math.Abs(r2.y - r1.y); float Dz = Math.Abs(r2.z - r1.z); … if (Dx > (r1.extentX+r2.extentX) && (Dy > (r1.extentY+r2.extentY) && (Dz > r1.extentZ+r2.extentZ)) // The boxes do not overlap. else // The boxes overlap.
The next proximity algorithm extends the circle proximity test to use spheres in a 3-D space.
if (Distance > Object1.radius+ Object2.radius) // => The circles do not overlap. else // => The circles are overlapping.
The last proximity test is used for Sphere/Cube intersections. You probably already get the idea on how to extend these simple intersection tests. In the case of Arvo’s Algorithm, you simply add a test for the z axis.
… // Check z axis. If Circle is outside box limits, add to distance. if (CircleCenterZ < this.MinZ) dist += Math.Sqr(CircleCenterZ – this.MinZ); else if (CircleCenterZ > this.MaxZ) dist += Math.Sqr(CircleCenterZ – this.MaxZ);
// Now that distances along x, y, and z axis are added, check if the square // of the Circle's radius is longer and return the boolean result. return (Radius*Radius) < dist; return (Radius*Radius) < dist;
In the next sections you’ll see how to apply these theoretical ideas in a real game project.
This chapter is from Beginning .NET Game Programming in C#, by David Weller, et al., (Apress, 2004, ISBN: 1590593197). Check it out at your favorite bookstore today.
The first step in developing any project is to establish the project’s scope and features.
Note: The main purpose for creating a game proposal is to have clear objectives stated; and everyone involved in the game creation must agree on every point.
For this project we can summarize the scope in a list of desired features, as shown here:
Your game will be a puzzle game, and it’ll be called .Nettrix.
The main objective of the game is to control falling blocks and try to cre ate full horizontal lines, while not allowing the block pile to reach the top of the game field.
The blocks will be made out of four squares (in every possible arrange ment) that fall down in the game field, until they reach the bottom of the field or a previously fallen block.
When the blocks are falling, the player can move the blocks horizontally and rotate them.
When a block stops falling, you’ll check to see if there are continuous hori zontal lines of squares in the game field. Every continuous line must be removed.
The player gets 100 points per removed line, multiplied by the current level.
With each new level, the blocks must start falling faster.
If the stack of blocks grows until it’s touching the top of the game field, the game ends.
This list contains many definitions that are important for any game proposal:
The game genre (e.g., puzzle)
The main objective of the game
The actions the player can perform (e.g., to shoot and to get objects)
Details about how the player interacts with the game and vice-versa: keyboard, intuitive interface, force-feedback joystick, etc.
How the player is rewarded for his or her efforts (points, extra lives, etc.)
How the player gets promoted from one level to the next (in this case, just a time frame)
The criteria for ending the game
Note: In more sophisticated games, there may be other considera tions, such as the storyline, the game flow, details about the level design or level of detail for the maps or textured surfaces, the diffi culty levels for the game, or even details on how the artificial intelligence (AI) of the game should work.
The Game Project
In a commercial game project, the game project starts with a complete game proposal (not just some simple phrases like ours) and continues with a project or functional specification. Although the proposal is written in natural language—so anyone can understand and approve it (including the Big Boss, who will approve or reject the budget for the project)—the project includes programming details that will guide the development team through the coding phase.
It’s not our objective here to explain what must appear in the project documents (it depends largely on the development methodology used by the team), and you won’t create any complete projects because this isn’t the focus of the book. But since it’s not advisable to start any coding without a project, we’ll give you a quick look at projects just to make some implementation details clearer.
Tip: Of course, you can start coding without a project, but even when working alone, a project is the best place to start, since it lets you organize your ideas and discover details that were not clear before you put pen to paper. Even if the project is just some draft annotations, you’ll see that the average quality of your code will improve with its use. The more detailed the project is, the better your code will be, since it’ll help you see the traps and pitfalls along the way before you fall into them.
Object-oriented (OO) techniques are the best to use in game projects, because usually games deal with some representation (sometimes a very twisted one) of the real world, as OO techniques do. For example, in Street Fighter, you don’t have real fighters on the screen; you have some moving drawings, controlled by the player or the computer, that create the illusion of a fight. Using an OO approach to project creation is roughly the same thing: You decide the important characteristics from the real-world objects that you want to represent in your program, and write them down. We aren’t going to go any deeper into this topic at this stage, but you can find some very good books on this topic. Look in Appendix A for recommended books and articles.
Since this is your first program, we’ll walk you through the process of making it step by step, in order to demonstrate how you evolve from the game proposal to the final code; in later chapters you’ll take a more direct approach. In the next sections you’ll see a first version of a class diagram, then pseudocode for the game main program, and after that you’ll go back to the class diagram and add some refinements.
This chapter is from Beginning .NET Game Programming in C#, by David Weller, et al., (Apress, 2004, ISBN: 1590593197). Check it out at your favorite bookstore today.
Start with a simple class diagram (shown in Figure 1-20) illustrating the basic structures of the objects for your game, and then you can add the details and go on refining until you have a complete version. Almost all of the object-oriented analysis methodologies suggest this cyclic approach, and it’s ideal to show how the game idea evolves from draft to a fully featured project.
From this game proposal you can see the first two classes: Block, which will represent each game piece, and Square, the basic component of the blocks.
Figure 1-20. The class diagram—first draft
Based on the game proposal, you can determine some methods (functions) and properties (variables) for the Block class, as described in Table 1-1.
Table 1-1. The Block Class Members
TYPE
NAME
DESCRIPTION
Method
Down
Makes the block go down on the screen
Method
Right
Moves the block right
Method
Left
Moves the block left
Method
Rotate
Rotates the block clockwise
Property
Square 1
Specifies one of the squares that compose the block
Property
Square 2
Specifies one of the squares that compose the block
Property
Square 3
Specifies one of the squares that compose the block
Property
Square 4
Specifies one of the squares that compose the block
Each block is composed of fours objects from the Square class, described in Table 1-2.
Table 1-2. The Square Class Members
TYPE
NAME
DESCRIPTION
Method
Show
Draws the square on the screen at its coordinates (Location property) and with its size (Size property),colored with a specific color (ForeColor property) and filled with BackColor
Method
Hide
Erases the square from the screen
Property
ForeColor
Specifies the square’s foreground color
Property
BackColor
Specifies the square’s background color
Property
Location
Specifies the x,y position of the square on the screen
Property
Size
Specifies the height and width of the square
Comparing the two tables, you can see that there are methods to show and hide the square. Because the squares will be drawn from the Block object, you must have corresponding methods in the Block class and the corresponding properties, too. You can adjust the first diagram accordingly to produce Figure 1-21.
Figure 1-21. The class diagram---second draft
You use SquareSize as the size property for the block, since it’s not important to know the block size, but the block must know the size of the squares so that it can create them.
You can return to this diagram later and adjust it if necessary. Now turn your attention to the game engine, described in the next section.
This chapter is from Beginning .NET Game Programming in C#, by David Weller, et al., (Apress, 2004, ISBN: 1590593197). Check it out at your favorite bookstore today.
Using the C# events jargon, you can think about coding three main events to implement the behaviors described at the game proposal:
When the form loads, you can create the first block.
At the form KeyPress event, you can handle the keyboard input from the user.
With a timer you can call the Down method at each clock tick, producing the desired falling effect for the blocks. As you’ll see later, using a timer isn’t a recommended practice when creating games that need to run at full speed, but that’s not the case here.
Writing pseudo-code is helpful for validating the class diagram, checking whether you use every method and property, and determining whether you can achieve the results stated in the game proposal with those class members. The pseudo-code for your game is shown in the following code sample:
Form_Load Creates an object (named currentBlock) of block class
You’ll use the currentBlock object in all other events, so it must have the same scope as the form.
Form_KeyPress If Left Arrow was pressed, call Left method of currentBlock If Right Arrow was pressed, call Right method of currentBlock If Up Arrow was pressed, call Rotate method of currentBlock If Down Arrow was pressed, call Down method of currentBlock
In the previous pseudo-code, you use the up arrow key to rotate the block and the down arrow key to force the block to go down faster, while the right arrow key and left arrow key move the block in the horizontal direction.
The game engine core will be the timer event. Reviewing the game proposal, you probably see what you must do here: Make the block fall, stop it according to the game rules, check to see if there are any full horizontal lines, and check for the game being over. Possible pseudo-code to do this is shown in the following sample:
If there is no block below currentBlock, and the currentBlock didn't reach the bottom of the screen then Call the Down method of currentBlock Else Stop the block If it's at the top of the screen then The game is over If we filled any horizontal lines then Increase the game score Erase the line Create a new block at the top of the screen
Analyzing this code, you may see some features your current class diagram doesn’t take into account. For instance, how can you check if there is no block below the current block? How can you erase the horizontal line you just managed to fill? We’ll discuss these points in the next section.
The Class Diagram: Final Version
In order to check the previous block positions to see if there are any blocks below the current block or if there are any filled lines, you must have a way to store and check each of the squares of the block, independently of the original blocks (remember, when you erase a line, you can erase just a square or two from a given block). You can do this by creating a new class representing the game field, which will store the information for all squares and have some methods that allow line erasing, among other features. With a quick brainstorm, you can add this class to your model, which will evolve into the diagram shown in Figure 1-22.
Figure 1-22. The final class diagram
Table 1-3 lists the methods and properties of the new class, along with a short description for each one.
Table 1-3. The Game Field Class Members
TYPE
NAME
DESCRIPTION
Properties
Width and Height
Represents the width and height of the game field, measured in squares.
Property
SquareSize
Indicates the size of each square, so you can translate pixels to squares.
Property
ArrGameField
Constitutes an array to store all the squares from all the blocks that stopped falling.
Method
CheckLines
Checks if there are any complete horizontal lines, erasing them if so, and returns the number of erased lines so the main program can increase the player’s score.
Method
IsEmpty
Checks if the square at a particular location (a given x and y) is empty, therefore telling you when a block is in motion.
Method
Redraw
Forces the full redraw of the game field. This will be used when a line has been erased or when another window has overlapped yours.
In a real project, you would possibly go beyond this point, refining all methods to include their interfaces (received parameters and return values) and specifying the data types for the properties, which would probably lead to another revision of your class diagram. But we’ve given you the basic idea here, and that’s the main point.
This chapter is from Beginning .NET Game Programming in C#, by David Weller, et al., (Apress, 2004, ISBN: 1590593197). Check it out at your favorite bookstore today.
When coding any project, it’s always useful to create drivers and stubs to allow you to test each component separately. Drivers are programs that control other lower-level programs, and stubs are programs that mimic low-level programs’ behavior, allowing the testing of higher level code. To provide a vision of a real coding phase, you’ll sometimes use such techniques to validate the code written step by step.
You’ll go through three versions, from your first draft to the final code:
First draft: Code the Square class.
Second draft: Code the Block class.
Final version: Code the GameField class and the game engine.
You start coding from the lowest level class, Square, in the next section.
First Draft: Coding the Square Class
Reviewing the game project, you find the basic structure of the class and create the public class interface.
public class Square { public Point Location; public Size Size; public Color ForeColor; public Color BackColor;
public void Show(System.IntPtr WinHandle) {} public void Hide(System.IntPtr WinHandle) {} }
The class methods are shown in the next section.
The Show and Hide Methods
In the Show method all you need to do is to adapt the code for creating a path gradient rectangle you saw in the previous section. For the Hide method, you can hide the rectangle in an easier way: Since you’ll be working with a one-color background (no textures or bitmaps yet), you can simply draw the rectangle again, this time using a solid color, the same as the background.
To create a generic code that can be updated later by any programmer, it’s always a good idea to not use fixed values inside your program. In this example, you’d better read the game field background color from some variable, so that if it’s updated later to another color, your Hide method will still work. This color value should be a property of the GameField class, but since this property doesn’t appear in your game project, you’ll need to update it with this new property. In a real project it’s common for some details (like this one) to only become visible at the coding phase, since it’s usually not possible for the project to predict all possible details.
The code for the Square class is shown here:
public class Square { public Point Location; public Size Size; public Color ForeColor; public Color BackColor;
// Draws a rectangle with gradient path using the properties above. public void Show(System.IntPtr WinHandle) { Graphics GameGraphics; GraphicsPath graphPath; PathGradientBrush brushSquare; Color[] surroundColor; Rectangle rectSquare;
// Gets the Graphics object of the background picture. GameGraphics = Graphics.FromHwnd(WinHandle);
// Creates a path consisting of one rectangle. graphPath = new GraphicsPath(); rectSquare = new Rectangle(Location.X,Location.Y,Size.Width,Size.Height); graphPath.AddRectangle(rectSquare);
// Creates the gradient brush that will draw the square. // Note: There's one center color and an array of border colors. brushSquare = new PathGradientBrush(graphPath); brushSquare.CenterColor = ForeColor; surroundColor = new Color[]{BackColor }; brushSquare.SurroundColors = surroundColor;
// Finally draws the square. GameGraphics.FillPath(brushSquare, graphPath); } public void Hide(System.IntPtr WinHandle) { Graphics GameGraphics; Rectangle rectSquare;
// Gets the Graphics object of the background picture. GameGraphics = Graphics.FromHwnd(WinHandle);
// Draws the square. rectSquare = new Rectangle(Location.X,Location.Y,Size.Width,Size.Height); GameGraphics.FillRectangle(new SolidBrush(GameField.BackColor), rectSquare);
} }
Note: In the Hide method shown previously, you can see an unusual use of the BackColor property: You are using the property directly from the class definition, instead of from a previously created object in this class. In this case, you are using a new feature of .NET: static properties and methods. Defining a method or a property as public static makes it available for any part of the program directly from the class name, without the need for explicitly creating an object. An important point is that the property or method is shared by all the instances of the objects created from the class. For example, you can have a static counter property that each object increments when it’s created and decrements when it’s destroyed, and any object can read this counter at any time in order to see how many objects are available at any given time.
This chapter is from Beginning .NET Game Programming in C#, by David Weller, et al., (Apress, 2004, ISBN: 1590593197). Check it out at your favorite bookstore today.
Now you are ready to test your program. To do this, you’ll need to create a driver to call the class (a window with a button and a pictureBox will suffice), and a stub for the GameField class, since your Square class uses the BackColor property of this class.
The stub is very simple, just a new file composed of the code lines shown in the next sample:
public class GameField { public static Color BackColor; }
The driver will be replaced by the main program in the final version, so you can implement it as code on the form that will be used as the game user interface. In this case, you can create a simple form with a picture (picBackground) and a button (cmdStart), with the code to create the objects and set the properties of the Square class, then call the Draw method.
private void cmdStart_Click(Object sender, System.EventArgs e) { Square square = new Square(); square.Location = new Point(40, 20); square.Size = new Size(10, 10); square.ForeColor = Color.Blue; square.BackColor = Color.Green;
// Set the background property of GameField class. GameField.BackColor = picBackground.BackColor;
// Draw the square. square. Draw(picBackground.Handle); }
Running the code, you can see the fruits of your labor: a nice path gradient– colored square is drawn on screen as shown in Figure 1-23.
Figure 1-23. Your first results with GDI+
Because in your game the squares won’t change color or size, you can assign these values when creating the objects, creating a new constructor in the Square class to do this, as illustrated in the next code sample:
So the code for your Start button will be as follows:
private void cmdStart_Click(object sender, System.EventArgs e) { // Clean the game field. Square square = new Square(new Size(10, 10), Color.Blue, Color.Green);
// Set the location of the square. square.Location = new Point(40, 20); // Set the background property of GameField. GameField.BackColor = picBackground.BackColor; // Draw the square. square.Draw(picBackground); }
Now that everything is working correctly, continue with the coding by looking at the Block class.
This chapter is from Beginning .NET Game Programming in C#, by David Weller, et al., (Apress, 2004, ISBN: 1590593197). Check it out at your favorite bookstore today.
You can map the Block class, defined in the class diagram created for your game project, to the final class interface, including the data types for the properties and parameters for the methods. The proposed class interface is shown in the next code listing:
public class Block { // The four squares that compose a block public Square square1; public Square square2; public Square square3; public Square square4; private const int squareSize = GameField.SquareSize;
public Block(Point location, BlockTypes newBlockType) {…} public bool Down() {…} public bool Right() {…} public bool Left() {…}
public void Rotate() {…} public void Show(System.IntPtr WinHandle) {…} public void Hide(System.IntPtr WinHandle) {…} }
In the game proposal, we said that the blocks will be composed of four squares (in every possible arrangement). You can start the coding by thinking about the possible combinations, and give each of them a name, as shown in Figure 1-24.
Figure 1-24. The square arrangements to form each block
Because each block will have a specific square combination, you can think of three new elements for your class: a BlockType property, an enumeration for the block types, and a constructor that creates the squares in the desired positions and the color of each square. To give a visual clue to the player, the colors must be fixed for each block type, so it’s a good idea to create arrays to hold the forecolor and backcolor for each type. The extra definitions for the class are shown in the next code listing:
public enum BlockTypes { Undefined = 0, Square = 1, Line = 2, J = 3, L = 4, T = 5, Z = 6, S = 7
}; public BlockTypes BlockType;
// The colors of each block type private Color[] backColors = {Color.Empty, Color.Red, Color.Blue, Color.Red, Color.Yellow, Color.Green, Color.White, Color.Black}; private Color[] foreColors = {Color.Empty, Color.Purple, Color.LightBlue, Color.Yellow, Color.Red, Color.LightGreen,Color.Black, Color.White};
The constructor will receive two parameters: the block type and the location where the block will be created. Since you need random block types, you can pass an Undefined value for the block type when you want to randomly create a block.
You might wonder why you allow anything other than Undefined for the block type in the first place, since during gameplay the blocks are randomly generated. The reason is that it makes testing far easier—you can test specific block types as you build up your game, giving you more control over incrementally testing the game. The code to do this is shown in the following listing:
public Block(Point location, BlockTypes newBlockType) { //Create the new block, choose a new type if necessary. if (newBlockType==BlockTypes.Undefined) { BlockType = (BlockTypes)(random.Next(7)) + 1; } else { BlockType = newBlockType; } // Create each of the squares of the block. // Set the square colors, based on the block type. square1 = new Square(new Size(squareSize, squareSize), backColors[(int)BlockType], foreColors[(int)BlockType]); square2 = new Square(new Size(squareSize, squareSize), backColors[(int)BlockType], foreColors[(int)BlockType]); square3 = new Square(new Size(squareSize, squareSize), backColors[(int)BlockType], foreColors[(int)BlockType]); square4 = new Square(new Size(squareSize, squareSize), backColors[(int)BlockType], foreColors[(int)BlockType]);
// Set the square positions based on the block type. switch(BlockType) { case BlockTypes.Square: // Create a Square block…. break; case BlockTypes.Line: // Create a Line block…. break; case BlockTypes.J: // Create a J block…. break; case BlockTypes.L: // Create an L block…. break; case BlockTypes.T: // Create a T block…. break; case BlockTypes.Z: // Create a Z block…. break; case BlockTypes.S: // Create an S block…. break; } }
In this sample, the code inside each case statement must set the square positions, based on each block type, according to Figure 1-24. For example, analyze the Square block type, depicted in Figure 1-25.
Figure 1-25. The squares for the Square block type
The code for creating the Square block type is shown here:
case BlockTypes.Square: square1.Location = new Point(Location.X, Location.Y); square2.Location = new Point(Location.X+squareSize, Location.Y); square3.Location = new Point(Location.X, Location.Y+squareSize); square4.Location = new Point(Location.X+squareSize, Location.Y+squareSize); break;
As for the Line block type, the squares that compose it are shown in Figure 1-26.
Figure 1-26. The squares for the Line block type
The code for the Line block type is as follows:
case BlockTypes.Line: square1.Location = new Point(Location.X, Location.Y); square2.Location = new Point(Location.X, Location.Y+squareSize); square3.Location = new Point(Location.X, Location.Y+2*squareSize); square4.Location = new Point(Location.X, Location.Y+3*squareSize); break;
The code for the other blocks follows the same idea. For the full code of the constructor, check the downloadable source code. Once the blocks are created, you can start coding the moving operations over them, as described in the next section.
This chapter is from Beginning .NET Game Programming in C#, by David Weller, et al., (Apress, 2004, ISBN: 1590593197). Check it out at your favorite bookstore today.
The next methods, following the class diagram order, are Down, Right, and Left. These methods are fairly simple, since all you need to do is to update the block position in the defined direction, regardless of the block type. The basic code for the Down procedure could be as simple as this:
public bool Down() { // Hide the block (in the previous position). Hide(GameField.WinHandle); // Update the block position. square1.location = new Point(square1.location.X, square1.location.Y+squareSize); square2.location = new Point(square2.location.X, square2.location.Y+squareSize); square3.location = new Point(square3.location.X, square3.location.Y+squareSize); square4.location = new Point(square4.location.X, square4.location.Y+squareSize); // Draw the block in the new position. Show(GameField.WinHandle); return true; }
Because you need to hide and redraw the block every time these methods are called, you can reduce the calling overhead by creating a new static property on the GameField class, the WinHandle, which was used in the preceding code.
This handle is a copy of the handle of the PicBackground, which is used as the game field on the form. With this approach, you can set this property in the constructor and use it for every drawing operation, instead of passing the handle as a parameter to the drawing methods every time it’s called.
The Right and Left methods will be similar to this one, except this time the horizontal block position is changed—incremented to move the block to the right and decremented to move the block to the left. You move the blocks using the default value of the SquareSize property, assigned to 10 in the class definition. This means that the blocks will always move a square down, left, or right, so you don’t have to worry about the square’s alignment.
There’s one more detail to include in this procedures: the test for collision detection. The block can’t move down, left, or right if there are any squares (or screen limits) in the way. Since the block itself can’t know if other blocks are in the way, it must ask the GameField class if it can move this way. This is already considered in the game project: The IsEmpty method of the GameField class will check if a specified square in the game field is empty.
In the Down method, you must check if there are any blocks in the way and stop your block from falling if it hits an obstacle. When the block stops falling, you must inform the GameField class of this, so it can update its internal controls to allow the proper function of the IsEmpty method. You can do this by creating a new method, named StopSquare, which will inform the GameField that a specific square is now not empty, and pass the square object and its coordinates as parameters. After that, each square will be treated separately from each other (no more blocks) by the GameField class, because when a line is removed, some squares of the block can be removed while others remain.
Since the IsEmpty and StopSquare methods are based on an array of Squares, ArrGameField (as defined in your game project), the logical approach is for these methods to receive the array coordinates to be used. You can translate screen coordinates to array positions by simply dividing the x and y position of each square by the square size.
The final code for the Down procedure will now be as follows:
public bool Down() { // If there's no block below the current one, go down. if (GameField.IsEmpty(square1.location.X/squareSize, square1.location.Y/squareSize+1) && GameField.IsEmpty(square2.location.X/squareSize, square2.location.Y/squareSize+1) && GameField.IsEmpty(square3.location.X/squareSize, square3.location.Y/squareSize+1) && GameField.IsEmpty(square4.location.X/squareSize, square4.location.Y/squareSize+1)) { // Hide the block (in the previous position). Hide(GameField.WinHandle);
// Update the block position. square1.location = new Point(square1.location.X, square1.location.Y+squareSize); square2.location = new Point(square2.location.X, square2.location.Y+squareSize); square3.location = new Point(square3.location.X, square3.location.Y+squareSize); square4.location = new Point(square4.location.X, square4.location.Y+squareSize); // Draw the block in the new position. Show(GameField.WinHandle); return true; } else { // If there's a block below the current one, doesn’t go down. // -> Put it on the array that controls the game and return FALSE. GameField.StopSquare(square1,square1.location.X/squareSize, square1.location.Y/squareSize); GameField.StopSquare(square2,square2.location.X/squareSize, square2.location.Y/squareSize); GameField.StopSquare(square3,square3.location.X/squareSize, square3.location.Y/squareSize); GameField.StopSquare(square4,square4.location.X/squareSize, square4.location.Y/squareSize); return false; } }
In this code sample, you use the GameField class again with static methods (no objects created). The concepts of static properties and methods were explained earlier in this chapter.
The Right and Left methods are very similar to this one, with the slight difference that you don’t stop the block if it can’t go right or left. The code for the Right method is shown next. The Left method is built upon the same basic structure.
public bool Right() { // If there's no block to the right of the current one, go right. if (GameField.IsEmpty(square1.location.X/squareSize+1, square1.location.Y/squareSize) && GameField.IsEmpty(square2.location.X/squareSize+1, square2.location.Y/squareSize) && GameField.IsEmpty(square3.location.X/squareSize+1, square3.location.Y/squareSize) && GameField.IsEmpty(square4.location.X/squareSize+1, square4.location.Y/squareSize)) { // Hide the block (in the previous position). Hide(GameField.WinHandle); // Update the block position. square1.location = new Point(square1.location.X+squareSize, square1.location.Y); square2.location = new Point(square2.location.X+squareSize, square2.location.Y); square3.location = new Point(square3.location.X+squareSize, square3.location.Y); square4.location = new Point(square4.location.X+squareSize, square4.location.Y); // Draw the block in the new position. Show(GameField.WinHandle) return true; } else { // If there's a block to the right of the current one, // don't go right and return FALSE. return false; } }
The next method for the Block class, Rotate, is a little more complicated, so we’ll give you a closer look at it in the next section.
This chapter is from Beginning .NET Game Programming in C#, by David Weller, et al., (Apress, 2004, ISBN: 1590593197). Check it out at your favorite bookstore today.
Although in the previously discussed methods all you needed to do was to change a single coordinate for all the squares of the block (incrementing y to go down, and modifying x to go right or left), in this case you need to change the squares’ positions, one by one, to achieve the effect of rotation. The rotation movement must be based on the block type and on the current orientation of the block.
To track the current rotation applied to the block, you need a new property. Creating a new enumeration for the possible rotation status will make your code more readable.
public enum RotationDirections { NORTH = 1, EAST = 2, SOUTH = 3, WEST = 4 };
public RotationDirections StatusRotation = RotationDirections.NORTH;
In order to make the method simpler, and to avoid calculating the rotation twice—once to test for empty squares and again to rotate the block—you store the current position, rotate the block, and then test to see if the squares of the new block position are empty. If so, you just draw the block in the new position. If not, you restore the previous position.
The basic structure for the method (without the rotation code for each block type) is shown next:
public void Rotate() { // Store the current block position. Point OldPosition1 = square1.Location; Point OldPosition2 = square2.Location; Point OldPosition3 = square3.Location; Point OldPosition4 = square4.Location; RotationDirections OldStatusRotation = StatusRotation; Hide(GameField.WinHandle); // Rotate the blocks. switch(BlockType) { case BlockTypes.Square: // Here will go the code to rotate this block type. break; case BlockTypes.Line: // Here will go the code to rotate this block type. break; case BlockTypes.J: // Rotate all squares around square 3. break; case BlockTypes.L: // Rotate all squares around square 3. break; case BlockTypes.T: break; case BlockTypes.Z: // Rotate all squares around square 2. break; case BlockTypes.S: // Rotate all squares around square 2. break; } // After rotating the squares, test if they overlap other squares. // If so, return to original position. if (!(GameField.IsEmpty(square1.Location.X/squareSize, square1.Location.Y/squareSize) && GameField.IsEmpty(square2.Location.X/squareSize, square2.Location.Y/squareSize) && GameField.IsEmpty(square3.Location.X/squareSize, square3.Location.Y/squareSize) && GameField.IsEmpty(square4.Location.X/squareSize, square4.Location.Y/squareSize))) { StatusRotation = OldStatusRotation; square1.Location = OldPosition1; square2.Location = OldPosition2; square3.Location = OldPosition3; square4.Location = OldPosition4; } // Draws the square at the correct position. Show(GameField.WinHandle); }
Based on each block type and its current status, you can calculate the rotations. There will be three types of rotation:
Square blocks: These do nothing. Squares don’t need to rotate since they look the same when rotated.
Line, S, and Z blocks: These will have only two possible directions for rotation, north and east.
T, J, and L blocks: These will have four different positions—north, east, south, and west.
In any case, you must choose a specific square to stay fixed while the others rotate around it. In the examples that follow, you see what must be in each case statement of the Rotate method, starting with the rotation for a Line block type, represented in Figure 1-27.
The code to implement the rotation of the Line block is shown in the next listing:
switch(StatusRotation) { case RotationDirections.NORTH: StatusRotation = RotationDirections.EAST; square1.Location = new Point (square2.Location.X-squareSize, square2.Location.Y); square3.Location = new Point (square2.Location.X+squareSize, square2.Location.Y); square4.Location = new Point (square2.Location.X+2*squareSize,square2.Location.Y); break; case RotationDirections.EAST: StatusRotation = RotationDirections.NORTH; square1.Location = new Point (square2.Location.X, square2.Location.Y-squareSize); square3.Location = new Point (square2.Location.X, square2.Location.Y+squareSize); square4.Location = new Point (square2.Location.X, square2.Location.Y+2*squareSize); break; }
Notice that the new square positions are all based on the position of the second square of the block; you just add or subtract the square sizes to move the square up and down (y coordinate) or right and left (x coordinate). In each case, you set the new status of the rotation.
Figure 1-27. Line block: rotation around the second square
Figure 1-28 illustrates the rotation for the Z block type. The S and Z block types rotate in a very similar way.
Figure 1-28. The Z block rotation
Following is the code for the Z block type; the S block follows the same logic.
switch(StatusRotation) { case RotationDirections.NORTH: StatusRotation = RotationDirections.EAST; square1.Location = new Point(square2.Location.X, square2.Location.Y-squareSize); square3.Location = new Point(square2.Location.X-squareSize, square2.Location.Y); square4.Location = new Point(square2.Location.X-squareSize, square2.Location.Y+squareSize); break; case RotationDirections.EAST: StatusRotation = RotationDirections.NORTH; square1.Location = new Point(square2.Location.X-squareSize, square2.Location.Y); square3.Location = new Point(square2.Location.X, square2.Location.Y+squareSize); square4.Location = new Point(square2.Location.X+ squareSize, square2.Location.Y+squareSize); break; }
As for the T, J, and L block types, the procedure will be a little longer, since you have four directions, but the basic idea remains the same: All squares run around a fixed one. We’ll show you some examples, starting with the T block type rotation, portrayed in Figure 1-29.
Figure 1-29.Rotation of the T block
The next code listing implements the rotation illustrated in Figure 1-29:
switch(StatusRotation) { case RotationDirections.NORTH: StatusRotation = RotationDirections.EAST; square1.Location = new Point(square2.Location.X, square2.Location.Y-squareSize); square3.Location = new Point(square2.Location.X, square2.Location.Y+squareSize); square4.Location = new Point(square2.Location.X-squareSize, square2.Location.Y); break; case RotationDirections.EAST: StatusRotation = RotationDirections.SOUTH; square1.Location = new Point(square2.Location.X+squareSize, square2.Location.Y); square3.Location = new Point(square2.Location.X-squareSize, square2.Location.Y); square4.Location = new Point(square2.Location.X, square2.Location.Y-squareSize); break; case RotationDirections.SOUTH: StatusRotation = RotationDirections.WEST; square1.Location = new Point(square2.Location.X, square2.Location.Y+squareSize); square3.Location = new Point(square2.Location.X, square2.Location.Y-squareSize); square4.Location = new Point(square2.Location.X+squareSize, square2.Location.Y); break; case RotationDirections.WEST: StatusRotation = RotationDirections.NORTH; square1.Location = new Point(square2.Location.X-squareSize, square2.Location.Y); square3.Location = new Point(square2.Location.X+ squareSize, square2.Location.Y); square4.Location = new Point(square2.Location.X, square2.Location.Y+squareSize); break; }
The code for rotating the J and L blocks is pretty much like the preceding code sample. The main difference is that these blocks will rotate around the third square, as shown in the rotation for the J block illustrated in Figure 1-30.
Figure 1-30. Rotation for the J block
The last two methods for the Block class are discussed in the next section.
This chapter is from Beginning .NET Game Programming in C#, by David Weller, et al., (Apress, 2004, ISBN: 1590593197). Check it out at your favorite bookstore today.
The implementation of the Show and Hide methods is very straightforward; the Show and Hide methods are called for each of the block squares, as shown here:
public void Show(System.IntPtr WinHandle) { // Draws each square of the block on the game field. square1.Show(WinHandle); square2.Show(WinHandle); square3.Show(WinHandle); square4.Show(WinHandle); }
public void Hide(System.IntPtr WinHandle) { // Hides each square of the block on the game field. square1.Hide(WinHandle); square2.Hide(WinHandle); square3.Hide(WinHandle); square4.Hide(WinHandle); }
To see the full code for the Block class, refer to the samples in the downloadable source code. To test your new class, you’ll have to create a new stub for the GameField class and update your main program, as shown in the next section.
Testing the Block Class
The new stub for the GameField class must include the properties and methods accessed by the Block class, as shown in the next code listing:
public class GameField { public static System.IntPtr WinHandle; public static Color BackColor;
public static bool IsEmpty(int x, int y) { return true; }
public static void StopSquare(Square Square, int x, int y) { }
The IsEmpty method always returns True; you’ll add code for IsEmpty and StopSquare in the final version of the program. The next code listing shows the logic for testing the Block class, and must be included in the game field form:
private Block CurrentBlock; … private void NetTrix_Load(object sender, System.EventArgs e) { // Set the properties of GameField class. GameField.BackColor = PicBackground.BackColor; GameField.WinHandle = PicBackground.Handle; }
private void CmdStart_Click(object sender, System.EventArgs e) { CurrentBlock = new Block(new Point(GameField.SquareSize * 6, 50), Block.BlockTypes.Undefined); CurrentBlock.Show(PicBackground.Handle); } private void NetTrix_KeyDown(object sender, System.Windows.Forms.KeyEventArgs e) { switch(e.KeyCode) { case Keys.Right: CurrentBlock.Right();break; case Keys.Left : CurrentBlock.Left();break; case Keys.Up : CurrentBlock.Rotate();break; case Keys.Down : CurrentBlock.Down();break; default: break; } }
Note: All the constants in the .NET Framework are organized within enumerations. This approach allows a more intuitive orga nization, so it’s easier to find exactly what you need in the help feature. The intelligence of Visual Studio was also improved, giving more hints and softening the learning curve. In the preceding sam ple code, you use the Keys enumeration to get the key code (Left, Down, Up, and Right). There are also modifiers to test if Shift, Ctrl, and Alt keys are pressed. The namespace for the Keys enumeration can be found in System.Windows.Forms.
To test the program, just run it, click the Start button, and press the various keys to move the blocks: The down arrow key makes the block go down, the up arrow key rotates the block, and the right arrow and left arrow keys move the block horizontally. Clicking the Start button again will create a new block, so you can test the random creation of different block types. A sample screen is shown in Figure 1-31.
Figure 1-31.Testing the Block class
In the next section you’ll implement the collision detection and the main program logic, finishing your game.
This chapter is from Beginning .NET Game Programming in C#, by David Weller, et al., (Apress, 2004, ISBN: 1590593197). Check it out at your favorite bookstore today.
To finish your program, you’ll have to complete the code for the game engine and the GameField class, as shown in the next sections.
GameField Class
Examine the code to implement the public properties and methods for the GameField class, as defined in your game project.
public class GameField { public const int Width = 16; public const int Height = 30; public const int SquareSize = 10; public static System.IntPtr WinHandle; public static Color BackColor;
private static Square[,] arrGameField = new Square[Width, Height];
public static bool IsEmpty(int x, int y) {…} public static int CheckLines() {…} public static void StopSquare(Square Square, int x, int y) {…} public static void Redraw() {…} public static void Reset() {…} }
The GameField interface shown in the preceding code has its members (properties and methods) defined in the class diagram proposed in the game project, plus the new properties and methods defined in the stubs you created previously. Although it isn’t unusual for such changes to happen during a real-life project, it should be one of your goals to define a clear and comprehensive project before starting to code. Remember, changing a project is far easier (and cheaper) than changing and adapting code; and if there are many unpredictable changes to code, the project tends to be more prone to errors and more difficult to maintain. (We refer to this as the Frankenstein syndrome: The project will no longer be a single and organized piece of code, but many not so well-sewed-on parts.)
One interesting point about this class is that every member is declared as static! In other words, you can access any method or property of the class without creating any objects. This isn’t the suggested use of static properties or methods; you usually create static class members when you need to create many objects in the class, and have some information—such as a counter for the number of objects created, or properties that, once set, affect all the objects created.
The next sections discuss the GameField class methods, starting with the IsEmpty method.
The IsEmpty Method
The first class method, IsEmpty, must check if a given x,y position of the game array (arrGameField) is empty. The next method, CheckLines, has to check each of the lines of the array to see if any one of them is full of squares, and remove any such lines.
Since the arrGameField is an array of Square objects, you can check if any position is assigned to a square with a simple test.
public static bool IsEmpty(int x, int y) { return arrGameField[x,y] != null; }
Some extra tests should be done to see if the x or the y position is above (or below) the array boundaries.
Although in this game you don’t need high-speed calculations, you can use an improved algorithm for collision detection, so that you can see a practical example of using these algorithms.
You can improve the performance of the IsEmpty and CheckLines functions using an array of bits to calculate the collisions. Since your game field is 16 squares wide, you can create a new array of integers, where each bit must be set if there’s a square associated with it. You still must maintain the arrGameField array, because it will be used to redraw the squares when a line is erased or the entire game field must be redrawn (for example, when the window gets the focus after being below another window).
The array that holds the bits for each line must have the same Height as the arrGameField, and will have just one dimension, since the Width will be given for the bits in each integer (16 bits per element). When a square stops inside the game field, a bit will be set (inside the StopSquare method) that will indicate a square is occupying that spot. The array definition is shown in the next code line:
private static int[] arrBitGameField = new int[Height];
And the IsEmpty function is as follows:
public static bool IsEmpty(int x, int y) { // If the y or x is beyond the game field, return false. if ((y<0||y>=Height)||(x<0||x>=Width)) { return false; } // Test the xth bit of the yth line of the game field. else if((arrBitGameField[y] & (1<<x)) !=0) { return false; } return true; }
In this sample code, the first if statement checks whether the x and y parameters are inside the game field range. The second if statement deserves a closer look: What is arrBitGameField[y] & (1<< x) supposed to test? In simple words, it just checks the xth bit of the arrBitGameField[y] byte.
This piece of code works well because the comparison operators work in a binary way. The & operator performs a bit-to-bit comparison, then returns a combination of both operands. If the same bit is set in both operands, this bit will be set in the result; if only one or none of the operators has the bit set, the result won’t have the bit set. Table 1-4 shows the operands’ bits for some & comparisons.
Table 1-4. Bits and Results for Some & Operations
NUMBERS
BITS
1 & 2 = 0
01 & 10 = 0 (false)
3 & 12 = 0
0011 & 1100 = 0000 (false)
3 & 11 = 3
0011 & 1011 = 0011 (true)
In your code, if you want to check, for example, the seventh bit, the first operand must be the array element you want to check, arrBitGameField[y], and the second operand must have the bits 00000000 01000000 (16 bits total, with the seventh one checked).
If you did your binary homework well, you’d remember that setting the bits one by one results in powers of 2: 1, 2, 4, 8, 16, and so on, for 00001, 00010, 00100, 01000, 10000, etc. The easiest way to calculate powers of 2 is just to shift the bits to the left; fortunately for you, C# has operators that will do bit shifting (<< for shifting bits to the left, and >> for shifting bits to the right).
Looking again at the second if statement, everything should make sense now:
arrBitGameField[y]: The 16 bits of the yth line of the game field.
1<<X: Shifts one bit over to the xth position.
arrBitGameField[y] & (1<< x): If the xth bit of the array element is set, then the test will return a nonzero number; any other bit set won’t affect the result, since the second operand has only the xth bit set.
This chapter is from Beginning .NET Game Programming in C#, by David Weller, et al., (Apress, 2004, ISBN: 1590593197). Check it out at your favorite bookstore today.
In the next GameField method, CheckLines, you need to check if a line is totally filled (all bits set) and, if so, erase this line and move down all the lines above it. You don’t need to copy the empty lines (all bits reset) one on top of another, but you must return the number of cleared lines. To improve the readability of your code, you define some private constants for the class.
private const int bitEmpty = 0x0; //00000000 0000000 private const int bitFull = 0xFFFF; //11111111 1111111
See the comments in the code and the following explanation to understand the function.
public static int CheckLines() {
int CheckLines_result = 0; // Returns the number of lines completed. int y = Height - 1;
while ( y >= 0) { // Stops the loop when the blank lines are reached. if (arrBitGameField[y]==bitEmpty) y = 0; // If all the bits of the line are set, then increment the // counter to clear the line and move all above lines down. if (arrBitGameField[y]==bitFull) { // Same as: if ((arrBitGameField(y) ^ bitFull) = 0 CheckLines_result++;
// Move all next lines down. for(int index = y; index >= 0; index--) { // If the current line is NOT the first of the game field, // copy the line above. if (index>0) { // Copy the bits from the line above. arrBitGameField[index] = arrBitGameField[index-1];
// Copy each of the squares from the line above. for(int x=0; x<Width; x++) { // Copy the square. arrGameField[x, index] = arrGameField[x, index-1]; // Update the Location property of the square. if (arrGameField[x, index] != null) arrGameField[x, index].location = new Point(arrGameField[x, index].location.X, arrGameField[x, index].location.Y+SquareSize); } } else { // If the current line is the first of the game field // just clear the line. arrBitGameField[index] = bitEmpty; for(int x=0; x<Width; x++) { arrGameField[x, index] = null; } } } } else { y--; } } return CheckLines_result; }
In the CheckLines method, you can see the real benefits of creating arrBitGameField for collision detection: You can check if a line is completely filled or empty with only one test, with the use of bitFull and bitEmpty constants you previously created, avoiding the 16 tests you would have had to create for each of the ArrGameField members in a line. The next code listing highlights these tests:
if (arrBitGameField[y]==bitFull) //The line is full. if (arrBitGameField[y]==bitEmpty) //The line is empty.
The next section discusses the last two methods for the GameField class.
The StopSquare and Redraw Methods
The last two methods, StopSquare (which sets the arrays when a block stops falling) and Redraw (which redraws the entire game field), have no surprises. The code implementing these methods is shown in the next listing:
public static void StopSquare(Square Square, int x, int y) { arrBitGameField[y] = arrBitGameField[y] | (1<<x); arrGameField[x, y] = Square; }
public static void Redraw() { for(int y=Height-1; y>=0; y--) if (arrBitGameField[y]!=bitEmpty) for(int x=Width-1; x>=0; x--) if (arrGameField[x, y] != null) arrGameField[x, y].Show(WinHandle); }
The next section shows the code for the final version of the main program, finishing your game code.
This chapter&, by David Weller, et al., (Apress, 2004, ISBN: 1590593197). Check it out at your favorite bookstore today.
Now that all the base classes are coded, you’ll finish the main procedures.
In the first drafts for the game engine, you used the form procedures to call methods in your base classes, so you could see if they were working well. Now, the game engine must be coded to implement the features defined in the game proposal, stated earlier in this chapter. Remind yourself of the pseudo-code defined in the game project.
Form_Load Creates an object (named currentBlock) of block class FormKeyPress If Left Arrow was pressed, call Left method of currentBlock If Right Arrow was pressed, call Right method of currentBlock If Up Arrow was pressed, call Rotate method of currentBlock If Down Arrow was pressed, call Down method of currentBlock TimerTick If there is no block below currentBlock, and the currentBlock didn't reach the bottom of the screen then Call the Down method of currentBlock Else Stop the block If it's at the top of the screen then The game is over If we filled any horizontal lines then Increase the game score Erase the line Create a new block at the top of the screen
Before starting to translate this pseudo-code to C#, it’s important to stress two points:
It’s not common to use timer objects to control games. The timer object doesn’t have the necessary precision or accuracy (you can’t trust it entirely when dealing with time frames less than 15 milliseconds). But for games like .Nettrix, the levels of accuracy and precision available with the timer are adequate (remember that you are trying to make the production of this game as simple as possible). In the next chapter, you’ll see a GDI+ application that runs at full speed, without using a timer.
It’s not common in game programming to put the game engine code in a form. Usually you create a GameEngine class that deals with all the game physics and rules (as you’ll see in the next chapter).
Looking back at the pseudo-code, you see the following instruction:
If it's at the top of the screen then
This tests if the block is at the top of the screen. Reviewing your Block class, you see that you have no direct way to retrieve the block Top position, so you would have to test each of the Top positions of the block’s composing squares. To solve this, make a final adjustment to the Block class, including a new method, as depicted in the next code listing:
public int Top() { return Math.Min(square1.location.Y, Math.Min (square2.location.Y, Math.Min(square3.location.Y, square4.location.Y))); }
Now you’re ready to finish your program. Based on the preceding pseudocode and on some minor changes made in the game coding phase, the code for the form will be as follows:
private void tmrGameClock_Tick(object sender, System.EventArgs e) { int erasedLines;
// Prevents the code from running if the previous tick // is still being processed. if (stillProcessing) return; stillProcessing = true;
// Manage the falling block. if (!CurrentBlock.Down()) { if (CurrentBlock.Top() == 0) { // Test for Game over. tmrGameClock.Enabled = false; CmdStart.Enabled = true; MessageBox.Show("GAME OVER", ".NETTrix", MessageBoxButtons.OK, MessageBoxIcon.Stop); stillProcessing = false; return; } // Increase score based on # of deleted lines. erasedLines = GameField.CheckLines(); if (erasedLines > 0) { score += 100 * erasedLines; lblScoreValue.Text = score.ToString(); PicBackground.Invalidate(); //Force the window to repaint. Application.DoEvents(); GameField.Redraw(); }
// Replace the current block... CurrentBlock = new Block(new Point (GameField.SquareSize*6,0), NextBlock.BlockType); CurrentBlock.Show(PicBackground.Handle); } stillProcessing = false; }
Compare the preceding code listing with the previous pseudo-code to make sure you understand each line of code.
The Load event for the form and the KeyDown event and the code for the Start button remain unchanged. The final version of .Nettrix has now been coded. When the game is run, it looks like the screen shown in Figure 1-32.
Figure 1-32. The final version of .Nettrix
You can now play your own homemade clone of Tetris, and are ready to improve it, with the changes discussed in the next section.
This chapter is from Beginning .NET Game Programming in C#, by David Weller, et al., (Apress, 2004, ISBN: 1590593197). Check it out at your favorite bookstore today.
After playing the first version of .Nettrix for a few minutes, every player will miss two important features present in almost every Tetris type of game: a feature to show the next block that will appear, and some way to pause the game, for emergency situations (like your boss crossing the office and heading in your direction).
Now that you have all base classes already finished, this is easily done. The next sections discuss these and some other features to improve your first game.
Coding the Next Block Feature
To show the next block, you can create a new pictureBox on the form to hold the next block image, and adjust the click of the Start button and the timer_tick event. You can use the optional parameter you created on the Block constructor to create the new blocks following the block type of the next block.
To implement this feature, you create a variable to hold the next block in the general section of the form.
private Block NextBlock;
At the end of the cmdStartclick event, you add two lines to create the next block.
NextBlock = new Block(new Point(20, 10), Block.BlockTypes.Undefined); NextBlock.Show(PicNextBlock.Handle);
And finally you adjust the Tick event of the timer to create a new block every time the current block stops falling, and to force the CurrentBlock type to be the same as the NextBlock type.
// Replace the current block... CurrentBlock= new Block(new Point(GameField.SquareSize*6,0), NextBlock.BlockType); CurrentBlock.Show(PicBackground.Handle); // Create the Next block. NextBlock.Hide(PicNextBlock.Handle); NextBlock = new Block(new Point(20,10), Block.BlockTypes.Undefined); NextBlock.Show(PicNextBlock.Handle);
You can now run the game and see the next block being displayed in the picture box you’ve just created, as shown in Figure 1-33.
Figure 1-33. Showing the next block
The next section shows another improvement, the game pause feature.
Coding the Game Pause Feature
To create a pause function, all you need to do is to stop the timer when a specific key is pressed—in this case, you use the Escape (Esc) key. A simple adjustment in the KeyDown event, including an extra case clause for the Keys.Escape value, will do the trick.
private void NetTrix_KeyDown(object sender, System.Windows.Forms.KeyEventArgs e) { switch(e.KeyCode) { case Keys.Right: CurrentBlock.Right();break; case Keys.Left : CurrentBlock.Left();break; case Keys.Up : CurrentBlock.Rotate();break; case Keys.Down : CurrentBlock.Down();break; case Keys.Escape: tmrGameClock.Enabled = !tmrGameClock.Enabled; if (tmrGameClock.Enabled) this.Text = ".NETTrix"; else this.Text = ".NETTrix -- Press 'Esc' to Continue"; break; default: break; } Invalidate(); }
In the next section, we’ll discuss an improvement to the graphical part of your game.
Coding the Window Redraw
A little problem with your game is that, when the .Nettrix window is covered by other windows, the game field isn’t redrawn. You can adjust this by including a call to the GameField’s Redraw method, at the Activate event of the form (the Activate event occurs every time the form gets the focus again, after losing it to another window).
private void NetTrix_Activated(object sender, System.EventArgs e) { // This event occurs when the window receives back // the focus after losing it to another window. // So, redraw the whole game field. PicBackground.Invalidate(); Application.DoEvents(); GameField.Redraw(); if (NextBlock != null) NextBlock.Show(PicNextBlock.Handle); }
Even using this approach there’ll be some situations when the windows won’t be redrawn properly. To achieve the best results, you should include the call to the Redraw method in the Tick event of the timer, but since it could compromise the speed of your game, keep the code as shown.
The next section discusses some suggestions for future enhancements to your game.
This chapter is from Beginning .NET Game Programming in C#, by David Weller, et al., (Apress, 2004, ISBN: 1590593197). Check it out at your favorite bookstore today.
Two last improvements you could make are creating levels for the game and producing a configurations screen, but these we’ll leave for you to do by yourself.
To create levels for the game, you could use a basic rule like this one: Every 3 minutes the block falling speed is increased by 10 percent, the game level is incremented by one, and the points earned for each block gets multiplied by the level number. You can just adjust the timer tick procedure to include the logic for this rule.
In the case of a configurations screen, you could choose to see or not to see the next block image (setting the Visible property of the picNextBlock accordingly) and adjust the block size on the screen, so the visually impaired can play with big blocks, and those who like to play pixel hunt can do so with single-pixel square blocks.
Because the whole game is based on the GameField.SquareSize constant, implementing this feature is just a matter of creating the configuration window and adjusting the screen size according to the chosen square size. The next code listing is provided to underscore this last point; just add the following code to the procedure to be able to adjust the screen size after the configuration:
// Adjusts the size and controls position based on the class constants. // On the window height, sums the size of the window title bar. this.Height = GameField.Height * GameField.SquareSize + (this.Height - this.ClientSize.Height) + 3; // 3=border width this.Width = GameField.Width * GameField.SquareSize + 92; this.PicBackground.Height = GameField.Height * GameField.SquareSize + 4; this.PicBackground.Width = GameField.Width * GameField.SquareSize + 4; this.PicNextBlock.Left = GameField.Width * GameField.SquareSize + 12; this.LblNextBlock.Left = GameField.Width * GameField.SquareSize + 12; this.lblScore.Left = GameField.Width * GameField.SquareSize + 12; this.lblScoreValue.Left = GameField.Width * GameField.SquareSize + 12; this.CmdStart.Left = GameField.Width * GameField.SquareSize + 12;
You are adjusting neither the font size nor the button sizes, so to work with smaller sizes, some updating of the code will be necessary.
In the downloadable source code, the code is on the Load event of the form, so you can play with different sizes by simply adjusting the SquareSize constant and recompiling the code.
Lastly, if you want to look at a more object-oriented implementation of this game, look at how Chris Sells did it in his implementation of Wahoo (http://www.sellsbrothers.com/wahoo). It uses a similar masking technique, but the handling of the blocks is different from this example.
Summary
In this chapter, you created your first game, .Nettrix, and explored some important concepts that will be used even in sophisticated games, including the following:
Basic concepts about GDI+ and the new Graphics objects used in C#
Basic concepts about collision detection and some suggestions on how to implement fast collision algorithms in your games
Creation of simple classes and bitwise operators in C#
Basic game engine creation, based on a game proposal and with the support of a game project
In the next chapter, we’ll introduce you to the concept of artificial intelligence, how to create a game with computer-controlled characters, and how to create faster graphics routines with GDI+. You’ll also examine some additional concepts concerning object-oriented programming.
Acknowledgments
The authors would like to thank Tristan Cartony, who assisted with the creation of the .Nettrix C# code.
Book Reference
James Arvo, “A Simple Method for Box-Sphere Intersection Testing,” in Graphics Gems, edited by Andrew S. Glassner (Academic Press, New York, 1990)
This chapter is from Beginning .NET Game Programming in C#, by David Weller, et al., (Apress, 2004, ISBN: 1590593197). Check it out at your favorite bookstore today.