The MultipleGameObjects example
project shows a simple example of using the game framework to display a
number of moving sprites on the screen with minimal code required in
the derived Game class.
The project creates a number of colored box objects
that move toward the bottom of the screen while rotating, a number of
colored ball objects that bounce around the screen (and squash against
the edges when they hit), and a text object that displays a message in
the middle of the screen. An image of the project can be seen in Figure 1.
Let's take a look at how the project has been put together.
1. Referencing the GameFramework Project
Before the game framework classes can be accessed, we need to add a reference to the GameFramework project from the main game project. Assuming that the GameFramework
project is already part of the current Solution, this is simply a
matter of right-clicking the main game project node in Solution
Explorer, selecting Add Reference, and then picking the GameFramework project from the Projects tab of the Add Reference dialog, as shown in Figure 2.
If the GameFramework project is not a part of your solution, you can either add it or add a reference to GameFramework.dll by using the Browse tab in the Add Reference window.
2. Setting Inheritance for the Main Game Class
We already know that a Windows Phone XNA game project must contain exactly one class that derives from Microsoft.Xna.Framework.Game, and by default Visual Studio creates new XNA projects with one such class present.
In order to use the game framework, we must change the inheritance of this class so that it instead derives from GameFramework.GameHost, as shown in Listing 1.
Example 1. The declaration of a game class using the game framework
/// <summary> /// This is the main type for your game /// </summary> public class MultipleObjectsGame : GameFramework.GameHost {
|
3. Creating Derived SpriteObject Classes
Now that we are set up to use the 17.200 game framework, we can start writing code to use it. In the MultipleGameObjects example, there are two classes that derive from the game framework's SpriteObject: BoxObject and BallObject. Each of these contains all the logic required for that object to update and draw itself.
3.1. The BoxObject Class
BoxObject is the more straightforward of the two derived object classes. Its constructor is shown in Listing 2.
Example 2. The constructor for the BoxObject class
internal BoxObject(MultipleObjectsGame game, Texture2D texture) : base(game, Vector2.Zero, texture) { // Store a strongly-typed reference to the game _game = game;
// Set a random position PositionX = GameHelper.RandomNext(0, _game.Window.ClientBounds.Width); PositionY = GameHelper.RandomNext(0, _game.Window.ClientBounds.Height);
// Set the origin Origin = new Vector2(texture.Width, texture.Height) / 2;
// Set a random color SpriteColor = new Color(GameHelper.RandomNext(0, 256), GameHelper.RandomNext(0, 256), GameHelper.RandomNext(0, 256));
// Set a random movement speed for the box
_moveSpeed = GameHelper.RandomNext(2.0f) + 2;
// Set a random rotation speed for the box _rotateSpeed = GameHelper.RandomNext(-5.0f, 5.0f); }
|
It accepts parameters that are relevant for the box, namely a reference to the MultipleGameObjects
game class and the texture to display in the box. After storing the
game class reference, the constructor randomizes the object ready for
display on the screen.
This process consists of setting random values for the PositionX, PositionY, and SpriteColor properties (all of which are provided by the SpriteObject base class). It also sets the Origin property to be at the center of the sprite, calculated by halving the width and height of the provided texture.
Once the base class properties have been set, the
constructor code also generates random values for its own
class-specific variables. These values are _moveSpeed, a variable that controls how fast the box moves, and _rotateSpeed, which controls how fast it rotates. These values will be used each time the sprite updates and will be added to the PositionY and Angle properties, respectively. The GameHelper class is used to generate random float
values for both of these. Note that the rotation speed can be either
positive or negative, which means that the box can rotate either
clockwise or counterclockwise.
Along with the functionality inherited from the SpriteObject
class, this is already sufficient to display the colored box in a
random position, but it won't yet move in any way. Movement is
implemented by overriding the Update method, as shown in Listing 3.
Example 3. The Update method for the BoxObject class
public override void Update(GameTime gameTime) { // Allow the base class to do any work it needs base.Update(gameTime);
// Update the position of the box PositionY += _moveSpeed; // If we pass the bottom of the window, reset back to the top if (BoundingBox.Top > _game.Window.ClientBounds.Bottom) { PositionY = -SpriteTexture.Height; }
// Rotate the box Angle += MathHelper.ToRadians(_rotateSpeed); }
|
This is all very simple: the movement speed is first added to the PositionY
property to update the sprite position. The position is then checked to
see whether the sprite has passed the bottom of the screen. This is the
case if its BoundingBox.Top is below the bottom of the
window, at which point the sprite's position is reset back to the top.
It is set so that its position is actually above the top of the screen
so that it doesn't "pop" into view.
The Angle property is then updated according to the rotation speed. Note that we use the MathHelper.ToRadians function when rotating, which means that the _rotateSpeed
variable is storing degrees rather than radians. It doesn't really
matter which unit you use, but you should stick to the same unit
throughout all of your game to avoid confusion.
Note that there is no need for any other code within
the class, including any to perform drawing, because this is all
provided by the SpriteObject base class. All the class needs to concern itself with is the simple task of moving itself.
3.2. The BallObject Class
The BallObject implementation is slightly
more complex, but only slightly. Instead of simply moving down the
screen, the balls need to bounce off the screen edges, obey gravity (or
an approximation thereof), and "wobble" when they hit the edges of the
screen, as if they were made of rubber.
To make the ball move in this way, we need to store
the direction in which it is moving (its velocity). In order to know an
object's velocity, we need to know both the direction in which it is
moving and the speed with which it is traveling. There are two methods
in which we can describe an object's velocity: either by storing its
direction (as an angle from 0 to 360 degrees) and its speed of movement
in that direction, or by storing its speed as a value along each of the
axes: x and y.
The first of these methods provides for a simulation
of movement that is closer to how things move in reality, and, in many
scenarios, this will be an appropriate system to use. For the sake of
simplicity in our example, however, we will use the second method. This
simply tracks the x and y distance that we want to move the object each
time it updates. To bounce an object off the side of the screen, we can
simply negate the speed in that particular axis: if we are adding a
value of 5 to the x position and we hit the right side of the screen,
we then start subtracting 5 to move to the left again. Subtracting 5 is
the same as adding −5, so changing the x speed from 5 to −5 will
reverse the direction on that axis. Exactly the same function applies
to the y axis. The calculation is illustrated in Figure 3.
Just as with the box class, we implement all this
inside the derived game object. We use several class-level variables to
support this:
_xadd and _yadd keep track of the ball's velocity in each of the two axes of movement.
_wobble tracks the amount that the ball is wobbling (where zero is not at all, and greater values indicate higher wobble levels).
The _xadd variable is set in the constructor along with some of the inherited sprite properties, as shown in Listing 4. The _yadd and _wobble variables are left with their default values of 0; they will be modified in the Update method, as we will see in a moment.
Example 4. The constructor for the BallObject class
internal BallObject(MultipleObjectsGame game, Texture2D texture) : base(game, Vector2.Zero, texture) { // Store a strongly-typed reference to the game _game = game;
// Set a random position PositionX = GameHelper.RandomNext(0, _game.Window.ClientBounds.Width); PositionY = GameHelper.RandomNext(0, _game.Window.ClientBounds.Height);
// Set the origin Origin = new Vector2(texture.Width, texture.Height) / 2;
// Set a random color SpriteColor = new Color(GameHelper.RandomNext(0, 256), GameHelper.RandomNext(0, 256), GameHelper.RandomNext(0, 256));
// Set a horizontal movement speed for the box _xadd = GameHelper.RandomNext(-5.0f, 5.0f); }
|
As with the box, the rest of the class code is contained within the Update
method. To make the ball move, we first add the velocity in each axis
to the corresponding position, detecting collisions with the left,
right, and bottom edges of the screen as described at the start of this
section. The beginning of the Update method, which handles these changes, is shown in Listing 5.
Example 5. Part of the ball's Update method: moving the ball and bouncing from the screen edges
public override void Update(GameTime gameTime) { // Allow the base class to do any work it needs base.Update(gameTime);
// Update the position of the ball PositionX += _xadd; PositionY += _yadd;
// If we reach the side of the window, reverse the x velocity so that the ball // bounces back if (PositionX < OriginX) { // Reset back to the left edge PositionX = OriginX; // Reverse the x velocity _xadd = -_xadd; // Add to the wobble _wobble += Math.Abs(_xadd); } if (PositionX > _game.Window.ClientBounds.Width - OriginX) { // Reset back to the right edge
PositionX = _game.Window.ClientBounds.Width - OriginX; // Reverse the x velocity _xadd = -_xadd; // Add to the wobble _wobble += Math.Abs(_xadd); }
// If we reach the bottom of the window, reverse the y velocity so that the ball // bounces upwards if (PositionY >= _game.Window.ClientBounds.Bottom - OriginY) { // Reset back to the bottom of the window PositionY = _game.Window.ClientBounds.Bottom - OriginY; // Reverse the y-velocity _yadd = -_yadd; // +0.3f; // Add to the wobble _wobble += Math.Abs(_yadd); } else { // Increase the y velocity to simulate gravity _yadd += 0.3f; }
|
The ball's position is checked against the screen
edges by taking the origin values into account because the origin is
set to be at the center of the ball. If the OriginXPositionX value falls below 20. The same applies to the right edge, where its OriginX value is subtracted from the window width to determine the maximum permitted PositionX value; and to the bottom edge, where its OriginY is subtracted from the window's Bottom value is 20, the ball has hit the left edge of the window if the position.
If the ball hasn't hit the bottom edge, we add a small amount to the _yadd variable, as seen in the final line of code in Listing 5.
This is the all-important update that simulates gravity. For every
update, the ball moves downward a little more than it did the previous
update. If the ball is moving upward, this erodes its upward momentum
until it eventually starts to fall downward again. Changing the amount
that is added to _yadd each update will increase or decrease the intensity of the simulated gravity.
The remainder of the update code deals with the ball's wobble. As you can also see in Listing 5, the _wobble
variable is increased each time the ball bounces by adding the
corresponding velocity. This means that the harder the ball hits into
the window edge, the more it wobbles. The Math.Abs function is used to ensure that the wobble value increases even if the velocity contains a negative value.
The wobble is implemented by updating the ball's
scale properties. We use a sine wave to make the ball oscillate between
being thinner and fatter than normal, and offset it so that when it is
at its thinnest on the x axis it is also at its fattest on the y axis,
and vice versa. This provides a pleasing rubbery visual effect.
Listing 6 shows the rest of the Update
function, containing the wobble code. If the wobble level is zero, it
ensures that the scaling is reset by assigning a unit vector to the Scale property. Otherwise, it uses the UpdateCount property from GameObjectBase as a mechanism for moving through the sine values. The WobbleSpeed constant controls how rapidly the ball oscillates, and WobbleIntensity controls how much it squashes the ball. Try altering these values and see the effect that it has on the balls.
Finally, the wobble level is gradually decreased
with each update and checked to stop it falling below 0 or getting too
high (at which point it looks silly).
Example 6. The remaining Update method code: making the ball wobble
// Is there any wobble? if (_wobble == 0) { // No, so reset the scale Scale = Vector2.One; } else { const float WobbleSpeed = 20.0f; const float WobbleIntensity = 0.015f;
// Yes, so calculate the scaling on the x and y axes ScaleX = (float)Math.Sin(MathHelper.ToRadians(UpdateCount * WobbleSpeed)) * _wobble * WobbleIntensity + 1; ScaleY = (float)Math.Sin(MathHelper.ToRadians(UpdateCount * WobbleSpeed + 180.0f)) * _wobble * WobbleIntensity + 1; // Reduce the wobble level _wobble -= 0.2f; // Don't allow the wobble to fall below zero or to rise too high if (_wobble < 0) _wobble = 0; if (_wobble > 50) _wobble = 50; } }
|