[0]起因
花了一個星期左右的時間學習了下FLTK圖形庫,拿掃雷游戲作為學習FLTK圖形庫的原因有二:
- 掃雷游戲算法很簡單;
- 之前寫過一個Win32API的掃雷游戲。(自己回頭去看看,感覺以前的按過程式的方法設計的代碼不便於理解,結構不清晰)
[1]設計一個可以載入圖片的GameObject類
[1.1]類的聲明如下:
class GameObject { public: GameObject(void); GameObject(const char* filename); GameObject(const GameObject& a_rightSide); GameObject& operator=(const GameObject& a_rightSide); ~GameObject(void) {}//沒有需要刪除的 void setImage(const char* filename); float getX(void)const {return m_pBox->x();} float getY(void)const {return m_pBox->y();} float getWidth(void)const {return m_pBox->w();} float getHeight(void)const {return m_pBox->h();} void setPosition(float a_x, float a_y); void setSize(float a_width, float a_height); private: auto_ptr<Fl_Box> m_pBox;//圖片框 auto_ptr<Fl_Image> m_pImage;//圖片 Fl_Shared_Image* m_pSharedImage;//共享區圖片 };
[1.2]類的定義
這里使用了組合的方式,將Fl_Box,Fl_Image組合在一起(使用了智能指針方便管理內存)。而且自己定義了拷貝構造函數和賦值運算符,實現自定義的賦值操作(主要是方便存放在vector里面)。方法的定義如下:
inline GameObject::GameObject(void) :m_pSharedImage(NULL) { m_pBox.reset(new Fl_Box(0,0,0,0)); } inline GameObject::GameObject(const char* filename) :m_pSharedImage(NULL) { m_pBox.reset(new Fl_Box(0,0,0,0,0)); setImage(filename);//設置圖片 } inline GameObject::GameObject(const GameObject& a_rightSide) { m_pBox.reset(new Fl_Box(a_rightSide.getX(),a_rightSide.getY(), a_rightSide.getWidth(),a_rightSide.getHeight()));//構建一個新的大小位置相同的box m_pSharedImage = Fl_Shared_Image::get(a_rightSide.m_pSharedImage->name());//獲取相同的sharedImage m_pImage.reset(m_pSharedImage->copy(a_rightSide.getWidth(),a_rightSide.getHeight()));//構建一幅大小相同的Image m_pBox->image(m_pImage.get()); } inline GameObject& GameObject::operator=(const GameObject& a_rightSide) { m_pBox->resize(a_rightSide.getX(),a_rightSide.getY(), a_rightSide.getWidth(),a_rightSide.getHeight()); m_pSharedImage = Fl_Shared_Image::get(a_rightSide.m_pSharedImage->name()); m_pImage.reset(m_pSharedImage->copy(a_rightSide.getWidth(),a_rightSide.getHeight()));//構建一幅大小相同的Image m_pBox->image(m_pImage.get()); return *this; } //設置圖片 inline void GameObject::setImage(const char *filename) { fl_register_images(); m_pSharedImage = Fl_Shared_Image::get(filename); m_pImage.reset(m_pSharedImage->copy()); m_pBox->image(m_pImage.get()); } //設定位置和大小 inline void GameObject::setPosition(float a_x, float a_y) { m_pBox->position(a_x,a_y); } inline void GameObject::setSize(float a_width, float a_height) { m_pBox->size(a_width,a_height); m_pImage.reset(m_pSharedImage->copy(a_width,a_height)); m_pBox->image(m_pImage.get());//重新修改image,因為之前的image會自動銷毀 }
這樣實現對於做控件的背景圖片還是可以的,但是有個重要的缺點是響應事件的問題。如果不是繼承含有handle()方法的類實現事件響應必須使用回調函數callback()(這只是我這幾天看FLTK1.3.0幫助文檔發現的,或許還有更好的方法我沒有發現)。所以有了下面的修改,就可以重寫父類的handle()方法來實現自定義的事件響應。
[1.3]使用到的FLTK類
[2]改進的圖片框ImageBox類
[2.1]類的實現
//用於放置圖片的圖片框 class ImageBox:public Fl_Box { public: ImageBox(void):Fl_Box(0,0,0,0,0) {} ImageBox(const char* filename):Fl_Box(0,0,0,0,0) { setImage(filename); } ImageBox(const ImageBox& a_rightSide) :Fl_Box(a_rightSide.getX(),a_rightSide.getY(),a_rightSide.getWidth(),a_rightSide.getHeight(),0) { m_x = a_rightSide.getX(); m_y = a_rightSide.getY(); m_width = a_rightSide.getWidth(); m_height = a_rightSide.getHeight(); m_pSharedImage = Fl_Shared_Image::get(a_rightSide.m_pSharedImage->name());//獲取相同的sharedImage m_pImage.reset(m_pSharedImage->copy(m_width,m_height));//構建一幅大小相同的Image image(m_pImage.get()); redraw(); } ImageBox& operator=(const ImageBox& a_rightSide) { m_x = a_rightSide.getX(); m_y = a_rightSide.getY(); m_width = a_rightSide.getWidth(); m_height = a_rightSide.getHeight(); resize(m_x,m_y,m_width,m_height); m_pSharedImage = Fl_Shared_Image::get(a_rightSide.m_pSharedImage->name());//獲取相同的sharedImage m_pImage.reset(m_pSharedImage->copy(m_width,m_height));//構建一幅大小相同的Image image(m_pImage.get()); redraw(); return *this; } //設置圖片 void setImage(const char *filename) { fl_register_images(); m_pSharedImage = Fl_Shared_Image::get(filename); m_pImage.reset(m_pSharedImage->copy()); image(m_pImage.get()); redraw(); } int getX(void)const {return x();} int getY(void)const {return y();} int getWidth(void)const {return w();} int getHeight(void)const {return h();} void setPosition(int a_x, int a_y) {position(a_x,a_y);} void setSize(int a_width, int a_height) { m_width = a_width; m_height = a_height; size(m_width,m_height); m_pImage.reset(m_pSharedImage->copy(m_width,m_height)); image(m_pImage.get());//重新修改image,因為之前的image會自動銷毀 redraw(); } //Event處理 int handle(int event) { if (!m_enable) return (0);//點開數字后失去操作權 using std::cout; using std::endl; switch (event) { case FL_PUSH: switch (Fl::event_button()) { case FL_LEFT_MOUSE: cout << "你點擊了鼠標左鍵" << endl; cout << "m_width=" << m_width << endl; m_pSharedImage = Fl_Shared_Image::get("res/images/bg.jpg"); m_pImage.reset(m_pSharedImage->copy(m_width,m_height)); image(m_pImage.get());//重新修改image,因為之前的image會自動銷毀 redraw(); break; } break; } return 0; } private: auto_ptr<Fl_Image> m_pImage;//圖片 Fl_Shared_Image* m_pSharedImage;//共享區圖片 int m_x,m_y; int m_width,m_height; bool m_enable;//是否可以操作(當點開數字后就設為false) };
這里包含了類的定義。在這里需要提到一個重要的問題,每當更換image后需要調用redraw()函數(第一個GameObject類沒有使用,所以它更換圖片的時候不會自動刷新的)。
[2.2]這種實現方式的優點
- 可以很方便的處理事件;
- 繼承FLTK的Fl_Box類,結構更清晰。
- 注:c++primer里有句經典的話,“拷貝構造函數、重載賦值運算符、析構函數三個中只要定義了其中一個就需要定義其他兩個”。在這里由於使用了智能指針auto_ptr的緣故,並不需要自己定義析構函數。
[2.3]handl()函數
使用handle()函數時,里面調用方法的過程大概是這樣的(掃雷游戲的基本鼠標操作):
- 單擊左鍵事件處理(完成點開格子的過程,調用掃雷算法的方法,完成image的修改,並置這些圖片為不可再響應事件);
- 單擊右鍵事件處理(該過程只是改變image狀態,右鍵第一次后改為紅旗,右鍵第二次改為問號,右鍵第三次改為原樣(前兩個狀態不能響應左鍵事件))。
[2.4]測試函數
下面是兩個測試函數: void Test::test_copy_construdtor(GameObject& t1, GameObject& t2) { //測試拷貝構造函數 t2.setSize(800,500); t1 = t2; t2.setSize(300,400); // t1.setSize(800,300); /* t2 = t1; t1.setSize(700,500); t1 = t2; t2.setSize(700,400); */ } void Test::test_ImageBox(ImageBox& t) { t.setSize(250,250); }
[2.5]使用到的FLTK類
注:后面由於掃雷游戲用到圖片框主要是每個格子使用圖片框,我就把名字改成了GridBlock類,定義和ImageBox差不多,只是后面慢慢的有些改進。
[3]進一步對掃雷游戲的設計
[3.1]設計單擊鼠標左鍵后的事件處理流程
- 傳遞該消息(行號和列號)給GUI (注:GUI類是控制主窗體的類,里面包含各種其他組件)
- GUI調用核心算法獲取點開的所有格子和對應的數字
- 根據地雷分布圖調用setImage(image[k]).k屬於[0,9],並置格子為不可響應事件setDisable()(格子點開之后必須禁止其繼續響應事件)
- GUI自身有一張數字地圖,9表示初始態,0~8表示點開后的數字。各個數字對應着各個圖片,10表示點到了地雷=》GameOver
如何處理左擊事件?
- 置該格子為不可響應事件狀態=》調用核心算法(傳遞行列值)=》獲取更新后的地圖狀態=》更換圖片
[3.2]定義一個消息結構體——獲取左鍵點開的格子和對應的數字
用於傳遞點開的格子的位置和數字的消息結構體
struct OpenGridMsg { int i; int j; int stat;//圖片狀態 };
核心算法中應該提供一個這樣的接口
//核心算法中給GUI提供的接口 void clickOpen(int i, int j, vector<OpenGridMsg>& msg);
返回的msg存儲着點開(i.j)位置后遞歸點開的位置和其狀態,點到雷的情況是msg[0].stat=9;
為了消除魔數還需要為類的狀態設定一個枚舉類型:
//格子最后顯示狀態的枚舉類型 enum ImageName { zero=0,one=1,two=2,three=3,four=4,five=5,six=6,seven=7,eight=8, mineBoom=9,mineInit=10,mineOut=11,mark=12,markWrong=13,markUnknow=14 };
[3.3]完成鼠標右鍵基本處理后的效果
右鍵事件處理的相關代碼:
鼠標的右鍵事件已經完成。 //處理右擊事件:狀體轉換,更換圖片。如果是“紅旗”和“問號”狀體則置不可響應左擊事件,否則置可響應左擊事件 inline void GridBlock::rightClick(void) { m_rightClickNumber++; if (m_rightClickNumber>2) { m_rightClickNumber=0; } setImage(imageName[markImage[m_rightClickNumber]]); //在標記狀態下停止響應左擊事件 if (m_rightClickNumber == 0) { m_isableLeftClick = true; } else { m_isableLeftClick = false; } }
[3.4]設計鼠標左鍵單擊事件的處理
為了方便實現鼠標左擊事件,我接下來把GUI類設計為單例模式,這樣的話在GridBlock::leftClick() 中容易調用GUI中的方法GUI::clickOpen(m_lineNumber,m_columnNumber);
調用過程很簡單,就一句話:
下面是GUI的單例模式定義:
實現如下:
[4]GUI類改進創建格子的過程
當知道大小的時候,使用vector時優先使用resize()。
inline void GUI::createGameObjects(void) { //知道大小的情況下,優先使用resize() m_grid.resize(m_numberLines); std::vector<GridBlock> grids; grids.resize(m_numberColumns); for (int i=0; i<m_numberLines; i++) { for (int j=0; j<m_numberColumns; j++) { GridBlock gridBlock(imageName[mineInit]); gridBlock.setPosition(j*initBlockWidth,i*initBlockHeight); gridBlock.setSize(initBlockWidth,initBlockHeight); gridBlock.setLineColumn(i,j);//設置行號和列號 cout << "i=" << i << ",j=" << j << endl; grids[j] = gridBlock; } m_grid[i] = grids; } }
為了要讓這個函數順利進行,還需要修改GridBlock類的默認構造函數,拷貝構造函數,重載復制運算符
inline GridBlock::GridBlock(void) :Fl_Box(0,0,0,0,0),m_pSharedImage(NULL),m_x(0),m_y(0),m_width(0),m_height(0), m_enable(true),m_isableLeftClick(true),m_lineNumber(0),m_columnNumber(0), m_rightClickNumber(0) { …
…
…
}
首先把pSharedImage置為NULL。我調試的時候發現的問題,這個問題需要記住,當 pSharedImage不存在的時候是不能調用它的方法的。(一般調試了就能找到)
inline GridBlock& GridBlock::operator=(const GridBlock& a_rightSide) { m_x = a_rightSide.getX(); m_y = a_rightSide.getY(); m_width = a_rightSide.getWidth(); m_height = a_rightSide.getHeight(); m_enable = a_rightSide.m_enable; m_isableLeftClick = a_rightSide.m_isableLeftClick; m_lineNumber = a_rightSide.m_lineNumber; m_columnNumber = a_rightSide.m_columnNumber; m_rightClickNumber = a_rightSide.m_rightClickNumber; resize(m_x,m_y,m_width,m_height); if (a_rightSide.m_pSharedImage != NULL)//Fl_Shared_Image不存在的情況下不能調用它的方法 { m_pSharedImage = Fl_Shared_Image::get(a_rightSide.m_pSharedImage->name());//獲取相同的sharedImage m_pImage.reset(m_pSharedImage->copy(m_width,m_height));//構建一幅大小相同的Image image(m_pImage.get()); redraw(); } return *this; }
然在賦值運算符重載函數中給個判斷 if (a_rightSide.m_pSharedImage != NULL)
這樣的話就不會在resize時出現Fl_Shared_Image不存在的情況下調用它的方法的錯誤問題了。
[5]掃雷算法類Mines類的設計
僅僅是一個矩陣(二維數組),操作只有這個地雷矩陣圖的簡單方法。GUI操作Mines類的接口見MinesInterface類。
[5.1]Mines類的的聲明
class Mines { public: Mines(void); void initMines(int a_lines, int a_columns); bool isMine(int a_line, a_column); int countRoundMines(int a_line, int a_column);//統計周圍的地雷數量 private: vector<vector<bool> > m_minesMap;//雷的布局圖 int m_numberLines;//行數 int m_numberColumns;//列數 };
當時我為設計每個元素為int型還是bool型糾結了一番的,最終選擇了bool類型(有過幾次想換為int型的沖動,結果還是沒換)。
[5.2]初始化雷區地圖
關於隨機放置雷的問題,采用如下方法:
inline void Mines::initMines(int a_lines, int a_columns, int a_minesCount) { assert(a_minesCount<a_lines*a_columns); m_minesCount = a_minesCount; m_minesMap.resize(a_lines); vector<bool> minesMapLines; minesMapLines.resize(a_columns); for (int i=0; i<a_lines; i++) { for (int j=0; j<a_columns; j++) { minesMapLines[j] = false; } m_minesMap[i] = minesMapLines; } for(int i=0; i<m_minesCount; i++) { //隨機填充 srand(time(NULL)); int index = rand() % (a_lines*a_columns);//[0,a_lines*a_columns-1] if(m_minesMap[index/a_columns][index%acolumns] ) i--; else m_minesMap[index/a_columns][index%a_columns] = true; } }
這樣的話每次放置只取一次隨機數,如果行列各取一次很容易出現相同的情形的。i--是為了防止已經放置了雷的地方重新放的問題。結果我又開始糾結了,隨機生成地圖慢啊!發現兩個缺陷(后面會有改進方案):
- 初始化所有元素為false可以更快(但這不是慢的原因)
- 隨機生減少重復
[5.3]計算周圍地雷個數
為了方便計算周圍的地雷個數,這樣設計isMines2方法。(內部使用,可以越界一格)
inline bool Mines::isMine2(int a_line, int a_column) { assert(a_line>-2); assert(a_column>-2); assert(a_line<m_numberLines+1); assert(a_column<m_numberColumns+1); if (a_line==-1 || a_line==m_numberLines || a_column==-1 || a_column==m_numberColumns)//邊界情況 { cout << "邊界情況" << endl; return false; } return m_minesMap[a_line][a_column]; }
這樣就可以很簡單的寫countRoundMines方法了。countRoundMines()方法定義如下:
inline int Mines::countRoundMines(int a_line, int a_column) { int count=0; if (isMine2(i-1,j-1)) count++; if (isMine2(i,j-1)) count++; if (isMine2(i+1,j-1)) count++; if (isMine2(i-1,j)) count++; if (isMine2(i+1,j)) count++; if (isMine2(i-1,j+1)) count++; if (isMine2(i,j+1)) count++; if (isMine2(i+1,j+1)) count++; return count; }
外部調用的isMine方法如下:(外部使用,不能出現越界情況)
inline bool Mines::isMine(int a_line, int a_column) { assert(a_line>-1); assert(a_column>-1); assert(a_line<m_numberLines); assert(a_column<m_numberColumns); return m_minesMap[a_line][a_column]; }
[6]設計SweepInterface類
用於GUI使用Mines類的接口類
[6.1]定義和實現
class SweepInterface { public: void initMines(int a_lines, int a_columns, int a_minesCount); void clickOpen(int i, int j, vector<OpenGridMsg>& msg); private: Mines m_mines; }; inline void SweepInterface::clickOpen(int i, int j, vector<OpenGridMsg>& msg) { OpenGridMsg amsg; if (m_mines.isMine(i,j))//踩到雷了 { amsg.i = i; amsg.j = j; amsg.state = mineBoom;//雷 msg.push_back(amsg); return; } amsg.i = i; amsg.j = j; amsg.state = m_mines.countRoundMines(i,j); msg.push_back(amsg); if (amsg.state == 0)//如果周圍沒有雷,繼續點開周圍的 { clickOpen(i-1,j-1,msg); clickOpen(i,j-1,msg); clickOpen(i+1,j-1,msg); clickOpen(i-1,j,msg); clickOpen(i+1,j,msg); clickOpen(i-1,j+1,msg); clickOpen(i,j+1,msg); clickOpen(i+1,j+1,msg); } }
現在發現問題了,邊界的問題,怎么解決了,我現在想到了兩個方法。
- 第一個是修改Mines類的isMine()方法,如果是雷返回1,不是雷返回0,是邊界外返回-1。
- 第二個是修改下面的if(amsg.state==0){/***/}大括號里面如果越界就不clickOpen()。
使用方法一需要進行比較大的修改,我選擇方法二:
if (amsg.state == 0)//如果周圍沒有雷,繼續點開周圍的 { if (i-1>=0 && j-1>=0) clickOpen(i-1,j-1,msg); if (j-1>=0) clickOpen(i,j-1,msg); if (i+1<m_numberLines && j-1>=0) clickOpen(i+1,j-1,msg); if (i-1>=0) clickOpen(i-1,j,msg); if (i+1<m_numberLines) clickOpen(i+1,j,msg); if (i-1>=0 && j+1<m_numberColumns) clickOpen(i-1,j+1,msg); if (j+1<m_numberColumns) clickOpen(i,j+1,msg); if (i+1<m_numberLines && j+1<m_numberColumns) clickOpen(i+1,j+1,msg); }
原理很簡單,就是迭代到了邊界就不在迭代。
[6.2]到此為止發現的兩個問題分析
現在又出現新問題了,每次迭代的時候會迭代到以前的位置。這樣的話,需要在每次迭代前檢測位置是否已經檢查過。現在遇到兩個問題:
- 迭代不完全。(空白區域不能繼續往下迭代)
- 初始化太慢了,那個隨機生成Map的需要修改。
初始化15個雷用的循環次數太多了
第1個問題的原因:
行數和列數初始化的時候沒有設置。所以引起右下角部分不能成功繼續迭代。(記住,包含數據成員的類一定要自己建立構造函數來初始化數據成員)
分析第一個問題的原因,沒吃調用srand里面的時間在循環里面幾乎是一樣的,所有每次求出的index值在短時間是一樣的。然后就需要很長時間才能找出不同的位置來放置地雷。我給出的解決方案如下,在srand的參數里面加入變量i來干擾隨機數,這樣的效果很好。幾乎m_minesCount次就能完成。
但是這樣還是不夠好,畢竟i大小有限制的。下面的這種更好。因為count的數字只增不減。
[6.3]修正隨機生成地圖
上面的方法有問題,當玩行列很大的時候就很慢了。還有就是srand()一般只需要用一次的。下面的結果很好了。
unsigned int count=0; unsigned int alpha=0; srand(time(NULL)); for(int i=0; i<m_minesCount; i++) { //隨機填充 int index = (rand())%(m_numberLines*m_numberColumns);//[0,m_numberLines*m_numberColumns-1] if(m_minesMap[index/m_numberColumns][index%m_numberColumns]) { i--; alpha++;//統計重復的次數 } else { m_minesMap[index/m_numberColumns][index%m_numberColumns] = true; } count++; } cout << "初始化雷區用來" << count << "次循環(雷數量:" << m_minesCount << ")" << endl; cout << "重復次數:" << alpha << endl; }
[7]試玩一把
到這里,已經完成了掃雷的基本要求,所以我就玩了一把
[8]對話框設計
[8.1]重來對話框的設計
void GUI::replayDialog(void)//GameOver對話框 { int hotspot = fl_message_hotspot(); fl_message_hotspot(0); fl_message_title("Game Over!"); int rep = fl_choice("重新開始?", "Level", "Replay", "Exit"); fl_message_hotspot(hotspot); if (rep==2) exit(0); else if (rep == 1) { m_coreMines.initMines(m_numberLines,m_numberColumns,m_minesCount); for (int i=0; i<m_numberLines; i++) { for (int j=0; j<m_numberColumns; j++) { m_grid[i][j].init(); m_grid[i][j].setImage(imageName[mineInit]); } } } }
FLTK提供的對話框很方便就能調用。Fl_ask.h文件中定義了所以對話框調用函數。下面是我用到的對話框函數:
FL_EXPORT void fl_message(const char *,...) __fl_attr((__format__ (__printf__, 1, 2))); FL_EXPORT void fl_alert(const char *,...) __fl_attr((__format__ (__printf__, 1, 2))); FL_EXPORT int fl_ask(const char *,...) __fl_attr((__format__ (__printf__, 1, 2), __deprecated__)); FL_EXPORT int fl_choice(const char *q,const char *b0,const char *b1,const char *b2,...) __fl_attr((__format__ (__printf__, 1, 5))); FL_EXPORT const char *fl_input(const char *label, const char *deflt = 0, ...) __fl_attr((__format__ (__printf__, 1, 3)));
[8.2]退出對話框
void window_callback(Fl_Widget*, void*) { int hotspot = fl_message_hotspot(); fl_message_hotspot(0); fl_message_title("Exit"); int rep = fl_ask("確定退出?"); fl_message_hotspot(hotspot); if (rep==1) exit(0); }
[8.3]等級設置
苦惱的問題,需要記住,FLTK中,如果刪除了Fl_Window,它里面的東西也將會被刪除。所以,在更換等級前必須做的事情是:先刪除m_grid;然后刪除窗體。這個問題是由於自己粗心,忘記了又這回事,最后通過測試函數:(慢慢思索,才找出原因的。)
void Test::test_GridBlock(vector<vector<GridBlock> >& vv) { vv.clear(); vv.resize(10); vector<GridBlock> v; v.resize(10); for (int i=0; i<10; i++) { for (int j=0; j<10; j++) { GridBlock t(imageName[mineBoom]); t.setPosition(initBlockWidth,initBlockHeight); t.setSize(initBlockWidth,initBlockHeight); v[j] = t; } vv[i] = v; } }
void GUI::setLevel(int a_level) { m_minesCount = initMinesCount*a_level; m_numberLines = initLines*a_level;//行數 m_numberColumns = initColumns*a_level;//列數 m_width=initBlockWidth*m_numberColumns; m_height=initBlockHeight*m_numberLines; m_pWindow->size(m_width,m_height); m_pWindow->begin();//往窗體里面添加對象 createGameObjects(); m_pWindow->end(); m_pWindow->show(); }
如果是這樣的話,因為沒有刪除窗體,所以m_grid里的內容不會被刪除。這解決辦法比上面的好多了。
[8.4]菜單設計
仿造下面的菜單設計
我設計的菜單如下
掃雷英雄榜對話框的設計
我的設計師這樣的
現在又發現FLTK的一個問題了,在寫漢字的時候如果出現異常可以在異常漢字前后加全角的空格來消除異常。![]()
[9]三大界面問題
[9.1]自定義雷區對話框
我自己畫的對話框
處理事件有點麻煩,兩個按鈕的事件回調函數如下:
void DialogWindow::OkCB(Fl_Widget* w) { DialogWindow* dialog = DialogWindow::getInstance(); dialog->m_strH = dialog->m_pInputs[0]->value(); dialog->m_strW = dialog->m_pInputs[1]->value(); dialog->m_strC = dialog->m_pInputs[2]->value(); dialog->hide(); GUI* gui = GUI::getInstance(); int H,W,C; H = atoi(m_strH.c_str()); W = atoi(m_strW.c_str()); C = atoi(m_strC.c_str()); gui->setLevelSelf(H,W,C); } void DialogWindow::CancelCB(Fl_Widget* w) { DialogWindow* dialog = DialogWindow::getInstance(); dialog->m_pInputs[0]->value(""); dialog->m_pInputs[1]->value(""); dialog->m_pInputs[2]->value(""); dialog->hide(); }
這樣運行時可以了,但是少個對輸入數據的判斷,接下來加個判斷進去:
void DialogWindow::OkCB(Fl_Widget* w) { DialogWindow* dialog = DialogWindow::getInstance(); string strH(dialog->m_pInputs[0]->value()); string strW(dialog->m_pInputs[1]->value()); string strC(dialog->m_pInputs[2]->value()); int H,W,C; H = atoi(strH.c_str()); W = atoi(strW.c_str()); C = atoi(strC.c_str()); if (H>20 || H<5 || W>30 || W<5 || C>W*H) { fl_alert(dialogWindowStr[6]); dialog->m_pInputs[0]->value(""); dialog->m_pInputs[1]->value(""); dialog->m_pInputs[2]->value(""); return; } dialog->hide(); GUI* gui = GUI::getInstance(); gui->setLevelSelf(H,W,C); dialog->m_pInputs[0]->value(""); dialog->m_pInputs[1]->value(""); dialog->m_pInputs[2]->value(""); }
調用的那個setLevelSelf()函數比setLevel()更簡單:
void GUI::setLevel(int a_level) { m_level = a_level; switch(m_level) { case 1: m_minesCount = initMinesCount; m_numberLines = initLines;//行數 m_numberColumns = initColumns;//列數 break; case 2: m_minesCount = initMinesCount*4; m_numberLines = initLines*2;//行數 m_numberColumns = initColumns*2;//列數 break; case 3: m_minesCount = initMinesCount*8; m_numberLines = initLines*2;//行數 m_numberColumns = initColumns*3;//列數 break; default: assert("a_level must be [1~3]"); return; } updateWindow(); } void GUI::updateWindow(void) { m_width=initBlockWidth*m_numberColumns; m_height=initBlockHeight*m_numberLines; m_pWindow->size(m_width,m_height+menuBarHeight); m_pMenuBar->size(m_width,menuBarHeight); m_pWindow->begin();//往窗體里面添加對象 createGameObjects(); m_pWindow->end(); m_pWindow->show(); updateMap(); } void GUI::setLevelSelf(int a_H, int a_W, int a_C) { m_level=0; m_minesCount = a_C; m_numberLines = a_H;//行數 m_numberColumns = a_W;//列數 updateWindow(); }
[9.2]游戲勝利對話框,包括記錄的保存
由於這個操作需要在右鍵單擊時候進行判斷,所以,我在GUI類中加一個rightClick()方法:
void GUI::rightClick(int a_line, int a_colum) { if (strcmp(m_grid[a_line][a_colum].getImageName(), imageName[markUnknow]) != 0)//如果不是問號就做判斷 { int i=0,j=0; for (; i<m_numberLines; i++) { j=0; for (; j<m_numberColumns; j++) { if (strcmp(m_grid[i][j].getImageName(),imageName[mineInit]) == 0) {//如果還有沒標記點開或者沒標記的 break; } if (strcmp(m_grid[i][j].getImageName(),imageName[mark]) == 0 && !m_coreMines.isMine(i,j))//該位置被標記,且該位置不是雷 { break; } if (strcmp(m_grid[i][j].getImageName(),imageName[markUnknow]) == 0)//如果有問號 { break; } } if (j<m_numberColumns) break;//很重要的一句話,跳出雙重循環 } if (i==m_numberLines && j==m_numberColumns) {//勝利 winnerDialog();//勝利對話框 } } }
這是對勝利的條件的判斷,如果判斷勝利了,就調用winnerDialog()方法。勝利對話框用一個輸入對話框來做會簡單些,設計如下:
現在我使用的是把數據保存到文件中,所以需要用來對文件的寫入操作(如果使用SQLite數據庫的話可能更方便后續添加網絡模塊)。對於這個數據保存數據半夜搞了兩個小時沒搞定,結果花了一上午搞定了。
void GUI::winnerDialog(void) { int hotspot = fl_message_hotspot(); fl_message_hotspot(0); fl_beep(FL_BEEP_MESSAGE); fl_message_hotspot(hotspot); fl_message_title(GUIStr[8]); int useTime = 102;//下一步的任務是計算時間 char name[20]=""; if (m_level != 0) { const char* str = fl_input(GUIStr[9],"",useTime); if (strlen(name)==0) { strcpy(name,GUIStr[12]);//匿名 } else { strcpy(name,str); } } else { fl_message(GUIStr[10],useTime); return; } saveRecord(useTime,name);//保存記錄 }
時間相關的等下一個時間顯示問題做好了再添加上去,暫時用了個常數代替
文件格式:
%d\t%s
文件有且僅有三行
保存文件相對來說是比較麻煩的,主要是格式問題和記錄的覆蓋問題,這個問題花了一上午才解決的,要記住,讀文件的時候先把內容全部讀出了再操作(內容較少的情況下)。
void GUI::saveRecord(int a_useTime, const char* a_userName) { assert(m_level!=0); using std::ofstream; using std::ifstream; ofstream writeFile; ifstream readFile; //備份 string text; string str; readFile.open(GUIStr[11]); while (getline(readFile,str)) { text += str+"\n"; } // cout <<"text:"<< text; readFile.close(); writeFile.open(GUIStr[11]); for (int i=1; i<4; i++) { int b = text.find_first_of('\n',0); str = text.substr(0,b); // cout <<"str:"<< str; text = text.substr(b+1,text.length()-b); // cout <<"text:"<< text; // cout << i << ":" << str << endl; if (i==m_level) { // cout << m_level << "級:" << str; if (str.length()==0) { writeFile<<a_useTime<<"\t"<<a_userName<<"\n"; } else { int index = str.find('\t'); string s = str.substr(0,index);//記住后面的參數是num int time = atoi(s.c_str()); // cout << "old:" << time << endl; // cout << "new:" << a_useTime << endl; if (time>a_useTime)//更新記錄 { writeFile<<a_useTime<<"\t"<<a_userName<<"\n"; } else { writeFile<<str<<"\n"; } } } else { writeFile<<str<<"\n"; } } writeFile.close(); }
也可以看到上面的cout部分都加了注釋,是調試的時候用的。String操作中有個要注意的地方就是取子串的時候,第二個參數是子串的長度。現在文件保存弄好了,接下來就要修改
這個對話框的載入記錄的內容了。
//排行榜對話框 void GUI::RankingsDialog(void) { int hotspot = fl_message_hotspot(); fl_message_hotspot(0); fl_message_title(GUIStr[7]); using std::ifstream; ifstream readFile(GUIStr[11]);//讀取文件中的數據 string text; string str; while (getline(readFile,str)) { text += str+"\n"; } readFile.close(); cout << text; string textout; //找到str為m_level行 for (int i=0; i<3; i++) { int b = text.find_first_of('\n',0); str = text.substr(0,b); text = text.substr(b+1,text.length()-b); int index = str.find('\t'); string time = str.substr(0,index);//記住后面的參數是num string name = str.substr(index+1,str.length()-index); cout << "time,name:" << time << "," << name << endl; textout+=GUIStr[13+i]+time+GUIStr[16]+name+"\n"; } /* "初級: ", //13 "中級: ", //14 "高級: ", //15 " 秒 \t", //16 */ fl_message("%s",textout.c_str()); }
到現在,也許已經注意到了許多GUIStr[]這樣的字符串數組的使用了吧,主要是為了后續的本地化。需要本地化的時候只需要修改這個數組就行了。
[9.3]時間顯示和雷數顯示
雷數量的顯示分兩個:剩余雷數和已經標記的雷數
這兩個顯示的話比較簡單,在右鍵事件中加入就行了。
inline void TimeShowBox::setRemainMines(int a_num) { if (a_num<0) a_num=0; sprintf(m_str,TimeShowBoxStr[0],a_num); m_pBox[0]->copy_label(m_str); m_pBox[0]->redraw_label(); } inline void TimeShowBox::setMarkMines(int a_num) { sprintf(m_str,TimeShowBoxStr[1],a_num); m_pBox[1]->copy_label(m_str); m_pBox[1]->redraw_label(); }
關於時間的顯示還是有點難度的。我有點不理解FLTK自帶的那個Fl_Timer。所以我就自己弄了個全局變量控制。
具體怎么控制這個g_useTime呢?首先在主函數中使用
void callback(void*) { useTime++; cout << "time:" << useTime << endl; Fl::repeat_timeout(1.0, callback); } int main(void) { GUI* gui = GUI::getInstance(); Fl::add_timeout(1.0, callback); return Fl::run(); }
這個的作用是,useTime每秒自增一次。為了防止溢出也可以加個判斷在回調函數里面的。
然后在GUI創建的時候置g_sseTime為0。在更新地圖的時候也置g_useTime為0。
//更新地圖 void GUI::updateMap(void) { g_useTime=0; m_coreMines.initMines(m_numberLines,m_numberColumns,m_minesCount); ... }
為了再彈出對話框的時候使時間暫停,我加了一個變量 g_isPauseTime=false;
控制是否暫停。需要暫停時間的彈出的對話框有:顯示排行榜對話框。
剛剛試了一下5X5的時候,上面的顯示不好看了。
所以我改下自定義的范圍。 if (H>20 || H<3 || W>40 || W<10 || C>W*H)
{
" 高度(3~20) ",
" 寬度(10~40) ",
[10]結束語
終於完工了,可以發布了,洗洗睡吧!
FLTK中還有強大的Event功能,這個掃雷游戲中只使用的了很基礎的Event功能。
忘記了一件重要的事情:公布源代碼!代碼已經push到github了,想看看,想玩玩的可以去下載,圖片素材用的是別人的:http://www.cnblogs.com/jacky87/archive/2010/08/03/1791395.html,
代碼下載地址:https://github.com/hanxi/MineSweeping


![wps_clip_image-4311[6] wps_clip_image-4311[6]](/image/aHR0cHM6Ly9pbWFnZXMuY25ibG9ncy5jb20vY25ibG9nc19jb20vaGFueGkvMjAxMjA5LzIwMTIwOTAyMjIyMDEyNzU5Ny5wbmc=.png)
![wps_clip_image-9713[6] wps_clip_image-9713[6]](/image/aHR0cHM6Ly9pbWFnZXMuY25ibG9ncy5jb20vY25ibG9nc19jb20vaGFueGkvMjAxMjA5LzIwMTIwOTAyMjIyMDEzNzAwNy5wbmc=.png)
![wps_clip_image-4877[6] wps_clip_image-4877[6]](/image/aHR0cHM6Ly9pbWFnZXMuY25ibG9ncy5jb20vY25ibG9nc19jb20vaGFueGkvMjAxMjA5LzIwMTIwOTAyMjIyMDEzODkyNi5wbmc=.png)























