【C语言】百行代码实现—俄罗斯方块

自述

这个代码是19年的末尾写的,最近就想着想把这个用博客分享出来,一方面是为了巩固自己的知识,另一方面也希望同学们能够因为这篇文章有所收获,所以也在原来代码的基础上添加了蛮多注释的(感觉就和重新写了一遍俄罗斯方块似的😂) ,如果对于这篇文章有任何问题都可以提出来,也希望这篇文章能够帮助到大家,谢谢大家。

整体框架和流程

开始游戏页面设计

欢迎界面
在这里插入图片描述
游戏开始界面
在这里插入图片描述
这些大家应该都可以知道这个页面的布局了吧,接下来我们来了解对于这些布局我们应该要利用哪些函数来设置


初始化画布
如果我们像要进行图形界面的编程和绘画的话就一定要去初始化画布

	//初始画布
	initgraph(550,660);//表示初始化长和宽

设置窗口标题:利用window接口

	//设置窗口标题:利用window接口
	HWND hwnd=GetHWnd();//hwnd变量来接收,句柄
	SetWindowText(hwnd,_T("俄罗斯方块  宋同学2019.12.18"));

对于显示中间的文字

	//设置文本的字体样式
	setfont(40,0,_T("微软雅黑"));//高度40个像素宽度自己适应
	setcolor(WHITE);
	outtextxy(205,200,_T("俄罗斯方块"));//利用坐标
	setfont(22,0,_T("楷体"));
	outtextxy(175,300,_T("编程从俄罗斯方块开始!"));
	Sleep(3000);//休眠3秒钟那样转到游戏界面就不会突兀,让玩家有准备时间

对于欢迎界面所有的代码:

void welcome(){
	//初始画布
	initgraph(550,660);

	//设置窗口标题:利用window接口
	HWND hwnd=GetHWnd();//hwnd变量来接收,句柄,用于
	SetWindowText(hwnd,_T("俄罗斯方块  宋同学2019.12.18"));

	//设置文本的字体样式
	setfont(40,0,_T("微软雅黑"));//高度40个像素宽度自己适应
	setcolor(WHITE);
	outtextxy(205,200,_T("俄罗斯方块"));
	setfont(22,0,_T("楷体"));
	outtextxy(175,300,_T("编程从俄罗斯方块开始!"));
	Sleep(3000);//休眠3秒钟那样转到游戏界面就不会突兀,让玩家有准备时间

}

初始化游戏界面
有些变量是设置为全局变量所以先不要太在意,等会儿都会有源码分享出来

//初始化游戏界面
void initGameScene(){
	char str[16];
	cleardevice();
	//绘制长方形框
	rectangle(27,27,336,635);
	rectangle(29,29,334,633);
	rectangle(370,50,515,195);

	setfont(24,0,_T("楷体"));
	setcolor(LIGHTGRAY);
	outtextxy(405,215,_T("下一个"));

	setcolor(RED);
	outtextxy(405,280,_T("分数"));
	//score是分数,定义为全局变量,等会回有源码分享
	sprintf(str,"%d",score);//把score写入到str中转换为字符串形式
	outtextxy(415,310,str);

	//和分数的设置同理
	outtextxy(405,375,_T("等级"));
	sprintf(str,"%d",rank);
	outtextxy(415,405,str);

	//操作说明
	setcolor(LIGHTBLUE);
	outtextxy(390,475,_T("操作说明"));
	outtextxy(390,500,"↑:旋转");
	outtextxy(390,525,"↓:下降");
	outtextxy(390,550,"←:左移");
	outtextxy(390,575,"→:右移");
	outtextxy(390,600,"空格:暂停");

}

由于代码量还是有一些的所以接下来就不每一个函数都进行分开讲,感觉那样那这篇博客就有点长了,后面有源码有挺多注释的,大家可以慢慢看。

游戏设计流程介绍

  • 设计图形界面
  • 更新"下一个"框框(利用随机取,当主窗口的图形落到最底下就更新(初始就直接先来一个))
  • 将下一个框框中的形态的图形复刻到主窗口的中
  • 实现主游戏窗口的方块进行移动
  • 进行消行处理
  • 更新分数以及等级

注意

其实这游戏设计起来其实不难,在我看来难点可能是出现在消行和更新方块的操作上,还有一点要注意的是,图形的更新可以通过先用黑方块把指定位置都给覆盖然后再重新画一个(这一点即适用于方块的下落也适用于下一个方块的更新上),对于消行的处理要注意的是:如果第i行的方块满了我们把这一行消了以后那么我们还要再次判断i行而不能直接判断i+1行,因为当我们第i行消了以后上面的方块压下来那么第i行可能还可以再消一次。


由于可能图形界面大家没怎么接触过所以我就单独的写了一下,其他的部分可能还是得靠大家自己的理解了,也没办法把所有的想法全部都写出来,下面的源码中加了蛮多的注释还是希望大家看看代码看看注释这样也能理解的深刻一些。

可执行源码-全部

头文件

#include<stdio.h>
#include<string>
#include<iostream>
#include<graphics.h>//图形界面编程
#include<time.h>
#include<conio.h>//kbhit()键盘捕捉的使用需要的头文件

各种宏定义及全局变量

#define BLOCK_COUNT 5
#define BlOCK_WIDTH 5
#define BLOCK_HEIGHT 5
#define UNIT_SIZE 20//右上角打印的方块的像素
#define START_X 130
#define START_Y 30
#define KEY_UP 72//向上按键ASCII码值为72
#define KEY_RIGHT 77
#define KEY_LEFT 75
#define KEY_DOWN 80
#define KEY_SPACE 32
int score=0;//分数
int rank=0;//等级

int speed=500;
int minX=30;
int minY=30;

//设置枚举值
typedef enum{
	BLOCK_UP,
	BLOCK_RIGHT,
	BLOCK_DOWN,
	BLOCK_LEFT
	
}block_dir_t;

typedef enum{
	MOVE_DOWN,
	MOVE_LEFT,
	MOVE_RIGHT
}move_dir_t;

int NextIndex=-1;//下一个方块的种类
int BlockIndex=-1;//当前方块的种类

int color[BLOCK_COUNT]={
	GREEN,CYAN,MAGENTA,BROWN,YELLOW
};

int visit[30][15];

int markcolor[30][15];//表示对应位置的方块的颜色

//初始化各种方块,包括其形态变换
int block[BLOCK_COUNT*4][BlOCK_WIDTH][BLOCK_HEIGHT]={
	// | 形方块
	{ 0,0,0,0,0,
	0,0,1,0,0,
	0,0,1,0,0,
	0,0,1,0,0,
	0,0,0,0,0 },

	{ 0,0,0,0,0,
	0,0,0,0,0,
	0,1,1,1,0,
	0,0,0,0,0,
	0,0,0,0,0 },

	{ 0,0,0,0,0,
	0,0,1,0,0,
	0,0,1,0,0,
	0,0,1,0,0,
	0,0,0,0,0 },

	{ 0,0,0,0,0,
	0,0,0,0,0,
	0,1,1,1,0,
	0,0,0,0,0,
	0,0,0,0,0 },

	// L 形方块
	{ 0,0,0,0,0,
	0,0,1,0,0,
	0,0,1,0,0,
	0,0,1,1,0,
	0,0,0,0,0 },

	{ 0,0,0,0,0,
	0,0,0,0,0,
	0,1,1,1,0,
	0,1,0,0,0,
	0,0,0,0,0 },

	{ 0,0,0,0,0,
	0,1,1,0,0,
	0,0,1,0,0,
	0,0,1,0,0,
	0,0,0,0,0 },

	{ 0,0,0,0,0,
	0,0,0,1,0,
	0,1,1,1,0,
	0,0,0,0,0,
	0,0,0,0,0 },

	// 田 形方块
	{ 0,0,0,0,0,
	0,1,1,0,0,
	0,1,1,0,0,
	0,0,0,0,0,
	0,0,0,0,0 },

	{ 0,0,0,0,0,
	0,1,1,0,0,
	0,1,1,0,0,
	0,0,0,0,0,
	0,0,0,0,0 },

	{ 0,0,0,0,0,
	0,1,1,0,0,
	0,1,1,0,0,
	0,0,0,0,0,
	0,0,0,0,0 },

	{ 0,0,0,0,0,
	0,1,1,0,0,
	0,1,1,0,0,
	0,0,0,0,0,
	0,0,0,0,0 },

	// T 形方块
	{ 0,0,0,0,0,
	0,1,1,1,0,
	0,0,1,0,0,
	0,0,0,0,0,
	0,0,0,0,0 },

	{ 0,0,0,0,0,
	0,0,0,1,0,
	0,0,1,1,0,
	0,0,0,1,0,
	0,0,0,0,0 },

	{ 0,0,0,0,0,
	0,0,1,0,0,
	0,1,1,1,0,
	0,0,0,0,0,
	0,0,0,0,0 },

	{ 0,0,0,0,0,
	0,1,0,0,0,
	0,1,1,0,0,
	0,1,0,0,0,
	0,0,0,0,0 },

	// Z 形方块
	{ 0,0,0,0,0,
	0,1,1,0,0,
	0,0,1,1,0,
	0,0,0,0,0,
	0,0,0,0,0 },

	{ 0,0,0,0,0,
	0,0,1,0,0,
	0,1,1,0,0,
	0,1,0,0,0,
	0,0,0,0,0 },

	{ 0,0,0,0,0,
	0,1,1,0,0,
	0,0,1,1,0,
	0,0,0,0,0,
	0,0,0,0,0 },

	{ 0,0,0,0,0,
	0,0,1,0,0,
	0,1,1,0,0,
	0,1,0,0,0,
	0,0,0,0,0 },
};//5种方块每种方块4种形态方块的表示是用5行5列来表示

各类方法

void welcome(){
	//初始画布
	initgraph(550,660);

	//设置窗口标题:利用window接口
	HWND hwnd=GetHWnd();//hwnd变量来接收
	SetWindowText(hwnd,_T("俄罗斯方块  宋同学2019.12.18"));

	//设置文本的字体样式
	setfont(40,0,_T("微软雅黑"));//高度40个像素宽度自己适应
	setcolor(WHITE);
	outtextxy(205,200,_T("俄罗斯方块"));
	setfont(22,0,_T("楷体"));
	outtextxy(175,300,_T("编程从俄罗斯方块开始!"));
	Sleep(3000);//休眠3秒钟那样转到游戏界面就不会突兀,让玩家有准备时间

}
//初始化游戏界面
void initGameScene(){
	char str[16];
	cleardevice();
	rectangle(27,27,336,635);
	rectangle(29,29,334,633);
	rectangle(370,50,515,195);

	setfont(24,0,_T("楷体"));
	setcolor(LIGHTGRAY);
	outtextxy(405,215,_T("下一个"));

	setcolor(RED);
	outtextxy(405,280,_T("分数"));

	sprintf(str,"%d",score);
	outtextxy(415,310,str);

	outtextxy(405,375,_T("等级"));
	sprintf(str,"%d",rank);
	outtextxy(415,405,str);

	//操作说明
	setcolor(LIGHTBLUE);
	outtextxy(390,475,_T("操作说明"));
	outtextxy(390,500,"↑:旋转");
	outtextxy(390,525,"↓:下降");
	outtextxy(390,550,"←:左移");
	outtextxy(390,575,"→:右移");
	outtextxy(390,600,"空格:暂停");

}
//清除右上角方块(用一个又一个的小黑方块把它遮住)
void clearBlock(){
	setcolor(BLACK);
	setfont(23,0,"楷体");//高度23

	for(int i=0;i<BLOCK_HEIGHT;i++){
		for(int j=0;j<BlOCK_WIDTH;j++){
			//"■"
			int x=391+UNIT_SIZE*j;
			int y=71+UNIT_SIZE*i;
			outtextxy(x,y, "■");	
		}
	}
}




//在右上角区域绘制下一个方块
void drawBlock(int x,int y){
	setcolor(color[NextIndex]);
	setfont(23,0,"楷体");//高度23

	for(int i=0;i<BLOCK_HEIGHT;i++){
		for(int j=0;j<BlOCK_WIDTH;j++){
			//"■"
			if(block[NextIndex*4][i][j]==1){//对应哪一种方块(NextIndex*4)

			int x2=x+UNIT_SIZE*j;
			int y2=y+UNIT_SIZE*i;
			outtextxy(x2,y2, "■");	
			}
		}

	}

}

//绘制方块,特定位置特定方向的方块
void drawBlock(int x,int y,int blockIndex,block_dir_t dir){
	setcolor(color[NextIndex]);
	setfont(23,0,"楷体");//高度23,宽度自动适应
	int id=blockIndex*4+dir;
	for(int i=0;i<BLOCK_HEIGHT;i++){
		for(int j=0;j<BlOCK_WIDTH;j++){
			//"■"
			if(block[id][i][j]==1){//对应哪一种方块(NextIndex*4)
			int x2=x+UNIT_SIZE*j;
			int y2=y+UNIT_SIZE*i;
			outtextxy(x2,y2, "■");	
			}
		}
	}
}

//下降过程中的清除方块的过程,一个一个的
//清除指定位置指定方向的方块
//参数x:方块的左上角的x坐标
//参数y:方块的左上角在游戏区域内的坐标,距离游戏区域顶部的距离
void clearBlock(int x,int y,block_dir_t dir){
	setcolor(BLACK);
	int id=BlockIndex*4+dir;
	y+=START_Y ;
	for(int i=0;i<5;i++){
		for(int j=0;j<5;j++){
			if(block[id][i][j]==1){ 
				//擦除该方块的第i行第j列
				outtextxy(x+20*j,y+20*i,"■");
			}
		}
	}
}

void nextblock(){
	clearBlock();//清除

	//随机选择一种方块
	srand(time(NULL));//使用时间函数的返回值来作为时间种子
	NextIndex=rand()%BLOCK_COUNT;
	drawBlock(391,71);//画方块(根据序号)
}

//如果可以在指定位置可以向指定位置移动就返回1,否则返回0
int moveAble(int x0,int y0,move_dir_t moveDir,block_dir_t blockDir){
	//计算当前方块的左上角在30*15的游戏区域中的位置(多少行多少列)
	int x=(y0-minY)/UNIT_SIZE;//一个方块表示20像素
	int y=(x0-minX)/UNIT_SIZE;
	int id=BlockIndex*4+blockDir;//哪个方块哪种状态
	int ret=1;
	if(moveDir==MOVE_DOWN){
		for(int i=0;i<5;i++){
			for(int j=0;j<5;j++){
				if(block[id][i][j]==1&&
					(x+i+1>=30||visit[x+i+1][y+j]==1)){
					ret=0;

				}
			}
		}
	}else if(moveDir==MOVE_LEFT){
		for(int i=0;i<5;i++){
			for(int j=0;j<5;j++){
				if(block[id][i][j]==1&&
					(y+j==0||visit[x+i][y+j-1]==1)){
					ret=0;		
				}
			}
		}
	}else if(moveDir==MOVE_RIGHT){
		for(int i=0;i<5;i++){
			for(int j=0;j<5;j++){
				if(block[id][i][j]==1&&
					(y+j+1==15||visit[x+i][y+j+1]==1)){
					ret=0;		
				}
			}
		}
	}

	return ret;
}

//检测函数是否结束
void failCheck(){
	//刚下来的时候都是向上的
	if(!moveAble(START_X,START_Y,MOVE_DOWN,BLOCK_UP)){
	setcolor(WHITE);
	setfont(45,0,"隶体");
	outtextxy(75,300,"GAME OVER!");
	Sleep(1000);
	system("pause");
	closegraph();
	exit(0);
	}
}

//等待函数,这样比sleep函数好的地方是不会卡顿
void wait(int interval){
	int count=interval/10;
	for(int i=0;i<count;i++){
		Sleep(10);
		if(kbhit()){
		return;
		}
	}
}

//很多时候可以利用反向思维去完成函数
//判断当前方块能否转向到指定方向
//注意此时还没开始转
int rotatable(int x,int y,block_dir_t Dir){
	int id=BlockIndex*4+Dir;
	int xIndex=(y-minY)/20;
	int yIndex=(x-minX)/20;
	if(!moveAble(x,y,MOVE_DOWN,Dir)){
		return 0;
	}
	
	for(int i=0;i<5;i++){
		for(int j=0;j<5;j++){
			if(block[id][i][j]==1&&(yIndex+j<0||yIndex+j>=15||visit[xIndex+i][yIndex+j]==1)){
				return 0;
			}
		}
	}
	return 1;
}

//方块固定函数
void mark(int x,int y,int blockIndex,block_dir_t dir){
	int id=blockIndex*4+dir;
	int x2=(y-minX)/20;
	int y2=(x-minY)/20;
	for(int i=0;i<5;i++){
		for(int j=0;j<5;j++){
			if(block[id][i][j]==1){
				visit[x2+i][y2+j]=1;
				markcolor[x2+i][y2+j]=color[blockIndex];
			}
		}
	}

}
//进行上下左右的移动
void move(){
	int x=START_X;
	int y=START_Y;
	int k=0;//偏移量
	block_dir_t blockDir=BLOCK_UP;
	int cur_speed=speed;
	//先检测游戏是否失败,再确认下降
	failCheck();
	//持续下降
	while(1){
		if(kbhit()){
			int key=getch();//和kbhit()组合使用
			if(key==KEY_SPACE){//碰到空格
				getch();//实现暂停
			}
		}
		//清除当前方块
		clearBlock(x,k,blockDir);//清除特定位置特定朝向的方块
		
		if(kbhit()){
			int key=getch();
			if(key==KEY_UP){
		   /********************
		    *变形
			*要求:要判断能不能翻转-》定义函数
			********************/
			block_dir_t nextDir=(block_dir_t)((blockDir+1)%4);
			if(rotatable(x,y+k,nextDir)){
				blockDir=nextDir;
			}
			}else if(key==KEY_DOWN){
			cur_speed=50;
			}else if(key==KEY_LEFT){
				if(moveAble(x,y+k+20,MOVE_LEFT,blockDir)){
					x-=UNIT_SIZE;
				}
			}else if(key==KEY_RIGHT){
				if(moveAble(x,y+k+20,MOVE_RIGHT,blockDir)){
					x+=UNIT_SIZE;
				}
			}
		}
		k+=20;
		//绘制当前方块
		drawBlock(x,y+k,BlockIndex,blockDir);
		wait(cur_speed);//利用sleep函数不太灵活容易出现卡顿所以就自己设计一个函数
		
		//方块的“固化”处理
		if(!moveAble(x,y+k,MOVE_DOWN,blockDir)){
			mark(x,y+k,BlockIndex,blockDir);//固定函数
			break;
		}
	}

}


void newblock(){
	//确定即将使用的方块的类别
	BlockIndex=NextIndex;

	//绘制刚从顶部下降的方块
	drawBlock(START_X,START_Y);

	//让新出现的方块停顿一会儿,让用户识别到
	Sleep(100);//0.1秒

	//右上角区域绘制下一个方块
	nextblock();

	//方块降落
	move();
}

void down(int x){
	for(int i=x;i>0;i--){
		//清除第i行,第j列的方块消失
		for(int j=0;j<15;j++){
		if(visit[i-1][j]){
			visit[i][j]=1;
			markcolor[i][j]=markcolor[i-1][j];
			setcolor(markcolor[i][j]);
			outtextxy(20*j+minX,20*i+minY,"■");
			}else{
				visit[i][j]=0;
				setcolor(BLACK);
				outtextxy(20*j+minX,20*i+minY,"■");
			}
		}
	
	}
	//清除最上面行
	setcolor(BLACK);
	for(int j=0;j<15;j++){
		visit[0][j]=0;
		outtextxy(20*j+minX,minY,"■");
	}
}

//更新分数
void addScore(int lines){
	char str[10];
	setcolor(RED);
	score+=lines*10;
	sprintf(str,"%d",score);
	outtextxy(415,310,str);
}

//更新等级
void updateGrade(){
	//更新等级提示
	//50分隔一级
	char str[16];
	rank=score/50+1;
	sprintf(str,"%d",rank);
	outtextxy(425,405,str);
	//更新速度,自己可调整
	speed=500-rank*100;
	if(speed<=100){
		speed=100;
	}

}

//消行处理
void check(){
	int i;
	int j;
	int clearLines=0;
	for(i=29;i>=0;i--){
		//利用visit函数可以提高效率
		for(j=0;j<15&&visit[i][j];j++);
			//执行到此处时有两种情况
			//1.i行没满,此时j<15
			//2.i行满了,此时j>=15
			if(j>=15){
				//此时第i行已经满了
				//此时来消除第i行
				down(i);
				i++;//可能那一行可能还是可以消的所以再检测一遍
				clearLines++;
			}
		}

	//更新分数
	addScore(clearLines);
	//更新等级
	updateGrade();
}

主方法:

int main(){
 	welcome();
	initGameScene();
	//产生新方块
	nextblock();
	Sleep(500);

	//初始化访问数组
	memset(visit,0,sizeof(visit));//从地址开始清为零大小为整个visit的数组大小
	
	while(1){
		newblock();
	//消除满行,并更新分数和等级
		check();
	}

	system("pause");
	closegraph();
	return 0;
}
  • 131
    点赞
  • 701
    收藏
    觉得还不错? 一键收藏
  • 51
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值