標籤:c++ 物件導向 開發 gtkmm 2048
本專欄文章列表
一、何為物件導向
二、C語言也能實現物件導向
三、C++中的不優雅特性
四、解決封裝,避免介面
五、合理使用模板,避免代碼冗餘
六、C++也能反射
七、單例模式解決靜態成員對象和全域對象的構造順序難題
八、更為進階的前置處理器PHP
九、Gtkmm的最佳實務
九、Gtkmm的最佳實務
在跨平台的gui開發中,Qt一直是非常受歡迎的GUI開發架構,但Qt一個是依賴反射,需要特殊的預先處理步驟,一個是庫太過龐大,這就造成了一些不便的地方。今天介紹給大家的是Gtk庫的C++綁定,Gtkmm,一個方便的跨平台GUI開發架構。
由於是C++的封裝,GTK不再那麼的難以使用,變得簡潔優雅,而且效率非常高,編譯也較QT快許多。
雖然C也能編寫,而且我們之前也介紹過了GObject的使用。但比較其實現起來較為繁瑣,程式碼數較C++多一些,而且每個成員函數都要手動傳入this指標,較為不便。
現在C++如果合理的封裝和按照之前的設計思想進行設計,結構十分緊湊,而且書寫非常方便,非常易用。
Gtkmm版的2048程式設計
為了更好的實踐,我們舉一個簡單的2048小遊戲的程式作為執行個體。大家會發現,合理的設計,能夠使代碼既清晰明了,又方便維護,可靠性很高。
我們簡要的進行一下程式設計,這裡我們不是要學會2048如何製作,而是要體會程式設計中的思想,以及設計中的美感和藝術感。
首先,2048作為一個簡單的小遊戲,廣受大家喜歡,原理很簡單,在一個4×4的數組中,讓數字不斷向各個方向合并,每次進行後,隨機位置建立新數字。
程式介面,一個視窗,上面一列標籤書寫當前得分,下面一個繪圖介面,自由繪圖,畫上4×4個的矩陣,上面書寫內容。
程式結構設計,按照一般程式架構設計,可以用MVC的結構,一個介面類,負責顯示,一個控制類,負責遊戲邏輯,一個模型類,負責資料的儲存與管理。
但由於資料的管理太過簡單,就放棄了模型類,直接使用一個4×4的矩陣就完成任務。
程式實踐
由於Gtkmm的良好封裝,我們並不需要太多複雜的處理,首先是Main檔案引導程式的啟動,所有gtk程式集合都是這樣引導。
/* * @Author: sxf* @Date: 2015-05-07 12:22:40* @Last Modified by: sxf* @Last Modified time: 2015-05-20 21:58:16*/#include <gtkmm.h>#include "game.h"int main(int argc, char *argv[]){ Glib::RefPtr<Gtk::Application> app = Gtk::Application::create(argc, argv, "org.abs.gtk2048"); Game game; //Shows the window and returns when it is closed. return app->run(game);}
Game類作為最核心的視窗類別,也是遊戲的主要控制類,並不需要暴露什麼方法給外部成員使用,所以它的定義很簡單:
/* * @Author: sxf* @Date: 2015-05-19 11:20:42* @Last Modified by: sxf* @Last Modified time: 2015-05-20 21:58:35*/#ifndef GAME_H#define GAME_H#include <gtkmm/window.h>class Game_private;class Game : public Gtk::Window{public: Game(); virtual ~Game();private: Game_private* priv;};#endif // GAME_H
這種寫法,就是在前幾章提到的增強代碼封裝性的方法,通過一個priv指標,解決了C++封裝不完善的問題。
這樣還有一個很大的好處就是,由於priv指標的書寫較為繁瑣,如果在public方法中,反覆的通過priv指標調用函數,就會顯得無比麻煩,但這正提醒你,你的寫法有問題,因為一般的方法,要儘可能寫成內部的private的,這樣你在不自覺的時刻,就形成了最大化private方法,最小化介面的設計習慣,這對於提升程式的內聚性,很有意義。
於是我們的Game類的內部定義就變得十分複雜,但這就使得代碼內聚性更高,暴露給外層的介面就更簡單。
class Game_private{public: Game_private(Game* game); ~Game_private(); Game* game; MyArea m_area; // 渲染類 Gtk::Box m_box; // 布局控制項 Gtk::Label m_score; // 得分標籤 int score; // 得分具體數字 const static int fx[4][2]; int data[4][4]; // 資料模型 bool combine(int i, int j, int k); // 將一個位置的數字向下合并 bool randomNew(); // 隨機建立新數字 void cleanData(); // 刪除全部數字,用來開局初始化 void gameWin(); // 顯示使用者勝利 void gameOver(); // 顯示遊戲結束 void gameRun(int k); // 遊戲控制迴圈 bool on_key_press_event(GdkEventKey* event); // 監聽鍵盤事件響應};
我不喜歡比臉還長的函數,但這裡的函數設計的還是不是那麼盡如人意,雖然如此,這裡也是本著簡單易懂的方式設計的。
這裡的combine方法設計的很特殊,因為合并時,還有可能出現遊戲勝利的情況,所以裡麵包含了判斷勝利的條件。
bool combine(int i, int j, int k) { int ni = i; int nj = j; ni += fx[k][0]; // fx是方向數組 nj += fx[k][1]; int obji = i, objj = j; while ( 0 <= ni && ni < 4 && 0 <= nj && nj < 4 ) // 防止越界,我這裡比較貪便宜,很多人處理越界是通過在數組外增加一個特殊值的外邊框來處理的 { if (data[ni][nj] == 0) { obji = ni; objj = nj; } else { if (data[ni][nj] == data[i][j]) { score += (1 << data[ni][nj]); // 處理得分 ++data[ni][nj]; data[i][j] = 0; if (data[ni][nj] == 12) return true; // 處理勝利條件 return false; } else break; } ni += fx[k][0]; nj += fx[k][1]; } if (!(obji == i && objj == j)) { // 未能合并的情況 data[obji][objj] = data[i][j]; data[i][j] = 0; } return false;}
這個函數設計的很健壯,考慮了許多邊界條件,這麼做是在類比物體碰撞時,碰撞面不斷擠壓的情況。例如下面的情況:
1 1 2 4
0 0 0 0
0 0 0 0
0 0 0 0
向左合并,能夠一次就被合成為8,但這也是和外層的合并順序控制是分不開的
在遊戲主迴圈控制時,是這樣處理的,對於不同的方向,迴圈順序是不一樣的:
void gameRun(int k) { bool winflag = false; switch (k) { case 0 : for (int i = 0; i < 4; ++i) for (int j = 0; j < 4; ++j) if (combine(i,j,k)) winflag = true; break; case 1 : for (int j = 3; j >= 0; --j) for (int i = 0; i < 4; ++i) if (combine(i,j,k)) winflag = true; break; case 2 : for (int i = 3; i >= 0; --i) for (int j = 0; j < 4; ++j) if (combine(i,j,k)) winflag = true; break; case 3 : for (int j = 0; j < 4; ++j) for (int i = 0; i < 4; ++i) if (combine(i,j,k)) winflag = true; break; } // 判斷勝負條件 if (winflag) { gameWin(); return; } if (!randomNew()) { gameOver(); } Glib::RefPtr<Gdk::Window> win = game->get_window(); if (win) { m_area.setData(data); Gdk::Rectangle r(0, 0, 600, 600); win->invalidate_rect(r, false); m_area.show(); char score_text[20]; memset(score_text, 0, 20); sprintf(score_text, "Score : %d", score); m_score.set_text(score_text); } }
而建立新數位方式也很清晰,但這裡使用類比棧的方式進行了處理。
設計很獨特,由於目前的位置數目有限,直接rand的方式,效率較低,我們先掃描所有可能的位置,然後將其入棧,random時,直接找一個位置,然後直接隨機從其中找一個就可以了。數組類比棧的方式,主要是希望避免vector的低效率,實現較簡易。而且擴充較方便,如果你想random跟多,修改起來也十分方便。
bool randomNew() { int empty_block[17][2]; int sum = 0; for (int i = 0; i < 4; ++i) { for (int j = 0; j < 4; ++j) { if (data[i][j] == 0) { empty_block[sum][0] = i; empty_block[sum][1] = j; ++sum; } } } if (sum < 1) return false; int t = rand() % sum; data[ empty_block[t][0] ][ empty_block[t][1] ] = (rand() % 4) < 3 ? 1 : 2; empty_block[t][0] = empty_block[sum-1][0]; empty_block[t][1] = empty_block[sum-1][1]; --sum; return true;}
渲染類十分簡單,主要就是根據數組中的數值,渲染出對應的映像,設計思想就是不斷將問題抽象,不斷簡化,將複雜的問題從上層一層層撥開,這樣就使得結構更加簡潔優雅。
整個項目完整代碼已經放到Github上了,需要的可以參考:
【github倉庫】
C和C++的物件導向專題(9)——Gtkmm的最佳實務