EGE专栏:EGE专栏

目录

  • 一、程序源码下载
    • 1. 百度网盘(示例二 2048)
    • 2. CSDN(示例二 2048)
  • 二、2048 游戏介绍
  • 三、EGE制作的2048游戏界面
  • 四、制作流程
    • 1. 素材获取
    • 2. 素材整理
    • 3. 界面设计
    • 4. 功能分析
  • 五、最基本的实现
    • 1. 游戏设计
      • 1.1 数据表示
      • 1.2 图像加载
      • 1.3 四方向移动
        • 1.3.1 按键控制移动方向
        • 1.3.2 移动
      • 1.4 添加随机数
    • 2. 基础版程序
  • 六、完整功能版实现
    • 1. 重新开始
    • 2. 添加音效
    • 3. 计分
    • 4. 自动存档,读档
    • 5. 游戏结束判断
    • 6. 添加移动动画(难点)
  • 七、完整版代码

一、程序源码下载

1. 百度网盘(示例二 2048)

https://pan.baidu.com/s/1BUDGLeenbIxpAfqd1XfNqg

  源代码分享于 百度网盘,里面也有程序用到的资源文件,可以查看下载。

2. CSDN(示例二 2048)

https://download.csdn.net/download/qq_39151563/12154829

  CSDN资源,设定为0积分,无需积分即可下载。

二、2048 游戏介绍

2048在线游戏链接: https://2048game.com/

2048 游戏规则

2048小游戏相信应该都玩过,规则很简单,上面也贴出了在线游戏链接,可以先玩一玩,了解一下游戏规则。

  • 游戏主体为 4×44\times 44×4 的格子,每个小格子可以放置一个方块,方块带有一个数字。
  • 游戏一开始随机出现两个数字方块,数字为2或4。
  • 方块可以 上下左右 四个方向滑动。滑动后,同方向上两个相同的相邻方块会合并成更大的方块,合成后的方块数字是原来两个方块之和, 并且在一次滑动中,一个方块只能和相同的方块合成一次,不能连续合成,且在移动方向前面的两个优先合成。
  • 每滑动一次,会在随机的一个空格中生成一个数字(2 或4, 大多是2)
  • 如果16个格子都填满且无法滑动,则游戏结束。

计分规则

  每合成一个方块加进行加分, 分值 = 合成的方块上的数字

三、EGE制作的2048游戏界面

  下面EGE制作的2048界面

四、制作流程

1. 素材获取

  网上有很多 Android 手机的游戏,想要相关的图片和音乐资源的可以先下载应用安装包,然后用 apktool 解析资源文件,得到里面的素材。(apktool 下载使用方法可以自行百度)
  示例程序链接中也放有我从一款2048游戏中解析得到的素材。(这款游戏一大堆广告,各种弹窗,真的**)

2. 素材整理

  一般别人做的游戏都比较花里胡哨的,东西很多,可以从中挑选一些自己用到的。有些图片尺寸不对,可以自行用PS, 或者其它的图像处理软件缩放一下,调成合适的尺寸, 并且改成合适文件名(可以在用到的时候再选取,并取好点的文件名)
   下面则是我从中挑出的所需要的图片和音乐素材, 并且对图片缩放过,以适配界面尺寸。

素材链接:https://download.csdn.net/download/qq_39151563/12154829


3. 界面设计

  这时候考虑窗口的大小,界面的布局,在哪里显示什么内容,界面跳转等等。
  再来说一下笔记本显示比例的问题,我笔记本是125%放大显示,估计一般的笔记本都是这样。因为对于笔记本来说,设置为100%的显示比例的时候,界面上的文字和图标真的很小。
  所以如果设置EGE窗口是 500x500, 那么你将窗口截屏,会发现截下来的图片,分辨率约为 625 * 625。如果你看到屏幕上某个尺寸挺合适,截图下来后查看分辨率,需要除以缩放比例才能得到原图的大小,以这样的大小绘制,才会得到想要的尺寸。


   根据素材所设计的界面布局(参考上面在线2048的布局)

4. 功能分析

基本实现

  • 4×44\times 44×4 方格
  • 四方向移动,出现数字的组合(难点)
  • 移动后随机在一个空格中出现2或4

附加项

  • 计分,只出现在合并时 (自娱自乐,没有也行)
  • 游戏结束判断:无法移动(16个格子满,且相邻格子都不同)
  • 移动动画(难点)
  • 游戏音效
  • 游戏自动存档读档 (常用)
  • 游戏重新开始 (必备)

五、最基本的实现

  即仅仅实现基础功能:在 4×44\times 44×4 格子中移动合并数字,并随机出现数字,游戏界面只有4x4个格子。不计分,不作游戏结束判断,不存档,无动画效果,无音效

1. 游戏设计

1.1 数据表示

  4x4 格子用二维数组表示即可。

int grid[4][4];

  由于方块上数字是 2,4,8,16,...2, 4, 8, 16,...2,4,8,16,...,是222的nnn次方, 所以可以考虑存储 nnn 即可, 这样方便编号,特别是图片, 编号 nnn从111到171717,分别对应数字2n\ 2^n 2n ,空格用000表示。因为元素就是编号,所以绘图时直接根据元素绘制即可。

为什么是1到17?
  因为随机出现的数字最大是4, 即 2 的2次方,从4开始,一共能排上16个数字,加上2,那么一共17个数字,即可能出现的最大数字是 217=1310722^{17} = 131072217=131072。(牛逼牛逼,131072只存在于理论上吧)
  如下图所示:

  上图每个方块所对应的存储数据分别为:


  即如果方块的值为 2n2^n2n,那么存储的数值为 nnn。

1.2 图像加载

  既然数字 222 到 131072131072131072, 分别对应编号 111 到 171717, 那么图片数据用长度为 181818 的 PIMAGE 数组 blockImgs[] 存储即可, 方块 2n2^{n}2n 的图片就存储于blockImgs[n],方块 2,4,⋯,1310722, 4, \cdots, 1310722,4,⋯,131072 分别存储于 blockImgs[1], blockImgs[2], … , blockImgs[17], 一共18个,blockImgs[0]不使用。另外还需要存储一张 4×44\times 44×4 格子的背景图。

#define NUM_BLOCK 18
PIMAGE blockImgs[NUM_BLOCK];
PIMAGE backgroundImg;

  数字对应的图片名字命名格式为 "block_数字", 存放于"resource\\\\image" 文件夹中。

  图片可以使用如下方式获取:(利用sprintf()生成文件名字符串)

void loadImage()
{//创建一个可以容纳生成的字符串的字符数组char imgName[64];for (int i = 1, num = 2; i < NUM_BLOCK; i++, num *= 2) {//生成图片文件名,存储到imgName[]中sprintf(imgName, "resource\\image\\block_%d.png", num);//创建图像,并从文件中读取blockImgs[i] = newimage();getimage(blockImgs[i], imgName);}//读取背景图backgroundImg = newimage();getimage(backgroundImg, "resource\\image\\background.png");
}

1.3 四方向移动

  这是整个游戏最核心的部分。
  首先移动需要检测按键,常用 AWDS 和四个方向键。

1.3.1 按键控制移动方向

  只需要一个变量 direct来记录移动的方向。
  数值 0~3 分别对应:左、上、右、下,这个可以用枚举,宏等进行定义,含义更清晰。

const int  LEFT = 0, UP = 1, RIGHT = 2, DOWN = 3;int direction = -1;
while (kbmsg()) {key_msg keyMsg = getkey();if (keyMsg.msg == key_msg_down) {switch (keyMsg.key) {case 'A': case key_left:  direction = LEFT;  break;case 'W': case key_up:      direction = UP;        break;case 'D': case key_right:   direction = RIGHT; break;case 'S': case key_down:    direction = DOWN;  break;}}
}

  读取后,如果不等于-1,说明按下了代表方向的按键。后面就根据direction的值进行移动。

1.3.2 移动

  移动时需要根据移动方向来检测数字,向左移动,那么就要对每一行,从左往右检测,即移动方向和检测方向是相反的,因为在前面的会优先合成。
  下图中的左边部分即为左移时的检测顺序,包含初始位置,下一个元素的位置偏移以及下一行(列)的位置偏移。

  四个方向,区别就是检测起点不同,检测方向不同,由此可以根据四个方向,得到这些数据。
  (x0, y0)检测起点坐标firstOffset为当前行(列)中下一个元素的坐标偏移量, secondOffset 为下一行(列)的坐标偏移量。

//索引0~3分别对应移动方向左上右下//初始检测位置
static int x0[4] = { 0, 0, 3, 0 }, y0[4] = { 0, 0, 0, 3 };// 分别对应四个移动方向的下一个元素的位置偏移(位置偏移与移动方向相反)
static int elemOffset[4][2] = { {1, 0},{0, 1},{-1, 0}, {0, -1} };
// 分别对应四个移动方向的下一行(列)的位置偏移(位置偏移与移动方向相反)
static int lineOffset[4][2] = { {0, 1}, {1, 0}, {0, 1}, {1, 0} };

  所以,对于移动方向direction, 按检测顺序遍历每个元素则为:

for (int i = 0; i < 4; i++) {//计算每一行(列)起点位置坐标int x = x0[direction] + i * lineOffset[direction][0];int y = y0[direction] + i * lineOffset[direction][1];for (int j = 0; j < 4; j++) {//这里可以检测元素grid[y][x];//移动至下一个元素x += elemOffset[direction][0];y += elemOffset[direction][1];}
}

  这样,就完成了四个方向遍历的统一。

合并问题变换
  这个问题变换为:一个长度为n的数组a,向下标为0的方向移动,忽略值为0的元素,相邻并且相同的元素将合并成一个值为两数之和的元素,并且每个元素只能参与一次合并,多个相同的元素相邻时,下标小的优先合并。
方法一:
  因为忽略值为0的元素,所以可以用right表示遍历到的非零元素,0 ~ left - 1 为左边完成移动合并的元素。left 代表可能将要移动到的空位或可能参与下一次合并的元素。

void merge1(int a[], int length)
{for (int left = 0, right = 1; right < length; right++) {//找到一个非空格子if (a[right] != 0) {//a[left] 是空格,直接将a[right]前移至空格处if (a[left] == 0) {a[left] = a[right];a[right] = 0;}else {//a[left]非空格//如果两个相同,直接合并if (a[left] == a[right]) {a[left] *= 2;a[right] = 0;}//两个位置不相邻,中间有空格,则将a[right]移动至a[left]的后一个空格处else if (left + 1 != right) {a[left + 1] = a[right];a[right] = 0;}// 当前位置已处理完毕,进行下一个位置的处理left++;}}}
}

方法二:
  直接忽略元素0,只在非零元素间判断,相同则合并,处理完成后,元素中间可能会夹杂许多0,这时再次遍历,像删掉字符串中的某个字符一样,除去元素中间的0。

void merge2(int a[], int length) {int l = 0, r = 1, end = 0;for (; r < length; r++) {if (a[r] != 0) {if (a[l] == a[r]) {a[l] *= 2;a[r] = 0;}end = l = r;}}for (l = 0, r = 0; r <= end; r++) {if (a[r] != 0) {a[l++] = a[r];}}while (l <= end)a[l++] = 0;
}

  算法已经实现,然后回到二维数组,分别对每一行或每一列进行合并即可。于是得到下面的移动算法:(使用的是方法一)

  move() 函数返回是否有格子发生的变动,这样可以根据是否进行元素移动或合并来决定需不需要添加一个随机数。如果返回 false,即格子没有变动,那么这次移动是无效动作,不需要添加随机数。
  emptyBlocks 表示当前的空格数,代码中根据这个值来判断是否还能添加随机数的。每产生一次合并,方块数会少1,所以空格数加1。

bool move(int direction)
{//索引0~3分别对应移动方向左上右下//初始检测位置static int x0[4] = { 0, 0, 3, 0 }, y0[4] = { 0, 0, 0, 3 };// 分别对应四个移动方向的下一个元素的位置偏移(位置偏移与移动方向相反)static int elemOffset[4][2] = { {1, 0},{0, 1},{-1, 0}, {0, -1} };// 分别对应四个移动方向的下一行(列)的位置偏移(位置偏移与移动方向相反)static int lineOffset[4][2] = { {0, 1}, {1, 0}, {0, 1}, {1, 0} };bool moved = false;     //是否有格子移动for (int i = 0; i < 4; i++) {// 计算每行(列)初始位置int xCur = x0[direction] + i * lineOffset[direction][0];int yCur = y0[direction] + i * lineOffset[direction][1];int xNext = xCur, yNext = yCur;for (int j = 1; j < 4; j++) {xNext += elemOffset[direction][0];yNext += elemOffset[direction][1];// 查找下一个非空格子位置if (grid[yNext][xNext] != 0) {//先判断当前格子移动前是否是空格子bool empty = (grid[yCur][xCur] == 0);//当前位置为空,直接将下一个非空格子移动至当前位置if (empty) {grid[yCur][xCur] = grid[yNext][xNext];grid[yNext][xNext] = 0;moved = true;}//当前格子不为空else {int xNextAdjacent = xCur + elemOffset[direction][0];int yNextAdjacent = yCur + elemOffset[direction][1];//如果两个格子的值相同,直接合并if (grid[yNext][xNext] == grid[yCur][xCur]) {// 当前位置数值 + 1,消除下一个格子++grid[yCur][xCur];grid[yNext][xNext] = 0;moved = true;emptyBlock++;  //格子被消除,空格数+1}//格子不同else {//查看当前位置和下一个非空格子位置是否相邻if (!((xNext == xNextAdjacent) && (yNext == yNextAdjacent))) {//不相邻则将下一个非空格子移动至相邻位置grid[yNextAdjacent][xNextAdjacent] = grid[yNext][xNext];grid[yNext][xNext] = 0;moved = true;}}//当前位置原本非空则移动至下一个格子,不考虑与其它格子进行合并xCur = xNextAdjacent;yCur = yNextAdjacent;}}}}return  moved;
}

1.4 添加随机数

  根据空格数 emptyBlock, 生成一个000到 empty-1 之间的随机数randEmptyBlock ,然后查找第randEmptyBlock个空格(从0开始编号), 往这个空格里添加一个2或4的方块,出现2的概率应该是大于4的,出现4的情况很少, 这里取 0.90.90.9 的概率出现数字 222, 0.10.10.1 的概率出现数字 444。参数 n 为添加的随机数个数,添加后,空格数 emptyBlock 减少 n,在这个过程中,也要判断空格数是否大于0,没有空格后就无法添加随机数,函数返回。

void addRandomNum(int n)
{while ((emptyBlock > 0) && (n-- > 0)) {int randEmptyBlock = rand() % emptyBlock;    // 随机选取一个空格int i = 0, count = 0;int* gridList = &grid[0][0];// 对数组进行遍历,查找对应的空格(空格从0开始编号)for (i = 0; i < 4 * 4; i++) {if ((gridList[i] == 0) && (count++ == randEmptyBlock))break;}//随机数字2或4,0.9概率是1,0.1概率是2gridList[i] = (rand() % 10 < 1) ? 2 : 1;emptyBlock--;}
}

2. 基础版程序

  综合上面,得到下面的代码, 共160行。
  图片放在 “./resource/image” 目录,并且数字图片命名格式为 “block_数字.png”, 背景图片命名为 “background.png”

#include <graphics.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>void loadImage();            // 加载图片
void releaseImage();        // 释放图片资源
void addRandomNum(int n);   // 添加随机数
bool move(int direction);   // 按方向移动格子
void draw();                // 绘制画面
void game2048();const int LEFT = 0, UP = 1, RIGHT = 2, DOWN = 3;#define DEVIDE 15
#define GRID_WIDTH 106//图片
#define NUM_BLOCK 18
PIMAGE blockImgs[NUM_BLOCK], backgroundImg;int grid[4][4];          //格子
int emptyBlock = 16;   //空格数int main()
{game2048();return 0;
}void draw()
{putimage_withalpha(NULL, backgroundImg, 0, 0);for (int i = 0; i < 4; i++) {for (int j = 0; j < 4; j++) {int x = (j + 1) * DEVIDE + j * GRID_WIDTH;int y = (i + 1) * DEVIDE + i * GRID_WIDTH;if (grid[i][j] != 0)putimage_withalpha(NULL, blockImgs[grid[i][j]], x, y);}}
}void addRandomNum(int n)
{while ((emptyBlock > 0) && (n-- > 0)) {int randEmptyBlock = rand() % emptyBlock;    // 随机选取一个空格int i = 0, count = 0;int* gridList = &grid[0][0];// 对数组进行遍历,查找对应的空格(空格从0开始编号)for (i = 0; i < 4 * 4; i++) {if ((gridList[i] == 0) && (count++ == randEmptyBlock))break;}//随机数字2或4,0.9概率是1,0.1概率是2gridList[i] = (rand() % 10 < 1) ? 2 : 1;emptyBlock--;}
}bool move(int direction)
{//索引0~3分别对应移动方向左上右下//初始检测位置static int x0[4] = { 0, 0, 3, 0 }, y0[4] = { 0, 0, 0, 3 };// 分别对应四个移动方向的下一个元素的位置偏移(位置偏移与移动方向相反)static int elemOffset[4][2] = { {1, 0},{0, 1},{-1, 0}, {0, -1} };// 分别对应四个移动方向的下一行(列)的位置偏移(位置偏移与移动方向相反)static int lineOffset[4][2] = { {0, 1}, {1, 0}, {0, 1}, {1, 0} };bool moved = false;     //是否有格子移动for (int i = 0; i < 4; i++) {// 计算每行(列)初始位置int xCur = x0[direction] + i * lineOffset[direction][0];int yCur = y0[direction] + i * lineOffset[direction][1];int xNext = xCur, yNext = yCur;for (int j = 1; j < 4; j++) {xNext += elemOffset[direction][0];yNext += elemOffset[direction][1];// 查找下一个非空格子位置if (grid[yNext][xNext] != 0) {//先判断当前格子移动前是否是空格子bool empty = (grid[yCur][xCur] == 0);//当前位置为空,直接将下一个非空格子移动至当前位置if (empty) {grid[yCur][xCur] = grid[yNext][xNext];grid[yNext][xNext] = 0;moved = true;}//当前格子不为空else {int xNextAdjacent = xCur + elemOffset[direction][0];int yNextAdjacent = yCur + elemOffset[direction][1];//如果两个格子的值相同,直接合并if (grid[yNext][xNext] == grid[yCur][xCur]) {// 当前位置数值 + 1,消除下一个格子++grid[yCur][xCur];grid[yNext][xNext] = 0;moved = true;emptyBlock++;  //格子被消除,空格数+1}//格子不同else {//查看当前位置和下一个非空格子位置是否相邻if (!((xNext == xNextAdjacent) && (yNext == yNextAdjacent))) {//不相邻则将下一个非空格子移动至相邻位置grid[yNextAdjacent][xNextAdjacent] = grid[yNext][xNext];grid[yNext][xNext] = 0;moved = true;}}//当前位置原本非空则移动至下一个格子,不考虑与其它格子进行合并xCur = xNextAdjacent;yCur = yNextAdjacent;}}}}return  moved;
}void loadImage()
{//创建一个可以容纳生成的字符串的字符数组char imgName[64];for (int i = 1, num = 2; i < NUM_BLOCK; i++, num *= 2) {//生成图片文件名,存储到imgName[]中sprintf(imgName, "resource\\image\\block_%d.png", num);//创建图像,并从文件中读取blockImgs[i] = newimage();getimage(blockImgs[i], imgName);}//读取背景图backgroundImg = newimage();getimage(backgroundImg, "resource\\image\\background.png");
}void releaseImage()
{for (int i = 0; i < NUM_BLOCK; i++)delimage(blockImgs[i]);delimage(backgroundImg);
}void game2048()
{initgraph(500, 500, INIT_RENDERMANUAL | INIT_NOFORCEEXIT);setcaption("2048");setbkcolor(WHITE);srand((unsigned int)time(0));loadImage();addRandomNum(2);draw();for (; is_run(); delay_fps(60)) {int direction = -1;while (kbmsg()) {key_msg keyMsg = getkey();if (keyMsg.msg == key_msg_down) {switch (keyMsg.key) {case 'A': case key_left:   direction = LEFT;  break;case 'W': case key_up:      direction = UP;    break;case 'D': case key_right:   direction = RIGHT; break;case 'S': case key_down:    direction = DOWN;  break;default: direction = -1; break;}}}//检测到按下方向键if (direction != -1) {//将格子按指定方向移动,如果发生了移动,,随机添加数字并清屏重绘if (move(direction)) {addRandomNum(1);cleardevice();draw();}}}releaseImage();closegraph();
}

程序界面截图

六、完整功能版实现

使用的素材



界面展示

1. 重新开始

重新开始按钮
  在图中添加了重新开始按钮,当检测到鼠标左键点击时,就判断点击位置是否在区域内。
  下面代码为判断点击位置是否在按钮区域,因为只有一个按钮,所以直接取了固定值。

//按钮点击判断
inline bool clickBtnRestart(int x, int y) {return (20 < x && x < 20 + 222) && (110 < y && y < 110 + 50);
}

  鼠标消息处理,判断是否有鼠标点击

//鼠标点击检测
bool leftClick = false;
while (mousemsg()) {mouse_msg mouseMsg = getmouse();if (mouseMsg.is_left() && mouseMsg.is_down()) { //左键按下leftClick = true;xClick = mouseMsg.x;yClick = mouseMsg.y;}
}

  点击按钮后标记需要重新开始。

// 重新开始按钮的点击判断
if (leftClick && clickBtnRestart(xClick, yClick)) {restartGameFlag = true;
}

游戏结束后按回车键
  在游戏结束后,可以直接按回车键重新开始,不需要用鼠标。游戏没有结束时,防止误碰,不对回车键响应。

if (gameOver) {// 游戏结束后,可以通过按回车键重新开始if (key == key_enter)restartGameFlag = true;
}

重新开始所需要做的工作
  重新开始需要把格子清零,空格数emptyBlock 设置为16,本局分数清零,还有结束标记 gameOver 清零,做好后,再做一些其它相关的操作。

if (restartGameFlag)
{restart();startMusic.Play(0);redrawFlag = true;
}
void restart()
{gameInfo.score = 0;gameOver = false;memset(grid, 0, sizeof(int) * 16);emptyBlock = 16;addRandomNum(2);
}

2. 添加音效

(音效可以不添加,因为EGE播放音乐会有点卡顿,影响流畅度)

  音效的添加很简单,先用MUSIC类打开音乐文件,然后在合适的时候调用Music.Play(0) 播放即可,因为Music.Play()中插了一个延时,动画会出现一帧的卡顿,如果是在移动动画中播放,延时一帧是可以感知到的卡顿,稍稍有点不流畅。所以可以看情况,决定要不要放音乐。
  选择了开始时和合并时播放音乐。

合并音效
  合并是在 move() 函数中检测的,用mergeMusic_flag 标记是否有合并。目前是设置为不在播放状态时才重新播放音效。这样的话如果音效放到一半又有其它方块合并,则并不会再次播放。

if (mergeFlag && mergeMusic.GetPlayStatus() != MUSIC_MODE_PLAY)mergeMusic.Play(0);

开始音效
  刚打开重新开始 时播放

//载入时
startMusic.Play(0);//重新开始时
if (restartGameFlag)
{restart();startMusic.Play(0);redrawFlag = true;
}

3. 计分

  分数分为 最高分数,当前分数,最大合成数字
  因为计分是出现在合并的时候,所以在move() 函数中加入。
  合并后计分,分值为合成的数字,同时更新最高分、最大合成数字。

// 计算分数
int scoring(int mergeNum)
{return mergeNum;
}// 加分
void addScore(int score)
{gameInfo.score += score;if (gameInfo.score > gameInfo.topScore)gameInfo.topScore = gameInfo.score;
}// 更新最大合成数字
void updateMaxMergeNum(int mergeNum)
{if (mergeNum > gameInfo.maxNum)gameInfo.maxNum = mergeNum;
}//累计单次移动时增加的分数
int singleScore = 0;// 合并时计算分数,分值 = 2的n次方(n为方块的值)
int num = 1 << grid[yCur][xCur];
if (num > singleMaxMergeNum)singleMaxMergeNum;//先统计
singleScore += scoring(num);//方便中间插入动画,动画完成再加分//后更新
addScore(singleScore);
updateMaxMergeNum(singleMaxMergeNum);

4. 自动存档,读档

数据文件名

const char* recordFile = "game2048Record.txt";

读档

  不能因为没有记录文件就无法运行,因为程序的运行不需要依赖记录文件。如果没有记录文件,那就自己初始化数据,重新开始。一开始没有运行过的游戏,哪来的记录文件呢?记录应该由程序自己生成,而不是自己手动添加。
  如果有记录文件,就读取记录,并且要适当地检查记录数据的正确性。

// 返回数据是否读取成功
bool loadRecord()
{FILE* fp = fopen(recordFileName, "r");if (fp == NULL)return false;int topScore, score, maxNum;if (fscanf(fp, "topScore:%d score:%d maxNum:%d", &topScore, &score, &maxNum) != 3) {fclose(fp);return false;}gameInfo.topScore = topScore;gameInfo.score = score;gameInfo.maxNum = maxNum;for (int i = 0; i < 4; i++) {for (int j = 0; j < 4; j++) {int readInCount = fscanf(fp, "%d", &grid[i][j]);// 读取数据出错或者数据无效if ((readInCount != 1) || (grid[i][j] < 0) || (NUM_BLOCK <= grid[i][j])) {fclose(fp);return false;}if (grid[i][j] != 0)emptyBlock--;}}fclose(fp);return true;
}

存档

  因为需要退出游戏后保存记录,所以初始化模式需要添加INIT_NOFORCEEXIT ,即关闭窗口后不强制结束程序,以便进行游戏保存工作
  为了方便看到保存的游戏数据,所以设置成文本文件格式保存。

void gameSave()
{//数据写入FILE* fp = fopen(recordFile, "w");if (fp == NULL)return;fprintf(fp, "topScore:%d\nscore:%d\nmaxNum:%d\n",gameInfo.topScore, gameInfo.score, gameInfo.maxNum);for (int i = 0; i < 4; i++) {for (int j = 0; j < 4; j++)fprintf(fp, "%d ", grid[i][j]);fprintf(fp, "\n");}fclose(fp);
}

5. 游戏结束判断

  游戏结束,那必定是在出现随机数后,或者一开始读取的记录就是已经结束的数据。
  当空格数 emptyBlock 为 0,并且不存在相邻的格子相同的情况,即为游戏结束,此时结束标记gameOver 置位,并且绘制上gameOver 图片。

void gameOverCheck()
{if (emptyBlock != 0)return;for (int i = 0; i < 4; i++) {for (int j = 0; j < 4; j++) {if ((j + 1 < 4 && grid[i][j] == grid[i][j + 1])|| (i + 1 < 4 && grid[i][j] == grid[i + 1][j]))             return;}}gameOver = true;
}

6. 添加移动动画(难点)

  因为要添加动画,所以要修改 move() 函数,最基本实现中是一次得到最终的移动结果,考虑到各个方块需要移动的距离不一定相同等情况,采用每次只整体移动一格,然后绘制动画的方式。因为最多移动三格,所以遍历三次即可,把要移动的格子作标记。然后在偏移位置绘制相应的图片即可,具体实现看完整代码。
  三次遍历,每次遍历之间没什么不同,无法区分是否被合并过,所以要做合并标记,两个数都没合并标记才能合并,因为只能合并一次。在本次移动操作中,方块合并后就不会再移动。
  增加了整体移动标记,如果一次检测没有移动,那么直接结束检测。

  三次遍历,增加的是检测的工作,因为只有4x4大小,相对窗口几十万个像素的修改来说,无关紧要,耗时部分是动画,动画绘制次数依然不变。

七、完整版代码

  为了方便修改和整理,所以定义了一些宏和全局常量。

  由于库本身代码的原因,播放音乐时会有一帧的卡顿,所以示例程序中默认不播放合并音效版,使移动动画更为流畅。==

#include <graphics.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <math.h>//控制是否播放合并音效,0:关闭,1: 播放
#define ENABLE_PLAY_MERGE_MUSIC 0void load();               //加载资源
void loadImage();           //加载图片
bool loadRecord();          //读取游戏记录
void loadMusic();           //加载音乐
void gameSave();            //游戏保存
int scoring(int mergeNum);  //根据合并的数字计分
void addScore(int score);   //加分
void updateMaxMergeNum(int mergeNum);   //更新最大合成数字
void releaseImage();        //释放图片资源
void releaseMusic();        //关闭音乐文件,释放资源
void draw();                //绘制画面
void drawGameInfo();        //绘制游戏信息
void addRandomNum(int n);   //增加随机数字
bool move(int direct);      //按方向移动格子
void drawBlocks();          //绘制格子
void restart();             //重新游戏
void gameOverCheck();       //游戏结束检测//界面布局参数
const int AREA_LEFT = 20, AREA_TOP = 178, AREA_WIDTH = 500, AREA_HEIGHT = 500;
const int GRID_WIDTH = 106, DEVIDE = 15;
const int SCR_WIDTH = AREA_WIDTH + AREA_LEFT * 2, SCR_HEIGHT = AREA_HEIGHT + AREA_TOP + AREA_LEFT;//颜色参数
const color_t textScoreColor = EGERGB(241, 231, 214);
const color_t backgroundColor = EGERGB(250, 248, 239);//动画参数
const int animationDuration_ms = 420;  //移动动画持续时长(ms),指最远距离时//按钮点击判断
inline bool clickBtnRestart(int x, int y) {return (20 < x && x < 20 + 222) && (110 < y && y < 110 + 50);
}//图片
#define NUM_BLOCK 18
PIMAGE blockImgs[NUM_BLOCK];#define NUM_IMG 5
PIMAGE pimgs[NUM_IMG];
const int ID_IMG_BACKGROUND = 0, ID_IMG_LOGO = 1, ID_IMG_SCORE_BG = 2, ID_IMG_RESTART = 3;
const int ID_IMG_GAMEOVER = 4;//图片文件位置
const char* imgFileDirection = "./resource/image";
const char* imgFiles[NUM_IMG] = {"background.png", "gamelogo.png", "scorebg.png", "restart.png", "gameOver.png",
};//数据文件
const char* recordFileName = "game2048Record.txt";//音乐
MUSIC mergeMusic;
MUSIC startMusic;
const char* mergeMusicFile = "./resource/music/merge.mp3";
const char* startMusicFile = "./resource/music/start.mp3";const int  LEFT = 0, UP = 1, RIGHT = 2, DOWN = 3;
//方向偏移
const int dx[4] = { -1, 0, 1, 0 };
const int dy[4] = { 0, -1, 0, 1 };struct GameInfo
{int score;int topScore;int maxNum;
};
GameInfo gameInfo;int grid[4][4];           //格子
int emptyBlock = 16;   //空格子数
bool gameOver = false;int main()
{//注意要INIT_NOFORCEEXIT, 即关闭窗口不强制退出程序,以便进行游戏保存工作initgraph(SCR_WIDTH, SCR_HEIGHT, INIT_RENDERMANUAL | INIT_NOFORCEEXIT);setcaption("2048");setbkcolor(backgroundColor);setbkmode(TRANSPARENT);srand((unsigned int)time(0));delay_ms(0);       //刷新窗口load();startMusic.Play(0);gameOverCheck();int xClick, yClick;bool redrawFlag = true;for (; is_run(); delay_fps(60)) {//按键检测int direction = -1;int key = 0;bool restartGameFlag = false;while (kbmsg()) {key_msg keyMsg = getkey();if (keyMsg.msg == key_msg_down) {switch (keyMsg.key) {case 'A': case key_left: direction = 0; break;case 'W': case key_up:      direction = 1; break;case 'D': case key_right:   direction = 2; break;case 'S': case key_down:    direction = 3; break;}}else if (keyMsg.msg == key_msg_up) {key = keyMsg.key;}}//鼠标点击检测bool leftClick = false;while (mousemsg()) {mouse_msg mouseMsg = getmouse();if (mouseMsg.is_left() && mouseMsg.is_down()) { //左键按下leftClick = true;xClick = mouseMsg.x;yClick = mouseMsg.y;}}if (!gameOver) {// 游戏没有结束,处理移动操作if (direction != -1 && move(direction)) {addRandomNum(1);gameOverCheck();redrawFlag = true;}}else {// 游戏结束后,可以通过按回车键重新开始if (key == key_enter)restartGameFlag = true;}// 重新开始按钮的点击判断if (leftClick && clickBtnRestart(xClick, yClick)) {restartGameFlag = true;}if (restartGameFlag){restart();startMusic.Play(0);redrawFlag = true;}if (redrawFlag) {cleardevice();draw();redrawFlag = false;}}gameSave();releaseImage();releaseMusic();closegraph();return 0;
}void drawGameInfo()
{putimage_withalpha(NULL, pimgs[ID_IMG_LOGO], AREA_LEFT + 14, 30); //图标putimage_withalpha(NULL, pimgs[ID_IMG_SCORE_BG], 260, 10);          //游戏分数背景putimage_withalpha(NULL, pimgs[ID_IMG_RESTART], 20, 110);           //重新开始按钮//游戏分数setcolor(textScoreColor);setfont(30, 0, "黑体");xyprintf(370,  24, "%8d", gameInfo.topScore);xyprintf(370,  72, "%8d", gameInfo.score);xyprintf(370, 120, "%8d", gameInfo.maxNum);
}void draw()
{drawGameInfo();drawBlocks();if (gameOver) {setfillcolor(EGEACOLOR(0x60, WHITE));ege_fillrect(AREA_LEFT, AREA_TOP, AREA_WIDTH, AREA_HEIGHT);putimage_withalpha(NULL, pimgs[ID_IMG_GAMEOVER], 120, 400);}
}void restart()
{gameInfo.score = 0;gameOver = false;memset(grid, 0, 16 * sizeof(int));emptyBlock = 16;addRandomNum(2);
}void drawBlocks()
{putimage_withalpha(NULL, pimgs[ID_IMG_BACKGROUND], AREA_LEFT, AREA_TOP);for (int i = 0; i < 4; i++) {for (int j = 0; j < 4; j++) {int x = AREA_LEFT + (j + 1) * DEVIDE + j * GRID_WIDTH;int y = AREA_TOP + (i + 1) * DEVIDE + i * GRID_WIDTH;if (grid[i][j] != 0)putimage_withalpha(NULL, blockImgs[grid[i][j]], x, y);}}
}void gameOverCheck()
{if (emptyBlock != 0)return;for (int i = 0; i < 4; i++) {for (int j = 0; j < 4; j++) {if ((j + 1 < 4 && grid[i][j] == grid[i][j + 1])|| (i + 1 < 4 && grid[i][j] == grid[i + 1][j]))return;}}gameOver = true;
}void addRandomNum(int n)
{while ((emptyBlock > 0) && (n-- > 0)) {int randEmptyBlock = rand() % emptyBlock;    // 随机选取一个空格int i = 0, count = 0;int* gridList = &grid[0][0];// 对数组进行遍历,查找对应的空格(空格从0开始编号)for (i = 0; i < 4 * 4; i++) {if ((gridList[i] == 0) && (count++ == randEmptyBlock))break;}//随机数字2或4,0.9概率是1,0.1概率是2gridList[i] = (rand() % 10 < 1) ? 2 : 1;emptyBlock--;}
}bool move(int direction)
{//索引0~3分别对应移动方向左上右下//初始检测位置static int x0[4] = { 0, 0, 3, 0 }, y0[4] = { 0, 0, 0, 3 };// 分别对应四个移动方向的下一个元素的位置偏移(位置偏移与移动方向相反)static int elemOffset[4][2] = { {1, 0},{0, 1},{-1, 0}, {0, -1} };// 分别对应四个移动方向的下一行(列)的位置偏移(位置偏移与移动方向相反)static int lineOffset[4][2] = { {0, 1}, {1, 0}, {0, 1}, {1, 0} };bool blockMergeFlag[4][4] = { false };      //记录方块是否被合并过bool mergeFlag = false, movingFlag = false;clock_t startClock = clock();for (int check = 3; check > 0; --check) {int oldGrid[4][4];              //未移动前数据保存memcpy(oldGrid, grid, sizeof(int) * 4 * 4);bool blockMovingFlag[4][4] = { false };bool singleMovingFlag = false;int singleMaxMergeNum = 0;     // 单次移动最大合成数字int singleScore = 0;                  // 单次移动加分//整体单格移动for (int i = 0; i < 4; i++) {int xCur = x0[direction] + i * lineOffset[direction][0];int yCur = y0[direction] + i * lineOffset[direction][1];for (int nextPos = 1; nextPos < 4; nextPos++) {int xNext = xCur + elemOffset[direction][0];int yNext = yCur + elemOffset[direction][1];//寻找下一个非空方块if (grid[yNext][xNext] != 0) {//方块前为空格,前移if (grid[yCur][xCur] == 0) {grid[yCur][xCur] = grid[yNext][xNext];grid[yNext][xNext] = 0;//标记方块移动singleMovingFlag = blockMovingFlag[yNext][xNext] = true;}// 相等且没有参与合并过,则进行合并else if ((grid[yCur][xCur] == grid[yNext][xNext]) && (!blockMergeFlag[yCur][xCur])&& (!blockMergeFlag[yNext][xNext])) {++grid[yCur][xCur];grid[yNext][xNext] = 0;emptyBlock++;mergeFlag = blockMergeFlag[yCur][xCur] = true;singleMovingFlag = blockMovingFlag[yNext][xNext] = true;// 合并时计算分数,分值 = 2的n次方(n为方块的值)int num = 1 << grid[yCur][xCur];if (num > singleMaxMergeNum)singleMaxMergeNum = num;singleScore += scoring(num);}}xCur = xNext;yCur = yNext;}}// 是否有单格移动if (singleMovingFlag) {//移动动画cleardevice();drawGameInfo();setfillcolor(getbkcolor());const int totalDistance = (GRID_WIDTH + DEVIDE);double tBegin = 0.0, tEnd = 1.0;double dt = (tEnd - tBegin) / (animationDuration_ms / (1000.0 * (4 - 1))* 60.0);int lastPosLeft = 0;bool first = true;for (double t = tBegin; t < tEnd; t += dt) {if (fabs(t - tEnd) < 1E-8)break;int distance = round(t * totalDistance);bar(AREA_LEFT, AREA_TOP, AREA_LEFT + AREA_WIDTH, AREA_TOP + AREA_HEIGHT);   //清除区域putimage_withalpha(NULL, pimgs[ID_IMG_BACKGROUND], AREA_LEFT, AREA_TOP);  //绘制背景//绘制方块for (int i = 0; i < 4; i++) {int xLine = x0[direction] + i * lineOffset[direction][0];int yLine = y0[direction] + i * lineOffset[direction][1];for (int pos = 0; pos < 4; pos++) {int x = xLine + pos * elemOffset[direction][0];int y = yLine + pos * elemOffset[direction][1];if (oldGrid[y][x] != 0) {// 计算方块左上角的位置坐标int left = AREA_LEFT + (x + 1) * DEVIDE + x * GRID_WIDTH;int top = AREA_TOP + (y + 1) * DEVIDE + y * GRID_WIDTH;if (blockMovingFlag[y][x]) {left += distance * dx[direction];top  += distance * dy[direction];}putimage_withalpha(NULL, blockImgs[oldGrid[y][x]], left, top);}}}delay_jfps(60);}}// 移动动画完成后才更新分值addScore(singleScore);updateMaxMergeNum(singleMaxMergeNum);if (singleMovingFlag)movingFlag = true;else {  // 无法继续移动,退出循环break;}}#if ENABLE_PLAY_MERGE_MUSICif (mergeFlag && mergeMusic.GetPlayStatus() != MUSIC_MODE_PLAY)mergeMusic.Play(0);
#endifreturn movingFlag;
}void load() {loadImage();loadMusic();if (!loadRecord())restart();
}void loadMusic()
{mergeMusic.OpenFile(mergeMusicFile);startMusic.OpenFile(startMusicFile);
}bool loadRecord()
{FILE* fp = fopen(recordFileName, "r");if (fp == NULL)return false;int topScore, score, maxNum;if (fscanf(fp, "topScore:%d score:%d maxNum:%d", &topScore, &score, &maxNum) != 3) {fclose(fp);return false;}gameInfo.topScore = topScore;gameInfo.score = score;gameInfo.maxNum = maxNum;for (int i = 0; i < 4; i++) {for (int j = 0; j < 4; j++) {int readInCount = fscanf(fp, "%d", &grid[i][j]);// 读取数据出错或者数据无效if ((readInCount != 1) || (grid[i][j] < 0) || (NUM_BLOCK <= grid[i][j])) {fclose(fp);return false;}if (grid[i][j] != 0)emptyBlock--;}}fclose(fp);return true;
}int scoring(int mergeNum)
{return mergeNum;
}void addScore(int score)
{gameInfo.score += score;if (gameInfo.score > gameInfo.topScore)gameInfo.topScore = gameInfo.score;
}void updateMaxMergeNum(int mergeNum)
{if (mergeNum > gameInfo.maxNum)gameInfo.maxNum = mergeNum;
}void gameSave()
{//数据写入FILE* fp = fopen(recordFileName, "w");if (fp == NULL)return;fprintf(fp, "topScore:%d\nscore:%d\nmaxNum:%d\n",gameInfo.topScore, gameInfo.score, gameInfo.maxNum);for (int i = 0; i < 4; i++) {for (int j = 0; j < 4; j++)fprintf(fp, "%d ", grid[i][j]);fprintf(fp, "\n");}fclose(fp);
}void loadImage()
{//创建一个可以容纳生成的字符串的字符数组,用于保存图片路径char imgPath[64];//获取图片for (int i = 0; i < NUM_IMG; i++) {//生成图片文件名,存储到imgName[]中sprintf(imgPath, "%s/%s", imgFileDirection, imgFiles[i]);pimgs[i] = newimage();getimage(pimgs[i], imgPath);}//获取数字图片for (int i = 1, num = 2; i < NUM_BLOCK; i++, num *= 2) {sprintf(imgPath, "%s/block_%d.png", imgFileDirection, num);blockImgs[i] = newimage();getimage(blockImgs[i], imgPath);}
}void releaseImage()
{//释放所有图片资源for (int i = 0; i < NUM_BLOCK; i++)delimage(blockImgs[i]);for (int i = 0; i < NUM_IMG; i++)delimage(pimgs[i]);
}void releaseMusic()
{if (mergeMusic.IsOpen())mergeMusic.Close();if (startMusic.IsOpen())startMusic.Close();
}

EGE专栏:EGE专栏

EGE示例程序——2048相关推荐

  1. EGE示例程序——花火闪烁的夜晚

    专栏:EGE专栏 专栏:EGE示例程序 示例程序下载 花火闪烁的夜晚 站点 链接 百度网盘 示例一 花火闪烁的夜晚 CSDN 示例一 花火闪烁的夜晚 (无需积分) 一.烟花   在做烟花特效前,先来看 ...

  2. EGE示例程序——分形

    EGE专栏:EGE专栏 EGE示例--分形 目录 一.分形 二.康托尔集(Cantor Set) 1. Koch雪花曲线 三.曼德布罗特集(Mandelbrot) 可局部放大的曼德布洛特集 四.谢尔宾 ...

  3. STM32 之三 标准外设版USB驱动库详解(架构+文件+函数+使用说明+示例程序)

    写在前面 目前,ST的USB驱动有两套,一套是早期的独立版USB驱动,官方培训文档中称为Legacy library:一套为针对其Cube 系列的驱动,根据芯片不同可能有区别,具体见对应芯片的Cube ...

  4. MindSpore部署图像分割示例程序

    MindSpore部署图像分割示例程序 本端侧图像分割Android示例程序使用Java实现,Java层主要通过Android Camera 2 API实现摄像头获取图像帧,进行相应的图像处理,之后调 ...

  5. BizTalk 2006 简单入门示例程序(附源项目文件下载)

    BizTalk 2006 简单入门示例程序(附源项目文件下载) 为初学BizTalk Server 2006的开发人员,提供一个简单入门的示例程序,包括一个Receive Port.Send Port ...

  6. 基于Struts2.3.x+Spring3.2.x+Hibernate4.2.x+EasyUI1.3.4+Maven架构的示例程序

    基于Struts2.3.x+Spring3.2.x+Hibernate4.2.x+EasyUI1.3.4+Maven架构的示例程序 不知道为什么,保存的时候显示有一个连接为违禁内容,可能是----. ...

  7. 如何编译ReactNative示例程序Examples

    通过示例程序可以看到一些基本组件的使用,对于学习ReactNative是很有帮助的. 编译示例程序需要将整个项目导入到androidStudio中,androidStudio导入项目时选择react- ...

  8. ASP.NET AJAX示例程序:实现IDragSource和IDropTarget接口将商品拖放至购物车中

    本文来自<ASP.NET AJAX程序设计--第II卷:客户端Microsoft AJAX Library相关>第9章第3节. 9.3 示例程序:实现IDragSource和IDropTa ...

  9. Linux下的示例程序

    linux下的示例程序 #if 0 /*  * 1. 遍历目录-1  */ #include <stdio.h> #include <dirent.h> #include &l ...

  10. python推荐系统-用python写个简单的推荐系统示例程序

    用python写个简单的推荐系统示例程序 作者:阿俊 发布于:2011-11-26 16:03 Saturday 分类:推荐系统 python这门语言写程序代码量非常少,短短几行就可以把程序写的很清楚 ...

最新文章

  1. 微软和谷歌分别开源分布式深度学习框架,各自厉害在哪?
  2. word2vec模型评估_干货 | NLP中的十个预训练模型
  3. 读取MySQL初始化配置_MySQL 初始化配置
  4. SSM 框架 Maven项目整合实例
  5. 幸福指数测试软件,测试你和ta的幸福指数能不能爆表
  6. 为什么按照 Angular 官网教程执行简单的测试代码,会遇到expect is not defined的错误消息
  7. 3.2 Lucene实战:一个简单的小程序
  8. 大话数据结构之数据结构
  9. HDU2006 求奇数的乘积【入门+序列处理】
  10. wps 插件_【追加功能】OFFICE插件管理工具重整后再上路,更好用易用。
  11. 4G无线预付费电表系统设计及其应用
  12. imageJ的二次开发(全)
  13. xp系统更改计算机名c盘,c盘满了怎么办,小编教你电脑xp的c盘满了怎么办
  14. 将源码打包成deb软件包
  15. 2222222222222
  16. 【蓝桥杯集训100题】scratch勾股数 蓝桥杯scratch比赛专项预测编程题 集训模拟练习题第20题
  17. 【一头扎进JMS】(4)----RabbitMQ概述
  18. 一文深度剖析扩散模型究竟学到了什么?
  19. 面对200多人演讲是一种什么体验?
  20. Spring rebooted --重新认识Spring

热门文章

  1. java实现modbus rtu协议与 modscan等工具(2)
  2. 项目01——图书进、销、存(jxc)系统(单机版)
  3. php7 libiconv,CentOS 7下编译libiconv
  4. 【C++ 程序】 解线性方程组(Cramer法则)(分数形式结果)
  5. LSTM神经网络和GRU
  6. 分析Python7个爬虫小案例(附源码)
  7. java的几个设计模式
  8. Spring源码下载编译全过程!超详细的步骤!!!
  9. 《FLUENT 14流场分析自学手册》——1.5 湍流模型
  10. 利用THINKPHP框架开发的自定义表单及数据字典模板