桌面山寨版2048---界面雖無聊,細節很重要

        弄個我的獨立的博客是個人一個一直存在想法,畢竟不想當優秀工程師的程序員不是好碼農,因此,我一直但願有個獨立的博客來記錄和分享本身的想法,最重要的但願能有一個平臺認識不少志同道合的人。可是弄完了以後怎樣讓別人知道我是志同道合的本身人又讓我陷入了沉思,百般思考以後,我以爲在這裏更新日誌是一個既不會被封殺又最有效的辦法,因此在個人博客沒有穩定以前,我會一直在這裏和獨立博客中一塊兒更新,直到個人博客可以猥名遠揚,被你們所知曉,但願我能成功吧!(這一節在博客那邊要不要去掉呢?思前想後,我仍是去掉了)c++

         請猛戳:http://www.richinmemory.com/git

         第一次玩2048的時候是github上的那個網頁版,當時真的是根本停不下來,玩着玩着我忽然冒出一個想法,這遊戲操做啥的都不復雜,並且最近下班閒着的無聊時光還蠻多的,因此我以爲我能夠試試能不能山寨一個。既然要山寨就得要山寨的專業,雖然這是個開源的項目,可是我 沒有看過一行他自己的代碼,由於我要作個有節操的山寨者。考慮到語言我最熟悉的莫過於的c/c++了,因此我也只能選擇他了。在山寨的過程當中,我深深的體會到了任何一個能夠拿到給你們使用的軟件都是不能大意的,一個小小的細節上的沒有注意致使的就是別人在使用上的bug。程序員

 1、2048山寨貨之界面篇github

        界面是我要寫的三篇裏面最無聊的一篇了,畢竟無論在技術仍是邏輯上,都是涉及的最少的地方,不過既然要山寨就得山寨的專業,因此首先我決定對於配色、畫面的比例來個專業點的山寨,因而,最開始,我研究出來的界面比例和顏色啥的是這樣的:編程

       

        可是我最終作出來的界面是這樣的:數組

        

        這些個比例如何獲得呢?我原先的想法是用截圖而後用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/),我彷彿已經聽到每一個人心中的罵聲,畢竟個人一大目的是但願本身的博客可以猥名遠揚。因此,先忍忍,我仍是會拼盡全力不讓忍忍的朋友失望。

 

      

       

相關文章
相關標籤/搜索