C++实现图形化2048小游戏


在这里插入图片描述

一、游戏规则

2048游戏开始时,棋盘内会随机出现2到3个数字,这2到3个数字通常是2或4。玩家可以通过方向键来控制棋盘上的数字方块向该方向移动,每次相同的数字方块会进行合并,新的数值为原来2个方块数值之和。游戏的目标是得出最高数2048这个数,一旦所有格子都被填满且未达到2048这个目标,游戏就失败了。

游戏的设计思路:

  1. 初始化游戏界面
  2. 随机生成2个数字(2或4)
  3. 捕获用户方向键操作
  4. 更新游戏界面
  5. 判断游戏结束

二、步骤实现

(一) SDL库的安装

需要SDL库、SDL-ttf库,在linux系统中安装命令apt install libsdl2-devapt 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)

知识点:伪随机数生成。

  1. 主要是在构造函数是实现的,能产生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()函数是不能生成伪随机数的。

  1. 其次在移动棋盘过程中也会随机出现不多于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小游戏,希望有所收获,如有好的建议欢迎留言,谢谢大家啦!

四、源码下载

2048小游戏(C++ SDL版)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值