源項(xiàng)目實(shí)戰(zhàn):用2048吃透Swift開(kāi)發(fā)核心算法與手勢(shì)識(shí)別)
簡(jiǎn)介這是一份面向iOS開(kāi)發(fā)者的《2048》游戲完整Objective-C實(shí)現(xiàn)適合想學(xué)習(xí)原生游戲開(kāi)發(fā)、MVC架構(gòu)與手勢(shì)交互的讀者。源碼演示了經(jīng)典2048玩法支持滑動(dòng)控制可通過(guò)調(diào)整F3HViewController參數(shù)配置棋盤尺寸、獲勝門檻及單元格顏色也支持按鈕控件與輕掃手勢(shì)切換。壓縮包共45個(gè)文件以.m/.h源文件為核心配合plist、json、strings等配置資源以及storyboard、工程文件與測(cè)試代碼整體僅86KB結(jié)構(gòu)簡(jiǎn)潔便于快速閱讀。目前已有292人學(xué)習(xí)適合中初級(jí)iOS開(kāi)發(fā)者參考。資源的最大價(jià)值在于提供了清晰可擴(kuò)展的架構(gòu)通過(guò)F3HGameModelProtocol橋接模型與視圖評(píng)分系統(tǒng)、勝利狀態(tài)通知和動(dòng)畫(huà)均有實(shí)現(xiàn)未借助SpriteKit有助于理解純UIKit/QuartzCore下的游戲動(dòng)畫(huà)實(shí)現(xiàn)思路。1. 項(xiàng)目概覽為什么2048是練手iOS開(kāi)發(fā)的絕佳選擇先聊點(diǎn)實(shí)在的。2048這個(gè)游戲2014年橫空出世規(guī)則簡(jiǎn)單到一句話就能說(shuō)完——4×4格子里滑來(lái)滑去相同數(shù)字撞一起就合并翻倍湊出2048就算贏。但就這么個(gè)看似不起眼的小游戲背后的技術(shù)點(diǎn)比很多人的職場(chǎng)簡(jiǎn)歷還豐富手勢(shì)識(shí)別、核心算法、UI狀態(tài)同步、動(dòng)畫(huà)過(guò)渡、隨機(jī)數(shù)邏輯一個(gè)不少。尤其是iOS版本的開(kāi)源代碼在GitHub上星標(biāo)大戶一抓一大把Swift和Objective-C兩個(gè)技術(shù)棧的都有簡(jiǎn)直是白嫖學(xué)習(xí)的寶庫(kù)。我當(dāng)時(shí)把這個(gè)開(kāi)源項(xiàng)目翻來(lái)覆去擼了好幾遍最大的感受是它的代碼量剛好卡在一個(gè)“看一眼能懂、動(dòng)手改會(huì)爽”的甜蜜點(diǎn)。比那種動(dòng)輒幾萬(wàn)行的商業(yè)項(xiàng)目容易上手得多又能覆蓋iOS開(kāi)發(fā)里最常用的幾個(gè)知識(shí)點(diǎn)。無(wú)論你是剛學(xué)完Swift語(yǔ)法的小白還是工作了兩三年想補(bǔ)補(bǔ)游戲邏輯的客戶端開(kāi)發(fā)這個(gè)項(xiàng)目都值得花一個(gè)周末仔細(xì)啃一遍。這篇文章我不打算泛泛而談“這個(gè)項(xiàng)目很好很棒”而是會(huì)帶著你把手上的開(kāi)源代碼從結(jié)構(gòu)到核心算法逐層剝開(kāi)最后再教你怎么把它跑起來(lái)并且基于它做二次開(kāi)發(fā)。里面還會(huì)穿插一些我自己實(shí)際調(diào)試時(shí)踩過(guò)的坑這些坑在文檔里基本找不到屬于純經(jīng)驗(yàn)輸出。2. 代碼結(jié)構(gòu)拆解拿到開(kāi)源庫(kù)先別急著Run先看懂它怎么組織2.1 從入口開(kāi)始看AppDelegate和ViewController各管什么事iOS應(yīng)用啟動(dòng)的入口固定是AppDelegate2048這個(gè)項(xiàng)目也一樣。但不同的是很多開(kāi)源項(xiàng)目會(huì)把整個(gè)游戲界面直接丟進(jìn)ViewController里一股腦寫(xiě)完而寫(xiě)得規(guī)范的項(xiàng)目會(huì)嚴(yán)格區(qū)分View層和Model層。核驗(yàn)一個(gè)2048開(kāi)源庫(kù)寫(xiě)得好不好我有個(gè)土辦法看ViewController的體積。如果這個(gè)文件超過(guò)500行八成是View和Model揉在一起了反之如果ViewController里主要只有手勢(shì)處理、UI刷新調(diào)用而游戲邏輯都在單獨(dú)的GameModel或GameBoard類里那這個(gè)項(xiàng)目就是結(jié)構(gòu)清晰的值得細(xì)看。這類規(guī)范化項(xiàng)目通常的文件組成是這樣的2048/ ├── AppDelegate.swift // 應(yīng)用生命周期管理 ├── GameViewController.swift // 視圖控制器負(fù)責(zé)接收手勢(shì)、發(fā)起游戲動(dòng)作 ├── GameModel.swift // 游戲核心模型矩陣存儲(chǔ)、合并算法、勝負(fù)判定 ├── GameBoardView.swift // 棋盤視圖負(fù)責(zé)展示格子數(shù)字和顏色 ├── TileView.swift // 單個(gè)瓷磚視圖顯示數(shù)字的方塊 └── Direction.swift // 手勢(shì)方向枚舉定義先別急著管每個(gè)文件的代碼細(xì)節(jié)你要做的是先建立整棵樹(shù)的輪廓。把ViewController看成網(wǎng)吧前臺(tái)用戶有要求滑一下就喊前臺(tái)前臺(tái)再去后廚Model下單后廚做完菜算法更新矩陣之后端菜員View再把菜端出來(lái)。就這么個(gè)三層關(guān)系。2.2 模型層的職責(zé)邊界不要把規(guī)則寫(xiě)到View里我見(jiàn)過(guò)一些初學(xué)者自己寫(xiě)的2048把滑動(dòng)合并的邏輯直接寫(xiě)在手勢(shì)處理方法里。代碼看起來(lái)也能跑但你想加個(gè)“撤銷”功能就頭大了——因?yàn)檎麄€(gè)游戲的中間狀態(tài)根本沒(méi)被緩存你無(wú)從恢復(fù)。開(kāi)源項(xiàng)目里設(shè)計(jì)得比較合理的方案是GameModel層中維護(hù)一個(gè)board二維數(shù)組向外暴露幾個(gè)核心方法比如move(_ direction: Direction)、isGameOver()、insertRandomTile()。View層的東西它一概不知也不該知道。這樣設(shè)計(jì)的好處是顯而易見(jiàn)的。以后要加AI輔助、要寫(xiě)自動(dòng)化測(cè)試、要做性能分析都可以直接面對(duì)Model層操作完全不需要關(guān)心界面上長(zhǎng)了什么樣。我在實(shí)際二次開(kāi)發(fā)的時(shí)候就深刻體會(huì)到這個(gè)邊界劃得越清楚后面寫(xiě)代碼就越輕松。2.3 游戲控制器手勢(shì)識(shí)別的背后邏輯GameViewController里的核心工作其實(shí)就兩件事識(shí)別用戶滑動(dòng)手勢(shì)然后把方向告訴Model讓它去算再讓View根據(jù)Model返回的新?tīng)顟B(tài)刷新UI。很多開(kāi)源項(xiàng)目用的是UISwipeGestureRecognizer上下左右四個(gè)方向各加一個(gè)直接、簡(jiǎn)單。但如果你試過(guò)就會(huì)發(fā)現(xiàn)這個(gè)手勢(shì)識(shí)別器的響應(yīng)有點(diǎn)“遲鈍”——它必須偵測(cè)到足夠明確的位移方向才回觸發(fā)快速滑動(dòng)偶爾會(huì)被系統(tǒng)過(guò)濾掉導(dǎo)致手感不夠爽快。如果項(xiàng)目用的是UIPanGestureRecognizer那通常會(huì)在touchesBegan時(shí)記錄起始點(diǎn)在touchesMoved或手勢(shì)回調(diào)里計(jì)算位移超過(guò)某個(gè)閾值比如20個(gè)點(diǎn)就判定方向。這種方式響應(yīng)更靈敏也給了開(kāi)發(fā)者更多手動(dòng)控制的空間。讀代碼的時(shí)候注意看它是用哪種這直接影響后面你調(diào)手感的思路。3. 核心算法與實(shí)現(xiàn)細(xì)節(jié)2048藏在代碼里的靈魂3.1 棋盤的數(shù)據(jù)存儲(chǔ)方式最常見(jiàn)的是二維數(shù)組在Swift里就是[[Int]]board[row][col]表示第row行第col列的數(shù)字0表示空格。有些優(yōu)化版本會(huì)用一維數(shù)組加下標(biāo)換算但開(kāi)源項(xiàng)目里二維數(shù)組居多因?yàn)樗钪庇^也最容易和UI的網(wǎng)格對(duì)應(yīng)起來(lái)。如果你想改得更有意思把棋盤從4×4擴(kuò)展到5×5甚至6×6二維數(shù)組的改動(dòng)成本也很低——只要在初始化時(shí)把它改成board Array(repeating: Array(repeating: 0, count: size), count: size)就行了。3.2 滑動(dòng)合并算法先把0拿掉再相鄰合并最后補(bǔ)回0這個(gè)算法是整個(gè)游戲的核心看懂它比看懂任何別的模塊都有價(jià)值。以向左滑動(dòng)為例算法本質(zhì)上是“對(duì)每一行執(zhí)行一次一維處理”。處理一行的一維邏輯可以總結(jié)成三步壓縮把這一行所有非零數(shù)字按順序擠到最左邊中間不留空。比如[0,2,0,2]處理完是[2,2,0,0]。合并從左往右掃描如果相鄰兩個(gè)非零數(shù)字相等就把右邊那個(gè)翻倍加到左邊右邊那個(gè)置0并累加得分。比如[2,2,4,0]處理完是[4,4,0,0]注意合并后要從下一個(gè)位置繼續(xù)不能對(duì)剛合并出來(lái)的數(shù)字再次合并。例如[2,2,2,2]應(yīng)該變成[4,4,0,0]而不是[8,0,0,0]。重新壓縮合并過(guò)程中會(huì)產(chǎn)生新的空隙所以再做一次壓縮把非零數(shù)字重新靠左對(duì)齊。用Swift寫(xiě)一行的核心邏輯大概長(zhǎng)這樣func mergeLine(_ line: [Int]) - (result: [Int], score: Int) { var compact line.filter { $0 ! 0 } // 第一步去掉所有0 var merged [Int]() var score 0 var index 0 while index compact.count { if index 1 compact.count, compact[index] compact[index 1] { let newValue compact[index] * 2 merged.append(newValue) score newValue index 2 } else { merged.append(compact[index]) index 1 } } while merged.count line.count { merged.append(0) // 補(bǔ)0回原長(zhǎng)度 } // 完整代碼里這里要同時(shí)返回“棋盤是否發(fā)生變化”的布爾量用于判斷是否生成新數(shù)字 return (merged, score) }這個(gè)函數(shù)有兩點(diǎn)很關(guān)鍵filter叫他去0合并時(shí)用while控制指針跳躍。用index 2跳過(guò)一個(gè)位置正好防止了“連消”的情況這是新手最愛(ài)犯的錯(cuò)。等四個(gè)方向的滑動(dòng)都處理完再根據(jù)方向決定按行處理還是按列處理按列處理時(shí)可以先轉(zhuǎn)置矩陣處理完再轉(zhuǎn)置回來(lái)代碼會(huì)簡(jiǎn)潔很多。3.3 隨機(jī)生成新數(shù)字的概率和位置滑動(dòng)合并后如果棋盤有變化就需要在空白位置隨機(jī)生成一個(gè)新數(shù)字。開(kāi)源項(xiàng)目里常見(jiàn)的規(guī)則是90%概率生成210%概率生成4這基本上是沿用了原版游戲的設(shè)定保證了游戲初期的難度曲線不會(huì)太陡。實(shí)現(xiàn)上通常分兩步先遍歷棋盤收集所有值為0的格子坐標(biāo)再用randomElement()隨機(jī)選一個(gè)賦值進(jìn)去。這里有一個(gè)小優(yōu)化我在看有些實(shí)現(xiàn)時(shí)特別注意過(guò)如果滑動(dòng)后棋盤沒(méi)變化千萬(wàn)不要生成新數(shù)字。否則玩家用手指亂劃棋盤也會(huì)瘋狂吐新數(shù)字游戲難度直接失控。func insertRandomTile() { let emptyTiles board.indices.flatMap { row in board[row].indices.compactMap { col in board[row][col] 0 ? (row, col) : nil } } guard let position emptyTiles.randomElement() else { return } board[position.0][position.1] Int.random(in: 0..10) 0 ? 4 : 2 }3.4 游戲結(jié)束與勝利判定的實(shí)現(xiàn)勝利判定很簡(jiǎn)單每次合并產(chǎn)生2048后就彈個(gè)Alert問(wèn)玩家“繼續(xù)還是收手”。但游戲結(jié)束的判定更講究不是說(shuō)沒(méi)有空位就算結(jié)束而是要在沒(méi)有空位的前提下再檢查是否還有相鄰兩個(gè)格子的數(shù)字相等。如果一行里存在[2,2,4]這種情況即使沒(méi)有空格也還能滑動(dòng)合并并非山窮水盡。這個(gè)判斷邏輯用代碼寫(xiě)就是兩個(gè)循環(huán)先掃空格有空格直接結(jié)束判斷沒(méi)空格再逐一比較當(dāng)前格子與右邊、下邊的格子是否相等。分四方向比對(duì)的寫(xiě)法很多開(kāi)源項(xiàng)目里實(shí)際上是簡(jiǎn)化過(guò)的——它們會(huì)復(fù)制一份棋盤分別嘗試四個(gè)方向是否都能移動(dòng)只要有一個(gè)方向能產(chǎn)生變化游戲就還沒(méi)完。4. 實(shí)操過(guò)程從克隆倉(cāng)庫(kù)到跑起來(lái)再到真機(jī)調(diào)優(yōu)4.1 在Xcode里運(yùn)行開(kāi)源項(xiàng)目的完整步驟很多新手拿到開(kāi)源項(xiàng)目的第一步就直接敗了——不是編譯報(bào)錯(cuò)就是缺依賴最后連游戲長(zhǎng)什么樣都沒(méi)見(jiàn)到。這里我按實(shí)際操作的順序把關(guān)鍵步驟說(shuō)清楚。第一步肯定是從GitHub把倉(cāng)庫(kù)克隆到本地。建議用git clone命令而不是直接Download ZIP因?yàn)楹竺婺阋峤蛔约旱母膭?dòng)、同步上游更新的時(shí)候git倉(cāng)庫(kù)會(huì)方便得多。git clone https://github.com/your-fork/2048-iOS.git cd 2048-iOS第二步是檢查項(xiàng)目依賴。這個(gè)項(xiàng)目如果用了CocoaPods你會(huì)在根目錄看到一個(gè)Podfile文件。此時(shí)直接打開(kāi).xcodeproj必然會(huì)報(bào)錯(cuò)因?yàn)榈谌綆?kù)還沒(méi)裝。正確的流程是pod install裝完之后注意以后都要打開(kāi).xcworkspace文件而不是.xcodeproj否則Pod里的代碼對(duì)不上。這一點(diǎn)剛?cè)腴T的老哥幾乎人人中招報(bào)錯(cuò)的時(shí)候Builder說(shuō)找不到某個(gè)類其實(shí)只是因?yàn)榇蜷_(kāi)錯(cuò)了文件。第三步是改Bundle Identifier。這一步很多人會(huì)忘。直接用原作者的Bundle ID在本地跑模擬器通常沒(méi)事但如果你想連真機(jī)調(diào)試需要把它改成你自己的標(biāo)識(shí)比如com.yourname.2048否則會(huì)提示簽名不可用。在Xcode左側(cè)導(dǎo)航欄選中項(xiàng)目target切到Signing Capabilities關(guān)掉Automatically manage signing再打開(kāi)一次輸入你自己的Apple ID即可。第四步是把運(yùn)行目標(biāo)選成你手邊的iPhone模擬器或真機(jī)然后CommandR。如果沒(méi)有意外游戲界面起來(lái)就是一個(gè)4×4的棋盤標(biāo)題下方有個(gè)“Score”計(jì)數(shù)器直接滑動(dòng)開(kāi)始玩了。4.2 模擬器和真機(jī)調(diào)試的注意事項(xiàng)在模擬器上跑2048手感其實(shí)是“假”的。鼠標(biāo)拖動(dòng)模擬出的滑動(dòng)手勢(shì)靈敏度和真機(jī)差不少尤其當(dāng)你測(cè)試快速連續(xù)滑動(dòng)的時(shí)候。所以我的建議是功能驗(yàn)證階段用模擬器手感調(diào)優(yōu)階段無(wú)論如何都要上真機(jī)。真機(jī)調(diào)試還有個(gè)小坑如果你在代碼里用到了設(shè)備振動(dòng)或聲音反饋而模擬器上沒(méi)有對(duì)應(yīng)硬件有可能觸發(fā)異常。有次我為了測(cè)試手勢(shì)反饋在代碼里加了一句AudioServicesPlaySystemSound模擬器上直接崩了報(bào)錯(cuò)還是那種不太容易定位的。后面吃了這個(gè)教訓(xùn)凡是涉及硬件能力調(diào)用的代碼我都用#if targetEnvironment(simulator)括起來(lái)跳過(guò)。5. 常見(jiàn)問(wèn)題與調(diào)優(yōu)實(shí)錄開(kāi)源不等于沒(méi)坑我踩過(guò)的都在這5.1 滑動(dòng)方向判定怎么調(diào)才順手前面提過(guò)UISwipeGestureRecognizer有靈敏度問(wèn)題。開(kāi)源項(xiàng)目里有不少直接用它的玩起來(lái)總覺(jué)得要“用力劃”才有反應(yīng)。后來(lái)我改成UIPanGestureRecognizer在touchesBegan記錄初始坐標(biāo)在touchesMoved里計(jì)算偏移量當(dāng)abs(dx)或abs(dy)超過(guò)某個(gè)閾值時(shí)觸發(fā)方向判定并且每觸發(fā)一次就重置初始點(diǎn)。這里閾值的選擇非常關(guān)鍵。太大比如60pt會(huì)導(dǎo)致你得劃很長(zhǎng)的距離才反應(yīng)太小比如5pt又會(huì)因?yàn)槭种篙p微抖動(dòng)而誤判方向。我自己測(cè)試下來(lái)20到30pt之間手感最像原版2048你可以根據(jù)自己的習(xí)慣微調(diào)。還有一個(gè)細(xì)節(jié)判定完方向后要把initialPoint重置為當(dāng)前點(diǎn)并且isDirectionLocked標(biāo)志置為true確保一次滑動(dòng)手勢(shì)只觸發(fā)一次移動(dòng)。否則手指輕輕一抖游戲會(huì)像抽風(fēng)一樣連走好幾步觀感極差。5.2 動(dòng)畫(huà)卡頓和閃爍問(wèn)題的根源與解法2048棋盤數(shù)字跳動(dòng)的時(shí)候如果用一個(gè)UILabel直接改text屬性顯示上會(huì)很生硬。很多開(kāi)源項(xiàng)目吸取了經(jīng)驗(yàn)會(huì)用UIView.animate加縮放動(dòng)畫(huà)每次數(shù)字合并或生成新塊時(shí)讓TileView從一個(gè)稍大的比例縮放回正常給人一種“彈”的感覺(jué)。但動(dòng)畫(huà)加多了低端真機(jī)上會(huì)出現(xiàn)明顯的卡頓。這種問(wèn)題通常是多個(gè)UIView.animate動(dòng)畫(huà)疊加并且對(duì)同一個(gè)視圖的屬性進(jìn)行連續(xù)修改導(dǎo)致的。有一個(gè)實(shí)用解法把多個(gè)動(dòng)畫(huà)放進(jìn)同一個(gè)UIView.animate的animations閉包里系統(tǒng)會(huì)嘗試合并一組動(dòng)畫(huà)或者用UIViewPropertyAnimator統(tǒng)一管理。還有個(gè)細(xì)節(jié)藏得很深如果你用CATransform3D做縮放動(dòng)畫(huà)而同一層級(jí)的視圖同時(shí)在做frame變化就會(huì)產(chǎn)生“閃爍”或“錯(cuò)位”。解決辦法是全部改成用transform屬性操作不混用frame和transform。這個(gè)是我當(dāng)時(shí)排查了兩個(gè)小時(shí)才發(fā)現(xiàn)的坑寫(xiě)出來(lái)希望能幫你省下這兩個(gè)小時(shí)。5.3 關(guān)于變量修改和數(shù)組淺拷貝的隱蔽問(wèn)題在Objective-C版本的2048開(kāi)源項(xiàng)目里一個(gè)經(jīng)典問(wèn)題是NSMutableArray的淺拷貝。因?yàn)槠灞P是NSMutableArray套NSMutableArray的兩層結(jié)構(gòu)如果你直接寫(xiě)let copiedBoard board那只是拷貝了外層數(shù)組的引用內(nèi)層數(shù)組依然指向同一份內(nèi)存。改一個(gè)格子原棋盤就跟著變了。做“撤銷”功能時(shí)這會(huì)直接導(dǎo)致回退失敗。正確的做法是兩層都拷貝在OC里用[[NSMutableArray alloc] initWithArray:board copyItems:YES]在Swift里更簡(jiǎn)單——因?yàn)锳rray是值類型它天然是深拷貝語(yǔ)義的你直接let copiedBoard board就不會(huì)有這個(gè)問(wèn)題。所以如果你看著OC版代碼一臉懵不妨換個(gè)Swift版的倉(cāng)庫(kù)看看邏輯一樣但少踩不少內(nèi)存管理的坑。5.4 用單元測(cè)試保護(hù)核心算法核心算法每次改動(dòng)都手動(dòng)玩幾個(gè)來(lái)回測(cè)試效率太低而且容易遺漏邊界情況。我的做法是為核心算法寫(xiě)完善的單元測(cè)試這是我在實(shí)際開(kāi)發(fā)中收益最大的一件事。比如mergeLine函數(shù)我把能想到的邊界情況都寫(xiě)成測(cè)試用例一行全是0、一行全是相同數(shù)字、數(shù)字交錯(cuò)、合并之后產(chǎn)生新空位等等。一改代碼跑一遍測(cè)試有任何回歸立刻就會(huì)暴露出來(lái)。后來(lái)我基于這份代碼做AI棋力評(píng)估功能時(shí)這些測(cè)試幫了大忙——畢竟沒(méi)有測(cè)試的保護(hù)改一個(gè)死循環(huán)排查的問(wèn)題又可能引入另一個(gè)邊界閃耀的bug。func testMergeZeroLine() { let (result, _) GameModel.mergeLine([0, 0, 0, 0]) XCTAssertEqual(result, [0, 0, 0, 0]) } func testMergeAllTwos() { let (result, _) GameModel.mergeLine([2, 2, 2, 2]) XCTAssertEqual(result, [4, 4, 0, 0]) }6. 進(jìn)階擴(kuò)展從照搬到創(chuàng)新你能在這個(gè)開(kāi)源項(xiàng)目上做什么等你能把它跑起來(lái)、也把核心算法吃透了這個(gè)項(xiàng)目就成了你發(fā)揮想象力的畫(huà)板。我這里提三個(gè)最常見(jiàn)的擴(kuò)展方向按投入產(chǎn)出比排序。第一個(gè)是AI輔助。給游戲加一個(gè)“AI自動(dòng)玩”模式其實(shí)就是在Model層跑一個(gè)極小化極大或者期望值最大算法。棋盤狀態(tài)空間看起來(lái)很大但2048的狀態(tài)其實(shí)是比較受限的用Alpha-Beta剪枝加一個(gè)簡(jiǎn)單的啟發(fā)式評(píng)價(jià)函數(shù)AI就能打出相當(dāng)不錯(cuò)的成績(jī)。這個(gè)項(xiàng)目做完你對(duì)博弈樹(shù)搜索的理解絕對(duì)會(huì)上一個(gè)新臺(tái)階。第二個(gè)是撤銷功能。實(shí)現(xiàn)思路也不算難在每次滑動(dòng)合并之前把當(dāng)前棋盤狀態(tài)和分?jǐn)?shù)壓入一個(gè)棧撤銷時(shí)從棧頂彈出來(lái)恢復(fù)即可。但這里有個(gè)隱私問(wèn)題——撤銷是不是應(yīng)該有限次無(wú)限撤銷會(huì)讓游戲變成“刪檔重來(lái)”失去挑戰(zhàn)性。我當(dāng)時(shí)的實(shí)現(xiàn)是只能撤銷最后一步和微信小游戲“悔棋”的思路保持一致。第三個(gè)是改造成多主題換膚。數(shù)字2用綠色、4用藍(lán)色……這些顏色本來(lái)就可以抽成配置文件。把配色表做成一套JSON甚至可以讓玩家自己上傳配色方案在服務(wù)端下發(fā)。這個(gè)方向做起來(lái)的工程量不大但對(duì)UI架構(gòu)的鍛煉很足尤其是對(duì)UIView的復(fù)用和刷新時(shí)機(jī)把控會(huì)理解得更深。我個(gè)人在實(shí)際操作中體會(huì)比較深的一點(diǎn)是別急著改代碼先把原作者的設(shè)計(jì)意圖看透。很多初學(xué)者一拿到開(kāi)源項(xiàng)目就手癢想改結(jié)果改了界面又破壞了邏輯最后越改越亂只能回退重來(lái)。先照著原版跑通一遍在給它加第一個(gè)功能這個(gè)順序帶給我很大的幫助。最后再分享一個(gè)小技巧如果你在改這個(gè)項(xiàng)目的過(guò)程中遇到什么奇怪的編譯問(wèn)題第一反應(yīng)先去檢查是不是Pod庫(kù)版本問(wèn)題——pod install跑一遍基本能解決一半莫名其妙的報(bào)錯(cuò)剩下的一半多數(shù)出在簽名配置上。本文還有配套的精品資源點(diǎn)擊獲取