C语言简易小游戏——2048

一.概述

       2048是一项休闲益智类小游戏,游戏在一个4x4的方格上进行,在这里面会不断产生新方块,方块的初始值为2,玩家通过合并,消除方块使得方块的数值越来越大,最终合成2048的方块来获取胜利,游戏的核心功能逻辑简单易实现,通过C语言的函数功能,同时配合easyx图形库美化游戏界面,一个简单的2048小游戏就可以实现。

二.各部分的功能实现

         对几个主要功能模块进行分析,分为六个模块讲解:

1.随机数产生函数

        用于产生整数2或4,这里设置产生2的概率为90%,产生4的概率为10%

void createNum()
{
	while (1)
	{
		int ret = 0;
		int x = rand() % MAX_SIZE;   //创建一个随机数对MAX_SIZE 取余结果坐标为0,1,2,3
		int y = rand() % MAX_SIZE;   //创建一个随机数对MAX_SIZE 取余结果坐标为0,1,2,3
		if (map[x][y] == 0) {     //查看随机的坐标是否等于0
			if (rand() % 10 == 1)    //判断随机数取10余数是否为1,来限制4出现的几率为10%
			{
				map[x][y] = 4;
			}
			else
			{
				map[x][y] = 2;    //否则2出现的几率为90%  
			}
			break;
		}
	}
}

2.游戏初始化函数

     初始化游戏,重置游戏方格区域的数据,并在两个随机位置产生2或4,概率同上,由此不难发现,该函数只需要在充值方格数据和生成两个随机位置后,调用以上随机数产生函数即可。

void gameInit()
{
	for (int i = 0; i < MAX_SIZE; i++) {
		for (int j = 0; j < MAX_SIZE; j++) {
			map[i][j] = 0; //遍历所有方格并初始化为0
		}
	}
	score = 0; //得分初始化为0
	//设置随机数种子
	srand(GetTickCount());
	createNum();
	createNum();
}

3.游戏界面函数(核心)

由于应用了easyx图形库,因此该函数的主要功能就说使用easy图形库的各种命令来优化图形界面,例如打印界面,设置各个区域以及不同数值方块相对应的颜色,打印得分区域和新游戏按钮等。

void gameView()
{
	//打印方格、设置颜色、打印方格值
	for (int i = 0; i < MAX_SIZE; i++) {      //遍历所以格子起点
		for (int j = 0; j < MAX_SIZE; j++) {     //遍历所以格子起点
			int x = j * GRID_W + (j + 1) * INTERVAL;    //起点X坐标
			int y = i * GRID_W + (i + 1) * INTERVAL;    //起点y坐标 
			int index = (int)log2((double)map[i][j]);
			if (map[i][j] == 0) {         //判断当前格子的数字是否为0
				COLORREF gcolor = arr[0];    //若为0赋值数组中颜色
				setfillcolor(gcolor); //设置空方格时的颜色
				solidroundrect(x, y, x + GRID_W, y + GRID_W, 5, 5);  //创建空方格
			}
			else { 	               //除方格值为0时,其余情况下打印值
				COLORREF gcolor = arr[index];
				setfillcolor(gcolor); //设置不同方格值时的颜色
				solidroundrect(x, y, x + GRID_W, y + GRID_W, 5, 5);  //创建不同颜色的方格
				settextstyle(50, 0, "Microsoft YaHei UI", 0, 0, 1000, 0, 0, 0);   //更改文字颜色大小
				if (map[i][j] < 8)  settextcolor(RGB(119, 110, 101));    //设置16以下文字的颜色
				else  settextcolor(WHITE);                             //16以上文字的颜色
				char snum[10] = "";   //创建一维数组
				sprintf(snum, "%d", map[i][j]);   //将数字转化成字符串
				int ntw = textwidth(snum);    //求输出的字的宽
				int nth = textheight(snum);   //求输出的字的高
				outtextxy(x + (100 - ntw) / 2, y + (100 - nth) / 2, snum);   //将文字输出
				//绘制方格值     
			}
		}
	}
	//将得分转化为字符串打印在得分板上
	char sscore[10];             //创建数组
	sprintf(sscore, "%d", score);   //将数字转化成字符串
	int stw = textwidth(sscore);
	int sth = textheight(sscore);//数字的宽
	settextstyle(30, 0, "Microsoft YaHei UI", 0, 0, 1000, 0, 0, 0);  //设置分数的大小的字体
	settextcolor(WHITE);    //分数的颜色
	setbkmode(TRANSPARENT);    //透明背景
	//将分数打印至得分板区域
	RECT r = { 530, 65, 670, 100 };
	HRGN rgn = CreateRectRgn(530, 65, 670, 100); 
	setcliprgn(rgn); //设置裁剪区
	clearcliprgn(); //每轮结束后,清空裁减区,重新打印分数(避免分数打印堆积)
	setcliprgn(NULL);
	drawtext(sscore, &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE); //在对应区域打印分数
}

4.移动函数(核心)

游戏的核心逻辑部分,接收到玩家的键盘指令后,需要做出相对应的移动逻辑,并且根据方格区域不同的情况做出不同的反应,例如所要移动的方向上无相同值方块可合并所有方块都到达边界无法前进,此时做出的回应是不执行任何命令,等待下一个键盘指令。而前方若有合并方块,则需要改变前方可合并方块的数值,使其翻倍,并使得距离所要移动方向更远的方块消失,而若方块朝向所要移动的方向上无其他方块,则需使其向着该方向移动至边界,而若同一行内有三或四个相同数值的方块,则需要先从靠近所移动方向的一侧开始遍历合并,并且每个方块每回合只能合并一次,合并后还需要继续前进直到遇到其他方块或边界,为此,将移动部分拆分为四个函数,根据不同情况产生不同的指令,其根本逻辑都是一样的,只是跟据移动方向的不同会改变遍历的初始位置和方向,此处只展示上移函数,其余移动函数请见完整代码部分

void moveUp()
{
	bool isMoved = 0;	//是否移动成功
	for (int i = 0; i < MAX_SIZE; i++)	//遍历列
	{
		int now = 0;//标尺位,每一列遍历时都将标尺位调至第一行
		for (int next = 1; next < MAX_SIZE; next++)	//遍历行,next是now后面的数字
		{
			//判断next处是不是0,意思就是除了第一行其他位置是否有数字存在
			if (map[next][i] != 0)//next处不是0
			{
				if (map[now][i] == 0)//出现数字的前方路径上全是0
				{
					map[now][i] = map[next][i];//将第一行变成该数字,
					map[next][i] = 0;//该数字原本的位置置为0
					isMoved = 1;//成功移动数字,随机在空格位置刷新2或4
				}
				else if (map[now][i] == map[next][i])//顺序遍历,标尺位数字和出现数字相同,且中间路径上没有数字
				{
					map[now][i] *= 2;//将数字合并,标尺位数字变为原来的二倍
					score += map[now][i];//方块合成后的得分
					map[next][i] = 0;//释放原来的数字
					now++;//标尺位向后一行
					isMoved = 1;//成功移动数字,随机在空格处刷新2或4
				}
				else//标尺位和数字不相等,那就将数字移动到标尺位的后一行,如果本身就在标尺位的后一行,依然可以赋值,但不删除原位置
				{
					map[now + 1][i] = map[next][i];//将与标尺位不相同的数字移动到标尺位的后一行
					if (now + 1 != next)//如果数字就在标尺位的后一行,他俩相邻,就不用执行这一步,因为now+1和next原本就是一个位置他俩原本的数字就相等
						//如果数字和标尺位中间有空格,就要执行这一步,因为原本的now+1是0,而next原本的数字是一个不为0的数,他俩不相等,要执行这一步进行 next原本位置的删除
					{
						map[next][i] = 0;//删除
						isMoved = 1;//成功移动数字刷新2或4
					}
					now++;//标尺位向后一行
				}
			}
		}
	}
	if (isMoved)
	{
		createNum(); //判定是否产生了移动,若是,则随机空白位置生成一个整数2或4
	}
	gameView();  //打印一次游戏界面
}

5.胜负判定函数

根据当前方格区域的方块数值分布情况来确定胜利或失败,若生成了2048的方块,则游戏胜利,返回数值1;若方格填满且所有方块无法继续合并免责判定游戏失败,返回数值2;否则判定胜负未定,返回数值3;若胜负已定,则会调用胜利/失败界面函数

int isWin()
{
	int ret = 2; //定义判别变量,初始状态为失败
	for (int i = 0; i < MAX_SIZE; i++)  //循环遍历每个数字  
	{
		for (int j = 0; j < MAX_SIZE; j++)
		{
			if (map[i][j] == 2048) {
				ret = 1;
				break;
			}  //如果有数字2048,改变判别变量,判定为胜利,终中断循环
			if (map[i][j] == 0) {
				ret = 0;
				break;
			}  //如果还有空方格,说明此时仍可移动,改变判别变量,判定为胜负未定,终止循环
			if (map[i][j] == map[i][j + 1] || map[i][j] == map[i + 1][j]) {
				if (i < 3 && j < 3) {
					ret = 0;
					break;
				}
			}
			if (i == 3 && j != 3 && map[i][j] == map[i][j + 1]) {
				ret = 0;
				break;
			}
			if (j == 3 && i != 3 && map[i][j] == map[i + 1][j]) {
				ret = 0;
				break;
			}  //如果在不符合前两个if的情形下,且对任意在游戏区域内的方块,其右侧和下侧的方块至少有一个与其相等,则改变判别变量判定为胜负未定,中断循环
		}
	}
	return ret; //否则判定游戏失败
}

6.胜利/失败界面函数

根据胜负判定函数的返回值来打印游戏胜利/失败界面

void winOrLoseView(int det)
{
	//接收isWin返回的参数,若为1打印胜利字样,为2打印失败字样
	HWND window = GetHWnd();          //获取窗口句柄
	if (det == 1)     //胜利判定
	{
		MessageBox(window, "You won the game!", "提示", MB_OK);  //打印提示
		gameInit();  //新开始游戏
	}
	else if (det == 2)
	{   //失败判定
		MessageBox(window, "Game over!", "提示", MB_OK);  //打印提示
		gameInit();  //新开始游戏
	}
}

7.定义主函数

int main(void)
{
	//创建游戏窗口,尺寸为500 * 700
	initgraph(GAME_ZONE + 200, GAME_ZONE);    //创建游戏窗口
	//设置背景(方格间距)颜色  
 
	setbkmode(TRANSPARENT);      //设置窗口为透明色
	setbkcolor(INTERVRL_COLOR);   //设置背景颜色
	cleardevice();     //清除背景
	//打印游戏右侧区域并设置颜色
	setfillcolor(RIGHT_RECTANGLE);    //设置矩形填充颜色
	solidrectangle(GAME_ZONE, 0, GAME_ZONE + 200, GAME_ZONE);     //创建游戏额外区域的矩形
	//打印并设置积分板颜色,并打印对应字体
	setfillcolor(SCORE_COLOR);             //设置游戏界面右侧的颜色
	solidroundrect(530, 40, 670, 100, 5, 5); //得分板的左上和右下坐标分别为(530,40), (670,100)   创建得分板
	settextcolor(RGB(238, 228, 218));        //设置字体颜色
	settextstyle(30, 0, "Microsoft YaHei UI", 0, 0, 1000, 0, 0, 0); //设置文字大小和字体
	outtextxy(565, 40, "SCORE");      //出入文字
	//打印New Gme按钮以及字体并设置字体颜色和格式
	setfillcolor(NEWGAME_COLOR);          //设置新游戏按钮区域背景颜色
	solidroundrect(525, 160, 675, 210, 5, 5); //New Game区域的左上和右下坐标分别为(525,160), (675,210)       
	int btw = textwidth("New Game");     //文字宽距离按钮居中更多距离
	int bth = textheight("New Game");    //文字高距离按钮居中更多距离
	settextcolor(WHITE);   //设置文字颜色为白色
	settextstyle(30, 0, "Microsoft YaHei UI", 0, 0, 1000, 0, 0, 0);    //设置文字的字体和大小
	outtextxy(525 + (150 - btw) / 2, 160 + (50 - bth) / 2, "New Game");  //将文字居中放在矩形中
	gameInit();
	gameView();    //调用函数
	ExMessage m; //定义接收键鼠输入的变量
 
	//开始游戏,置于恒定循环内
	BeginBatchDraw(); //开始批量绘图,防止游戏过程中闪屏
	while (1)
	{
		gameView();
		//每轮开始时,进行一次胜负判定,若胜负未定则继续游戏,否则调用胜利/失败界面函数
		int det = isWin();
		if (det != 0) winOrLoseView(det);
		//接收键鼠输入
		m = getmessage(EX_MOUSE | EX_KEY);
		switch (m.message)
		{
		case WM_KEYDOWN: //如果键盘输入wasd或上下左右键,则做出相对应移动指令
			if (m.vkcode == VK_UP || m.vkcode == 'w' || m.vkcode == 'W') moveUp();
			if (m.vkcode == VK_DOWN || m.vkcode == 's' || m.vkcode == 'S')moveDown();
			if (m.vkcode == VK_LEFT || m.vkcode == 'a' || m.vkcode == 'A')moveLeft();
			if (m.vkcode == VK_RIGHT || m.vkcode == 'd' || m.vkcode == 'D')moveRight();
			break;
		case WM_LBUTTONDOWN: //若点击了New game按钮所在区域,则调用gameInit函数,即初始化游戏数据并打印游戏界面
			if (m.x >= 525 && m.x <= 675 && m.y >= 160 && m.y < 210) {
				gameInit();
				break;
			}
		}
		FlushBatchDraw(); //结束批量绘图
	}
 
	getchar();
	return 0;
}

三.成品图展示

     

四.总结

可以看出,整个游戏从构思倒实现还是比较简单的,总代码量也只有450行左右,只是一个功能比较简略、仅仅实现了核心功能和一点点拓展功能的版本,实际上该游戏的可扩展之处还有很多,例如音乐、主菜单、游戏设置、图形界面字体的优化、游戏的特效以及设置更多游戏模式等,这代表着实现从深度到广度的延伸,使得游戏更具有可玩性.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值