Swift 實現俄羅斯方塊詳細思路解析(附完整項目)

一:寫在開發前git

    俄羅斯方塊,是一款咱們小時候都玩過的小遊戲,我本身也是看着書上的思路,學着用 Swift 來寫這個小遊戲,在寫這個遊戲的過程當中,除了一些位置的計算,數據模型和理解 Swift 語言以外,最好知道UIKIt框架中的 Quartz2D 這個知識點。是我在簡書上面找的,是關於 Quartz2D 這個知識點的,看它我以爲也就夠學習。通過這兩天的整理,充分以爲在寫這些以前,必定要理清楚思路,你可能會花不少時間在它上面,你要知道了,怎麼寫就變的反而簡單了。github

二:具體開發思路及主要代碼編程

    我在博客的最下面附上了完整的代碼,你們能夠在Git上下載到它,你要也使用Git,就順便給我個小星星吧 O(∩_∩)O哈哈~。。swift

  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》小遊戲的數據模型dom

        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:一張遊戲運行圖片

 

四:寫在開發後

       差很少到這裏也就結束了,但裏面有一個BUG,有些時候會發生一個數組的越界致使的崩潰,這個問題有時間在好好看一下,本身寫的裏面可能還有我不知道的問題,也沒作大量的測試,感興趣的朋友能夠本身好好完善一下,好比試試暫停,從新開始這些功能的。。反正確定還有寫的很差的地方,有問題你們能夠發消息隨時交流!!

 

 

    寫完了,說點無聊的,說說本身😄,其實在大學的時候,我打死也不可能相信本身未來會走上編程這條路,一個大一連C語言都掛科不懂得人。如今想一想真的就是着實蛋疼。要是那時候上帝給我說一句,你未來要會是一個開發軟件的,我必定以爲是上帝瘋了。可轉眼工做也一年多了,慢慢的,我喜歡上了本身作的事,至少我本身以爲挺好的。工做第一,但也得給本身充充電,天天敲着代碼 PS:還有加着班,但內心踏實。沒有爲碌碌無爲,荒廢一天又一天感到不安!難道有什麼比你內心踏實更重要的麼,固然你要是有鴻鵠之志,額~~~你仍是得充電呀,,O(∩_∩)O哈哈~

   最後就是完整代碼。。Git地址給你們。。。點擊下載 Swift俄羅斯方塊完整代碼

相關文章
相關標籤/搜索