弄個個人獨立的博客是我的一個一直存在想法,畢竟不想當優秀工程師的程序員不是好碼農,所以,我一直希望有個獨立的博客來記錄和分享自己的想法,最重要的希望能有一個平台認識很多志同道合的人。但是弄完了之后怎樣讓別人知道我是志同道合的自己人又讓我陷入了沉思,百般思考之后,我覺得在這里更新日志是一個既不會被封殺又最有效的辦法,所以在我的博客沒有穩定之前,我會一直在這里和獨立博客中一起更新,直到我的博客能夠猥名遠揚,被大家所知曉,希望我能成功吧!(這一節在博客那邊要不要去掉呢?思前想后,我還是去掉了)
請猛戳:http://www.richinmemory.com/
第一次玩2048的時候是github上的那個網頁版,當時真的是根本停不下來,玩着玩着我突然冒出一個想法,這游戲操作啥的都不復雜,而且最近下班閑着的無聊時光還蠻多的,所以我覺得我可以試試能不能山寨一個。既然要山寨就得要山寨的專業,雖然這是個開源的項目,但是我 沒有看過一行他本身的代碼,因為我要做個有節操的山寨者。考慮到語言我最熟悉的莫過於的c/c++了,所以我也只能選擇他了。在山寨的過程中,我深深的體會到了任何一個可以拿到給大家使用的軟件都是不能大意的,一個小小的細節上的沒有注意導致的就是別人在使用上的bug。
一、2048山寨貨之界面篇
界面是我要寫的三篇里面最無聊的一篇了,畢竟不管在技術還是邏輯上,都是涉及的最少的地方,不過既然要山寨就得山寨的專業,所以首先我決定對於配色、畫面的比例來個專業點的山寨,於是,最開始,我研究出來的界面比例和顏色啥的是這樣的:
但是我最終做出來的界面是這樣的:
這些個比例如何得到呢?我原先的想法是用截圖然后用PS量,是不是聽起來特別的有誠意,但實際上,看看這字體大小,位置,方塊的顏色,唉!實際我的做法是在代碼里拍腦袋的決定,然后調試之后發現沒有遮擋和看着還行就可以了。事實再一次說明懶惰和湊合絕逼是普通人和牛人之間的一個重大差距啊!這么看來處女座的程序員應該更容易成為大牛!
這個游戲在設計上,我使用四個矩形來進行布局,一個是左上角的名稱,然后是右上角的最高分和當前得分,最后是游戲區域。
雖然不是按照完美的山寨的理想,但是我還是有一些設計的,我沒有采用固定坐標的方法來排布界面是因為我當時考慮到在不同分辨率的電腦上希望能有一個看起來差不多的界面比例。但是我最初在這里的一個細節上的錯誤導致了在不同的電腦上界面的極端不一致,雖然我現在進行了改進,但是還不是最完美的方案,如果有興趣,可以繼續修改。
最初我的代碼大致是這樣的:
m_rtGameBorder.left = m_rtClient.right/8; m_rtGameBorder.top = m_rtClient.bottom/6; m_rtGameBorder.right = 7*m_rtClient.right/8; m_rtGameBorder.bottom = m_rtClient.bottom/6 + 3*m_rtClient.right/4;
對於除法,我直接使用的是整數除以整數,這樣導致四舍五入之后誤差較大,在一些電腦上可能界面剛剛好,但是在另外一些電腦上,就會導致重疊。所以最終我把代碼改成了這樣:
m_rtGameBorder.left = (double)m_rtClient.right/8.0; m_rtGameBorder.top = (double)m_rtClient.bottom/6.0; m_rtGameBorder.right = 7.0*(double)m_rtClient.right/8.0; m_rtGameBorder.bottom = (double)m_rtClient.bottom/6.0 + 3.0*(double)m_rtClient.right/4.0;
這樣的代碼在某種方面解決了一些問題,但畢竟不是真正的完美解決方案,如果想真正的完美解決,可以參考GDI里面關於映射方式的內容。
名稱區和得分區沒有什么特別的技術含量,只要你選好顏色,調好坐標,調用MFC 的畫圖函數就可以做到,這里的得分區的配色我是完全山寨的原來游戲,名稱區嘛,湊合一下好了,哈哈。
對於游戲區,主要分三個部分,外面的大邊框,格子線和每個游戲方塊。
我從一開始就決定這個山寨游戲自由度要高一點,不僅可以4x4,還可以5x5等等,所以我留下了一個接口,在后面我還會對此進行說明。這個接口函數使用兩個參數,重要的一個是行/列數,主要方法就是使用邊長為(行數+1)的小正方形進行挨個填充,這樣我想的一個好處是可以用剩下的空間作為這些小正方形的間隔距離。比如說,填充一個4x4的游戲區域,假設我們的大游戲框架是一個600像素x600像素的區域,那么每個小的游戲方塊就是邊長為600/(4+1)=120像素大小的正方形,這樣會剩下一個120大小的區域沒有填充,這時正好將這個120分成5份,成為這4個正方形之間的間隔。寫成代碼我反而覺得更容易理解這個邏輯:
void CChildView::DrawGrid(int nRowAndCol,CPaintDC *dc) { CRect tmpRect; tmpRect.CopyRect(&m_rtGrid); for (int i=0;i<m_nRowAndCol;i++) { for(int j=0;j<m_nRowAndCol;j++) { dc->FillRect(&tmpRect,&m_brhGrid); tmpRect.left = tmpRect.left + tmpRect.Width() + m_rtGameBorder.Width()/(double)((m_nRowAndCol+1)*(m_nRowAndCol+1)); tmpRect.right = tmpRect.left + m_rtGameBorder.Width()/(double)(m_nRowAndCol+1); } tmpRect.left = m_rtGameBorder.left + m_rtGameBorder.Width()/(double)((m_nRowAndCol+1)*(m_nRowAndCol+1)); tmpRect.right = tmpRect.left + m_rtGameBorder.Width()/(double)(m_nRowAndCol+1); tmpRect.top = tmpRect.top + tmpRect.Height() + m_rtGameBorder.Width()/(double)((m_nRowAndCol+1)*(m_nRowAndCol+1)); tmpRect.bottom = tmpRect.top + m_rtGameBorder.Width()/(double)(m_nRowAndCol+1); } return; }
說完了游戲區域,下面主要的一個就是一個個的游戲方塊了,對於這個方塊,我想簡單的封裝一下。我想了想2048的方塊,總結了下,這些個要素是必須的:數字(文字)、顏色、當前的位置。雖然說這顏色和文字是一一對應的,但是我覺得這個冗余設置會在編碼上帶來方便。除了這三個信息,我還使用的兩個信息是bshow 和bjoin,第一個是用來標識當前游戲塊是否顯示狀態,后面一個是為了表示當前代碼塊是否被合並過,這兩個成員變量都對我的編碼帶來了極大的方便。做這樣一個小小的封裝還有一個好處就是在后面如果想進行擴展,會十分的方便。這一點在后面三篇文章中更會感受的更加深刻。
#pragma once class ItemBox { public: ItemBox(void); ~ItemBox(void); public: int nRowIndex; int nColIndex; CString strItemText; COLORREF crItem; bool bShow; bool bJoin; };
在使用這個簡單的封裝以后,在程序的一開始就利用一個行數×行數的數組,這個數組里是一個個的ItemBox,然后在一定時候初始化它們就可以了。我這樣做的目的主要是在程序一開始的時候全部建立起來,這樣只要在析構的時候全部銷毀就不會造成內存的問題,相比只有在游戲方塊出現的時候再new一個新的,這種方法不僅可以減少編程的復雜性而且可以防止造成內存的問題。
建立起這樣的數組,下面的問題就是如何在游戲區域的指定位置繪畫出游戲方塊了,我想,最自然的思維方式就是根據方塊的坐標在指定位置繪畫出其圖形,這也是為什么要在封裝的信息里提供一個位置的信息。根據最熟悉的二維笛卡爾坐標體系,首先我們得給所有的方格賦予初值,這些初值包括,每一個游戲方塊的縱坐標,橫坐標,文字,顏色,是否顯示(自然是不顯示),是否被合並過(貌似名字叫bjoined更靠譜)。然后按照我們得產生兩對不一樣的坐標,因為根據其規則最開始是有兩個方塊產生的。那么就不得不使用隨機數了,而且要保證這個坐標不能越界,這里我使用了一個簡單的隨機數函數。值得注意的細節就是這個隨機數一定要設置種子,因為如果不是這樣的話,在后面大量需要隨機數的時候就會很容易產生相同的坐標。還有一個就是即使是最開始產生的這兩個坐標,也一定要保證其不能相同,我采用的是一個循環檢測的辦法。這里就要使用是否當前位置的游戲塊是顯示狀態,如果是,那么就繼續產生坐標,直到找到未顯示的塊位置為止。
void CChildView::InitializeItemBoxes() { for(int i=0; i<m_nRowAndCol*m_nRowAndCol; i++) { m_itemBoxArray[i].nRowIndex = i/m_nRowAndCol; m_itemBoxArray[i].nColIndex = i%m_nRowAndCol; m_itemBoxArray[i].strItemText = _T(""); m_itemBoxArray[i].crItem = m_arrClrItems[1]; m_itemBoxArray[i].bShow = false; m_itemBoxArray[i].bJoin = false; //DrawNumber(m_itemBoxArray[i]); } GetRand(4.0,0.0); int nCol = GetRand(4.0,0.0); int nRow = GetRand(4.0,0.0); int nCol2 = GetRand(4.0,0.0); int nRow2 = GetRand(4.0,0.0); m_itemBoxArray[nRow*4+nCol].bShow = true; m_itemBoxArray[nRow*4+nCol].strItemText = m_arrStrItemTexts[0];// _T("2"); m_itemBoxArray[nRow*4+nCol].crItem = m_arrClrItems[1]; while(m_itemBoxArray[nRow2*4+nCol2].bShow) { nCol2 = GetRand(4.0,0.0); nRow2 = GetRand(4.0,0.0); } m_itemBoxArray[nRow2*4+nCol2].bShow = true; m_itemBoxArray[nRow2*4+nCol2].strItemText = m_arrStrItemTexts[0];// _T("2"); m_itemBoxArray[nRow2*4+nCol2].crItem = m_arrClrItems[1]; }
這些都做完了,接着就是如何將這些和MFC的OnPaint結合了,如何繪制出一個啟動的出是畫面。我采用的方法是利用上面說的bshow,如果當前位置需要顯示方塊,那么就選取相應的顏色,輸出相應的文字,這三點就是前面封裝的三個成員變量。如果不需要,那么就選取方塊的本身的背景顏色。這樣就造成了一種某個方塊沒有顯示的假象,在OnPaint的每次重繪里,遍歷所有方塊區域,然后根據設置對每個方塊進行處理,是要繪制出相應的游戲方塊,還是只是繪制背景色。另外,我把分數的更新也放在了這里。
int CChildView::DrawNumber(const ItemBox& itembox) { CRect rtNumber; CBrush brhNumber; rtNumber.left = m_rtGrid.left + itembox.nColIndex*m_rtGrid.Width() + itembox.nColIndex* m_rtGameBorder.Width()/(double)((m_nRowAndCol+1)*(m_nRowAndCol+1)); rtNumber.right = rtNumber.left + m_rtGameBorder.Width()/(double)(m_nRowAndCol+1); rtNumber.top = m_rtGrid.top + itembox.nRowIndex*m_rtGrid.Height() + itembox.nRowIndex*m_rtGameBorder.Width()/(double)((m_nRowAndCol+1)*(m_nRowAndCol+1)); rtNumber.bottom = rtNumber.top + m_rtGameBorder.Width()/(double)(m_nRowAndCol+1); COLORREF clrItemBackground = m_arrClrItems[GetIndex(itembox.strItemText)]; if(itembox.bShow) brhNumber.CreateSolidBrush(clrItemBackground); else brhNumber.CreateSolidBrush(RGB(205,193,179)); CClientDC dc(this); dc.SelectObject(&m_itemFont); dc.FillRect(&rtNumber,&brhNumber); // DBDBDB SetBkColor(dc.m_hDC,clrItemBackground); LOGFONT lf; m_itemFont.GetLogFont(&lf); CSize sz; TEXTMETRIC tm; sz = dc.GetTextExtent(itembox.strItemText); dc.GetTextMetrics(&tm); int yOffset = (rtNumber.Height()-lf.lfHeight)/2; int xOffset = (rtNumber.Width()- sz.cx)/2; dc.TextOut(rtNumber.left+xOffset,rtNumber.top+yOffset,itembox.strItemText); dc.SelectObject(&m_scoreNumberFont); dc.SetTextColor(RGB(250,248,239)); SetBkColor(dc.m_hDC,RGB(187,173,160)); CString strScore; strScore.Format(_T("%d"),m_nScore); dc.TextOut(m_rtScore.left+10,m_rtScore.top+35,strScore); m_nBestScore = m_nScore>=m_nBestScore?m_nScore:m_nBestScore; CString strBestScore; strBestScore.Format(_T("%d"),m_nBestScore); dc.TextOut(m_rtBest.left+10,m_rtBest.top+35,strBestScore); return 0; }
關於界面篇,就只有這么多了,感覺這篇我寫的很無聊,全靠代碼湊的數,但是這個小游戲的界面還是比較簡單,需要注意的應該主要是細節,而大部分細節又被我湊合掉了。但是,說實話,界面絕對是一個產品成功與否的關鍵,所以如果不是山寨而是自己原創的東西的話,一定要畫大心思在界面上。除非你的軟件的用處是從全世界的銀行賬戶中都轉10塊錢到自己的賬戶里,那樣無論你的界面有多丑,按鈕有多難找,有多難操作,用戶都會去孜孜不倦的尋找正確的用法。還有一點就是,做桌面版軟件一定要在多個電腦上測試,因為可能一切界面在你的電腦中良好,到另外一個電腦里就是面目全非了。
界面篇寫完了,下面是邏輯部署篇和優化篇,這兩篇從字面上就比界面有意思,但是沒有界面再好的軟件也出不來,咋說呢,感興趣的先忍忍,我拼盡全力不讓忍忍的朋友失望。
另外:感興趣,想所要源代碼的同學,可以去我的博客(那里有郵箱喔),再次請猛戳(http://www.richinmemory.com/),我仿佛已經聽到每個人心中的罵聲,畢竟我的一大目的是希望自己的博客能夠猥名遠揚。所以,先忍忍,我還是會拼盡全力不讓忍忍的朋友失望。

