当前位置:文档之家› VC++6.0扫雷

VC++6.0扫雷



1. 游戏实现



扫雷,是附带在Window里面的游戏,是个简单的游戏。因此我们就从扫雷开始我们的游戏旅程。很多人都玩过这个游戏,只是不知道怎么用程序实现。不过还有人不知道怎么玩,下面就先说说游戏的规则:



● 开始:按左键开始游戏,按按钮或菜单重新开始。

● 左键:按下时,是雷则结束,非雷则显示数字。

● 数字:代表此数字周围一圈八格中雷的个数。

● 右键:奇次按下表示雷,偶数按下表示对上次的否定。

● 结束:左键按到雷结束,找出全部雷结束。



接下来就该介绍游戏的编程过程了。不过要先交代一下一些内容。



● 添加位图。

● 添加全局变量。

● 画初始界面。

● 添加函数。



为什么要按这种次序呢?因为我们在画初始界面时,可能要用到位图或变量,而变量的定义又可能要对位图进行定义。这样的步骤的好处还有:在做一步之后都可以运行,有错就改,无错就做下一步。

上图是扫雷的一个画面。

下面就一步一步地演示,以编程的思路进行,当然,由于编程过程中有一些函数中的代码是分成两三次写的,我们就不重复,全部代码在第一次讲到时列出,而后面讲到时就只是提一下。



新建单文档工程2_1。



2. 资源编辑



添加位图:



前十二幅是在雷区的,后四幅是按钮。为了便于加载,必须各自保证其连续性。另外,为什么不添加一个按钮而用位图呢?是因为即使我们添加了按钮也要添加四幅位图!



位图的ID号:

按扭位图: 30*30 IDB_ANNIU1、IDB_ANNIU 2、IDB_ANNIU3、 IDB_ANNIU4

雷区位图: 14*14 ID号按下图依次为:IDB_BITMAP14。。。。。。IDB_BITMAP25



位图:下图(图2-1)。


图2-1



3. 变量函数



定义新类:



对于雷,我们是单独定义一个类,这样有利于程序的操作。

class Lei

{

public:

//显示哪一个位图

int weitu;

//这个位置相应的值

int shumu;

};

并有如下规定(图2-2):




图2-2



视图类变量:



接着是在View类添加变量和函数:



//剩下雷数

int leftnum;

//雷数

int leinum;

//结束

int jieshu;

//计时

short second;

//开始计时

int secondstart;

//位图数组

CBitmap m_Bitmap[12];

//按扭位图数组

CBitmap m_anniu[4];

//雷区行数

int m_RowCount;

//雷区列数


int m_ColCount;

//最大雷区

Lei lei[50][50];



//这个位置周围雷数为0

void leizero();

//计时器函数

afx_msg void OnTimer(UINT nIDEvent);

//鼠标按下左键

afx_msg void OnLButtonDown(UINT nFlags, CPoint point);

//鼠标按下右键

afx_msg void OnRButtonDown(UINT nFlags, CPoint point);

//初始化函数

afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);

//鼠标左键松开

afx_msg void OnLButtonUp(UINT nFlags, CPoint point);



4. 具体实现



删去状态栏和工具栏:



开始执行程序,就能见到一个有状态栏和工具栏的大的单文档,与上图不同,所以我们第一步就是整理框架:

打开下面函数,把里面的一些语句去掉。如下所示:

int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)

{

if (CFrameWnd::OnCreate(lpCreateStruct) == -1)

return -1;



/* if (!m_wndToolBar.CreateEx(this, TBSTYLE_FLAT, WS_CHILD | WS_VISIBLE | CBRS_TOP

| CBRS_GRIPPER | CBRS_TOOLTIPS | CBRS_FLYBY | CBRS_SIZE_DYNAMIC) ||

!m_wndToolBar.LoadToolBar(IDR_MAINFRAME))

{

TRACE0("Failed to create toolbar\n");

return -1; // fail to create

}



if (!m_wndStatusBar.Create(this) ||

!m_wndStatusBar.SetIndicators(indicators,

sizeof(indicators)/sizeof(UINT)))

{

TRACE0("Failed to create status bar\n");

return -1; // fail to create

}



// TODO: Delete these three lines if you don't want the toolbar to

// be dockable

m_wndToolBar.EnableDocking(CBRS_ALIGN_ANY);

EnableDocking(CBRS_ALIGN_ANY);

DockControlBar(&m_wndToolBar);

*/

return 0;

}



设置窗口大小:



运行附加的代码,还能看到扫雷游戏的框架是不能调大小的,而且总是显示在最前面,这又是怎么实现的呢?

在下面函数里添加语句,你能说出前三句是什么意思吗?注释已经被我去掉了,如果不知道,不如按一下F1。



BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs)

{

if( !CFrameWnd::PreCreateWindow(cs) )

return FALSE;

// TODO: Modify the Window class or styles here by modifying

// the CREATESTRUCT cs

cs.dwExStyle=cs.dwExStyle|WS_EX_TOPMOST; //

cs.style=WS_SYSMENU|WS_OVERLAPPED|WS_MINIMIZEBOX;//;

//设置窗口大小:400*340

cs.cx=400;

cs.cy=340;

return TRUE;

}



构造函数:



由于构造函数是程序运行时就执行的,所以,除了对变量赋值之外,我们还可以把游戏的核心结构即内部数组赋值:先

是把全部格子的位图和雷数赋值为0,然后调用随机函数按指定雷数赋值为-1,最后把不是雷的格子的雷数赋值为相应的值。



CMy2_1View::CMy2_1View()

{

// TODO: add construction code here

for(int ii=0;ii<16;ii++)

m_Bitmap[ii].LoadBitmap(IDB_BITMAP14+ii);

for(int jj=0;jj<4;jj++)

m_anniu[jj].LoadBitmap(IDB_ANNIU1+jj);

//计时

second=0;

//1时开始计时

secondstart=0;

//行数

m_RowCount=25;

//列数

m_ColCount=16;

//雷数

leinum=80;

//剩余雷数

leftnum=leinum;

//jieshu=1时停止

jieshu=0;



int aa=0;



//初始化为0

for(int i=0;i
{

for(int j=0;j
{

lei[i][j].shumu=0;

lei[i][j].weitu=0;

}

}

//获取当前时间

CTime time=GetCurrentTime();

int s;

//获取秒数

s=time.GetSecond();

//设置40个雷

do

{

//以当前秒数为产生随机算法

int k=(rand()*s)%m_RowCount;

int l=(rand()*s)%m_ColCount;

//为了避免一个位置同时算两个雷

//只允许当前位置不是雷时赋值为雷

if(lei[k][l].shumu!=-1)

{

lei[k][l].shumu=-1;

aa++;

}



}while(aa!=leinum);

//给方格赋值,计算雷数

for(int a=0;a
for(int b=0;b
if(lei[a][b].shumu==0)

{

for(int c=a-1;c
for(int d=b-1;d
if(c>=0&&c=0&&d
if(lei[c][d].shumu==-1)

lei[a][b].shumu++;

}

}



界面函数:



现在,可以开始画界面了。如下函数:

很明显,前面部分是用画的方法画出整个界面,但是,后面for循环显示的位图并不是现在画界面的内容,为什么要写呢?

这是为了用户框重画的需要,当我们的游戏玩了一半后最小化,或是把部分窗口移出屏幕,或是执行了新的应用程序覆盖了原来的程序时,必须重画。我们调用重画函数,它都要重新执行OnDraw(CDC* pDC)函数,那么,此时它就必须把已经显示出来的位图也显示出来。而开始时雷区位图是不可见的,并不影响界面的初始化。



void CMy2_1View::OnDraw(CDC* pDC)

{

CMy2_1Doc* pDoc = GetDocument();

ASSERT_VALID(pDoc);

// TODO: add draw code for native data here



//画背景

CBrush mybrush1;

mybrush1.CreateSolidBrush(RGB(192,192,192));

CRect myrect1(0,0,1200,800);

pDC->FillRect(myrect1,&mybrush1);

//画黑框

CBrush mybrush;

mybrush.CreateSolidBrush(RGB(0,0,0));

CRect myrect(20,10,70,40);

pDC->FillRect(myrect,&mybrush);



CRect myrect2(325,10,375,40);

pDC->FillRect(myrect2,&mybrush);



CPen mypen;

CPen*myoldPen;

mypen.CreatePen(PS_SOLID,2,RGB(255,255,255));

myoldPen=pDC->SelectObject(&mypen);

//画黑框的白线

pDC->MoveTo(20,40);

pDC->LineTo(70,40);

pDC->LineTo(70,10);

pDC->MoveTo(325,40);

pDC->LineTo(375,40);

pDC->LineTo(375,10);

//画雷区边线

//左上角是白线,右下角是黑线,以显示立体感

for(int i=0;i
for(int j=0;j
{

pDC->MoveTo(10+i*15,50+j*15+14);

pDC->LineTo(10+i*15,50+j*15);

pDC->LineTo(10+i*15+14,50+j*15);

}

pDC->SelectObject(myoldPen);



CPen mypen2;

CPen*myoldPen2;

mypen2.CreatePen(PS_SOLID,1,RGB(0,0,0));

myoldPen2=pDC->SelectObject(&mypen2);

for(int ii=0;ii
for(int jj=0;jj
{

pDC->MoveTo(10+ii*15,50+jj*15+14);

pDC->LineTo(10+ii*15+14,50+jj*15+14);

pDC->LineTo(10+ii*15+14,50+jj*15);

}

pDC->SelectObject(myoldPen2);



CDC Dc;

if(Dc.CreateCompatibleDC(pDC)==FALSE)

AfxMessageBox("Can't create DC");

//显示按钮

Dc.SelectObject(m_anniu[0]);

pDC->BitBlt(180,10,160,160,&Dc,0,0,SRCCOPY);

//判断显示什么位图

//weitu=1已按下的数字区

//weitu=2显示旗

//weitu=3显示问号

for(int a=0;a
for(int b=0;b
{

if(lei[a][b].weitu==1)

{

Dc.SelectObject(m_Bitmap[lei[a][b].shumu]);

pDC->BitBlt(a*15+10,b*15+50,160,160,&Dc,0,0,SRCCOPY);

}

if(lei[a][b].weitu==2)

{

Dc.SelectObject(m_Bitmap[9]);

pDC->BitBlt(a*15+10,b*15+50,160,160,&Dc,0,0,SRCCOPY);

}

if(lei[a][b].weitu==3)

{

Dc.SelectObject(m_Bitmap[10]);

pDC->BitBlt(a*15+10,b*15+50

,160,160,&Dc,0,0,SRCCOPY);

}

//结束

if(jieshu==1&&lei[a][b].shumu==-1)

{

Dc.SelectObject(m_Bitmap[11]);

pDC->BitBlt(a*15+10,b*15+50,160,160,&Dc,0,0,SRCCOPY);

Dc.SelectObject(m_anniu[3]);

pDC->BitBlt(180,10,160,160,&Dc,0,0,SRCCOPY);

}



}



//显示黑框里的数字

int nOldDC=pDC->SaveDC();

pDC->SetTextColor(RGB(255,0,0));

pDC->SetBkColor(RGB(0,0,0));

CFont font;

if(0==font.CreatePointFont(160,"Comic Sans MS"))

{

AfxMessageBox("Can't Create Font");

}

pDC->SelectObject(&font);

CString str;

//利用判断显示位数,不够三位前面加0

if(leftnum<10)

str.Format("00%d",leftnum);

else

str.Format("0%d",leftnum);

pDC->TextOut(25,10,str);

if(second<10)

str.Format("00%d",second);

else if(second<100)

str.Format("0%d" ,second);

else

str.Format("%d" ,second);

pDC->TextOut(330,10,str);

pDC->RestoreDC(nOldDC);



}

运行一下,外观已经出来了,只是还不能玩。那我们就来添加一些功能函数,使它可以玩。

当然,如果你对程序已经有一定的经验的话,你就会指出上面的函数太长了。这并不太符合我们编程的要求。我们编程有一个讲究,就是尽量使函数的代码少,一般为一页左右,便于查看。那么,我们可以把上面的函数细分为几个小函数,然后在这个函数里面分别调用。



按下鼠标左键:



用if语句判断,如果在按钮上面,则显示按钮按下位图;如果在扫雷区,先把按钮位图改为张口位图,再判断按下的是否是雷,是就结束,重画,以显示所有的雷;否则,重画相应格子以显示数字。



void CMy2_1View::OnLButtonDown(UINT nFlags, CPoint point)

{

// TODO: Add your message handler code here and/or call default

//获取指针pdc

CDC *pDC=GetDC();

CDC Dc;

if(Dc.CreateCompatibleDC(pDC)==FALSE)

AfxMessageBox("Can't create DC");

//显示按下按钮

if(point.x>180&&point.x<210&&point.y>10&&point.y<40)

{

Dc.SelectObject(m_anniu[3]);

pDC->BitBlt(180,10,160,160,&Dc,0,0,SRCCOPY);

}

if((point.x>=10)&&(point.x<=385)&&(point.y>=50)&&(point.y<=290))

{

if(ji

eshu==1)

return;



//显示张口按钮

Dc.SelectObject(m_anniu[1]);

pDC->BitBlt(180,10,160,160,&Dc,0,0,SRCCOPY);

// secondstart为1时计时有效

secondstart=1;

//鼠标坐标转换为数组坐标

int a=(point.x-10)/15;

int b=(point.y-50)/15;

if(lei[a][b].weitu==0||lei[a][b].weitu==3)

{

if(lei[a][b].shumu==-1)

{

jieshu=1;

//结束时,释放Timer

KillTimer(1);

//重画,因为这次重画将显示全部的雷,

//不能用部分重画

Invalidate();

}

else

{

lei[a][b].weitu=1;

CRect rect;

rect.left=a*15+10;

rect.right=a*15+25;

rect.top=b*15+50;

rect.bottom=b*15+65;

InvalidateRect(&rect);

}

}



} CView::OnLButtonDown(nFlags, point);

}

如果你现在运行的话,你会发现按下按钮时并不还原,这就涉及到鼠标函数:OnLButtonUp(UINT nFlags, CPoint point)



松开鼠标左键:



松开左键时,显示按钮没有按下的位图;再判断,如果结束,就要显示失败的位图;另外,如果是在按钮上松开按钮,即表示我们已经按下了重新开始的按钮,必须调用重新开始函数OnStart()。

由于OnStart()函数是与菜单里的开始共有的,此处先保留不说,若有必要运行,可以先去掉最后两行。



void CMy2_1View::OnLButtonUp(UINT nFlags, CPoint point)

{

// TODO: Add your message handler code here and/or call default

CDC *pDC=GetDC();

CDC Dc;

if(Dc.CreateCompatibleDC(pDC)==FALSE)

AfxMessageBox("Can't create DC");

//显示按钮

Dc.SelectObject(m_anniu[0]);

pDC->BitBlt(180,10,160,160,&Dc,0,0,SRCCOPY);



if(jieshu==1)

{

//显示按扭位图

Dc.SelectObject(m_anniu[2]);

pDC->BitBlt(180,10,160,160,&Dc,0,0,SRCCOPY);

}



//如果按下的是按扭,重新开始

if(point.x>180&&point.x<210&&point.y>10&&point.y<40)

OnStart();



CView::OnLButtonUp(nFlags, point);

}



按下鼠标右键:



如果是雷,我们按右键,显示旗子,并减少一个剩下雷数;如果我们认为那旗子的格子不是雷,我们按右键,显示问号,并在剩下雷数加上1。有关函数:



void CMy2_1View::OnRButtonDown(UINT nFlags, CPoint point)

{

// TODO: Add your message handler code here and/or call default

//结束,返回

if(jieshu==1)

return;

if((point.x>=10)&&

(point.x<=385)&&(point.y>=50)&&(point.y<=290))

{

int a=(point.x-10)/15;

int b=(point.y-50)/15;



if(lei[a][b].weitu==0||lei[a][b].weitu==3)

{

lei[a][b].weitu=2;

leftnum--;



}

else

if(lei[a][b].weitu==2)

{

lei[a][b].weitu=3;

leftnum++;

}



//重画剩下雷数

CRect rect2;

rect2.left=20;

rect2.right=70;

rect2.top=10;

rect2.bottom=40;

InvalidateRect(&rect2);



//重画打击格子

CRect rect;

rect.left=a*15+10;

rect.right=a*15+25;

rect.top=b*15+50;

rect.bottom=b*15+65;

InvalidateRect(&rect);

}

CView::OnRButtonDown(nFlags, point);

}



显示没有雷的区域:



运行,玩一下,你会发现当按下的是一个周围没有雷的格子是它并不会象Window里面的扫雷游戏一样显示它周围的格子雷数。怎么实现呢?

添加一个如下函数:



//扫描,如果是已经被按下且雷数为0,显示它周围的八个格,并重画

void CMy2_1View::leizero()

{

for(int i=0;i
for(int j=0;j
if(lei[i][j].shumu==0&&lei[i][j].weitu==1)

{

for(int n=i-1;n
for(int m=j-1;m
if(n>=0&&n<25&&m>=0&&m
if(lei[n][m].shumu!=-1&&lei[n][m].weitu==0)

{

lei[n][m].weitu=1;

CRect rect;

rect.left=n*15+10;

rect.right=n*15+25;

rect.top=m*15+50;

rect.bottom=m*15+65;

InvalidateRect(&rect);

}

}

}

再运行,效果是有的,只是它只显示一部分,即这个周围的几个。那么我们应该怎样使它显示全部呢?可以利用计时器函数。



计时器函数:



OnTimer(UINT nIDEvent)函数,同时也可以实现计时显示。添加OnCreate(LPCREATESTRUCT lpCreateStruct)和 OnTimer(UINT nIDEvent):



int CMy2_1View::OnCreate(LPCREATESTRUCT lpCreateStruct)

{

if (CView::OnCreate(lpCreateStruct) == -1)


return -1;

// TODO: Add your specialized creation code here

//20次为一秒

SetTimer(1,50,NULL);

return 0;

}



void CMy2_1View::OnTimer(UINT nIDEvent)

{

// TODO: Add your message handler code here and/or call default

//结束,返回

if(jieshu==1)

return;

//显示个数为0的方格

leizero();

//计时

if(secondstart>0)

secondstart++;

//二十次为一秒

if(secondstart==20)

{

secondstart=1;

second++;

//重画时间

CRect rect3;

rect3.left=325;

rect3.right=375;

rect3.top=10;

rect3.bottom=40;

InvalidateRect(&rect3);

}

CView::OnTimer(nIDEvent);

}



扫雷游戏就这样就是了。下面是附加内容,它将说明菜单的添加和重新开始函数的算法。





5. 附加内容



修改菜单:



游戏已经可以玩了,只是点到雷之后就完了,无法重新开始。还有,菜单还没有改。下面就修改菜单并实现重新开始功能:

把菜单改为如下图2-3。




图2-3



并在View()函数中按下图添加OnStart()函数(图2-4):


图2-4



开始函数:



OnStart()函数其实只是构造函数的再版。



void CMy2_1View::OnStart()

{

SetTimer(1,50,NULL);

// TODO: Add your command handler code here

second=0;//计时

secondstart=0;//1时开始计时

// num=0;

leftnum=leinum;//剩余雷数

jieshu=0;//jieshu=1时停止

int aa=0;

//初始化0

for(int i=0;i
{

for(int j=0;j
{

lei[i][j].shumu=0;

lei[i][j].weitu=0;

}

}

//设置leinum个雷

do

{

int k=rand()%m_RowCount;

int l=rand()%m_ColCount;

if(lei[k][l].shumu!=-1)

{

lei[k][l].shumu=-1;

aa++;

}



}while(aa!=leinum);

//给方格赋值

for(int a=0;a
for(int b=0;b
if(lei[a][b].shumu==0)

{

for(int c=a-1;c
for(int d=b-1;d
if(c>=0&&c=0&&d
if(lei[c][d].shumu==-1)

lei[a][b].shumu++;

}

Invalidate();



}



这样,整个程序完成了。






6. 小结





当然,这个游戏比Window自带的简单。但就目前来说,离它其实也不远。添加菜单项,并相应修改参数值:m_RowCount、 m_ColCount、leinum,并重新初始化界面就行了。

本书的例子都只是一些最基本的游戏算法,它教你怎样去实现游戏,而至于游戏的扩展,我只是提一些建议,让你自己去实现。

相关主题
文本预览
相关文档 最新文档