设为首页 收藏本站
查看: 1082|回复: 0

《XNA高级编程:Xbox 360和Windows》3-8

[复制链接]

尚未签到

发表于 2015-5-23 10:18:47 | 显示全部楼层 |阅读模式
3.8 Breakout游戏

     前面介绍了很多辅助类,现在是时候使用它们了。这里我将跳过游戏的构思阶段,Breakout游戏只有单人模式,对手则是砖块,所以可以把它说成是Pong游戏缩略版本。最初Breakout游戏是由Nolan Bushnell和Steve Wozniak发明的,并在1976年由Atari公司发行。在这个早期版本中,它仅仅是个黑白游戏,就像Pong游戏一样。但为了让它更有意思,给显示器蒙了一层透明条纹来给砖块上色(如图3-13所示)。

DSC0000.jpg
图3-13

     您也将做一个这样的游戏,并使用一些Pong的游戏组件以及前面介绍的辅助类。Breakout比Pong游戏更加复杂,它可以设置很多级别,而且有很大的改进空间。例如,Arkanoid就是Breakout游戏的克隆版本,并且在20世纪80到90年代有一大批的游戏都是基于这个创意,它们添加了武器系统、更好的图形特效,以及通过变换砖块的摆放位置来设置不同的级别。

     从图3-14中可以看到,BreakoutGame类的结构很像上一章的Pong类,同时使用SpriteHelper类取代上一章的sprite操作方式,其它一些内部方法和调用也换成了对应的辅助类。例如,StartLevel方法根据当前level值产生新的随机level值,这里就使用了RandomHelper类产生这些随机值。

DSC0001.jpg
图3-14

     注意,这个类中还包含了一些测试方法,在下一章中将对它们和辅助类做一些改进,下一章主要介绍了BaseGame和TestGame类,它们使对游戏类的处理,尤其是单元测试变得更加简单,而且更有条理。

     图3-15展示了接下来要做的Breakout游戏的总体样子,它非常有趣,肯定会比Pong游戏更能让人多玩几次,因为Pong游戏只有两个人玩的时候才比较有意思。Breakout游戏使用了Pong游戏项目中的背景素材和两个声音文件,另外还为球板(paddle)、球和砖块使用了一个新的素材BreakoutGame.png,添加过关(BreakoutVictory.wav)和打碎砖块(BreakoutBlockKill.wav)声效。

DSC0002.jpg
图3-15

Breakout中的单元测试

     在开始复制/粘贴上一章项目的代码、使用新的辅助类以及绘制新的游戏元素之前,您应该仔细考虑一下整个游戏以及可能遇到的问题。当然,您可以不用这么做,而直接开始实现游戏,但后面可能会遇到很难解决的问题,比如碰撞检测,它也是这个游戏最难的部分。单元测试可以帮助您,可以很容易地检查游戏的基本功能,帮助您组织代码,只编写真正需要的东西。就如以前一样,先从游戏最直观的部分开始,并测试它,然后添加更多的单元测试直到所有部分都完成,最后把所有部分组合起来并测试最终的游戏。

     下面是Breakout游戏中使用的单元测试的简单介绍,更详细的内容可以查看本章的源代码。此时还没有TestGame类,所以您还得像上一章那样来进行单元测试,或者作为更好的选择,查看下一章中的相关内容来做静态单元测试。此处只有三个单元测试,但它们会被反反复复地使用和修改。

  • TestSounds:测试项目新增的几个声音特效,按下空格键、Alt键、Control键和Shift键来播放不同的声音。我还在播放下一个声音之前添加了一个小小的停顿,这样更容易听清楚。这个测试用来检查为这个游戏新建的XACT项目。
  • TestGameSprites:这个测试最初是用来测试SpriteHelper类的,不过后来所有代码都被移到了游戏类的Draw方法中。它还用来初始化游戏中的砖块,这部分代码被移到了构造函数中。这个测试告诉您的重点并不是去写一个复杂的测试,因为该测试本身只有4行代码,重点是利用测试让您编写游戏的生活更加简单。需要的时候您可以从这些单元测试中复制一些有用的代码来用。静态单元测试没有必要像动态单元测试那样完美,因为您只在构建和测试游戏的时候使用它们。当游戏能运行之后,您将不再需要这些静态单元测试,除非后面还需要它们随时对游戏进行测试。
  • TestBallCollisions:就像上一章一样碰撞检测是最有用的单元测试。此处要检测球接触屏幕边缘和球板时是否真的会发生碰撞,这部分只要使用上一章的代码并做些较小的改动就行了。然后就是更加复杂的砖块碰撞检测代码,这个稍后将详细介绍。另外,您还可以想出很多其他的方法来检测碰撞,如果愿意的话还可以对游戏进行一些改进。例如,可以试着把球发射到砖块墙的后面,看看它能否把所有的砖块都打碎。
Breakout的级别
     因为Breakout要使用的很多东西在Pong游戏里都是现成的,所以可以跳过那些相似的或者完全相同的实现代码,您要关注的是Breakout游戏里的那些新的变量:


///
/// How many block columns and rows are displayed?
///
const int NumOfColumns = 14,NumOfRows = 12;

///
/// Current paddle positions, 0 means left, 1 means right.
///
float paddlePosition = 0.5f;

///
/// Level we are in and the current score.
///
int level = 0, score = -1;

///
/// All blocks of the current play field. If they are
/// all cleared, we advance to the next level.
///
bool[,] blocks = new bool[NumOfColumns, NumOfRows];

///
/// Block positions for each block we have, initialized in Initialize().
///
Vector2[,] blockPositions = new Vector2[NumOfColumns, NumOfRows];

///
/// Bounding boxes for each of the blocks, also precalculated and checked
/// each frame if the ball collides with one of the blocks.
///
BoundingBox[,] blockBoxes = new BoundingBox[NumOfColumns, NumOfRows];
     首先定义砖块的最大列数和行数,以决定砖块数量的最大值,在第一级的时候不会填满砖块,只使用砖块最大数量的10%。球板位置的计算也比Pong游戏中的简单一些,因为这里只有一个玩家。还要保存当前游戏的级别以及得分,这些在Pong游戏中是没有的。在Pong游戏中每个玩家只有三条命,当三条命都没了游戏就结束了。而在Breakout中,玩家从第一级开始,可以不断地向上升级,直到输了球为止。这里分数不会非常高,也不用处理任何游戏字体,所以级数和分数就直接在窗口的标题栏上进行更新。

     接下来定义砖块,最重要的一个数组就是blocks,它会告诉您当前使用哪个砖块。Blocks在每一级开始之前初始化,而blockPositions和blockBoxes只在游戏类的构造函数中初始化一次,其中blockPositions用来计算渲染砖块的中心位置,blockBoxes用来确定砖块的碰撞检测的边界盒(bounding box)。要注意的是,这些数据都没有使用屏幕坐标系统,所有的位置数据在0-1范围内,0代表左边或者顶部,1代表右边或者底部。这种方式可以使游戏独立于分辨率,而且使渲染碰撞检测更加容易。

     级别是在StartLevel方法中产生的,这个方法在游戏开始以及升级的时候被调用:


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] = RandomHelper.GetRandomInt(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
     在第一个for循环里,根据当前级别重新设置砖块数组的值。在第1级中,level等于0,只使用10%的砖块。RandomHelper.GetRandomInt(10)方法返回值在0-9范围内,小于1的概率只有10%。在第2级,这个概率就上升到20%,当到达第10级或者更高,那就是100% 了。实际上游戏没有上限,只要想玩就可以一直玩下去。

     然后清除底下三行的砖块,这样游戏开始的几级就会容易一些。在第三级的时候,会移除2行,在第5级的时候就只移除1行,到了7级以后所有的行都显示出来。

     和Pong游戏不同的是,在新游戏开始的时候球并不马上运动,球停留在球板上,直到用户按下空格键或者A键才开始运动。然后球朝着一个随机方向离开球板,在砖块、屏幕边缘以及球板之间来回运动,当所有的砖块都被打碎了玩家就赢了,或者玩家没有接到球而输了。

     最后,更新窗口的标题栏来显示到目前为止玩家的级数和得分。在这个简单的版本中,玩家每打碎一个砖块只得到1分,达到100分就非常棒了。但就如我之前所说的,游戏没有限制,尽量取得更高的分数来体验游戏的快乐。

游戏循环

     Pong游戏中的循环非常简单,包含了大部分的用户输入以及碰撞检测代码。Breakout的循环就稍微复杂一些,因为这里要处理球的两种不同状态。一种状态是球静止在球板上,等待用户按下空格键来开始游戏;另一种状态是游戏开始之后,此时要检测球与屏幕边缘、球板和砖块之间的碰撞。

     Update方法中的大部分结构都和上一章的很像,处理第二个玩家的代码被去掉了,同时也增加了一些新的代码:


// Game not started yet? Then put ball on paddle.
if (pressSpaceToStart)
{
    ballPosition = new Vector2(paddlePosition, 0.95f - 0.035f);
    // Handle space
    if (keyboard.IsKeyDown(Keys.Space) ||
        gamePad.Buttons.A == ButtonState.Pressed)
    {
        StartNewBall();
    } // if
} // if
else
{
    // Check collisions
    CheckBallCollisions(moveFactorPerSecond);
    // Update ball position and bounce off the borders
    ballPosition += ballSpeedVector *
        moveFactorPerSecond * BallSpeedMultiplicator;
    // Ball lost?
    if (ballPosition.Y > 0.985f)
    {
        // Play sound
        soundBank.PlayCue("PongBallLost");
        // Show lost message, reset is done above in StartNewBall!
        lostGame = true;
        pressSpaceToStart = true;
        // Play sound
        soundBank.PlayCue("PongBallLost");
        // Game over, reset to level 0
        level = 0;
        StartLevel();
        // 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])
            {
                allBlocksKilled = false;
                break;
            } // for for if
    // We won, start next level
    if (allBlocksKilled == true)
    {
        // Play sound
        soundBank.PlayCue("BreakoutVictory");
        lostGame = false;
        level++;
        StartLevel();
    } // if
} // else
     首先检查是否已经开始游戏了,如果没有,重设球的位置,把它放在球板中心。然后检查用户是否按下了空格键或者A键,如果按下就开始游戏(随机设置ballSpeedVector的值并把球向砖块发射出去)。

     其中最重要的一个方法就是CheckBallCollisions,这个方法稍后再介绍。然后就像Pong游戏那样更新球的位置,并检查是否没有接到球。如果没有接到球,游戏结束,玩家可以从第1级重新开始。

     最后,检查是否所有砖块都被打碎了,如果是,则播放胜利的音效,此时屏幕上出现&#8220;You Won!&#8221;信息(在Draw方法中),按下空格键就可以进入下一级了。

绘制游戏

     使用SpriteHelper类以后Draw方法变得更加简洁了:


protected override void Draw(GameTime gameTime)
{
    // Render background
    background.Render();
    SpriteHelper.DrawSprites(width, height);
    // Render all game graphics            
    paddle.RenderCentered(paddlePosition, 0.95f);
    ball.RenderCentered(ballPosition);
    // 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 &&
        score >= 0)
    {
        if (lostGame)
            youLost.RenderCentered(0.5f, 0.65f, 2);
        else
            youWon.RenderCentered(0.5f, 0.65f, 2);
    } // if
    // Draw all sprites on the screen
    SpriteHelper.DrawSprites(width, height);
    base.Draw(gameTime);
} // Draw(gameTime)
     首先渲染背景。这里不必先清空背景,因为背景纹理会填充整个游戏背景。为了确保其他所有游戏元素都能在背景之上渲染出来,要在渲染其他元素之前先把背景画出来。

     接下来画球板和球,因为使用了SpriteHelper类中的RenderCentered方法,这个操作就很简单了。这个方法有三个重载版本,如下所示:


public void RenderCentered(float x, float y, float scale)
{
    Render(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, 1);
} // RenderCentered(x, y)

public void RenderCentered(Vector2 pos)
{
    RenderCentered(pos.X, pos.Y);
} // RenderCentered(pos)
     RenderCentered方法接收一个Vector2类型的参数,或者两个float类型的参数,并按照1024&#215;768的分辨率按比例缩放计算位置。然后,SpriteHelper类的DrawSprites方法再把所有的游戏元素从1024&#215;768的分辨率按比例缩放到当前的屏幕分辨率。这听起来很复杂,但它用起来很方便。

     接下来渲染本级中的所有砖块,这也很简单,因为砖块的位置已经在构造函数中计算好了。下面是初始化砖块位置的代码:


// 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(
            GameBlockRect.X/1024.0f, GameBlockRect.Y/768, 0);
        blockBoxes[x, y] = new BoundingBox(
            pos - blockSize/2,
            pos + blockSize/2);
    } // for for
其中变量blockBoxes用于存储砖块的边界盒数据,用于碰撞检测,这个稍后介绍。位置计算也很简单,x坐标范围从0.05到0.95,也可以把常量NumOfColumns的值改成20,这样就会有更多的砖块。

     最后,如果玩家升级了或者输了,就会在屏幕上渲染对应的消息。然后,调用SpriteHelper类的DrawSprites方法渲染所有的游戏元素输出到屏幕上。看看对应的单元测试中是如何渲染砖块、球板和游戏信息的,我就是从单元测试开始的,然后才去实现游戏。

碰撞检测

     Breakout游戏中的碰撞检测要比Pong游戏稍微复杂一点,因为Pong游戏中只要检测与挡板和屏幕边缘的碰撞。这里最复杂的部分就是,当球撞击砖块的时候能正确地反弹回来。完整的检测代码请查看本章的源代码。

     像上一个游戏一样,这里也有球、屏幕边缘和球板等元素。只有砖块是新的元素,并且为了检测每一个碰撞,每一帧都要检测所有这些砖块。图3-16是撞击砖块的碰撞检测示意图:

DSC0003.jpg
图3-16

     仔细看一下砖块的碰撞检测代码。屏幕边缘和球板的碰撞检测与Pong游戏中的基本上一样,而且可以使用TestBallCollisions单元测试来检查。而在检测砖块的碰撞时,要检测所有砖块,看看球的边界盒是否和砖块的边界盒发生了碰撞。实际的游戏代码还要稍微复杂一些,因为要检测撞上了边界盒的哪一边,球要向哪个方向反弹。不过其余代码和整体思想都是一样的。


// 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
                score++;
                // Update title
                Window.Title =
                    "XnaBreakout - Level " + (level + 1) + " - Score " + score;
                // Play sound
                soundBank.PlayCue("BreakoutBlockKill");
                // Bounce ball back
              ballSpeedVector = -ballSpeedVector;
                // Go outa here, only handle 1 block at a time
                break;
            } // if
        } // for for if

运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-69751-1-1.html 上篇帖子: Windows Phone 8 中的一些新东西. 下篇帖子: windows 8 metro 风格开发(9)-Interactive(Behavior和EventTrigger)
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表