《XNA高级编程:Xbox 360和Windows》4-5

4.5 Tetris,Tetris,Tetris!


     关于辅助类和游戏组件已经讨论很多了,现在我们就来编写一个很酷的游戏。正是借助于这些辅助类,在这个新的游戏中,我们才能非常容易地在屏幕上输出文本,绘制
sprites ,处理用户输入以及播放声音特效。

     在深入到 Tetris 游戏逻辑细节之前,仔细考虑一下游戏元素的布置会很有帮助,就像您在前几章所做的那样。起初我们并不是把所有的游戏组件都输出到屏幕上,而是只显示背景边框,看看即将输出那些内容。这里的背景再一次沿用之前的太空背景图(我保证,这将是最后一次)。背景边框是一个新的素材,并且有两种显示模式(如图 4-7 所示)。它用来区分不同的游戏组件,这样可以让这些组件输出到屏幕上更好看。由于左右两边的显示尺寸不一样,如果使用同一个素材的话会很难看,所以需要把该素材修改成不同的尺寸来使用。

图4-7

4-7

渲染背景

     此处再次使用 SpriteHelper 类来渲染这些背景边框,并使用下面的单元测试:

public   static   void  TestBackgroundBoxes()
{
    TestGame.Start(
"TestBackgroundBoxes",
        
delegate
        
{
            
// Render background
            TestGame.game.background.Render();
            
// Draw background boxes for all the components
            TestGame.game.backgroundBigBox.Render(new Rectangle(
                (
512 - 200- 1540 - 12400 + 23, (768 - 40+ 16));
            TestGame.game.backgroundSmallBox.Render(
new Rectangle(
                (
512 - 480- 1540 - 10290 - 30300));
            TestGame.game.backgroundSmallBox.Render(
new Rectangle(
                (
512 + 240- 1540 - 10290 - 30190));
        }
);
}
  //  TestBackgroundBoxes()

     该测试的输出如图
4-8 所示:

图4-8

4-8

     您可能会问为什么右边的边框小一些,而且这些值是如何取得的。其实这里的值都是随意定义的,它们会在最终的游戏里做最合理的调整。 First, the background is drawn in the unit test because you will not call the Draw method of TetrisGame if you are in the unit test (otherwise the unit tests won’t work anymore later when the game is fully implemented).

     左上角的区域用来显示下一个要显示的方块,中间的区域用来显示 Tetris 网格,右上角的区域用来显示记分板。

处理网格

     现在我们来填充上述边框的内容。首先从主要组件 TetrisGrid 开始,它负责显示整个游戏的网格区域。它还处理用户的输入、移动下落的方块以及显示所有既有的数据。在讨论游戏组件的小节中已经说了 TetrisGrid 类使用了哪些方法,在渲染网格之前您应该检查一下该类定义的一些常量:

Constants #region Constants
public const int GridWidth = 12;
public const int GridHeight = 20;
..

     还有很多其它有趣的常量,不过现在您只需要网格的尺寸,这里定义了
12 列和 20 行。借助于 Block.png 素材(就是一个正方形方块),您可以很方便地在 Draw 方法中绘制出完整的网格区域:

//  Calc sizes for block, etc.
int  blockWidth  =  gridRect.Width  /  GridWidth;
int  blockHeight  =  gridRect.Height  /  GridHeight;
for  ( int  x  =   0 ; x  <  GridWidth; x ++ )
    
for  ( int  y  =   0 ; y  <  GridHeight; y ++ )
    
{
        game.BlockSprite.Render(
new Rectangle(
             gridRect.X 
+ x * blockWidth,
             gridRect.Y 
+ y * blockHeight,
             blockWidth 
- 1, blockHeight - 1),
             
new Color(606060128)); // Empty Color
    }
  //  for for

     其中变量
gridRect 是在主类中传递过来的,用来定义绘制网格的区域。它和背景边框使用的是同一个矩形区域,当然会稍微小一点点以便更好地填充背景。这里您首先要做的就是计算方块的宽度与高度,然后遍历数组,使用 SpriteHelper.Render 方法来绘制方块,并使用半透明黑色,来展现一个空的背景网格。如图 4-9 所示。正是因为使用了游戏组件,在单元测试中您都不用去写所有这些代码。单元测试只绘制了背景边框,然后调用 TetrisGrid.Draw 方法来显示结果(参见 TestEmptyGrid 单元测试)。
图4-9

4-9

方块类型

     在向网格上渲染东西之前,您需要考虑一下游戏中都将使用哪些种类的方块。标准的 Tetris 游戏有 7 种方块类型,它们都是由四块更小的方块彼此连接在一起组合而成的(如图 4-10 所示)。其中最受欢迎的当然是直线型的,因为它可以同时消去四行方块,而且得分也最多。

图4-10

4-10

     这些方块类型必须在 TetrisGrid 类中定义。方法之一就是使用枚举来定义所有可能的方块类型。该枚举还定义了空类型的方块,可以应用于整个网格,因为网格中的每一个小方格要么包含预定义方块的一个部分,要么为空。下面来看一下 TetrisGrid 类中定义的其它常量:

/**/ /// <summary>
/// Block types we can have for each new block that falls down.
/// </summary>

public   enum  BlockTypes
{
    Empty,
    Block,
    Triangle,
    Line,
    RightT,
    LeftT,
    RightShape,
    LeftShape,
}
  //  enum BlockTypes
/**/ /// <summary>
/// Number of block types we can use for each grid block.
/// </summary>

public   static   readonly   int  NumOfBlockTypes  =
            EnumHelper.GetSize(
typeof (BlockTypes));
/**/ /// <summary>
/// Block colors for each block type.
/// </summary>

public   static   readonly  Color[] BlockColor  =   new  Color[]
{
    
new Color( 606060128 ), // Empty, color unused
    new Color( 5050255255 ), // Line, blue
    new Color( 160160160255 ), // Block, gray
    new Color( 2555050255 ), // RightT, red
    new Color( 25525550255 ), // LeftT, yellow
    new Color( 50255255255 ), // RightShape, teal
    new Color( 25550255255 ), // LeftShape, purple
    new Color( 5025550255 ), // Triangle, green
}
//  Color[] BlockColor
/**/ /// <summary>
/// Unrotated shapes
/// </summary>

public   static   readonly   int [][,] BlockTypeShapesNormal  =   new   int [][,]
{
    
// Empty
    new int[,] 0 } },
    
// Line
    new int[,] 010 }010 }010 }010 } },
    
// Block
    new int[,] 11 }11 } },
    
// RightT
    new int[,] 11 }10 }10 } },
    
// LeftT
    new int[,] 11 }01 }01 } },
    
// RightShape
    new int[,] 011 }110 } },
    
// LeftShape
    new int[,] 110 }011 } },
    
// LeftShape
    new int[,] 010 }111 }000 } },
}
//  BlockTypeShapesNormal

     其中 BlockTypes 就是之前我们讨论的方块类型枚举,它包含了所有可能的类型,并用于 NextBlock 游戏组件中随机生成新方块。网格区域在开始时都被空类型的方块填充,网格的定义如下:

/**/ /// <summary>
/// The actual grid, contains all blocks,
/// including the currently falling block.
/// </summary>

BlockTypes[,] grid  =   new  BlockTypes[GridWidth, GridHeight];

     另外, NumOfBlockTypes 则向您展示了枚举类的好处,您可以很方便地知道 BlockTypes 枚举中有多少种类型。

     接下来为每种方块类型定义了颜色,这些颜色在生成 NextBlock 的预览时使用,也用于渲染整个网格。每一个小方格都包含一个方块类型,您可以把枚举类型转换为整型来方便地使用 BlockColor ,就像在 Draw 方法中那样:

BlockColor[( int )grid[x,y]]

     最后定义方块形状,这看起来有些复杂,尤其是当您考虑旋转这些方块时。此处使用 BlockTypeShapes 来实现,它是一个保存了所有方块及其旋转的大数组,在 TetrisGrid 初始化时进行计算。

     当向网格添加一个新方块时,您可以把方块的每一个部分分别添加到网格上,这个操作在方法 AddRandomBlock 中进行。这里使用 floatingGrid 数组来保存每当 Update 方法被调用时网格的哪些部分需要向下移动(参考下一小节“自由下落”,您不能让所有东西都下落):

//  Randomize block type and rotation
currentBlockType  =  ( int )nextBlock.SetNewRandomBlock();
currentBlockRot 
=  RandomHelper.GetRandomInt( 4 );
//  Get precalculated shape
int [,] shape  =  BlockTypeShapes[currentBlockType,currentBlockRot];
int  xPos  =  GridWidth / 2 - shape.GetLength( 0 ) / 2 ;
//  Center block at top most position of our grid
currentBlockPos  =   new  Point(xPos,  0 );
//  Add new block
for  (  int  x = 0 ; x < shape.GetLength( 0 ); x ++  )
    
for  (  int  y = 0 ; y < shape.GetLength( 1 ); y ++  )
        
if  ( shape[x,y]  >   0  )
        
{
            
// Check if there is already something
            if (grid[x + xPos, y] != BlockTypes.Empty)
            
{
                
// Then game is over dude!
                gameOver = true;
                Sound.Play(Sound.Sounds.Lose);
            }
 // if
            else
            
{
                grid[x 
+ xPos, y] = (BlockTypes)currentBlockType;
                floatingGrid[x 
+ xPos, y] = true;
            }
 // else
        }
  //  for for if

     首先要确定添加哪种类型的方块。此处使用 NextBlock 类中的一个辅助方法来实现,该方法随机产生一个方块类型,并返回“下一个方块”窗口中显示的方块类型。另外,方块的旋转也是随机的,该操作则借助 RandomHelper 辅助类来实现。

     有了这些数据,现在您就可以在网格的顶端中央位置显示计算出来的形状。两个 for 循环遍历整个 shape 二维数组,它添加形状的每个部分直到和已有的网格数据相冲突。如果这样的话,游戏就结束了,并播放失败的声音。当方块一直堆积到网格的顶端时,您也就无法再添加新的方块了。

     现在,您可以在网格上显示新的方块了。但它如果一直停留在顶部就没什么意思了,它应该往下落。

自由下落

     使用单元测试 TestFallingBlockAndLineKill 来测试当前方块的自由下落。每次调用 TetrisGrid Update 方法时,当前活动的方块都会被更新,不过这样的操作并不经常发生。在游戏第一级的时候,每隔 1000 毫秒(即 1 秒)调用一次 Update 方法。此时要检查当前方块是否还可以向下移动:

//  Try to move floating stuff down
if  (MoveBlock(MoveTypes.Down)  ==   false   ||
    movingDownWasBlocked)
{
    
// Failed? Then fix floating stuff, not longer moveable!
    for ( int x=0; x<GridWidth; x++ )
        
for ( int y=0; y<GridHeight; y++ )
            floatingGrid[x,y] 
= false;
    Sound.Play(Sound.Sounds.BlockFalldown);
}
  //  if
movingDownWasBlocked  =   false ;

     大部分的游戏逻辑都是在 MoveBlock 方法中处理的,它会检测往某个特定方向移动是否可行。如果无法再移动,那它就会被固定住,然后清空 floatingGrid 数组,并播放方块落地的声音。

     清空 floatingGrid 数组之后,就没有活动的方块可以移动了,然后使用下面的代码检测是否消除了一行方块:

//  Check if we got any moveable stuff,
//  if not add new random block at top!
bool  canMove  =   false ;
for  (  int  x = 0 ; x < GridWidth; x ++  )
    
for  (  int  y = 0 ; y < GridHeight; y ++  )
        
if  ( floatingGrid[x,y] )
            canMove 
=   true ;
if  (canMove  ==   false )
{
    
int linesKilled = 0;
    
// Check if we got a full line
    for ( int y=0; y<GridHeight; y++ )
    
{
        
bool fullLine = true;
        
for ( int x=0; x<GridWidth; x++ )
            
if ( grid[x,y] == BlockTypes.Empty )
            
{
                fullLine 
= false;
                
break;
            }
 // for if
        
// We got a full line?
        if (fullLine)
        
{
            
// Move everything down
            for ( int yDown=y-1; yDown>0; yDown-- )
                
for ( int x=0; x<GridWidth; x++ )
                    grid[x,yDown
+1= grid[x,yDown];
            
// Clear top line
            for ( int x=0; x<GridWidth; x++ )
                grid[
0,x] = BlockTypes.Empty;
            
// Add 10 points and count line
            score += 10;
            lines
++;
            linesKilled
++;
            Sound.Play(Sound.Sounds.LineKill);
        }
 // if
    }
 // for
    
// If we killed 2 or more lines, add extra score
    if (linesKilled >= 2)
        score 
+= 5;
    
if (linesKilled >= 3)
        score 
+= 10;
    
if (linesKilled >= 4)
        score 
+= 25;
    
// Add new block at top
    AddRandomBlock();
}
  //  if

     首先判断是否有活动的正在移动的方块。如果没有,则进入 if 语句块,检测是否有一行被填充并销毁该行。要判断某行是否被填充,您可以先假设它是填充的,然后检查该行是否有空类型的方块。然后,如果该行没有被填充,则检查下一行。如果该行被填充了,则移除它,并将其上面的所有行向下移动一行。这样,玩家就会得到 10 分,并听到该行被消除的声音。

     如果玩家消除不止一行的方块,还会得到更多的奖励分。然后,调用 AddRandomBlock 方法在顶部生成一个新方块。

处理输入

     借助于 Input 辅助类,处理用户的输入不再是什么难事,您可以很容易地检测到是否按下了键盘或者游戏手柄。 BaseGame 类中处理了按下 Escape Back 键的操作,此时将退出游戏。不过,在该 Tetris 游戏中您只需要四个键,使用左右键进行左右移动,向上键则用来旋转方块,使用向下键,或者空格键,或者 A 键,可以让方块更快速地下落。

     与检测自由下落来判断是否可以向下移动类似,您还要检测是否可以向左右移动。这部分代码在 TetrisGame Update 方法中实现,因为您需要每一帧都检测用户的输入,而不只是更新 TetrisGrid 的时候,后者每隔 1000 毫秒执行一次。之前这部分代码是放在 TetrisGrid Update 方法中的,但为了提升用户体验,这些代码被转移了,并被大量地改进,可以让玩家通过连续敲击键盘快速地进行左右移动。

     现在,您已经学习了很多辅助性的代码,而且可以初步运行 Tetris 游戏了。不过,您应该多多注意 MoveBlock 方法,因为它是 Tetris 游戏中最重要的部分。另一个非常重要的方法是 RotateBlock ,它用来检测方块是否可以旋转,您可以自己查看一下源代码。可以使用 TetrisGame 类中的单元测试来学习这些方法是如何工作的:

Move block #region Move block
public enum MoveTypes
{
    Left,
    Right,
    Down,
}
 // enum MoveTypes
/**//// <summary>
/// Remember if moving down was blocked, this increases
/// the game speed because we can force the next block!
/// </summary>

public bool movingDownWasBlocked = false;
/**//// <summary>
/// Move current floating block to left, right or down.
/// If anything is blocking, moving is not possible and
/// nothing gets changed!
/// </summary>
/// <returns>Returns true if moving was successful, otherwise false</returns>

public bool MoveBlock(MoveTypes moveType)
{
    
// Clear old pos
    for ( int x=0; x<GridWidth; x++ )
        
for ( int y=0; y<GridHeight; y++ )
            
if ( floatingGrid[x,y] )
                grid[x,y] 
= BlockTypes.Empty;
    
// Move stuff to new position
    bool anythingBlocking = false;
    Point[] newPos 
= new Point[4];
    
int newPosNum = 0;
    
if ( moveType == MoveTypes.Left )
    
{
        
for ( int x=0; x<GridWidth; x++ )
            
for ( int y=0; y<GridHeight; y++ )
                
if ( floatingGrid[x,y] )
                
{
                    
if ( x-1 < 0 ||
                        grid[x
-1,y] != BlockTypes.Empty )
                        anythingBlocking 
= true;
                    
else if ( newPosNum < 4 )
                    
{
                        newPos[newPosNum] 
= new Point( x-1, y );
                        newPosNum
++;
                    }
 // else if
                }
 // for for if
    }
 // if (left)
    else if ( moveType == MoveTypes.Right )
    
{
        
for ( int x=0; x<GridWidth; x++ )
            
for ( int y=0; y<GridHeight; y++ )
                
if ( floatingGrid[x,y] )
                
{
                    
if ( x+1 >= GridWidth ||
                        grid[x
+1,y] != BlockTypes.Empty )
                        anythingBlocking 
= true;
                    
else if ( newPosNum < 4 )
                    
{
                        newPos[newPosNum] 
= new Point( x+1, y );
                        newPosNum
++;
                    }
 // else if
                }
 // for for if
    }
 // if (right)
    else if ( moveType == MoveTypes.Down )
    
{
        
for ( int x=0; x<GridWidth; x++ )
            
for ( int y=0; y<GridHeight; y++ )
                
if ( floatingGrid[x,y] )
                
{
                    
if ( y+1 >= GridHeight ||
                        grid[x,y
+1!= BlockTypes.Empty )
                        anythingBlocking 
= true;
                    
else if ( newPosNum < 4 )
                    
{
                        newPos[newPosNum] 
= new Point( x, y+1 );
                        newPosNum
++;
                    }
 // else if
                }
 // for for if
        if ( anythingBlocking == true )
            movingDownWasBlocked 
= true;
    }
 // if (down)
    
// If anything is blocking restore old state
    if ( anythingBlocking ||
        
// Or we didn't get all 4 new positions?
        newPosNum != 4 )
    
{
        
for ( int x=0; x<GridWidth; x++ )
            
for ( int y=0; y<GridHeight; y++ )
                
if ( floatingGrid[x,y] )
                    grid[x,y] 
= (BlockTypes)currentBlockType;
        
return false;
    }
 // if
    else
    
{
        
if ( moveType == MoveTypes.Left )
            currentBlockPos 
= new Point( currentBlockPos.X-1, currentBlockPos.Y );
        
else if ( moveType == MoveTypes.Right )
            currentBlockPos 
= new Point( currentBlockPos.X+1, currentBlockPos.Y );
        
else if ( moveType == MoveTypes.Down )
            currentBlockPos 
= new Point( currentBlockPos.X, currentBlockPos.Y+1 );
        
// Else we can move to the new position, lets do it!
        for ( int x=0; x<GridWidth; x++ )
            
for ( int y=0; y<GridHeight; y++ )
                floatingGrid[x,y] 
= false;
        
for ( int i=0; i<4; i++ )
        
{
            grid[newPos[i].X,newPos[i].Y] 
= (BlockTypes)currentBlockType;
            floatingGrid[newPos[i].X,newPos[i].Y] 
= true;
        }
 // for
        Sound.Play(Sound.Sounds.BlockMove);
        
return true;
    }
 // else
}
 // MoveBlock(moveType)
#endregion

     这里有三种类型的移动:向左、向右和向下,每种移动都放在单独的程序块中进行检测。在详细研究这个方法之前,有两点需要注意。首先,在方法之前定义了一个 movingDownWasBlocked 变量。使用该变量是为了加快检测方块是否落地的过程,它作为类级别的变量可以方便 Update 方法的调用(可能是若干帧之后), and make the gravity code you saw earlier update much faster than in the case when the user doesn’t want to drop the block down right here. 这是游戏非常重要的部分,因为如果每个方块落地时立即被固定,那么游戏会变得很难,并且随着游戏速度变快以及网格被填得越来越满,游戏的乐趣也就消失了。

     接下来使用另一个技巧来简化判断的过程,即暂时把当前方块从网格上移除。这样您就可以很容易地检测新位置是否可用,因为当前位置已经不会再有阻碍了。另外还使用了几个辅助变量来存储新的位置数据,这样负责检测方块四个部分的代码就简化了一些。如果您改变了方块类型以及组成每个方块的小方块的数量,那么也要相应地更改该方法。

     当一切准备就绪之后,您要在三段代码块中检测方块的临时位置是否可用。通常都是可用的,此时 newPosNum 数组就保存了四个新的值。如果少于三个值的话,变量 anythingBlocking 就被设置为真。这样的话,原有的方块位置信息被重新保存起来,数组 grid floatingGrid 都保持不变。

     但如果移动是可行的,方块位置信息将被更新,并清空 floatingGrid 数组。最后,把该方块添加到 grid floatingGrid 数组中,就可以在新位置上添加该方块了。同时,用户还可听到很小的方块移动的声音,这样该方法也就结束了。

测试

     使用 TetrisGrid 类中新增的代码,现在您可以测试 TetrisGame 类中的单元测试了。除了之前您见过的单元测试之外,还有两个更重要的测试游戏逻辑的单元测试:
  • TestRotatingBlock:测试TetrisGrid类的RotateBlock方法
  • TestFallingBlockAndKillLine:测试自由下落以及用户输入
     您经常会根据对游戏的最新更改而去修改旧有的单元测试,这是很显然的。例如,之前介绍的 TestBackgroundBoxes 单元测试很简单,但在实现以及测试游戏组件的时候这些背景边框的布局以及位置都会发生很大的变化,这个时候就要修改该单元测试以反映这些变化。例子之一就是记分板,它被背景边框包围起来,但在知道记分板到底有多大之前,您需要确定其中的内容以及这些内容要占据的空间。当写完 TestScoreboard 方法之后,您会发现记分板比“ NextBlock ”区域小得多。

     游戏测试的另一部分就是要不断地检查游戏的 bug 以及改进游戏代码。前面做的几个游戏都很简单,在完成初始版本之后您只需要做很小的改进。但 Tetris 游戏要复杂的多,您会花很多时间来修正并改进它。

     最后要测试的就是在 Xbox 360 平台上运行该游戏,具体的步骤在第一章已经介绍过了。如果您写了新的代码,还要确保在 Xbox 360 平台下编译它们。您也不能调用非托管程序集,而且 .NET 2.0 Framework 中的某些类和方法也无法在 Xbox 360 上使用。

转载于:https://www.cnblogs.com/AlexCheng/archive/2010/05/11/2120322.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值