開(kāi)源項(xiàng)目Swift-2048學(xué)習(xí)、分析

開(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ǔ)的valueTileView蜻拨。所以這個(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)用的方法便是setupBackground

  • setupBackground 方法
    十分經(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)嵌方法。xPositionToCenterViewyPositionForViewAtPosition罩润。首先得明白玖翅,游戲中"大"的view就只有顯示總分?jǐn)?shù)的scoreView和游戲面板GameboardView。而這兩個(gè)view的位置關(guān)系總是scoreView在上GameboardView在下割以,并且兩個(gè)view都是居于屏幕最中央(水平且垂直居中)金度。自己可以換幾個(gè)不同屏幕大小試一試。而讓他們位置有自適應(yīng)的能力的便就是xPositionToCenterViewyPositionForViewAtPosition兩個(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距離底部的位置层亿。

scoreVIewGameboardView創(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
    }
  }
    ...
}

附上gameboardGameboardView關(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)
        }
      }
      

SingleMoveOrderDoubleMoveOrder處理上幾乎一致歹垫,所以繼續(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è)方法condensecollapse暮芭、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的步驟盖溺。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末西轩,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子横殴,更是在濱河造成了極大的恐慌尚粘,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,561評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蝇庭,死亡現(xiàn)場(chǎng)離奇詭異醉鳖,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)哮内,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,218評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門盗棵,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人北发,你說(shuō)我怎么就攤上這事纹因。” “怎么了鲫竞?”我有些...
    開(kāi)封第一講書人閱讀 157,162評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵辐怕,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我从绘,道長(zhǎng)寄疏,這世上最難降的妖魔是什么是牢? 我笑而不...
    開(kāi)封第一講書人閱讀 56,470評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮陕截,結(jié)果婚禮上驳棱,老公的妹妹穿的比我還像新娘。我一直安慰自己农曲,他們只是感情好社搅,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,550評(píng)論 6 385
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著乳规,像睡著了一般形葬。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上暮的,一...
    開(kāi)封第一講書人閱讀 49,806評(píng)論 1 290
  • 那天笙以,我揣著相機(jī)與錄音,去河邊找鬼冻辩。 笑死猖腕,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的恨闪。 我是一名探鬼主播倘感,決...
    沈念sama閱讀 38,951評(píng)論 3 407
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼咙咽!你這毒婦竟也來(lái)了老玛?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 37,712評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤钧敞,失蹤者是張志新(化名)和其女友劉穎逻炊,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體犁享,經(jīng)...
    沈念sama閱讀 44,166評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,510評(píng)論 2 327
  • 正文 我和宋清朗相戀三年豹休,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了炊昆。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,643評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡威根,死狀恐怖凤巨,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情洛搀,我是刑警寧澤敢茁,帶...
    沈念sama閱讀 34,306評(píng)論 4 330
  • 正文 年R本政府宣布,位于F島的核電站留美,受9級(jí)特大地震影響彰檬,放射性物質(zhì)發(fā)生泄漏伸刃。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,930評(píng)論 3 313
  • 文/蒙蒙 一逢倍、第九天 我趴在偏房一處隱蔽的房頂上張望捧颅。 院中可真熱鬧,春花似錦较雕、人聲如沸碉哑。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,745評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)扣典。三九已至,卻和暖如春慎玖,著一層夾襖步出監(jiān)牢的瞬間贮尖,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 31,983評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工凄吏, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留远舅,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,351評(píng)論 2 360
  • 正文 我出身青樓痕钢,卻偏偏與公主長(zhǎng)得像图柏,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子任连,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,509評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容