Swift 简单消消乐demo

游戏规则:

  • 点击某一方块,当该方块的上下左右四个方向同颜色方块可连续(大于等于2)即可消除。
  • 方块消除后,上面的方块往下掉。
  • 中间整列都空的话,旁边的列往中间靠拢。
  • 如果没有可消除的方块,游戏结束。

效果如下:

效果图

流程:

  1. 使用随机方法产生方块的颜色,然后创建背景色与之相对应的方块。
  2. 判断游戏是否结束。
  3. 用户点击方块,判断该方块的上下左右方向上是否存在同颜色的方块。有,把方块保存起来,进入第四步,没有,不做任何响应,并等待用户点击。
  4. 清除方块,向下移动方块。存在中间某列空了,两侧的列往中间移动。跳转到第二步。
  5. 用户点击重新开始按钮,跳转到第一步。

实现:

先来看一下demo的文件结构:


文件结构

其中:TwoDimentionalBrain是demo的逻辑处理,DiamondsImageView是继承UIImageView, 是带有点击功能的方块。

DiamondsImageView 类

class DiamondsImageView: UIImageView {
var backgroundType : backgroundType = .clear
var itemIndex : Int = 0

//起始位置和将要移动时的位置,动画效果
var currentLocation : CGPoint?
var toLocation : CGPoint?

typealias returnIndexAndType = (Int, backgroundType) -> ()
var returnTuple : returnIndexAndType?
}

方块在demo中是使用一个二维数组存储,itemIndex代表该数组的index(i * row + column), backgroundType表示该方块的颜色值。

enum backgroundType {
case yellow
case blue
case red
case green
case clear
}

而returnTuple是一个block, 主要是在点击方块时,在单击响应方法里将该方块的的itemIndex和backgroundType传递给viewController.

func touchInside(_ sender: UITapGestureRecognizer) {
    //点击空白方块,则不响应
    if backgroundType == .clear {
        print("不能点击")
        return
    }
    
    //点击颜色方块,将方块信息传递给viewController
    if let tuple = returnTuple {
        tuple(itemIndex, backgroundType)
    }
    }

TwoDimentionalBrain 结构体

struct TwoDimentionalBrain {
//存储方块颜色
private var sourceDataArray = [[backgroundType]]()
//存储消除方块的单个分数值(count, value)
private let scoreArray = [(0, 5), (5, 8), (10, 10), (13, 12), (15, 13), (100, 15)]
//存储需要清除的方块 1:清除, 0:不清除
private var clearArray = [(Int, Int)]()
//存储需要移动的方块列
private var emptyColumnArray = Array<Int>(repeating: 0, count: ColumnCount)
//存储游戏分数
var score = 0;

//同列需要往下掉的方块,传递给viewController
typealias exchangeRowInColumn = (Int, Int, Int) -> ()
var itemMoveDown: exchangeRowInColumn?

//需要整列移动的方块,传递给viewController
typealias exchangeColumn = (Int, Int) -> ()
var itemChangeColumn: exchangeColumn?
}

随机产生一行的颜色排列,返回的数组添加到sourceDataArray里面去。

private mutating func setOneArray() -> [backgroundType] {
    var array = [backgroundType]()
    for _ in 0..<ColumnCount {
        let data = arc4random() % 4
        
        switch data {
        case 0:
            array.append(.yellow)
        case 1:
            array.append(.blue)
        case 2:
            array.append(.red)
        case 3:
            array.append(.green)
        default:
            array.append(.clear)
        }
    }
    
    return array
    }

mutating func setSourceDataArray() {
    //先清空
    sourceDataArray = [[backgroundType]]()
    
    for _ in 0..<RowCount {
        let array = setOneArray()
        
        sourceDataArray.append(array)
    }
   }

用递归方法寻找点击方块的上下左右同颜色的方块,用寻找左侧方向来示例:
首先需要判断当前方块的左侧方块颜色是否一致。一致,则继续在该方向寻找;否则,该方向的寻找结束。

private mutating func findSameTypeWithRound(row: Int, column: Int) {
    let isLeft = isLeftSame(row: row, column: column)
    //如果颜色一致,则继续往左边寻找相同颜色的方块
    if isLeft {
        findSameTypeWithRound(row: row, column: column - 1)
    }
    }

判断当前方块的左侧方块颜色是否一致的方法如下:

private mutating func isLeftSame(row: Int, column: Int) -> Bool {
    if column <= 0 || isVisited(row: row, column: column - 1) {
        //如果已经是最左边或者已经访问过了
        return false
    }
    
    if sourceDataArray[row][column - 1] == .clear {
        //如果已经是空白方块
        return false
    }
    
    if sourceDataArray[row][column - 1] == sourceDataArray[row][column] {
        //左侧方块和当前方块的颜色一致,则将左侧方块的行与列坐标添加到clearArray
        clearArray.append((row, column - 1))
        return true
    }
    
    return false
    }

其他方向的寻找类似,就不多陈述了。如果颜色一致,则添加到clearArray,在四个方向都寻找结束之后,根据clearArray的数据计算分数和对方块进行消除。

if clearArray.count < 2 {
        clearArray = [(Int, Int)]()
        return;
    }
    
    for (row, column) in clearArray {  
        //将需要消除的方块的颜色设置为透明
        sourceDataArray[row][column] = .clear
    }
    
    //计算分数
    let count = clearArray.count
    
    for (item, value) in scoreArray {
        if count >= item {
            score += value * count;
            break;
        }
    }

方块往下掉:遍历sourceDataArray数组,如果方块的颜色不是.clear 就在该方块所在列的下一行递归寻找.clear的方块,直到碰到有颜色的方块或者是边界。使用count存储两者的行数的间隔:

private func getUnClearUpCount(row: Int, column: Int) -> Int {
    if row < 0 {
        //遇到边界
        return -1
    }
    
    var count = 0
    //遍历该列下方的所有方块
    for index in 0...row {
        if sourceDataArray[row - index][column] == .clear {
            //遇到空白方块,则间隔+1
            count = count + 1
        }
        else {
            //遇到有颜色的方块,则返回
            return count
        }
    }
    
    return count
    }

private mutating func moveDown() {
    for row in 0..<RowCount {
        for column in 0..<ColumnCount {
            if sourceDataArray[row][column] != .clear {
                //寻找同列的下方是否有空的方块可以进行移动
                let count = getUnClearUpCount(row: row - 1, column: column)
                
                if count > 0 {
                    //有,进行移动。
                    sourceDataArray[row - count][column] = sourceDataArray[row][column]
                    sourceDataArray[row][column] = .clear
                    
                    if let itemMD = itemMoveDown {
                        //传递给viewController itemMD(所在列, 原来的行, 需要移动到的行)
                        itemMD(column, row, row - count)
                    }
                }
            }
        }
    }
    }

如果消除了之后发现某列空了,则需要判断两侧方块是否需要整列往中间移动。使用二分法,从中间let centerColumn = ColumnCount / 2 分界。左边部分:从centerColumn ~ 0 进行遍历,如果当前列不空,并且右侧有空的,则整列向右移动;右边部分:从centerColumn + 1 到右侧边界ColumnCount。如果当前列不空,并且左侧有空的,则整列往左移动。

let centerColumn = ColumnCount / 2
    //存储需要移动两列的列数间隔
    var count = 0
    for column in centerColumn + 1..<ColumnCount {
        if emptyColumnArray[column] == 1 {
            //如果是空的话,就加一
            count += 1
        }
        else if (count > 0) {
            //如果当前列不空,并且左侧有空的,则整列移动
            for i in 0..<ColumnCount - centerColumn {
                if column + i < ColumnCount {
                    moveColumnToAnother(fromColumn: column + i, toColumn: column - count + i)
                }
            }
            
            count = 0
        }
        else {
            //当前列不空,并且左侧没有空的,则什么都不做
        }

整列移动的方法如下:

private mutating func moveColumnToAnother(fromColumn: Int, toColumn: Int) {
    for row in 0..<sourceDataArray.count {
        sourceDataArray[row][toColumn] = sourceDataArray[row][fromColumn]
        sourceDataArray[row][fromColumn] = .clear
        
        emptyColumnArray[fromColumn] = 1
        emptyColumnArray[toColumn] = 0
        
        //传递给viewController
        if let changeColumn = itemChangeColumn {
            //changeColumn(当前的列, 移动后的列)
            changeColumn(fromColumn, toColumn)
        }
    }
    }

遍历所有的方块,如果没有连续的同颜色方块,则游戏结束。

 mutating func isGameOver() -> Bool {
    //判断是否已经结束游戏了
    for row in 0..<RowCount {
        for column in 0..<ColumnCount {
            if sourceDataArray[row][column] == .clear {
                //遇到空白方块,不执行下面的内容,继续下一次循环
                continue;
            }
            
            //先清空,并把当前的方块添加到消除数组
            clearArray = [(Int, Int)]()
            clearArray.append((row, column))
            //在四个方向上寻找同颜色的方块
            findSameTypeWithRound(row: row, column: column)
            
            if clearArray.count >= 2 {
                //存在连续的同颜色方块,游戏继续
                return false
            }
        }
    }
    
    return true
}

viewController

//显示游戏得分
@IBOutlet var scoreLabel: UILabel!
//存储方块的数组
private var imageArray = [[DiamondsImageView]]()
//游戏逻辑的引用
private var diamondsBrain = TwoDimentionalBrain()

在页面加载完成时,创建随机颜色的方块。并且实现TwoDimentionalBrain结构体中两个移动方块的block。

override func viewDidLoad() {
    super.viewDidLoad()
    //获取随机方块颜色
    diamondsBrain.setSourceDataArray()
    
    //方块移动的实现方法
    weak var weakSelf = self
    diamondsBrain.itemMoveDown = {(column, fromRow, toRow) in
        weakSelf?.imageViewMoveDown(column: column, fromRow: fromRow, toRow: toRow)
    }
    diamondsBrain.itemChangeColumn = {(fromColunm, toColumn) in
        weakSelf?.imageViewExchangeColumn(fromColumn: fromColunm, toColumn: toColumn)
    }
    
    //创建方块
    createImageView()
    
    if diamondsBrain.isGameOver() {
        print("Game Over!")
    }
    }

//同列的两个方块进行交换
private func imageViewMoveDown(column: Int, fromRow: Int, toRow: Int) {
    let fromImage = imageArray[fromRow][column]
    let toImage = imageArray[toRow][column]
    
    exchangeImage(fromImage: fromImage, toImage: toImage)
    
    imageArray[fromRow][column] = toImage
    imageArray[toRow][column] = fromImage
}

//交换两列的方块
private func imageViewExchangeColumn(fromColumn: Int, toColumn: Int) {
    for row in 0..<RowCount {
        if imageArray[row][fromColumn].backgroundType == .clear {
            return
        }
        else {
            let fromImage = imageArray[row][fromColumn]
            let toImage = imageArray[row][toColumn]
            exchangeImage(fromImage: fromImage, toImage: toImage)
            
            imageArray[row][fromColumn] = toImage
            imageArray[row][toColumn] = fromImage
        }
    }
}

使用UIView动画,展示两个方块交换的过程(已经清楚的方块设置成空白,所以只看到有颜色的方块在移动)。

 private func exchangeImage(fromImage: DiamondsImageView, toImage: DiamondsImageView) {
    UIView.animate(withDuration: 0.2, animations: {
        let origin = fromImage.frame.origin
        fromImage.frame.origin = toImage.frame.origin
        toImage.frame.origin = origin
        
        })
    
    let index = fromImage.itemIndex
    fromImage.itemIndex = toImage.itemIndex
    toImage.itemIndex = index
    }

创建方块时,根据diamondsBrain的soureDataArray的存储元素决定方块的颜色,而itemIndex由数组的行和列决定 itemIndex = row * ColumnCount + column,并且接收方块点击的block,进行相关的处理。

private func createImageView() {
    //根据sourceDataArray的颜色创建方块
    let dataArray = diamondsBrain.getSourceArray()

    for (row, itemArray) in dataArray.enumerated() {
        var rowImageArray = [DiamondsImageView]()
        
        for (column, item) in itemArray.enumerated() {
            let originX = space + CGFloat(column) * (width + ImageSpace)
            let originY = height - CGFloat(row + 1) * (width + ImageSpace)
            let rect = CGRect(x: originX, y: originY, width: width, height: width)
            let imageView = DiamondsImageView(frame: rect)
            imageView.backgroundType = item
            imageView.itemIndex = row * ColumnCount + column
            
            weak var weakSelf = self
            imageView.returnTuple = {(index, type) in
                let column = index % ColumnCount
                let row = index / ColumnCount
                weakSelf?.clearItem(row: row, column: column)
            }
            
            rowImageArray.append(imageView)
            self.view.addSubview(imageView)
        }
        
        imageArray.append(rowImageArray)
    }
    
    updateUI()
    }

在用户点击了方块之后,根据传过来的itemIndex确定点击的方块的行和列,然后调用diamondsBrain.getClearItem方法消除方块。消除方块之后再判断游戏是否结束。

private func clearItem(row: Int, column: Int) {
    diamondsBrain.getClearItem(row: row, column: column)
    let dataArray = diamondsBrain.getSourceArray()
    
    for (row, itemArray) in dataArray.enumerated() {
        let rowImageArray = imageArray[row]
        
        for (column, item) in itemArray.enumerated() {
            rowImageArray[column].backgroundType = item
        }
    }
    
    updateUI()
    
    if diamondsBrain.isGameOver() {
        print("Game Over!")
    }
    }

根据方块backgroundTyped对方块的背景颜色进行赋值

private func updateUI() {
    let score = diamondsBrain.score;
    scoreLabel.text = String.init(stringInterpolationSegment: score)
    
    for (_, itemArray) in imageArray.enumerated() {
        for (_, item) in itemArray.enumerated() {
            switch item.backgroundType {
            case .green:
                item.backgroundColor = UIColor.green
            case .red:
                item.backgroundColor = UIColor.red
            case .blue:
                item.backgroundColor = UIColor.blue
            case .yellow:
                item.backgroundColor = UIColor.yellow
            case .clear:
                item.backgroundColor = UIColor.clear
            }
        }
    }
    }

重新开始游戏时,使用随机方法产生方块颜色,然后对ImageArray的方块的背景色进行赋值。

@IBAction func resetGame(_ sender: UIButton) {
    diamondsBrain.setSourceDataArray()
    diamondsBrain.score = 0;
    
    let dataArray = diamondsBrain.getSourceArray()
    
    for (row, itemArray) in dataArray.enumerated() {
        var rowImageArray = imageArray[row]
        
        for (column, item) in itemArray.enumerated() {
            let originX = space + CGFloat(column) * (width + ImageSpace)
            let originY = height - CGFloat(row + 1) * (width + ImageSpace)
            let rect = CGRect(x: originX, y: originY, width: width, height: width)
            let imageView = rowImageArray[column]
            imageView.frame = rect
            imageView.backgroundType = item
            imageView.itemIndex = row * ColumnCount + column
            
            rowImageArray[column] = imageView
        }
        
        imageArray[row] = rowImageArray
    }

    updateUI()
    }

感觉都在用代码说话,包涵一下,贴个demo放在百度云ClearGame 喜欢的可以去下载。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 222,183评论 6 516
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,850评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 168,766评论 0 361
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,854评论 1 299
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,871评论 6 398
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,457评论 1 311
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,999评论 3 422
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,914评论 0 277
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,465评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,543评论 3 342
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,675评论 1 353
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,354评论 5 351
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 42,029评论 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,514评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,616评论 1 274
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 49,091评论 3 378
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,685评论 2 360

推荐阅读更多精彩内容