EGE专栏:EGE专栏
https://pan.baidu.com/s/1BUDGLeenbIxpAfqd1XfNqg
源代码分享于 百度网盘,里面也有程序用到的资源文件,可以查看下载。
https://download.csdn.net/download/qq_39151563/12154829
CSDN资源,设定为0积分,无需积分即可下载。
2048在线游戏链接: https://2048game.com/
2048 游戏规则
2048小游戏相信应该都玩过,规则很简单,上面也贴出了在线游戏链接,可以先玩一玩,了解一下游戏规则。
计分规则
每合成一个方块加进行加分, 分值 = 合成的方块上的数字。
下面EGE制作的2048界面
网上有很多 Android 手机的游戏,想要相关的图片和音乐资源的可以先下载应用安装包,然后用 apktool 解析资源文件,得到里面的素材。(apktool 下载使用方法可以自行百度)
示例程序链接中也放有我从一款2048游戏中解析得到的素材。(这款游戏一大堆广告,各种弹窗,真的**)
一般别人做的游戏都比较花里胡哨的,东西很多,可以从中挑选一些自己用到的。有些图片尺寸不对,可以自行用PS, 或者其它的图像处理软件缩放一下,调成合适的尺寸, 并且改成合适文件名(可以在用到的时候再选取,并取好点的文件名)
。
下面则是我从中挑出的所需要的图片和音乐素材, 并且对图片缩放过,以适配界面尺寸。
素材链接:https://download.csdn.net/download/qq_39151563/12154829
这时候考虑窗口的大小,界面的布局,在哪里显示什么内容,界面跳转等等。
再来说一下笔记本显示比例的问题,我笔记本是125%放大显示,估计一般的笔记本都是这样。因为对于笔记本来说,设置为100%的显示比例的时候,界面上的文字和图标真的很小。
所以如果设置EGE窗口是 500x500, 那么你将窗口截屏,会发现截下来的图片,分辨率约为 625 * 625。如果你看到屏幕上某个尺寸挺合适,截图下来后查看分辨率,需要除以缩放比例才能得到原图的大小,以这样的大小绘制,才会得到想要的尺寸。
基本实现
(难点)
附加项
(自娱自乐,没有也行)
(16个格子满,且相邻格子都不同)
(难点)
(常用)
(必备)
即仅仅实现基础功能:在 4 × 4 4\times 4 4×4 格子中移动合并数字,并随机出现数字,游戏界面只有4x4个格子。不计分,不作游戏结束判断,不存档,无动画效果,无音效。
4x4 格子用二维数组表示即可。
int grid[4][4];
由于方块上数字是 2 , 4 , 8 , 16 , . . . 2, 4, 8, 16,... 2,4,8,16,...,是 2 2 2的 n n n次方, 所以可以考虑存储 n n n 即可, 这样方便编号,特别是图片, 编号 n n n从 1 1 1到 17 17 17,分别对应数字 2 n \ 2^n 2n ,空格用 0 0 0表示。因为元素就是编号,所以绘图时直接根据元素绘制即可。
为什么是1到17?
因为随机出现的数字最大是4, 即 2 的2次方,从4开始,一共能排上16个数字,加上2,那么一共17个数字,即可能出现的最大数字是 2 17 = 131072 2^{17} = 131072 217=131072。(牛逼牛逼,131072只存在于理论上吧)
如下图所示:
上图每个方块所对应的存储数据分别为:
即如果方块的值为 2 n 2^n 2n,那么存储的数值为 n n n。
既然数字 2 2 2 到 131072 131072 131072, 分别对应编号 1 1 1 到 17 17 17, 那么图片数据用长度为 18 18 18 的 PIMAGE 数组 blockImgs[] 存储即可, 方块 2 n 2^{n} 2n 的图片就存储于blockImgs[n],方块 2 , 4 , ⋯ , 131072 2, 4, \cdots, 131072 2,4,⋯,131072 分别存储于 blockImgs[1], blockImgs[2], … , blockImgs[17], 一共18个,blockImgs[0]不使用。另外还需要存储一张 4 × 4 4\times 4 4×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");
}
这是整个游戏最核心的部分。
首先移动需要检测按键,常用 AWDS 和四个方向键。
只需要一个变量 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的值进行移动。
移动时需要根据移动方向来检测数字,向左移动,那么就要对每一行,从左往右检测,即移动方向和检测方向是相反的,因为在前面的会优先合成。
下图中的左边部分即为左移时的检测顺序,包含初始位置,下一个元素的位置偏移以及下一行(列)的位置偏移。
四个方向,区别就是检测起点不同,检测方向不同,由此可以根据四个方向,得到这些数据。
(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;
}
根据空格数 emptyBlock, 生成一个 0 0 0到 empty-1 之间的随机数randEmptyBlock ,然后查找第randEmptyBlock个空格(从0开始编号), 往这个空格里添加一个2或4的方块,出现2的概率应该是大于4的,出现4的情况很少, 这里取 0.9 0.9 0.9 的概率出现数字 2 2 2, 0.1 0.1 0.1 的概率出现数字 4 4 4。参数 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概率是2
gridList[i] = (rand() % 10 < 1) ? 2 : 1;
emptyBlock--;
}
}
综合上面,得到下面的代码, 共160行。
图片放在 “./resource/image” 目录,并且数字图片命名格式为 “block_数字.png”, 背景图片命名为 “background.png”。
#include
#include
#include
#include
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概率是2
gridList[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();
}
程序界面截图
使用的素材
重新开始按钮
在图中添加了重新开始按钮,当检测到鼠标左键点击时,就判断点击位置是否在区域内。
下面代码为判断点击位置是否在按钮区域,因为只有一个按钮,所以直接取了固定值。
//按钮点击判断
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);
}
(音效可以不添加,因为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;
}
分数分为 最高分数,当前分数,最大合成数字
因为计分是出现在合并的时候,所以在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);
数据文件名
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);
}
游戏结束,那必定是在出现随机数后,或者一开始读取的记录就是已经结束的数据。
当空格数 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;
}
因为要添加动画,所以要修改 move() 函数,最基本实现中是一次得到最终的移动结果,考虑到各个方块需要移动的距离不一定相同等情况,采用每次只整体移动一格,然后绘制动画的方式。因为最多移动三格,所以遍历三次即可,把要移动的格子作标记。然后在偏移位置绘制相应的图片即可,具体实现看完整代码。
三次遍历,每次遍历之间没什么不同,无法区分是否被合并过,所以要做合并标记,两个数都没合并标记才能合并,因为只能合并一次。在本次移动操作中,方块合并后就不会再移动。
增加了整体移动标记,如果一次检测没有移动,那么直接结束检测。
三次遍历,增加的是检测的工作,因为只有4x4大小,相对窗口几十万个像素的修改来说,无关紧要,耗时部分是动画,动画绘制次数依然不变。
为了方便修改和整理,所以定义了一些宏和全局常量。
由于库本身代码的原因,播放音乐时会有一帧的卡顿,所以示例程序中默认不播放合并音效版,使移动动画更为流畅。==
#include
#include
#include
#include
#include
//控制是否播放合并音效,0:关闭,1: 播放
#define ENABLE_PLAY_MERGE_MUSIC 0
void 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概率是2
gridList[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_MUSIC
if (mergeFlag && mergeMusic.GetPlayStatus() != MUSIC_MODE_PLAY)
mergeMusic.Play(0);
#endif
return 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专栏