Swift 实现俄罗斯方块详细思路解析

标签: Swift
发布时间: 2016/5/12 17:02:31

前言

    俄罗斯方块,是一款我们小时候都玩过的小游戏,我自己也是看着书上的思路,学着用 Swift 来写这个小游戏,在写这个游戏的过程中,除了一些位置的计算,数据模型和理解 Swift 语言之外,最好知道UIKIt框架中的 Quartz2D 这个知识点。是我在简书上面找的,是关于 Quartz2D 这个知识点的,看它我觉得也就够学习。经过这两天的整理,充分觉得在写这些之前,一定要理清楚思路,你可能会花很多时间在它上面,你要知道了,怎么写就变的反而简单了。

开发思路

    我在博客的最下面附上了完整的代码,大家可以在Git上下载到它,你要也使用Git,就顺便给我个小星星吧 O(∩_∩)O哈哈~。。

1.游戏界面的布局设计

    这个里面的Label 和 Button 就不多费口舌了,这不是我们的重点,看看这个效果我们也就一笔带过了吧!重点是我们使用的上面说的利用 Quartz2D 这个知识画出来表格。它单看就是一个 N * M 的表格,在它里面就要运行我们的俄罗斯小方块,在下面的代码里面也会详细的说明它的制作。


下面是我们绘制上面网格视图的方法,下面所有代码方法里面的有些参数是定义成全局变量的,大家可以下载完整版的代码去看看。在代码中也加了许多的注释,相信都能看的明白的。

// MARK: 绘制俄罗斯方库网格的方法
   func creatcells(rows:Int,cols:Int,cellwidth:Int,cellHeight:Int) -> Void {
        
       // 开始创建路径
       CGContextBeginPath(CTX)
       // 绘制横向网格对应的路径
       for  i  in 0...TETRIS_Row {
            
           CGContextMoveToPoint(CTX, 0, CGFloat(i  *  CELL_Size))
           CGContextAddLineToPoint(CTX, CGFloat(TETRIS_Cols * CELL_Size), CGFloat(i * CELL_Size))
            
       }
       // 绘制纵向的网格对应路径
       for  i  in 0...TETRIS_Cols {
            
           CGContextMoveToPoint(CTX, CGFloat(i  *  CELL_Size),0)
           CGContextAddLineToPoint(CTX, CGFloat(i * CELL_Size), CGFloat(TETRIS_Row * CELL_Size))
            
       }
       // 关闭
       CGContextClosePath(CTX)
        
       // 设置笔触颜色
       CGContextSetStrokeColorWithColor(CTX, UIColor(red: 0.9 , green: 0.9 , blue: 0.9,alpha: 1).CGColor)
       // 设置效线条粗细
       CGContextSetLineWidth(CTX, CGFloat(STROKE_Width))
       // 绘制线条
       CGContextStrokePath(CTX)
        
   }


2.小游戏的数据模型

1: 游戏的游戏界面是一个 N * M 的网格,每一张网格显示一张图片,但对于我们来说,我门就得用一个二维数组来定义,纪录每一块的行和列!来保存游戏的状态。我们在最开始把每一个小块的游状态都初始化为 0 ,看下面代码。

// 定义用于纪录方块游戏状态的二维数组
var tetris_status = [[Int]]()
 
// MARK初始化游戏状态
func initTetrisStatus() -> Void {
     
    let tmpRow = Array.init(count: TETRIS_Cols, repeatedValue: NO_Block)
    tetris_status  = Array.init(count: TETRIS_Row, repeatedValue: tmpRow)
     
}

2: 游戏的过程中有一只处于“下落”状态的四个方块,这四个方块我们也会是要纪录,才可以做它的旋转、向左、向右等等的处理。我们就用一个数组包含着四个方块,那具体到这四个方块呢?我们就用一个结构体去体现你这四个方块它的 X、Y值和颜色。

struct Block {
     
    var X:Int
    var Y:Int
    var Color:Int
    var description:String {
         
        return "Block[X=\(X),Y=\(Y),Color=\(Color)]"
    }
}

3:在俄罗斯方块这个游戏中,你也肯定得知道有哪些 方块的组合可以下落,这也是一个数据源!你也得定义好,在每次要下落的时候你就随机取出这个而数据源里面的数据,让它随机的出现下落。这些工作也就是你要 在初始化上面要纪录的四个正在下落的方块数组的时候做的事了,下面是这些个组合的数据源。

// 几种可能的组合方块
self.blockArr = [
   
    // 第一种可能出现的组合 Z
    [
        Block(X:TETRIS_Cols/2 - 1,Y:0,Color:1),
        Block(X:TETRIS_Cols/2,Y:0,Color:1),
        Block(X:TETRIS_Cols/2,Y:1,Color:1),
        Block(X:TETRIS_Cols/2 + 1,Y:1,Color:1)
     
    ],
    // 第二种可能出现的组合 反Z
    [
        Block(X:TETRIS_Cols/2 + 1,Y:0,Color:2),
        Block(X:TETRIS_Cols/2,Y:0,Color:2),
        Block(X:TETRIS_Cols/2,Y:1,Color:2),
        Block(X:TETRIS_Cols/2 - 1,Y:1,Color:2)
         
    ],
    // 第三种可能出现的组合 田
    [
        Block(X:TETRIS_Cols/2 - 1,Y:0,Color:3),
        Block(X:TETRIS_Cols/2,Y:0,Color:3),
        Block(X:TETRIS_Cols/2 - 1,Y:1,Color:3),
        Block(X:TETRIS_Cols/2 ,Y:1,Color:3)
             
    ],
    // 第四种可能出现的组合 L
    [
        Block(X:TETRIS_Cols/2 - 1,Y:0,Color:4),
        Block(X:TETRIS_Cols/2 - 1,Y:1,Color:4),
        Block(X:TETRIS_Cols/2 - 1,Y:2,Color:4),
        Block(X:TETRIS_Cols/2 ,Y:2,Color:4)
             
    ],
    // 第五种可能出现的组合 J
    [
        Block(X:TETRIS_Cols/2,Y:0,Color:5),
        Block(X:TETRIS_Cols/2,Y:1,Color:5),
        Block(X:TETRIS_Cols/2,Y:2,Color:5),
        Block(X:TETRIS_Cols/2 - 1,Y:2,Color:5)
             
    ],
    // 第六种可能出现的组合 ——
    [
        Block(X:TETRIS_Cols/2,Y:0,Color:6),
        Block(X:TETRIS_Cols/2,Y:1,Color:6),
        Block(X:TETRIS_Cols/2,Y:2,Color:6),
        Block(X:TETRIS_Cols/2,Y:3,Color:6)
         
    ],
    // 第七种可能出现的组合 土缺一
    [
        Block(X:TETRIS_Cols/2,Y:0,Color:7),
        Block(X:TETRIS_Cols/2-1,Y:1,Color:7),
        Block(X:TETRIS_Cols/2,Y:1,Color:7),
        Block(X:TETRIS_Cols/2 + 1,Y:1,Color:7)
             
    ],
]

随机取出下落

// 定义纪录 “正在下掉的四个方块” 位置
 var currentFall = [Block]()
 func initBlock() -> Void {
      
     // 生成一个在 0 - blockArr.count  之间的随机数
     let rand =  Int(arc4random()) % blockArr.count
     // 随机取出 blockArr 数组中的某个元素为正在下掉的方块组合
     currentFall = blockArr[rand]
 
 }

3.游戏逻辑处理

    1:下落

   前面我们提到过有用数组纪录正在下落的四个方块的状态,我们梳理一下“下落”状态的逻辑关系。如果在下落的状态,你只需要把这四个正在下落的方块的 Y 值加 1 即可! 但是得注意什么情况下它不能再下落了。。

       (1):如果方块组合中任意一个方块已经到达了最底下就不能再下落了。

       (2) :如果方库组合中任意一个方块的下面有了方块就不能再下落了。

       下落的实现思路就是,如果有方块可以下落,那么就把方块组合原来所在位置的颜色清楚,然后把组合中的每一个方块的 Y 属性加1 ,最后把当前方块的所在位置加上相应的颜色,下面是思路实现的代码。

// MARK:控制方块组合向下移动
 func movedown () -> Void {
      
     // 定义能否向下掉落的 标签
     var canDown = true
      
     // 遍历每一块方块,判断它是否能向下掉落
     for i in 0..<currentFall.count {
          
         // 第一种情况,如果位置到行数最底下了,不能再下落
         if currentFall[i].Y >= TETRIS_Row - 1 {
              
             canDown = false
             break
         }
         // 第二种情况,如果他的下面有了方块,不能再下落
         if tetris_status[currentFall[i].Y + 1][currentFall[i].X] != NO_Block {
              
             canDown = false
             break
         }
     }
     // 如果能向下掉落
     if canDown {
  
         self.drawBlock()//
          
         for i in 0..<currentFall.count {
              
             let cur = currentFall[i]
             // 设置填充颜色
             CGContextSetFillColorWithColor(CTX, UIColor.whiteColor().CGColor)
              
             CGContextFillRect(CTX, CGRectMake(CGFloat(cur.X * CELL_Size + STROKE_Width), CGFloat(cur.Y * CELL_Size + STROKE_Width), CGFloat(CELL_Size - STROKE_Width * 2),CGFloat(CELL_Size - STROKE_Width * 2)))
              
 
         }
         //  遍历每一个方块。控制每一个方块的 有坐标都 加 1
         for i in 0..<currentFall.count {
      
             currentFall[i].Y += 1
              
         }
         //  将下移后的每一个方块的背景涂色称该方块的颜色
         for i in 0..<currentFall.count {
      
             let cur = currentFall[i]
             // print(cur.X   ,   cur.Y)
             CGContextSetFillColorWithColor(CTX, colors[cur.Color])
             CGContextFillRect(CTX, CGRectMake(CGFloat(cur.X * CELL_Size + STROKE_Width), CGFloat(cur.Y * CELL_Size + STROKE_Width), CGFloat(CELL_Size - STROKE_Width * 2),CGFloat(CELL_Size - STROKE_Width * 2)))
              
         }
     }
     // 不能向下掉落
     else
     {
         // 遍历每个方块,把每个方块的值纪录到
         for i in 0..<currentFall.count {
              
             let cur = currentFall[i]
             // 小于2表示已经到最上面,游戏要结束了
             if cur.Y < 2 {
                  
                 // 计时器失效
                 curTimer?.invalidate()
                 // 提示游戏结束
                 self.delegate.UpdateGameState()
                  
             }
              
             // 把每个方块当前所在的位置赋值为当前方块的颜色值
             tetris_status[cur.Y][cur.X] = cur .Color
              
     }
         // 判断是否有可消除的行
         lineFull()
         // 开始一组新的方块
         initBlock()
 }
  
 // 获取缓存区的图片
 image = UIGraphicsGetImageFromCurrentImageContext()
 // 通知重绘
 self.setNeedsDisplay()
}

里面的代理更新UI(及分数和速度)我们就不多说了,说说 drawBlock() 这个方法,它是来绘制了我们在所有的方块,相当于把我们的互数据模型给全都可视化;

//MARK: 绘制俄罗斯方块的状态
   func drawBlock() -> Void {
        
       for i in 0..<TETRIS_Row {
            
           for j in 0..<TETRIS_Cols {
                
               if tetris_status[i][j] != NO_Block {
                    
                   // 设置填充颜色
                   CGContextSetFillColorWithColor(CTX, colors[tetris_status[i][j]])
                   CGContextFillRect(CTX, CGRectMake(CGFloat(j * CELL_Size + STROKE_Width),CGFloat(i * CELL_Size + STROKE_Width) , CGFloat(CELL_Size - STROKE_Width * 2), CGFloat(CELL_Size - STROKE_Width * 2)))
                    
               }
               else
               {
                
                   // 设置填充颜色
                   CGContextSetFillColorWithColor(CTX, UIColor.whiteColor().CGColor)
                   CGContextFillRect(CTX, CGRectMake(CGFloat(j * CELL_Size + STROKE_Width),CGFloat(i * CELL_Size + STROKE_Width) , CGFloat(CELL_Size - STROKE_Width * 2), CGFloat(CELL_Size - STROKE_Width * 2)))
                    
               }
           }
       }
   }

2:判断这行是否已满    

上面是让它下落了,里面有调用判断一行是否已满,其实这里的逻辑就是遍历每一行每一个方块,给你的每一行都加一个状态,这里是 true ,判断你该行的每一个方块的状态是不是初始化时候的 0  ,要是,那说明是缺方块的,这行没有满,跳出。。要是都不是,那就说明这行都满了。。就可以进行消除这行的后续操作了。增加积分,消除相应的行等,下面是它的代码。

// MARK: 判断是否有一行已满
    func lineFull() -> Void{
      // 遍历每一行
        for i in 0..<TETRIS_Row {
             
            var flag = true
            // 遍历每一行的每一个单元
            for j in 0..<TETRIS_Cols {
                 
                if tetris_status[i][j] == NO_Block {
                     
                    flag = false
                    break
                }
            }
            // 如果当前行已经全部有了方块
            if flag {
                 
                // 当前积分增加 100
                curScore += 100
                // 代理更新当前积分
                self.delegate.UpdateScore(curScore)
 
                if curScore >= curSpeed * curSpeed * 500{
                     
                    curSpeed += 1
                    // 代理更新当前速度
                    self.delegate.UpdateSpeed(curSpeed)
                    curTimer?.invalidate()
                    curTimer = NSTimer.scheduledTimerWithTimeInterval(BASE_Speed/Double(curSpeed), target: self, selector: #selector(self.movedown), userInfo: nil, repeats: true)
                }
                 
            }
            // 把所有的整体下移一行
            for var j = i; j < 0 ; j -= 1 {
                 
                for k in 0..<TETRIS_Cols {
                     
                    tetris_status[j][k] = tetris_status[j-1][k]
                     
                }
                 
            }
            // 播放消除的音乐
//            if !disBackGroundMusicPlayer.play() {
//               
//                disBackGroundMusicPlayer.play()
//            }
        }
    }

3.左移处理

它的处理方式和上面的下落的逻辑是一样的,也就是两点,到了最左边和左边有了两类型的情况,代码如下。

//MARK: 定义左边移动的方法
   func moveLeft () -> Void {
        
       // 定义左边移动的标签
       var canLeft = true
       for i in 0..<currentFall.count {
            
           if currentFall[i].X <= 0 {
                
               canLeft = false
               break
           }
           // 左变位置的前边一块
           if tetris_status[currentFall[i].Y][currentFall[i].X - 1] != NO_Block  {
                
               canLeft = false
               break
                
           }
       }
       // 如果可以左移
       if canLeft {
            
           self.drawBlock()
           // 将左移前的的每一个方块背景涂成白底
           for i in 0..<currentFall.count {
                
               let  cur = currentFall[i]
               CGContextSetFillColorWithColor(CTX, UIColor.whiteColor()
               .CGColor)
               CGContextFillRect(CTX, CGRectMake(CGFloat(cur.X * CELL_Size + STROKE_Width), CGFloat(cur.Y * CELL_Size + STROKE_Width), CGFloat(CELL_Size - STROKE_Width * 2), CGFloat(CELL_Size - STROKE_Width * 2)))
                
           }
            
           // 左移正字啊下掉的方块
           for i in 0..<currentFall.count {
                
               currentFall[i].X -= 1
                
           }
            
           // 将左移后的的每一个方块背景涂成对应的颜色
           for i in 0..<currentFall.count {
                
               let  cur = currentFall[i]
               CGContextSetFillColorWithColor(CTX,colors[cur.Color])
               CGContextFillRect(CTX, CGRectMake(CGFloat(cur.X * CELL_Size + STROKE_Width), CGFloat(cur.Y * CELL_Size + STROKE_Width), CGFloat(CELL_Size - STROKE_Width * 2), CGFloat(CELL_Size - STROKE_Width * 2)))
                
           }
           // 获取缓冲区的图片
           image = UIGraphicsGetImageFromCurrentImageContext()
 
           // 通知重新绘制
           self.setNeedsDisplay()
       
       }
   }

4.右移处理

右边移动的处理情况几乎就和左边的完全相同了,见代码:

// MARK: 定义右边移动的方法
   func moveRight () -> Void {
        
       // 能否右移动的标签
       var canRight = true
       for i in 0..<currentFall.count {
            
           // 如果已经到最右边就不能再移动
           if currentFall[i].X >= TETRIS_Cols - 1 {
                
               canRight = false
               break
           }
           // 如果右边有方块,就不能再移动
           if tetris_status[currentFall[i].Y][currentFall[i].X + 1] != NO_Block {
                
               canRight = false
               break
           }
       }
       // 如果能右边移动
       if canRight {
            
           self.drawBlock()
           // 将香油移动的每个方块涂白色
           for i in 0..<currentFall.count {
                
               let cur = currentFall[i]
               CGContextSetFillColorWithColor(CTX, UIColor.whiteColor().CGColor)
               CGContextFillRect(CTX, CGRectMake(CGFloat(cur.X * CELL_Size + STROKE_Width), CGFloat(cur.Y * CELL_Size + STROKE_Width), CGFloat(CELL_Size - STROKE_Width * 2), CGFloat(CELL_Size - STROKE_Width * 2)))
                
           }
       }
       // 右边移动正在下落的所有的方块
       for i in 0..<currentFall.count {
            
           currentFall[i].X += 1
            
       }
       // 有以后将每个方块的颜色背景图成各自方块对应的颜色
       for i in 0..<currentFall.count {
            
           let  cur = currentFall[i]
           // 设置填充颜色
           CGContextSetFillColorWithColor(CTX, colors[cur.Color])
           // 绘制矩形
           CGContextFillRect(CTX, CGRectMake(CGFloat(cur.X * CELL_Size + STROKE_Width), CGFloat(cur.Y * CELL_Size + STROKE_Width), CGFloat(CELL_Size - STROKE_Width * 2), CGFloat(CELL_Size - STROKE_Width * 2)))
            
           image = UIGraphicsGetImageFromCurrentImageContext()
           // 通知重新绘制
           self.setNeedsDisplay()
            
       }
   }

5.旋转处理 

旋转处理,就得用点数学知识了,你画一个坐标轴,试着把一个点顺时针或者逆时针旋转九十度,你再写出旋转后的坐标。其实清楚了这点也就OK了,我们是按逆时针旋转处理的,四个方块,就按照第三个作为它的旋转轴心。

// MARK: 定义旋转的方法
   func rotate () -> Void {
     
      // 定义是否能旋转的标签
       var canRotate = true
       for i in 0..<currentFall.count
       {
            
           let preX = currentFall[i].X
           let preY = currentFall[i].Y
           // 始终以第三块作为旋转的中心
           // 当 i == 2的时候,说明是旋转的中心
           if i != 2
           {
                
               // 计算方块旋转后的X,Y坐标
               let afterRotateX  =  currentFall[2].X + preY - currentFall[2].Y
               let afterRotateY  =  currentFall[2].Y + currentFall[2].X - preX
 
               // 如果旋转后的x,y坐标越界,或者旋转后的位置已有别的方块,表示不能旋转
               if afterRotateX < 0 || afterRotateX > TETRIS_Cols - 1 || afterRotateY < 0 || afterRotateY > TETRIS_Row - 1 || tetris_status[afterRotateY][afterRotateX] != NO_Block
               {
                    
                   canRotate = false
                   break
                    
               }
           }
       }
        
       // 如果能旋转
       if canRotate
       {
                
               self.drawBlock()
                
               for i in 0..<currentFall.count
               {
                    
                   let  cur = currentFall[i]
                   // 设置填充颜色
                   CGContextSetFillColorWithColor(CTX, UIColor.whiteColor().CGColor)
                   // 绘制矩形
                   CGContextFillRect(CTX, CGRectMake(CGFloat(cur.X * CELL_Size + STROKE_Width), CGFloat(cur.Y * CELL_Size + STROKE_Width), CGFloat(CELL_Size - STROKE_Width * 2), CGFloat(CELL_Size - STROKE_Width * 2)))
                    
               }
                
               for i in 0..<currentFall.count
               {
                    
                   let preX = currentFall[i].X
                   let preY = currentFall[i].Y
                    
                   // 始终第三个作为旋转中心
                   if i != 2
                   {                       
                       currentFall[i].X = currentFall[2].X + preY - currentFall[2].Y
                       currentFall[i].Y = currentFall[2].Y + currentFall[2].X - preX
                   }
               }
 
               for i in 0..<currentFall.count
               {
                    
                   let cur = currentFall[i]
                   CGContextSetFillColorWithColor(CTX, colors[cur.Color])
                   // 绘制矩形
                   CGContextFillRect(CTX, CGRectMake(CGFloat(cur.X * CELL_Size + STROKE_Width), CGFloat(cur.Y * CELL_Size + STROKE_Width), CGFloat(CELL_Size - STROKE_Width * 2), CGFloat(CELL_Size - STROKE_Width * 2)))
 
               }
 
               // 获取缓存区的图片
               image = UIGraphicsGetImageFromCurrentImageContext()
               // 通知重新绘制
               self.setNeedsDisplay()
                
           }
   }

启动游戏

做完了上面的工作,你就可以启动你的游戏了,你的做的工作就有下面这些:

1.重置游戏积分,将积分设置为 0

2.重置下落的速度,也将它设置为0

3.初始化俄罗斯方块的状态,将它们的值全都初始化为 0

4.生成一组在下落的方块组

5.启动计时器,控制下落的方块

// MARK:开始游戏
   func startGame()
   {
        
       self.curSpeed = 1
       self.delegate.UpdateSpeed(self.curSpeed)
        
       self.curScore = 0
       self.delegate.UpdateScore(self.curScore)
        
       // 初始化游戏状态
       self.initTetrisStatus()
        
       // 初始化四个正在下落的方块
       self.initBlock()
        
       // 定时器控制下落
       curTimer = NSTimer.scheduledTimerWithTimeInterval(BASE_Speed/Double(curSpeed), target: self, selector: #selector(self.movedown), userInfo: nil, repeats: true)
        
   }

PS:一张游戏运行图片

源码下载

https://github.com/TaoaXu/Tetris.git

赞助商