首页
登录 | 注册

Swift 实现俄罗斯方块详细思路解析(附完整项目)

一:写在开发前

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

二:具体开发思路及主要代码

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

  1》游戏界面的布局设计

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

    Swift 实现俄罗斯方块详细思路解析(附完整项目)

 

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

 // 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()
                
            }
    }

三:启动游戏

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

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

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

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

    生成一组在下落的方块组

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

 // 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:一张游戏运行图片

Swift 实现俄罗斯方块详细思路解析(附完整项目)

 

四:写在开发后

       差不多到这里也就结束了,但里面有一个BUG,有些时候会发生一个数组的越界导致的崩溃,这个问题有时间在好好看一下,自己写的里面可能还有我不知道的问题,也没做大量的测试,感兴趣的朋友可以自己好好完善一下,比如试试暂停,重新开始这些功能的。。反正肯定还有写的不好的地方,有问题大家可以发消息随时交流!!

Swift 实现俄罗斯方块详细思路解析(附完整项目)

 

 

    写完了,说点无聊的,说说自己


相关文章

  • Dom4J配合XPath解析schema约束的xml配置文件问题
    如果一个xml文件没有引入约束,或者引入的是DTD约束时,那么使用dom4j和xpath是可以正常解析的,不引入约束的情况本文不再展示. 引入DTD约束的情况 mybook.dtd: <?xml version="1.0&q ...
  • 前言:今天在做一个小项目时,客户要求的xml,跟现在有系统要求的不一样,所以要自己重新写函数支持返回,进行简单总结,希望对大家有所帮助. 首先,使用xml函数需要链上动态库libxml2,需要在电脑上安装libxml的开发包,安装方法如下: ...
  • SpringBoot集成Lombok,应用+源码解析,让代码优雅起来
    一.Lombok简介 (1)Lombok官网(https://projectlombok.org/)对lombok的介绍 (2)GitHub项目地址:https://github.com/rzwitserloot/lombok 虽然是生硬的 ...
  • 学了很多乱七杂八的东西,但是依然停留在前端,在工作中一直和后端交流,但是不太了解数据库是怎么回事,为了加强学习,准备学习一些关于数据库相关的东西. 说起数据库可能会有很多很多,SQLServer.Oracle.Sybase等等等,还有就是要 ...
  • ERP不规范,同事两行泪
    最近的很多次对外交流,都聊到了ERP建设的话题,并且无一例外的不那么让人省心,回想我这么多年走过的ERP坑坑路,在这里也写下经验和总结,希望能给正在或者即将走上ERP建设路的企业一些思考和帮助. 导读 1.几个瞎眼而普遍的案例 2.ERP的 ...
  • Android6.0 源码修改之 Contacts应用
    一.Contacts应用的主界面和联系人详情界面增加顶部菜单添加退出按钮 通过Hierarchy View 工具可以发现 主界面对应的类为 PeopleActivity 联系人详情界面对应的类为 QuickContactActivity 左 ...

2019 cecdns.com webmaster#cecdns.com
12 q. 0.074 s.
京ICP备10005923号