開(kāi)源項(xiàng)目Swift-2048學(xué)習(xí)萨西、分析
[TOC]
這篇博客寫了什么如输?
剛開(kāi)始使用swift編寫ios程序鼓黔,花了兩三天的時(shí)間看了下《The Swift Programming Language》,看了就忘了o(╯□╰)o不见,于是干脆從一個(gè)項(xiàng)目入手澳化,一邊開(kāi)發(fā)一邊學(xué)習(xí)。github上面找了一個(gè)挺有名的開(kāi)源項(xiàng)目[swift-2048][id],經(jīng)過(guò)一天半的"刻苦"學(xué)習(xí)脖祈,終于略有小成肆捕。
[id]: https://github.com/austinzheng/swift-2048 "gitHub"
項(xiàng)目結(jié)構(gòu)
除去xocde7自動(dòng)生成的一些文件外,austinzheng(2048項(xiàng)目作者),一共就使用7個(gè).swift文件完成了整個(gè)項(xiàng)目盖高,讓本人第一感覺(jué)這個(gè)項(xiàng)目挺簡(jiǎn)單的慎陵。接下來(lái)就簡(jiǎn)單的介紹一下每一個(gè)文件的大概用處。
-
GameModel
全工程最龐大的一個(gè)文件喻奥,在models文件夾下席纽,這個(gè)文件主要是算法的實(shí)現(xiàn)(移動(dòng)合并算法),雖然說(shuō)是model文件后綴,但是本人實(shí)在想不到這個(gè)文件和MVC中的model有什么關(guān)系撞蚕。
-
AuxiliaryModels
里面定義著本項(xiàng)目用到的所有的润梯、用戶自定義的結(jié)構(gòu)體與枚舉
-
AccessoryViews
本文件里面定義著代表分?jǐn)?shù)的view
-
GameboardView
和文件名稱一樣,這個(gè)文件是游戲的主要面板也就是下面這個(gè)
-
TileView
文件名也出賣了它甥厦,這個(gè)讓用戶看起來(lái)就是2048游戲中的那些可以移動(dòng)的數(shù)字纺铭。
-
NumberTileGame
游戲的主要控制器,幾乎所有的邏輯均在這里處理
-
AppearanceProvider
項(xiàng)目輔助功能刀疙,它決定著游戲中數(shù)字以及TileView的顏色
代碼分析
以文件為單位舶赔,對(duì)2048項(xiàng)目進(jìn)行一個(gè)簡(jiǎn)單的分析。
TileView.swift
2048項(xiàng)目里面較為簡(jiǎn)單的一個(gè)文件谦秧。TileView也就是2048中可以移動(dòng)的方塊
-
value 屬性
這個(gè)屬性代表著一個(gè)tileView上面所顯示的分?jǐn)?shù)的數(shù)值,并設(shè)置了一個(gè)屬性觀察器didSet,這個(gè)屬性觀察器在每一次value屬性被設(shè)置新值的時(shí)候調(diào)用竟纳。目的是為了在每一次值改變的時(shí)候給TileView的背景顏色、文字顏色疚鲤、以及TileView上顯示的Label數(shù)值锥累,這給3個(gè)屬性重新賦值(2048游戲中,TIleVIew數(shù)值的不同集歇,顏色是不一樣的,2,4,8的顏色都不一樣)
-
delegate 屬性
遵守AppearanceProviderProtocol協(xié)議的代理桶略,主要用于更改顏色
-
numberLabel 屬性
TileView上每個(gè)數(shù)字都是一個(gè)Label
-
構(gòu)造方法
無(wú)疑是對(duì)TileView本身進(jìn)行一些初始化,包括本身的大小以及UILabel的創(chuàng)建之類
AccessoryViews.swift
前面項(xiàng)目結(jié)構(gòu)以及分析了,這個(gè)文件主要負(fù)責(zé)獲得分?jǐn)?shù)的顯示.這個(gè)文件也十分的簡(jiǎn)單,里面主要的類就是 ScoreView它遵守了ScoreViewProtocol協(xié)議诲宇。因?yàn)楹?jiǎn)單际歼,所以不過(guò)多解釋.
-
score 屬性
游戲總共的得分,同樣也有一個(gè)didSet屬性觀察器,再每次score屬性發(fā)生改變的時(shí)候焕窝,更新Label的顯示
scoreChanged 方法
在每一次分?jǐn)?shù)改變的時(shí)候調(diào)用蹬挺,用于跟新分?jǐn)?shù)顯示(個(gè)人感覺(jué)這個(gè)方法和協(xié)議寫的有點(diǎn)多余)
AppearanceProvider.swift
一個(gè)輔助用的,主要用于TileView顏色的控制,簡(jiǎn)單不多解釋.(沒(méi)有這個(gè)文件提供的功能,項(xiàng)目一樣可以跑它掂,只是丑點(diǎn))
GameboardView.swift
一個(gè)稍稍復(fù)雜的文件巴帮,代表游戲面板的view,也就是下面這個(gè)黑框框(十分明顯的九宮格布局)虐秋。當(dāng)然還實(shí)現(xiàn)了一些對(duì)TileView的移動(dòng)榕茧、插入等操作。接下來(lái)只解釋一些主要屬性客给。
tiles 屬性
一個(gè)Dictionary<NSIndexPath, TileView>類型的數(shù)據(jù)用押,其實(shí)就是OC里面的一個(gè)字典,只是key換成了NSIndexPath類型靶剑,存儲(chǔ)的value是TileView蜻拨。所以這個(gè)字典里面存儲(chǔ)的是整個(gè)游戲中池充,所有的TIleView,至于使用NSIndexPath做為key的目的很明顯,能夠使用類似于(1,3)的方式便捷取出任意一個(gè)TileView缎讼。provider 屬性
控制顏色用的收夸。。僅僅在創(chuàng)建TIleView的時(shí)候作為參賽傳入即可xxxTime xxScale
一系列以這種方式結(jié)尾的屬性血崭,都是用來(lái)控制動(dòng)畫用的卧惜。。直接忽視它們吧夹纫。咽瓷。。構(gòu)造方法
構(gòu)造方法除了對(duì)屬性的一些初始化舰讹,最主要的任務(wù)就是創(chuàng)建上圖的那個(gè)九宮格黑框框,調(diào)用的方法便是setupBackgroundsetupBackground 方法
十分經(jīng)典的九宮格布局的創(chuàng)建方法茅姜。。也沒(méi)啥可說(shuō)的跺涤,(啥匈睁?你不知道什么是九宮格布局?)-
insertTile 方法
這個(gè)就是創(chuàng)建TileView并且顯示的方法桶错,方法接收(pos: (Int, Int), value: Int)2個(gè)參數(shù)航唆,pos一個(gè)元組類型的數(shù)據(jù),表示TileView應(yīng)該插入的位置院刁,Value便是值咯糯钙。創(chuàng)建完后利用下面這句話添加進(jìn)入tiles字典(這下知道為什么tiles類型會(huì)是<NSIndexPath, TileView>了吧)tiles[NSIndexPath(forRow: row, inSection: col)] = tile
-
moveOneTile 方法
從名字就可以知道,這是移動(dòng)一個(gè)tileView的方法,參數(shù)from退腥、to任岸、value的意義也十分明了。整個(gè)過(guò)程就是狡刘,首先利用from取出需要移動(dòng)的TileView享潜,然后根據(jù)to參數(shù)計(jì)算出目標(biāo)位置的x,y嗅蔬。
func moveOneTile(from: (Int, Int), to: (Int, Int), value: Int) { ... //這里是計(jì)算目標(biāo)位置的x剑按,y finalFrame.origin.x = tilePadding + CGFloat(toCol)*(tileWidth + tilePadding) finalFrame.origin.y = tilePadding + CGFloat(toRow)*(tileWidth + tilePadding) // 將原來(lái)的TileView從tiles中剔除 tiles.removeValueForKey(fromKey) // 將擁有新位置的tileView重新放入tiles字典 tiles[toKey] = tile // 到這里,內(nèi)部邏輯已經(jīng)將tile平移,但是界面沒(méi)有顯示 // Animate ...//一些動(dòng)畫效果澜术,設(shè)置需要移動(dòng)tile的frame和數(shù)值 }
-
moveTwoTiles 方法
功能幾乎和moveOneTile類似艺蝴,只是它的from有兩個(gè)來(lái)源。為什么會(huì)有兩個(gè)來(lái)源鸟废?想一想tileView的合并猜敢。兩個(gè)TileView合并過(guò)程應(yīng)該是這樣的。
|[][2][][2]| ==>向左滑動(dòng) |[2][2][][]| ==> |[4][][][]|
所以是先移動(dòng),再進(jìn)行合并缩擂!
整個(gè)的邏輯是這樣1 根據(jù)from取出2個(gè)需要移動(dòng)的tile
2 根據(jù)to參數(shù)計(jì)算出需要移動(dòng)的位置(2個(gè)同時(shí)都是移動(dòng)到那個(gè)位置鼠冕,動(dòng)畫給人一種合并的效果)
3 將2個(gè)tile從tiles字典中移除,并將其中一個(gè)(總是里to這個(gè)位置最近的那個(gè))tiles添加進(jìn)tiles字典
4 在動(dòng)畫中修改兩個(gè)tiles的frame(營(yíng)造動(dòng)畫效果)
5 將其中一個(gè)從GameboadView中移除撇叁,設(shè)置另外一個(gè)tileView新的value數(shù)值供鸠。
到達(dá)這里畦贸,完成平移與合并操作陨闹。
NumberTileGame.swift
這個(gè)文件就是本項(xiàng)目最主要的一個(gè)視圖控制器的實(shí)現(xiàn)。處理著絕大部分的邏輯薄坏。
-
model 屬性
這個(gè)屬性稍后會(huì)在GameModel.swift中進(jìn)行詳細(xì)介紹趋厉,這里先有個(gè)初步了解,它處理游戲中平移與合并算法胶坠。2048游戲最重要也是最難的便是它的平移和移動(dòng)算法的實(shí)現(xiàn)君账。
-
其他屬性
其他的屬性幾乎都是前面介紹過(guò)的。
-
構(gòu)造方法
構(gòu)造方法中對(duì)幾個(gè)屬性進(jìn)行了初始化沈善,并調(diào)用setupSwipeControls方法添加了4個(gè)手勢(shì)識(shí)別器乡数。
-
setupGame 方法
由ViewDidLoad調(diào)用,用來(lái)對(duì)游戲界面進(jìn)行初始化(創(chuàng)建顯示分?jǐn)?shù)的ScoreView和游戲面板GameboardView)闻牡。值得一提的便是其中的兩個(gè)用于計(jì)算x净赴,y的內(nèi)嵌方法。xPositionToCenterView與yPositionForViewAtPosition罩润。首先得明白玖翅,游戲中"大"的view就只有顯示總分?jǐn)?shù)的scoreView和游戲面板GameboardView。而這兩個(gè)view的位置關(guān)系總是scoreView在上GameboardView在下割以,并且兩個(gè)view都是居于屏幕最中央(水平且垂直居中)金度。自己可以換幾個(gè)不同屏幕大小試一試。而讓他們位置有自適應(yīng)的能力的便就是xPositionToCenterView與yPositionForViewAtPosition兩個(gè)內(nèi)嵌方法
func xPositionToCenterView(v: UIView) -> CGFloat {
let viewWidth = v.bounds.size.width
let tentativeX = 0.5*(vcWidth - viewWidth)
return tentativeX >= 0 ? tentativeX : 0
}
這個(gè)方法還不算復(fù)雜严沥,其目的是:計(jì)算出能使傳入?yún)?shù)v這個(gè)view猜极,在控制器中居中顯示的x的值。
func yPositionForViewAtPosition(order: Int, views: [UIView]) -> CGFloat {
...
//所有控件高度之和(包括間距),views.map({ $0.bounds.size.height })將所有view的高度取出消玄,然后通過(guò).reduce對(duì)所有高度進(jìn)行求和跟伏。
let totalHeight = CGFloat(views.count - 1)*viewPadding + views.map({ $0.bounds.size.height }).reduce(verticalViewOffset, combine: { $0 + $1 })
// 這便是計(jì)算出來(lái)的views整體的起點(diǎn)y
let viewsTop = 0.5*(vcHeight - totalHeight) >= 0 ? 0.5*(vcHeight - totalHeight) : 0
// 然后根據(jù)order,數(shù)值0莱找,代表第一個(gè)view也就是最上面的酬姆;數(shù)值1就是第二個(gè)view(本項(xiàng)目一共只有2個(gè)view所以也就是最下面的view)計(jì)算出任意一個(gè)view的y值
var acc: CGFloat = 0
for i in 0..<order {
acc += viewPadding + views[i].bounds.size.height
}
return viewsTop + acc
}
yPositionForViewAtPosition就比較復(fù)雜了。前面已經(jīng)說(shuō)明這兩個(gè)方法是為了讓兩個(gè)view居中顯示奥溺。yPositionForViewAtPosition就是為了找到能讓任意一個(gè)view垂直居中的y值辞色。因?yàn)樵诖怪泵嫔嫌卸鄠€(gè)view(這里是2個(gè)),所以單獨(dú)憑借一個(gè)view是無(wú)法計(jì)算的浮定,必須把所有的view都傳進(jìn)來(lái)相满,再根據(jù)所有view的高度和計(jì)算出scoreView應(yīng)該距離頂部的位置或者GameboardView距離底部的位置层亿。
在scoreVIew與GameboardView創(chuàng)建完后,調(diào)用insertTileAtRandomLocation插入2個(gè)tileVIew結(jié)束立美。
-
followUp 方法
在每一次成功移動(dòng)tileView后調(diào)用匿又,判定游戲是否結(jié)束,如果沒(méi)有結(jié)束隨機(jī)生成一個(gè)tileview
-
upCommand 方法
手勢(shì)識(shí)別器的監(jiān)聽(tīng)方法建蹄,當(dāng)上劃操作時(shí)候調(diào)用碌更。這里調(diào)用GameModel的queueMove方法,進(jìn)行移動(dòng)操作(稍后會(huì)有解釋)洞慎。其他Command方法幾乎一樣
-
一堆代理方法
基本都是調(diào)用其他文件內(nèi)實(shí)現(xiàn)的方法.
GameModel
終于來(lái)到2048核心所在痛单!先來(lái)簡(jiǎn)單的看一下所包含的屬性。
-
gameboard 屬性
它代表的是游戲的邏輯面板劲腿,為什么是邏輯面板旭绒?在前面已經(jīng)有一個(gè)GameboardView這是一個(gè)實(shí)際的能人用戶看到的游戲面板,空的就是黑黑的框焦人,有數(shù)值的就能看到一個(gè)個(gè)2挥吵、4、8之類的數(shù)字花椭。這些都是呈現(xiàn)給用戶看的忽匈。而我們實(shí)際進(jìn)行計(jì)算的是在gameboard這個(gè)邏輯面板中,定義如下.
struct SquareGameboard<T> {
let dimension : Int // 面板大小
var boardArray : [T] // 這里是存儲(chǔ)TileObject類型的數(shù)組
init(dimension d: Int, initialValue: T) {
dimension = d
boardArray = [T](count:d*d, repeatedValue:initialValue)
}
//下標(biāo)腳本个从,這樣能夠快速訪問(wèn)到boardArray任意一個(gè)元素(gameboard[0][1])
subscript(row: Int, col: Int) -> T {
get {
assert(row >= 0 && row < dimension)
assert(col >= 0 && col < dimension)
return boardArray[row*dimension + col]
}
set {
assert(row >= 0 && row < dimension)
assert(col >= 0 && col < dimension)
boardArray[row*dimension + col] = newValue
}
}
...
}
附上gameboard與GameboardView關(guān)系圖一張
-
queue 屬性
一個(gè)裝MoveCommand枚舉的數(shù)組脉幢,這個(gè)枚舉的意思是:direction這次滑動(dòng)是哪個(gè)方向,completion以及tileView移動(dòng)完成后需要做些什么嗦锐。這里定義成一個(gè)數(shù)組嫌松。然而實(shí)際這個(gè)數(shù)組長(zhǎng)度是不可能超過(guò)1的。奕污。
-
timer 屬性
一個(gè)定時(shí)器萎羔,目的是為了不讓因?yàn)槭种富瑒?dòng)過(guò)快而導(dǎo)致tileView過(guò)快的移動(dòng)(實(shí)際也沒(méi)有什么用。碳默。因?yàn)槟愕氖炙偈且话氵_(dá)不到那么快的)
-
queueMove 方法
在NumberTileGame.swift已經(jīng)提及到贾陷,一旦用進(jìn)行滑動(dòng)操作,便會(huì)調(diào)用這個(gè)方法嘱根。首先先將操作放入quenen數(shù)組,然后在看有沒(méi)有定時(shí)器在運(yùn)行髓废,沒(méi)有就調(diào)用timerFired方法.在屬性解釋的時(shí)候已經(jīng)說(shuō)過(guò)quenen以及定時(shí)器的作用(其實(shí)并沒(méi)什么作用)。
-
timerFired 方法
進(jìn)行一些無(wú)聊的判斷后調(diào)用performMove準(zhǔn)備進(jìn)行移動(dòng)!當(dāng)然如果有發(fā)生移動(dòng)该抒,那么得重新啟動(dòng)定時(shí)器.
-
performMove 方法
重點(diǎn)終于來(lái)了慌洪!一進(jìn)來(lái)就看到一個(gè)龐大的閉包
//閉包接收一個(gè)整數(shù)作為參數(shù),并且返回一個(gè)[(int),(int)]裝有元組的數(shù)組 let coordinateGenerator: (Int) -> [(Int, Int)] = { (iteration: Int) -> [(Int, Int)] in var buffer = Array<(Int, Int)>(count:self.dimension, repeatedValue: (0, 0)) for i in 0..<self.dimension { switch direction { case .Up: buffer[i] = (i, iteration) case .Down: buffer[i] = (self.dimension - i - 1, iteration) case .Left: buffer[i] = (iteration, i) case .Right: buffer[i] = (iteration, self.dimension - i - 1) } } return buffer }
這個(gè)方法目的是:根據(jù)滑動(dòng)方向的不同冈爹,返回對(duì)應(yīng)的滑動(dòng)順序涌攻。看不懂频伤?沒(méi)事恳谎,看下面的圖解。
接著繼續(xù)往下走,下面的代碼就是把取出來(lái)準(zhǔn)備進(jìn)行移動(dòng)計(jì)算的每一列(行),根據(jù)其(Int, Int)類型的數(shù)據(jù)憋肖,取出gameboard對(duì)應(yīng)的每一項(xiàng)TIleObject因痛。
// coords數(shù)組存放的順序也就是移動(dòng)的順序
let tiles = coords.map() { (c: (Int, Int)) -> TileObject in
let (x, y) = c
return self.gameboard[x, y]
}
提供一個(gè)思考方式
每當(dāng)獲得tiles這個(gè)需要的移動(dòng)的邏輯tiles,便會(huì)開(kāi)始執(zhí)行合并操作--調(diào)用merge方法.這個(gè)方法返回bool類型,只有發(fā)生移動(dòng)/合并操作才會(huì)返回true.讀到這里瞬哼,筆者推薦先去看看下文介紹的merge方法的實(shí)現(xiàn)再繼續(xù)看下面的解釋婚肆。
讀到這里,筆者默認(rèn)你已經(jīng)讀完下面解釋merge三個(gè)步驟
任然接著算法第三步返回的例子,返回值我還記得是【SingleMoveOrder(1, 0坐慰,4,ture),SingleMoveOrder(2,1,2,false)】
接著對(duì)返回的數(shù)據(jù)(操作)進(jìn)行處理用僧,項(xiàng)目使用的是forin的循環(huán)结胀。請(qǐng)看代碼注釋(先感嘆一下,好巧妙的映射方式)
for object in orders {
switch object {
case let MoveOrder.SingleMoveOrder(s, d, v, wasMerge):
// 是不是已經(jīng)忘記coords里面是什么了?同樣在map映射解釋那有寫哦
let (sx, sy) = coords[s]//針對(duì)我們的例子這里的值是[(1,3),1,2),1,1),1,0)]责循。 我們算出來(lái)都是2個(gè)SingleMoveOrder類型,這里分析合并情況即s=1,所以取出來(lái)的是(1,2)
let (dx, dy) = coords[d] //這里取出來(lái)的是(1,3)
if wasMerge {
score += v //這個(gè)是總得分糟港,這里會(huì)觸發(fā)屬性觀察器從而調(diào)用代理
}
gameboard[sx, sy] = TileObject.Empty // 設(shè)置(跟新)邏輯面板狀態(tài)
gameboard[dx, dy] = TileObject.Tile(v)// 設(shè)置(跟新)邏輯面板狀態(tài)
//到這里,邏輯面板已經(jīng)移動(dòng)完畢院仿,接下來(lái)就是改變UI了秸抚,所以調(diào)用下面的方法。這個(gè)方法在GameboardView.swift中實(shí)現(xiàn).
delegate.moveOneTile(coords[s], to: coords[d], value: v)
case let MoveOrder.DoubleMoveOrder(s1, s2, d, v):
// Perform a simultaneous two-tile move
let (s1x, s1y) = coords[s1]
let (s2x, s2y) = coords[s2]
let (dx, dy) = coords[d]
score += v
gameboard[s1x, s1y] = TileObject.Empty
gameboard[s2x, s2y] = TileObject.Empty
gameboard[dx, dy] = TileObject.Tile(v)
delegate.moveTwoTiles((coords[s1], coords[s2]), to: coords[d], value: v)
}
}
SingleMoveOrder與DoubleMoveOrder處理上幾乎一致歹垫,所以繼續(xù)分析剥汤。到這里,整個(gè)項(xiàng)目幾乎已經(jīng)分析完排惨。項(xiàng)目中最難理解的一個(gè)是合并算法吭敢,另外一個(gè)筆者便認(rèn)為是項(xiàng)目作者設(shè)計(jì)的一種巧妙的映射方式,最后附上全部映射關(guān)系圖一張.
-
merge 方法
2048的作者已經(jīng)將算法分為三步來(lái)實(shí)現(xiàn),分別對(duì)應(yīng)3個(gè)方法condense、collapse暮芭、convert,接下來(lái)對(duì)這些方法進(jìn)行逐個(gè)解釋
-
condense 方法
在介紹詳細(xì)算法之前鹿驼,必須先知道,這個(gè)項(xiàng)目的合并算法是先移動(dòng)再合并!
而合并其實(shí)就是將2個(gè)需要合并的tile辕宏,一個(gè)從GameboardView中刪除畜晰,另外一個(gè)則改變其數(shù)值大小,給人一種合并的假象瑞筐。
例如
[2][ ][2][4]--->是先將下標(biāo)為2的2移動(dòng)到下標(biāo)為0的2(重疊)
哪怕是這樣
[2][2][4][8]--->也是先將標(biāo)為1的2移動(dòng)到下標(biāo)為0的2(重疊)合并算法的第一步.
目的:“去除”數(shù)組中的空的項(xiàng)(也就是移動(dòng)),列如[2][][4][] ---> [2][4]
var tokenBuffer = [ActionToken]()
ActionToken枚舉里面定義了一共四種操作類型凄鼻,condense只用到兩種,后兩種便先不仔細(xì)介紹.這個(gè)方法是為了移動(dòng)。那怎么樣判斷是否需要移動(dòng)呢野宜?
需要移動(dòng)的必須滿足的條件
1:本身是非"空"的即tile不是.Empty類型
2:這個(gè)tile前面必須有"位置"能夠移動(dòng)扫步,也就是說(shuō),排在這個(gè)tile移動(dòng)順序之前的tiles內(nèi)最少有以一個(gè)是.Empty類型.
TileObject有兩個(gè)類型的值.Empty代表空的匈子。 .tile(value)代表這個(gè)是存在Tile的河胎,滿足條件1.
如果是[2][2][2][2]這種情況,tokenBuffer會(huì)老老實(shí)實(shí)的調(diào)用tokenBuffer.append(ActionToken.NoAction(source: idx, value: value))把這些全部添加進(jìn)去虎敦。一旦出現(xiàn)了一個(gè)為.Empty類型的tile游岳,這次switch會(huì)直接執(zhí)行default,從而導(dǎo)致where tokenBuffer.count == idx這個(gè)條件永遠(yuǎn)為false!.這才會(huì)調(diào)用tokenBuffer.append(ActionToken.Move(source: idx, value: value))其徙。
這個(gè)方法執(zhí)行完胚迫,返回tokenBuffer,此時(shí)tokenBuffer中裝有是所有tile需要進(jìn)行的操作要么是Move要么是NoAction。任然用這個(gè)例子:
在我向右滑動(dòng)上面那個(gè)面板唾那,用第二行來(lái)舉例访锻。condense接收的group內(nèi)容是
【Value(2),Value(2),Value(2),Empty】(不知道為什么?返回看上面map映射的解釋那張圖)在經(jīng)過(guò)本condense計(jì)算后返回的tokenBuffer是【NoAtion(0,2),NoAtion(1,2),NoAtion(2,2)】這里必須得對(duì)ActionToken進(jìn)行一些解釋,這個(gè)source參數(shù)是代表這個(gè)tile的的原位置不是它在tokenBuffer中的位置闹获,一定得記住是tile的的原位置期犬!tile的的原位置!tile的的原位置避诽!
-
collapse
合并算法第二步:合并相同數(shù)值的tile:[2][2][2][4] -> [4][2][4]
根據(jù)代碼進(jìn)行分析龟虎,筆者添加了很多注釋
func collapse(group: [ActionToken]) -> [ActionToken] { var tokenBuffer = [ActionToken]() var skipNext = false //如果發(fā)生了合并的操作,那么下一個(gè)tile將不進(jìn)行操作(已經(jīng)被合并) for (idx, token) in group.enumerate() { if skipNext { //如果發(fā)生了合并的操作沙庐,那么下一個(gè)tile將不進(jìn)行操作(已經(jīng)被合并) skipNext = false continue } switch token { ... case let .NoAction(s, v) where (idx < group.count-1 && v == group[idx+1].getValue() && GameModel.quiescentTileStillQuiescent(idx, outputLength: tokenBuffer.count, originalPosition: s)): //這種情況應(yīng)用在tile需要合并但不需要移動(dòng)的情況鲤妥,這個(gè)方法在一個(gè)數(shù)組隊(duì)列中最多調(diào)用一次, //即tile需要合并但不需要移動(dòng)的情況只有一次拱雏,因?yàn)橐坏┬枰喜⒚薨玻竺娴膖ile都需要進(jìn)行移動(dòng)操作 //哪怕是在第一步計(jì)算中是不需要移動(dòng)的 let next = group[idx+1] let nv = v + group[idx+1].getValue() skipNext = true tokenBuffer.append(ActionToken.SingleCombine(source: next.getSource(), value: nv)) case let t where (idx < group.count-1 && t.getValue() == group[idx+1].getValue()):
//這種情況應(yīng)用在tile需要移動(dòng)且需要合并的情況,需要移動(dòng)的狀態(tài)包括第一步計(jì)算出來(lái)的與前面發(fā)生過(guò)合并導(dǎo)致的
let next = group[idx+1]
let nv = t.getValue() + group[idx+1].getValue()
skipNext = true
tokenBuffer.append(ActionToken.DoubleCombine(source: t.getSource(), second: next.getSource(), value: nv))
case let .NoAction(s, v) where !GameModel.quiescentTileStillQuiescent(idx, outputLength: tokenBuffer.count, originalPosition: s):
//這種情況應(yīng)用于需要移動(dòng)但不需要合并,而需要移動(dòng)是因?yàn)榍懊孢M(jìn)行過(guò)合并操作造成的
tokenBuffer.append(ActionToken.Move(source: s, value: v))
case let .NoAction(s, v):
//不需要移動(dòng)且不合并
tokenBuffer.append(ActionToken.NoAction(source: s, value: v))
case let .Move(s, v):
// 僅僅需要移動(dòng)
tokenBuffer.append(ActionToken.Move(source: s, value: v))
default:
break
}
}
return tokenBuffer
}
總結(jié)一下古涧,合并/移動(dòng)的分類
1 tile需要合并但不需要移動(dòng)的情況垂券,這個(gè)種情況在一列/行*tiles*中最多存在一次。 因?yàn)橐驗(yàn)橐坏┬枰喜⑾刍竺娴膖ile都需要進(jìn)行移動(dòng)操作菇爪。哪怕是在第一步計(jì)算中是不需 要移動(dòng)的。列如[2][2][8][2]---->[4][ ][8][2]--->[4][8][2][]柒昏,這種情況經(jīng) 過(guò)算法第一步后全是*NoAction*,但是在算法第二步因?yàn)榍?個(gè)[2]會(huì)發(fā)生合并凳宙,所以導(dǎo) 致第二個(gè)位置會(huì)空從而導(dǎo)致原本NoAction的[8][2]也要移動(dòng),所以一旦有發(fā)生合并职祷,后 面的都**必須進(jìn)行移動(dòng)**.
2 tile需要移動(dòng)且需要合并的情況氏涩,需要移動(dòng)的狀態(tài)包括第一步計(jì)算出來(lái)的與前面發(fā)生過(guò) 合并導(dǎo)致的届囚。比如[2][2][4][4]-->[4][ ][4][4]--->[4][8][][]
3 需要移動(dòng)但不需要合并,而需要移動(dòng)是因?yàn)榍懊孢M(jìn)行過(guò)合并操作造成的(參考1)
4 不需要移動(dòng)且不合并:列如[2][4][8][16]
5 僅僅需要移動(dòng),比如[2][ ][4][8] -->[2][4][8]
最后解釋一下為什么在第一種分類下是*SingleCombine*而第二種是 *DoubleCombine*。兩個(gè)類型的不同就在于*DoubleCombine*多了一個(gè)*second:*參 數(shù)是尖。至于為什么這樣意系?不妨回想一下上面1情況與2情況的分別。沒(méi)錯(cuò)饺汹,區(qū)別在與我說(shuō)的 *tile*是否需要移動(dòng)蛔添,鄭重強(qiáng)調(diào):**只要發(fā)生合并操作,絕對(duì)是需要進(jìn)行tile的移動(dòng) 的兜辞!絕對(duì)需要移動(dòng)**迎瞧,是不是感覺(jué)奇怪,明明前面說(shuō)不需要移動(dòng)逸吵。對(duì)凶硅,我說(shuō)tile不需要移 動(dòng)是指*for*循環(huán)中**token**代表的當(dāng)前的那個(gè)tile,但是合并是兩個(gè)tile的事情扫皱, 所以*let next = group[idx+1]*取出了下一個(gè)tile足绅,而這個(gè)利用*idx+1*取出來(lái) 的tile是**一定得需要移動(dòng)的**,而*token*代表的那個(gè)tile不一定需要移動(dòng)啸罢,所以: *SingleCombine*是指只要移動(dòng)一個(gè)*tile*的情況编检,而*DoubleCombine*是值2個(gè)需 要合并的*tile*均需要移動(dòng)!(其實(shí)在分析**condense**(算法第一步)的時(shí)候已經(jīng)強(qiáng) 調(diào)扰才,算法的步驟是**先移動(dòng)再合并**)。任然回到算法第一步最后那個(gè)例子
經(jīng)過(guò)第一步計(jì)算傳入的group內(nèi)容是【NoAtion(0,2),NoAtion(1,2),NoAtion(2,2)】
經(jīng)過(guò)第二步計(jì)算出來(lái)的返回值tokenBuffer是【SingleCombine(1,4),Move(2,2)】
* convert
這是最后一步了厕怜,這里又多出了一個(gè)*MoveOrder*枚舉衩匣,這個(gè)枚舉把需要進(jìn)行的操作再度 簡(jiǎn)化就分為*SingleMoveOrder*與*DoubleMoveOrder*兩種操作類別,十分顯然 *DoubleMoveOrder*對(duì)應(yīng)的是前面需要進(jìn)行兩個(gè)tile移動(dòng)的且這兩個(gè)需要合并的操作( 對(duì)應(yīng)算法第二步分類中的2)粥航。*SingleMoveOrder*是單一一個(gè)tile的移動(dòng)操作琅捏。啥?你說(shuō)少了一種合并情況递雀?tile需要合并當(dāng)不需要移動(dòng)的操作被吃了柄延?我就問(wèn)了:算法第二步分類中的1是不是也需要移動(dòng)一塊被合并的tile(用*let next = group[idx+1]*取出的那塊)?,這就對(duì)了缀程,*SingleMoveOrder*有個(gè)參數(shù)*wasMerge:*代表的就是需不需要合并搜吧。所以*SingleMoveOrder*對(duì)應(yīng)算法第二步中分類的1、3杨凑、5滤奈。至于4,人家都說(shuō)了不移動(dòng)不合并,就讓人家好好原地待著撩满。接著例子來(lái)蜒程,我們看看最后返回的是什么
經(jīng)過(guò)第二步計(jì)算傳入的group是【SingleCombine(1,4),Move(2,2)】
經(jīng)過(guò)第三步計(jì)算出返回值moveBuffer是【SingleMoveOrder(1, 0绅你,4,ture),SingleMoveOrder(2,1,2,false)】
看到這里算法分析基本結(jié)束昭躺,可以返回去繼續(xù)看**performMove**方法忌锯,看后續(xù)操作.
## 總結(jié)
到這里應(yīng)該就告一段落了,雖然還有些代碼沒(méi)有分析领炫,但那些是不太重要的東西了偶垮。整個(gè)項(xiàng)目可以說(shuō)不難,比較適合初學(xué)者驹吮,關(guān)鍵是要理解作者設(shè)置的映射關(guān)系與合并算法针史,。想要徹底的了解這個(gè)2048碟狞,最好就是自己從頭到尾從零開(kāi)始寫一個(gè)啄枕。先從搭建界面開(kāi)始,一步一步慢慢的來(lái)族沃。筆者2048項(xiàng)目花了1天辦時(shí)間重寫频祝,而寫這篇文章卻花了將近三天。如果有時(shí)間脆淹〕?眨可能會(huì)繼續(xù)寫關(guān)于2048的博文,應(yīng)該是一步一步的去記錄實(shí)現(xiàn)2048的步驟盖溺。