C++实现图形化2048小游戏
目录
- 一、游戏规则
- 二、步骤实现
- (一) SDL库的安装
- (二) 初始化游戏界面
- 1. 后台数字模型
- 2 显示模型
- 2.1 SDL库的使用
- 2.1.1 窗口渲染
- 2.1.2 矩形绘制
- 2.2 SDL-ttf库的使用
- 2.2.1 设置字体属性
- 2.2.2 创建纹理图层
- 2.2.3 绘制文字
- (三) 随机生成2个数字(2或4)
- (四) 捕获用户方向键操作
- 1.合并运算函数
- (五) 更新游戏界面
- (六) 判断游戏结束
- 1.判断获胜
- 2.判断失败
- 三、总结
- 四、源码下载

一、游戏规则
2048游戏开始时,棋盘内会随机出现2到3个数字,这2到3个数字通常是2或4。玩家可以通过方向键来控制棋盘上的数字方块向该方向移动,每次相同的数字方块会进行合并,新的数值为原来2个方块数值之和。游戏的目标是得出最高数2048这个数,一旦所有格子都被填满且未达到2048这个目标,游戏就失败了。
游戏的设计思路:
- 初始化游戏界面
- 随机生成2个数字(2或4)
- 捕获用户方向键操作
- 更新游戏界面
- 判断游戏结束
二、步骤实现
(一) SDL库的安装
需要SDL库、SDL-ttf库,在linux系统中安装命令apt install libsdl2-dev
、apt install libsdl2-ttf-dev
即可安装。
(二) 初始化游戏界面
SDL库使用的顺序是生成窗口 → \to →生成渲染器 → \to →生成纹理 → \to →纹理拷贝至渲染器 → \to →显示。
SDL-ttf库使用的顺序是打开字库 → \to →设置字体属性 → \to →创建表面 → \to →创建纹理。
我希望游戏的格子是4X4的,金黄和橙色相间,而格子里的不同数字也使用不同的颜色渲染(初步设计成3色)。
知识点:类、类外函数定义。
class Chess2048 {private:int gameBoard[16] = {}; //4X4棋盘格SDL_Renderer * render; // 内置窗口渲染器指针TTF_Font *font; //内置字体指针TextSize textSize[11]; //2、4、8...2048的字体尺寸SDL_Texture *texture[11]; //2、4、8...2048的字体纹理指针SDL_Color bgColor[2] = {{255,255,0,255}, {255,180,0,255}}; //金黄、橙SDL_Color fgColor[3] = {{25, 160, 0, 255}, {85, 200, 200, 255}, {55, 0, 100, 255}}; //绿、天蓝、紫public:Chess2048(SDL_Renderer * , TTF_Font *); //构造~Chess2048(); //析构//棋盘移动操作bool up();bool down();bool left();bool right();//判断是否满格bool full();//判断是否胜利bool win();//显示函数void show(int);
};
1. 后台数字模型
如上段代码,其中int gameBoard[16] = {};
就是后台的棋盘数字4X4的模型,其他的成员都跟图像显示有关。
2 显示模型
知识点:SDL库的使用、SDL-ttf库的使用。
2.1 SDL库的使用
2.1.1 窗口渲染
在main函数中进行SDL初始化操作,如下:
//初始化SDL成视频模式
SDL_Init(SDL_INIT_VIDEO);
//初始化窗口,位置(x,y)默认,尺寸800X800,窗口为显示模式
SDL_Window *window = SDL_CreateWindow("2048小游戏", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 4*DIM, 4*DIM, SDL_WINDOW_SHOWN);
if (!window) {cout << "窗口生成异常" << endl;return 1;
}
//初始化窗口渲染器,相当于画布,-1表示默认的渲染设备,使用软件渲染
SDL_Renderer *render = SDL_CreateRenderer(window, -1, SDL_RENDERER_SOFTWARE);
if (!render) {cout << "渲染器生成异常" << endl;return 1;
}
生成了窗口渲染器后传递给棋盘类的构造函数,还有1个参数ttf,在2.2部分会讲:
Chess2048 chess = Chess2048(render, ttf);
这里另外涉及1个全局变量DIM,设置成你想要的尺寸就行,我这里定义为200,代表棋盘上1个格子的边长。
2.1.2 矩形绘制
棋盘总共是4X4个格子,所以要绘制16个格子,而每个格子的颜色是相间的,所以这里用到了SDL_Color bgColor[2] = {{255,255,0,255}, {255,180,0,255}}; //金黄、橙
这个变量。矩形绘制是在show成员函数中实现的:
//param:interval ms刷新频率
void Chess2048::show(int interval) {//设置背景色白色SDL_SetRenderDrawColor(render, 255, 255, 255, 255); //清空渲染器SDL_RenderClear(render);for (int i = 0; i < 4; i++) {for (int j = 0; j < 4; j++) {// cout << this->gameBoard[i*4+j] << " ";//命令行显示//绘制格子SDL_Rect gridRect = {j*DIM, i*DIM, DIM, DIM}; //格子矩形SDL_SetRenderDrawColor(this->render, this->bgColor[(i+j)%2].r, this->bgColor[(i+j)%2].g, this->bgColor[(i+j)%2].b, this->bgColor[(i+j)%2].a);SDL_RenderFillRect(this->render, &gridRect);//绘制数字部分//...}// cout << endl; //命令行显示}SDL_RenderPresent(this->render); //显示SDL_Delay(interval); //延时
}
2.2 SDL-ttf库的使用
在main函数中进行SDL-ttf初始化操作,并传递给棋盘类的构造函数,其中相应的字体我提前拷贝到了程序文件夹,否则就会找不到字体:
//初始化字体
TTF_Init();
//打开字库
TTF_Font *ttf = TTF_OpenFont("NotoSansTamil-Regular.ttf", 50);
if (!ttf) {cout << "字体打开失败" << endl;return 1;
}
Chess2048 chess = Chess2048(render, ttf);
2.2.1 设置字体属性
在构造函数中设置字体加粗属性,当然还有其他属性比如描边等,大家可以自行探索:
TTF_SetFontStyle(font, TTF_STYLE_BOLD);
2.2.2 创建纹理图层
同样是在构造函数中完成的,因为程序一直要用到2、4、8…2048这11个数字,所以我把它设置成了成员数组保存,而我又需要3种颜色,于是随便设置了3种颜色:
private:TextSize textSize[11]; //2、4、8...2048的字体尺寸SDL_Texture *texture[11]; //2、4、8...2048的字体纹理指针SDL_Color fgColor[3] = {{25, 160, 0, 255}, {85, 200, 200, 255}, {55, 0, 100, 255}}; //绿、天蓝、紫
而字体纹理是由字体表面得来的,于是接下来就通过构建字体表面进而构建出纹理来:
Chess2048::Chess2048(SDL_Renderer * render, TTF_Font *font): render(render), font(font) {//在随机的位置随机生成2个数2或4//...//创建字体表面,保存字体纹理for (int i = 0; i < 11; i++) {string s = to_string(((int)pow(2, i+1))); //2、4、8...2048SDL_Surface * textSurface = TTF_RenderUTF8_Solid(font, s.c_str(), this->fgColor[i % 3]);this->textSize[i].w = textSurface->w; this->textSize[i].h = textSurface->h;this->texture[i] = SDL_CreateTextureFromSurface(render, textSurface);SDL_FreeSurface(textSurface); //释放字体表面}
}
2.2.3 绘制文字
同矩形绘制一样,在show成员函数中实现:
void Chess2048::show(int interval) {//设置背景色白色SDL_SetRenderDrawColor(render, 255, 255, 255, 255); //清空渲染器SDL_RenderClear(render);for (int i = 0; i < 4; i++) {for (int j = 0; j < 4; j++) {// cout << this->gameBoard[i*4+j] << " ";//命令行显示//绘制格子部分//...//绘制不为0的数字int num = this->gameBoard[i*4+j];if (num) {int index = (int)log2(num) - 1;SDL_Rect textRect = {j*DIM+DIM/2 - this->textSize[index].w/2, i*DIM+DIM/2 - this->textSize[index].h/2, this->textSize[index].w, this->textSize[index].h}; //字 SDL_RenderCopy(this->render, this->texture[index], nullptr, &textRect); //显示}}// cout << endl; //命令行显示}SDL_RenderPresent(this->render);SDL_Delay(interval);
}
(三) 随机生成2个数字(2或4)
知识点:伪随机数生成。
- 主要是在构造函数是实现的,能产生2到3个随机的2或4:
Chess2048::Chess2048(SDL_Renderer * render, TTF_Font *font): render(render), font(font) {//在随机的位置随机生成2个数2或4srand(time(NULL));this->gameBoard[rand() % 16] = (rand() % 2 + 1) * 2;this->gameBoard[rand() % 16] = (rand() % 2 + 1) * 2;//随机生成第3个数if (rand() % 2) {this->gameBoard[rand() % 16] = (rand() % 2 + 1) * 2;}//构造字体部分//...
}
这里需要提醒的是srand(time(NULL));
必须要,不然光靠rand()
函数是不能生成伪随机数的。
- 其次在移动棋盘过程中也会随机出现不多于2个的数字2或4:
//如果发生了移动则在空余位置生成随机的2或4
if (moved) {int pos = rand() % 16;if (!this->gameBoard[pos]) this->gameBoard[pos] = (rand() % 2 + 1) * 2;pos = rand() % 16;if (!this->gameBoard[pos]) this->gameBoard[pos] = (rand() % 2 + 1) * 2;
}
(四) 捕获用户方向键操作
在main函数中实现:
Chess2048 chess = Chess2048(render, ttf);
while(true) {if (chess.win()) {cout << "You Win!" << endl;break;} else if (chess.full()) {cout << "You Lose!" << endl;break;}//捕获事件SDL_Event event;SDL_PollEvent(&event); //等待事件
// cout << event.type << " " << flush;if (event.type == SDL_QUIT) { // 退出break;} else if (event.type == SDL_KEYDOWN) { // 按键//防抖while(true) {SDL_PollEvent(&event); //等待事件if ((event.type == SDL_KEYUP)) break;}switch (event.key.keysym.sym) { //检测按了哪个方向键case SDLK_UP:chess.up();break;case SDLK_DOWN:chess.down();break;case SDLK_LEFT:chess.left();break;case SDLK_RIGHT:chess.right();break;}} //end of ifchess.show(20); //按频率显示
} //退出循环
这时event.type
表示SDL事件类型,我捕获的是按键,但按键有按下和抬起2种事件,因此增加了防抖机制,即直到按下后抬起再开始响应。而event.key.keysym.sym
可以通过与SDL内置的键码进行比较,就可判断是哪个方向键按下了。
1.合并运算函数
这一部分是重难点,以上移操作为例,使用了变量k作为指示功能:
bool Chess2048::up() {bool moved = false;//所有格子偿试上移for (int j = 0; j < 4; j++) { //列int k = -1; // 标记压缩后不为0的行标末尾if (this->gameBoard[j]) k = 0;for (int i = 1; i < 4; i++) { //行if (!this->gameBoard[i*4+j]) continue;k++;if (k < i) { //直到不为0时往前压缩this->gameBoard[k*4+j] = this->gameBoard[i*4+j];this->gameBoard[i*4+j] = 0;moved = true;}if (k > 0 && this->gameBoard[(k-1)*4+j] == this->gameBoard[k*4+j]) { //与上一行相同则合并this->gameBoard[(k-1)*4+j] *= 2;this->gameBoard[k*4+j] = 0;k--;moved = true;}}}//如果发生了移动则在空余位置生成随机的2或4//...return moved;
}
(五) 更新游戏界面
在(四) 捕获用户方向键操作部分已经提到了,即chess.show(20); //按频率显示
。
(六) 判断游戏结束
1.判断获胜
bool Chess2048::win() {for (int i = 0; i < 16; i++) {if (this->gameBoard[i] == 2048) return true;}return false;
}
2.判断失败
判断完获胜后,只需判断有没有没到2048就满格的,但需判断是不是假满格,所谓假满格就是相邻格子可以合并的:
//判断是否满格,不能是假满格(即相邻单元格可以合并)
bool Chess2048::full() {// 所有数字全部和左、上比较for (int i = 0; i < 4; i++) {for (int j = 0; j < 4; j++) {if (!this->gameBoard[i*4+j]) return false; //如果有0,就不满if (i == 0 && j == 0) continue;if (j > 0 && this->gameBoard[i*4+j] == this->gameBoard[i*4+j-1]) return false; //和左边相等,假满格if (i > 0 && this->gameBoard[i*4+j] == this->gameBoard[(i-1)*4+j]) return false; //和上边相等,假满格}}return true;
}
三、总结
本文教大家使用C++语言实现一个图形化2048小游戏,希望有所收获,如有好的建议欢迎留言,谢谢大家啦!
四、源码下载
2048小游戏(C++ SDL版)