7.C++实现2048小游戏

一、前言

本文主要介绍如何使用C/C++实现一个控制台版本的2048小游戏。

如果你想要实现带图形界面的,可以参考本站:推箱子小游戏,使用EGE图形库,贴图片即可

当然,网上已经有了很多现成的代码,但新手应该也很难看懂他们的代码逻辑

但我会详尽介绍每一步的来由,力争让每个人都能轻易看懂。

如果你还没有玩过这个游戏,那建议你先通过下面这个网站在线玩玩看:2048

思考一下它的实现原理是什么?

二、思考游戏逻辑

玩了这个游戏之后,你应该能明显感受到它本质就是一个二维数组

  • 最开始随机在两个位置生成两个数字
  • 然后通过响应用户的按键信息,做出4个方向相同数字合并的动作,并再随机产生一个数字
  • 最后再将处理好的游戏数据显示出来即可

首先是二维数组,这个很简单:

int map[4][4]{}; //存储游戏数据

注意最后的{},意思是将这个二维数组清零。

然后是生成随机数。

在程序中生成一个完全随机的随机数是一个非常困难的问题,因为程序本质就是计算,或者抽象成一个函数。

也就是说,一个函数在没有任何输入的情况下,基本不可能生成每次都不同的结果。

明白了这个道理,我们就可以来看看C++中如何生成随机数

我们产生随机数的函数为:rand()

它会直接返回一个0RAND_MAX之间的整数值,而RAND_MAX这个宏在我这里定义的值是32767

但正如前面所说,一个函数如果没有任何输入,那么它输出的值就不可能完全随机

比如我这里直接运行4次下面这段代码:

cout << rand() << endl;
cout << rand() << endl;
cout << rand() << endl;
cout << rand() << endl;

每次运行的结果都是固定的值:

41
18467
6334
26500

所以我们需要给它设定一个数值,在这里叫做初始化种子

即这个函数会通过我们这个初始化数值来随机出后面的数值,只要我们每次传入的种子值不同,那么其随机出的数值就不会相同

为了保证每次的种子都设置的不一样,一般我们就用时间数值来作为种子

//设置一个随机数种子
srand(time(NULL));
  • srand函数就是用来设置种子的,它唯一一个参数就是这个种子的数值
  • 而这个数值我们通过time函数来获取,给它传入NULL,就会返回当前的时间数值

这就能够保证每次的种子都不同,那么计算出来的随机数也就不会相同了

至此我们就完成了基本的知识点讲解,下面正式开始写代码

三、游戏主循环

所谓游戏,便是一个不断循环的程序而已

所以第一步我们就得写好我们的游戏主循环

首先是头文件、枚举变量以及地图数据的全局定义:

#include <iostream> //主要输入输出头文件,还包括常用c函数,比如这里用到的rand,srand函数等等
#include<conio.h> //获取用户输入键值函数_getchar函数的头文件
#include <ctime> //time这个函数头文件
using namespace std;

int map[4][4]{}; //存储游戏数据

//上下左右
enum {
	UP, //上
	DOWN, //下
	LEFT, //左
	RIGHT //右
};
//游戏所处的状态
enum {
	GAME_OVER, //游戏结束
	GAME_WIN, //游戏获胜
	GAME_CONTINUE //游戏继续
};

代码我注释的非常详细,关于枚举更加详细的介绍,可以参照本站的另一篇文章:枚举与枚举量

接下来就是我们需要写的函数:

//根据数据打印游戏界面
void Show();
//在地图中随机产生一个随机数
bool CreateNumber();
//根据用户输入的键值进行移动数据
void Move(int direction);
//处理输入输出,返回上下左右
int Input();
//判断游戏状态
int IsWin();

那么我们如何知道要写上面这些函数呢?那当然是从main函数开始思考游戏逻辑

main函数先写好:

int main()
{
	//设置一个随机数种子
	srand(time(NULL));

	//初始化,先随机产生两个随机数
	CreateNumber();
	CreateNumber();

	//显示地图
	Show();
	//进入游戏循环
	while (true)
	{
	//用户输入,然后游戏
	}
}
  • 这里首先设置好随机数的种子srand(time(NULL));
  • 然后2048这个游戏需要在最开始生成两个随机数,所以我们就需要写一个生成随机数的函数CreateNumber,并调用两次
  • 然后就可以显示游戏界面了,所以我们得写一个显示界面的函数Show
  • 最后就正式进入游戏循环

然后接下来我们再思考进入游戏循环后,应该做些什么

	//进入游戏循环
	while (true)
	{
		//获取用户输入的移动方向
		int direction = Input();
		//移动数据
		Move(direction); 
		//判断目前游戏是否已经赢了或结束了
		int gameState = IsWin();

		if (gameState == GAME_CONTINUE) //游戏继续
		{
			CreateNumber(); //在地图中产生一个随机数
			Show(); //然后刷新游戏界面
		}
		else if (gameState == GAME_WIN) //游戏已经胜利
		{
			Show(); //显示数据
			cout << "You Win!" << endl;
			break;
		}
		else if (gameState == GAME_OVER) //游戏结束
		{
			Show();
			cout << "You lose!" << endl;
			break;
		}
	}
  • 首先我们应该等待用户的输入,即用户按了哪个方向的键,所以我们需要一个函数Input等待用户输入,并返回方向

  • 得到用户输入的方向后,我们就可以根据这个方向来移动游戏数据了,所以就得写一个根据方向来移动数据的函数Move(int direction)

  • 移动完成后,我们就得判断当前游戏的状态,是赢了,输了还是进行下一次循环?所以就有了函数IsWin,用它来获取当前游戏的状态并返回

  • 最后我们根据这个返回的状态,来判断我们需要做哪些操作

    • 如果游戏继续,那么就在地图上随机产生一个随机数,然后进入下一次循环
    • 如果赢了或输了,那就根据情况输出相应的提示信息即可

至此,我们就思考好了游戏的整体逻辑,那么下一步就是写好各个函数的具体内容即可

四、函数详解

1.CreateNumber

首先是在地图上产生随机数的函数:

bool CreateNumber()
{

	bool f = false; //记录当前地图中是否存在空位置,假设没有
	for (int i = 0; i < 4; i++) {
		for (int j = 0; j < 4; j++)
		{
			if (map[i][j] == 0) {
				f = true; //有空位置,则置true
			}
		}
	}
	if (!f) return false; //没有空位置,则不产生随机数,直接返回

	int x = -1;
	int y = -1;
	//三分之二的概率生成2,三分之一的概率生成4
	int whitch = rand() % 3 ? 2 : 4;

	//循环找到一个随机坐标,放入随机数
	do
	{
		x = rand() % 4; //随机x坐标
		y = rand() % 4; //随机y坐标
	} while (map[x][y] != 0);


	map[x][y] = whitch;
	return true;
}

这个函数的主要逻辑就是:

  • 先判断当前地图有没有空位置,如果没有,则直接返回false
  • 先产生一个随机数,2/3的概率为21/3的概率为4
  • 然后再在这个地图数据(二维数组)中随机找到一个空位置(即为0)
  • 让这个空位置等于上面产生的随机数,最后返回即可

这里讲解一下三元运算符的用法:

ret= bool ? res1 : res2的意思是,如果booltrue,即不为0,那么ret就等于res1,否则ret就等于res2

懂了这个语句,我们再来看这里就会简单很多

  • rand() % 3代表产生一个随机数然后对3取模,那么结果只能为0 ,1 ,2
  • 0代表false12就代表true,这就恰好是1/32/3的关系,所以2写在了:前,即为1,2的时候,就取2,为0的时候就取4

然后接下来再来看如何产生一个随机坐标的

因为我们这里本质就是一个二维数组,且下标只能取0,1,2,3 ,所以就可以通过产生一个随机数,对4取模,就能得到0,1,2,3中的随机值

	do
	{
		x = rand() % 4; //随机x坐标
		y = rand() % 4; //随机y坐标
	} while (map[x][y] != 0);

这里写入do while语句的目的就是,为了确保取得的这个坐标为空,如果不为空,那就重新随机,直到取得一个为空的坐标

2.Show

创建完两个随机数,我们就得显示界面了

void Show()
{
	system("cls"); //清空屏幕
	cout << "*****************  2048 控 制 台 版  ******************" << endl;
	cout << "*****************  作者:cadn 余识 ******************" << endl << endl;

	//双循环按map变量中的数据生成地图
	for (int i = 0; i < 4; ++i)
	{
		cout << "---------------------------------" << endl; //上下行数据的分割线
		for (int j = 0; j < 4; ++j)  //遍历显示数据
		{
			if (map[i][j] == 0) //等于0说明为空
			{
				cout << "|   \t";
			}
			else //不等于0则输出数据
			{
				cout << "|   " << map[i][j] << "\t";
			}
		}
		cout << "|" << endl;
	}
	cout << "---------------------------------" << endl; //最底下的横线
}

由于这个游戏界面就是许多横竖线组成的框,而控制台想要在指定位置输出字符并不太好控制

所以这里采取的就是直接一行一行的输出

首先我们需要清空屏幕:

system("cls"); //清空屏幕

clswindows 批处理指令,它应该是clear screen的缩写,system就是c提供给我们在代码中执行批处理指令的函数, 它的作用就是清空屏幕

然后下面就是一个双循环,用于输出游戏界面,输出后长下面这样:

在这里插入图片描述

这是比较基础的东西了,就是循环输出而已,只是有些繁琐,需要注意输出的各种格式

这里就不再过多讲解了

3.Input

然后就是获取用户的输入了!

int Input()
{
	while (true) {
		char c = _getch();
		switch (c) {
		case 'w':
		case 'W':
		case 72: //测试得到的上箭头对应的值
			return UP;
		case 's':
		case 'S':
		case 80: //下箭头对应的键码值
			return DOWN;
		case 'a':
		case 'A':
		case 75: //左箭头对应的键码值
			return LEFT;
		case 'd':
		case 'D':
		case 77: //右箭头对应的键码值
			return RIGHT;
		default:
			break; //不是指定的键,继续下一次循环
		}

	}
}

这里主要用到的函数是_getch,这个函数的作用是直接接收一个用户输入的字符就返回,不会显示在屏幕上,也不需要你按enter

根据一般习惯,w s a d这四个键一般用于上下左右,所以上面就通过多个case语句,进行判断返回

但也有人更习惯于键盘上的上下左右箭头,所以就可以自己测试一下当你按键盘上的下上下左右箭头时,这个函数返回的是什么值,上面的各个数值就是我测试出来的

如果用户输入的其它值,就不返回,进入下一次循环,否则直接返回对应的方向

4.Move

然后是移动数据,这个函数是我们游戏的核心,内容也稍微多一些:

void Move(int direction)
{
	switch (direction)
	{
	case UP:
		//最上面一行不动
		for (int row = 1; row < 4; row++)
		{
			for (int crow = row; crow >= 1; crow--)
			{
				for (int col = 0; col < 4; col++)
				{
					//上一个格子为空
					if (map[crow - 1][col] == 0)
					{
						map[crow - 1][col] = map[crow][col];
						map[crow][col] = 0;
					}
					else
					{
						//合并
						if (map[crow - 1][col] == map[crow][col])
						{
							map[crow - 1][col] *= 2;
							map[crow][col] = 0;
						}

					}
				}
			}
		}
		break;
	case DOWN:
		//最下面一行不动
		for (int row = 4 - 2; row >= 0; --row)
		{
			for (int crow = row; crow < 4 - 1; ++crow)
			{
				for (int col = 0; col < 4; ++col)
				{
					//上一个格子为空
					if (map[crow + 1][col] == 0)
					{
						map[crow + 1][col] = map[crow][col];
						map[crow][col] = 0;
					}
					else
					{
						//合并
						if (map[crow + 1][col] == map[crow][col])
						{
							map[crow + 1][col] *= 2;
							map[crow][col] = 0;
						}

					}
				}
			}
		}
		break;
	case LEFT:
		//最左边一列不动
		for (int col = 1; col < 4; ++col)
		{
			for (int ccol = col; ccol >= 1; --ccol)
			{
				for (int row = 0; row < 4; ++row)
				{
					//上一个格子为空
					if (map[row][ccol - 1] == 0)
					{
						map[row][ccol - 1] = map[row][ccol];
						map[row][ccol] = 0;
					}
					else
					{
						//合并
						if (map[row][ccol - 1] == map[row][ccol])
						{
							map[row][ccol - 1] *= 2;
							map[row][ccol] = 0;
						}

					}
				}
			}
		}
		break;
	case RIGHT:
		//最右边一列不动
		for (int col = 4 - 2; col >= 0; --col)
		{
			for (int ccol = col; ccol <= 4 - 2; ++ccol)
			{
				for (int row = 0; row < 4; ++row)
				{
					//上一个格子为空
					if (map[row][ccol + 1] == 0)
					{
						map[row][ccol + 1] = map[row][ccol];
						map[row][ccol] = 0;
					}
					else
					{
						//合并
						if (map[row][ccol + 1] == map[row][ccol])
						{
							map[row][ccol + 1] *= 2;
							map[row][ccol] = 0;
						}

					}
				}
			}
		}
		break;
	}
}

它的主体逻辑就是通过具体的方向,执行对应的数据合并移动等操作

四个方向的逻辑是相同的,所以我这里就只讲解其中一个方向的逻辑,其它的类似即可

比如当用户按了向上的键

那么第一行的内容就不用动了,我们从第二行开始,即从下标1开始循环

		//最上面一行不动
		for (int row = 1; row < 4; row++)
		{
			//遍历
		}

我们要做的就是从第二行开始,遍历查找同列的前一行,是空位置,还是相同的数值,或是不同的数值

每一行,我们都需要将当前行以前的所有行进行遍历,并向前合并移动,考虑下面这个例子:

初始列 第一次向上移动 第二次向上移动
0 1 1
1 0 1
1 1 0

上面只是单纯的后面一行往前一行进行合并,但明显不合情理,第二次移动后,最前面两个1还应该合并一下,所以我们我们在代码中就需要向前遍历循环,来解决这种情况

所以里面还有两个循环:

			for (int crow = row; crow >= 1; crow--)
			{
				for (int col = 0; col < 4; col++)
				{
					//上一个格子为空
					if (map[crow - 1][col] == 0)
					{
						map[crow - 1][col] = map[crow][col];
						map[crow][col] = 0;
					}
					else
					{
						//合并
						if (map[crow - 1][col] == map[crow][col])
						{
							map[crow - 1][col] *= 2;
							map[crow][col] = 0;
						}

					}
				}
			}

外层循环就是从当前行向前面所有行进行遍历,并向前合并

内层就是遍历该行所有的列,然后进行判断,如果与前面一行相同,则合并,为空,则向前移动,否则不处理

5.IsWin

然后我们还需要判断当前的游戏状态:

int IsWin()
{
	//出现2048则赢得游戏
	for (int i = 0; i < 4; ++i)
	{
		for (int j = 0; j < 4; ++j)
		{
			if (map[i][j] == 2048)
			{
				return GAME_WIN;
				break;
			}
		}
	}

	//判断是否游戏已经结束
	

	//横向检查
	for (int i = 0; i < 4; ++i)
	{
		for (int j = 0; j < 4 - 1; ++j)
		{
			if (!map[i][j] || (map[i][j] == map[i][j + 1])) //判断是否有空格,或者有两个相邻元素等值,有着直接返回,游戏继续
			{
				return GAME_CONTINUE;
				break;
			}
		}
	}
	//纵向检查
	for (int j = 0; j < 4; ++j)
	{
		for (int i = 0; i < 4 - 1; ++i)
		{
			if (!map[i][j] || (map[i][j] == map[i + 1][j]))//判断是否有空格,或者有两个相邻元素等值,有着直接返回,游戏继续
			{
				return GAME_CONTINUE;
				break;
			}
		}
	}

	//如果运行到这里,说明不符合上述两种状况,游戏结束
	return GAME_OVER;

}

判断方法也很简单,就是进行全遍历

  • 如果出现2048则判为胜利,直接return GAME_WIN
  • 如果没有胜利,那就判断是否存在空格,以及相邻两个数字相等的清空,有者直接return GAME_CONTINUE,游戏继续
  • 如果上面两种情况都不存在,那就是游戏结束了,直接return GAME_OVER;

三、完整代码

#include <iostream> //主要输入输出头文件,还包括常用c函数,比如这里用到的rand,srand函数等等
#include<conio.h> //获取用户输入键值函数_getchar函数的头文件
#include <ctime> //time这个函数头文件
using namespace std;

int map[4][4]{}; //存储游戏数据

//上下左右
enum {
	UP, //上
	DOWN, //下
	LEFT, //左
	RIGHT //右
};
//游戏所处的状态
enum {
	GAME_OVER, //游戏结束
	GAME_WIN, //游戏获胜
	GAME_CONTINUE //游戏继续
};
//根据数据打印游戏界面
void Show();
//在地图中随机产生一个随机数
bool CreateNumber();
//根据用户输入的键值进行移动数据
void Move(int direction);
//处理输入输出,返回上下左右
int Input();
//判断游戏状态
int IsWin();

int main()
{
	//设置一个随机数种子
	srand(time(NULL));

	//初始化,先随机产生两个随机数
	CreateNumber();
	CreateNumber();

	//显示地图
	Show();

	//进入游戏循环
	while (true)
	{
		//获取用户输入的移动方向
		int direction = Input();
		//移动数据
		Move(direction); 
		//判断目前游戏是否已经赢了或结束了
		int gameState = IsWin();

		if (gameState == GAME_CONTINUE) //游戏继续
		{
			CreateNumber(); //在地图中产生一个随机数
			Show(); //然后刷新游戏界面
		}
		else if (gameState == GAME_WIN) //游戏已经胜利
		{
			Show(); //显示数据
			cout << "You Win!" << endl;
			break;
		}
		else if (gameState == GAME_OVER) //游戏结束
		{
			Show();
			cout << "You lose!" << endl;
			break;
		}
	}
}



void Show()
{
	system("cls"); //清空屏幕
	cout << "*****************  2048 控 制 台 版  ******************" << endl;
	cout << "*****************  作者:cadn 余识 ******************" << endl << endl;

	//双循环按map变量中的数据生成地图
	for (int i = 0; i < 4; ++i)
	{
		cout << "---------------------------------" << endl; //上下行数据的分割线
		for (int j = 0; j < 4; ++j)  //遍历显示数据
		{
			if (map[i][j] == 0) //等于0说明为空
			{
				cout << "|   \t";
			}
			else //不等于0则输出数据
			{
				cout << "|   " << map[i][j] << "\t";
			}
		}
		cout << "|" << endl;
	}
	cout << "---------------------------------" << endl; //最底下的横线
}

bool CreateNumber()
{
	bool f = false; //记录当前地图中是否存在空位置,假设没有
	for (int i = 0; i < 4; i++) {
		for (int j = 0; j < 4; j++)
		{
			if (map[i][j] == 0) {
				f = true; //有空位置,则置true
			}
		}
	}
	if (!f) return false; //没有空位置,则不产生随机数,直接返回


	int x = -1;
	int y = -1;
	//三分之二的概率生成2,三分之一的概率生成4
	int whitch = rand() % 3 ? 2 : 4;

	//循环找到一个随机坐标,放入随机数
	do
	{
		x = rand() % 4; //随机x坐标
		y = rand() % 4; //随机y坐标
	} while (map[x][y] != 0);


	map[x][y] = whitch;
	return true;
}

int IsWin()
{
	//出现2048则赢得游戏
	for (int i = 0; i < 4; ++i)
	{
		for (int j = 0; j < 4; ++j)
		{
			if (map[i][j] == 2048)
			{
				return GAME_WIN;
				break;
			}
		}
	}

	//判断是否游戏已经结束
	

	//横向检查
	for (int i = 0; i < 4; ++i)
	{
		for (int j = 0; j < 4 - 1; ++j)
		{
			if (!map[i][j] || (map[i][j] == map[i][j + 1])) //判断是否有空格,或者有两个相邻元素等值,有着直接返回,游戏继续
			{
				return GAME_CONTINUE;
				break;
			}
		}
	}
	//纵向检查
	for (int j = 0; j < 4; ++j)
	{
		for (int i = 0; i < 4 - 1; ++i)
		{
			if (!map[i][j] || (map[i][j] == map[i + 1][j]))//判断是否有空格,或者有两个相邻元素等值,有着直接返回,游戏继续
			{
				return GAME_CONTINUE;
				break;
			}
		}
	}

	//如果运行到这里,说明不符合上述两种状况,游戏结束
	return GAME_OVER;

}

int Input()
{
	while (true) {
		char c = _getch();
		switch (c) {
		case 'w':
		case 'W':
		case 72: //测试得到的上箭头对应的值
			return UP;
		case 's':
		case 'S':
		case 80: //下箭头对应的键码值
			return DOWN;
		case 'a':
		case 'A':
		case 75: //左箭头对应的键码值
			return LEFT;
		case 'd':
		case 'D':
		case 77: //右箭头对应的键码值
			return RIGHT;
		default:
			break; //不是指定的键,继续下一次循环
		}

	}
}
void Move(int direction)
{
	switch (direction)
	{
	case UP:
		//最上面一行不动
		for (int row = 1; row < 4; row++)
		{
			for (int crow = row; crow >= 1; crow--)
			{
				for (int col = 0; col < 4; col++)
				{
					//上一个格子为空
					if (map[crow - 1][col] == 0)
					{
						map[crow - 1][col] = map[crow][col];
						map[crow][col] = 0;
					}
					else
					{
						//合并
						if (map[crow - 1][col] == map[crow][col])
						{
							map[crow - 1][col] *= 2;
							map[crow][col] = 0;
						}

					}
				}
			}
		}
		break;
	case DOWN:
		//最下面一行不动
		for (int row = 4 - 2; row >= 0; --row)
		{
			for (int crow = row; crow < 4 - 1; ++crow)
			{
				for (int col = 0; col < 4; ++col)
				{
					//上一个格子为空
					if (map[crow + 1][col] == 0)
					{
						map[crow + 1][col] = map[crow][col];
						map[crow][col] = 0;
					}
					else
					{
						//合并
						if (map[crow + 1][col] == map[crow][col])
						{
							map[crow + 1][col] *= 2;
							map[crow][col] = 0;
						}

					}
				}
			}
		}
		break;
	case LEFT:
		//最左边一列不动
		for (int col = 1; col < 4; ++col)
		{
			for (int ccol = col; ccol >= 1; --ccol)
			{
				for (int row = 0; row < 4; ++row)
				{
					//上一个格子为空
					if (map[row][ccol - 1] == 0)
					{
						map[row][ccol - 1] = map[row][ccol];
						map[row][ccol] = 0;
					}
					else
					{
						//合并
						if (map[row][ccol - 1] == map[row][ccol])
						{
							map[row][ccol - 1] *= 2;
							map[row][ccol] = 0;
						}

					}
				}
			}
		}
		break;
	case RIGHT:
		//最右边一列不动
		for (int col = 4 - 2; col >= 0; --col)
		{
			for (int ccol = col; ccol <= 4 - 2; ++ccol)
			{
				for (int row = 0; row < 4; ++row)
				{
					//上一个格子为空
					if (map[row][ccol + 1] == 0)
					{
						map[row][ccol + 1] = map[row][ccol];
						map[row][ccol] = 0;
					}
					else
					{
						//合并
						if (map[row][ccol + 1] == map[row][ccol])
						{
							map[row][ccol + 1] *= 2;
							map[row][ccol] = 0;
						}

					}
				}
			}
		}
		break;
	}
}
作者:余识
全部文章:0
会员文章:0
总阅读量:0
c/c++pythonrustJavaScriptwindowslinux