baldwin.jpg

Programming with XNA Game Studio

Improving the Sprite Class

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


Preface

General

XNA Game Studio 3.1
Note that all references to XNA in this lesson are references to version 3.1 or later.

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.)

Viewing tip

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.

Figures

Listings

Supplemental material

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.

General background information

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.

Preview

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.
Screen shot of 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.

Discussion and sample code

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

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.

Properties

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

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

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.

The Move method

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 Draw method

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

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 modified constructor

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

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

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.

The overridden Game.Draw method

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.

Run 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. 

Summary

In this lesson, you learned how to add more sophistication to the Sprite class that was introduced in an earlier lesson.

What's next?

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 program listings

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

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.

About the author

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.

Baldwin@DickBaldwin.com

-end-