自己動手寫一個iOS2048游戲(超簡單思路版)

最近在網上下了一個仿2048游戲的Demo,發現里面的實現思路做得比較復雜:將數字塊的移動操作封裝成模型并保存起來,然后根據操作模型的值對滑塊逐塊地進行操作,具體的實現方式可以自己下下來感受一下。

然后我分析了一下這個游戲,重新整理了一種更簡單的實現思路,大體可以分為三步:

  • 界面布局
  • 數字塊操作
  • 按方向把所有數字塊分成4組,然后進行排序
  • 查找鄰近相同數字塊,計算該行(列)合并后的塊數
  • 按方向整行(列)同時移動數字塊
  • 移動塊的同時隨機添加數字塊
  • 游戲結束重置游戲

實現效果如下:

具體實現

界面布局

界面布局是最簡單的一步,主要分為三大塊:分數欄,游戲背景板和數字塊背景,以下是代碼:

private func setupUI() {
    
    let boardWH = SCREEN_WIDTH - boardLeftMargin * 2
    let gameBoard = ZYGameBoard(frame: CGRect(x: 0, y: 0, width: boardWH, height: boardWH))
    gameBoard.center = view.center
    view.addSubview(gameBoard)
    
    let cellWH = (gameBoard.frame.width - margin * CGFloat(cellRowCount + 1)) / CGFloat(cellRowCount)
    let scoreW = cellWH * 3
    let scoreH = cellWH * 0.9
    scoreView = UILabel(frame: CGRect(x: 0, y: 0, width: scoreW, height: scoreH))
    scoreView?.center = CGPoint(x: SCREEN_WIDTH / 2, y: gameBoard.frame.minY - margin * 2 - scoreH / 2)
    scoreView?.font = UIFont.boldSystemFont(ofSize: 20)
    scoreView?.textAlignment = .center
    scoreView?.textColor = UIColor.white
    scoreView?.backgroundColor = UIColor.gray
    scoreView?.text = "SCORE:0"
    scoreView?.layer.cornerRadius = 8
    scoreView?.clipsToBounds = true
    view.addSubview(scoreView!)
}

這里ZYGameBoard是一個自定義View,在自定義View是里面添加了數字塊的背景還有所有的數字塊移動邏輯,以下是ZYGameBoard的界面布局:

self.backgroundColor = UIColor.black
self.layer.cornerRadius = 8
self.clipsToBounds = true

for index in 0..<cellRowCount * cellRowCount {

    let row = index / cellRowCount
    let column = index % cellRowCount
    
    let cellBgView = UIView(frame: tileCellFrame(row: row, column: column))
    cellBgView.backgroundColor = UIColor.darkGray
    cellBgView.layer.cornerRadius = 5
    cellBgView.clipsToBounds = true
    self.addSubview(cellBgView)   
}

然后,還需要定義數據塊的自定義View:

class ZYTileCell: UILabel {
    var isCombined = false
    var tilePath : ZYTilePath
    let cellID : Int
        
    init(indexPath: ZYTilePath, cellID : Int) {
        self.tilePath = indexPath
        self.cellID = cellID
        let cellWH = (SCREEN_WIDTH - boardLeftMargin * 2 - margin * CGFloat(cellRowCount + 1)) / CGFloat(cellRowCount)
        let cellX = CGFloat(indexPath.column) * (cellWH + margin) + margin
        let cellY = CGFloat(indexPath.row) * (cellWH + margin) + margin
        super.init(frame: CGRect(x: cellX, y: cellY, width: cellWH, height: cellWH))
        self.font = UIFont.boldSystemFont(ofSize: 25)
        self.backgroundColor = UIColor.clear
        self.textColor = UIColor.rgbColor(red: 119, green: 110, blue: 100)
        self.layer.cornerRadius = 5
        self.clipsToBounds = true
        self.textAlignment = .center
    }
}

其中省略了不同數字塊顏色的定義,具體的可以看本文的源代碼,見文末。
至此,界面布局部分就完成了。

數字塊操作

這部分為整個游戲的核心部分,在這部分之前,需要對4個方向添加不同的swipe手勢,每個手勢添加不同的響應方法,這里略過了,以下具體講響應方法的具體實現:

按方向把所有數字塊分成4組,然后進行排序

var cellLines = [[ZYTileCell]]()
for _ in 0..<4 {
    cellLines.append([ZYTileCell]())
}
    
for tileCell in tileCells {
    for index in 0..<cellLines.count {
        if ((direction == .up || direction == .down) && tileCell.tilePath.column == index) || ((direction == .left || direction == .right) && tileCell.tilePath.row == index) {
            cellLines[index].append(tileCell)
        }
    }
}
var sortWay : (ZYTileCell, ZYTileCell) -> Bool
switch direction {
case .up:
    sortWay = { (cell1, cell2) -> Bool in
        return cell1.tilePath.row < cell2.tilePath.row
    }
    break
case .down:
    sortWay = { (cell1, cell2) -> Bool in
        return cell1.tilePath.row > cell2.tilePath.row
    }
    break
case .left:
    sortWay = { (cell1, cell2) -> Bool in
        return cell1.tilePath.column < cell2.tilePath.column
    }
    break
case .right:
    sortWay = { (cell1, cell2) -> Bool in
        return cell1.tilePath.column > cell2.tilePath.column
    }
    break
}
var combinedCellLines = [[ZYTileCell]]()
isAnyLineSameNumExist = false
for index in 0..<cellLines.count {
    let sortedLine = cellLines[index].sorted(by: sortWay)
    cellLines[index] = sortedLine
    combinedCellLines.append(findSameNumAndCombine(needMove: needMove, sortedCells: sortedLine))
    if isSameNumExist {
        isAnyLineSameNumExist = true
    }
}

查找鄰近相同數字塊,計算該行(列)合并后的塊數

接下來把排序后的數組相鄰的塊進行比較,相同的進行合并,把需要刪除的數字塊從數組移除并執行移除動畫:

private func findSameNumAndCombine(needMove: Bool, sortedCells: [ZYTileCell]) -> [ZYTileCell] {
    if sortedCells.count < 2 { return sortedCells}
    isSameNumExist = false
    var newTileCells = [ZYTileCell]()
    for tileCell in sortedCells {
        newTileCells.append(tileCell)
    }
    var combinedIndex = 1000
    for (cellIndex, cell) in sortedCells.enumerated() {
        cell.isCombined = false
        if cellIndex + 1 < sortedCells.count && cellIndex != combinedIndex + 1 {
            if sortedCells[cellIndex].num == sortedCells[cellIndex + 1].num {
                if needMove {
                    cell.num = cell.num! * 2
                    if isSameNumExist {
                        newTileCells.remove(at: cellIndex)
                    }else {
                        newTileCells.remove(at: cellIndex + 1)
                    }
                    let extraCell = sortedCells[cellIndex + 1]
                    UIView.animate(withDuration: 0.1, animations: {
                        extraCell.alpha = 0
                    }, completion: { (_) in
                        extraCell.removeFromSuperview()
                    })
                    var index = 0
                    for tileCell in tileCells {
                        if extraCell.cellID == tileCell.cellID {
                            break
                        }
                        index += 1
                    }
                    if index < tileCells.count {
                        tileCells.remove(at: index)
                    }
                    cell.isCombined = true
                    if scoreChanged != nil {
                        scoreChanged!(cell.num!)
                    }
                }
                combinedIndex = cellIndex
                isSameNumExist = true
            }
        }
    }
    return newTileCells
}

在上面的方法里有一個細節就是如果所有的數字塊滿了,需要挨個模擬各個方向的滑動,在這個方法里判斷是否有可以合并的數字塊,如果有游戲繼續,否則游戲結束。

按方向整行(列)同時移動數字塊

接下來就可以真正地開始移動數字塊了,移動數字塊的思路就是計算出整行或者整列的數字塊移動后的最終位置,然后用最終位置和初始位置的差確定位置的距離和方向

思路雖然是這樣,但在寫代碼的時候要簡單得多,直接遍歷整行(整列的)排序后的數字塊,下標為最終位置,自身的坐標為初始位置,用這兩個值即可計算出位移,坐標軸如下:

實現代碼:

    private func moveTile(direction: ZYDirection, cellLines: [[ZYTileCell]], combinedCellLines: [[ZYTileCell]]) {
        for (lineIndex, lineCells) in cellLines.enumerated() {
            var needMoreStep = false
            for (index, originCell) in lineCells.enumerated() {
                
                let combinedCells = combinedCellLines[lineIndex]
                var toIndex = index
                var distance : CGFloat = 0
                var cellFrame = originCell.frame
                
                if toIndex > combinedCells.count - 1 {
                    toIndex = combinedCells.count - 1
                }
                var delta = 0
                if needMoreStep {
                    delta = 1
                }
                
                switch direction {
                case .up:
                    distance = CGFloat(toIndex - originCell.tilePath.row - delta) * (originCell.frame.height + margin)
                    cellFrame.origin.y += distance
                    originCell.tilePath = ZYTilePath(row: toIndex, column: lineIndex)
                    break
                case .down:
                    toIndex = 3 - toIndex
                    distance = CGFloat(toIndex - originCell.tilePath.row + delta) * (originCell.frame.height + margin)
                    cellFrame.origin.y += distance
                    originCell.tilePath = ZYTilePath(row: toIndex, column: lineIndex)
                    break
                case .left:
                    distance = CGFloat(toIndex - originCell.tilePath.column - delta) * (originCell.frame.height + margin)
                    cellFrame.origin.x += distance
                    originCell.tilePath = ZYTilePath(row: lineIndex, column: toIndex)
                    break
                case .right:
                    toIndex = 3 - toIndex
                    distance = CGFloat(toIndex - originCell.tilePath.column + delta) * (originCell.frame.height + margin)
                    cellFrame.origin.x += distance
                    originCell.tilePath = ZYTilePath(row: lineIndex, column: toIndex)
                    break
                }
                needMoreStep = originCell.isCombined
                if distance != 0 {
                    UIView.animate(withDuration: 0.1, animations: {
                        originCell.frame = cellFrame
                    }, completion: { (_) in
                        if originCell.isCombined == false { return }
                        originCell.transform = CGAffineTransform(scaleX: 0.8, y: 0.8)
                        UIView.animate(withDuration: 0.5, delay: 0.1, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.5, options: UIViewAnimationOptions(rawValue: 0), animations: {
                            originCell.transform = CGAffineTransform.identity
                        }, completion: nil)
                    })
                }
            }
        }
    }

上面的代碼對該行存在合并數字塊的情況進行了處理:如果該行有數字塊合并了(即保留前面一塊,移除后面一塊),后面所有塊都會多移一步,避免中間出現空白。

至此,移動數字塊部分也完成了,接下來就是隨機添加數字塊了。

移動塊的同時隨機添加數字塊

這步應該是數字塊操作里面最簡單的一步,從所有可能的坐標數組中刪除有數字塊的元素,剩下都是沒有數字塊的坐標,在這些坐標中隨機選一個添加數字塊即可

private func addRandomTileCell() {
        
    var tilePaths = [ZYTilePath]()
    for index in 0..<cellRowCount * cellRowCount {
        let row = index / cellRowCount
        let column = index % cellRowCount
        tilePaths.append(ZYTilePath(row: row, column: column))
    }
    if tileCells.count > 0 {
        for tileCell in tileCells {
            for (index, tilePath) in tilePaths.enumerated() {
                if tileCell.tilePath.row == tilePath.row && tileCell.tilePath.column == tilePath.column {
                    tilePaths.remove(at: index)
                }
            }
        }
    }
    let randomTilePath = tilePaths[Int(arc4random_uniform(UInt32(tilePaths.count) - 1))]
    let tileCell = ZYTileCell(indexPath: randomTilePath, cellID: currentID)
    currentID += 1
    tileCell.num = arc4random() % 3 == 0 ? 4 : 2
    tileCells.append(tileCell)
    tileCell.frame = tileCellFrame(row: randomTilePath.row, column: randomTilePath.column)
    addSubview(tileCell)
    
    tileCell.transform = CGAffineTransform(scaleX: 0.2, y: 0.2)
    UIView.animate(withDuration: 0.5, delay: 0.1, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.5, options: UIViewAnimationOptions(rawValue: 0), animations: {
        tileCell.transform = CGAffineTransform.identity
    }, completion: nil)
    
}

當然,在添加數字塊的同時,如果剩余空坐標為0,那么就要進行游戲是否結束的驗證,如果游戲結束,就以閉包的形式在控制器進行彈窗操作:

// 判斷游戲是否結束
if tilePaths.count == 0 {
    swipeAction(direction: .up,    needMove: false)
    let upSameNumExist = isAnyLineSameNumExist
    swipeAction(direction: .down,  needMove: false)
    let downSameNumExist = isAnyLineSameNumExist
    swipeAction(direction: .left,  needMove: false)
    let leftSameNumExist = isAnyLineSameNumExist
    swipeAction(direction: .right, needMove: false)
    let rightSameNumExist = isAnyLineSameNumExist
    if gameOver != nil && !(upSameNumExist || downSameNumExist || leftSameNumExist || rightSameNumExist) {
        gameOver!()
    }
    return
}
// 如果游戲結束,在控制器進行操作
gameBoard.gameOver = {
    let alertContr = UIAlertController(title: "提示", message: "游戲失敗,請重試", preferredStyle: .alert)
    let confirmAction = UIAlertAction(title: "確定", style: .default, handler: { (alertAction) in
        gameBoard.resetGame()
        self.score = 0
    })
    alertContr.addAction(confirmAction)
    self.present(alertContr, animated: true, completion: nil)
}

游戲結束重置游戲

接下來就是最后一步,重置游戲數據:

func resetGame() {
    for tileCell in self.tileCells {
        tileCell.removeFromSuperview()
    }
    self.tileCells.removeAll()
}

好了,大功告成,現在可以裝到手機上玩兒一把了。

看本文源代碼點這里

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,663評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,125評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,506評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,614評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,402評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,934評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,021評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,168評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,690評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,596評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,288評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,027評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,404評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,662評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,398評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,743評論 2 370

推薦閱讀更多精彩內容