SDL游戏教程第四课 井字游戏

翻译声明:
    本系列教程来自
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)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值