书接上文,这几天完成了以下任务:
1.阳光:随机掉落,收集动画,不捡拾消失功能。
2.僵尸:随机生成,行走动画。
3.豌豆子弹:发射判定,发射动画,碰撞检测,爆炸特效等。
同时对游戏制作的基本框架有了一个更加深入的认识。
今天主要聊下对象的处理。
例如僵尸,我们需要先创建一个结构体储存僵尸的基本信息,也就是僵尸的“身份证”。这个“身份证”里储存了僵尸的属性。如xy横纵坐标来表示僵尸位置,由于僵尸是一行行进攻的,也可以再定义一个row变量来表示僵尸所处的行,后续处理起来会更加方便。位置确定了,僵尸的血量也可以通过定义一个变量blood来表示。那行走时的动画怎么办呢,动画本质上是一系列图片的快速播放,我们可以自己去找这样的一组图片,它们循环播放就是僵尸行走的动画,然后给每个图片都标好序数,这样,我们可以在僵尸的“身份证”里定义一个动画帧序数,用来表示当前播放到第几帧。通过不断刷新界面,僵尸就可以完成行走动作啦。
除了行走动画,僵尸也要设计死亡动画,还要判定僵尸是否死亡等等,这些都可以写入“身份证”里。
另外,在编写时,我们可以利用池的概念。所谓池,就是若干个对象的集合。当我们需要生成僵尸的时候,不是直接创造的,而是从池里拿出一个僵尸来,使用完后再放回去。那么这里如何表示僵尸是否使用呢?就需要在“身份证”里再加入一个布尔型变量used,若为真,那么僵尸处于场上,若为假,那么僵尸位于池中。
经过多方研究,最终僵尸的“身份证”明细如下
struct zm {
int x, y;
int row;
int frameIndex;
bool used;
int speed;
int health;
bool dead;
};
再创建僵尸池。
struct zm zms[10];
这样,创建工作就完成啦。
然后是我们的初始化工作。我们需要将zms结构体数组中的所有变量先清零,也就是初始化。
memset(zms,0,sizeof(zms));
通过这样一个内存赋值语句将zms清零
再导入图片
for (int i = 0; i < 22; i++) {
sprintf_s(name, sizeof(name), "res\\zm\\%d.png", i + 1);
loadimage(&imgZM[i], name);
}
这边把图片名设计成连续的数字,用sprintf函数依次把文件名赋给name,再以name为路径把图片导入到imgZM中(imgZM是之前创建的图片数组)
这样我们前期工作就完成了。
接下来制作生成模块。
生成僵尸的时候,先从池里拿出一个来,如果未被使用,则进行生成,如果被使用了,则换下一个
int i;
int zmMax = sizeof(zms) / sizeof(zms[0]);
for (i = 0; i < zmMax && zms[i].used; i++);
if (i < zmMax) {
zms[i].used = true;
zms[i].x = WIN_WIDTH;
zms[i].speed = 3;
zms[i].row = rand() % 3;
zms[i].y = 172 + (1 + zms[i].row) * 100;
zms[i].health = 100;
}
这里定义了僵尸的初始值。zmMax是池子的大小,可以在创建之初就进行修改。使用了rand函数来随机僵尸出现的行数,需要在初始化里配置一个随机种子。
有些同学可能对这里的for语句感到奇怪,因为这个语句并没有后续的内容。
其实它只是负责生成没有被使用僵尸的序号。
那我们的僵尸这样就生成好了,只不过是数据层面的生成,并不能在图上显示出来。
而且这样生成的僵尸是静态的,为此我们需要把这个僵尸的参数传入更新模块,让僵尸的参数动态变化,需要动态变化的值有动画帧序数,僵尸的横坐标,血量等。
在进行更新操作之前,还有一个小细节要处理。就是如果没有限制的话,这个程序会一次性生成池子里的所有僵尸。因为电脑的程序运行是很快的,在很短的时间里这个程序会多次重复,所以我们需要限制僵尸生成的速度。
static int zmFre = 50;//改改改啊
static int count = 0;
count++;
if (count > zmFre) {
count = 0;
zmFre = rand() % 200 + 300;
int i;
int zmMax = sizeof(zms) / sizeof(zms[0]);
for (i = 0; i < zmMax && zms[i].used; i++);
if (i < zmMax) {
zms[i].used = true;
zms[i].x = WIN_WIDTH;
zms[i].speed = 3;
zms[i].row = rand() % 3;
zms[i].y = 172 + (1 + zms[i].row) * 100;
zms[i].health = 100;
}
}
对生成代码做以上修饰。利用一个静态变量记录程序重复的次数,当重复到一定次数时才执行。
大意是,当程序运行次数达到fre规定的次数时,才会生成僵尸。初始是运行51次后生成第一只僵尸,随后利用rand函数让僵尸出现的频率随机化。
值得注意的是,如果你想降低某个动作发生的频率,也可以借用这种处理。
这样就可以保证僵尸不再扎堆出现了。
接下来进行我们的更新操作。将数据传入更新模块,僵尸的位置变化所需的数据更新在这里完成。
for (int i = 0; i < zmMax; i++) {
if (zms[i].used) {
zms[i].x -= zms[i].speed;}
还记得zms[i].used代表了什么吗。这个参数是用于判断僵尸是否在池内的。遍历池的所有僵尸,只对在池外的僵尸进行坐标变换操作
for (int i = 0; i < zmMax; i++) {
if (zms[i].used) {
zms[i].frameIndex = (zms[i].frameIndex + 1) % 22;
}
遍历池里的所有僵尸,只对池外的僵尸进行动画帧序数变换操作。大家可以学习一下这里的数字从1到22循环变化的表达法哦。
同样在生成模块里出现过的问题,僵尸移动太快了,动画播放也太快了,非常鬼畜。
那么如何减缓移动呢?
上文刚刚提到,我们利用一个静态变量进行计数,减缓了僵
尸生成的速度,那么我们可以用同样的方式减低僵尸的移动速度和动画播放速度。这也是为什么我不将这两个函数合并在一起写的原因,因为需要调节不同的频率。比如程序运行总共6次吧,僵尸移动函数段执行2次,动画帧序数变换函数段执行3次。需要定义不同的静态变量来计时。具体实现如下
int zmMax = sizeof(zms) / sizeof(zms[0]);
static int count = 0;
count++;
if (count > 2) {
count = 0;
for (int i = 0; i < zmMax; i++) {
if (zms[i].used) {
zms[i].x -= zms[i].speed;
if (zms[i].x < 170) {
printf("GAME OVER\n");
MessageBox(NULL, "over", "over", 0);//后期优化游戏结算
exit(0);
}
}
}
}
static int count2 = 0;
count2++;
if (count2 > 1) {
count2 = 0;
for (int i = 0; i < zmMax; i++) {
if (zms[i].used) {
zms[i].frameIndex = (zms[i].frameIndex + 1) % 22;
}
}
}
中间加了点简陋的失败判定,后续会进行优化。
直到现在,博客写了将近3000字,但我们还差最后一步:渲染。
目前我们所做的所有工作都是数字层面的,要真正实现屏幕上的姜丝,还需要将数据再传入渲染模块。
int zmCount = sizeof(zms) / sizeof(zms[0]);
for (int i = 0; i < zmCount; i++) {
IMAGE* img = &imgZM[zms[i].frameIndex];
putimagePNG(zms[i].x, zms[i].y - img->getheight(), img);
}
遍历池里所有的僵尸,将该僵尸的动画帧序数传入指针,指针指向之前导入的照片组。
然后就能实现僵尸行走啦!