翻譯聲明:
本系列教程來自Dev Hub,一切解釋權歸原作者。我只是出自個人愛好,才翻譯了本系列教程。因為本人也是個初學者,而且英語水平有限,錯誤難免,望各路高手指正。
本課原文地址:http://www.sdltutorials.com/sdl-tutorial-tic-tac-toe/
到了這個時候,我們已經為開發一個遊戲打下了良好的基礎。至此,我們已經建立了一個最基礎的架構來處理一般的規則,而且建立了一個特殊的類來處理訊息,還有一個處理一些表面函數的類。本課,我們就要用到全部這些東西,把他們結合起來,然後建立一個井字遊戲。別擔心,這東西很簡單的。在上節課的基礎上開始。
第一件要做的事,就是要規劃我們的遊戲。根據經驗,我們知道井字遊戲有3*3格子,你可以往這些格子裡放置X或O。那麼,我們就知道了需要3張圖片,一個充當網格,一個充當X,一個充當O。我們並不需要重複的多個X或O,因為可以在程式裡多次繪製同一張圖片。讓我們走出第一步。我們的網格是600*600的,還有X和O都是200*200的。
現在我們已經有圖片了,我們需要用一種方法把他們載入到我們的程式裡。開啟CApp.h做點修改。刪除測試用的表面,然後添加3個新表面。
- #ifndef _CAPP_H_
- #define _CAPP_H_
- #include <SDL.h>
- #include "CEvent.h"
- #include "CSurface.h"
- class CApp : public CEvent {
- private:
- bool Running;
- SDL_Surface* Surf_Display;
- private:
- SDL_Surface* Surf_Grid;
- SDL_Surface* Surf_X;
- SDL_Surface* Surf_O;
- public:
- CApp();
- int OnExecute();
- public:
- bool OnInit();
- void OnEvent(SDL_Event* Event);
- void OnExit();
- void OnLoop();
- void OnRender();
- void OnCleanup();
- };
- #endif
同樣的,開啟CApp.cpp並做相應修改。刪除測試表面,再次添加3個新的。
- #include "CApp.h"
- CApp::CApp() {
- Surf_Grid = NULL;
- Surf_X = NULL;
- Surf_O = NULL;
- Surf_Display = NULL;
- Running = true;
- }
- int CApp::OnExecute() {
- if(OnInit() == false) {
- return -1;
- }
- SDL_Event Event;
- while(Running) {
- while(SDL_PollEvent(&Event)) {
- OnEvent(&Event);
- }
- OnLoop();
- OnRender();
- }
- OnCleanup();
- return 0;
- }
- int main(int argc, char* argv[]) {
- CApp theApp;
- return theApp.OnExecute();
- }
呵呵,猜對了,開啟CApp_OnCleanup.cpp做同樣修改。和前面一樣,去掉測試表面,然添加3個新的。
- #include "CApp.h"
- void CApp::OnCleanup() {
- SDL_FreeSurface(Surf_Grid);
- SDL_FreeSurface(Surf_X);
- SDL_FreeSurface(Surf_O);
- SDL_FreeSurface(Surf_Display);
- SDL_Quit();
- }
現在我們就完成了表面的設定,並把他們載入到了記憶體。開啟CApp_OnInit.cpp,做點修改。在此去掉測試表面,然後載入3個新的。確保檔案名稱都正確。然後,把視窗尺寸改為600*600,也就是網格的大小。這樣就可以填滿整個視窗而不會在視窗周圍用不到的地方留下空白。
- #include "CApp.h"
- bool CApp::OnInit() {
- if(SDL_Init(SDL_INIT_EVERYTHING) < 0) {
- return false;
- }
- if((Surf_Display = SDL_SetVideoMode(600, 600, 32, SDL_HWSURFACE | SDL_DOUBLEBUF)) == NULL) {
- return false;
- }
- if((Surf_Grid = CSurface::OnLoad("./gfx/grid.bmp")) == NULL) {
- return false;
- }
- if((Surf_X = CSurface::OnLoad("./gfx/x.bmp")) == NULL) {
- return false;
- }
- if((Surf_O = CSurface::OnLoad("./gfx/o.bmp")) == NULL) {
- return false;
- }
- return true;
- }
或許你已經注意到了我在檔案名稱上做了點修改。我在檔案名稱前添加了./gfx/來指定圖片所在的檔案夾。在遊戲增長之前,一定確定實際上的一個檔案夾包含這些檔案。正因為此,從此以後,所有的圖片將會被放到gfx檔案夾。
現在,我們就把網格顯示到螢幕上。開啟CApp_OnRender.cpp,把測試表面的渲染改成渲染網格。
- #include "CApp.h"
- void CApp::OnRender() {
- CSurface::OnDraw(Surf_Display, Surf_Grid, 0, 0);
- SDL_Flip(Surf_Display);
- }
試試編譯下你的程式,如果成功,你應該可以看到網格被顯示出來了。一定要記得,使用一個表面一般只要5步:聲明、設定為NULL、載入、繪製、然後清理。最好你現在就牢牢地記住這5步,因為假如一會你忘了某一步的話就會有問題了。例如,你忽略了把一個表面設定為NULL,就會導致未定義。如果你忽略了釋放一個表面,就會導致記憶體流失。
或許你會感覺我們的圖片有點奇怪,怎麼X和O都是紫色的背景。當然有原因了,我們要對這些表面實現透明效果。其實,只要是顯示紫色的地方我們會把紫色做成透明的。SDL提供了一個簡單的函數來完成這個效果,SDL_SetColorKey。為了實現它,開啟CSurface.h添加一個新函數。
- #ifndef _CSURFACE_H_
- #define _CSURFACE_H_
- #include <SDL.h>
- class CSurface {
- public:
- CSurface();
- public:
- static SDL_Surface* OnLoad(char* File);
- static bool OnDraw(SDL_Surface* Surf_Dest, SDL_Surface* Surf_Src, int X, int Y);
- static bool OnDraw(SDL_Surface* Surf_Dest, SDL_Surface* Surf_Src, int X, int Y, int X2, int Y2, int W, int H);
- static bool Transparent(SDL_Surface* Surf_Dest, int R, int G, int B);
- };
- #endif
現在就實現這個函數,開啟CSurface.cpp然後添加它:
- bool CSurface::Transparent(SDL_Surface* Surf_Dest, int R, int G, int B) {
- if(Surf_Dest == NULL) {
- return false;
- }
- SDL_SetColorKey(Surf_Dest, SDL_SRCCOLORKEY | SDL_RLEACCEL, SDL_MapRGB(Surf_Dest->format, R, G, B));
- return true;
- }
注意下我們傳遞的除了表面多出的3個參數。有3個我們要做透明的顏色值,並不僅僅是紫色。舉個例子,如果我們要把紅色做成透明的,那就是 255,0,0。
這個函數首先檢查我們是不是傳遞了有效表面。如果是,我們為一個顏色設定一個色鍵(透明)。第一個參數就是我們我應用色鍵的表面,第二個是告訴SDL如何進行操作的一些標誌,然後第三個參數是要做透明的顏色。這些用到的標誌都很基礎,第一個告訴SDL在一個源(就是傳遞進去的表面)上應用色鍵,第二個告訴SDL試著用RLE加速(其實就是盡量繪製得更快些)。第三個參數有點複雜,為了建立一個顏色,我們用到了SDL_MapRGB。這個函數使用一個表面,還需要指定顏色值(R,G,B),然後試著在表面上去找一個最接近的顏色值來匹配它。你或許在想,這有什麼用。並不是所有的表面都有同樣的調色盤。還記得早期的NES年代裡只有很少一些顏色值可以用嗎?同樣的,SDL_MapRGB使用一個顏色值,然後在一個表面的調色盤裡找一個最接近的值。
現在就讓我們對我們的表面應用這個新函數,開啟CApp_OnInit.cpp,然後做如下改變:
- #include "CApp.h"
- bool CApp::OnInit() {
- if(SDL_Init(SDL_INIT_EVERYTHING) < 0) {
- return false;
- }
- if((Surf_Display = SDL_SetVideoMode(600, 600, 32, SDL_HWSURFACE | SDL_DOUBLEBUF)) == NULL) {
- return false;
- }
- if((Surf_Grid = CSurface::OnLoad("./gfx/grid.bmp")) == NULL) {
- return false;
- }
- if((Surf_X = CSurface::OnLoad("./gfx/x.bmp")) == NULL) {
- return false;
- }
- if((Surf_O = CSurface::OnLoad("./gfx/o.bmp")) == NULL) {
- return false;
- }
- CSurface::Transparent(Surf_X, 255, 0, 255);
- CSurface::Transparent(Surf_O, 255, 0, 255);
- return true;
- }
表面的一切都已搞定。下面我們要做的就是用一種方式來繪製X和O。我們可以在網格的任何地方繪製他們,因為他們並不總是在同一點。下面我們要做的就是建立一個9個容器的數組,由數組裡的資料告訴我們網格上每個單元的值。於是0點就是左上,1就是中上,2就是右上,等等。把建立的數組添加到CApp.h:
- #ifndef _CAPP_H_
- #define _CAPP_H_
- #include <SDL.h>
- #include "CEvent.h"
- #include "CSurface.h"
- class CApp : public CEvent {
- private:
- bool Running;
- SDL_Surface* Surf_Display;
- private:
- SDL_Surface* Surf_Grid;
- SDL_Surface* Surf_X;
- SDL_Surface* Surf_O;
- private:
- int Grid[9];
- public:
- CApp();
- int OnExecute();
- public:
- bool OnInit();
- void OnEvent(SDL_Event* Event);
- void OnExit();
- void OnLoop();
- void OnRender();
- void OnCleanup();
- };
- #endif
我們知道,每個單元可取的值包括:空,X和O。它們告訴我們一個單元裡目前是什麼。為了比0,1,2來得更整潔點,我們在此使用一個enum來代替。如果你不明白enum是怎麼回事,可以去找點關於這方面的速成看一下。只要知道GRID_TYPE_NONE = 0,GRID_TYPE_X = 1,GRID_TYPE_O = 2就行了。回到CApp.h,在網格數組下面添加如下:
enum {
GRID_TYPE_NONE = 0,
GRID_TYPE_X,
GRID_TYPE_O
};
注意,目前為止我一旦提到不同檔案裡的代碼都是把它們全都貼出來。從現在開始,我希望你能清除代碼進行到哪兒了。多數時候我會告訴你該往哪裡添加它,有時我也會把它們全顯示出來。
現在我們已經有了實現一個單元的方法,我們就需要一種重設面板的方法。讓我們在CApp.h底部建立一個新的函數Reset。
public:
void Reset();
開啟CApp.cpp,在main()之前添加如下函數:
void CApp::Reset() {
for(int i = 0;i < 9;i++) {
Grid[i] = GRID_TYPE_NONE;
}
}
這個迴圈將把網格上所有單元都設定為GRID_TYPE_NONE,意思就是所有單元都是空的。我們要在每次程式載入的最開始這麼做,所有在CApp_OnInit.cpp裡調用此函數:
//…
CSurface::Transparent(Surf_X, 255, 0, 255);
CSurface::Transparent(Surf_O, 255, 0, 255);
Reset();
目前一切正常。接下來我們就要實現在螢幕上放置X和O了。通過建立一個新函數來搞定它。再次開啟CApp.h,就在Reset後面添加:
void SetCell(int ID, int Type);
現在,開啟CApp.cpp,添加這個函數:
void CApp::SetCell(int ID, int Type) {
if(ID < 0 || ID >= 9) return;
if(Type < 0 || Type > GRID_TYPE_O) return;
Grid[ID] = Type;
}
此函數有兩個參數,第一個是要改變的單元的ID,第二個是要改變成的類型。我們需要在此限制一下,首先要保證訪問數組不能越界(否則我們的程式就會崩潰),其次保證我們傳入的類型合適。就這麼簡單。
現在,讓我們來實現一種繪製X和O的方法。開啟CApp_OnRender,就在網格後面添加如下代碼:
for(int i = 0;i < 9;i++) {
int X = (i % 3) * 200;
int Y = (i / 3) * 200;
if(Grid[i] == GRID_TYPE_X) {
CSurface::OnDraw(Surf_Display, Surf_X, X, Y);
}else
if(Grid[i] == GRID_TYPE_O) {
CSurface::OnDraw(Surf_Display, Surf_O, X, Y);
}
}
這個與往常相比貌似有點複雜。首先,我們遍曆網格中的每一個單元。然後我們把網格的ID號轉譯為X和Y座標。我們用兩種不同的方法來完成這些。為了得到X,我們讓i對3取餘。這樣當i為0,我們得到0,i為1我們得到1,i為2我們得到2,i為3我們得到0等等。由於每個但都是200*200的,我們還得把它乘上200。為得到Y,我們我們除以3,這樣當i為0,1,2時Y就是0;i是3,4,5時,Y就是1等等。最後同樣乘以200。我建議你最好搞清楚這裡究竟是怎麼回事,因為這類方法對於基於磚瓦的遊戲很常用。
接下來,我們要做的就是檢查單元的類型,然後正確的繪製上面的表面。
現在我們繪製了表面,我們還需要一種人機互動的方法。在此我們使用滑鼠訊息。當使用者單擊一個單元,就會進行相應設定。我們要重寫CEvent的一個函數來實現這點。開啟CApp.h,在OnEvent後面緊接著OnExit添加下面函數:
void OnLButtonDown(int mX, int mY);
現在,開啟CApp_OnEvent.cpp添加這個函數:
void CApp::OnLButtonDown(int mX, int mY) {
int ID = mX / 200;
ID = ID + ((mY / 200) * 3);
if(Grid[ID] != GRID_TYPE_NONE) {
return;
}
if(CurrentPlayer == 0) {
SetCell(ID, GRID_TYPE_X);
CurrentPlayer = 1;
}else{
SetCell(ID, GRID_TYPE_O);
CurrentPlayer = 0;
}
}
首先,我們要做的與把X和Y轉譯成ID剛好相反,這次是轉換成ID。然後,我們要保證這個單元還沒被佔用,否則,就函數返回。接下來,我們檢測輪到哪個玩家了,然後對此單元做相應修改,並輪流玩家。CurrentPlayer就是要指定輪到誰了的一個新變數,我們需要添加它。開啟CApp.h,在網格數組下面添加這個變數:
int CurrentPlayer;
同樣的,在CApp.cpp裡設定此變數的預設值:
CApp::CApp() {
CurrentPlayer = 0;
Surf_Grid = NULL;
Surf_X = NULL;
Surf_O = NULL;
Surf_Display = NULL;
Running = true;
}
試著編譯一下,你應該可以得到一個基本能夠啟動並執行井字遊戲了。祝賀你!
接下來的事情就看你的了。我們已經有了開發自己遊戲的一個相當穩定的基礎,並且大部分工作都已完成。我希望你能走得更遠點。試著添加一個“X贏了”,“O贏了”,並在每次遊戲結束的時候“繪製”它們(這裡需要額外的圖片)。想想看,你如何檢測到底是誰贏了(一個完成這個目的的函數是不是很合適)?試著在遊戲完成以後重新設定它。如果你很有想法,就試著添加一些普通的AI來和玩家對戰。如果你更勇敢,可以試著添加玩家對戰玩家,或者玩家對戰電腦的功能。
如果你都準備好了,並且對本課已經心領神會,繼續進行下一課去看看幀動畫吧。
SDL教程Tic Tac Toe —— 課程檔案:
Win32: Zip, Rar
Linux: Tar(Thanks Gaten)