Learn how to make improvements to the Sprite class that was introduced in an earlier lesson.
Published: January 27, 2010z
Validated with Amaya
By Richard G. Baldwin
XNA Programming Notes # 0128
|
This tutorial lesson is part of a continuing series dedicated to programming with the XNA Game Studio. I am writing this series of lessons primarily for the benefit of students enrolled in an introductory XNA game programming course that I teach. However, everyone is welcome to study and benefit from the lessons.
An earlier lesson titled Getting Started provided information on how to get started programming with Microsoft's XNA Game Studio. (See Baldwin's XNA programming website in Resources.)
I recommend that you open another copy of this document in a separate browser window and use the following links to easily find and view the figures and listings while you are reading about them.
I recommend that you also study the other lessons in my extensive collection of online programming tutorials. You will find a consolidated index at www.DickBaldwin.com.
You learned how to design, create, and use a simple Sprite class in an earlier lesson. You also learned to use a generic List object to store references to objects of the Sprite class.
I will explain improvements made to the Sprite class and will show you how to write a Game1 class that takes advantage of those improvements.
The screen output
Figure 1 shows a reduced screen shot of the program while it is running.
Figure. 1 Screen shot of the
running program.
Lots and lots of sprites
When this screen shot was taken, the program had 24 space rocks, 12 red power pills and six UFOs all navigating in the game window.
Will discuss in fragments
As usual, I will discuss and explain the program code in fragments. A complete listing of the Sprite class is provided in Listing 19 and a complete listing of the Game1 class is provided in Listing 20.
The Sprite class begins in Listing 1 with the declaration of several instance variables.
Listing 1. Beginning of the Sprite class. namespace XNA0128Proj { class Sprite { private Texture2D image; private Vector2 position = new Vector2(0,0); private Vector2 direction = new Vector2(0,0); private Point windowSize; private Random random; double elapsedTime;//in milliseconds //The following value is the inverse of speed in // moves/msec expressed in msec/move. double elapsedTimeTarget; |
The purpose of these instance variables will become clear later in the discussion.
This class has the following properties:
Other instance variables
In addition, the class has the following instance variables that are set either by the constructor or by a method:
The first three property accessor methods
The first three property accessor methods are shown in Listing 2.
Listing 2. The first three property accessor methods. //Position property accessor public Vector2 Position { get { return position; } set { position = value; }//end set }//end Position property accessor //-------------------------------------------------// //WindowSize property accessor public Point WindowSize { set { windowSize = value; }//end set }//end WindowSize property accessor //-------------------------------------------------// //Direction property accessor public Vector2 Direction { get { return direction; } set { direction = value; }//end set }//end Direction property accessor |
These three accessor methods are straightforward and shouldn't require further explanation.
The property accessor method for Speed
Humans usually find it easier to think in terms of speed such as miles per hour while it is sometimes easier to write computer programs that deal with the reciprocal of speed such as hours per mile.
The property accessor method for the property named Speed is shown in Listing 3.
Listing 3. The property accessor method for Speed. //Speed property accessor. The set side should be
// called with speed in moves/msec. The get side
// returns speed moves/msec.
public double Speed {
get {
//Convert from elapsed time in msec/move to
// speed in moves/msec.
return elapsedTimeTarget/1000;
}
set {
//Convert from speed in moves/msec to
// elapsed time in milliseconds/move.
elapsedTimeTarget = 1000/value;
}//end set
}//end Speed property accessor
|
The set side
The set side of the property accessor method for the Speed property receives the incoming value as moves per millisecond. The code converts this value to milliseconds per move and saves it in the instance variable named elapsedTimeTarget mentioned earlier.
This is the target for the elapsed time in milliseconds from one movement to the next movement of the sprite in the game window. Every time a sprite moves, it moves the same distance. Therefore, the apparent speed of sprite movement as seen by the viewer can be controlled by controlling the elapsed time between movements.
The get side
The get side of the property accessor method for the Speed property converts the returned value from milliseconds per move back to moves per millisecond.
The constructor for the Sprite class is shown in Listing 4.
Listing 4. The
constructor for the Sprite class.
public Sprite(String assetName, ContentManager contentManager, Random random) { image = contentManager.Load<Texture2D>(assetName); this.random = random; }//end constructor |
Load an image
The constructor loads an image for the sprite when it is instantiated. Therefore, it requires an Asset Name for the image and a reference to a ContentManager object.
A random number generator
The constructor also requires a reference to a Random object capable of generating a sequence of pseudo random values of type double.
(The purpose of the random number generator will become clear later.)
The program should use the same Random object for all sprites to avoid getting the same sequence of values for different sprites when two or more sprites are instantiated in a very short period of time.
The SetImage method is shown in Listing 5.
Listing 5. The SetImage method. public void SetImage(String assetName, ContentManager contentManager) { image = contentManager.Load<Texture2D>(assetName); }//end SetImage |
This method can be called to load a new image for an existing sprite. The method is essentially the same as a method having the same name that I explained in an earlier lesson, so no further explanation should be required.
This method causes the sprite to move in the direction of the direction vector if the elapsed time since the last move exceeds the elapsed time target based on the specified speed.
Beginning of the Move method
The Move method begins in Listing 6.
Listing 6. Beginning of the Move method. public void Move(GameTime gameTime) {
//Accumulate elapsed time since the last move.
elapsedTime +=
gameTime.ElapsedGameTime.Milliseconds;
if(elapsedTime > elapsedTimeTarget){
//It's time to make a move. Set the elapsed
// time to a value that will attempt to produce
// the specified speed on the average.
elapsedTime -= elapsedTimeTarget;
|
The sprite doesn't necessarily move every time the Move method is called. Instead, it uses the incoming parameter to compute the elapsed time since the last time that it actually moved.
To move or not to move
If that elapsed time exceeds the target that is based on the specified speed in moves/millisecond, then it reduces the elapsed time value by the target value and makes an adjustment to the position value. Changing the position value will cause the sprite to move in the game window the next time it is drawn.
Keeping up on the average
By reducing the elapsed time by the target time instead of setting it to zero, the sprite attempts to achieve the target speed on the average. For example, assume that for some reason, there is a long delay between calls to the Move method and the elapsed time value is two or three times greater than the target time.
Oops! Need to catch up
This means that the sprite has gotten behind and is not in the position that it should be in. In that case, the sprite will move every time the Move method is called for several successive calls to the Move method. (In other words, the sprite will experience a short spurt in speed.) This should cause it to catch up and be in the correct position once it does catch up.
Of course, if the elapsed time between calls to the Move method is greater than the target time over the long term, the sprite will never be able to keep up.
Code in the body of the if statement
If the conditional expression highlighted in yellow in Listing 6 returns true, then the last statement in Listing 6 along with the remainder of the body of the if statement will be executed. Otherwise, that statement and the remaining body of the if statement will be skipped.
The remaining body of the if statement begins in Listing 7.
Add the direction vector to the position vector
One of the advantages of treating the position and the direction as 2D vectors based on the structure named Vector2 is that the Vector2 structure provides various methods that can be used to manipulate vectors.
The code in Listing 7 calls the Add method of the Vector2 class to add the direction vector to the position vector returning the sum of the two vectors. The sum is saved as the new position vector.
Listing 7. Add the direction vector to the position vector. position = Vector2.Add(position,direction);
|
Vector addition
In case you are unfamiliar with the addition of 2D vectors, if you add a pair of 2D vectors, the X component of the sum is the sum of the X components and the Y component of the sum is the sum of the Y components.
A collision with an edge of the game window
The code in Listing 8 checks for a collision with an edge of the game window.
If the sprite collides with an edge, the code in Listing 8 causes the sprite to wrap around and reappear at the opposite edge, moving at the same speed in a different direction within the same quadrant as before.
In other words...
In other words, if a sprite is moving down and to the right and collides with the right edge of the window, it will reappear at the left edge, still moving down and to the right but not in exactly the same direction down and to the right.
Listing 8. Process a collision with an edge of the game window. if(position.X < -image.Width){ position.X = windowSize.X; NewDirection(); }//end if if(position.X > windowSize.X){ position.X = -image.Width/2; NewDirection(); }//end if if(position.Y < -image.Height) { position.Y = windowSize.Y; NewDirection(); }//end if if(position.Y > windowSize.Y){ position.Y = -image.Height / 2; NewDirection(); }//end if on position.Y }//end if on elapsed time }//end Move |
Modify the position and call the NewDirection method
In all cases shown in Listing 8, if a collision occurs, the position of the sprite is modified to position the sprite at the opposite edge. Then the method named NewDirection is called to modify the direction pointed to by the direction vector.
(The NewDirection method is declared private to prevent it from being accessible to code outside the Sprite class because it has no meaning outside the Sprite class.)
The end of the Move method
Listing 8 signals the end of the Move method.
Beginning of the method named NewDirection
The method named NewDirection begins in Listing 9.
Listing 9. Beginning of the method named NewDirection. private void NewDirection() { double length = Math.Sqrt( direction.X * direction.X + direction.Y * direction.Y); Boolean xNegative = (direction.X < 0)?true:false; Boolean yNegative = (direction.Y < 0)?true:false; |
The length of the direction vector and the signs of the components
Listing 9 begins by determining the length of the current direction vector along with the signs of the X and Y components of the vector.
Compute the hypotenuse
The code with the magenta highlight in Listing 9 calls the Math.Sqrt method and uses the Pythagorean Theorem to compute the length of the hypotenuse of the right triangle formed by the X and Y components of the direction vector. This is the length of the direction vector.
Use the conditional operator
Then the code with the yellow highlight in Listing 9 uses the conditional operator to determine if the signs of the components are negative. If so, the variables named xNegative and/or yNegative are set to true.
Compute components of a new direction vector
Having accomplished that task, the code in Listing 10 computes the components for a new direction vector of the same length with the X and Y components having random (but consistent) lengths and the same signs as before.
The NextDouble method of the Random class
For the code in Listing 10 to make any sense at all, you must know that the call to random.NextDouble returns a pseudo-random value, uniformly distributed between 0.0 and 1.0.
Listing 10. Compute components of a new direction vector. //Compute a new X component as a random portion of // the vector length. direction.X = (float)(length * random.NextDouble()); //Compute a corresponding Y component that will // keep the same vector length. direction.Y = (float)Math.Sqrt(length*length - direction.X*direction.X); //Set the signs on the X and Y components to match // the signs from the original direction vector. if(xNegative) direction.X = -direction.X; if(yNegative) direction.Y = -direction.Y; }//end NewDirection |
Compute a new random value for the X component
The code with the yellow highlight in Listing 10 computes a new value for the X component of the current direction vector, which is a random portion of the length of the current direction vector ranging from 0 to the full length of the vector.
Compute a consistent value for the Y component
Then the code with the cyan highlight in Listing 10 uses the Sqrt method along with the Pythagorean Theorem to compute a new value for the Y component, which when combined with the new X component will produce a direction vector having the same length as before.
Adjust the signs of the X and Y components
Finally, the code with the magenta highlight in Listing 10 uses the information gleaned earlier to cause the signs of the new X and Y components to match the signs of the original components.
A new direction vector with the same length in the same quadrant
By modifying the lengths of the X and Y components, the code in Listing 10 causes the direction pointed to by the new vector to be different from the direction pointed to by the original direction vector.
By causing the X and Y components to have the same signs, the code in Listing 10 causes the new direction vector to point into the same quadrant as before.
The Sprite.Draw method is shown in its entirety in Listing 11.
Listing 11. The Sprite.Draw method. public void Draw(SpriteBatch spriteBatch) { //Call the simplest available version of // SpriteBatch.Draw spriteBatch.Draw(image,position,Color.White); }//end Draw method //-------------------------------------------------// }//end Sprite class }//end namespace |
This Draw method is essentially the same as the Draw method that I explained in an earlier lesson so it shouldn't require further explanation.
Three different Draw methods
To avoid becoming confused, however, you should keep in mind that this program deals with the following three methods having the name Draw:
The end of the class
Listing 11 also signals the end of the Sprite class.
The Game1 class begins in Listing 12.
Listing 12. Beginning of the Game1 class. namespace XNA0128Proj { public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; SpriteBatch spriteBatch; //Use the following values to set the size of the // client area of the game window. The actual window // with its frame is somewhat larger depending on // the OS display options. On my machine with its // current display options, these dimensions // produce a 1024x768 game window. int windowWidth = 1017; int windowHeight = 738; //This is the length of the greatest distance in // pixels that any sprite will move in a single // frame of the game loop. double maxVectorLength = 5.0; //References to the space rocks are stored in this // List object. List<Sprite> rocks = new List<Sprite>(); int numRocks = 24;//Number of rocks. //The following value should never exceed 60 moves // per second unless the default frame rate is also // increased to more than 60 frames per second. double maxRockSpeed = 50;//moves per second //References to the power pills are stored in // this List. List<Sprite> pills = new List<Sprite>(); int numPills = 12;//Number of pills. double maxPillSpeed = 40;//moves per second //References to the UFOs are stored in this List. List<Sprite> ufos = new List<Sprite>(); int numUfos = 6;//Max number of ufos double maxUfoSpeed = 30; //Random number generator. It is best to use a single // object of the Random class to avoid the // possibility of using different streams that // produce the same sequence of values. //Note that the random.NextDouble() method produces // a pseudo-random value where the sequence of values // is uniformly distributed between 0.0 and 1.0. Random random = new Random(); |
Instance variables
Listing 12 declares several instance variables. Comments are provided to explain most of the instance variables. No explanation beyond the comments in Listing 12 should be required.
The constructor is shown in its entirety in Listing 13.
Listing 13. The modified constructor. public Game1() {//constructor graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; //Set the size of the game window. graphics.PreferredBackBufferWidth = windowWidth; graphics.PreferredBackBufferHeight = windowHeight; }//end constructor |
Nothing in this constructor is new to this lesson. Therefore, no further explanation should be required.
The overridden LoadContent method begins in Listing 14.
Listing 14. Beginning of the overridden LoadContent method. protected override void LoadContent() { spriteBatch = new SpriteBatch(GraphicsDevice); for(int cnt = 0;cnt < numRocks;cnt++){ rocks.Add(new Sprite("Rock",Content,random)); //Set the position of the current rock at a // random location within the game window. rocks[cnt].Position = new Vector2( (float)(windowWidth * random.NextDouble()), (float)(windowHeight * random.NextDouble())); //Get a direction vector for the current rock. // Make both components positive to cause the // vector to point down and to the right. rocks[cnt].Direction = DirectionVector( (float)maxVectorLength, (float)(maxVectorLength * random.NextDouble()), false,//xNeg false);//yNeg //Notify the Sprite object of the size of the // game window. rocks[cnt].WindowSize = new Point(windowWidth,windowHeight); //Set the speed in moves per second for the // current sprite to a random value between // maxRockSpeed/2 and maxRockSpeed. rocks[cnt].Speed = maxRockSpeed/2 + maxRockSpeed * random.NextDouble()/2; }//end for loop |
Instantiate all of the space rock sprites
The code in Listing 14 uses a for loop to instantiate all of the space rocks and to prepare them to move from left to right, top to bottom across the game window.
Call the Sprite constructor
The for loop begins with a call to the Sprite constructor (see the yellow highlight) to construct a new object of the Sprite class.
(As mentioned earlier, you should pass a reference to the same Random object each time you make a call to the Sprite constructor.)
Each new Sprite object's reference is added to the list referred to by the instance variable named rocks.
Set the property values
Once the new Sprite object is constructed, the object's reference is accessed and used to set the following property values:
Straightforward code
Except for the call to the DirectionVector method, the code in Listing 14 is straightforward and should not require an explanation beyond the embedded comments. I will explain the DirectionVector method shortly.
Instantiate and set properties on the power pills and the UFOs
Listing 15 uses essentially the same code to instantiate and set the property values on the power pill sprites and the UFO sprites.
Listing 15. Instantiate and set properties on the power pills and the UFOs. //Use the same process to instantiate all of the // power pills and cause them to move from right // to left, top to bottom. for(int cnt = 0;cnt < numPills;cnt++) { pills.Add(new Sprite("ball",Content,random)); pills[cnt].Position = new Vector2( (float)(windowWidth * random.NextDouble()), (float)(windowHeight * random.NextDouble())); pills[cnt].Direction = DirectionVector( (float)maxVectorLength, (float)(maxVectorLength * random.NextDouble()), true,//xNeg false);//yNeg pills[cnt].WindowSize = new Point(windowWidth,windowHeight); pills[cnt].Speed = maxPillSpeed/2 + maxPillSpeed * random.NextDouble()/2; }//end for loop //Use the same process to instantiate all of the // ufos and cause them to move from right to left, // bottom to top. for(int cnt = 0;cnt < numUfos;cnt++) { ufos.Add(new Sprite("ufo",Content,random)); ufos[cnt].Position = new Vector2( (float)(windowWidth * random.NextDouble()), (float)(windowHeight * random.NextDouble())); ufos[cnt].Direction = DirectionVector( (float)maxVectorLength, (float)(maxVectorLength * random.NextDouble()), true,//xNeg true);//yNeg ufos[cnt].WindowSize = new Point(windowWidth,windowHeight); ufos[cnt].Speed = maxUfoSpeed/2 + maxUfoSpeed * random.NextDouble()/2; }//end for loop }//end LoadContent |
The private DirectionVector method
The DirectionVector method shown in Listing 16 is called each time the code in Listing 14 and Listing 15 needs to set the Direction property on a sprite. This method is declared private because it has no meaning outside the Sprite class.
Listing 16. The private DirectionVector method. private Vector2 DirectionVector(float vecLen, float xLen, Boolean negX, Boolean negY){ Vector2 result = new Vector2(xLen,0); result.Y = (float)Math.Sqrt(vecLen*vecLen - xLen*xLen); if(negX) result.X = -result.X; if(negY) result.Y = -result.Y; return result; }//end DirectionVector |
Return a direction vector
The DirectionVector method returns a direction vector as type Vector2 given the length of the vector, the length of the X component of the vector, the sign of the X component, and the sign of the Y component.
The signs of the components
You should set negX and/or negY to true to cause them to be negative.
By adjusting the signs on the X and Y components, the vector can be caused to point into any of the four quadrants. The relationships between these two values and the direction of motion of the sprite are as follows:
Very similar to earlier code
The code in Listing 16 is very similar to the code that I explained earlier in Listing 10, so the code in Listing 16 should not require further explanation.
The overridden Update method is shown in its entirety in Listing 17.
Listing 17. Tell the sprites to move. protected override void Update(GameTime gameTime) { //Tell all the rocks in the list to move. for(int cnt = 0;cnt < rocks.Count;cnt++) { rocks[cnt].Move(gameTime); }//end for loop //Tell all the power pills in the list to move. for(int cnt = 0;cnt < pills.Count;cnt++) { pills[cnt].Move(gameTime); }//end for loop //Tell all the ufos in the list to move. for(int cnt = 0;cnt < ufos.Count;cnt++) { ufos[cnt].Move(gameTime); }//end for loop base.Update(gameTime); }//end Update method |
Very simple code
Because the code to instantiate the Sprite objects and set their properties was placed in the LoadContent method in this program, the code in the overridden Update method is very simple.
The code in Listing 17 uses for loops to access the references and call the Move method on every Sprite object.
To move or not to move, that is the question
As you learned earlier, when the Move method is called on an individual Sprite object, the sprite may or it may not actually move depending on the value of its Speed property and the elapsed time since its last actual move.
A characteristic of an object-oriented program
One of the characteristics of an object-oriented program is that the individual objects know how to behave with minimal supervision. In effect, a call to the Sprite.Move method in Listing 17 tells the object to make its own decision and to move if it is time for it to move.
Listing 18 shows the overridden Game.Draw method in its entirety.
Listing 18. The overridden Game.Draw method. protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); spriteBatch.Begin(); //Draw all rocks. for(int cnt = 0;cnt < rocks.Count;cnt++) { rocks[cnt].Draw(spriteBatch); }//end for loop //Draw all power pills. for(int cnt = 0;cnt < pills.Count;cnt++) { pills[cnt].Draw(spriteBatch); }//end for loop //Draw all ufos. for(int cnt = 0;cnt < ufos.Count;cnt++) { ufos[cnt].Draw(spriteBatch); }//end for loop spriteBatch.End(); base.Draw(gameTime); }//end Draw method //-------------------------------------------------// }//end class }//end namespace |
Erase and redraw the entire game window
Listing 18 begins by painting over everything in the game window with CornflowerBlue pixels. Then it uses for loops to access and call the Sprite.Draw method on every Sprite object.
Go redraw yourself
Each call to a Sprite object's Draw method in Listing 18 is a notification to the Sprite object that it should cause itself to be redrawn in the appropriate position with the appropriate image in the game window.
(This is another manifestation of an object knowing how to behave with minimal supervision. The overridden Game.Draw method doesn't know and doesn't care where the Sprite object should be positioned or what image it should draw to represent itself. The Game.Draw method simply knows that every Sprite object must redraw itself at the appropriate position with the appropriate image at this point in time. Decisions regarding position and image are left up to the Sprite object.)
Regardless of whether or not a sprite has decided to move, it should cause itself to be redrawn in the game window because its image has just been replaced by a bunch of CornflowerBlue pixels.
Not the same approach as other game engines
Some game engines take a different approach and in the interest of speed, redraw only those portions of the game window that have changed. This can lead to a great deal of complexity and it is often the responsibility of the game programmer to manage that complexity.
That is not the case with XNA. From the viewpoint of the XNA game programmer, the entire game window is redrawn once during every iteration of the game loop. This causes XNA to be easier to program than some other game engines. On the other hand, it might be argued that XNA sacrifices speed for simplicity.
The end of the program
Listing 18 also signals the end of the overridden Game.Draw method, the end of the Game1 class, and the end of the program.
I encourage you to copy the code from Listing 19 and Listing 20. Use that code to create an XNA project. (You should be able to use any small image files as sprites.) Compile and run the project. Experiment with the code, making changes, and observing the results of your changes. Make certain that you can explain why your changes behave as they do.
In this lesson, you learned how to add more sophistication to the Sprite class that was introduced in an earlier lesson.
We're getting very close to being able to write a 2D arcade style game involving UFOs, space rocks, and power pills. There are just a few more tools that we need and I will show you how to create those tools in the upcoming lessons.
The critical tools are:
Once we have those tools, we can write a game where the challenge is to cause the UFO to successfully navigate across the game window without colliding with a space rock, so I will concentrate on those two tools in the next few lessons. (There are probably other games that we could also create using those tools.)
Beyond that, there are several other tools that will make it possible for us to create more sophisticated and interesting games:
I will show you how to create those tools later in this series of lessons.
Complete listings of the XNA program files discussed in this lesson are provided in Listing 19 and Listing 20.Listing 19. The class named Sprite for the project named XNA0128Proj. /*Project XNA0128Proj * This file defines a Sprite class from which a Sprite * object can be instantiated. *******************************************************/ using System; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; namespace XNA0128Proj { class Sprite { private Texture2D image; private Vector2 position = new Vector2(0,0); private Vector2 direction = new Vector2(0,0); private Point windowSize; private Random random; double elapsedTime;//in milliseconds //The following value is the inverse of speed in // moves/msec expressed in msec/move. double elapsedTimeTarget; //-------------------------------------------------// //Position property accessor public Vector2 Position { get { return position; } set { position = value; }//end set }//end Position property accessor //-------------------------------------------------// //WindowSize property accessor public Point WindowSize { set { windowSize = value; }//end set }//end WindowSize property accessor //-------------------------------------------------// //Direction property accessor public Vector2 Direction { get { return direction; } set { direction = value; }//end set }//end Direction property accessor //-------------------------------------------------// //Speed property accessor. The set side should be // called with speed in moves/msec. The get side // returns speed moves/msec. public double Speed { get { //Convert from elapsed time in msec/move to // speed in moves/msec. return elapsedTimeTarget/1000; } set { //Convert from speed in moves/msec to // elapsed time in msec/move. elapsedTimeTarget = 1000/value; }//end set }//end Speed property accessor //-------------------------------------------------// //This constructor loads an image for the sprite // when it is instantiated. Therefore, it requires // an asset name for the image and a reference to a // ContentManager object. //Requires a reference to a Random object. Should // use the same Random object for all sprites to // avoid getting the same sequence for different // sprites. public Sprite(String assetName, ContentManager contentManager, Random random) { image = contentManager.Load<Texture2D>(assetName); this.random = random; }//end constructor //-------------------------------------------------// //This method can be called to load a new image // for the sprite. public void SetImage(String assetName, ContentManager contentManager) { image = contentManager.Load<Texture2D>(assetName); }//end SetImage //-------------------------------------------------// //This method causes the sprite to move in the // direction of the direction vector if the elapsed // time since the last move exceeds the elapsed // time target based on the specified speed. public void Move(GameTime gameTime) { //Accumulate elapsed time since the last move. elapsedTime += gameTime.ElapsedGameTime.Milliseconds; if(elapsedTime > elapsedTimeTarget){ //It's time to make a move. Set the elapsed // time to a value that will attempt to produce // the specified speed on the average. elapsedTime -= elapsedTimeTarget; //Add the direction vector to the position // vector to get a new position vector. position = Vector2.Add(position,direction); //Check for a collision with an edge of the game // window. If the sprite reaches an edge, cause // the sprite to wrap around and reappear at the // other edge, moving at the same speed in a // different direction within the same quadrant // as before. if(position.X < -image.Width){ position.X = windowSize.X; NewDirection(); }//end if if(position.X > windowSize.X){ position.X = -image.Width/2; NewDirection(); }//end if if(position.Y < -image.Height) { position.Y = windowSize.Y; NewDirection(); }//end if if(position.Y > windowSize.Y){ position.Y = -image.Height / 2; NewDirection(); }//end if on position.Y }//end if on elapsed time }//end Move //-------------------------------------------------// //This method determines the length of the current // direction vector along with the signs of the X // and Y components of the current direction vector. // It computes a new direction vector of the same // length with the X and Y components having random // lengths and the same signs. //Note that random.NextDouble returns a // pseudo-random value, uniformly distrubuted // between 0.0 and 1.0. private void NewDirection() { //Get information about the current direction // vector. double length = Math.Sqrt( direction.X * direction.X + direction.Y * direction.Y); Boolean xNegative = (direction.X < 0)?true:false; Boolean yNegative = (direction.Y < 0)?true:false; //Compute a new X component as a random portion of // the vector length. direction.X = (float)(length * random.NextDouble()); //Compute a corresponding Y component that will // keep the same vector length. direction.Y = (float)Math.Sqrt(length*length - direction.X*direction.X); //Set the signs on the X and Y components to match // the signs from the original direction vector. if(xNegative) direction.X = -direction.X; if(yNegative) direction.Y = -direction.Y; }//end NewDirection //-------------------------------------------------// public void Draw(SpriteBatch spriteBatch) { //Call the simplest available version of // SpriteBatch.Draw spriteBatch.Draw(image,position,Color.White); }//end Draw method //-------------------------------------------------// }//end class }//end namespace |
Listing 20. The class named Game1 for the project named XNA0128Proj. /*Project XNA0128Proj * This project demonstrates how to integrate space * rocks, power pills, and ufos in a program using * objects of a Sprite class. This could be the * beginnings of a space game. * *****************************************************/ using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using XNA0128Proj; namespace XNA0128Proj { public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; SpriteBatch spriteBatch; //Use the following values to set the size of the // client area of the game window. The actual window // with its frame is somewhat larger depending on // the OS display options. On my machine with its // current display options, these dimensions // produce a 1024x768 game window. int windowWidth = 1017; int windowHeight = 738; //This is the length of the greatest distance in // pixels that any sprite will move in a single // frame of the game loop. double maxVectorLength = 5.0; //References to the space rocks are stored in this // List object. List<Sprite> rocks = new List<Sprite>(); int numRocks = 24;//Number of rocks. //The following value should never exceed 60 moves // per second unless the default frame rate is also // increased to more than 60 frames per second. double maxRockSpeed = 50;//frames per second //References to the power pills are stored in // this List. List<Sprite> pills = new List<Sprite>(); int numPills = 12;//Number of pills. double maxPillSpeed = 40;//moves per second //References to the UFOs are stored in this List. List<Sprite> ufos = new List<Sprite>(); int numUfos = 6;//Max number of ufos double maxUfoSpeed = 30; //Random number generator. It is best to use a single // object of the Random class to avoid the // possibility of using different streams that // produce the same sequence of values. //Note that the random.NextDouble() method produces // a pseudo-random value where the sequence of values // is uniformly distributed between 0.0 and 1.0. Random random = new Random(); //-------------------------------------------------// public Game1() {//constructor graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; //Set the size of the game window. graphics.PreferredBackBufferWidth = windowWidth; graphics.PreferredBackBufferHeight = windowHeight; }//end constructor //-------------------------------------------------// protected override void Initialize() { //No initialization required. base.Initialize(); }//end Initialize //-------------------------------------------------// protected override void LoadContent() { spriteBatch = new SpriteBatch(GraphicsDevice); //Instantiate all of the rocks and cause them to // move from left to right, top to // bottom. Pass a reference to the same Random // object to all of the sprites. for(int cnt = 0;cnt < numRocks;cnt++){ rocks.Add(new Sprite("Rock",Content,random)); //Set the position of the current rock at a // random location within the game window. rocks[cnt].Position = new Vector2( (float)(windowWidth * random.NextDouble()), (float)(windowHeight * random.NextDouble())); //Get a direction vector for the current rock. // Make both components positive to cause the // vector to point down and to the right. rocks[cnt].Direction = DirectionVector( (float)maxVectorLength, (float)(maxVectorLength * random.NextDouble()), false,//xNeg false);//yNeg //Notify the Sprite object of the size of the // game window. rocks[cnt].WindowSize = new Point(windowWidth,windowHeight); //Set the speed in moves per second for the // current sprite to a random value between // maxRockSpeed/2 and maxRockSpeed. rocks[cnt].Speed = maxRockSpeed/2 + maxRockSpeed * random.NextDouble()/2; }//end for loop //Use the same process to instantiate all of the // power pills and cause them to move from right // to left, top to bottom. for(int cnt = 0;cnt < numPills;cnt++) { pills.Add(new Sprite("ball",Content,random)); pills[cnt].Position = new Vector2( (float)(windowWidth * random.NextDouble()), (float)(windowHeight * random.NextDouble())); pills[cnt].Direction = DirectionVector( (float)maxVectorLength, (float)(maxVectorLength * random.NextDouble()), true,//xNeg false);//yNeg pills[cnt].WindowSize = new Point(windowWidth,windowHeight); pills[cnt].Speed = maxPillSpeed/2 + maxPillSpeed * random.NextDouble()/2; }//end for loop //Use the same process to instantiate all of the // ufos and cause them to move from right to left, // bottom to top. for(int cnt = 0;cnt < numUfos;cnt++) { ufos.Add(new Sprite("ufo",Content,random)); ufos[cnt].Position = new Vector2( (float)(windowWidth * random.NextDouble()), (float)(windowHeight * random.NextDouble())); ufos[cnt].Direction = DirectionVector( (float)maxVectorLength, (float)(maxVectorLength * random.NextDouble()), true,//xNeg true);//yNeg ufos[cnt].WindowSize = new Point(windowWidth,windowHeight); ufos[cnt].Speed = maxUfoSpeed/2 + maxUfoSpeed * random.NextDouble()/2; }//end for loop }//end LoadContent //-------------------------------------------------// //This method returns a direction vector given the // length of the vector, the length of the // X component, the sign of the X component, and the // sign of the Y component. Set negX and/or negY to // true to cause them to be negative. By adjusting // the signs on the X and Y components, the vector // can be caused to point into any of the four // quadrants. private Vector2 DirectionVector(float vecLen, float xLen, Boolean negX, Boolean negY){ Vector2 result = new Vector2(xLen,0); result.Y = (float)Math.Sqrt(vecLen*vecLen - xLen*xLen); if(negX) result.X = -result.X; if(negY) result.Y = -result.Y; return result; }//end DirectionVector //-------------------------------------------------// protected override void UnloadContent() { //No content unload required. }//end unloadContent //-------------------------------------------------// protected override void Update(GameTime gameTime) { //Tell all the rocks in the list to move. for(int cnt = 0;cnt < rocks.Count;cnt++) { rocks[cnt].Move(gameTime); }//end for loop //Tell all the power pills in the list to move. for(int cnt = 0;cnt < pills.Count;cnt++) { pills[cnt].Move(gameTime); }//end for loop //Tell all the ufos in the list to move. for(int cnt = 0;cnt < ufos.Count;cnt++) { ufos[cnt].Move(gameTime); }//end for loop base.Update(gameTime); }//end Update method //-------------------------------------------------// protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); spriteBatch.Begin(); //Draw all rocks. for(int cnt = 0;cnt < rocks.Count;cnt++) { rocks[cnt].Draw(spriteBatch); }//end for loop //Draw all power pills. for(int cnt = 0;cnt < pills.Count;cnt++) { pills[cnt].Draw(spriteBatch); }//end for loop //Draw all ufos. for(int cnt = 0;cnt < ufos.Count;cnt++) { ufos[cnt].Draw(spriteBatch); }//end for loop spriteBatch.End(); base.Draw(gameTime); }//end Draw method //-------------------------------------------------// }//end class }//end namespace |
Copyright 2009, Richard G. Baldwin. Reproduction in whole or in part in any form or medium without express written permission from Richard Baldwin is prohibited.
Richard Baldwin is a college professor (at Austin Community College in Austin, TX) and private consultant whose primary focus is object-oriented programming using Java and other OOP languages.
Richard has participated in numerous consulting projects and he frequently provides onsite training at the high-tech companies located in and around Austin, Texas. He is the author of Baldwin's Programming Tutorials, which have gained a worldwide following among experienced and aspiring programmers.
In addition to his programming expertise, Richard has many years of practical experience in Digital Signal Processing (DSP). His first job after he earned his Bachelor's degree was doing DSP in the Seismic Research Department of Texas Instruments. (TI is still a world leader in DSP.) In the following years, he applied his programming and DSP expertise to other interesting areas including sonar and underwater acoustics.
Richard holds an MSEE degree from Southern Methodist University and has many years of experience in the application of computer technology to real-world problems.
-end-