手把手教你用vc6做俄罗斯方块小游戏
第四章 俄罗斯方块
俄罗斯方块是我大学一年级刚学VC++时的课程设计,当时的课程设计有三种,单文档、
多文档和俄罗斯方块。我选择俄罗斯方块,就是因为它是游戏。
之前我是玩过俄罗斯方块的,一种是单人的(单人版),一种是两人对战的(对战版),
还有一种是网络版的,由于我还不了解网络,所以就决定编前两种。可是,这样没有新意,
我就想到了另外一种,配合游戏,或者称为情侣版。这里我先介绍三种,而网络版,由于我
们将介绍五子棋的网络游戏,鉴于它的简单性,我们将不介绍。
:以下三部分,可以以三章看待。
1、 1、 游戏实现
俄罗斯方块,或称积木游戏,它是利用一些形状各异却又是用正方形组成的方块,经
过不同位置不同角度的变化之后,堆积在一起的一种智力游戏。
而从我们编程的角度讲,我们只需要提供各种方块的图形,提供几个键盘操作键以供
方块的形状和位置的变化,提供几个功能函数以供游戏的正常进行。
各种方块图形:利用数组定形,然后利用随机函数随机地不按顺序地按游戏的需要而
出现。
键盘操作键:就是四个方向键。其中左、右、下三个键意思一样,上键的功能不是使
方块向上,而是使方块的下落角度改变。 功能函数将在变量函数里面介绍。
新建单文档工程4_1。
2、 2、 资源编辑
封面: IDB_BITMAP1
背景: IDB_BITMAP2
方块: IDB_BITMAP4
开始: ID_MENU_START
3、 3、 变量函数
接着就是定义变量了,但是,由于这个游戏要添加的变量和函数太多了,我们要建一
个新类。
是否应该先添加应该类呢?最好是这样。因为新类将会涉及到变量。
添加普通类Crussia,见下图。
图4-1-1
由于两个类一共有很多变量函数,列举如下:
// 4_1View.h :
//俄罗斯类
CRussia russia;
//开始标志
bool start;
//封面
CBitmap fenmian;
//暂停
BOOL m_bPause;
//开始菜单
afx_msg void OnMenuStart();
//计时器
afx_msg void OnTimer(UINT nIDEvent);
//键盘操作
afx_msg void OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags);
// Russia.h:
//游戏数组
int Russia[100][100];
// 当前图形
int Now[4][4];
//上一图形
int Will[4][4];
//变换后的图形
int After[4][4];
//当前图形的左上角位置
CPoint NowPosition;
//当前可能出现的图形形状数,
int Count;
//游戏结束
bool end;
//级别
int m_Level;
//速度
int m_Speed;
//分数
int m_Score;
//行列数
int m_RowCount,m_ColCount;
//方块
CBitmap fangkuai;
//背景
CBitmap jiemian;
//显示分数等内容
void DrawScore(CDC*pDC);
//消行
void LineDelete();
//方块移动
void Move(int direction);
//方块变化,即方向键上键操作
bool Change(int a[][4],CPoint p,int b[][100]);
//是否与原来方块接触,或与边界接触
bool Meet(int a[][4],int direction,CPoint p);
//显示下一个方块
void DrawWill();
//显示界面
void DrawJiemian(CDC*pDC);
//开始
void Start();
4、 4、 具体实现
然后,我们就可以一步一步地实现游戏了。函数依然是一个一个添加,如果有还没定
义的函数,添加空函数。以保证程序的条理性和可运行性。
CMy4_1View::CMy4_1View()
{
// TODO: add construction code here
fenmian.LoadBitmap(IDB_BITMAP1);
start=false;
m_bPause=false;
}
CRussia::CRussia()
{
jiemian.LoadBitmap(IDB_BITMAP2);
fangkuai.LoadBitmap(IDB_BITMAP4);
}
void CMy4_1View::OnDraw(CDC* pDC)
{
CMy4_1Doc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
// TODO: add draw code for native data here
CDC Dc;
if(Dc.CreateCompatibleDC(pDC)==FALSE)
AfxMessageBox("Can't create DC");
//没有开始,显示封面
if( !start)
{
Dc.SelectObject(fenmian);
pDC->BitBlt(0,0,500,550,&Dc,0,0,SRCCOPY);
}
//显示背景
else
russia.DrawJiemian(pDC);
}
开始时我们是设start为假,它就会在OnDraw()函数中画封面,而当我们开始游戏,start
为真,那么,它干什么呢?画背景!其函数如下:
还是那个道理,当有一些客户区生效(被挡住或最小化)时,它必须重画,而如果游戏
只是玩了一半,它必然在重画时必须把原先已经出现的方块、分数等也显示出来,怎么办?
就必须在画封面的同时也画出它们。当然,刚开始时它们是不会符合条件的。
void CRussia::DrawJiemian(CDC*pDC)
{
CDC Dc;
if(Dc.CreateCompatibleDC(pDC)==FALSE)
AfxMessageBox("Can't create DC");
//画背景
Dc.SelectObject(jiemian);
pDC->BitBlt(0,0,500,550,&Dc,0,0,SRCCOPY);
//画分数,速度,难度
DrawScore(pDC);
//如果有方块,显示方块
//游戏区
for(int i=0;i
BitBlt(j*30,i*30,30,30,&Dc,0,0,SRCCOPY);
}
//预先图形方块
for(int n=0;n<4;n++)
for(int m=0;m<4;m++)
if(Will[n][m]==1)
{
Dc.SelectObject(fangkuai);
pDC->BitBlt(365+m*30,240+n*30,30,30,&Dc,0,0,SRCCOPY);
}
}
其中还涉及另外一个函数DrawScore(pDC),它是画分数、速度、难度(本程序省略)
的。由于它的代码不是太少,另外用了一个函数,这样有利于理解。
void CRussia::DrawScore(CDC*pDC)
{
int nOldDC=pDC->SaveDC();
//设置字体
CFont font;
if(0==font.CreatePointFont(300,"Comic Sans MS"))
{
AfxMessageBox("Can't Create Font");
}
pDC->SelectObject(&font);
//设置字体颜色及其背景颜色
CString str;
pDC->SetTextColor(RGB(39,244,10));
pDC->SetBkColor(RGB(255,255,0));
//输出数字
str.Format("%d",m_Level);
if(m_Level>=0)
pDC->TextOut(440,120,str);
str.Format("%d",m_Speed);
if(m_Speed>=0)
pDC->TextOut(440,64,str);
str.Format("%d",m_Score);
if(m_Score>=0)
pDC->TextOut(440,2,str);
pDC->RestoreDC(nOldDC);
}
至此,可以看的都画完了。程序一般都是会先处理图形界面,因为这样在编核心内容时
能够让人有一个检查的机会。
现在,游戏总该开始了吧。添加菜单开始函数:ID_MENU_START
其函数如下:
void CMy4_1View::OnMenuStart()
{
// TODO: Add your command handler code here
start=true;
russia.Start();
SetTimer(1,50*(11-russia.m_Speed ),NULL);
}
先把start赋值为true,再调用russia.Start()函数,让它对俄罗斯方块游戏的相应变量赋
值,为了使游戏能够调整速度,设置一个可变的计数器。那么,russia.Start()函数做了什么
呢?
void CRussia::Start()
{
end=false;//运行结束标志
m_Score=0; //初始分数
m_Speed=0; //初始速度
m_Level=1; //初始难度
m_RowCount=18; //行数
m_ColCount=12; //列数
Count=7; //方块种类
//清空背景数组
for(int i=0;ii) k=i;
if(l>j) l=j;
}
for(i=0;i<4;i++)
for(j=0;j<4;j++)
Will[i][j]=0;
//把变换后的矩阵移到左上角
for(i=k;i<4;i++)
for(j=l;j<4;j++)
Will[i-k][j-l]=tmp[i][j];
// Now[][]的开始位置
NowPosition.x=0;
NowPosition.y=m_ColCount/2;
}
有了SetTimer( ),就别忘了OnTimer(UINT nIDEvent)函数:
先下移,再重画。
void CMy4_1View::OnTimer(UINT nIDEvent)
{
// TODO: Add your message handler code here and/or call default
//下移
russia.Move(3);
//重画
russia.DrawJiemian(GetDC());
CView::OnTimer(nIDEvent);
}
下移,用Move()函数,见下面。
那么,其中的参数是什么意思?方向!就是四个方向键。显然,上面只是用了其中之
一,即下移。那这个函数究竟是在哪里调用?聪明的读者一定知道就是键盘上的方向键!
就让我们在讲Move()函数之前,添加一个OnKeyDown(UINT nChar, UINT nRepCnt,
UINT nFlags)函数。
void CMy4_1View::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
{
// TODO: Add your message handler code here and/or call default
//没有开始
if(!start)
return;
//暂停
if(m_bPause==TRUE)
return;
switch(nChar)
{
case VK_LEFT:
russia.Move(1);
break;
case VK_RIGHT:
russia.Move(2);
break;
case VK_UP:
russia.Move(4);
break;
case VK_DOWN:
russia.Move(3);
break;
}
//重画
CDC* pDC=GetDC();
russia.DrawJiemian(pDC);
ReleaseDC(pDC);
CView::OnKeyDown(nChar, nRepCnt, nFlags);
}
很明显,上面其实都是根据下面的函数而工作的。下面的函数,先判断哪个方向,即
按了哪个方向键,然后调用一个判断是否过界或重叠的函数Meet(Now,1,NowPosition),然
后或者返回,或者移动。
另外,当下移并且被阻挡时,必须判断是否可以消行。
void CRussia::Move(int direction)
{
if(end)
return;
switch(direction)
{
//左
case 1:
if(Meet(Now,1,NowPosition)) break;
NowPosition.y--;
break;
//右
case 2:
if(Meet(Now,2,NowPosition)) break;
NowPosition.y++;
break;
//下
case 3:
if(Meet(Now,3,NowPosition))
{
LineDelete();
break;
}
NowPosition.x++;
break;
//上
case 4:
Meet(Now,4,NowPosition);
break;
default:
break;
}
}
上面涉及两个新的函数,介绍如下:
//消去行
void CRussia::LineDelete()
{
int m=0; //本次共消去的行数
bool flag=0;
for(int i=0;i0;k--)
{
//上行给下行
for(int l=0;l=m_ColCount) goto exit;
if(Russia[p.x+i][p.y+j+1]==1) goto exit;
break;
case 3://下移
if((p.x+i+1)>=m_RowCount) goto exit;
if(Russia[p.x+i+1][p.y+j]==1) goto exit;
break;
case 4://变换
if(!Change(a,p,Russia)) goto exit;
for(i=0;i<4;i++)
for(j=0;j<4;j++)
{
Now[i][j]=After[i][j];
a[i][j]=Now[i][j];
}
return false;
break;
}
}
int x,y;
x=p.x;
y=p.y;
//移动位置,重新给数组赋值
switch(direction)
{
case 1:
y--;break;
case 2:
y++;break;
case 3:
x++;break;
case 4:
break;
}
for(i=0;i<4;i++)
for(j=0;j<4;j++)
if(a[i][j]==1)
Russia[x+i][y+j]=1;
return false;
exit:
for(i=0;i<4;i++)
for(j=0;j<4;j++)
if(a[i][j]==1)
Russia[p.x+i][p.y+j]=1;
return true;
}
此函数是先把背景数组的相应位置赋值为零,而利用当前数组和一些局部变量数组的
交换赋值,然后检查是否符合放下背景数组的要求,是则按照这情况赋值,否则按原先情况
赋值。
其中变换时有一个新函数,是检查是否可以变换的。如下面。
转换,就是当按下向上方向键时,也要判断是否出界或重叠。
//转换
bool CRussia::Change(int a[][4], CPoint p,int b[][100])
{
int tmp[4][4];
int i,j;
int k=4,l=4;
for(i=0;i<4;i++)
for(j=0;j<4;j++)
{
tmp[i][j]=a[j][3-i];
After[i][j]=0; //存放变换后的方块矩阵
}
for(i=0;i<4;i++)
for(j=0;j<4;j++)
if(tmp[i][j]==1)
{
if(k>i) k=i;
if(l>j) l=j;
}
for(i=k;i<4;i++)
for(j=l;j<4;j++)
{
After[i-k][j-l]=tmp[i][j];
} //把变换后的矩阵移到左上角
//判断是否接触,是:返回失败
for(i=0;i<4;i++)
for(j=0;j<4;j++)
{
if(After[i][j]==0) continue;
if(((p.x+i)>=m_RowCount)||((p.y+j)<0)||((p.y+j)>=m_ColCount)) return false;
if(b[p.x+i][p.y+j]==1)
return false;
}
return true;
}
现在,我们的程序编好了,可以玩了。不难吧!
5、 附加内容
但是,比起附带的程序代码,是否还少了什么?暂停、热键、还有艺术字!
, ,
添加菜单如下图,添加函数如下:
图4-1-2
void CMy4_1View::OnMenuPause()
{
// TODO: Add your command handler code here
m_bPause=!m_bPause;
//停止计数器
if(m_bPause)
KillTimer(1);
//开始计数器
else
SetTimer(1,50*(11-russia.m_Speed ),NULL);
}
void CMy4_1View::OnUpdateMenuPause(CCmdUI* pCmdUI)
{
// TODO: Add your command update UI handler code here
//是否显示钩
pCmdUI->SetCheck(m_bPause);
}
, ,
如上图,打开Accelerator,添加如上ID号,选择如上ID号和热键就行了。
, ,
看了我的封面上的艺术字,是否有些心动,用VC++怎么设置出这样的艺术字?
我的答案是这不是用VC++做的,它只是一张位图。不过,这张位图是我自己做的,
怎么做?我可以说一下:
利用Word里面的艺术字,做好之后,把它拷贝到位图上就行了!
6、 6、 小结
一个人的游戏我们已经编出来了。
两个人的游戏又是如何的呢?
请看下一节!
1. 1. 游戏实现
上面各种界面告诉我们,接下来的工作将对游戏的界面进行很大的加工。最先是封面的
添加,接着是单人版的界面增加,最后还有本节的主要部分,对战版的添加。
封面:既然我们有了多种游戏,既然我们需要菜单选项,既然我们有了界面并且我们的
界面下面有空白可以让我们添加,我们就可以在封面是添加菜单选项。
界面:这里指的是游戏时的背景。上图我们显然是对界面进行了扩充,一是为了学习,
二是为了游戏的需要。由于原来的界面是接近正方形的,不利于我们这个双人游戏的显示;
我们最好是改变它,使它在玩双人游戏时的界面也是接近正方形的。当然,原来的可以保留。
使程序有多种界面可以选择。
对战版:本节的主要内容,它的实现是利用我们新建的类,利用类的多对象,同时产生
两个对象而形成的双人游戏。在这里,你将可以看到程序的另外一种扩充,你将会体会到原
来程序的扩充并不像上一章的方法,这里的更加简单且有趣。
当然,这是在4_1的基础上继续编程的。先复制文件夹4_1,改名为4_2,打开4_2文
件夹中的工作区,继续我们的程序。
2. 2. 资源编辑
图4-2-1
上面只是位图的组合,下面给出位图及其ID号(分别为IDB_BITMAP5~9)。
背景: IDB_BITMAP5
方块: IDB_BITMAP6
菜单: IDB_BITMAP7、IDB_BITMAP8、IDB_BITMAP9
删除开始菜单。
为了和封面菜单对应,我们添加菜单项如下:
文件:
单人游戏:ID_MENU_START
对战游戏:ID_MENU_DSTART
配合游戏:ID_MENU_TSTART
查看:
左视图: ID_VIEW_1
上视图: ID_VIEW_2。
其中,配合游戏只是一个空函数,它将在第三节实现。
3. 3. 变量函数
// 4_1View.h
//选择三种菜单图形
CBitmap xuanze[3];
//选择哪一种游戏(相对于菜单位图)
int ixuanze;
//选择哪一种游戏(相对于菜单)
int player;
//哪种界面(菜单)
int view;
//新对象
CRussia russia2;
// Russia.h:
//哪种界面(位图)
CBitmap jiemian2;
//哪种方块(大小)
CBitmap fangkuai2;
//新的界面函数
void DrawJiemian1(int a,int b,CDC*pDC);
void DrawJiemian2(int a,int b,CDC*pDC);
4. 4. 具体实现
记住变量要在各自的构造函数里面赋值。
CMy4_1View::CMy4_1View()
{
// TODO: add construction code here
fenmian.LoadBitmap(IDB_BITMAP1);
for(int i=0;i<3;i++)
xuanze[i].LoadBitmap(IDB_BITMAP7+i);
start=false;
m_bPause=false;
//第一种背景
view=1;
//第一种游戏
player=1;
//第一个封面菜单
ixuanze=1;
}
CRussia::CRussia()
{
jiemian.LoadBitmap(IDB_BITMAP2);
fangkuai.LoadBitmap(IDB_BITMAP4);
fangkuai2.LoadBitmap(IDB_BITMAP6);
jiemian2.LoadBitmap(IDB_BITMAP5);
}
如果是两人游戏,比起原先的界面,正方形的是不是感觉更好一些?那么,我们就必
须在原来的基础上重新添加一张位图(前面已经添加了),重新对DrawJiemian()函数进
行改造。但是,既然要改造,为何不干脆做成两种界面呢?
把查看菜单改为:左视图和上视图,其ID值分别为:ID_VIEW_1和ID_VIEW_2。并
添加如下函数:
void CMy4_1View::OnView1()
{
// TODO: Add your command handler code here
view=1;
//调整窗口大小
if(player==1)
AfxGetMainWnd()
->SetWindowPos(NULL,0,0,500,590,SWP_NOMOVE|SWP_NOZORDER );
if(player==2)
AfxGetMainWnd()
->SetWindowPos(NULL,0,0,1000,590,SWP_NOMOVE|SWP_NOZORDER );
}
void CMy4_1View::OnView2()
{
// TODO: Add your command handler code here
view=2;
if(!start)
return;
//调整窗口大小
if(player==1)
AfxGetMainWnd()
->SetWindowPos(NULL,0,0,253,510,SWP_NOMOVE|SWP_NOZORDER );
if(player==2)
AfxGetMainWnd()
->SetWindowPos(NULL,0,0,510,510,SWP_NOMOVE|SWP_NOZORDER );
}
//判断在哪里画勾
void CMy4_1View::OnUpdateView1(CCmdUI* pCmdUI) {
// TODO: Add your command update UI handler code here
pCmdUI->SetCheck(view==1);
}
void CMy4_1View::OnUpdateView2(CCmdUI* pCmdUI) {
// TODO: Add your command update UI handler code here
pCmdUI->SetCheck(view==2);
}
前两个函数分别赋值参数view,以有利于OnDraw()函数显示相应背景。而由于各个背
景大小不一,就需要对框架进行改变。后两个函数是判断应该在哪个菜单打钩。
那画界面的函数应该怎样改变呢?
它变化太大了,我们不得不说。函数由一个参数变成三个参数,前面两个是干什么的
呢?我们添加这的目的是为了可以在不同地方显示界面(的左上角),而添加的两个参数就
是显示界面的左上角的点。而为了能在正确位置显示分数,我们放弃了DrawScore()函数,
而是把它添加到这个函数里面。
void CRussia::DrawJiemian2(int a,int b,CDC*pDC)
{
CDC Dc;
if(Dc.CreateCompatibleDC(pDC)==FALSE)
AfxMessageBox("Can't create DC");
//画背景
Dc.SelectObject(jiemian2);
pDC->BitBlt(a,b,240,460,&Dc,0,0,SRCCOPY);
//画分数,速度,难度
//设置字体颜色及其背景颜色
CString str;
pDC->SetTextColor(RGB(198,24,190));
pDC->SetBkColor(RGB(255,255,0));
//输出数字
str.Format("%d",m_Level);
if(m_Level>=0)
pDC->TextOut(a+50,b+70,str);
str.Format("%d",m_Speed);
if(m_Speed>=0)
pDC->TextOut(a+50,b+42,str);
str.Format("%d",m_Score);
if(m_Score>=0)
pDC->TextOut(a+50,b+12,str);
//如果有方块,显示方块
//游戏区
for(int i=0;iBitBlt(a+j*20,b+100+i*20,30,30,&Dc,0,0,SRCCOPY);
}
//预先图形
for(int n=0;n<4;n++)
for(int m=0;m<4;m++)
if(Will[n][m]==1)
{
Dc.SelectObject(fangkuai2);
pDC->BitBlt( a+120+m*20,b+10+n*20,30,30,&Dc,0,0,SRCCOPY);
}
}
既然你已经理解了这个问题,那么我们是否也该把原来的DrawJiemian(CDC*pDC)函数
改了呢?是的。如下:
void CRussia::DrawJiemian1(int a,int b,CDC*pDC)
{
CDC Dc;
if(Dc.CreateCompatibleDC(pDC)==FALSE)
AfxMessageBox("Can't create DC");
//画背景
Dc.SelectObject(jiemian);
pDC->BitBlt(a,b,500,550,&Dc,0,0,SRCCOPY);
//画分数,速度,难度
int nOldDC=pDC->SaveDC();
//设置字体
CFont font;
if(0==font.CreatePointFont(300,"Comic Sans MS"))
{
AfxMessageBox("Can't Create Font");
}
pDC->SelectObject(&font);
//设置字体颜色及其背景颜色
CString str;
pDC->SetTextColor(RGB(39,244,10));
pDC->SetBkColor(RGB(255,255,0));
//输出数字
str.Format("%d",m_Level);
if(m_Level>=0)
pDC->TextOut(a+440,b+120,str);
str.Format("%d",m_Speed);
if(m_Speed>=0)
pDC->TextOut(a+440,b+64,str);
str.Format("%d",m_Score);
if(m_Score>=0)
pDC->TextOut(a+440,b+2,str);
pDC->RestoreDC(nOldDC);
// DrawScore(pDC);
//如果有方块,显示方块
//游戏区
for(int i=0;iBitBlt(a+j*30,b+i*30,30,30,&Dc,0,0,SRCCOPY);
}
//预先图形
for(int n=0;n<4;n++)
for(int m=0;m<4;m++)
if(Will[n][m]==1)
{
Dc.SelectObject(fangkuai);
pDC->BitBlt(a+365+m*30,b+240+n*30,30,30,&Dc,0,0,SRCCOPY);
}
}
接着,为了能看到效果,必须改变OnDraw(CDC* pDC)函数,如果没有开始,显示封
面,否则,依照view的值显示相应背景。由于我们现在player的值只有1,所以下面那个
player=2的条件不会执行,可以不管它。
void CMy4_1View::OnDraw(CDC* pDC) {
//窗口在中间
AfxGetMainWnd()->CenterWindow();
CMy4_1Doc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
// TODO: add draw code for native data here
CDC Dc;
if(Dc.CreateCompatibleDC(pDC)==FALSE)
AfxMessageBox("Can't create DC");
//没有开始,显示封面
if( !start)
{
Dc.SelectObject(fenmian);
pDC->BitBlt(0,0,500,550,&Dc,0,0,SRCCOPY);
//显示选择位图
Dc.SelectObject(xuanze[ixuanze-1]);
pDC->BitBlt(200,350,150,150,&Dc,0,0,SRCCOPY);
}
//显示背景
else
{
if(view==1)
{
if(player==1)
russia.DrawJiemian1(0,0,pDC);
if(player==2)
{
russia.DrawJiemian1(500,0,pDC);
russia2.DrawJiemian1(0,0,pDC);
}
}
if(view==2)
{
if(player==1)
russia.DrawJiemian2(0,0,pDC);
if(player==2)
{
russia.DrawJiemian2(253,0,pDC);
russia2.DrawJiemian2(0,0,pDC);
}
}
}
}
那么,上面的player=2是什么意思?封面已经告诉我们,我们的程序有三个子程序,
我们就分别用player=1,2,3来表示的。Player=3是下节的内容,这里只是顺便添加。三个
子程序,应该对应三个菜单项。添加函数如下:
//单人版
void CMy4_1View::OnMenuStart()
{
// TODO: Add your command handler code here
//先改变框架大小
if(view==1)
AfxGetMainWnd()
->SetWindowPos(NULL,0,0,500,590,SWP_NOMOVE|SWP_NOZORDER );
if(view==2)
AfxGetMainWnd()
->SetWindowPos(NULL,0,0,253,510,SWP_NOMOVE|SWP_NOZORDER );
//player赋值为1
player=1;
start=true;
russia.Start();
SetTimer(1,50*(11-russia.m_Speed ),NULL);
}
//双人版
void CMy4_1View::OnMenuDstart()
{
// TODO: Add your command handler code here
if(view==1)
AfxGetMainWnd()
->SetWindowPos(NULL,0,0,1000,590,SWP_NOMOVE|SWP_NOZORDER );
if(view==2)
AfxGetMainWnd()
->SetWindowPos(NULL,0,0,510,510,SWP_NOMOVE|SWP_NOZORDER );
//player赋值为2
player=2;
start=true;
//开始第一人
russia.Start();
//时间间隔
Sleep(300);
//开始第二人
russia2.Start();
SetTimer(1,50*(11-russia.m_Speed ),NULL);
}
//配合版
void CMy4_1View::OnMenuTstart()
{
// TODO: Add your command handler code here
AfxMessageBox("还没有完成!"); }
现在运行,界面出来了,可是对战时,一边操作不了。
原来是OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)函数还没有改。
在 case VK_DOWN后面添加A,S,D,W四个键的操作:
case VK_DOWN:
russia.Move(3);
break;
case 65:
russia2.Move(1);
break;
case 68:
russia2.Move(2);
break;
case 87:
russia2.Move(4);
break;
case 83:
russia2.Move(3);
break;
可还是不对,它不会自动下落。
改变OnTimer(UINT nIDEvent)函数如下:
void CMy4_1View::OnTimer(UINT nIDEvent)
{
// TODO: Add your message handler code here and/or call default
//下移
russia.Move(3);
//如果是两人,第二个也下移
if(player==2)
russia2.Move(3);
//重画
OnDraw(GetDC());
CView::OnTimer(nIDEvent);
}
这样,对战游戏就完成了。简单又有趣。我们不止添加了游戏,还添加了功能。
5. 5. 附加内容
下面我们添加鼠标和键盘的应用。它只要体现在封面菜单的选择上面。我们的封面有三
个菜单项,我们分别用键盘和鼠标了实现我们对游戏开始的选择。
上面有一个变量ixuanze没有用到,它是干什么的?就是这三个菜单项的参数。数值分
别对应于player。
在OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)函数里面添加如下内容:如果
没有开始,如果按下上下键,改变菜单先项,体现在位图的更换。如果按空格键,开始相应
的游戏。
下面是整个函数的内容:
void CMy4_1View::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags) {
// TODO: Add your message handler code here and/or call default
//没有开始
if(!start)
{
//部分重画
CRect rect;
rect.left=170;
rect.top=330;
rect.right=340;
rect.bottom=450;
//按下键,ixuanze循环增加
if(nChar==VK_DOWN)
{
if(ixuanze<3)
ixuanze++;
else
ixuanze=1;
InvalidateRect(&rect);
}
//按上键,ixuanze循环减少
if(nChar==VK_UP)
{
if(ixuanze>1)
ixuanze--;
else
ixuanze=3;
InvalidateRect(&rect);
}
if(nChar==VK_SPACE)
{
if(ixuanze==1)
OnMenuStart();
if(ixuanze==2)
OnMenuDstart();
if(ixuanze==3)
OnMenuTstart();
}
return;
}
//暂停
if(m_bPause==TRUE)
return;
switch(nChar)
{
case VK_LEFT:
russia.Move(1);
break;
case VK_RIGHT:
russia.Move(2);
break;
case VK_UP:
russia.Move(4);
break;
case VK_DOWN:
russia.Move(3);
break;
case 65:
russia2.Move(1);
break;
case 68:
russia2.Move(2);
break;
case 87:
russia2.Move(4);
break;
case 83:
russia2.Move(3);
break;
}
//重画
OnDraw(GetDC());
CView::OnKeyDown(nChar, nRepCnt, nFlags); }
添加鼠标操作函数。鼠标有两种操作:一种是选择,利用鼠标的移动,选择相应的
参数;一种是开始,利用鼠标的按下,开始相应的游戏。添加函数:
void CMy4_1View::OnMouseMove(UINT nFlags, CPoint point)
{
// TODO: Add your message handler code here and/or call default
//如果开始,退出
if(start)
return;
//部分重画
CRect rect;
rect.left=170;
rect.top=330;
rect.right=340;
rect.bottom=450;
//point.x和point.y是判断指针是否在菜单上面
if(point.x>200&&point.x<350)
{
if(point.y>350&&point.y<380)
{
//如果选择菜单不同,改变,重画
if(ixuanze!=1)
{
ixuanze=1;
InvalidateRect(&rect);
}
}
if(point.y>380&&point.y<410)
if(ixuanze!=2)
{
ixuanze=2;
InvalidateRect(&rect);
}
if(point.y>410&&point.y<440)
if(ixuanze!=3)
{
ixuanze=3;
InvalidateRect(&rect);
}
}
CView::OnMouseMove(nFlags, point);
}
现在,当我们的鼠标指针在菜单上面移动时,就会显示不同的选项。但是,我们该
怎样让它开始?添加如下函数:
void CMy4_1View::OnLButtonDown(UINT nFlags, CPoint point) {
// TODO: Add your message handler code here and/or call default
//判断是否在菜单位图上
if(point.x>200&&point.x<340&&point.y>350&&point.y<450)
{
//判断菜单参数,开始相应游戏
if(ixuanze==1)
OnMenuStart();
if(ixuanze==2)
OnMenuDstart();
if(ixuanze==3)
OnMenuTstart();
}
CView::OnLButtonDown(nFlags, point);
}
6. 6. 小结
如果说,五子棋游戏的扩展太复杂的话,那这个游戏的扩展就简单多了。而且好象还
添加了更多的内容。为什么?因为五子棋的扩展是把其中一个人改为计算机,本来很多人的
想法都必须添加到程序里面,从而使程序复杂化。而这个游戏,它是利用资源重复的道理,
在原来的函数基础上改变一下,形成新的函数,实现新的功能,又利用一个类可以派生多个
对象的原理,根本就只要几行代码而已。
下面,我们将进行再一次的扩充,它又会是怎样的呢?
1、 1、 游戏实现
配合版的界面就在上面,它的规则是:两个人一起游戏,只有一个游戏区域,整行满
了才能消去;只有一个预备方块,谁先要就给谁,当然这也可以配合;还有一点,两个活动
方块都可以在整个区域移动,但是,如果下移时被下面的活动方块挡住的话,就会停止下移。
这个程序,我们将用到的是类的继承。为了实现一个新的游戏,我们不得不添加代码,
为了不至于添加太多的代码,我们需要利用原来的代码,继承就是这种思想。
关于这个游戏的实现,我要谈到创新的问题。何为创新?就是以独特的思维方式,想
出或做出有实际意义的而又不是通常能注意到的问题或方法。
如果妈妈生病进了医院,我们应该为她担心。这是正常的,能够得以理解的。当时,
我更为爸爸的身体担心!为什么呢?因为妈妈有很多人照顾,可爸爸没有,可能连他自己都
忘记了照顾自己,我很为他担心。而这个,是经常被人所忽略了的。当时,它确实很有意义。
首先,我们复制4_2 并改名为4_3,打开工作区, 开始编程。
2、 2、 资源编辑
添加如上背景位图ID_BITMAP10。
3、 3、 变量函数
新的游戏,新的需求。既然原来的类不能满足我们的要求,我们就必须添加新类。已
经说过,我们需要继承:
class CRussia0 : public CRussia
并在CMy4_1View()中添加对象:russia0。
在CRussia0类中添加变量函数:
//第二个活动位置
CPoint NowPosition0;
//第二个活动方块
int Now0[4][4];
//背景
CBitmap jiemian0;
//函数意思和上面相同
void LineDelete(int a[][4]);
bool Meet0(int a[][4],int direction,CPoint p);
void Move(int direction);
void DrawWill(int a[][4]);
void Start();
void DrawJiemian(CDC*pDC);
除了添加的之外,其他的都继承Crussia类。
4、 4、 具体实现
有了新类和变量jiemian0,我们可以来显示背景位图:在构造函数里添加语句:
jiemian0.LoadBitmap(IDB_BITMAP10);
添加显示函数,如下:
void CRussia0::DrawJiemian(CDC *pDC)
{
CDC Dc;
if(Dc.CreateCompatibleDC(pDC)==FALSE)
AfxMessageBox("Can't create DC");
//画背景
Dc.SelectObject(jiemian0);
pDC->BitBlt(0,0,500,600,&Dc,0,0,SRCCOPY);
//画分数,速度,难度
//设置字体颜色及其背景颜色
CString str;
pDC->SetTextColor(RGB(198,24,190));
pDC->SetBkColor(RGB(255,255,0));
//输出数字
str.Format("%d",m_Level);
if(m_Level>=0)
pDC->TextOut(80,70,str);
str.Format("%d",m_Speed);
if(m_Speed>=0)
pDC->TextOut(80,42,str);
str.Format("%d",m_Score);
if(m_Score>=0)
pDC->TextOut(80,12,str);
//如果有方块,显示方块
//游戏区
for(int i=0;iBitBlt(j*20,100+i*20,30,30,&Dc,0,0,SRCCOPY);
}
//预先图形
for(int n=0;n<4;n++)
for(int m=0;m<4;m++)
if(Will[n][m]==1)
{
Dc.SelectObject(fangkuai2);
pDC->BitBlt( 220+m*20,10+n*20,30,30,&Dc,0,0,SRCCOPY);
}
}
这个函数就不用解释了。
然后,在void CMy4_1View::OnDraw()函数里面添加显示位图的内容:在else里最后的
地方添加两个语句:
//显示背景
else
{
//省略。。。。。。。。。。。。。。
if(player==3)
russia0.DrawJiemian(pDC);
}
如果现在运行,本选择配合游戏,就能看到界面了。现在,我们从菜单开始,实现游
戏的具体功能。
在OnMenuTstart()函数中去掉原来的消息框,改变窗口大小,调用开始函数:
void CMy4_1View::OnMenuTstart()
{
// TODO: Add your command handler code here
// AfxMessageBox("还没有完成!");
AfxGetMainWnd()
->SetWindowPos(NULL,0,0,513,650,SWP_NOMOVE|SWP_NOZORDER );
player=3;
start=true;
russia0.Start();
SetTimer(1,50*(11-russia0.m_Speed ),NULL);
}
开始函数是新游戏的开始,是不可能继承的,添加如下:
void CRussia0::Start()
{
end=false;//运行结束标志
m_Score=0; //初始分数
m_Speed=0; //初始速度
m_Level=0; //初始难度
m_RowCount=25; //行数
m_ColCount=25; //列数
Count=7; //方块种类
for(int i=0;ii) k=i;
if(l>j) l=j;
}
for(i=0;i<4;i++)
for(j=0;j<4;j++)
Will[i][j]=0;
//把变换后的矩阵移到左上角
for(i=k;i<4;i++)
for(j=l;j<4;j++)
Will[i-k][j-l]=tmp[i][j];
//开始位置
if(a==Now)
{
NowPosition.x=0;
NowPosition.y=m_ColCount/4;
}
if(a==Now0)
{
NowPosition0.x=0;
NowPosition0.y=3*m_ColCount/4;
}
}
现在运行,方块出来了,可是不会动,设置计数器:把OnTimer(UINT nIDEvent) 函数
改为如下:
void CMy4_1View::OnTimer(UINT nIDEvent)
{
// TODO: Add your message handler code here and/or call default
//下移
if(player==1)
russia.Move(3);
if(player==2)
{
russia.Move(3);
russia2.Move(3);
}
if(player==3)
{
russia0.Move(3);
russia0.Move(7);
}
OnDraw(GetDC());
CView::OnTimer(nIDEvent);
}
Move(3)是行的,Move(7)对于程序运行也没有问题。问题是,这有什么用,Move
(7)对于原来的函数根本就没有作用。也就是说,这个函数我们也不能继承,那么,怎么
办?
添加如下:当然,这是一个翻版的函数,但由于同时有八个键在操作,我们不得不这
么做。
void CRussia0::Move(int direction)
{
if(end) return;
switch(direction)
{
//左
case 1:
if(Meet(Now,1,NowPosition)) break;
NowPosition.y--;
break;
//右
case 2:
if(Meet(Now,2,NowPosition)) break;
NowPosition.y++;
break;
//下
case 3:
if(Meet(Now,3,NowPosition))
{
LineDelete(Now);
break;
}
NowPosition.x++;
break;
//上
case 4:
Meet(Now,4,NowPosition);
break;
//左
case 5:
if(Meet(Now0,1,NowPosition0)) break;
NowPosition0.y--;
break;
//右
case 6:
if(Meet(Now0,2,NowPosition0)) break;
NowPosition0.y++;
break;
//下
case 7:
if(Meet(Now0,3,NowPosition0))
{
LineDelete(Now0);
break;
}
NowPosition0.x++;
break;
//上
case 8:
Meet0(Now0,8,NowPosition0);
break;
default:
break;
}
}
上面出现了两个问题:一是有两种Meet()函数,二是LineDelete()函数有了参数。
对于第一个问题,是因为前面七种情况适合父类的函数,我们采用继承;而最后一个
由于关系到一部分新类的变量,我们只能另外添加,并做了合理的调整:
bool CRussia0::Meet0(int a[][4], int direction, CPoint p)
{
int i,j;
//先把原位置清0
for(i=0;i<4;i++)
for(j=0;j<4;j++)
if(a[i][j]==1)
Russia[p.x+i][p.y+j]=0;
for(i=0;i<4;i++)
for(j=0;j<4;j++)
if(a[i][j]==1)
{
if(!Change(a,p,Russia))
{
for(i=0;i<4;i++)
for(j=0;j<4;j++)
if(a[i][j]==1)
Russia[p.x+i][p.y+j]=1;
return true;
}
for(i=0;i<4;i++)
for(j=0;j<4;j++)
{
Now0[i][j]=After[i][j];
a[i][j]=Now0[i][j];
}
return false;
}
return true;
}
显然,第二个问题也是这种情况,它涉及到新类的变量。可是,我们没有那么幸运,
不得不重写全部的代码:
本函数的关键部分是:中间的生成新活动方块;后面的判断是否结束:它必须是刚停
止的活动方块达到顶点才结束。
void CRussia0::LineDelete(int a[][4])
{
int m=0; //本次共消去的行数
bool flag=0;
for(int i=0;i0;k--)
//上行给下行
for(int l=0;l目录。
5. 5. 选中Pop-up Menu,即右键菜单。 6. 6. 单击插入按钮,确定。 7. 7. 在弹出对话框中选择CMy4_1View。确定,关闭。
8. 8. 打开ResourceView中的Menu,双击新添加的菜单项。然后按编辑菜单的方法
添加要添加的项目。
运行,右键点击,菜单就出来了。 可是,我们的封面已经有够多的菜单选项了。我们能否让右键菜单在进去之后才生效
呢?找到以下函数,在第一行添加语句:
void CMy4_1View::OnContextMenu(CWnd*, CPoint point)
{
//如果还没有开始
if(!start)
//返回,
return;
// CG: This block was added by the Pop-up Menu component
{
if (point.x == -1 && point.y == -1){
//keystroke invocation
CRect rect;
GetClientRect(rect);
ClientToScreen(rect);
point = rect.TopLeft();
point.Offset(5, 5);
}
CMenu menu;
VERIFY(menu.LoadMenu(CG_IDR_POPUP_MY4_1_VIEW));
CMenu* pPopup = menu.GetSubMenu(0);
ASSERT(pPopup != NULL);
CWnd* pWndPopupOwner = this;
while (pWndPopupOwner->GetStyle() & WS_CHILD)
pWndPopupOwner = pWndPopupOwner->GetParent();
pPopup->TrackPopupMenu(TPM_LEFTALIGN | TPM_RIGHTBUTTON, point.x,
point.y,
pWndPopupOwner);
}
}
6、 6、 小结
这一章,我们学习了俄罗斯方块游戏。
我们学习了不只是游戏的算法,我们的重点是学习程序的扩展过程,扩展的思路。
我们学习了类的继承,类的多对象性。
最后,我们还学习了一个实用的函数:鼠标的右键菜单。
程序,本来不是扩展而来的;而是有目的,有计划的进行编程的。
但是,这不是本书的目的;本书的编写目的是为了学习。 学习是应该从无到有,从小到大的。而这,体现出来的就是程序的扩展!
程序的扩展,可以是功能的扩展,可以是项目的扩展。
下面,我们将再介绍一些本程序的扩展思路:
1 1
这个游戏,前面我们只涉及到传统的方块类型。而事实上,我们在玩游戏的时候总
是会遇到一些其它的,形状更怪的,难道也就增加的方块。我们也可以添加,并改
变功能函数让其实现。
如下:
//适应于难度扩展
case 7:
Will[0][0]=1;
Will[1][0]=1;
Will[1][1]=1;
Will[1][2]=1;
Will[0][2]=1;
break;
case 8:
Will[0][0]=1;
Will[1][0]=1;
Will[2][0]=1;
Will[1][1]=1;
Will[1][2]=1;
break;
2 2由于本游戏是用位图实现方块的,我们用不了多颜色的变换。当然,你也可以制作多种
颜色的方块位图,然后实现随机变换颜色。
3 3
即设置边长不同的方块,同时也涉及到框架大小的变化。
4 4
可以是背景的修饰,也可以是方块的修饰,如在方块上面画一朵玫瑰花,画一颗红
心等。
5 5
添加联机版本的游戏。实现网上游戏。 本书最后将会介绍联机游戏。一章联机基础,一章五子棋联机游戏。那么,俄罗斯方块
的联机,就留给读者练习。