C語言小遊戲: 2048.c
概要:2048.c是一個C語言編寫的2048遊戲,本文將詳細分析它的原始碼和實現。C語言是一種經典實用的程式語言,本身也不復雜,但是學會C語言和能夠編寫實用的程式還是有一道鴻溝的。本文試圖通過一個例子展示如何用C語言實現一個簡單但有用的程式。
一、程式簡介
本文分析的程式碼是mevdschee在GitHub上的專案2048.c,遊戲的規則和安裝說明都可以到主頁檢視,本文不再贅述。順便一提,這個程式雖然是純C編寫的,但是它適用於Linux終端,因此如果你想要看一下執行效果可能需要一個Linux.
2048.c原始碼只有一個檔案,也就是2048.c。它支援圖形和色彩,右上角顯示分數,下面是操作說明。介面整體看起來挺簡潔美觀,我們一會看一下它是怎麼做到的。
二、程式碼結構
我們先看一下程式所包含的函式,大體瞭解它的結構和功能。
程式入口和測試:
- main (argc,argv[])
- test ()
繪製介面相關:
- getColor (value,color,length)
- drawBoard (board[][])
- setBufferedInput (enable):設定終端的行為
- signal_callback_handler (signum)
遊戲邏輯:
- findTarget (array[],x,stop)
- slideArray (array[])
- rotateBoard (board[][])
- moveUp (board[][])
- moveLeft (board[][])
- moveDown (board[][])
- moveRight (board[][])
- findPairDown (board[][])
- countEmpty (board[][])
- gameEnded (board[][])
- addRandom (board[][])
- initBoard (board[][])
從函式的引數中可以看出,遊戲使用的主要的資料結構是一個二維陣列,在主函式中定義: uint8_t board[SIZE][SIZE] 。SIZE的值預設是4,這是2048遊戲面板的一般大小,下文直接稱為4。陣列中的元素儲存的是指數,例如如果顯示的數是1024,那麼儲存的應該是10。在初始化過程中,該陣列被填滿0.
主函式中完成一些初始化和設定工作,然後進入主迴圈。在迴圈中接受使用者的鍵盤輸入,然後呼叫相應的函式。
三、圖形繪製函式
1 void drawBoard(uint8_t board[SIZE][SIZE]) { 2 uint8_t x,y; 3 char color[40], reset[] = "\033[m"; 4 printf("\033[H"); 5 6 printf("2048.c %17d pts\n\n",score); 7 8 for (y=0;y<SIZE;y++) { 9 for (x=0;x<SIZE;x++) { 10 getColor(board[x][y],color,40); 11 printf("%s",color); 12 printf(" "); 13 printf("%s",reset); 14 } 15 printf("\n"); 16 for (x=0;x<SIZE;x++) { 17 getColor(board[x][y],color,40); 18 printf("%s",color); 19 if (board[x][y]!=0) { 20 char s[8]; 21 snprintf(s,8,"%u",(uint32_t)1<<board[x][y]); 22 uint8_t t = 7-strlen(s); 23 printf("%*s%s%*s",t-t/2,"",s,t/2,""); 24 } else { 25 printf(" · "); 26 } 27 printf("%s",reset); 28 } 29 printf("\n"); 30 for (x=0;x<SIZE;x++) { 31 getColor(board[x][y],color,40); 32 printf("%s",color); 33 printf(" "); 34 printf("%s",reset); 35 } 36 printf("\n"); 37 } 38 printf("\n"); 39 printf(" ←,↑,→,↓ or q \n"); 40 printf("\033[A"); // one line up 41 }View Code
在drawBoard函式中我們看到繪製的實現過程。函式的主體是一個for迴圈,每迴圈一次畫一行,這裡指的是Board中的一行。迴圈體中有3個小for迴圈,每個迴圈畫出終端中的一行,也就是說Board的一行是終端的3行。每個格子的尺寸是3行7列,最中間的位置是數字,如果沒有數字則輸出一個點。其他區域則用空格填充。
細心的朋友可能已經發現,外迴圈的變數是y,內迴圈的變數為x,這樣一來board[0][0]到board[3][0]表示的是第1行,board[0][1]到board[3][1]表示第2行,這種對應關係需要特別注意。
"\033m"之類的符號用於控制終端的顏色和其他一些行為。下面給出本程式中出現的用法,更多控制序列的用法可以參考這個網頁。通過輸出帶顏色的空格和字元,2048.c在終端中實現了類似圖形介面的效果。
\33[0m 關閉所有屬性
\33[30m -- \33[37m 設定前景色
\33[40m -- \33[47m 設定背景色
\33[nA 游標上移n行
\33[nB 游標下移n行
\33[nC 游標右移n行
\33[nD 游標左移n行
\33[y;xH設定游標位置
\33[2J 清屏
\33[?25l 隱藏游標
\33[?25h 顯示游標
四、遊戲邏輯
我們現在已經知道遊戲的主要資料結構,以及如何將它顯示在螢幕上,我們接下來要關注遊戲羅傑是怎麼實現的。2048遊戲本身非常簡單,其實我們只想關心劃的那一下是怎麼實現的。我們已經看到2048.c實現了moveUp、moveLeft、moveDown、moveRight四個函式,表示4個劃的方向。
moveUp函式看起來也非常簡單,它僅僅呼叫4次slideArray函式。還記得剛剛說過的二維陣列和盤面的對應規則嗎,矩陣的每一行代表的是盤面的一列,因此每次滑動一個一維陣列,實際上滑動的是一列。slideArray函式負責將陣列從高index到低index滑動,對應在螢幕上,也就是向上滑動了。
1 bool moveUp(uint8_t board[SIZE][SIZE]) { 2 bool success = false; 3 uint8_t x; 4 for (x=0;x<SIZE;x++) { 5 success |= slideArray(board[x]); 6 } 7 return success; 8 }View Code
slideArray函式和它的輔助函式findTarget任務已經比較簡單明瞭,就不需要詳細說了。需要注意的就是在滑的時候合併的塊不能第二次合併了,例如2 2 2 2一次合併的結果是4 4,而不會是8.
其他幾個函式實現比較巧妙,作者先把盤面進行旋轉,然後再呼叫這個moveUp函式實現。作者通過rotateBoard函式把這個4x4的矩陣旋轉90度。陣列的下標可以通過建立座標系得到。
1 void rotateBoard(uint8_t board[SIZE][SIZE]) { 2 uint8_t i,j,n=SIZE; 3 uint8_t tmp; 4 for (i=0; i<n/2; i++) { 5 for (j=i; j<n-i-1; j++) { 6 tmp = board[i][j]; 7 board[i][j] = board[j][n-i-1]; 8 board[j][n-i-1] = board[n-i-1][n-j-1]; 9 board[n-i-1][n-j-1] = board[n-j-1][i]; 10 board[n-j-1][i] = tmp; 11 } 12 } 13 }View Code
瞭解了這些資訊,再看其他的函式比如countEmpty、addRandom等就非常簡單了,大家直接去看程式碼就可以了。
五、總結
2048.c這個小遊戲雖然只有400多行,但復現了2048遊戲的精髓。而且程式以純C語言實現,沒有使用ncurses之類的第三方庫,得到了很不錯的效果。實現的過程也有一些精巧的地方,例如如何把問題化繁為簡的,如何避免多次編寫move函式。其實2048.c不僅可以拿來閱讀,無聊的時候玩一局也是相當不錯的。