来源:微信公众号「编程学习基地」
终极目标:打造多功能拼图游戏
制作环境: VS2015
支持VC++2010,VS各个版本 easyx图形库
拼图这个游戏之前有分享过,但我觉得不是很完美,没有对应的算法支持,还有人吐槽背景图片太low,没办法,就改了点东西,优化了拼图打乱顺序算法,新增自定义背景图片
游戏规则就是点击空白图片上下左右的图片与之交换,最终将散乱的图片拼成原图,这样游戏就胜利了。
第一步获取鼠标点击的图片,或者说获取鼠标点击的位置。
Easyx图形库给出了鼠标消息 MOUSEMSG 对象,可以通过GetMouseMsg()这个函数将鼠标点击消息存储在MOUSEMSG 对象里面。
MOUSEMSG msg; //鼠标消息
msg = GetMouseMsg(); //获取鼠标消息
获得了鼠标点击的位置怎样判断玩家点击的是那张图片呢!Easyx图形库给出的是像素坐标,怎样将像素转换成二维数组下标。
这个时候我们需要第二步操作:
col = msg.x / 100;
if (msg.x == 400)
col = 3;
row = msg.y / 100;
if (msg.y == 400)
row = 3;
这个if判断,因为400/100=4,而我们4*4数组没有4这个下标,只能将这个不稳定因素排除掉。
判断输赢就是每张图片在对应在对应的位置上。这里我用逆序数为0判断输赢。
第三步就是打乱图片顺序,我参考了大量网上资源,发现利用线性代数里面一个概念:逆序数 来判断拼图游戏是否可以完成拼图。
我们先将初始化一个乱序的map数据,然后再来判断是否可以完成拼图游戏。
int index; //a数组的下标
int size = 15; //a数组的元素个数
int a[15] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14 };
for (int i = 0; i < 4; i++)
{
for (int j = 0; j < 4; j++)
{
//右下角空白可交换图片不动
if (i == 3 && j == 3) continue;
//从15个数字中随机取一个
index = rand() % size;
//将随机抽取d额数字逐个放入地图中
map[i][j] = a[index];
//将抽取的位置后面d额数字往前挪一位
for (int k = index; k < size; k++)
{
a[k] = a[k + 1];
}
//控制数组长度 抽取了一张图片长度减一
size--;
}
}
如果没有逆序数的判断,你制作的拼图游戏很可能出现无法通关的情况(亲测),网上的拼图教程大都有无法过关的情况。
函数计算逆序数:
int inverseNumber()
{
int sum = 0, k = 0; //sum为逆序数
int arr[ N * N ];
//将map里面的数据导入到一维数组arr里面去
for (int i = 0; i < N; i++)
{
for (int j = 0; j < N; j++)
{
arr[k++] = map[i][j];
}
}
//求逆序数
for (int i = 1; i < N * N; i++) //从第二个数起有逆序数
{
for (int j = 0; j < i; j++) //循环遍历 第i个数 前面的数
{
if (arr[i] < arr[j])
{
sum++;
}
}
}
return sum; //返回逆序数
}
1、加载游戏数据(初始化 GameInit();)
2、绘制图形(绘图 DrawMap();)
3、玩家操作(数据更新 play();)
GameInit(); //初始化游戏
while(1)
{
RenderGame(); //绘制游戏
UpdateGame(); //数据更新
}
int main()
{
initgraph(N * 100, N * 100); //初始化窗口大小为400*400
init(); //加载资源
GameInit(); //游戏初始化
DrawMap(); //游戏渲染
while (1)
{
play(); //玩家操作
Judg(); //判断输赢
}
closegraph(); //关闭窗口
return 0;
}
主函数里面构建好游戏框架,原理就是游戏三部曲,重点是这些函数的实现
void init()
{
//加载资源图片 4张图片4个关卡
loadimage(&img[0], L"images/1.jpg", 400, 400);
loadimage(&img[1], L"images/2.jpg", 400, 400);
loadimage(&img[2], L"images/3.jpg", 400, 400);
loadimage(&img[3], L"images/4.jpg", 400, 400);
//设置最后一张图片为空白图片,作为目标图片
loadimage(&imgs[15], L"images/15.jpg", 100, 100);
/********播放音乐********/
//文件路径采用相对路径 alias 取别名
mciSendString(L"open images/music/爱河.mp3 alias back", nullptr, 0, nullptr);
mciSendString(_T("play back repeat"), 0, 0, 0); //repeat循环播放
srand((unsigned)time(NULL)); //设置随机种子
}
void GameInit()
{
//把拼图图片贴上去
putimage(0, 0, &img[NUM]);
//设置绘图目标为 img 对象 对拼图图片进行切割
SetWorkingImage(&img[NUM]);
for (int y = 0, n = 0; y < N; y++)
{
for (int x = 0; x < N; x++)
{
if (n == 15) break;
//逐个获取100*100像素图片 存储在imgs里面
getimage(&imgs[n++], x * 100, y * 100, (x + 1) * 100, (y + 1) * 100);
}
}
//设置绘图目标为绘图窗口
SetWorkingImage();
map[N - 1][N - 1] = 15;
do
{
int index; //a数组的下标
int size = 15; //a数组的元素个数
int a[15] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14 };
for (int i = 0; i < 4; i++)
{
for (int j = 0; j < 4; j++)
{
//右下角空白可交换图片不动
if (i == 3 && j == 3) continue;
//从15个数字中随机取一个
index = rand() % size;
//将随机抽取d额数字逐个放入地图中
map[i][j] = a[index];
//将抽取的位置后面d额数字往前挪一位
for (int k = index; k < size; k++)
{
a[k] = a[k + 1];
}
//控制数组长度 抽取了一张图片长度减一
size--;
}
}
} while (inverseNumber() % 2); //当 map里面的数据 不是偶排列时执行上面循环
}
这里的打乱图片顺序利用的就是线性代数里面逆序数的奇排列和偶排列的性质。
详情百度逆序数或者在线性代数课本上找排列及其逆序数这一小节,一般在课本的第一章,很小的一个知识点,如果不是查阅相关信息我都想不起来。
所以说数学越好的程序员越牛逼,这不是空口白话。
绘制地图就是调用putimage()函数,只需要确定贴图的位置。
void DrawMap()
{
BeginBatchDraw(); //开始渲染图片
for (int y = 0; y < N; y++)
{
for (int x = 0; x < N; x++)
{
putimage(x * 100, y * 100, &imgs[map[y][x]]);
}
}
EndBatchDraw(); //结束渲染图片
}
数据更新就是不断获取鼠标操作,对鼠标的操作进行相应的响应
void play()
{
int col, row; //鼠标点击的位置 行 列
MOUSEMSG msg; //鼠标消息
msg = GetMouseMsg(); //获取鼠标消息
switch (msg.uMsg) //对鼠标消息进行匹配
{
case WM_LBUTTONDOWN: //当鼠标消息是左键按下时
//获取鼠标按下所在列
col = msg.x / 100;
if (msg.x == 400)
col = 3;
//获取鼠标按下所在行
row = msg.y / 100;
if (msg.y == 400)
row = 3;
//得到目标所在行和列
for (int i = 0; i < N; i++)
{
for (int j = 0; j < N; j++)
{
if (map[i][j] == 15) //空白处为交换目标
{
aim_r = i;
aim_c = j;
}
}
}
//判难鼠标点击位置和目标是否相邻,相邻交换数据
if (row == aim_r && col == aim_c + 1 ||
row == aim_r && col == aim_c - 1 ||
row == aim_r + 1 && col == aim_c ||
row == aim_r - 1 && col == aim_c){
//鼠标点击图片和空白目标图片交换
map[aim_r][aim_c] = map[row][col];
map[row][col] = 15;
}
DrawMap();
break;
case WM_RBUTTONDOWN: //当鼠标消息是右键按下时
putimage(0, 0, &img[NUM]); //将关卡图片贴到窗口上
break;
case WM_RBUTTONUP: //当鼠标消息是右键抬起时
DrawMap(); //重新绘制地图
break;
case WM_MBUTTONDOWN:
{
//打开文件夹调用的是windows提供的API,代码是文档里面查找的
OPENFILENAME ofn;
wchar_t szFile[260];
//初始化ofn数据
ZeroMemory(&ofn, sizeof(ofn));
ofn.lStructSize = sizeof(ofn);
ofn.lpstrFile = szFile;
ofn.lpstrFile[0] = '\0';
ofn.nMaxFile = sizeof(szFile);
ofn.lpstrFilter = L"All\0*.*\0jpg\0*.jpg\0";
if (GetOpenFileName(&ofn) == TRUE)
{
loadimage(&img[NUM], szFile, 400, 400);
GameInit(); //游戏初始化
DrawMap(); //渲染地图
}
}
break;
}
}
在这里我添加了自定义图片功能,按下鼠标中键可以选择本地图片来自定义背景图片。
判断输赢只需要保证每张图片在指定位置就胜利,没有失败。这里梦凡利用的是逆序数为0判断是否获得胜利。
当然你也可以设置一个时间,当玩家在规定时间还没有完成游戏视为失败。
void Judg()
{
//判断当每张图片是否在对应位置
if (inverseNumber() == 0) //逆序数为0就是数组按照从小到大的排列顺序
{
//挑战成功之后将全图贴上
putimage(0, 0, &img[NUM++]);
//四个关卡都胜利之后退出程序
if (NUM == 4){
MessageBox(GetHWnd(), L"挑战成功", L"Vectory", MB_OK);
exit(0);
return;
}
//没过一个关卡判断是否进入下一个关卡
if(MessageBox(GetHWnd(), L"是否进入下一关", L"Vectory", MB_YESNO) == IDYES){
//重新开始游戏
GameInit(); //游戏初始化
DrawMap(); //渲染地图
}
//退出游戏
else exit(0);
}
}
这个程序时通过num变量来控制关卡的,每次胜利之后num++,然后判断玩家是否想要进入下一关,进入下一关就重新初始化游戏,开始新的一轮的挑战,不想继续就exit(0)正常退出程序。
拼图游戏完成了,可是总觉得缺点什么。玩游戏怎能缺少音乐呢,C++播放音乐的方式,如何利用windows里面的API播放你的音乐,让你的游戏拥有灵魂。
什么是程序打包?
你安装软件的时候有安装向导,就是把可执行文件(.exe文件)和依赖库(包括素材)保存在一个目录下,并创建桌面链接。
我看了N篇拼图的文章,终于找到了打乱图片顺序的方法,尽管这个方法用的是线性代数里面的知识,但只是一个很简单的知识点,大家可以了解一下,无论是否学过线性代数这门课程。
经典推荐:
推箱子项目传送门:推箱子教程
贪吃蛇项目传送门:贪吃蛇教程
扫雷项目传送门 :扫雷教程
C语言修炼宝典传送门:数组篇