昨天看到博客(www.richinmemory.com)的流量統計,居然還有一位朋友評論了,感動的滿眼都是淚啊!謝謝支持啊!為了使互動的朋友更方便的互動,今天我加了個能用微博等帳號登錄評論的插件。需要源碼的朋友可以直接發信到我的郵箱。猛戳之后(www.richinmemory.com)若覺得還過得去,可以嘗試收藏啊,親。有朝一日有幸流量穩定了,我就開始放棄這邊更新了,不過這個肯定還要很久很久才能達到。
二、桌面山寨版2048—游戲邏輯篇之移動方塊
這個小游戲的基本邏輯就是:合並同類項。玩家通過上下左右鍵操縱游戲方塊,然后操縱方向上所有相鄰相同的數字的方塊會被合並,合並之后方塊上的數字會被更新並且改變顏色。至此我開始了邏輯方面的第一步嘗試。
對於邏輯推理這件事,我一直深深相信的就是“從1到2再到無窮”的方式,這還是我高中老師教會我的方法,對於任何一個有規則但是復雜的事務,先從簡單的開 始總是不會錯的。於是,首先考慮只有兩個方塊在一個方向進行操作(我選擇的是“下”這個方向)的時候。根據在界面篇中的原則,所有的游戲方塊都是被保存在 一個ItemBox的數組里並且在程序的一開始就進行了初始化,所以,現在要處理的邏輯就是如何控制每個方格里方塊的顯示與否。
如果只有兩個游戲方塊時,用戶按下了“下”方向鍵,要考慮的情況有兩種:一是兩個游戲方塊不在一列,二是兩個游戲方塊位於一列。首先思考一下情況一的行為 模式應該是:兩個游戲方塊都應該向下移動直到它們碰到了游戲區域的邊框。這個邏輯並不難實現,最簡單的辦法,采用一個循環,遍歷所有的方格,如果當前方格 的bshow屬性是false,那么不進行操作,否則,獲取當前方格的位置信息(坐標),文本,顏色。將當前列的最后一行的方格賦予相同的文本和顏色,同 時將當前游戲方塊的信息清空(方塊顏色設置為背景色,文本清空),刷新界面,這樣就可以造成當前游戲方格“移動”到最后一行的假象。當一個方向做成功之 后,另外三個方向只要以此類推就可以了。雖然說就這個邏輯和最終的邏輯還相差十萬八千里,甚至對於整個游戲邏輯來說甚至是不正確的。但是如果沒有學會加 法,那么你怎么也不會學會三重積分吧。
忘記上面一段扯的廢話,現在來考慮情況二,如果兩個方塊位於一列,就要考慮到合並的情況(先假設兩個方塊的文本是一樣的)。這時按下方向鍵,正確的期望結 果是兩個方塊合並並且位於當前列的最后一行,這里的做法思路有太多了。我最最最開始的第一腦想法就是從(0,0)位置的方塊開始遍歷,如果bshow是 false,就不用做任何操作,如果不是,首先檢查當前列最后一行的方塊bshow的屬性,如果是false,那么和上面情況一樣,設置當前列的最后一行 的方塊和當前方塊信息一致並清空當前方塊信息。否則,則說明需要合並,這時只要將當前列最后一行方塊升一級(比如”2->4”)並清空當前方塊信息 就可以了。如此,兩個方塊的移動和合並已經做完了。可以按照這個思路先先出個代碼試試。
現在已經考慮完“1”的情況了,下面要開始進一步,考慮“2”的情況了。當兩個初始方塊合並完畢以后,開始思考又多生成了一個新的方塊該怎么辦。這里也有好幾種情況需要被考慮。
第一種,第一次出現的兩個方塊已經合並,新的方塊出現並且和被合並的方塊不在一列上(還是先只考慮一個方向上的情況)。這種最簡單,也就是兩個不在一列的方塊而已,上面已經說過了。
第二種,同樣是最初出現的兩個方塊已經合並,新的方塊與舊的方塊在同一列中。這時新出現的方塊和已經合並的方塊文字不一樣,不可能發生合並(暫時先從最簡 單的情況開始)。所以這時的思路是上面的一個擴展,也是從左上角的位置進行遍歷,依次判斷每個方塊的bshow是否是true,如果是,那么這個移動方式 就有點不同了,由於目前已知最后一行是已經合並的方塊,所以你知道當前這個方塊應該在倒數第二行上出現。但是如果我們不知道當前列最后一個bshow為 false的行是多少該怎么辦?怎么使用程序來找到呢?由於我們知道當前位置的縱坐標橫坐標,所以從最后一行開始,依次向上遍歷,如果遇到bshow為 false就立馬退出循環並且記錄下當前的行坐標。這樣就能完成上面所說的任務,有了這個坐標就可以知道當前的游戲塊應該放在哪個位置,其余的操作就和前 面所說的一致就可以了。
第三種,最初的兩個方塊沒有合並,那么這種情況肯定和前面只有兩個方塊之中的某一種是一樣的。
現在我們已經處理了”2”的情況,那么這種”2”的情況能不能推及到無窮呢?如果這時又出現了一個新的方塊,那么情況能不能直接套用上面的思路呢?稍微想 一想,總感覺可能可以又可能不可以,因為“無窮”的情況實在是太多了。我總結了下,用一個圖表示,雖然在實際情況中不可能同時出現這四列,但是四列單獨出 來,都是可能出現的:
第一列和第四列的情況最簡單,直接移動合並就可以,具體步驟前面已經描述過了。
第二列,需要判斷出同一列的下一行的文字和當前的文字不相同,只能移動不能發生合並。
第三列,有兩個能合並的地方,列1和列2的“2”可以合並,列3和列4的“4”可以合並,而且合並后,兩個“2”合並出的“4”要顯示在第三行,“4”合並出來的“8”要出現在第四行。
人腦思維分析完了,現在就要轉換成為電腦的思維考慮如何用程序實現。按照前面的思維模式下來,從左上角第一個游戲方塊開始進行遍歷,如果遇到當前行的 bshow是true,取得當前游戲方塊的列標,從當前列最后一行開始,依次判斷當前的bshow是否是false,如果是,記錄下當前的行序號並且返 回。獲得這個返回的行序號之后,判斷這個行序號的下一行(當然是同一列)和當前行的游戲方塊文字是否一致(有點繞,意思你懂的就行),如果一致,那么要考 慮合並,並且重新設置信息。
考慮到這里,貌似覺得邏輯都屢順了,我當時反正就是這么想的。於是第一次我寫的代碼是這樣的,GetCoordsFromIndex(i,it)是我寫的一個函數,目的是根據當前游戲方塊的序號得到游戲方塊的縱坐標和橫坐標,這個使用除法和取余操作很容易做到:
if(nChar == VK_DOWN) { for(int i=0;i<nTotalGrids-1;i++) { if(m_itemBoxArray[i].bShow) { GetCoordsFromIndex(i,it); for(int j=nTotalGrids - (m_nRowAndCol-it.nColIndex);j>i;j-=m_nRowAndCol) { // 找到當前列bshow為false的最大行序號 if(!m_itemBoxArray[j].bShow) { m_itemBoxArray[i].bShow = false; m_itemBoxArray[j].bShow = true; m_itemBoxArray[j].strItemText = m_itemBoxArray[i].strItemText; m_itemBoxArray[i].strItemText = _T(""); GetCoordsFromIndex(j,it); break; } } //合並 if ( it.nRowIndex < m_nRowAndCol -1 ) { ItemBox itCurrent = m_itemBoxArray[GetIndexFromCoords(it)]; ++it.nRowIndex; ItemBox itNextRow = m_itemBoxArray[GetIndexFromCoords(it)]; if ( itNextRow.bShow && itNextRow.strItemText == itCurrent.strItemText ) { m_itemBoxArray[GetIndexFromCoords(it)].strItemText = m_arrStrItemTexts[GetIndex(itNextRow.strItemText)+1]; --it.nRowIndex; m_itemBoxArray[GetIndexFromCoords(it)].bShow = false; m_itemBoxArray[GetIndexFromCoords(it)].strItemText = _T(""); } } } } }
但是一測試,發現貌似情況沒有向我想像的方向走啊。我觀察到了一個現象,那就是按照這個思路,上圖列3的行為完全不正確。然后我又回到了這個代碼,畢竟走 到這一步,從代碼的角度出發更容易找出問題所在,這也不違背數學歸納法的原則。如果按照這個代碼,列3這種情況就會出現這樣一種情況,由於我們是從左上角 開始遍歷的,那么第一行的2和第二行的2合並之后成為第二行的4,遍歷繼續,當遍歷到第三行的4時,決定與第四行的4進行合並,這樣就形成了第四行的8, 但是此時,第二行的4應該移動到第三行。然而,按照我們的代碼,我們已經遍歷過了第二行,不可能再回去了,所以就造成了在錯誤,就會造成合並的4和合並的 8分別在第二行和第四行,第三行空出來了,這明顯是不正確的。如何解決這個問題呢?通過分析可以發現,這個問題就是因為我們的遍歷順序有問題,那么就簡單 了,在向下這方向上,只要從最后一個游戲方格向第一個方格進行遍歷這個問題就迎刃而解,具體邏輯同志們可以再思考思考。
這樣之后,帶着洋洋得意的心情再次編譯,嘗試邏輯是否正確,結果發現還是太naive,問題又出現了,如果同一列出現了4個”2“,這時按一下”下“方向 鍵,這4個”2“會直接合並成一個”8“,根據原游戲的規則,這也是不對的,畢竟我們是山寨,山寨就得有點誠意,不能亂竄改原先的游戲規則。這個問題又如 何解決呢?首先得找出這個問題出現的原因,仔細思考下不難發現,原來游戲的規則是一次操作中,合並過的游戲方塊不可以再次合並,那么,這個問題很容易解 決,使用在封裝中的另一個成員變量bJoin,標識已經合並的方塊。所以最終”下“這個方向的代碼如下所示:
if(nChar == VK_DOWN) { for(int i=nTotalGrids-1;i>=0;i--) //從后向前遍歷 { if(m_itemBoxArray[i].bShow) { GetCoordsFromIndex(i,it); // 查詢同一列的bshow為false的最大行序號 for(int j=nTotalGrids - (m_nRowAndCol-it.nColIndex);j>i;j-=m_nRowAndCol) { if(!m_itemBoxArray[j].bShow) { m_itemBoxArray[i].bShow = false; m_itemBoxArray[j].bShow = true; m_itemBoxArray[j].strItemText = m_itemBoxArray[i].strItemText; m_itemBoxArray[i].strItemText = _T(""); GetCoordsFromIndex(j,it); break; } } // 合並 if ( it.nRowIndex < m_nRowAndCol -1 ) { ItemBox itCurrent = m_itemBoxArray[GetIndexFromCoords(it)]; ++it.nRowIndex; ItemBox itNextRow = m_itemBoxArray[GetIndexFromCoords(it)]; if ( itNextRow.bShow && itNextRow.strItemText == itCurrent.strItemText && !itNextRow.bJoin // 判斷當前塊有無被合並過 ) { m_itemBoxArray[GetIndexFromCoords(it)].strItemText = m_arrStrItemTexts[GetIndex(itNextRow.strItemText)+1]; m_nScore += 2*pow(2.0, GetIndex(itNextRow.strItemText)+1 ); m_itemBoxArray[GetIndexFromCoords(it)].bJoin = true; // 設置當前塊已經被合並 --it.nRowIndex; m_itemBoxArray[GetIndexFromCoords(it)].bShow = false; m_itemBoxArray[GetIndexFromCoords(it)].strItemText = _T(""); } } } } }
其余三個方向的代碼依”下“方向類推,但要注意遍歷順序,具體代碼可進入博客,那里有我的郵箱,若需要源代碼,可以留言或者發信給我。到這里,對於一個完 整的游戲,邏輯只能說完成了一個框架,還不能成為一個合格的山寨品,所以請看下一篇”邏輯面之緩緩出現的細節像風葉“。
