| XNA Game-Themed CS1 Examples ( XGC1 ) | |
|
Release 2.0 (XNA V3.1)
|
|
References:
Goals:
1. Obtain the example code
When the game starts, you'll see a screen that
looks similar to this:
For this tutorial, we go back & redo the Pong game, this time using Object-Oriented Programming techniques. Changes from the previous tutorial include the addition of a MoveableBlock (which moves a short, vertical distance when the SoccerBall object collides with it), and putting all the logic that relates to the array of blocks into a separate class (named ArrayOfBlocks)
1. Block.cs, BreakableBlock.cs
These are effectively identical to what was presented in the prior tutorial. - we've changed some named constants to make things look nicer, but that's it. You should be able to understand all of these changes on your own.
2. Paddle.cs,
This is very similar to what was presented in the prior tutorial. Some of the named constants have been adjusted slightly in order to make everything look better, and the constructor now calls the new InitializePaddleState method in order to initialize (or reinitialize) each paddle object.
|
///
<summary>
///
Initializes the paddle's collision count
///
</summary>
public
void
InitializePaddleState()
{
m_HitCount = 0;
}
|
3. MovableBlock.cs,
As you can see from the code, this new class inherits from from the basic Block class. Given that we want to create a new type of block that closely resembles the existing type of block, with the exception of a new and more specialized behavior when the soccer ball collides with this new block, inheritance makes a lot of sense.
As you examine the code (and at this point, you should be able to confidently and correctly examine all of MovableBlock on your own), you'll notice that we declare the named constant MOVE_UNIT to define the default distance that a MovableBlock will be moved, when hit. We then copy that constant into an instance variable in the constructor, so that we can change the direction each time the block is hit by changing the sign on the number from positive to negative.
The PlayCollisionCue method is overridden so that hitting a MovableBlock will play a different sound, and the CollideWithSoccer method is overridden in order to adjust the MovableBlock's location when it gets hit. Note that the direction is reversed each time that the MovableBlock is hit, so that the block doesn't move off the screen
4. ArrayOfBlocks
In the previous tutorial, we used an array of Block (and BreakableBlock) objects In this tutorial, we will take all the functionality of that deals with the 'array of Blocks' and nicely encapsulated into a class, so as to make the code easier to understand (i.e., to simplify Game.cs) and promote reuse (in case we wanted to use this elsewhere). The ArrayOfBlocks' responsibilities include creating the array, creating a randomly chosen set of Blocks to put into the array, detecting collisions between the soccer ball and any block, and displaying some interesting information to the top status bar.
|
public
class
ArrayOfBlocks
{
#region
Constants for the
blocks
// in x, blocks will be equally
spaced begining from 20, and end at 80
private
const
float
BLOCK_BEGIN_X_POSITION = 20.0f;
private
const
float
BLOCK_X_SPAN = (80.0f - BLOCK_BEGIN_X_POSITION);
private
const
float
CHANCE_OF_BREAKABLE = 0.4f;
// chance a
block is breakable
private
const
float
CHANCE_OF_MOVABLE = 0.6f;
// chance a
block is movable
#endregion
|
public
class
ArrayOfBlocks
|
private
Block
[] m_Blocks =
null
;
// Array of blocks
private
int
m_TotalBlocks;
// total number of blocks
private
int
m_NumBreakable;
// number of breakable ones
private
int
m_NumMovable;
// number of movable ones
private
int
m_TotalHit;
// total number of collisions with
the blocks
|
private
Block
[] m_Blocks =
null
;
// Array of blocks
private
int
m_TotalBlocks;
// total number of blocks
private
int
m_NumBreakable;
// number of breakable ones
private
int
m_NumMovable;
// number of movable ones
private
int
m_TotalHit;
// total number of collisions with the
blocks
|
///
<summary>
///
Create
n blocks, will create breakable, movable, and regular blocks
///
</summary>
///
<param name="total">
Total
number of blocks to create.
</param>
public
ArrayOfBlocks(
int
total)
{
m_TotalBlocks = total;
InitializeBlocks();
}
///
<summary>
///
Allocate and initialize all the blocks. If blocks have been
previously initialize, will remove
///
all
blocks from the AutoDrawSet and re-create the blocks.
///
</summary>
public
void
InitializeBlocks()
{
if
(
null
!= m_Blocks)
{
// blocks already showing, must
remove them first
for
(
int
i = 0; i < m_Blocks.Length; i++)
m_Blocks[i].RemoveFromAutoDrawSet();
}
m_Blocks =
new
Block
[m_TotalBlocks];
m_NumBreakable = 0;
m_NumMovable = 0;
m_TotalHit = 0;
float
offset = BLOCK_X_SPAN /
m_Blocks.Length;
float
blockXPos =
BLOCK_BEGIN_X_POSITION;
for
(
int
i = 0; i< m_Blocks.Length; i++)
{
if
(
XNACS1Base
.RandomFloat()
< CHANCE_OF_BREAKABLE)
{
m_Blocks[i] =
new
BreakableBlock
(blockXPos);
m_NumBreakable++;
}
else
if
(
XNACS1Base
.RandomFloat()
< CHANCE_OF_MOVABLE)
{
m_Blocks[i] =
new
MovableBlock
(blockXPos);
m_NumMovable++;
}
else
{
m_Blocks[i] =
new
Block
(blockXPos);
}
blockXPos = blockXPos + offset;
}
}
|
if
(
null
!= m_Blocks)
{
// blocks already showing, must remove
them first
for
(
int
i = 0; i < m_Blocks.Length; i++)
m_Blocks[i].RemoveFromAutoDrawSet();
}
m_Blocks =
new
Block
[m_TotalBlocks];
m_NumBreakable = 0;
m_NumMovable = 0;
m_TotalHit = 0;
float
offset = BLOCK_X_SPAN /
m_Blocks.Length;
float
blockXPos =
BLOCK_BEGIN_X_POSITION;
for
(
int
i = 0; i< m_Blocks.Length; i++)
{
if
(
XNACS1Base
.RandomFloat()
< CHANCE_OF_BREAKABLE)
{
m_Blocks[i] =
new
BreakableBlock
(blockXPos);
m_NumBreakable++;
}
blockXPos = blockXPos + offset;
|
///
<summary>
///
Collide the soccer ball with each of the blocks in the array.
///
</summary>
///
<param name="theSoccer">
The
soccer ball to bounce with.
</param>
///
<returns>
true
if collided, otherwise returns false.
</returns>
public
bool
CollideWithSoccer(
SoccerBall
theSoccer)
{
bool
found =
false
;
// have not found the first block
that collide with the soccer
int
i = 0;
// loop iterator
while
( (!found) && (i<m_Blocks.Length)
) {
if
( (m_Blocks[i].CollideWithSoccer(theSoccer))
) {
found =
true
;
m_TotalHit++;
if
(!m_Blocks[i].BlockIsActive())
m_NumBreakable--;
}
else
i = i + 1;
}
return
found;
// found one collision
}
|
In a nutshell, this method is intended to find the first wall that the soccer ball has collided with (if any). It does this by going through the array, and stopping on the first block that collides with the soccer ball.
In order to both count through the array slots, and stop once we've
found a collision, we declare two variables. We'll initialize
found to false, to indicate that we have not yet found a collision, and
set it to true if and when we do find a collision. The integer
i
will be used as a counter:
bool
found =
false
;
// have not found the first block
that collide with the soccer
int
i = 0;
// loop iterator
The loop will continue while we have NOT found a collision, and while
i is still inside the bounds of the array:
while
( (!found) && (i<m_Blocks.Length)
) {
Within the loop, we'll check to see if the soccer ball has collided
with the current block. If it has, then we'll set found to true
(thus exiting the loop), increment the total number of times any block
has been hit, and if the block was a breakable block that has switched
from being active to being inactive, we'll decrement the m_NumBreakable
count by one. If the soccer ball hasn't collided with any blocks,
then we'll simply increment
i
by one, so that we'll
examine the next block on then next iteration:
while
( (!found) && (i<m_Blocks.Length)
) {
if
( (m_Blocks[i].CollideWithSoccer(theSoccer))
) {
found =
true
;
m_TotalHit++;
if
(!m_Blocks[i].BlockIsActive())
m_NumBreakable--;
}
else
i = i + 1;
}
At the end of the above, we'll return found, which will be true, if
we found a block that the soccer ball has collided with, and will be
false if the soccer ball didn't collide with anything.
return
found;
// found one collision
|
///
<summary>
///
///
</summary>
///
<returns>
Total
number of breakable blocks
</returns>
public
int
BreakbleBlocksLeft()
{
return
m_NumBreakable;
}
|
|
///
<summary>
///
Collect the state information of the current blocks and echo to
TopStatus
///
</summary>
public
void
ComputeAndEchoBlockStat()
{
int
minBounce = m_Blocks[0].TotalHits();
// initialization
int
maxBounce = m_Blocks[0].TotalHits();
int
numActive = 0;
float
avgBounce = 0.0f;
for
(
int
i = 0; i < m_Blocks.Length; i++)
{
// compute min
if
(m_Blocks[i].TotalHits() <
minBounce)
minBounce = m_Blocks[i].TotalHits();
// comptue max
if
(m_Blocks[i].TotalHits() >
maxBounce)
maxBounce = m_Blocks[i].TotalHits();
// accumate conditionally
if
(m_Blocks[i].BlockIsActive())
numActive++;
}
avgBounce = (
float
)m_TotalHit
/ (
float
)m_Blocks.Length;
XNACS1Base
.EchoToTopStatus(
"Block
Stat: Bounces:(total="
+ m_TotalHit +
" min="
+ minBounce +
" max="
+ maxBounce +
" avg="
+ avgBounce +
") "
+
"NumActive="
+ numActive);
}
|
ComputeAndEchoBlockStat is responsbible for collecting up some simple statistics, and displaying them into the top status bar.
We use a loop to figure out what is the smallest number of hits that any one block has, storing that result into minBounce .
We that same loop to figure out what is the largest number of hits that any one block has, storing that result into maxBounce .
We also use that same loop to figure out how many blocks are still active.
Having calculated the above (and since we've still got m_TotalHit that we're maintaining), we can calculate the average number of bounces (hits) per block. We then echo all of the above to the top status bar.
3. SoccerBall.cs,
In additon to handling the image, position, and velocity of the on-screen soccer ball, this class will also track whether the ball is in 'expert mode', and the total count of the number of misses. Each time the ball has hit the left/right edge of the screen counts as a 'miss' (i.e., the number of times it has been missed by the player). When the ball is in 'expert mode', the speed will be increased. Note that the ball only tracks which mode it's in - the Game1 object will actually decide when to go into expert mode.
|
#region
Constants for
soccer ball
// Ball size, position, velocity
and lossing conditions
private
const
float
BALL_INIT_X = 50.0f;
// initial ball X and Y positions
private
const
float
BALL_INIT_Y = 25.0f;
private
const
float
BALL_RADIUS = 1f;
private
const
float
BALL_VELOCITY_MIN_X = 0.5f;
private
const
float
BALL_VELOCITY_MAX_X = 1.1f;
private
const
float
BALL_VELOCITY_MIN_Y = BALL_VELOCITY_MIN_X / 3.0f;
private
const
float
BALL_VELOCITY_MAX_Y = BALL_VELOCITY_MAX_X / 3.0f;
private
const
float
BALL_VELOCITY_EXPERT_BOOST_X = 0.1f;
private
const
float
BALL_VELOCITY_EXPERT_BOOST_Y = BALL_VELOCITY_EXPERT_BOOST_X / 3.0f;
#endregion
private
bool
m_ExpertBall;
// if the soccer ball is in expert
mode
private int m_BallsMissed; // how many times did the soccer ball fall of the side boundaries.
|
private
bool
m_ExpertBall;
// if the soccer ball is in expert mode
private int m_BallsMissed; // how many times did the soccer ball fall of the side boundaries.
|
///
<summary>
///
Constructor of the soccer ball, with default position and velocity.
///
</summary>
public
SoccerBall()
{
// instance variable in the class
m_ExpertBall =
false
;
m_BallsMissed = 0;
ShouldTravel =
true
;
InitializeBall();
}
///
<summary>
///
Private utility class to initialze the ball position and velocity
///
will check and set the ball to an "expert" ball if in the proper
mode.
///
</summary>
public
void
InitializeBall()
{
// These are accessors from the
XNACS1Circle super class.
Center =
new
Vector2
(BALL_INIT_X,
BALL_INIT_Y);
Radius = BALL_RADIUS;
Texture =
"SoccerBall"
;
Velocity =
new
Vector2
();
VelocityX =
XNACS1Base
.RandomFloat(BALL_VELOCITY_MIN_X,
BALL_VELOCITY_MAX_X);
VelocityY =
XNACS1Base
.RandomFloat(BALL_VELOCITY_MIN_Y,
BALL_VELOCITY_MAX_Y);
if
(m_ExpertBall)
SetToExpertMode();
}
///
<summary>
///
Sets the ball to expert mode by increasing its velocity
///
</summary>
public
void
SetToExpertMode()
{
m_ExpertBall =
true
;
VelocityX +=
Math
.Sign(VelocityX)
* BALL_VELOCITY_EXPERT_BOOST_X;
VelocityY +=
Math
.Sign(VelocityY)
* BALL_VELOCITY_EXPERT_BOOST_Y;
}
|
|
///
<summary>
///
Updates the ball by colliding it with the world boundaries. Will
update the ball miss number if the
///
ball
goes out of bound (on either sides of the play field).
///
</summary>
public
void
UpdateBall()
{
XNACS1Lib.
BoundCollideStatus
status =
XNACS1Base
.
World
.CollideWorldBound(
this
);
switch
(status)
{
case
BoundCollideStatus
.CollideTop:
case
BoundCollideStatus
.CollideBottom:
VelocityY = -VelocityY;
break
;
case
BoundCollideStatus
.CollideRight:
case
BoundCollideStatus
.CollideLeft:
// Fall off from either side, must
reinitialize
InitializeBall();
m_BallsMissed++;
XNACS1Base
.PlayACue(
"BallDie"
);
break
;
case
BoundCollideStatus
.InsideBound:
// REMOVE THIS??
break
;
}
}
|
case
BoundCollideStatus
.CollideRight:
case
BoundCollideStatus
.CollideLeft:
// Fall off from either side, must
reinitialize
InitializeBall();
m_BallsMissed++;
XNACS1Base
.PlayACue(
"BallDie"
);
break
;
2. Game.cs
This object keeps track of the game overall (using the SoccerBall, the two Paddles, and the ArrayOfBlocks), and it's responsible for knowing when to advance to expert mode. Instead of examining the entire file here, we'll walk through the UpdateWorld method, and leave the rest for you to examine in more detail.
|
protected
override
void
UpdateWorld()
{
if
(GamePad.ButtonBackClicked())
this
.Exit();
if
(GamePad.ButtonStartClicked())
{
if
(m_GameState ==
GameState
.Paused)
m_GameState =
GameState
.Playing;
else
m_GameState =
GameState
.Paused;
}
if
(GamePad.ButtonAClicked())
{
m_LeftPaddle.InitializePaddleState();
m_RightPaddle.InitializePaddleState();
m_TheSoccer.InitializeBall();
m_Blocks.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_GameState !=
GameState
.Playing)
{
m_TheSoccer.ShouldTravel =
false
;
return
;
}
else
{
m_TheSoccer.ShouldTravel =
true
;
}
#region
move paddles and update the soccer
// update the left and right
paddles with thumbstick
m_LeftPaddle.UpdatePaddlePosition(GamePad.ThumbSticks.Left.Y);
m_RightPaddle.UpdatePaddlePosition(GamePad.ThumbSticks.Right.Y);
m_TheSoccer.UpdateBall();
#endregion
// Check for collision with the
blocks
m_Blocks.CollideWithSoccer(m_TheSoccer);
m_Blocks.ComputeAndEchoBlockStat();
// Check for collision with the
paddles
m_LeftPaddle.CollideWithSoccer(m_TheSoccer);
m_RightPaddle.CollideWithSoccer(m_TheSoccer);
m_NumBounces = m_LeftPaddle.TotalHits() +
m_RightPaddle.TotalHits();
// If we did collide with a paddle,
and we have a new
// max bounces, update that here
if
(m_NumBounces >
m_MaxBounces)
m_MaxBounces = m_NumBounces;
// Should we move from Notice to
Expert mode?
if
(m_MaxBounces >
EXPERT_LEVEL_BOUNCES)
{
// String equality test
if
(m_SkillLevel ==
"Novice"
)
{
World
.SetBackgroundTexture(
"ExpertImage"
);
PlayACue(
"Victory"
);
m_TheSoccer.SetToExpertMode();
}
m_SkillLevel =
"Expert"
;
}
EchoToBottomStatus(
"["
+
m_SkillLevel +
"] "
+
"CurrentBounces:"
+
m_NumBounces +
" MaxBounces:"
+
m_MaxBounces +
" Total Balls Missed:"
+
m_TheSoccer.BallsMissed());
// Check to see if the player has
won or lost
CheckForEndState();
}
|
Following that, we ask the ArrayOfBlocks object is any of the blocks
are overlapping the SoccerBall. Once that's done, we ask it to
display the interesting statistics at the top of the screen:
// Check for collision with the blocks
m_Blocks.CollideWithSoccer(m_TheSoccer);
m_Blocks.ComputeAndEchoBlockStat();
Next we figure out if the soccer ball collided with either of the paddles, followed by calculating the maximum number of bounces off both paddles since the last time a ball was missed. If that maximum is greater than EXPERT_LEVEL_BOUNCES , we then advance into 'expert mode'
Finally, we check if the game is over. If it is, then we'll
stop updating the screen on the next update (because
if
(m_GameState !=
GameState
.Playing)
FURTHER EXERCISES::