You take a look at the code and it turns out that she is right. The following statements create a BaseSprite reference called b and then try to draw it:
BaseSprite b = new BaseSprite();
b.Draw(spriteBatch);
These statements cause an exception to be thrown because the Draw method would try to use values of spriteRectangle and spriteTexture in the class that haven’t been set up yet. What you want is a way of making sure that whenever a BaseSprite is created, it must be given a texture and rectangle. It turns out that this is very easy to do—you just need to add a constructor to the BaseSprite
class. This is code that gets control when your object is being
created, and can be used to set it up. Your constructor method, shown in
bold in here, has the same name as the class and accepts two
parameters:
public class BaseSprite
{
protected Texture2D spriteTexture;
protected Rectangle spriteRectangle;
public void LoadTexture(Texture2D inSpriteTexture)
{
spriteTexture = inSpriteTexture;
}
public void SetRectangle(Rectangle inSpriteRectangle)
{
spriteRectangle = inSpriteRectangle;
}
public virtual void Draw(SpriteBatch spriteBatch)
{
spriteBatch.Draw(spriteTexture, spriteRectangle, Color.White);
}
public virtual void Update(BreadAndCheeseGame game)
{
}
public BaseSprite(Texture2D inSpriteTexture, Rectangle inRectangle)
{
LoadTexture(inSpriteTexture);
SetRectangle(inRectangle);
}
}
This constructor for BaseSprite
is given the texture to draw and the rectangle to be used to draw it.
The constructor then calls the methods in the class to set these values.
This means that now the only way that you can create a BaseSprite is by supplying a texture and a rectangle when you use the new key word to create a BaseSprite instance:
Texture2D background = Content.Load<Texture2D>("Images/Background");
Rectangle position = new Rectangle ( 0,0, 500,500 );
BaseSprite b = new BaseSprite(background, position);
Any BaseSprite
instance referred to now always has a texture and a rectangle, which
means that it can be drawn without problems. You have been using new in this way ever since your first program. Even this code uses new in the constructor call to set up the Rectangle being used to make the BaseSprite.
You can provide as many
constructors as you like for a class, so that if there are different
ways of providing the initial values, you can provide a constructor for
each. You have already seen this in action, too: the Color type provides lots of different constructors so that you can make a new color value in many different ways.
1. Constructors in Structures
There is a
subtle difference in the way that constructors are applied to value
types. If you create a constructor for a value type, it must set a value
for every data member of the structure:
struct demo
{
int i;
int j;
int k;
public demo(int newi, int newj, int newk)
{
i = newi;
j = newj;
k = newk;
}
}
The structure called demo
(which is a value type) contains three data members. If you create a
constructor for it, the compiler insists that the constructor must
accept some parameters and must explicitly set all three members of the
structure. This is not the same as for types managed by reference, where
the compiler is much more relaxed about what has been initialized and
automatically sets member data to default values (0 for numbers and null for references).
2. Constructors in Class Hierarchies
You
haven’t had to create constructors before because the compiler has
provided an "empty" constructor (that is, one that accepts no
parameters) automatically for each object you have created. However,
once you add your own constructor, the compiler stops doing this. The
designers of C# worked on the basis that if you provide a constructor
you are indicating that you want complete control over how classes are
created. This can lead to problems, as you now discover.
Armed with your knowledge of how constructors work, you now decide to sort out all the classes in the BreadAndSprite game. This does not go well. As soon as you add a proper constructor to the BaseSprite class to improve the program, it actually breaks everything. Figure 1 shows the errors that are produced by XNA Game Studio from this "improvement."
The compiler is not very happy with the BaseSprite
class. It seems to want back the empty constructor, the "constructor
that takes 0 arguments." The compiler is trying to tell you that some
parts of your program are trying to use the empty constructor to create a
BaseSprite class. This no longer exists because you have provided your own constructor. You could start by fixing the Background sprite, which is a BaseSprite instance that draws the background. When it is created, the game must provide the texture and rectangle for this sprite:
Background = new BaseSprite(
Content.Load<Texture2D>("Images/Background"),
new Rectangle(0, 0, displayWidth, displayHeight));
The Background is now created in the LoadContent
method because this is the point at which the texture is loaded. This
gets rid of one of the errors, but there are still quite a few left.
2.1. Constructors in Child Classes
The next class that you could fix is the TitleSprite class, which is a child of the BaseSprite class. From what we know of class hierarchies, this means that when a TitleSprite instance is created, the system must create a BaseSprite first. If a parent class contains a constructor (as ours now does), this means that the child constructor must call the parent
constructor to ensure that the parent class is set up properly before
the child is constructed. The C# language provides a means of doing this
very easily, as shown here in bold:
public class TitleSprite : BaseSprite
{
// TitleSprite contents
// TitleSprite constructor:
public TitleSprite(Texture2D inSpriteTexture, Rectangle inRectangle)
: base (inSpriteTexture, inRectangle)
{
// The constructor doesn't actually have to do anything
}
}
The constructor for TitleSprite
actually just needs to call the constructor for the base class. The
preceding code shows how this is done. The parameters to the TitleSprite call are passed into a call of a method called base.
We have seen this before when we called parent methods from overridden
ones. In this context, it is doing something very similar, calling the
constructor of the parent class (sometimes called the base class).
The rather strange syntax, with the call actually appearing outside the
body of the constructor method, is designed to make it clear that the
constructor for the parent must run before the code in the child
constructor runs.
To make the program compile all the children of the BaseSprite
class must include a call of the base constructor like this. This calls
for some changes to the code, but it is worth the effort as we shall
see in a moment.
The construction of objects in your system is something that you should plan carefully when you design your program.