Dotcpp  >  编程教程  >  C/C++游戏类项目  >  C语言2048游戏教程及源码(easyX版)

C语言2048游戏教程及源码(easyX版)

点击打开在线编译器,边学边练

一、项目介绍

这是一个可以单人进行的2048小游戏。

游戏的目的是逐渐增大界面上的数字,获取更高的分数,直至有数字达到2048.游戏用方向键控制(或者是wasd),每当你按下方向键,所有的数字都会向那个方向运动到头,如果有两个相同的数字碰撞在一起,则会产生一个2倍的数字。

 

编译环境:visual c++ 6.0

第三方库:Easyx2022

 

二、运行截图

2048游戏12048游戏2

2048游戏32048游戏4

 

三、源码解析

我们先来思考一下游戏的逻辑。

在经过了初始化以及界面生成之后,玩家其实只需要做出很简单的输入,就可以推进游戏的进程,无非就按下方向键,界面做出反应,然后接收下一次指令……这样,整体逻辑就已经很清晰了。

初始化;

绘制界面;

玩家操作,界面及数据变化,检测是否胜利,若非胜利,循环操作步骤。

下面是主函数:

int main()
{
       bool ctn = true;                                 // 该值代表是否重开新局
       SetWindowText(initgraph(350, 440), "2048-dotcpp.com");                       // 初始化图形界面
       srand((unsigned)time(NULL));
       while (ctn)
       {
              init();                                                // 新的一局,程序初始化
              drawmap();                                      // 绘制界面
              int endmode = 0;                       // 结束方式,1 代表胜利,2 代表失败
              while (1)
              {
                     move();                                     // 玩家操作
                     drawmap();                               // 绘制界面
                     if (win())                             // 胜利
                     {
                            endmode = 1;
                            break;
                     }
                     if (gameover())                         // 失败
                     {
                            endmode = 2;
                            break;
                     }
              }
              int t;                                          // 获取用户选择的按钮
              if (endmode == 1)                     // 胜利
                     t = MessageBox(0, _T("You win!\n再来一局?"), _T("继续"), MB_OKCANCEL);
              if (endmode == 2)                     // 失败
                     t = MessageBox(0, _T("Game over!\n再来一局?"), _T("继续"), MB_OKCANCEL);
              if (t == IDCANCEL)ctn = false;  // 若用户选择 取消,则不重新开局
       }
       closegraph();                                   // 关闭图形界面
       return 0;
}

Initgraph用于初始化图形窗口,参数为窗口大小。在头文件easyx中引入。

接着构建循环,在需要时重复一局游戏。

初始化,然后不断接收用户的输入。

最后用MessageBox输出文字。MessageBox是Windows.h当中的内容,用于弹出对话框。

还要说明的一点是_T()这个函数。_T()是一个宏,他的作用是让程序支持Unicode编码,用来保证兼容性。VC支持ascii和unicode两种字符类型,用_T可以保证从ascii编码类型转换到unicode编码类型的时候,程序不需要修改。

 

接下来看需要定义的函数。

viod init()//初始化函数

void drawmap()// 定义绘制界面

void move()// 定义玩家操作

bool gameover()// 判断游戏结束

bool win()// 判断胜利

还有变量:

const COLORREF BGC = RGB(250, 248, 239);// 定义背景色常量

int score, best, a[5][5], b[5][5];// score 为本局分数,best为当前最佳纪录,a数组为棋盘,b数组为a的备份

bool mov[5][5];// 棋盘上的点是否已被移动过(避免重复移动)

可以用二维数组来存储游戏的数据。

 

然后是每个函数的实现过程。

初始化:

void init()
{
       setbkcolor(BGC);
       setbkmode(TRANSPARENT);
       score = 0;
       memset(a, 0, sizeof(a));
       int x1 = rand() % 4 + 1, y1 = rand() % 4 + 1, x2 = rand() % 4 + 1, y2 = rand() % 4 + 1;  // 随机生成两个初始点
       a[x1][y1] = a[x2][y2] = 2;                          // 初始点初始化为 2
}
setbkcolor用于设置当前设备绘图背景色。
setbkmode用于设置当前设备图案填充和文字输出时的背景模式,TRANSPARENT意味着背景色是透明的。
在游戏开始时,要将两个点的值设为2。
 
绘制界面:
void drawmap()
{
       // 开始批量绘图
       BeginBatchDraw();
 
       // 绘制界面主体
       cleardevice();
       settextcolor(RGB(119, 110, 101));
       settextstyle(50, 0, _T("微软雅黑"));
       outtextxy(10, 10, "2048");
       settextstyle(20, 0, _T("微软雅黑"), 0, 0, 550, false, false, false);
       outtextxy(10, 65, "Join the numbers and get to the 2048 tile!");
 
       setfillcolor(RGB(187, 173, 160));
 
       // 绘制当前分数
       solidroundrect(200, 15, 290, 60, 5, 5);
       settextcolor(RGB(230, 220, 210));
       settextstyle(15, 0, _T("微软雅黑"), 0, 0, 600, false, false, false);
       outtextxy(230, 20, "SCORE");
       char sc[10];
       sprintf(sc, "%d", score);
       settextcolor(WHITE);
       settextstyle(25, 0, _T("微软雅黑"), 0, 0, 600, false, false, false);
       RECT r = { 200, 30, 290, 60 };
       drawtext(sc, &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
 
       // 绘制最佳纪录
       solidroundrect(295, 15, 385, 60, 5, 5);
       settextcolor(RGB(230, 220, 210));
       settextstyle(15, 0, _T("微软雅黑"), 0, 0, 600, false, false, false);
       outtextxy(330, 20, "BEST");
       char bs[10];
       sprintf(bs, "%d", best);
       settextcolor(WHITE);
       settextstyle(25, 0, _T("微软雅黑"), 0, 0, 600, false, false, false);
       RECT s = { 295, 30, 385, 60 };
       drawtext(bs, &s, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
 
       // 绘制数字棋盘
       solidroundrect(10, 90, 390, 470, 5, 5);
       settextstyle(23, 0, _T("微软雅黑"));
       settextcolor(WHITE);
       for (int i = 1; i <= 4; i++)
              for (int j = 1; j <= 4; j++)
                     if (a[i][j])               // 如果该位置没有数字,则不绘制
                     {
                            // 用类似哈希的方法,为每个数字计算出对应的颜色
                            setfillcolor(RGB((unsigned int)(BGC - 3 * (a[i][j] ^ 29)) % 256, (unsigned int)(BGC - 11 * (a[i][j] ^ 23)) % 256, (unsigned int)(BGC + 7 * (a[i][j] ^ 37)) % 256));
                            solidroundrect(94 * j - 80, 94 * i, 94 * j + 10, 94 * i + 90, 5, 5);
                            char num[10];
                            sprintf(num, "%d", a[i][j]);
                            RECT t = { 94 * j - 80, 94 * i, 94 * j + 10, 94 * i + 90 };
                            drawtext(num, &t, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
                     }
 
       // 结束批量绘图
       EndBatchDraw();
}

BeginBatchDraw函数用于开始批量绘图。执行后,任何绘图操作都将暂时不输出到绘图窗口上,直到执行 FlushBatchDraw 或 EndBatchDraw 才将之前的绘图输出。

cleardevice函数使用当前背景色清空绘图设备。

settextcolor用于设置当前文字颜色。

settextstyle用于设置当前字体。

outtextxy用于在指定位置绘制字符。

drawtext函数用于在指定区域内以指定格式输出字符串。

solidroundrect用于画无边框的填充圆角矩形。

setfillcolor用于设置当前设备填充颜色。

以上的几个函数都在easyx当中定义。

绘制文字比较简单,按照顺序来即可;绘制棋盘则是用二重循环,依次绘制出有或者无数字的区域。注意这里的颜色是以数字作为自变量算出来的。

 

然后是玩家操作部分,也是最主要的部分。

void move()// 定义玩家操作

上下左右移动的代码比较类似,这里只放一部分即可:

memcpy(b, a, sizeof(a));                  // 将 a 备份至 b
memset(mov, false, sizeof(mov));    // 初始化 mov 为 false(所有点均未移动)
       // 获取用户操作
       char userkey = _getch();
       if (userkey == -32)
              userkey = -_getch();
       // 移动棋盘(移动 a 数组)
       int i,j;
       switch (userkey)
       {
       // 向上
       case 'w':
       case 'W':
       case -72:
              for (j = 1; j <= 4; j++)
                     for (i = 2; i <= 4; i++)
                     {
                            if (!a[i][j])continue;
                            int k = i;
                            while (!a[k - 1][j] && k >= 2)
                            {
                                   a[k - 1][j] = a[k][j];
                                   a[k][j] = 0;
                                   k--;
                            }
                            if (a[k][j] == a[k - 1][j] && !mov[k - 1][j])
                            {
                                   a[k - 1][j] = 2 * a[k][j];
                                   a[k][j] = 0;
                                   mov[k - 1][j] = true;
                                   score += a[k - 1][j];
                            }
                     }
              break;
     }

memcpy(b, a, sizeof(a))表示从a复制sizeof(a)个字节的内容到b。

memset(mov, false, sizeof(mov));表示将mov中sizeof(mov)字节的内存全部赋值为false。

这两个函数都在string.h当中。

_getch()用于获取用户键入的单个字符,且不需要按下回车。定义于头文件conio.h当中。

根据得到的这个输入的不同,我们需要给出程序的反应。这里以按住上键为例。

在按住上键之后,所有的数字都会向上方移动,当然最上面一行不需要(它已经在终点等着了)。所以我们用双层循环从上到下遍历二到四行的数字,执行移动操作。没有数字自然可以跳过,有则判断它的上方是否为空,是则继续移动。移动到头后,再判断它碰到的数字是否与它相同,是则执行合并操作。

bool change = false;                         // 判断经过移动,棋盘是否改变
       // 比较当前棋盘与移动前(b 数组)棋盘
       for (i = 1; i <= 4; i++)
              for (int j = 1; j <= 4; j++)
                     if (a[i][j] != b[i][j])
                     {
                            change = true;
                            break;
                     }
       if (!change)return;                            // 如果棋盘没有改变,退出
       // 生成一个新数字(且不与已有数字重合)
       int x, y;
       do
       {
              x = rand() % 4 + 1;
              y = rand() % 4 + 1;
       } while (a[x][y]);
       // 有 1/6 的几率生成数字为 4,其余情况生成数字为 2
       int n = rand() % 6;
       if (n == 5)a[x][y] = 4;
       else a[x][y] = 2;
       // 更新最佳纪录
       best = max(best, score);
}

在执行完移动行为之后,将棋盘与备份对照,如果没有改变,则此次操作无效。(游戏结束会在后边的函数当中另行判断)。

最后在空位生成两个数字,更新分数。

 

然后是检查是否游戏结束。

bool gameover()
{
       // 对于任意一个位置,该位置为空 或 四周有位置上的数字与该位置上数字相等,说明可继续移动(游戏可继续)
       for (int i = 1; i <= 4; i++)
              for (int j = 1; j <= 4; j++)
                     if (!a[i][j] || a[i][j] == a[i + 1][j] || a[i][j] == a[i - 1][j] || a[i][j] == a[i][j + 1] || a[i][j] == a[i][j - 1])return false;
       // 否则游戏结束
       return true;
}

简单的双层循环遍历即可实现。

 检查胜利的函数更为简单。只需验证棋盘上的数字有无达到2048就可以了。

 

四、完整源码

 C语言生命游戏完整源码(easyX版)

 

 



本文固定URL:https://www.dotcpp.com/course/1232

C语言网提供由在职研发工程师或ACM蓝桥杯竞赛优秀选手录制的视频教程,并配有习题和答疑,点击了解:

一点编程也不会写的:零基础C语言学练课程

解决困扰你多年的C语言疑难杂症特性的C语言进阶课程

从零到写出一个爬虫的Python编程课程

只会语法写不出代码?手把手带你写100个编程真题的编程百练课程

信息学奥赛或C++选手的 必学C++课程

蓝桥杯ACM、信息学奥赛的必学课程:算法竞赛课入门课程

手把手讲解近五年真题的蓝桥杯辅导课程

趣味项目教程
第一章 C/C++游戏类项目
第二章 C/C++工具及其他类项目
第三章 Python趣味项目
Dotcpp在线编译      (登录可减少运行等待时间)