十二、实现僵尸和植物碰撞效果(僵尸吃植物)
首先,对植物结构体进行增加俩个成员,deadtime表示吃几次植物会死亡,eated表示植物的状态(被吃状态),植物和僵尸进行碰撞后,植物状态变为被吃;在种植植物时,将map[row][col].eated=false;;同样的,创建僵尸时僵尸结构体成员bool eating=false,植物和僵尸进行碰撞后,僵尸状态状态变为吃,当僵尸处于吃状态,则僵尸吃的动作帧改变。
struct plant { int type;//0表示没有植物,1表示选中第一种植物,2表示选中第二种植物 int frameIndex;//序列帧的序号 bool eated;//是否被吃 int deadtime;//死亡计数 int timer;//喷射阳光的计时器 int x, y; };
像第十一节一样
第一步,将初始化(加载)僵尸吃植物图片(注:植物被吃时图像画面没有状态改变,不需要初始化)
//初始化僵尸吃东西的图片 for (int i = 0; i < 21; i++) { sprintf_s(plantname, sizeof(plantname), "res/zm_eat/%d.png", i + 1); loadimage(&imgeat[i], plantname); }
第二步,写僵尸和植物的碰撞函数
void collisioncheck() { checkbullet_to_zm();//子弹对僵尸的碰撞检测 checkzm_to_plant();//僵尸对植物的碰撞检测 }
首先,先找一个未死且在使用的僵尸,若僵尸所在行有植物,且僵尸嘴部位置在植物左右区间位置时,若僵尸是正常行走状态,则僵尸停下来,状态变成吃植物状态,植物变成被吃状态;若僵尸处于吃植物状态,累计吃植物状态50次,植物消失,植物被吃状态解除(因为这里被吃状态解除,所以之前种植植物时初始化植物不被吃状态可以省略),僵尸速度恢复。
注:这里我们并没有用植物状态作为判断条件,所以可以省略植物结构体中植物状态的成员
void checkzm_to_plant() { int zmmax = sizeof(zms) / sizeof(zms[0]); for (int i = 0; i < zmmax; i++) { if (zms[i].use ==false|| zms[i].dead) continue;//若该僵尸未使用或者死亡,则判断下一个僵尸 int row = zms[i].row; for (int j = 0; j < 8; j++) { //if (map[row][j].type == 0) // continue;//僵尸所在行的j列没有植物,检测下一列 if (map[row][j].type > 0)//僵尸所在行有植物 { int plantx = 261 + 81 * j; int x1 = plantx + 10;//植物左右距离 int x2 = plantx + 60; int x3 = zms[i].x + 80;//僵尸嘴巴位置 if (x3 > x1 && x3 < x2)//僵尸和植物发生碰撞 { if (zms[i].eating)//正在吃,修改数据 { mciSendString("play res/audio/zmeat.mp3", 0, 0, 0); map[row][j].deadtime++; if (map[row][j].deadtime > 50) { map[row][j].deadtime = 0; map[row][j].type = 0;//植物消失 zms[i].eating = false; zms[i].frameIndex = 0; zms[i].speed = 1; mciSendString("play res/audio/plantDead.mp3", 0, 0, 0); } } else { map[row][j].eated = true; map[row][j].deadtime = 0; zms[i].eating = true; zms[i].speed = 0; zms[i].frameIndex = 0; } } } } } }
第三步,通过碰撞检测后,僵尸和植物状态发生变化,此时以植物和僵尸碰撞后的状态为条件来进行判断何时发生僵尸吃植物图片的帧序号变化
在updatezm()中更改僵尸吃植物图片帧序号
//更新僵尸图片帧序号 static int count2 = 0; count2++; if (count2 > 1) { count2 = 0; for (int i = 0; i < zmmax; i++) { if (zms[i].use) { if (zms[i].dead) { zms[i].frameIndex++; if (zms[i].frameIndex >= 20) { zms[i].use = false;//变灰烬结束,回收僵尸 } } else if (zms[i].eating) { zms[i].frameIndex = (zms[i].frameIndex + 1) % 21; } else//僵尸正常行走状态 { zms[i].frameIndex = (zms[i].frameIndex + 1) % 22 ;//从下标1开始,因初始化下标0 } } }
最后对僵尸吃植物的图片进行渲染,对原来的僵尸图片的渲染函数进行修改。
在updatewindow()进行渲染动作
//渲染僵尸图片 int zmmax = sizeof(zms) / sizeof(zms[0]); for (int i = 0; i < zmmax; i++) { if (zms[i].use) { //IMAGE* img = &imgzm[zms[i].frameIndex]; //IMAGE* img = ((zms[i].dead) ? imgzmdead : imgzm); IMAGE* img = NULL; if (zms[i].dead) { img = imgzmdead; } else if (zms[i].eating)//正在吃 { img = imgeat; } else { img = imgzm; } img = img + zms[i].frameIndex;//指针跳过几个IMAGE数据类型大小 putimagePNG(zms[i].x, zms[i].y-45,img); } }
运行程序,我们看一下效果
十三、片头巡场
第一步,屏幕长度是900个像素,图片大小是1400个像素,图片从(0,0)位置一直移动到(-500,0)位置处,每次移动3个像素,停顿1毫秒,这样初步就实现转场,但是没有僵尸,这怎么办呢?我们创建一个vector2类型的结构体,用来储存9个僵尸的位置,然后让僵尸和背景图片同频率移动,就可以看见僵尸出现,此时僵尸是不动的,僵尸出场时是各自摇摆抖动的,所以我们修改站立僵尸图片的帧序号,为了不抖动频率高,我们使用了控制频率技巧,为了使僵尸抖动姿势不同,对于开始初始的站立图片帧做了处理(每个僵尸起始帧图片为随机帧数站立图片)。
第二步,转到最后的位置会停留一会,这个照葫芦画瓢,渲染背景图片,渲染僵尸站立图片,并改变僵尸站立帧序号。
最后一步,是场景转回来,这个就很简单了,将转过去的代码复制下来,位移方向进行修改就好了
在此之前,我们先创建一个站立僵尸图片数组并且初始化
void viewscence() { //PlaySound("res/audio/cannotselect.wav", NULL, SND_FILENAME | SND_ASYNC); mciSendString("play res/audio/Kitanai Sekai.mp3", 0, 0, 0); int xmin =WIN_WIDTH - imgBg.getwidth();//900-1400 vector2 points[9] = {{560,80},{530,160},{630,170},{540,200},{520,270},{598,290},{610,340},{710,299},{700,340} }; int initposture[9]; for (int i = 0; i < 9; i++) { initposture[i] = rand() % 11;//起始站姿图片序号 } int count=0; for(int x = 0; x >= xmin; x -=3)//每次移动2个像素 { BeginBatchDraw(); putimage(x, 0, &imgBg); count++; for (int j = 0; j < 9; j++) { putimagePNG(points[j].x - xmin + x, points[j].y, &imgzmstand[initposture[j]]); if (count > 9)//说明移动18帧,站姿图片到下一张 { initposture[j] = (initposture[j] + 1) % 11;//变换下一帧 } } if (count > 9) { count = 0; } Sleep(1);//每次移动俩像素停顿3毫秒 EndBatchDraw(); } //停留1S左右 count = 0; for (int i = 0; i < 30; i++) { BeginBatchDraw(); putimage(xmin, 0, &imgBg); count++; for (int j = 0;j < 9; j++) { putimagePNG(points[j].x, points[j].y, &imgzmstand[initposture[j]]); if (count > 2) { initposture[j] = (initposture[j] + 1) % 11;//变换下一帧 } } if (count > 2) { count = 0; } EndBatchDraw(); Sleep(15); } count = 0; for (int x = 0; x >= xmin; x -= 3)//每次移动2个像素 { BeginBatchDraw(); putimage(xmin-x, 0, &imgBg); count++; for (int j = 0; j < 9; j++) { putimagePNG(points[j].x - x, points[j].y, &imgzmstand[initposture[j]]); if (count >9 )//说明移动18帧,站姿图片到下一张 { initposture[j] = (initposture[j] + 1) % 11;//变换下一帧 } } if (count > 9) { count = 0; } Sleep(1);//每次移动俩像素停顿1毫秒 EndBatchDraw(); } }
十四、判断游戏输赢
为了方便我们枚举输赢,创建评判输赢的条件,杀十个僵尸就胜利,游戏状态变量
#define ZM_MAX 10 enum{GOING,WIN,FAIL}; int killcount;//已经杀掉的僵尸个数 int zmcount;//已经出现的僵尸个数 int gamestatus;
初始化这些变量
当出现僵尸个数等于10,就不创建了,表示把这出现的全部杀完就结束游戏,当然这句话也可以不加。
僵尸到家时,游戏结束,游戏状态变为失败
僵尸死亡时,则记录死亡个数加1,当僵尸死亡个数到10个,即游戏状态为胜利
接下来在主函数中判断游戏游戏胜利或者失败,每次更新数据后判断,失败或者胜利都退出循环
(1)若游戏状态为胜利,让胜利场景停2秒,在渲染胜利图片
(2)若游戏状态为失败,让失败场景停2秒,在渲染失败图片
bool checkover() { int ret = false; if (gamestatus == WIN) { mciSendString("close res/audio/UraniwaNi.mp3 ", 0, 0, 0); mciSendString("play res/win.mp3", 0, 0, 0); Sleep(2000); loadimage(0, "res/gameWin.png"); ret = true; } else if (gamestatus == FAIL) { mciSendString("close res/audio/UraniwaNi.mp3 ", 0, 0, 0); mciSendString("play res/lose.mp3", 0, 0, 0); Sleep(2000); loadimage(0, "res/fail2.png"); ret = true; } return ret; }
十五、总结
当然还有卡牌及工具栏自动下降没有实现,以及游戏音效没有
我们先说下音效
windows有俩个播放音乐的函数:mciSendString和PlaySound
使用这俩个函数前需要包含一个头文件和加载静态库
(1)mciSendString("close res/audio/UraniwaNi.mp3 ", 0, 0, 0)
mciSendString的函数使用格式如上,第一个参数用来发出指令,一共四个: open 打开音乐文件 play 播放音乐 repeat 重复播放 close 关闭音乐文件
我们插入背景音乐,一般用到play和close(保证音乐在某个区间播放)
注意的是repeat一般放在音乐文件路径后面,其他的在前面
mciSendString支持的格式有mp3
但是它需要前面一个音乐播放完,才能播放下一个音乐,不能实现音效重叠效果
(2)PlaySound函数的用法如下图,支持wav格式音乐,而且可以实现音乐重叠,在收集阳光时连续点击都要有声音,就用PlaySound函数
卡牌及工具栏下降实现还是蛮容易的,这里就不过多叙述了
完整的代码及素材链接: