游戏规则:
- 点击某一方块,当该方块的上下左右四个方向同颜色方块可连续(大于等于2)即可消除。
- 方块消除后,上面的方块往下掉。
- 中间整列都空的话,旁边的列往中间靠拢。
- 如果没有可消除的方块,游戏结束。
效果如下:
流程:
- 使用随机方法产生方块的颜色,然后创建背景色与之相对应的方块。
- 判断游戏是否结束。
- 用户点击方块,判断该方块的上下左右方向上是否存在同颜色的方块。有,把方块保存起来,进入第四步,没有,不做任何响应,并等待用户点击。
- 清除方块,向下移动方块。存在中间某列空了,两侧的列往中间移动。跳转到第二步。
- 用户点击重新开始按钮,跳转到第一步。
实现:
先来看一下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 喜欢的可以去下载。