SDL遊戲教程第四課 井字遊戲

來源:互聯網
上載者:User
翻譯聲明:
    本系列教程來自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個新表面。

  1. #ifndef _CAPP_H_
  2. #define _CAPP_H_
  3. #include <SDL.h>
  4. #include "CEvent.h"
  5. #include "CSurface.h"
  6. class CApp : public CEvent {
  7.     private:
  8.         bool            Running;
  9.         SDL_Surface*    Surf_Display;
  10.     private:
  11.         SDL_Surface*    Surf_Grid;
  12.         SDL_Surface*    Surf_X;
  13.         SDL_Surface*    Surf_O;
  14.     public:
  15.         CApp();
  16.         int OnExecute();
  17.     public:
  18.         bool OnInit();
  19.         void OnEvent(SDL_Event* Event);
  20.         void OnExit();
  21.         void OnLoop();
  22.         void OnRender();
  23.         void OnCleanup();
  24. };
  25. #endif

同樣的,開啟CApp.cpp並做相應修改。刪除測試表面,再次添加3個新的。

  1. #include "CApp.h"
  2. CApp::CApp() {
  3.     Surf_Grid = NULL;
  4.     Surf_X = NULL;
  5.     Surf_O = NULL;
  6.     Surf_Display = NULL;
  7.     Running = true;
  8. }
  9. int CApp::OnExecute() {
  10.     if(OnInit() == false) {
  11.         return -1;
  12.     }
  13.     SDL_Event Event;
  14.     while(Running) {
  15.         while(SDL_PollEvent(&Event)) {
  16.             OnEvent(&Event);
  17.         }
  18.         OnLoop();
  19.         OnRender();
  20.     }
  21.     OnCleanup();
  22.     return 0;
  23. }
  24. int main(int argc, char* argv[]) {
  25.     CApp theApp;
  26.     return theApp.OnExecute();
  27. }

呵呵,猜對了,開啟CApp_OnCleanup.cpp做同樣修改。和前面一樣,去掉測試表面,然添加3個新的。

  1. #include "CApp.h"
  2. void CApp::OnCleanup() {
  3.     SDL_FreeSurface(Surf_Grid);
  4.     SDL_FreeSurface(Surf_X);
  5.     SDL_FreeSurface(Surf_O);
  6.     SDL_FreeSurface(Surf_Display);
  7.     SDL_Quit();
  8. }

現在我們就完成了表面的設定,並把他們載入到了記憶體。開啟CApp_OnInit.cpp,做點修改。在此去掉測試表面,然後載入3個新的。確保檔案名稱都正確。然後,把視窗尺寸改為600*600,也就是網格的大小。這樣就可以填滿整個視窗而不會在視窗周圍用不到的地方留下空白。

  1. #include "CApp.h"
  2. bool CApp::OnInit() {
  3.     if(SDL_Init(SDL_INIT_EVERYTHING) < 0) {
  4.         return false;
  5.     }
  6.     if((Surf_Display = SDL_SetVideoMode(600, 600, 32, SDL_HWSURFACE | SDL_DOUBLEBUF)) == NULL) {
  7.         return false;
  8.     }
  9.     if((Surf_Grid = CSurface::OnLoad("./gfx/grid.bmp")) == NULL) {
  10.     return false;
  11.     }
  12.     if((Surf_X = CSurface::OnLoad("./gfx/x.bmp")) == NULL) {
  13.     return false;
  14.     }
  15.     if((Surf_O = CSurface::OnLoad("./gfx/o.bmp")) == NULL) {
  16.     return false;
  17.     }
  18.     return true;
  19. }

或許你已經注意到了我在檔案名稱上做了點修改。我在檔案名稱前添加了./gfx/來指定圖片所在的檔案夾。在遊戲增長之前,一定確定實際上的一個檔案夾包含這些檔案。正因為此,從此以後,所有的圖片將會被放到gfx檔案夾。

現在,我們就把網格顯示到螢幕上。開啟CApp_OnRender.cpp,把測試表面的渲染改成渲染網格。

  1. #include "CApp.h"
  2. void CApp::OnRender() {
  3.     CSurface::OnDraw(Surf_Display, Surf_Grid, 0, 0);
  4.     SDL_Flip(Surf_Display);
  5. }

試試編譯下你的程式,如果成功,你應該可以看到網格被顯示出來了。一定要記得,使用一個表面一般只要5步:聲明、設定為NULL、載入、繪製、然後清理。最好你現在就牢牢地記住這5步,因為假如一會你忘了某一步的話就會有問題了。例如,你忽略了把一個表面設定為NULL,就會導致未定義。如果你忽略了釋放一個表面,就會導致記憶體流失。

或許你會感覺我們的圖片有點奇怪,怎麼X和O都是紫色的背景。當然有原因了,我們要對這些表面實現透明效果。其實,只要是顯示紫色的地方我們會把紫色做成透明的。SDL提供了一個簡單的函數來完成這個效果,SDL_SetColorKey。為了實現它,開啟CSurface.h添加一個新函數。

  1. #ifndef _CSURFACE_H_
  2. #define _CSURFACE_H_
  3. #include <SDL.h>
  4. class CSurface {
  5.     public:
  6.         CSurface();
  7.     public:
  8.         static SDL_Surface* OnLoad(char* File);
  9.         static bool OnDraw(SDL_Surface* Surf_Dest, SDL_Surface* Surf_Src, int X, int Y);
  10.         static bool OnDraw(SDL_Surface* Surf_Dest, SDL_Surface* Surf_Src, int X, int Y, int X2, int Y2, int W, int H);
  11.         static bool Transparent(SDL_Surface* Surf_Dest, int R, int G, int B);
  12. };
  13. #endif

現在就實現這個函數,開啟CSurface.cpp然後添加它:

  1. bool CSurface::Transparent(SDL_Surface* Surf_Dest, int R, int G, int B) {
  2.     if(Surf_Dest == NULL) {
  3.         return false;
  4.     }
  5.     SDL_SetColorKey(Surf_Dest, SDL_SRCCOLORKEY | SDL_RLEACCEL, SDL_MapRGB(Surf_Dest->format, R, G, B));
  6.     return true;
  7. }

注意下我們傳遞的除了表面多出的3個參數。有3個我們要做透明的顏色值,並不僅僅是紫色。舉個例子,如果我們要把紅色做成透明的,那就是 255,0,0。

這個函數首先檢查我們是不是傳遞了有效表面。如果是,我們為一個顏色設定一個色鍵(透明)。第一個參數就是我們我應用色鍵的表面,第二個是告訴SDL如何進行操作的一些標誌,然後第三個參數是要做透明的顏色。這些用到的標誌都很基礎,第一個告訴SDL在一個源(就是傳遞進去的表面)上應用色鍵,第二個告訴SDL試著用RLE加速(其實就是盡量繪製得更快些)。第三個參數有點複雜,為了建立一個顏色,我們用到了SDL_MapRGB。這個函數使用一個表面,還需要指定顏色值(R,G,B),然後試著在表面上去找一個最接近的顏色值來匹配它。你或許在想,這有什麼用。並不是所有的表面都有同樣的調色盤。還記得早期的NES年代裡只有很少一些顏色值可以用嗎?同樣的,SDL_MapRGB使用一個顏色值,然後在一個表面的調色盤裡找一個最接近的值。

現在就讓我們對我們的表面應用這個新函數,開啟CApp_OnInit.cpp,然後做如下改變:

  1. #include "CApp.h"
  2. bool CApp::OnInit() {
  3.     if(SDL_Init(SDL_INIT_EVERYTHING) < 0) {
  4.         return false;
  5.     }
  6.     if((Surf_Display = SDL_SetVideoMode(600, 600, 32, SDL_HWSURFACE | SDL_DOUBLEBUF)) == NULL) {
  7.         return false;
  8.     }
  9.     if((Surf_Grid = CSurface::OnLoad("./gfx/grid.bmp")) == NULL) {
  10.         return false;
  11.     }
  12.     if((Surf_X = CSurface::OnLoad("./gfx/x.bmp")) == NULL) {
  13.         return false;
  14.     }
  15.     if((Surf_O = CSurface::OnLoad("./gfx/o.bmp")) == NULL) {
  16.         return false;
  17.     }
  18.     CSurface::Transparent(Surf_X, 255, 0, 255);
  19.     CSurface::Transparent(Surf_O, 255, 0, 255);
  20.     return true;
  21. }

表面的一切都已搞定。下面我們要做的就是用一種方式來繪製X和O。我們可以在網格的任何地方繪製他們,因為他們並不總是在同一點。下面我們要做的就是建立一個9個容器的數組,由數組裡的資料告訴我們網格上每個單元的值。於是0點就是左上,1就是中上,2就是右上,等等。把建立的數組添加到CApp.h:

  1. #ifndef _CAPP_H_
  2. #define _CAPP_H_
  3. #include <SDL.h>
  4. #include "CEvent.h"
  5. #include "CSurface.h"
  6. class CApp : public CEvent {
  7.     private:
  8.         bool            Running;
  9.         SDL_Surface*    Surf_Display;
  10.     private:
  11.         SDL_Surface*    Surf_Grid;
  12.         SDL_Surface*    Surf_X;
  13.         SDL_Surface*    Surf_O;
  14.     private:
  15.         int     Grid[9];
  16.     public:
  17.         CApp();
  18.         int OnExecute();
  19.     public:
  20.         bool OnInit();
  21.         void OnEvent(SDL_Event* Event);
  22.         void OnExit();
  23.         void OnLoop();
  24.         void OnRender();
  25.         void OnCleanup();
  26. };
  27. #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)

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.