Download and unzip the zip file and you will see an ExampleProgram folder. Open the ExampleProgram
folder, the EXE folder contains the compiled program and you can double click on the .sln
file to work with the source code.
When the game starts, you'll
see a screen that looks similar to this:
This program is built off the Soccer Pong game that you
have seen previously. The new features for this tutorial are:
The player can pause the game by pressing the 'Start' button (which is
mapped to the W key, on the keyboard)
The player can start the game over by pressing the 'A' button.
InitializeBlocks: new function to create and initialize the blocks.
The stats about the game are being echoed to the top status bar, using the
functions we saw in the previous tutorial
The declarations of several of the named constant have been changed, so
that it will be easier to add new blocks to the game
Let's examine these new features, one by one:
2. Examining The Program: Pausing the Game
When the player presses the 'Start' button ('W' on the keyboard), the game
will pause. What this means that during each call to UpdateWorld, no work
will be done. The soccer ball will not be moved, the paddles will not be
moved - nothing. The only thing that will be done is to check and see if
the player wants to exit the game, or if the player has decided to unpause the
game.
Declaring the instance variables and named constants, and initialize
them
private int
m_MaxBounces;
private bool
m_YouHaveWon;
private
bool
m_PausedGame;
#endregion
We will need to use an instance variable to keep track of whether the
game is currently paused or not. It's a bool because the game is
either paused (represented by the value
true), or it's not
paused (represented by the value
false).
We make sure to assign the value
false to the
variable in the InitializeStats() function, in order to be very clear that
the game starts off unpaused.
Technically, C# will initialize all
bool instance
variables to have the value
false, but it's
good to be clear.
UpdateWorld():
protected override
void
UpdateWorld()
{
if (GamePad.ButtonBackClicked())
this.Exit();
#region
handling of Paused Game
if
(GamePad.ButtonStartClicked())
m_PausedGame = !m_PausedGame;
if
(m_PausedGame)
return;
//
paused
#endregion
if
(GamePad.ButtonAClicked())
{
InitializeSoccer();
InitializeBlocks();
InitializeStats();
}
UpdateWorld starts off by checking to see if the player wants to exit
the game, like normal. Because this is the very first thing in UpdateWorld,
we will do this check before we do anything relating to pausing
(or unpausing) the game.
Next, if the player is pressing the Start button, we will 'flip' the
value of m_PausedGame.
if
(GamePad.ButtonStartClicked())
m_PausedGame = !m_PausedGame;
If m_PausedGame
is false (meaning that the game is not currently paused), and the
player has pressed the Start button, this line will take the current value
of m_PausedGame
(false), switch it to true (because of the
!) , then assign it back to
m_PausedGame, thus
pausing the game.
If m_PausedGame
is true (meaning that the game is currently paused), and the player
has pressed the Start button, this line will take the current value of
m_PausedGame
(true), switch it to false(because of the
!) , then assign it back to
m_PausedGame, thus unpausing the game.
3. Examining The Program: InitializeBlocks and restarting the game.
Let's examine the C# source code that produces the restarting behavior we
see on-screen:
UpdateWorld():
if (m_PausedGame)
return; //
paused
#endregion
if (GamePad.ButtonAClicked())
{
InitializeSoccer();
InitializeBlocks();
InitializeStats();
}
//
The player has won, then don't do anything else
//
Don't let the player move the paddles,
//
Don't move the ball, etc
if
(m_YouHaveWon)
{
return;
}
When the player presses the 'A' button, the game will restart. In
order to make the game restart, we'll just call all the 'initialization'
functions again, in order to re-initialize the soccer ball, blocks, and
game. The InitializeStats()
and
InitializeSoccer()
functions work the exact same way in both cases, but the
InitializeBlocks() function behaves
slightly differently, depending on whether they're being used to initialize
the game the very first time, or whether they're being used to restart a
game later on.
InitializeBlocks():
///
<summary>
///
Allocate the memory and initialize all the arrays that are
///
associated with the blocks:
///
///
m_Blocks -- the blocks
///
m_BlocksHits -- the number of hits for each corresponding block
///
m_BlocksActive -- if the block is still active
///
</summary>
private
void
InitializeBlocks()
{
if (null
!= m_Blocks)
// already initialized: already showing
Pretty much everything here should be familiar, except for the first if
statement:
if (null
!= m_Blocks)
// already initialized: already showing
{
// blocks
already showing, must remove them first
for
(int
i = 0; i < m_Blocks.Length; i++)
m_Blocks[i].RemoveFromDrawSet();
}
What's happening is this: when InitializeBlocks is called the first time,
we just want to create the two arrays (m_Blocks, and m_BlockHits), and then
initialize those arrays. However, when we create the individual
XNACS1Rectangle objects, they get added to a collection of things that will
be automatically drawn on the screen for us. Automatically adding new
rectangles and circles to this collection (which is called 'the drawing set') is
really handy, and exactly what we want to have happen the first time that we
create the objects. However, unless we tell the XNACS1Lib otherwise,
they will keep getting drawn on the screen. Even if we create
brand-new rectangles that we want the game to draw instead of the rectangles
from the last game.
Furthermore, we only want to remove the blocks from the drawing set.
Everything else we want to leave on the screen.
So if InitializeWorld is being used to restart the game, then we first
need to remove the current rectangles, before creating any new ones.
We can detect if this is the very first time InitializeWorld is being called
using the following line:
if (null
!= m_Blocks)
C# initializes m_Blocks to have the special value
null, meaning that
when m_Blocks is created, it doesn't refer to an array (yet). Once we
execute the line "m_Blocks =
new
XNACS1Rectangle[NUM_BLOCKS];",
then m_Blocks will refer to an array, and therefore have a non-null
value. So if m_Blocks does not equal
null, it must be
because because we've called InitializeWorld already (wherein m_Blocks was
assigned an array to refer to).
If the above if statement is true, then we use the following code to
remove the current blocks from the set of things that XNACS1 should
automatically draw:
// blocks already
showing, must remove them first
for
(int
i = 0; i < m_Blocks.Length; i++)
m_Blocks[i].RemoveFromDrawSet();
4. Examining The Program: Collecting Statistics And Displaying Them
Let's examine the C# source code that produces the restarting behavior we
see on-screen:
ComputeAndEchoBlockStat():
///
<summary>
///
Collect the state information of the current blocks and echo to
TopStatus
///
</summary>
private
void
ComputeAndEchoBlockStat()
{
int totalBounces =
SumOfArray(m_BlockHits);
float avgBounce =
AverageOfArray(m_BlockHits);
int
minBounce = MinInArray(m_BlockHits);
int
maxBounce = MaxInArray(m_BlockHits);
EchoToTopStatus(
"Block
Stat: Bounces:(total="
+ totalBounces +
"
min=" + minBounce +
"
max=" + maxBounce +
" avg=" +
avgBounce + ") ");
}
As you can see, the above code is almost identical to the code from the
previous tutorial. The various 'array processing' functions (SumOfArray,
etc) are all identical to the ones presented in the previous tutorial.
While it's not strictly necessary to have the 'array processing'
functions be given the array to operate on as a parameter, you'll notice
that they are extremely easy to reuse (from the previous tutorial), in part
because of the use of parameters. So instead of re-writing a slightly
different version, we will just re-use the version that we wrote in the
previous tutorial.
5. Examining The Program: Making It Easier To Add Bricks
Let's examine the C# source code that produces the restarting behavior we
see on-screen:
You'll notice that we've changed the number of blocks that we want to
create:
private const
int NUM_BLOCKS = 15;
You'll also notice that we've changed how we declare the space between
the blocks (along the X axis), so that BLOCK_X_OFFSET now depends on the
value of NUM_BLOCKS:
private const
float BLOCK_X_OFFSET =
(100.0f-BLOCK_A_X_POSITION)/NUM_BLOCKS;
What this does is decide that the right-most X value will be 100.0f,
then subtract BLOCK_A_X_POSITION
from it in order to get the distance between the left-most block's X
position (BLOCK_A_X_POSITION), and
the right boundary of 100.0f. It then divides this distance equally,
based on the number of blocks in the game.
The advantage to this is that if we want to change the number of blocks
on the screen, all we need to do is change NUM_BLOCKS, recompile, and re-run
our program. The blocks will be evenly space across the screen.
FURTHER EXERCISES:
Start from a blank starter project (1000.201, if you need it), and re-do
the code from memory as much as possible. On your first try, do what
you can, and keep the above code open so that when you get stuck, you can
quickly look up what you forgot (and that after you finish a line, so that
you can compare your line to the 'correct' line). On the next try, do
the same thing, but try to use the finished code less. Repeat this
until you can type everything, without referring to the tutorial's code.
Repeat this exercise daily for several days, so that you really get the
hang of this. As you go on, periodically review this by re-doing this
exercise.
Examining The InitializeBlocks()
Function
For this exercise, you should use the same project that was explained in the
above tutorial.
What happens if you remove the the line that calls RemoveFromDrawSet on the
current blocks? Which blocks does the soccer ball collide with?
Why?