Chapter 3(3):The Breakout Game

806人阅读 评论(0) 收藏 举报

The Breakout Game

Alright, this chapter talked a lot about helper classes and it is finally time to put them to some use. I will skip the concept phase here and basically just say that Breakout is an abbreviation of Pong for just one player to play against a wall of blocks. Breakout was initially created by Nolan Bushnell and Steve Wozniak and released by Atari in 1976. In this early version it was just a black and white game like Pong, but to make it more “exciting” transparent stripes were placed over the monitor to color the blocks (see Figure 3-13).

好了,这一章讲了这么多关于辅助类的东西,现在是时候将其赋予使用了。现在我会掠过游戏的概念阶段,因为这个游戏只是一个Pong的简化版,一个单人游戏,对抗一个由砖块组成的墙。Breakout最先是由Nolan Bushnell发明的,由Atari1976年发布。在早期的版本中这就像Pong一样是黑白游戏,但是为了让它“更令人激动”,使用了彩色。

Figure 3-13

You will actually go a similar road by reusing some of the Pong components and using the helper classes you learned about in this chapter. Breakout is a more complex game than Pong; it can have many levels and can be improved quite a lot. For example, Arkanoid is a clone of Breakout and there were many games in the 1980s and 1990s that still used this basic game idea and added weapons, better graphic effects, and many levels with different block placements.


As you can see in Figure 3-14, the BreakoutGame class is structured in a similar way to the Pong class from the last chapter. The sprite handling is missing because it is done now with help of the SpriteHelper class. Some other internal methods and calls are also replaced by some of the helper classes. For example, StartLevel generates a random new level based on the level value, and to generate the random values you will use the RandomHelper class.


Figure 3-14

Please also note that many test methods are visible in the class. This will be improved similar to the helper classes in the next chapter, which introduces the BaseGame and TestGame classes that make handling the game class and especially unit testing a lot easier and more organized.


Take a look at Figure 3-15 for a quick overview of the Breakout game you are going to develop in the next few pages. It is quite a lot of fun and certainly has a greater replay value than Pong, which is only fun with two human players anyway. The Breakout game uses the same background texture and the two sound files from the Pong project, but you also add a new texture (BreakoutGame.png) for the paddle, ball, and blocks and you have new sounds for winning a level (BreakoutVictory.wav) and for destroying blocks (BreakoutBlockKill.wav).


Figure 3-15

Unit Testing in Breakout

Before you start copying and pasting code over from the last project, using your new helper classes, and drawing the new game elements, you should think about the game and the problems you might run into. Sure you can just go ahead and implement the game, but it will be much harder to test the collisions, for example, which are the hardest part of this game. The unit tests help you out and provide you with an easy way to at least check all the basic parts of your game, and they also help you to organize your code and force you to program only what is really required. As always start with the most obvious part of the game and unit test it, then add more unit tests until you are done, and finally put everything together and test the final game.


Here is a quick overview of the unit tests for the Breakout game; check the full source code for this chapter for more details. You don’t have the TestGame class yet, so you still use the same kind of unit testing you used in the last chapter. Check out the next chapter on a better way to do static unit tests. You only have three unit tests, but they were used and changed a lot as I implemented the game.


§  TestSounds - Just a quick test to check out all the new sounds for your project. Press space, Alt, Control, and Shift to play the sounds. I also added a little pause after playing the next sound to make it a little easier to hear the sounds. This test was used to check out the new XACT project I created for this game.


§  TestGameSprites - This test was initially used to test the SpriteHelper class, but then all the code was moved to the Draw method of the game. The test was also used to initialize all the blocks in the game; the code was moved to the constructor, which is shown at the end of this chapter. This test shows you that it is not important to have a complicated test at the end because it is now only four lines of code, but the important part is to make your life easier while you are coding the game. Copy and paste useful parts of unit tests to your code as often as you need. Static unit tests also don’t have to be intact like dynamic unit tests for the helper classes because you only use them to build and test your game. When the game works you don’t need the static unit tests anymore except for testing parts of the game at a later point in time.


§  TestBallCollisions - Like in the previous chapter testing the ball collisions is the most useful unit test. Here you check if the collisions happen with the screen borders and paddle as expected. Only minor changes were required to get this to work. Then you can go to the more complicated block collision code, which is explained in more detail a bit later. You might even be able to think of more ways to test the collision and improve the game if you like. For example, it would make sense to trap the ball behind the wall of blocks and see if it destroys all the blocks correctly.


Breakout Levels

Because you are using many of the existing Pong ideas, you can skip the code that is similar or identical. You should focus on the new variables for now:


/// <summary>
/// How many block columns and rows are displayed?
/// 要显示多少行和多少列的砖块
/// </summary>

const int NumOfColumns = 14,
= 12;
/// <summary>
/// Current paddle positions, 0 means left, 1 means right.
/// 当前挡板位置,0代表左,1代表右
/// </summary>

float paddlePosition = 0.5f;

/// <summary>
/// Level we are in and the current score.
/// 当前的游戏等级和分数
/// </summary>

int level = 0, score = -1;

/// <summary>
/// All blocks of the current play field. If they are
/// all cleared, we advance to the next level.
/// 标记砖块是否被清除。如果砖块都被清楚了,我们就可以进到下一关
/// </summary>

bool[,] blocks = new bool[NumOfColumns, NumOfRows];

/// <summary>
/// Block positions for each block we have, initialized in Initialize().
/// 每一个砖块的位置,在Initialize()中初始化。
/// </summary>

Vector2[,] blockPositions = new Vector2[NumOfColumns, NumOfRows];

/// <summary>
/// Bounding boxes for each of the blocks, also precalculated and
/// checked each frame if the ball collides with one of the blocks.
/// 每一个砖块的范围框,预先算好的,并检查每一帧是否有砖块被球碰到
/// </summary>

BoundingBox[,] blockBoxes = new BoundingBox[NumOfColumns, NumOfRows];
First you define how many columns and blocks you can have at maximum; in the first levels you will not fill all the lines and only use 10% of the blocks. The paddle position is also a little bit easier than in Pong because you have just one player. Then you store the current level and the score, which is new. In Pong each player just had three balls and the game was over if all balls were lost. In Breakout the player starts at level 1 and works his way up until he finally loses a ball. You don’t have a high score here or any game font, so the level and score data is just updated in the title of the window.


Then all the blocks are defined; the most important array is blocks, which just tells you which block is currently used. The blocks are initialized before each level starts, whereas the blockPositions and blockBoxes are initialized only once in the constructor of the game; blockPositions is used to determine the centered position of the block for rendering and blockBoxes defines the bounding box of the block for collision testing. It is important to note that none of these lists or position values use screen coordinates. All position data is stored in the 0-1 format: 0 is left or top, and 1 is right or bottom. This way the game stays resolution-independent and makes both rendering and collision checking easier.

接着所有的砖块被定义好;最重要的数组是blocks,告诉你现在哪些砖块是可用的。Blocks数组在每一关开始前被初始化,blockPositions blockBoxes自由的构造器中被初始化一次。blockPositions用来决定砖块渲染的中心位置,blockBoxes定义了碰撞测试的范围框。注意这些列表或位置值没有一个使用的是屏幕坐标。所有的位置数据都储存为01的格式:0代表左边或顶部,1代表右边或底部。这个方式能让游戏不依赖于分辨率,让渲染和碰撞检验变得容易。

The levels are generated in the StartLevel method, which is called at the beginning of the game and every time you advance one level:


void StartLevel()
// Randomize levels, but make it more harder each level
// 初始化等级,但难度逐级递增
  for (int y = 0; y < NumOfRows; y++)
for (int x = 0; x < NumOfColumns; x++)
      blocks[x, y] 
10< level+1;
// Use the lower blocks only for later levels
  if (level < 6)
for (int x = 0; x < NumOfColumns; x++)
      blocks[x, NumOfRows 
- 1= false;
if (level < 4)
for (int x = 0; x < NumOfColumns; x++)
      blocks[x, NumOfRows 
- 2= false;
if (level < 2)
for (int x = 0; x < NumOfColumns; x++)
      blocks[x, NumOfRows 
- 3= false;

// Halt game
  ballSpeedVector = Vector2.Zero;

// Wait until user presses space or A to start a level.
  pressSpaceToStart = true;

// Update title
  Window.Title =
"XnaBreakout - Level " + (level+1+
" - Score " + Math.Max(0, score);
 // StartLevel
In the first for loop you just fill the whole block array with new values depending on the level. In level 1 the level value is 0 and you will only fill 10% of the blocks. RandomHelper.GetRandomInt(10) returns 0–9, which is smaller than 1 in only 10% of the cases. In level 2 this goes up to 20% until you reach level 10 or higher, where 100% of the level is filled. The game actually has no limit; you can play as long as you want.



Then you clear the lower three lines for the first levels to make the first levels easier. At level 3 only two lines are removed and at level 5 just one line is removed until you reach level 7 where all the lines are used.


Unlike Pong the ball speed vector is not immediately started for a new game. The ball stays on the paddle until the user presses space or A. Then the ball bounces off the paddle to a random location and the ball goes between the wall blocks, the screen borders, and the player paddle until either all blocks are removed to win a level or the player loses by not catching the ball.


Finally the window’s title is updated to show the current level number and the score the player has reached so far. In this very simple game the player only gets one point for every block he destroys; reaching a score of 100 is really good, but as I said before, there is no limit. Try to go higher and have fun with the game.


The Game Loop

The game loop in Pong was quite easy and contained mostly input and collision code. Breakout is a little bit more complicated because you have to handle two states of the ball. It is either still on the paddle and awaits the user to press space or you are in the game and have to check for any collisions with the screen borders, the paddle, or any of the blocks in the game.


Most of the Update method looks the same way as in the previous chapter; the second player was removed and a little bit of new code was added at the bottom:


// Game not started yet? Then put ball on paddle.
// 游戏还没开始?把球放在挡板上
if (pressSpaceToStart)
= new Vector2(paddlePosition, 0.95f - 0.035f);
// Handle space
  if (keyboard.IsKeyDown(Keys.Space) ||
== ButtonState.Pressed)
 // if
 // if
// Check collisions
// Update ball position and bounce off the borders
// 更新球的位置,让球反弹
  ballPosition += ballSpeedVector *
* BallSpeedMultiplicator;
// Ball lost?
// 丢球?
  if (ballPosition.Y > 0.985f)
// Play sound
// Game over, reset to level 0
    level = 0;
// Show lost message
    lostGame = true;
 // if
// Check if all blocks are killed and if we won this level
  bool allBlocksKilled = true;
for (int y = 0; y < NumOfRows; y++)
for (int x = 0; x < NumOfColumns; x++)
if (blocks[x, y])
= false;
 // for for if
// We won, start next level
  if (allBlocksKilled == true)
// Play sound
= false;
 // if
 // else
First you check if the ball was not started yet. If not update the ball position and put it on the center of the player’s paddle. Then check if space or A was pressed and start the ball then (just randomizes the ballSpeedVector for you and bounces the ball off to the wall of blocks).


The most important method is CheckBallCollisions, which you will check out in a second. Then the ball is updated like in the Pong game and you check if the ball is lost. If the player did not catch the ball, the game is over and the player can start over at level 1.


Finally you check if all blocks were removed and the level is complete. If all blocks are killed you can play the new victory sound and start the next level. The player sees a “You Won!” message on the screen (see Draw method) and can press space to start the next level.

最后你还要检查所有砖块是否被清除完了,如果是则这一关完成。如果所有的砖块都被清楚了你就可以播放一个表示胜利的声音,然后开始下一关。玩家将会在屏幕上看到“You Won!”的信息,然后按下空格键进入下一关。

Drawing Breakout

Thanks to the SpriteHelper class the Draw method of the Breakout game is short and easy:


protected override void Draw(GameTime gameTime)
// Render background
  SpriteHelper.DrawSprites(width, height);

// Render all game graphics
  paddle.RenderCentered(paddlePosition, 0.95f);
// Render all blocks
  for (int y = 0; y < NumOfRows; y++)
for (int x = 0; x < NumOfColumns; x++)
if (blocks[x, y])
        block.RenderCentered(blockPositions[x, y]);

if (pressSpaceToStart &&
>= 0)
if (lostGame)
 // if

// Draw all sprites on the screen
  SpriteHelper.DrawSprites(width, height);

 // Draw(gameTime)
You start by rendering the background; you don’t have to clear the background because the background texture fills the complete background. To make sure everything is rendered on top of the background, you draw it immediately before rendering the rest of the game sprites. 


Next you draw the paddle and the ball, which is very easy to do because of the RenderCentered helpermethod in the SpriteHelper class, which works like this (the three overloads are just for a more convenient use of this method):

接下来你要绘制挡板和球,这是非常容易的,因为有了SpriteHelper类的 RenderCentered辅助方法的帮助,它就像这样工作(为了更方便使用,这个方法提供了三个重载):

public void RenderCentered(float x, float y, float scale)
new Rectangle(
int)(x * 1024 - scale * gfxRect.Width/2),
int)(y * 768 - scale * gfxRect.Height/2),
int)(scale * gfxRect.Width),
int)(scale * gfxRect.Height)));
 // RenderCentered(x, y)

public void RenderCentered(float x, float y)
  RenderCentered(x, y, 
 // RenderCentered(x, y)

public void RenderCentered(Vector2 pos)
  RenderCentered(pos.X, pos.Y);
 // RenderCentered(pos)

RenderCentered takes a Vector2 or x and y float values and rescales the positions from 0 to 1 (the format you use in your game) to 1024×768. The Draw method of SpriteHelper then rescales everything to the current screen resolution from 1024×768. It may sound complicated, but it is really easy to use.


Then all the blocks of the current level are rendered and again that is very easy thanks to the position you have calculated in the constructor of your game. Take a look at the code on how to initialize all block positions at the upper part of the screen:


// Init all blocks, set positions and bounding boxes
for (int y = 0; y < NumOfRows; y++)
for (int x = 0; x < NumOfColumns; x++)
    blockPositions[x, y] 
= new Vector2(
0.05f + 0.9f * x / (float)(NumOfColumns - 1),
0.066f + 0.5f * y / (float)(NumOfRows - 1));
    Vector3 pos 
= new Vector3(blockPositions[x, y], 0);
    Vector3 blockSize 
= new Vector3(
/1024.0f, GameBlockRect.Y/7680);
    blockBoxes[x, y] 
= new BoundingBox(
- blockSize/2, pos + blockSize/2);
 // for for


The blockBoxes bounding boxes are used for the collision testing discussed in a second. The calculation of the position is also no big deal; the x coordinate goes from 0.05 to 0.95 in as many steps as you have columns (14 if I remember correctly). You can try to change the NumOfColumns constant to 20 and the field will have many more blocks.


Finally a small message is rendered on the screen with a scaling factor of two in case the player has won a level or lost the game. Then you just call the Draw method of SpriteHelper to render all game elements on the screen. Check out the unit tests in the game for how the rendering of the blocks, paddle, and game messages was developed. I started with the unit tests again and then wrote the implementation.


Collision Testing

The collision testing for the Breakout game is a little bit more complicated than just checking the paddles and the screen border in Pong. The most complicated part is to correctly bounce off the blocks the ball hits. For the complete code check the source code for this chapter.


Like the last game you have a ball with a bounding box, screen borders, and the paddle. The blocks are new and to check for any collisions you have check all of them each frame. See Figure 3-16 for an example collision happening with a block in the game.


Figure 3-16

Take a closer look at the basic collision code with the blocks. The collision with the screen border and paddle is pretty much the same as in the Pong game and is checked with the help of the TestBallCollisions unit test. To check for collisions with the blocks you iterate through all of them and check if the bounding box of the ball hits the bounding box of the blocks. The actual game code is a little bit more complicated to check which side of the bounding box you hit and in which direction you have to bounce off, but the rest of the code and the general idea is still the same.



// Ball hits any block?
for (int y = 0; y < NumOfRows; y++)
for (int x = 0; x < NumOfColumns; x++)
if (blocks[x, y])
// Collision check
      if (ballBox.Intersects(blockBoxes[x, y]))
// Kill block
        blocks[x, y] = false;
// Add score
// Update title
    Window.Title =
"XnaBreakout - Level " + (level + 1+ " - Score " + score;
// Play sound
// Bounce ball back
    ballSpeedVector = -ballSpeedVector;
// Go outta here, only handle 1 block at a time
 // if
 // for for if

* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    • 访问:15943次
    • 积分:267
    • 等级:
    • 排名:千里之外
    • 原创:1篇
    • 转载:0篇
    • 译文:11篇
    • 评论:10条