Learn to design, create, and use a simple Sprite class. Also learn to use a generic List object.
Published: January 27, 2010
Validated with Amaya
By Richard G. Baldwin
XNA Programming Notes # 0126
|
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.
The three pillars of OOP
An object-oriented programming language like C# supports encapsulation, inheritance, and polymorphism. In this lesson, you will learn how to take advantage of encapsulation.
Wanted, lots of sprites
Assume that you are writing a game program in which you need to have several dozen similar sprites on the screen at the same time. Creating and controlling that many sprites without using encapsulation would require you to write a lot of program code.
Encapsulation to the rescue
However, by encapsulating the characteristics of a sprite into a class, which is a blueprint for an object, you can instantiate sprite objects just like cutting out cookies with a cookie cutter.
In this lesson, you will learn to define and use a very simple Sprite class. In future lessons, you will learn how to modify the Sprite class to make it more sophisticated by making use of inheritance and polymorphism in addition to encapsulation
The XNA project that I will explain in this lesson is named XNA0126Proj. This project demonstrates how to design and use a very simple version of a Sprite class. An object instantiated from the Sprite class has the following general characteristics:
Demonstrate how to use the Sprite class.
Methods are overridden in the standard XNA Game1 class that demonstrate the use of the Sprite class.
One Sprite object is instantiated in the overridden LoadContent method of the Game1 class. The object's reference is saved in a generic List object. Twenty-three more Sprite objects are instantiated in the overridden Update method while the game loop is running.
Instantiate Sprite objects on the move
A new Sprite object is instantiated in the Update method every 8th iteration of the game loop until twenty-four Sprite objects have been instantiated. The object's references are saved in the same generic List object mentioned above.
An image of a blue ball is stored in 12 of the objects and an image of a red ball is stored in the other 12 objects. The red and blue balls alternate and the Sprite objects are drawn in a diagonal line as shown in Figure 1.
Figure 1, Seven Sprite
objects.
Seven Sprite objects
Figure 1 shows the game window after seven of the twenty-four Sprite objects have been instantiated and drawn in the game window.
Move to the right and down
The line of Sprite objects moves across the game window from upper left to lower right as the Sprite objects are being instantiated. They stop moving when they reach the bottom right corner of the game window.
When the objects stop moving, the image in the topmost Sprite object is changed from a blue ball to a green ball as shown in Figure 2.
Figure 2, Twenty-four Sprite
objects with a green one at the top.
Will discuss in fragments
As usual, I will explain the program code in fragments. A complete listing of the class named Sprite is provided in Listing 14 and a complete listing of the class named Game1 is provided in Listing 15.
I will begin my explanation with the class named Sprite.
The file named Sprite.cs (see Listing 14) defines a simple version of a Sprite class from which multiple Sprite objects can be instantiated, loaded with an image, and drawn in the game window. The Position property of each Sprite object can be accessed by the user to control the position at which the sprite is drawn.
Beginning of the Sprite class
The definition of the Sprite class begins in Listing 1.
Listing 1. Beginning of the Sprite class. namespace XNA0126Proj { class Sprite { private Texture2D texture; private Vector2 position = new Vector2(0,0); //-------------------------------------------------// public Vector2 Position { get { return position; }//end get set { position = value; }//end set }//end Position property accessor |
Two instance variables and a property accessor method
Listing 1 declares two instance variables and defines an accessor for the Position property.
The first instance variable named texture will be used to store the image for the sprite. The second instance variable named position will be used to store the value for the Position property.
Listing 2 defines two overloaded constructors for the Sprite class.
Listing 2. Two overloaded constructors. public Sprite() {//constructor }//end noarg constructor //-------------------------------------------------// public Sprite(String assetName, ContentManager contentManager) { texture = contentManager.Load<Texture2D>(assetName); }//end constructor |
The first overloaded constructor, which requires no parameters, makes it possible to instantiate a Sprite object without loading an image for the object when it is constructed. A method named SetImage can be called later to load an image for the object.
(Be aware that if you instantiate a Sprite object using this constructor and then attempt to draw the object without first calling the SetImage method to load an image into the object, the program will fail with a runtime error.)
Image loaded during construction
The second overloaded constructor, which requires two parameters, makes it possible to load an image for the Sprite object when it is constructed.
The first parameter
The first parameter required by the second constructor is the Asset Name property for an image file that is added to the Content folder during the project design phase.
The second parameter
The second parameter is a reference to the ContentManager object that is inherited into the Game1 object from the Game class.
Loading the image
You are already familiar with the code in the body of the constructor that is used to load the image into the object.
The SetImage method is shown in its entirety in Listing 3.
Listing 3. The SetImage method. public void SetImage(String assetName, ContentManager contentManager) { texture = contentManager.Load<Texture2D>(assetName); }//end SetImage |
Load an image into a Sprite object
The SetImage method makes it possible to load an image into a Sprite object that was originally constructed without an image, or to change the image in a Sprite object that already contains an image.
The parameters and the body
The parameters required by this method and the body of the method are the same as for the first constructor discussed above.
Listing 4 shows the Draw method of the Sprite class in its entirety.
Listing 4. The Draw method of the Sprite class. public void Draw(SpriteBatch spriteBatch) {
spriteBatch.Draw(texture,position,Color.White);
}//end Draw method
//-------------------------------------------------//
}//end Sprite class
}//end namespace
|
This method should be called after a call to the SpriteBatch.Begin method and before a call to the SpriteBatch.End method.
A single parameter
The method requires a single parameter, which is a reference to the SpriteBatch object on which the Begin method has been called.
The body of the Sprite.Draw method
You should recognize the single statement highlighted in yellow as a call to the simplest available Draw method belonging to the SpriteBatch object. This version of the SpriteBatch.Draw method allows the caller to specify
The end of the class
Listing 4 signals the end of the Sprite class.
Methods of the Game1 class were overridden to demonstrate the use of the Sprite class to produce the output described earlier.
Beginning of the class named Game1
The class named Game1 begins in Listing 5.
Listing 5. Beginning of the class named Game1. namespace XNA0126Proj { public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; SpriteBatch spriteBatch; //References to the Sprite objects are stored in this // List object. List<Sprite> sprites = new List<Sprite>(); int maxSprites = 24;//Max number of sprites. int frameCnt = 0;//Game loop frame counter //This is the limit on the number of frames in which // the sprites are moved. int moveLim = 200; |
Listing 5 simply declares several instance variables, most of which you should recognize. The others are well described by the comments. However, one of the instance variables, sprites, introduces a concept that is new to this lesson.
Listing 5 declares a variable named sprites and populates it with a reference to a new generic List object that is conditioned to store and retrieve references to objects of the class Sprite.
What does the List class documentation have to say?
Here is some of what the documentation has to say about this class:
"Represents a strongly typed list of objects that can be accessed by index. Provides methods to search, sort, and manipulate lists."
How will I use this class in this program?
The generic List class provides many more capabilities than I will use in this program. I will use an object of the generic List class to store references to Sprite objects that I can later access using a zero-based index. This will eliminate the requirement to declare a separate reference variable for each of the Sprite objects that I instantiate.
The constructor for the Game1 class was modified to set the size of the game window as shown in Listing 6.
Listing 6. The modified constructor for the Game1 class. public Game1() {//constructor graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; //Set the size of the game window. graphics.PreferredBackBufferWidth = 450; graphics.PreferredBackBufferHeight = 450; }//end constructor |
You have seen code identical to this code in earlier lessons so there is nothing new to discuss here.
The overridden LoadContent method is shown in Listing 7.
Listing 7. The overridden LoadContent method. protected override void LoadContent() { spriteBatch = new SpriteBatch(GraphicsDevice); //Create the first sprite in the LoadContent // method using the noarg constructor. sprites.Add(new Sprite()); //Assign an image to the sprite. sprites[0].SetImage("blueball",Content); }//end LoadContent |
A new Sprite object
The statement with the yellow highlight in Listing 7 instantiates a new Sprite object with no image and calls the Add method of the generic List object to add the object's reference to the end of the list.
List capacity considerations
One advantage of using a generic List object as an alternative to a simple array is that it is not necessary to declare the capacity of the List object when the program is compiled. The capacity of the List object increases automatically as necessary to accommodate all of the references that are added to the list.
Since this is the first reference added to the list, it can be accessed later using an index value of 0.
Assign an image to the Sprite object
The statement with the cyan highlight in Listing 7 retrieves the reference from the list at index 0 and uses that reference to call the SetImage method on the Sprite object to which it refers.
You learned about the SetImage method in the discussion of the code in Listing 3. This call causes the Sprite object whose reference is stored at index 0 in the list to load the image from the image file with the Asset Name property value of "blueball".
As mentioned earlier, the second parameter named Content is a reference to a ContentManager object that is inherited from the Game class.
As you learned earlier, this program creates, manipulates, and draws 24 objects of the Sprite class in the game window as shown in Figure 2. The first Sprite object was created in the overridden LoadContent method when it was called earlier.
The Count property of the List object
The generic List object referred to by sprites has a property named Count that keeps track of the number of references contained in the object. The first time the Update method is called, the value of Count is 1 as a result of the Sprite object's reference having been added to the list in the LoadContent method earlier.
The remaining 23 Sprite objects
This program creates the remaining 23 sprites in the Update method to simulate a game in which sprites come and go as the game progresses.
The overridden Update method begins in Listing 8.
Listing 8. Beginning of the overridden Update method. protected override void Update(GameTime gameTime) { if(sprites.Count < (maxSprites)) { |
Adding Sprite object references to the list
Listing 8 shows the beginning of an if statement in which a new Sprite objects reference will be added to the list every eighth iteration (frame) of the game loop until all 24 sprites have been added.
Note that a frame counter named frameCnt is declared and initialized to zero in Listing 5 and is incremented near the end of the overridden Update method in Listing 12.
Instantiate new Sprite objects
The code in Listing 9 uses the modulus (%) operator to identify every eighth iteration of the game loop and to instantiate a new Sprite object during those iterations.
Listing 9. Instantiate new Sprite objects. if(frameCnt % 8 == 0) { //Instantiate a new sprite every 8th frame. if((sprites.Count) % 2 == 0) { //Even numbered sprites sprites.Add(new Sprite("blueball",Content)); } else { //Odd numbered sprites sprites.Add(new Sprite("redball",Content)); }//end else }//end if on frameCnt }//end if on sprites.Count |
The modulus operator
In case you have forgotten, the modulus operator returns the remainder of a division instead of returning the quotient. If an integer value is divided by 8, the returned value is 0 only when the integer value is a multiple of 8. (Also by definition, 0 % 8 returns 0.)
Every eighth iteration
Therefore, the conditional expression highlighted in yellow in Listing 9 will allow the statements highlighted in cyan, (which instantiate new Sprite objects), to be executed only during every eighth iteration of the game loop.
Further, the conditional expression in Listing 8 will not allow the code in Listing 9 to be executed after 24 Sprite objects have been instantiated.
Even and odd sprite images
The conditional expression highlighted in magenta in Listing 9 causes the new Sprite objects that are instantiated to alternate between the "blueball" and "redball" image images shown in Figure 1.
Make all the existing sprites move
Listing 5 declares an instance variable named moveLim and sets its value to 200. The code in Listing 10 makes all of the existing sprites move to the right and down if the value of the frame counter is less than 200.
Listing 10. Make all the existing sprites move. if(frameCnt < moveLim) { for(int cnt = 0;cnt < sprites.Count;cnt++) { sprites[cnt].Position = new Vector2( 10 * cnt + frameCnt,10 * cnt + frameCnt); }//end for loop }//end if |
Use a for loop
The code in Listing 10 uses a for loop to access each of the Sprite object references currently stored in the list, iterating from 0 to one less than the count of references stored in the list given by sprites.Count.
Put a new value in the Position property
Once a Sprite object's reference has been accessed, Listing 10 sets the Position property stored in the object to a new Vector2 object for which the X and Y values have been modified on the basis of the frame counter.
The new X and Y values cause the object to be drawn a little further down and to the right the next time it is drawn relative to its current position. This causes the entire diagonal line of Sprite objects to move down and to the right in the game window.
Load a green ball image in the topmost Sprite object
Listing 11 calls the SetImage method to change the image in the topmost sprite at the end of the run from a blue ball to a green ball.
Listing 11. Load a
green ball image in the topmost Sprite object.
if(frameCnt == moveLim) { sprites[0].SetImage("greenball",Content); }//end if |
This capability would be useful, for example to change a sprite's image into a fireball in the event of a collision with another sprite.
Maintain the frame counter
The code in Listing 12 keeps track of the count of the first moveLim iterations of the game loop.
Listing 12. Maintain the frame counter. if(frameCnt < moveLim) { frameCnt++; }//end if base.Update(gameTime); }//end Update method |
The end of the overridden Update method
Then Listing 12 makes the required call to the Update method in the superclass and signals the end of the method.
The overridden Game1.Draw method is shown in its entirety in Listing 13.
Listing 13. The overridden Game1.Draw method. protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); spriteBatch.Begin(); //Draw all sprites. for(int cnt = 0;cnt < sprites.Count;cnt++) { sprites[cnt].Draw(spriteBatch); }//end for loop spriteBatch.End(); base.Draw(gameTime); }//end Draw method //-------------------------------------------------// }//end class }//end namespace |
Draw all Sprite objects
After calling the SpriteBatch.Begin method and before calling the SpriteBatch.End method, Listing 13 uses a for loop to call the Sprite.Draw method on every Sprite object whose reference is stored in the list, passing a reference to the SpriteBatch object as a parameter in each call.
Call the SpriteBatch.Draw method
This causes each object to execute the statement highlighted in yellow in Listing 4. The statement in Listing 4 causes the Sprite object to call the SpriteBatch.Draw method to draw itself at the position specified by the current value of its Position property.
Note that there are three different methods named Draw being used here:
The end of the Game1.Draw method
After calling the SpriteBatch.End method, Listing 13 makes the required call to the superclass' Game.Draw method and then signals the end of the Game1.Draw method. Listing 13 also signals the end of the class and the end of the program.
I encourage you to copy the code from Listing 14 and Listing 15. Use that code to create an XNA project. 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.
You learned how to design, create, and use a simple Sprite class. You also learned how to use a generic List object.
Complete listings of the XNA program files discussed in this lesson are provided in Listing 14 and Listing 15 below.Listing 14. Contents of the file named Sprite.cs /*Project XNA0126Proj * This file defines a very simple version of a Sprite * class from which multiple Sprite objects can be * instantiated, loaded with an image, and drawn. * The Position property can be accessed by the user * to control the position at which the sprite is drawn. *******************************************************/ using System; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; namespace XNA0126Proj { class Sprite { private Texture2D texture; private Vector2 position = new Vector2(0,0); //-------------------------------------------------// public Vector2 Position { get { return position; } set { position = value; }//end set }//end Position property accessor //-------------------------------------------------// //This constructor makes it possible to instantiate // a sprite without assigning an image to the sprite. public Sprite() {//constructor }//end noarg constructor //-------------------------------------------------// //This constructor makes it possible to assign an // image to the sprite when it is instantiated. public Sprite(String assetName, ContentManager contentManager) { texture = contentManager.Load<Texture2D>(assetName); }//end constructor //-------------------------------------------------// //This method makes it possible to assign a new image // to the sprite. public void SetImage(String assetName, ContentManager contentManager) { texture = contentManager.Load<Texture2D>(assetName); }//end SetImage //-------------------------------------------------// public void Draw(SpriteBatch spriteBatch) { //Call the simplest available version of // SpriteBatch.Draw spriteBatch.Draw(texture,position,Color.White); }//end Draw method //-------------------------------------------------// }//end class }//end namespace |
Listing 15. Contents of the file named Game1.cs. /*Project XNA0126Proj * This project demonstrates how to design and use a very * simple version of a Sprite class. * * One Sprite object is instantiated in the LoadContent * method. The object's reference is saved in a generic * List object. * * Twenty-three more Sprite objects are instantiated * while the game loop is running. A new object is * instantiated every 8th iteration of the game loop * until 24 objects have been instantiated. Their * references are saved in a generic List object. * * An image of a blueball is stored in 12 of the objects * and an image of a redball is stored in the other 12 * objects. * * The Sprite objects are drawn in a diagonal line in * the game window. The line of Sprite objects moves * across the game window from upper left to lower right. * The Sprite objects stop moving when they reach the * bottom right corner of the game window. * * When the objects stop moving, the image in the * topmost Sprite object is changed from a blueball to a * greenball. * *****************************************************/ using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using XNA0126Proj; namespace XNA0126Proj { public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; SpriteBatch spriteBatch; //References to the Sprite objects are stored in this // List object. List<Sprite> sprites = new List<Sprite>(); int maxSprites = 24;//Max number of sprites. int frameCnt = 0;//Game loop frame counter //This is the limit on the number of frames in which // the sprites are moved. int moveLim = 200; //-------------------------------------------------// public Game1() {//constructor graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; //Set the size of the game window. graphics.PreferredBackBufferWidth = 450; graphics.PreferredBackBufferHeight = 450; }//end constructor //-------------------------------------------------// protected override void Initialize() { //No initialization required. base.Initialize(); }//end Initialize //-------------------------------------------------// protected override void LoadContent() { spriteBatch = new SpriteBatch(GraphicsDevice); //Create the first sprite in the LoadContent // method using the noarg constructor. sprites.Add(new Sprite()); //Assign an image to the sprite. sprites[0].SetImage("blueball",Content); //More ontent is loaded in the Update method. }//end LoadContent //-------------------------------------------------// protected override void UnloadContent() { //No content unload required. }//end unloadContent //-------------------------------------------------// protected override void Update(GameTime gameTime) { //Create remaining sprites in the Update method to // simulate a game in which sprites come and go as // the game progresses. if(sprites.Count < (maxSprites)) { if(frameCnt % 8 == 0) { //Instantiate a new sprite every 8th frame. if((sprites.Count) % 2 == 0) { //Even numbered sprites sprites.Add(new Sprite("blueball",Content)); } else { //Odd numbered sprites sprites.Add(new Sprite("redball",Content)); }//end else }//end if on frameCnt }//end if on sprites.Count //Make all the sprites move. if(frameCnt < moveLim) { for(int cnt = 0;cnt < sprites.Count;cnt++) { sprites[cnt].Position = new Vector2( 10 * cnt + frameCnt,10 * cnt + frameCnt); }//end for loop }//end if //Change the image on the first sprite at the end // of the run. Could be used, for example to // change a sprite's image to a fireball in the // event of a collision. if(frameCnt == moveLim) { sprites[0].SetImage("greenball",Content); }//end if //Keep track of the count of the first moveLim // iterations of the game loop. if(frameCnt < moveLim) { frameCnt++; }//end if base.Update(gameTime); }//end Update method //-------------------------------------------------// protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue); spriteBatch.Begin(); //Draw all sprites. for(int cnt = 0;cnt < sprites.Count;cnt++) { sprites[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-