自己动手写一个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阅读 159,458评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,454评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,171评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,062评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,440评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,661评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,906评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,609评论 0 200
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,379评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,600评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,085评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,409评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,072评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,088评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,860评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,704评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,608评论 2 270

推荐阅读更多精彩内容