目录
一、游戏规则
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...2048
SDL_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或4
srand(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 if
chess.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小游戏,希望有所收获,如有好的建议欢迎留言,谢谢大家啦!

217

被折叠的 条评论
为什么被折叠?



