我罗斯方块最终篇
这个作业属于哪个课程 | 2020年面向对象程序设计 (福州大学 - 数学与计算机科学学院) |
---|---|
这个作业要求在哪里 | 我罗斯方块最终篇 |
这个作业的目标 | 1. 代码的 git 仓库链接。 |
2.运行截图/运行视频。 | |
3.代码要点 | |
4.收获与心得。 | |
5.依然存在的问题。 | |
小组成员 | 031902209 赖展业 |
031902239 林雨欣 | |
031902213 林志锋 | |
GitHub地址 | https://github.com/1355158565/- |
运行截图
运行视频
代码要点
渲染类
初始化界面
struct Face
{
int data[29][60]; //代表地图上每个块的数值,为2是墙,为1是方块,为0是空格
int color[29][60]; //地图上对应方块的颜色
};
void draw::inter_face() //定义初始化界面
{ int i,j;
for(i=0;i<29;i++) //宽为29,长为60
{ for(j=0;j<60;j++)
{ if(j==0 || j==19 || j==29 || j==30 || j==49 || j==59) //建墙
{ face.data[i][j]=2;
face1.data[i][j]=2;
gotoxy(i,2*j);
cout<<"■";
}
else if(i==28) //最底下一行设置为1,方便之后下落的判断
{ face.data[i][j]=1;
face1.data[i][j]=1;
gotoxy(i,2*j);
cout<<"■";
}
else //其余的都为空
{
face.data[i][j]=0;
face1.data[i][j]=0;
}
}
}
gotoxy(11,45);
cout<<"傻der";
gotoxy(13,45);
cout<<"左移:A";
gotoxy(15,45);
cout<<"右移:D";
gotoxy(17,45);
cout<<"加速下落:S";
gotoxy(19,45);
cout<<"旋转:W";
gotoxy(21,45);
cout<<"退出: ESC";
gotoxy(11,104);
cout<<"傻妞";
gotoxy(13,104);
cout<<"左移: ←";
gotoxy(15,104);
cout<<"右移: →";
gotoxy(17,104);
cout<<"加速下落: ↓";
gotoxy(19,104);
cout<<"旋转: ↑";
}
-
要做一个游戏,首先想要的肯定是要先设计一个界面,界面就是游戏的体现,如同第一次看人先看的是外在一样。
-
首先先要定义一个结构体,里面的face数组数据代表每一块方块的值,color数组则代表颜色。然后自己设定一个长度和宽度,然后根据face数组的值在边上以及规定的区域打印出墙来,而其余的地方都为空。
-
最后就是在自己规定的区域打印出操作提示,玩家名称等等。
移动光标以及隐藏光标
void draw::gotoxy(int x,int y) //移动坐标
{
COORD coord;
coord.X=y;
coord.Y=x;
SetConsoleCursorPosition( GetStdHandle( STD_OUTPUT_HANDLE ), coord );
}
void draw::hidden() //隐藏光标
{
HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO cci;
GetConsoleCursorInfo(hOut,&cci);
cci.bVisible=0;//赋1为显示,赋0为隐藏
SetConsoleCursorInfo(hOut,&cci);
}
- gotoxy是一种c/c++函数,功能是将光标移动到指定位置,用于后面对于方块的清空以及打印,快捷而且迅速。但是我发现仅仅只有gotoxy函数还是不够完美,因为会出现光标闪烁的情况。
如图所示:
- 因此我上网查询了隐藏光标的方法,即以上的hidden函数。
关于颜色
int draw::color(int ch) //定义颜色
{
switch(ch)
{
case 0: ch=9;break;
case 1:
case 2: ch=12;break;
case 3:
case 4: ch=14;break;
case 5: ch=10;break;
case 6: ch=13;break;
default: ch=7;break;
}
SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), ch); //更改文字颜色
return 0;
}
- SetConsoleTextAttribute函数是一个用于设置控制台窗口字体颜色和背景色的计算机函数,通过输入数字来获取所对应的颜色,因此我们可以给不同的方块设置不同的颜色。
方块类
typedef struct Diamonds
{ int space[4][4]; //4*4矩阵,为1为方块,0为空
}Dia;
class block{ //定义方块类
protected:
Dia block[7][4]; //一维基础7个方块,二维表示4个旋转次数
int temp[4][4];
public:
void init_dia(){
int i,j,k,z;
for(i=0;i<3;i++)
block[0][0].space[1][i]=1;
block[0][0].space[2][1]=1; //土形
for(i=1;i<4;i++)
block[1][0].space[i][1]=1;
block[1][0].space[1][2]=1; //L形--1
for(i=1;i<4;i++)
block[2][0].space[i][2]=1;
block[2][0].space[1][1]=1; //L形--2
for(i=0;i<2;i++)
{ block[3][0].space[1][i]=1;
block[3][0].space[2][i+1]=1; //Z形--1
block[4][0].space[1][i+1]=1;
block[4][0].space[2][i]=1; //Z形--2
block[5][0].space[1][i+1]=1;
block[5][0].space[2][i+1]=1; //田字形
}
for(i=0;i<4;i++)
block[6][0].space[i][2]=1; //1形
//基础7个形状
for(i=0;i<7;i++)
{
for(z=0;z<3;z++)
{
for(j=0;j<4;j++)
{
for(k=0;k<4;k++)
{
temp[j][k]=block[i][z].space[j][k];
}
}
for(j=0;j<4;j++)
{
for(k=0;k<4;k++)
{
block[i][z+1].space[j][k]=temp[4-k-1][j];
}
}
}
} //旋转后的21个形状
}
};
-
我们既然要使用方块,肯定要初始化方块,玩过俄罗斯方块的人都知道俄罗斯方块有7种类型,因此把所有的方块看成一个4*4的二维数组,有“砖”的地方表示为1,没有“砖”的地方表示为0,初始化七种方块的初始状态,再初始化旋转后的21个形状。
-
结构体数组block[][]的一维表示7种方块的种类,二维表示旋转次数。
游戏类
-
游戏类是我罗斯程序段中最重要的代码,起根本的运行和操作的作用。
-
在讲游戏类主体函数之前先介绍将要用到的重要函数。
画方块
void game::draw_block(int type,int space_c,int x,int y) //画方块
{
int i,j;
for(i=0;i<4;i++)
{
for(j=0;j<4;j++)
{
gotoxy(x+i,2*(y+j));
if(block[type][space_c].space[i][j]==1)
cout<<"■";
}
}
}
- 画方块函数的作用就是将代码程序中数字的变化转化成我们肉眼可见的方块图样显示在界面中,方块的移动需要它来不断地更新图样,以及下一个方块的显示也需要此函数。
画空格
void game::draw_kong(int type,int space_c,int x,int y) //画空方块
{
int i,j;
for(i=0;i<4;i++)
{
for(j=0;j<4;j++)
{
gotoxy(x+i,2*(y+j));
if(block[type][space_c].space[i][j]==1)
cout<<" ";
}
}
}
- 画空格函数与画方块函数是配套使用的,我们在方块不断移动的时候,将原来位置显示图样的方块擦除,然后画方块函数又会画出新的图样,以此来给我们显示方块移动的效果,并且在下一个方块显示的地方可以擦除上一个显示的方块,再利用画方块函数画出下一个将要来临的方块。
消除以及此消彼长
int game::xc() //消除
{
int i,j,k,sum;
for(i=27;i>4;i--)
{
sum=0;
for(j=1;j<19;j++)
{
sum+=face.data[i][j]; //记录某行总格数
}
if(sum==0)
break;
if(sum==18) //满一行,减掉
{
color(8);
for(j=1;j<19;j++)
{
face.data[i][j]=0;
gotoxy(i,2*j);
cout<<" ";
}
addline1(&face1); //此消彼长
for(j=i;j>1;j--)
{ sum=0;
for(k=1;k<19;k++)
{
sum+=face.data[j-1][k]+face.data[j][k];
face.data[j][k]=face.data[j-1][k];
if(face.data[j][k]==0)
{
gotoxy(j,2*k);
cout<<" ";
}
else
{
gotoxy(j,2*k);
color(face.color[j][k]);
cout<<"■"; //消除之后剩余的方块
}
}
if(sum==0)
return 1;
}
}
}
for(i=1;i<19;i++)
{
if(face.data[1][i]==1)
{
char n;
Sleep(10); //延时判断到顶
system("cls");
color(7);
gotoxy(29/2,50);
cout<<"傻妞胜利!!"<<endl;
exit(0);
}
}
return 0;
}
void game::addline(struct Face *t){ //定义增加垃圾行的操作
int i,j;
for(i=1;i<=27;i++) //将整体画布坐标上移
{
for(j=1;j<19;j++)
{
t->data[i-1][j]=t->data[i][j];
t->color[i-1][j]=t->color[i][j];
}
}
for(i=0;i<=27;i++) //将全部方块都清空
{
for(j=1;j<19;j++)
{
gotoxy(i,j*2);
cout <<" ";
gotoxy(i,j*2+1);
cout <<" ";
}
}
srand(time(NULL)); //根据不同时间生成不同的种子
int deadline=rand()%17+8; //随机产生生成行的方块数
for(i=1;i<=18;i++)
t->data[27][i]=0; //使最底下一行为空
for(i=1;i<=deadline;i++){
int k=rand()%18+1;
if(t->data[27][k]==0){
t->data[27][k]+=1;
t->color[27][k]=0;}
}
for(i=0;i<=27;i++) //重新画画布
{
for(j=1;j<19;j++)
{
if(t->data[i][j]==1)
{
gotoxy(i,j*2);
color(t->color[i][j]);
cout<<"■";
}
else
{
gotoxy(i,j*2);
cout <<" ";
}
}
}
}
-
显而易见,消除函数的作用就是将满行的方块删除,思路便是从下到上开始遍历,如果遇到满行的方块便进行消行处理,将那一行的方块打印为空,并且将那行以上的画布向下平移一格
(即将上方的face数组的值赋给下一格的face数组,然后将=0的数组的位置清空),便成功进行了消行操作。 -
在双人对战中,有一个特殊的要求,那就是自己消除一行后,对方要增加一行,即为此消彼长,说实话在实现了消行操作后,此消彼长的实现其实并不是那么难,我的想法就是:由于是要在方块的底部增加垃圾行,因此我们要先将对方的face数组传进函数,然后将整体的画布向上平移一格,然后先用随机数函数生成垃圾方块的个数,再用进行个数次数的循环(在循环中使用随机数函数随机生成垃圾方块在行中的位置),便可实现此消彼长的操作。刚开始的时候我设置的随机数是从1~18,然后以下这段代码的循环中,每次循环也有可能随机到相同的位置,因此需要将增加的随机的方块数从8起步。
srand(time(NULL)); //根据不同时间生成不同的种子
int deadline=rand()%17+8; //随机产生生成行的方块数
for(i=1;i<=18;i++)
t->data[27][i]=0; //使最底下一行为空
for(i=1;i<=deadline;i++){
int k=rand()%18+1;
if(t->data[27][k]==0){
t->data[27][k]+=1;
t->color[27][k]=0;}
}
- 最后在消除的函数中,我们还可以进行封顶的判断,如果最顶上已经有方块了,则延时后判断对方胜利。
判断
int game::pd(int n,int space_c,int x,int y)
{
int i,j;
for(i=0;i<4;i++)
{
for(j=0;j<4;j++)
{
if(block[n][space_c].space[i][j]==0)
continue;
else if(face.data[x+i][y+j]==2||face.data[x+i][y+j]==1)
return 0;
}
}
return 1;
}
- 判断函数就是先找出这个方块哪个格子有砖,通过有砖的部位来判断四周的环境是否允许移动,如果有墙或者有砖便不可移动。
开始游戏函数
void start_game1()
{ int n,ch,t=0,x=0,y=8,i,j;
draw_kong(nn,0,4,23); //用于擦除上一个显示的方块
n=nn;
nn=rand()%7; //随机生成下一块
color(nn);
draw_block(nn,0,4,23); //用于画下一个方块
while(1)
{
color(n);
draw_block(n,space_c,x,y); //画出图形
if(t==0)
t=10000; //控制下降时间(速度)
while(--t)
{ if(kbhit()!=0) //有输入就跳出
break;
}
if(t==0)
{
if(pd(n,space_c,x+1,y)==1) //判断下一行是否能下降
{ draw_kong(n,space_c,x,y);
x++; //向下降落
}
else //到底则固定方块
{
for(i=0;i<4;i++)
{
for(j=0;j<4;j++)
{
if(block[n][space_c].space[i][j]==1)
{
face.data[x+i][y+j]=1;
face.color[x+i][y+j]=n;
while(xc()); //判定是否能消除
}
}
}
break;
}
}
else
{
ch=getch();
switch(ch) //移动
{
case 'a': if(pd(n,space_c,x,y-1)==1) //判断是否可以移动 //左移
{ draw_kong(n,space_c,x,y);
y--;
}
break;
case 'd': if(pd(n,space_c,x,y+1)==1) //右移
{ draw_kong(n,space_c,x,y);
y++;
}
break;
case 's': if(pd(n,space_c,x+1,y)==1) //加速下降
{ draw_kong(n,space_c,x,y);
x++;
}
break;
case 'w': if(pd(n,(space_c+1)%4,x+1,y)==1) //旋转
{ draw_kong(n,space_c,x,y);
space_c=(space_c+1)%4; //记录旋转次数
}
break;
case 27 : system("cls"); //清屏
gotoxy(29/2,45);
printf("---游戏结束!---\n\n");
gotoxy(29/2+2,45);
printf("---按任意键退出!---\n");
getch();
exit(0); //结束循环
break;
}
}
}
}
void start_game2()
{ int n,ch,t=0,x=0,y=38,i,j;
draw_kong(nnn,0,4,53);
n=nnn;
nnn=rand()%7;
color(nnn);
draw_block(nnn,0,4,53);
while(1)
{
color(n);
draw_block(n,space_c1,x,y);
if(t==0)
t=10000;
while(--t)
{ if(kbhit()!=0)
break;
}
if(t==0)
{
if(pd1(n,space_c1,x+1,y)==1)
{ draw_kong(n,space_c1,x,y);
x++;
}
else
{
for(i=0;i<4;i++)
{
for(j=0;j<4;j++)
{
if(block[n][space_c1].space[i][j]==1)
{
face1.data[x+i][y+j]=1;
face1.color[x+i][y+j]=n;
while(xc1());
}
}
}
break;
}
}
else
{
ch=getch();
switch(ch)
{
case 75: if(pd1(n,space_c1,x,y-1)==1)
{ draw_kong(n,space_c1,x,y);
y--;
}
break;
case 77: if(pd1(n,space_c1,x,y+1)==1)
{ draw_kong(n,space_c1,x,y);
y++;
}
break;
case 80: if(pd1(n,space_c1,x+1,y)==1)
{ draw_kong(n,space_c1,x,y);
x++;
}
break;
case 72: if(pd1(n,(space_c1+1)%4,x+1,y)==1)
{ draw_kong(n,space_c1,x,y);
space_c1=(space_c1+1)%4;
}
break;
case 27 : system("cls");
gotoxy(29/2,45);
printf("---游戏结束!---\n\n");
gotoxy(29/2+2,45);
printf("---按任意键退出!---\n");
getch();
exit(0);
break;
}
}
}
}
-
开始游戏函数有两个,一个代表玩家1,一个代表玩家2。
-
这个函数的思路就是随机从方块类中挑选出来一个方块,并且随机的挑选一个他的初始化状态。然后这个被挑选的方块,不断的按照自己设定的速度进行往下运动。同时可以使用方向键使得它可以左右运动,并循环的变化他的状态。如果这个矩阵在运动的方向上遇到了数值为1或2的时候则停止运动,在左右运动上表现为不能移动,在往下运动的时候则表现为这个方块运动的结束。把这个矩阵的数值复制到背景矩阵中去。检测是否可以消行,检查所有的行,并且做同样动作。检查完成后,进入下个方块的随机挑选,下落。当某个方块下落完成的时候。他的x坐标在背景中为0的时候。游戏结束。在这个函数中,我设置了双方的按键设定,且玩家一可以通过按Esc键来退出游戏。
主函数
int main(){
system("cls");
system("title 我罗斯方块");
system("mode con cols=120 lines=30");
srand(time(NULL));
int ans=rand()%7;
int ans1=rand()%7;
game player(ans,ans1);
player.inter_face();
player.hidden();
player.init_dia();
while(1)
{
player.start_game1(); //玩家1
player.start_game2(); //玩家2
}
system("pause");
}
-
设置控制台大小以及标题。
-
建立游戏类对象player,通过while循环进行游戏。
-
在本次的我罗斯程序中,我通过试验以及测试别人的代码发现,单单只有Khbit函数是无法实现两个人同时操作的,即一个人按住按键时,另外一个人动不了,大大降低了游戏的可玩性,由于我不懂得如何能够实现双人同时自由操作,因此我便将本次的我罗斯双人对战游戏变成了回合制,稍微加快方块的下落速度,经过自己测试颇具可玩性(自己一个人也可以玩哈哈哈哈哈哈哈哈哈哈,比比自己的左手和右手哪个强),不仅可以单人操作,双人也可以玩的开心。
收获与心得
- 本次面向对象程序设计课程所布置的大作业以与以往单做编程题所截然不同的形式呈现,无疑是对本课程学习更深层次的探索。知识层面不但涉及到面向对象的类和对象,还涉及到了利用Windows窗口绘制游戏界面、方块渲染等其他知识。可以说将“纸上得来终觉浅,绝知此事要躬行”的道理表现得淋漓尽致。从刚开始的对网络检索到的俄罗斯方块代码看得一头雾水到逐渐看懂各部分代码实现的相应功能,从对Windows窗口绘制一无所知到动手操作。程序的完整实现依赖科学的需求分析和从全局到局部的渐进,学习c++的同时运用c++的思想,主课课程的推进(如学到继承和派生)更是让程序设计变得更加高效简单。且相较于先前学习不同的是,编写我罗斯方块采用分文件的方法,将游戏的block类、draw类、game类区分开。另外,编写我罗斯方块对细节的要求较多,如定时处理机制、游戏控制需求、界面区域需求等,与普通的俄罗斯方块不同的是,我们的需求增加了双人对战和一玩家消行另一玩家增行。逐步地分析他人的代码得到启发、触类旁通、问题一个个迎刃而解。原来的设想构思变为可执行的真正意义上的我罗斯方块。此次实践,我们体会到了学以致用的乐趣,我罗斯方块也许还有待优化的地方或者说能够增加更多有趣的功能,这就要求我们学习更多的知识。在本次大作业的任务中,我们还懂得了团队协作的重要性,众人拾柴火焰高,即使再难的任务与作业,我相信在多人的合作下,每个人真心付出,我们依旧可以将作业做好,希望在以后的工作与学习中我们依然拥有团队,享受团队。
依然存在的问题
-
我觉得本小组这次写出来的我罗斯方块最大的问题其实还是在于无法实现双人同时自由操作的问题上,期待学会了双线程后能够再次改善这个程序。
-
还有一个问题是本次实现代码没有用到许多类的功能,单纯只用了继承,在类的封装性有极大的缺失,希望以后能够完善。
-
因为是第一次写这种小游戏程序,与以往的编程题大不一样,因此并未实现一些花里胡哨的功能哈哈哈,比较简单吧。