最近在網上下了一個仿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()
}
好了,大功告成,現在可以裝到手機上玩兒一把了。
看本文源代碼點這里