.Netterpillars: Artificial Intelligence and Sprites
Learn about artificial intelligence while creating a computer game. This article is excerpted from chapter two of the book Beginning .NET Game Programming in C#, by Ellen Hatton et. al. (Apress, 2004, ISBN: 1590593197) and is the first of two parts.
IN THIS CHAPTER, WE’LL INTRODUCE YOU to the concepts of artificial intelligence (AI) and sprites. You’ll also get a chance to extend your knowledge of GDI+ functions, including some tips intended to give your games a boost in performance. To accomplish these goals and illustrate these concepts, we’ll walk you through the steps for creating a game called .Netterpillars (see Figure 2-1).
Figure 2-1. .Netterpillars, this chapter's sample game
.Netterpillars is an arcade game in which each player controls a caterpillar (in fact, a netterpillar) that takes part in a mushroom-eating race with other netterpillars. The objective of the game is to be the last surviving netterpillar, or the longest one (they grow when they eat) when every mushroom has been eaten.
We’ll describe the game in more detail in the section “The Game Proposal” later in this chapter.
.Netterpillars is a more complex game than the one you saw in the last chapter because it involves the following components:
AI: Creating a game with opponents will make you exercise your ability to create a computer-controlled character that challenges players, while giving them a fair chance of winning.
Sprites: Using nonrectangular game objects will force you to find a way to draw them on the screen in a simple, efficient manner. Including a background image in your game screen will help you to check whether your moving code is working (remember, in the last chapter you simply painted the objects with the flat background color).
GDI+: Creating an interface where many objects (one to four caterpillars, wooden branches, and a lot of mushrooms) will be drawn and interact with each other will challenge you to find a faster way to update the screen.
While covering these topics, you’ll also look at new concepts related to object-oriented programming so you can create easily reusable classes to improve productivity when coding your games. For example, a Sprite class is something that almost any game will need; so you can code it once and use it forever. We’ll discuss all these points in the next sections, starting with some object-oriented concepts.
This chapter is from Beginning .NET Game Programming in C# by Ellen Hatton et al. (Apress, 2004, ISBN: 1590593197). Check it out at your favorite bookstore today. Buy this book now.
There are many technical books that explain the academic details of object-oriented analysis (OOA) and object-oriented programming (OOP). It’s not our goal to enter into such particulars, but instead loosely define a few terms and demonstrate some practical uses of these techniques.
The main idea behind creating objects is to make your code simpler to write and easier to maintain. By creating high-level objects to take care of specific tasks, you can build your games using these objects without needing to remember every tiny detail about a new game.
A good analogy is a house. A house is composed of many different rooms with many purposes, and in any neighborhood, you’ll find a variety of shapes and sizes of homes, each uniquely defined by characteristics such as shape, size, and color. However, a house is built from a template, usually a blueprint, which describes how that house can be built in a repeatable way. You can even break the house down into smaller pieces called subsystems, which provide certain functions in a repeatable way (plumbing, electricity, and heating are simple examples). These subsystems are themselves built from repeatable and reusable components. In the case of an electrical subsystem, you have switches, outlets, and wiring.
The fundamental point is that good object-oriented approaches tend to mimic real-world environments and systems, and that these systems are often able to be used in even more complex systems.
Table 2-1 lists some common terms used when talking about object-oriented programming and analysis, along with a definition of each.
TERM
DEFINITION
Class
The code you write that is used as a blueprint to create objects. It describes the characteristics of an object: what kind of attributes it has, how it can be asked to do things, and how it responds to events.
Object
An instance of a class. Generally created by invoking a class’s constructor.
Methods
Functions defined inside a class. Generally speaking, a method describes an action that the object can be told to do.
Properties or attributes
Variables defined inside a class. Class attributes typically describe the qualities (state) of the object. In some cases, attributes might not be accessible to the user of an object because you (the author) have decided those attributes should not be easily modified by a user. Properties are a special type of attribute that let you define more complex ways to read or write to an attribute.
Events
Methods in the object triggered by an external action. May be associated with a user action (such as clicking a button) or a system action (such as a specific time slice that has elapsed).
Constructor
Special method called when creating an object—in C#, this is done by using the keyword “new” followed by the class name.
Destructor
Special method called when the object is being destroyed. In C#, to code the destructor you have to override (see the Overriding entry) the Dispose method of the base class. However, because of the automatic garbage collection found in the common language runtime, explicitly calling a destructor is rarely needed.
Inheritance
Object-oriented concept that defines that one class can be derived from another class or classes (called base classes), and inherit their interface and code (called the derived or child class).
Overriding
Object-oriented concept that defines that a derived class can create a different implementation of a base class method. In effect, it completely overrides the base class’s behavior.
Interface
A “contract” that defines the structure of methods, properties, events, and indexers. You can’t create an object directly from an interface. You must first create a class that implements the interface’s features.
Encapsulation
The concept of gathering methods, properties, events, and attributes into a cohesive class and removing the details from the user. An example of encapsulation would be a car—you operate a car by steering, braking, and accelerating. Good encapsulation removes the need for you to worry about managing fuel injection flow, brake fluid hydraulics, and proper internal combustion.
Overloading
Object-oriented concept that states that one method can have many different interfaces, while keeping the same name.
Table 2-1.Common Object-Oriented Terminology
Polymorphism Object-oriented concept that says that different objects can have different implementations of the same function. An Add method, for example, can sum integers and concatenate strings.
NOTEWe’ll refer to these concepts and terms throughout the rest of the book, reinforcing their meanings as we go along.
Continuing with the introductory concepts of this chapter, let’s talk about artificial intelligence, demonstrating a real-life application of this concept born in science fiction books.
This chapter is from Beginning .NET Game Programming in C# by Ellen Hatton et al. (Apress, 2004, ISBN: 1590593197). Check it out at your favorite bookstore today. Buy this book now.
AI, for our purposes, is the code in a program that determines the behavior of an object—in other words, how each game object will act upon and react to the game environment in each specific time frame.
The game’s AI is often confused with the game physics, or the simulation as some gamers prefer to call it. While the AI decides what to do, the physics sets the constraints and limits of all players in the system, including your own game character. Some examples will make this distinction clearer:
Classic pinball games have no AI, only physics.
In the SimCity game series, when players can’t build a new residential block over a river, it’s the game physics acting. When the Sims start creating their houses, it’s the game AI’s turn.
In the 3-D maze fever started long ago by Castle Wolfenstein, the game physics tells players that they can’t go through walls, and that their bullets will lower the enemy’s energy until death. The game AI tells the enemy to turn around and fire at players if they shoot him, or if he “hears” them shooting.
A good game project usually has the physics and the AI very well defined and separated, and most times the AI acts just like a player over the game physics. For example, in a multiplayer race game, the players control some cars, and the AI will drive all cars with no pilots, ideally with the same difficulties that the human players have.
AI Categories
You can divide the AI into three categories:
Environmental AI: The kind of AI found in games like SimCity, where the environment (in this example, the city) acts as a lifelike environment, reacting to the player’s input and including some unexpected behavior of its own.
Opposing player AI: Used in games where the AI will act like a player playing against the human. For example, in chess and other board games, you usually have a very sophisticated AI to play the part of an opponent.
Nonplayer characters (NPCs): Many games have computer-controlled characters that could be friendly (for example, the warriors that join players in a quest on role-playing games, or RPGs, like Diablo), unfriendly (the monsters and enemies in 3-D mazes), or neutral (the characters are there just to add color to the environment, such as the cooker at the Scumm bar in LucasArts’s The Secret of Monkey Island).
Of course, this division exists only for teaching purposes; sometimes there’s no distinct barrier between the categories.
General AI Considerations
Without entering into specific details, there are some things you have to remember when writing AI code:
Don’t let users find out that the AI has access to their internal data. For example, in games like Microsoft’s Age of Empires, players only see part of the map. Even though the AI can access the full map, the computer-controlled tribes don’t act as if they know all the players’ characters positions.
Create different levels of difficulty. Having a game with different levels lets players decide how tough they want their opponents to be. In some chess games, for example, players can choose how many future moves the computer will analyze, making the game easier or harder.
Let the AI fail sometimes. If there’s anything computers do well, it’s executing code exactly the same way over and over. If you are coding a shooter game where the computer can shoot the player, don’t forget to make the computer miss sometimes; and don’t forget that an opponent that never misses is as bad as an opponent that always misses. Players play the game to win, but if they don’t find it challenging, they’ll never play your game again.
Don’t forget to take into account the environment variables. If players can’t see through the walls, the NPCs must act as if they can’t either. If the computer-controlled adversary has low energy, but is very well protected by walls, he or she won’t run away. If players can hear sounds when someone is approaching or when someone shoots, the NPCs must act like they hear it, too.
Always add some random behavior. The correct balance of randomness will challenge players more, without making the game so unpredictable that it becomes unplayable. If the game has no element of chance, players can find a “golden path” that will allow them to always win when using a specific strategy.
Let the AI “predict” players’ moves. In some games, it’s possible to predict players’ moves by analyzing the possibilities based on the current situation, like in a checkers game. But in other games the AI can “cheat” a little, pretending that it predicted the moves of a good human player. For example, if the AI discovers that a player is sending soldiers through a narrow passage in the direction of its headquarters, it can put a sentinel in the passage and pretend that it “had considered” that someone could use that passage. And never forget to give players a chance (they can kill the sentinel, for example)!
Common AI Techniques
When talking about AI, it’s usual to hear about neural networks, genetic algorithms, fuzzy logic, and other technical terms. It’s beyond the scope of this book to explain each of these approaches, but those who want to get deeper into the AI topic can look in Appendix A to find more information.
These terms, when applied to games, have the main goals of adding unpredictability to the game actions and helping to create a game that seems to learn players’ tricks and adapt to them to be more challenging. To take a more practical approach, you can obtain these results by applying some simple tricks that will require a lot less effort. In the next sections, we discuss some of these tricks.
Adaptable Percentage Tables
A neural network can be simplified as a table with adaptable results, represented by percentages. For example, when coding a war game, you can create a table to help the AI choose the tactics with which to attack the other players. The AI will use each tactic a set percentage of the time depending on the success rate that is represented by the percentage. The greater the success rate, the more often this tactic will be used. The table can be filled with some initial values, as shown in Table 2-2, and can evolve according to the results of each turn in the game.
ATTACK TYPE
PERCENTAGE
Attack with “V” formation
20 percent
Divide the soldiers in small groups and attack in waves
20 percent
Guerrilla attack—surprise attack with a few soldiers, shoot and run away
20 percent
Attack with full force, in a big group
20 percent
Surround the player and attack from every direction
20 percent
Table 2-2.Starting Values for an Adaptable Percentage Table
After each attack, you’ll change the table values according to the results. For example, if the attack is successful, you can add 10 percent to its corresponding percentage column on the table; if not, subtract 10 percent, distributing the difference to the other attack types. After some attacks, the program will “learn” which kind of attack is most efficient against the current player. For example, if the AI uses the first kind of attack (in “V” formation) and it was successful, the table would be updated to the values shown in Table 2-3.
ATTACK TYPE
PERCENTAGE
Attack with “V” formation
30 percent
Divide the soldiers into small groups and attack in waves
17.5 percent
Guerrilla attack—surprise attack with a few soldiers, shoot and run away
17.5 percent
Attack with full force, in a big group
17.5 percent
Surround the player and attack from every direction
17.5 percent
Table 2-3. Adaptable Percentage Table Values After a Successful “V” Formation Attack
In the next turn, if the AI tries an attack using the guerrilla tactic and it fails, the table will be updated again, to the values shown in Table 2-4.
Table 2-4.Adaptable Percentage Table Values After a Failed Guerrilla
ATTACK TYPE
PERCENTAGE
Attack with “V” formation
32.25 percent
Divide the soldiers in small groups and attack in waves
20 percent
Guerrilla attack—surprise attack with a few soldiers, shoot and run away
7.75 percent
Attack with full force, in a big group
20 percent
Surround the player and attack from every direction
20 percent
And so on . . .
Of course, in a real game it’s better to add many interacting factors. For example, you can choose the best attack for each type of terrain or climatic condition. The more factors you take into account, the better results you’ll have. In games like SimCity, there are dozens (sometimes even hundreds) of factors that contribute to generating the desired result.
This chapter is from Beginning .NET Game Programming in C# by Ellen Hatton et al. (Apress, 2004, ISBN: 1590593197). Check it out at your favorite bookstore today. Buy this book now.
For games that use NPCs, a classical problem is how to discover whether the computer character can see the player or not. There are many different solutions to this problem, but possibly the simplest one is the line of sight algorithm. You can implement this in a few steps:
Consider an NPC’s eyes as a point just in front of it. It will be “looking” in this direction.
Using the techniques for calculating the distance between two points, which you saw in the previous chapter, calculate the distance between the NPC and the player’s character. If distance to the player is greater than a certain value (the “seeing distance”), the NPC can’t see the player, as shown in Figure 2-2.
If the distance is less than the seeing distance of the NPC, create an (invisible) object having the player character as the center and the NPC’s “eyes” as vertices.
Figure 2-2.The player (good guy) is outside the seeing distance of the NPC devil
Use one of the collision detection algorithms you saw in the previous chapter to calculate whether there’s a collision between this object and the NPC’s head. If so, it’s because the line of sight goes through the NPC’s head. The player is not in front of the NPC, so the NPC can’t see the player. Figure 2-3 illustrates this situation.
Figure 2-3.The player is behind the NPC, so it can't see the player.
If there’s no collision with the NPC’s head, calculate the collision among the created object and other game objects. If there’s no collision, there’re no obstacles between the player and the NPC, so the NPC can see the player. See Figure 2-4 for a graphical view of this last calculation.
Figure 2-4.The NPC tries to see the player.
Making NPCs “Hear” the Player
There’s a simple solution to making NPCs aware of player sounds: Every time the player makes a sound, the program must compute the distance (using the Pythagorean theorem, discussed in Chapter 1) from the player to the NPCs. Any NPC whose distance is less than a constant value (the “hearing distance”) would turn to look for the sound origin. After a while, if there are no further sounds and the NPC has not seen the player, the NPC returns to its previous activity (patrol, stand still, walk erratically, etc.).
It’s a common practice to have different hearing distances for different kinds of sounds: A gun shooting can be heard from a long distance, whereas the player must be really near to the NPC for it to hear his or her footsteps.
Path Finding
Like the line of sight problem, there are also many different algorithms to solve the problem of path finding. If you don’t know in advance how the game field will take shape, you could employ some of the following methods:
Mark some “milestones” along the path the character is walking. If it hits an obstacle, return to the last milestone and try another way. This algo rithm is useful when you have labyrinths or tiled game fields.
Use invisible “bumpers” around the game characters. The program checks for any collision with these invisible objects, and chooses a way according to the noncolliding paths. The game can create bumpers following the NPCs from different distances, in order to allow them to see remote obstacles.
Create a line of sight between the current position and the destination position. If there are obstacles in the way, move the line of sight to one side until there’s no obstacle. Mark this point as a way point, and repeat the process between this point and the desired destination point.
If you know the game field, such as a fixed screen in an adventure game, some common approaches are as follows:
Define fixed paths, so the player and the NPCs always walk over these paths.
Define path boxes, where each part of the screen is defined as a box with some characteristics, including a list of reachable boxes from that area. When walking inside a box, the player and the NPCs have full freedom; when going to a place on screen that’s inside another box, have the player and NPCs walk to the junction point between the two boxes, and then to the desired point in the next box. This method provides a more flexible look and feel for the game, but the boxes must be well planned to avoid strange behaviors (like the NPC running in circles if all the boxes are connected). This is the approach used by LucasArts in the first three games of the Monkey Island series.
This chapter is from Beginning .NET Game Programming in C# by Ellen Hatton et al. (Apress, 2004, ISBN: 1590593197). Check it out at your favorite bookstore today. Buy this book now.
Although a lot of different techniques exist for solving problems relating to a game’s AI, there’s always room for new ideas. Learn from other people’s experience; see how the games behave and try to figure out how to mimic such behaviors in your game. There are a lot of good game developers’ sites where you can learn directly from the masters; a simple Web search using the keywords “artificial intelligence” and “games” will uncover the most interesting ones.
Keep Libraries of Reusable Graphics and Objects
Our final piece of advice is to always have your graphical routines and objects well polished and ready to use, so you can spend more time on the game’s physics and AI, the most important parts. Of course, you should also understand that the “first time around” isn’t always perfect, and be ready to refactor your code into more workable pieces as your knowledge about the objects improves. To this effect, we’ll show you how to start your library with a Sprite class, described in the next section.
Sprites and Performance Boosting Tricks
You’ll now start to create a set of generic classes that can be used in your future game projects, such as the Sprite class.
In the food chain of game programming, sprites are like plankton. They’re at the very bottom of the food chain, but they’re fundamental building blocks to modern graphics programming. In game development, a sprite is a common term to specify any active object on the screen—for example, the player character, bullets, bonus objects, etc. You can also define sprite as any element on a game screen that is neither background nor information (such as menus or tips on the screen). A simple example of a sprite is your mouse pointer.
In this chapter, you’ll create a simple Sprite class, which can be enhanced later to include additional features. Table 2-5 lists some of the basic attributes you may need.
PROPERTY NAME
DESCRIPTION
Bitmap
Holds a simple image for the sprite. In advanced sprite objects, you can have multiple arrays of images to deal with different animations (such as walking, jumping, dying, etc.).
Position
The actual x,y position of the sprite. Following the .NET property names, you can call this property Location.
Scale
The scale to be used for the position coordinates: pixel or the sprite’s size.
Direction
If the object is moving to (or “looking at”) a new position, you must have a direction property to hold this information.
Table 2-5.Suggested Properties for a Simple Sprite Class
As for the methods, three basic routines are obviously needed, and these are shown in Table 2-6.
METHOD NAME
DESCRIPTION
Constructor
You can create overloaded constructors that will receive different parameters: the sprite bitmap, the bitmap and the position, these two plus the direction, and so on. You will use method overloading to implement these different initialization methods.
Draw
This one is a must: All sprites must be drawn.
Erase
Erases the sprite, restoring the background picture, if it exists. To erase the sprite, this method must have access to the background bitmap, in order to copy the background over the previously drawn sprite.
Table 2-6.Suggested Methods for a Simple Sprite Class
Figure 2-5.The Sprite class
Of course, you can come up with many other attributes and methods, such as velocity and acceleration attributes and a move method (which, using the direction, velocity, and acceleration, erases the sprite from the previous position and draws it in the new one). But let’s keep it simple for now! This kind of approach— defining the basic structure of a class or program, and then redefining it to produce many interactions (if needed)—is recognized as a good approach by the latest object-oriented software processes, such as the Microsoft Solutions Framework (MSF). We’ll not enter into any details here, but you’ll get a chance to see some simplified concepts from this software development philosophy in use.
Sprite: Fast and Transparent
Before you start coding the Sprite class, there are two things you must know:
How to draw the sprite as fast as possible.
How to draw nonrectangular sprites. Since most of your game objects won’t be rectangles or squares (like in the .Nettrix example), and all the functions draw rectangular images, you have to learn how to draw an image with a transparent color, in order to achieve the illusion of non-rectangular sprites.
As for the first point, the GDI+ Graphics object has a method called DrawImage that draws an image at a given position in your work area. This method is very flexible, but it incurs a lot of overhead since it includes an internal method to scale the image, even when you don’t use the scaling parameters.
Fortunately, you have a second method, DrawImageUnscaled, that just blits (copies a memory block directly to video memory) the source image, as is, to the destination position, with very low overhead. You’ll use this function, since it gives you all the speed you need.
There’s also a third, even faster, function on the Graphics namespace, called DrawCachedBitmap, that maps the bitmap to the current memory video settings, so the drawing is just a matter of copying a memory position to video memory. This approach has only one drawback: If the player changes the monitor resolution when the game is playing, you’ll have unpredictable results. Unfortunately, this function is currently only available to C++ programs. Because you’ll learn how to work with high-speed graphics through DirectX in the next chapters, this limitation won’t be a problem if you want to create fast-paced action games.
As for the transparent color, you have two possible approaches: You can set a so-called color key to be transparent, after loading the image, with the MakeTransparent Graphics method, or you can create a color-mapping array, which is much more flexible because you can set different degrees of transparency to different colors. We’ll be demonstrating the first approach here, because it’s simpler and all you need for now is a single transparent color, but we’ll also show you how to use a color map array, which can be used in other situations.
The Sprite class is the base class for all active game objects, and since it must have access to some of the properties of the class that will manage the game (such as the background image used in erasing), some programmers like to derive it from that class. You’ll use this approach here, deriving the Sprite class from the GameEngine class (discussed later in the section, “The Game Proposal”).
This chapter is from Beginning .NET Game Programming in C# by Ellen Hatton et al. (Apress, 2004, ISBN: 1590593197). Check it out at your favorite bookstore today. Buy this book now.
Start by coding the attributes. Because attributes don’t require special treatment for now, you’ll create them as public variables and some helper enumerations.
public class Sprite : GameEngine { // Images path and size, to be used by the child classes. public const string IMAGE_PATH = “Images”; public const int IMAGE_SIZE = 15;
protected Bitmap Source; public CompassDirections Direction; public Point Location; public ScaleSizes Scale = ScaleSizes.Sprite;
public enum CompassDirections { North = 1, NorthEast = 2, East = 3, SouthEast = 4, South = 5, SouthWest = 6, West = 7, NorthWest = 8 }; }
The Sprite’s Constructor Method
As for the constructor of the class, you can define many different overloaded functions for it: a method that receives no parameters (to be implemented by the derived classes, if needed), a method that receives the sprite image name, and two others that receive the initial position of the sprite and the color code to be used as a transparent color. If you need more overloads, you can create them as the project evolves. Observe that, in order to simplify the constructor code, you create a private Load method, which can be called with one or more parameters according to the constructor used when creating the object.
public Sprite() { // This empty constructor is to be used by the child classes when they // want to implement everything from the ground up. } // public Sprite(string strImageName) { Source = Load(strImageName); }
public Sprite(string strImageName, Point point) { Source = Load(strImageName); Location = point; }
public Bitmap Load(string strImageName) { Bitmap Load_result; Color BackColor;
try { Load_result = (Bitmap)Bitmap.FromFile(strImageName); // The transparent color (keycolor) was not informed, // then it will be the color of the first pixel. BackColor = Load_result.GetPixel(0, 0); Load_result.MakeTransparent(BackColor); }
catch { MessageBox.Show(“An image file was not found.” +Keys.Enter+ “Please make sure that the file “+strImageName+ ” exists.”, “.Netterpillars”, MessageBoxButtons.OK, MessageBoxIcon.Stop); Load_result = null; } return Load_result; }
public Bitmap Load(string strImageName, Color keycolor) { Bitmap Load_result; try { Load_result = (Bitmap)Bitmap.FromFile(strImageName); Load_result.MakeTransparent(keycolor); } catch { MessageBox.Show(“An image file was not found.” +Keys.Enter+ “Please make sure that the file “+strImageName+ ” exists.”, “.Netterpillars”, MessageBoxButtons.OK, MessageBoxIcon.Stop); Load_result = null; } return Load_result; } public Sprite(string strImageNamem, Color keycolor) { Load(strImageNamem, keycolor); }
NOTE In C#, you can create methods with the same name and different parameters in order to implement different behaviors. As you saw in the “Object-Oriented Programming” section, this is called method overload, and it’s not a new idea; many object-oriented languages already have this feature.
The main purpose for creating various methods with the same name and different parameters is to give the programmers that will use your class enough flexibility to use only the parameters they need in a given case. For example, if you are creating a sprite that will be fixed throughout the game, you’ll probably want to pass this fixed position when creating the sprite; if the sprite moves every time, it’s better to pass only the image name, and so on.
Drawing and Erasing Sprite Code
The last two methods of a basic Sprite class must be, as we said before, the Draw and Erase methods.
public void Erase(System.IntPtr WinHandle) { Graphics graphBack = Graphics.FromHwnd(WinHandle); graphBack.DrawImage(BackgroundImage, new Rectangle (Location.X*(int)Scale, Location.Y*(int)Scale, IMAGE_SIZE, IMAGE_SIZE), new Rectangle(Location.X*(int)Scale, Location.Y*(int)Scale, IMAGE_SIZE, IMAGE_SIZE), GraphicsUnit.Pixel); graphBack.Dispose(); }
In the Erase method, you use a background image property that will be shared by all the sprites, and that stores the background image of the game field, which must be drawn over the sprite image to create an illusion of erasing it. Because you need a little more flexibility than DrawImageUnscaled offers, you use the DrawImage function to copy a specific rectangle of the background image over the sprite image.
If you want to extend the class to deal with multiple transparent colors or different degrees of transparency, you can adjust the constructor to use a color map table, as shown in the following code. The color alpha values range from 255 (opaque) to 0 (totally transparent).
// This sample code shows the use of color map tables to // assign different degrees of transparency to different colors. public Sprite(string strImageName, Color ColorKey) { ImageAttributes ImgAttributes; ColorMap[] ImgColorMap; Color BackColor; int width; int height;
Using the Dispose() method of the Graphics object ensures that the memory used by the Graphics object will be released as soon as possible, which is very important because you’ll be calling the Draw and Erase methods many times a second.
This completes the explanation of the technical concepts you’ll use in your game. We’ll define some details of this chapter’s sample game, .Netterpillars, in the next section, “The Game Proposal.”
What Does Dispose() Really Do?
You should already be familiar with the fact that the common language runtime (CLR) handles automatic garbage collection. The nice thing about this is that is takes care of all the little bits of memory that you allocate in the system and makes sure everything stays tidy. But what about times when you want to tell the runtime that you want to make memory resources available for reuse right away? That’s where Dispose() comes in. For any object that implements the IDisposable interface, it exposes a custom Dispose() method that tells the runtime that the resources are immediately available for reuse. Generally speaking, any object that has a Dispose() method should be implemented within a try/finally block. This is important because it ensures that the Dispose() method is always called on the IDisposable object. For instance, the Sprite.Draw() method should actually be written this way:
This try/finally pattern is so common that there’s convenient shorthand in C# that does this for you through the “using” keyword.
public void Draw(System.IntPtr WinHandle) { Graphics graphBack = Graphics.FromHwnd(WinHandle); using (graphBack) { graphBack.DrawImageUnscaled(Source, Location.X*(int)Scale, Location.Y*(int)Scale); } }
We’ll leave it as an exercise for you to convert the Dispose() methods in the .Netterpillars code into the proper format.
This chapter is from Beginning .NET Game Programming in C# by Ellen Hatton et al. (Apress, 2004, ISBN: 1590593197). Check it out at your favorite bookstore today. Buy this book now.
When creating games, remember that the very first step is to write a clearly defined game proposal. This ensures that everyone involved in the game creation process can understand and agree with the game objectives. Even very sophisticated games must start with a simple proposal, so the programmers can build the project upon a previously established goal.
As mentioned in the introduction to this chapter, you’ll learn how to create a fast-action arcade game called .Netterpillars. Here are some details about the game:
The game objective is to control a caterpillar-like character around the game field, trying not to collide with other caterpillars or any obstacles. If you collide, you are dead.
The game field must be filled with mushrooms, and every time a netter-pillar eats a mushroom, it gets bigger.
The game is over when all the players die (computer or human ones), or when the last mushroom is eaten.
There must be a configuration screen where the player can choose the field size, how many mushrooms there’ll be in the game, and the number of computer-controlled opponents (from 0 to 3).
The game must allow the smooth inclusion of multiplayer routines in future versions, so all the project and coding must be done with this goal in mind.
The basic idea in creating different configurations for a game is to add endurance to the game. That means that the game will interest the player for a longer time. It’s a common approach to add many different ways of playing in order to keep the player’s attention. A good example of this approach is Microsoft’s Age of Empires: Players can start a new game and go on building from the ground up, or players can choose a quest, where a previously created status quo is presented and they must solve some specific problems to win the game.
In this sample game, the player can choose, for example, a small and mush-room-crowded game field, and try to eat them all without getting trapped by his or her own tail; or choose a big field with fewer mushrooms and more opponents, in order to try to be the longest one, eating as many mushrooms as possible while trying to kill the enemies, trapping them with his or her tail. Many intermediary combinations would be possible, making the game more interesting to play.
With the basic game plan set, it’s time to start thinking about the technical details: creating a project for the game.
The Game Project
Once all the team members share the project vision, it’s time to create your project. This can be as simple as a feature list and a scratch class diagram on paper. And even if you are working solo, as you will see, organizing and planning work before actually doing it is highly beneficial!
Your game project will include a simple class diagram, showing the class, properties and methods, a main program workflow definition, and the drafts for each game screen (as discussed in the next sections).
Defining the Game Classes and the Game Engine
The game characters are the first natural candidates for game objects, based on your library’s Sprite class. So you can take the nouns on the list of topics from the game proposal and create a first draft of your class diagram, as shown in Figure 2-6.
Figure 2-6.The class diagram-first draft
This looks fine for a first draft. You see here an extra class, not mentioned in the game proposal: the Branch class. You include it just for fun, in order to improve the look of the game with branches at the screen limits.
Following what you learned in the previous chapter, there must be a class for controlling the game field and physics. Since this class will have more features than the GameField class from Chapter 1, you’ll use a more appropriate name for it: GameEngine.
Before putting this class in your diagram, it must be clear what the game engine should and shouldn’t do. It follows that the game engine is solely responsible for creating and maintaining the entire environment where the game characters will act. Usually, the game engine works according to the physical laws (gravity, action-reaction, etc.) in the real world, with more or less realism depending on the game goals.
The game engine doesn’t include the AI control. Instead, it just puts constraints over the game’s characters, regardless of whether they are computer or human controlled. Hence you’ll need another class to control the AI of your computer-controlled netterpillars. Since this class must have a high integration with the game engine (to collect information that will allow it to make decisions— for example choosing the direction to go), you’ll create this class as a child of the GameEngine class.
Because the sprite must have access to the game field background in order to erase itself, you’ll also include the Sprite class as a derived class from the game engine in the class diagram.
The final class diagram (without the attributes and methods) is shown in Figure 2-7. Notice that it’s not the right diagram, or the only approach. It’s just an idea, and if you don’t agree with it—great! You understand your subject so well that you already have your own opinion about it.
Figure 2-7.The class diagram-second draft
As for the properties (attributes) and methods, you can use what you learned before as a starting point, and build on it.
After more brainstorming, you select a set of attributes and methods for each class, as shown in Figure 2-8. Don’t expect any surprises with the classes that deal with the game objects, and the other ones, such as the AI class, are created based on your previous experience of similar projects.
Figure 2-8.The final class diagram
You don’t need to have a totally finished diagram by now, just a guide for your coding. You can return to this diagram later, at the game-coding phase, and adjust it if necessary, when new ideas and problems arise. Even the most detailed projects aren’t steady; modifications always occur after the coding starts. There is a quote from a famous general that fits perfectly in this situation: “No battle is won according to the battle plans, but no battle was ever won without a plan.”
In the next sections we’ll discuss each of the classes shown in the diagram from Figure 2-8, including brief explanations of their properties and methods.
This chapter is from Beginning .NET Game Programming in C# by Ellen Hatton et al. (Apress, 2004, ISBN: 1590593197). Check it out at your favorite bookstore today. Buy this book now.
You’ll use this class, defined in the “Sprites and Performance Boosting Tricks” section, as the base class for all other game objects. Remember that all the members (properties and methods) of this class become members of the derived classes, too.
The Netterpillar Class
The Netterpillar class will control the drawing of the netterpillar characters on screen, both for human-controlled and computer-controlled ones. It’ll have some methods for making the netterpillar bigger (when it eats a mushroom) and to store the bitmaps and current status for the character. Table 2-7 lists the initial suggestion for the members of this class, along with a short description.
TYPE
NAME
DESCRIPTION
Method
constructor
You’ll have an overloaded constructor, which will load with the multiple images of a netterpillar: the head (looking at different directions) and the body parts.
Method
Draw
The overloaded function that draws all the parts of the netterpillar.
Method
Move
Instead of Erase, Move is more appropriate here: Move the head and every body part according to the current direction, erasing the field only after the last body part.
Method
EatAndMove
If the netterpillar eats a mushroom, the last part doesn’t get erased, because the body length is increased.
You’ll need one image for the head for each direction that the netterpillar is looking at/moving to.
Property
NetterBody()
You’ll need an array to store all the body parts
Property
NetterBodyLength
The current size of the netterpillar, which will be used to keep track of the body parts array.
Property
IsComputer
You have to have a way of knowing which players are human controlled and which are computer controlled; this will help with the future evolution of a multiplayer
game.
Property
IsDead
Instead of actually destroying the netterpillar, you’ll just set a flag saying that it’s dead, thus avoiding internal tests to see whether a
given netterpillar object exists.
Table 2-7.The Members of the Netterpillar Class
The Mushroom Class
Because a mushroom does nothing except for standing and waiting to be eaten, all you need is an overloaded constructor that will load the appropriate image file, so you won’t need to pass the filename when creating a new mushroom. The Mushroom class will then have all the members from the Sprite class, plus the overloaded constructor that loads the mushroom image file in a given position.
The Branch Class
A branch will be composed of three different images: one for each branch edge, and a middle one that can be repeated many times to create bigger branches. Since the Sprite base class only stores a single image, you’ll have to create three properties to store these images, and create new overloaded functions for the constructor and Draw methods. Since the branch doesn’t move, you won’t need to create an Erase method. The list of members for the branch class is shown in Table 2-8.
Table 2-8. The Members of the Branch Class
TYPE
NAME
DESCRIPTION
Method
constructor
This overloaded version of the constructor method will receive the size and orientation (north-south or east-west) of the branch.
Method
Draw
This method draws the branch according to its size, position, and orientation.
Property
Size
The size of the branch.
Properties
BranchTop, BranchBottom
The images of the branch extremities.
Property
BranchMiddle
The image for the middle part of the branch that will be repeated over and over by the Draw method, until the branch reaches the desired size.
The AINetterpillar Class
To define a basic set of members for the class that will handle the netterpillar artificial intelligence requires a little more thinking. The first question that arises is, How smart is your computer-controlled character meant to be? Even in this simple game, you can think about some very difficult AI routines. For example, will a computer-controlled netterpillar do any of the following?
Chase the player (or one another) and try to surround the player with its tail in order to kill him or her
Analyze its tail positions on every move in order to avoid getting trapped by its own tail
Analyze the whole game field to look for places where there are more mushrooms or fewer netterpillars
Since all you need here is a simple example, your netterpillar won’t be that smart, at least for the first version of the game. All you want to do is:
Avoid getting killed by hitting a wall, while eating everything that is near to the head
Add some random behavior to make the movement of the computer-con-trolled netterpillars more unpredictable to the player
Table 2-9 shows the first suggested methods and properties you’ll create to address these goals.
TYPE
NAME
DESCRIPTION
Method
ChooseNetterpillarDirection
This method will analyze the netterpillar position and direction and choose the best direction to move to, based on the immediate surroundings of the netterpillar’s head.
Method Property
RandomDirection RandomPercent
This method will add the random behavior, based on the RandomPercent property, and take care not to lead the netterpillar straight to collision and death. This property will control how random the behavior of your netterpillar will be. Remember that a new direction will be chosen many times each second, so any number greater than 10 may make the netterpillar’s movements too random to seem intelligent.
Table 2-9.The Members of the AINetterpillar Class
Of course, these members could also be part of your Netterpillar class, but for this example you’ll create a new class for them in order to have the artificial intelligence code isolated from the drawing code, making it easier to maintain and improve.
The last game class, which deals with the game engine, is discussed next.
The GameEngine Class
For the GameEngine class, you can use some ideas from the .Nettrix sample you saw in the last chapter:
It’s important to have a method to redraw the game field.
You’ll also need a direct reference to the game field (such as a handle) to be used in the drawing operations.
Since you’ll have a dedicated class to control the game, you’ll need a property to control whether the game is running or paused, just like the variable on the form in the previous chapter. A property to control whether the game is over is a good idea, too.
According to the idea of having an array to control collisions (which seems to be the right choice in this case, since your game will be a tile-based one), you’ll need a property to store the game field array.
Since the game engine will need to do all the physics of the game, it’ll need to have access to all game objects. The best way to allow this is to let the GameEngine class create and handle them, so you’ll need properties to store the branch objects, the netterpillar objects and the netterpillars quantity, and the mushroom objects and the mushroom quantity.
You’ll have a configuration screen to set some game properties, and you’ll need corresponding properties to store the configurable parameters, width, and height properties, because your game field can have different sizes; a property to hold the desired mushroom quantity; and another one to hold how many netterpillars will be present.
Because you’ll control only one netterpillar, you’ll need some property to define, for each netterpillar, if it’s computer controlled or human controlled. Having such a property will help in another game objective: to code a game ready to be turned into a multiplayer version in the future. In this case, in the next version you can add information to tell whether the netterpillar is a local gamer, a remote gamer, or a computer.
Since the sprites will need to erase themselves, you’ll need a property to store the initial background image of the game field.
NOTE That’s a lot of things to be thinking about, and we haven’t covered the methods yet. But don’t expect to remember everything in the first brainstorm. It’s usual to create a first draft, and then refine it. When you think about the game logic and create some pseudo-code for the most important parts of the game, new properties and methods arise. When refining the new set, other new details arise. This process is repeated over and over until you have a stable set of classes, properties, and methods. In (very) few words, that’s the basis of what is suggested in most of the books covering object-oriented development currently: Start small and increase complexity as you iterate over the project.
You can list a basic set of methods based on the features coded in the previous chapter—for example, a method to initialize the game field, a method to redraw it, a method to render (which will basically do the physics, update the object states, and then redraw the game field), and some methods to move the game objects and to change their states (such as setting a netterpillar as dead, and asking the Netterpillar object to remove its drawing from the screen).
Based on the previously discussed points, your class will have the interface shown in Table 2-10.
TYPE
NAME
DESCRIPTION
Method
constructor
This method creates the game field and initializes all properties.
Method
MoveNetterpillars
A method for moving the netterpillars, according to the current direction of each one. Also checks for collisions.
Method
KillNetterpillar
This method removes the netterpillar from the game field, if it collides with some wall or other netterpillar.
Method
Redraw
This method redraws the game field.
Method
Render
A method for calling all other methods; in other words, it moves everyone, kills anyone who must be killed, checks for game over, and calls the Redraw method.
Property
ScreenWinHandle
The handle of the game field window, used for drawing the game objects.
Properties
Width, Height
The game field dimensions, which will be configured by the user.
Properties
NetterPillars(), NetterpillarNumber
The netterpillar objects array and the total number of netterpillars.
Property
Branch()
The branch objects array.
Properties
ObjMushrooms(), MushroomNumber
The mushroom objects array and its total number.
Property
GameOver
If true, the game is over.
Property
Paused
If true, the Render procedure won’t move any netterpillar.
Property
ArrGameField()
The array with the game objects, used for implementing the collision detection.
Property
BackGroundImage
The initial background image, which will be used by the sprites to erase themselves (drawing a portion of the background image over them).
Table 2-10. The Members of the GameEngine Class
Because your class diagram now is stable, it’s time to define how the main program will call the classes. In the next section, we discuss the structure of the game’s main program.
This chapter is from Beginning .NET Game Programming in C# by Ellen Hatton et al. (Apress, 2004, ISBN: 1590593197). Check it out at your favorite bookstore today. Buy this book now.
Now let’s think about how the game will work. You need to define a starting place from which the game engine object and the game window will be created, and from which the Render procedure of the game engine will be called repeatedly.
Because you’ll also need a configuration screen, it’s better to first have an introductory screen, in which players can choose whether they want to start the game or to change the game configuration.
Although it’s common in some OOD techniques to suggest the creation of new classes for the forms (sometimes called interface classes), it’ll be easier not to mix user interface with the game logic for now. Instead, you’ll use common window forms, and create a simple workflow diagram, as shown in Figure 2-9, in order to clarify how the game flow will be.
Figure 2-9.The game main workflow
We could give details of the Render procedure, including in the loop shown on the diagram in Figure 2-9 boxes for such processes as gathering user input, updating game objects, redrawing, etc. (and in a real project we strongly suggest that you do). However, the goal for this diagram is only to make it easier to understand the basic game flow across the many screens and the basic game loop, and it does this effectively.
In the next section, you’ll see how to create a draft of each game screen, thus finishing your game project.
Defining the Game Screens
Although the windows implementation will be done in the code phase, it’s good practice to create at least a draft of the screens in the project phase, because when drawing the screen you’ll usually remember more details that can be added to the class diagram. If you can imagine how the previously discussed classes will work in each screen, then there’s a good chance you haven’t missed any important details.
Since C# allows you to create screens quickly, the best sketches are the ones done directly in a form editor like that found in Visual Studio .NET. Let’s call your first screens visual prototypes. The next images will show the visual prototypes for each game screen, starting with the introductory screen on Figure 2-10.
Figure 2-10.The Intro screen
The intro screen will only show an intro image (or splash screen) for the game, along with buttons to allow the player to end the game, start a new game, or change the game configuration. According to the workflow shown in the last section, after a game ends, players will be redirected to this screen.
Figure 2-11.The game configuration screen
Figure 2-11 shows the second draft: the game configuration screen.
On the configuration screen, you can set the number of netterpillars and mushrooms and the size of the game field. Since it’s not up to the user to decide the exact number of pixels in a game or the exact number of mushrooms on screen, you can use domain up-down controls to make the configuration more user friendly: Few/Just Right/Many selections for mushrooms and Small/ Medium/Big selections for the game field size.
As we said before, as the game project evolves, you’ll uncover new details that may require new properties and methods. Looking at the screen shown in Figure 2-11, you need only two enumerations for the GameEngine class, which will lead to simpler and cleaner code: MushroomSizes for the number of mushrooms, and GameFieldSizes for the possible field sizes. You’ll also include two new properties that will receive the values of these enumerations directly from the configuration screen—Mushrooms and Size.
In the code phase, you’ll see how to code properties in C#: You include a pair of procedures in the class that correspond to an object property, allowing you to do some processing—such as setting the Width and Height properties when the Size property is set, and setting the MushroomNumber property when the Mushrooms property is set.
The draft for the next game screen is shown in Figure 2-12.
Figure 2-12.The game field screen is just a form with an image control
You can set the Picture property of the image control in the game field window with any bitmap you want to use as background, since you write a generic code in the Sprite class to do the drawing and erasing. In this case, you set it to a simple sand pattern.
Refining the Game Project
You’ve learned about making progressive refinements in the game project, until you reach the point to start the coding phase. But how do you know when to stop making refinements?
If, after you’ve drawn the class diagram and the workflow diagram and also created the visual prototypes for all game screens, you still don’t have a clear idea about how any part of the game will work, it’s important to write pseudocode for this part and check the workflow, the classes, and the screen drafts again until everything seems to fit. Only start the code phase after you have a clear idea about how things will work, but take care not to get stuck on the project, creating an excessive level of details (except, maybe, for big projects where the lack of detail can cost a lot).
Just remember: It’s much easier and faster to correct a class diagram or a screen prototype than to redo a lot of code because you forgot something important!
With these points in mind, let’s get into the next phase: the code.
This chapter is from Beginning .NET Game Programming in C# by Ellen Hatton et al. (Apress, 2004, ISBN: 1590593197). Check it out at your favorite bookstore today. Buy this book now.