C 实现植物大战僵尸(三)
十 实现豌豆子弹
原设计
这里的设计思路和原 UP 主思路差异比较大,罗列如下
原作中只要僵尸在出现在某条道路上,且存在豌豆射手,豌豆射手就会发射子弹,(这里是网页在线版的链接 4399 在线玩植物大战僵尸 H5)
可以看到正常情况下,同一豌豆射手只有上次发射的子弹爆炸后才会发射下一颗(也就是豌豆射击是有时间间隔的)
如果按照原 UP 主思路设计一个子弹类
c++
typedef struct Bullet {
int x; //当前 X 轴坐标
int y; //当前 Y 轴坐标
int frameId; //当前图片帧编号
int speed; //子弹移动的速度
bool used; //是否在使用
};
//同一豌豆射手只有上次发射的子弹爆炸后才会发射下一颗
Bullet bullets[GRASS_GRID_ROW * GRASS_GRID_COL];
IMAGE peaNormal;
在更新游戏数据(updateGame) 地方创建子弹和更新子弹数据
c++
void updateGame()
{
for (int i = 0; i < GRASS_GRID_ROW; ++i)
{
for (int j = 0; j < GRASS_GRID_COL; ++j)
{
if (plants[i][j].type >= 0)
{
if (imgPlant[plants[i][j].type][++plants[i][j].frameId] == NULL)
plants[i][j].frameId = 0;
}
}
}
createSunshine();
updateSunshine();
createZombie();
updateZombie();
//创建子弹和更新子弹数据
createBullets();
updateBullets();
}
在 gameInit 中加载图片
c++
//加载子弹图片
loadimage(&peaNormal, "res/bullets/PeaNormal/PeaNormal_0.png");
在 updateWindow 中渲染子弹
c++
//渲染子弹
for (int i = 0; i < GRASS_GRID_ROW * GRASS_GRID_COL; ++i)
{
if (bullets[i].used)
putimagePNG(bullets[i].x, bullets[i].y, &peaNormal);
}
重点 接下来实现 createBullets()
和 updateBullets()
函数
c++
void createBullets()
{
int peaX = 0, peaY = 0, pic_width = 0;
//遍历是否存在僵尸
for (int i = 0; i < MAX_ZOMBIE_NUM && zombies[i].used; ++i)
{
//printf("%s zombies i = %d row = %d \n", __FUNCTION__ , i, zombies[i].row);
//遍历当前行是否存在豌豆
for (int j = 0; j < GRASS_GRID_COL &&
plants[zombies[i].row][j].type == (int)PEA; ++j)
{
//printf("%s pea i = %d j = %d \n", __FUNCTION__, zombies[i].row, j);
//找到一颗未使用的子弹
for (int k = 0; k < (GRASS_GRID_ROW * GRASS_GRID_COL)
&& !bullets[k].used; ++k)
{
//printf("%s bullet k = %d \n", __FUNCTION__,k);
//之前豌豆的 X Y 坐标
peaX = GRASS_LEFT_MARGIN + j * GRASS_GRID_WIDTH + 5;
peaY = GRASS_TOP_MARGIN + zombies[i].row * GRASS_GRID_HIGHT + 10;
pic_width = imgPlant[0][0]->getwidth();
//初始化子弹
bullets[k].x = peaX + pic_width;
bullets[k].y = peaY + 5;
bullets[k].speed = 7;
bullets[k].frameId = 0;
bullets[k].used = true;
break;
}
}
}
}
void updateBullets()
{
for (int k = 0; k < (GRASS_GRID_ROW * GRASS_GRID_COL)
&& bullets[k].used; ++k)
{
bullets[k].x += bullets[k].speed;
if (bullets[k].x >= WIN_WIDTH)
{
bullets[k].used = false;
}
}
}
上述 createBullets 函数存在问题点,因为没有判断该该豌豆是否已经发射了子弹(假设豌豆射击的时间间隔为,在子弹爆炸前不会再发射子弹),所以子弹会瞬间用完
这时可以在 Plant 结构体中增加成员变量来记录,用 index 来记录子弹数组的下标,通过在上述第三层循环中增加 plants[zombies[i].row][j].index = k;
就可以判断 该豌豆是否已经发射了子弹
c++
typedef struct Plant
{
int type; //植物类型, -1 表示草地
int frameId; //表示植物摆动帧
int index; //新增: 记录下标,如果植物类型是豌豆,表示未发射子弹
}Plant;
发射的问题解决了,但在豌豆碰撞到僵尸时,是需要把上述豌豆的 index 重新置为 -1 (初始化时 memset 值)的,所以还需要在 Bullet 记录下豌豆的坐标,当在 updateBullets 函数,if (bullets[k].x >= WIN_WIDTH)
时,把 plants[currPeaX][currPeaY].index = -1
c++
typedef struct Bullet {
int currPeaX; //新增 豌豆 X 坐标
int currPeaY; //新增 豌豆 Y 坐标
int x; //当前 X 轴坐标
int y; //当前 Y 轴坐标
int frameId; //当前图片帧编号
int speed; //子弹移动的速度
bool used; //是否在使用
};
当然新增其它数据数据,豌豆和子弹间利用新增结构体之间联系也可以。但豌豆和子弹本身应该是从属关系,所以无论是新增结构体或是用上面新增成员变量的方式(在 Plant 结构体中加上专属于 Pea 的成员也很奇怪),代码都会有一种割裂感(原本一个整体却被割开了)
现设计
因此感觉这里需要的应该是豌豆射手结构体(用结构体嵌套方式),上述中的豌豆射击的时间间隔,可以把它定义为豌豆的射击速度
设计结构体如下
c++
/* 植物相关结构和变量 */
typedef struct Plant // 植物结构体
{
int type; //植物类型, -1 表示草地
int frameId; //表示植物摆动帧
}Plant;
Plant* plants[GRASS_GRID_ROW][GRASS_GRID_COL]; //注意这里改成了指针二维数组
/* 草地结构体 */
typedef struct Grass
{
Plant plant;
} Grass;
/* 向日葵结构体 */
typedef struct SunFlower
{
Plant plant;
} SunFlower;
/* 豌豆射手相关结构和变量 */
#define MAX_BULLET_NUM 1 //默认同一豌豆只有上次发射子弹爆炸后才发射下一颗, 也可更改
IMAGE peaNormal;
IMAGE peaNormalExplode;
/* 子弹结构体 */
typedef struct Bullet {
int x; //当前 X 轴坐标
int y; //当前 Y 轴坐标
int speed; //子弹移动的速度
bool used; //是否在使用
}Bullet;
#define DEFAULT_SHOOT_TIME -1
#define MAX_TIME_INTERVAL 100
/* 豌豆射手结构体 */
typedef struct PeaShooter
{
Plant plant;
int shootSpeed; //豌豆射击速度, 或者叫豌豆发射子弹的时间间隔, -1 表示可发射子弹
Bullet bullet[MAX_BULLET_NUM]; //子弹夹
} PeaShooter;
所以相当于之前涉及到 plants 的地方都需要修改,因此把整体代码调整了一下。主要调整点在于
① plants 在 gameInit 中把所有格子初始化为草地
② 种植植物时,先释放对应草格子内存,然后把 plants 二维指针数组对应位置,指向初始化的植物
③ 在游戏结束时候调用 destroyPlants 接口把申请的内存销毁并把 plants 成员置 NULL
④ shoot 为豌豆射手发射子弹接口,豌豆射手需要到达射击时间且子弹未使用状态时才能发射豌豆射击。updateBullets 更新子弹图片帧接口,和之前逻辑基本一致
改动位置较多,无法一一说明,贴当前项目全部代码如下,所有代码内容均有注释
c++
#include <stdio.h>
#include <graphics.h> // 引用图形库头文件
#include <time.h>
#include <math.h>
#include <mmsystem.h>
#include <assert.h>
#include <stdlib.h>
#include "tools.h"
#pragma commet(lib, "winmm.lib")
/* 一些数据宏定义, 具体含义参见宏名称 */
#define WIN_WIDTH 900 //窗口属性宽高宏定义
#define WIN_HIGHT 600
#define MAX_PICTURE_NUM 20 //动态植物图片属性宏定义
#define PIC_LEFT_MARGIN 338
#define PIC_WIDTH 65
#define GRASS_LEFT_MARGIN 252 //草格子属性宏定义
#define GRASS_TOP_MARGIN 82
#define GRASS_GRID_ROW 5
#define GRASS_GRID_COL 9
#define GRASS_GRID_HIGHT 98
#define GRASS_GRID_WIDTH 81
#define UI_LEFT_MARGIN 474 //游戏菜单属性宏定义
#define UI_TOP_MARGIN 75
#define UI_WIDTH 300
#define UI_HIGHT 140
int currX = 0, currY = 0, currIndex = -1; //当前拖动植物的坐标和类型
enum PLANT_CARDS { PEA, SUNFLOWER, PLANT_CNT }; //使用 PLANT_CNT 统计 PLANT 总数
IMAGE imgBg; //背景图片
IMAGE imgBar; //工具栏图片
IMAGE imgCards[PLANT_CNT]; //植物卡片
IMAGE* imgPlant[PLANT_CNT][MAX_PICTURE_NUM]; //动态植物素材 (也可使用二维数组, 但存在浪费空间问题)
/* 植物相关结构和变量 */
typedef struct Plant // 植物结构体
{
int type; //植物类型, -1 表示草地
int frameId; //表示植物摆动帧
}Plant;
Plant* plants[GRASS_GRID_ROW][GRASS_GRID_COL];
/* 草地结构体 */
typedef struct Grass
{
Plant plant;
} Grass;
/* 向日葵结构体 */
typedef struct SunFlower
{
Plant plant;
} SunFlower;
/* 豌豆射手相关结构和变量 */
#define MAX_BULLET_NUM 1 //默认同一豌豆只有上次发射子弹爆炸后才发射下一颗, 也可更改
IMAGE peaNormal;
IMAGE peaNormalExplode;
/* 子弹结构体 */
typedef struct Bullet {
int x; //当前 X 轴坐标
int y; //当前 Y 轴坐标
int speed; //子弹移动的速度
bool used; //是否在使用
}Bullet;
#define DEFAULT_SHOOT_TIME -1
#define MAX_TIME_INTERVAL 100
/* 豌豆射手结构体 */
typedef struct PeaShooter
{
Plant plant;
int shootSpeed; //豌豆射击速度, 或者叫豌豆发射子弹的时间间隔, -1 表示可发射子弹
Bullet bullet[MAX_BULLET_NUM]; //子弹夹
} PeaShooter;
/* 阳光球相关结构和变量 */
typedef struct SunShineBall
{
int x; //当前 X 轴坐标, 阳光球在飘落过程中 X 坐标不变
int y; //当前 Y 轴坐标
int frameId; //当前图片帧编号
int destination; //飘落目标位置 Y 坐标
bool used; //是否在使用
int timer; //统计飘落目标位置后的帧次数
float xOffset; //阳光球飞跃过程中每次 X 轴偏移量
float yOffset; //阳光球飞跃过程中每次 Y 轴偏移量
}SunShineBall;
#define MAX_BALLS_NUM 10
#define SUM_SHINE_PIC_NUM 29
SunShineBall balls[MAX_BALLS_NUM];
IMAGE imgSunShineBall[SUM_SHINE_PIC_NUM];
int sunShineVal = 50; //全局变量阳光值
/* 僵尸相关结构和变量 */
#define MAX_ZOMBIE_NUM 10
#define MAX_ZOMBIE_PIC_NUM 22
typedef struct Zombie {
int x; //当前 X 轴坐标
int y; //当前 Y 轴坐标
int frameId; //当前图片帧编号
int speed; //僵尸移动的速度
int row; //僵尸所在行
bool used; //是否在使用
};
Zombie zombies[MAX_ZOMBIE_NUM];
IMAGE imgZombies[MAX_ZOMBIE_PIC_NUM];
/* 判断文件是否存在接口 */
bool fileExist(const char* name)
{
FILE* file = NULL;
if (file = fopen(name,"r"))
fclose(file);
return file == NULL ? false : true;
}
/* 游戏初始化接口, 主要加载游戏图片至内存 */
void gameInit()
{
loadimage(&imgBg, "res/map0.jpg"); //加载背景图片
loadimage(&imgBar, "res/bar5.png");
char name[64];
memset(imgPlant, 0, sizeof(imgPlant)); //将二维指针数组内存空间置零
memset(balls, 0, sizeof(balls));
memset(zombies, 0, sizeof(zombies));
memset(plants, 0, sizeof(plants));
Grass* grassPtr = NULL;
for (int i = 0; i < GRASS_GRID_ROW; ++i) //将植物数组全初始化为草地
{
for (int j = 0; j < GRASS_GRID_COL; ++j)
{
grassPtr = (Grass*)calloc(1, sizeof(Grass));
assert(grassPtr);
plants[i][j] = (Plant*)grassPtr;
plants[i][j]->type = -1;
}
}
for (int i = 0; i < PLANT_CNT; ++i)
{
sprintf(name, "res/Cards/card_%d.png", i + 1); //获取植物卡片相对路径名称
loadimage(&imgCards[i], name);
for (int j = 0;i < MAX_PICTURE_NUM; ++j)
{
sprintf(name, "res/Plants/%d/%d.png", i, j + 1); //获取动态植物素材相对路径名称
if (fileExist(name)) {
imgPlant[i][j] = new IMAGE;
loadimage(imgPlant[i][j], name);
}
else break;
}
}
for (int i = 0; i < SUM_SHINE_PIC_NUM; ++i) //加载阳光图片
{
sprintf(name, "res/sunshine/%d.png", i + 1);
loadimage(&imgSunShineBall[i], name);
}
for (int i = 0; i < MAX_ZOMBIE_PIC_NUM; ++i) //加载僵尸图片
{
sprintf(name, "res/zm/0/%d.png", i + 1);
loadimage(&imgZombies[i], name);
}
loadimage(&peaNormal, "res/bullets/PeaNormal/PeaNormal_0.png"); //加载子弹图片
srand(time(NULL)); //配置随机种子
initgraph(WIN_WIDTH, WIN_HIGHT, 1); //创建游戏图形窗口
LOGFONT f; //设置字体
gettextstyle(&f);
f.lfHeight = 30;
f.lfWidth = 15;
strcpy(f.lfFaceName, "Segoe UI Black");
f.lfQuality = ANTIALIASED_QUALITY; //抗锯齿化效果
settextstyle(&f);
setbkmode(TRANSPARENT); //设置背景透明
setcolor(BLACK); //设置字体颜色
}
/* 游戏更新窗口接口, 主要渲染游戏图片至输出窗口 */
void updateWindow()
{
BeginBatchDraw(); //使用双缓冲, 解决输出窗口闪屏
putimage(0, 0, &imgBg); //渲染背景图至窗口
putimagePNG(250, 0, &imgBar);
for (int i = 0;i < PLANT_CNT;++i) //渲染植物卡牌
putimage(PIC_LEFT_MARGIN + i * PIC_WIDTH, 6, &imgCards[i]);
for (int i = 0; i < GRASS_GRID_ROW; ++i) //渲染种植植物
{
for (int j = 0; j < GRASS_GRID_COL; ++j)
{
if (plants[i][j]->type >= 0)
putimagePNG(GRASS_LEFT_MARGIN + j * GRASS_GRID_WIDTH + 5, //微调植物种植位置
GRASS_TOP_MARGIN + i * GRASS_GRID_HIGHT + 10,
imgPlant[plants[i][j]->type][plants[i][j]->frameId]);
}
}
for (int i = 0; i < MAX_BALLS_NUM; ++i) //渲染随机阳光
{
if (balls[i].used || balls[i].xOffset)
putimagePNG(balls[i].x, balls[i].y, &imgSunShineBall[balls[i].frameId]);
}
if (currIndex >= 0) //渲染当前拖动的植物
{
IMAGE* currImage = imgPlant[currIndex][0];
putimagePNG(currX - currImage->getwidth() / 2,
currY - currImage->getheight() / 2, currImage);
}
char scoreText[8]; //渲染阳光值
sprintf(scoreText, "%d", sunShineVal);
outtextxy(277, 67, scoreText);
for (int i = 0; i < MAX_ZOMBIE_NUM; ++i) //渲染僵尸
{
if (zombies[i].used)
{
IMAGE* img = &imgZombies[zombies[i].frameId];
putimagePNG(zombies[i].x, zombies[i].y + 30, img);
}
}
PeaShooter* peaShooter = NULL; //渲染子弹
for (int i = 0; i < GRASS_GRID_ROW; ++i)
{
for (int j = 0; j < GRASS_GRID_COL; ++j)
{
if (plants[i][j]->type == PEA)
{
peaShooter = (PeaShooter*)plants[i][j];
for (int k = 0; k < MAX_BULLET_NUM; ++k) //默认只有一发子弹, 但可调整
{
if (peaShooter->bullet[k].used)
putimagePNG(peaShooter->bullet[k].x, peaShooter->bullet[k].y, &peaNormal);
}
}
}
}
EndBatchDraw(); //结束双缓冲
}
/* 收集随机阳光接口 */
void collectSunShine(ExMessage* msg)
{
IMAGE* imgSunShine = NULL;
for (int i = 0; i < MAX_BALLS_NUM; ++i) //遍历阳光球
{
if (balls[i].used) //阳光球在使用中
{
imgSunShine = &imgSunShineBall[balls[i].frameId]; //找到对应的阳光球图片
if (msg->x > balls[i].x && msg->x < balls[i].x + imgSunShine->getwidth()
&& msg->y > balls[i].y && msg->y < balls[i].y + imgSunShine->getheight()) //判断鼠标移动的位置是否处于当前阳光球的位置
{
PlaySound("res/audio/sunshine.wav", NULL, SND_FILENAME | SND_ASYNC); //异步播放收集阳光球音效
balls[i].used = false; //将阳光球状态更改为未使用 (飞跃状态, 因为 xOffset 赋值了)
const float angle = atan((float)(balls[i].y - 0) / (float)(balls[i].x - 262)); //使用正切函数
balls[i].xOffset = 16 * cos(angle); //计算 X 轴偏移
balls[i].yOffset = 16 * sin(angle); //计算 Y 轴偏移
}
}
}
}
/* 种植植物接口, 主要释放草格子内存, 二维指针数组对应位置,指向初始化的植物 */
Plant* growPlants(Plant* plant, int type)
{
assert(plant);
free((Grass*)plant); //释放该位置草格子内存
if (type == PEA) //根据类型初始化 PeaShooter
{
PeaShooter* peaShooter = (PeaShooter*)calloc(1, sizeof(PeaShooter)); //calloc 函数替代 malloc, 省略 memset
assert(peaShooter);
peaShooter->shootSpeed = DEFAULT_SHOOT_TIME; //豌豆射击速度, 或者叫豌豆发射子弹的时间间隔, -1 表示可发射子弹
peaShooter->bullet[0].speed = 10; //默认只使用了第一枚子弹, 可更改
return (Plant*)peaShooter;
}
else if (type == SUNFLOWER) //根据类型初始化 SunFlower
{
SunFlower* sunFlower = (SunFlower*)calloc(1, sizeof(SunFlower));
assert(sunFlower);
sunFlower->plant.type = 1;
return (Plant*)sunFlower;
}
}
/* 销毁植物接口, 主要释放草格子和种植植物的内存 */
void destroyPlants()
{
for (int i = 0; i < GRASS_GRID_ROW; ++i)
{
for (int j = 0; j < GRASS_GRID_COL; ++j)
{
if (plants[i][j]->type == PEA)
free ((PeaShooter*)plants[i][j]);
else if (plants[i][j]->type == SUNFLOWER)
free ((SunFlower*)plants[i][j]);
else
free ((Grass*)plants[i][j]);
}
}
memset(plants, 0, sizeof(plants)); //将指针全部置 NULL
}
/* 用户点击接口, 主要监听鼠标事件并调用相应的函数 */
void userClick()
{
ExMessage msg; //创建消息体
static int status = 0; //种植植物必须先选中再拖动(拖动需先左键点击再拖动)
if (peekmessage(&msg)) //该函数用于获取一个消息,并立即返回
{
collectSunShine(&msg);
if (msg.message == WM_LBUTTONDOWN) //鼠标点击
{
if (msg.x > PIC_LEFT_MARGIN &&
msg.x < PIC_LEFT_MARGIN + PLANT_CNT * PIC_WIDTH &&
msg.y < 96)
{
currX = msg.x, currY = msg.y;
currIndex = (msg.x - PIC_LEFT_MARGIN) / PIC_WIDTH;
status = 1;
}
}
else if (msg.message == WM_MOUSEMOVE && status == 1) //鼠标拖动
{
currX = msg.x, currY = msg.y; //记录当前拖动位置
}
else if (msg.message == WM_LBUTTONUP) //鼠标抬起
{
if (msg.x >= GRASS_LEFT_MARGIN &&
msg.x <= GRASS_LEFT_MARGIN + GRASS_GRID_COL * GRASS_GRID_WIDTH &&
msg.y >= GRASS_TOP_MARGIN &&
msg.y <= GRASS_TOP_MARGIN + GRASS_GRID_ROW * GRASS_GRID_HIGHT) //当植物拖到至草地位置终止, 则种植植物
{
int x = (msg.y - GRASS_TOP_MARGIN) / GRASS_GRID_HIGHT; //计算第几行
int y = (msg.x - GRASS_LEFT_MARGIN) / GRASS_GRID_WIDTH; //计算第几列
if (plants[x][y]->type < 0 && status == 1) //未点击植物或当前位置已种植过植物,则不种植植物
plants[x][y] = growPlants(plants[x][y], currIndex); //种植植物
}
status = 0, currIndex = -1; //停止拖动当前植物
}
}
}
/* 创建随机阳光球接口, 主要初始化随机阳光球 */
void createSunshine()
{
static int sunCallCnt = 0; //延缓函数调用次数并增加些随机性
static int randSunCallCnt = 400;
if (++sunCallCnt < randSunCallCnt) return;
randSunCallCnt = 200 + rand() % 200;
sunCallCnt = 0;
for (int i = 0; i < MAX_BALLS_NUM; ++i) //从阳光池中取一个可用阳光
{
if (!balls[i].used && balls[i].xOffset == 0) //找到一个未使用的阳光, 则进行初始化
{
balls[i].x = GRASS_LEFT_MARGIN + GRASS_GRID_WIDTH //只允许阳光掉落在草地范围内(不允许左一格)
+ (rand() % GRASS_GRID_COL) * GRASS_GRID_WIDTH; //因为左一格的位置可能在上方阳光栏图片左边
balls[i].y = GRASS_TOP_MARGIN;
balls[i].frameId = 0;
balls[i].destination = GRASS_TOP_MARGIN
+ GRASS_GRID_HIGHT + (rand() % (3 * GRASS_GRID_HIGHT)); //目标点在中间三行
balls[i].used = true;
balls[i].timer = 0;
balls[i].xOffset = 0;
balls[i].yOffset = 0;
break;
}
}
}
/* 更新随机阳光球接口, 主要更新随机阳光球的图片帧和处理飞跃状态时的 X Y 轴偏移 */
void updateSunshine()
{
for (int i = 0; i < MAX_BALLS_NUM; ++i)
{
if (balls[i].used)
{
if (balls[i].y < balls[i].destination)
{
balls[i].y += 2; //每次移动两个像素
balls[i].frameId = ++balls[i].frameId % SUM_SHINE_PIC_NUM; //修改当前图片帧编号, 并在到达 SUM_SHINE_PIC_NUM 时重置图片帧为 0
}
else //当阳光下落至目标位置时, 停止移动
{
if (balls[i].timer < MAX_TIME_INTERVAL) ++balls[i].timer;
else balls[i].used = false;
}
}
else if (balls[i].xOffset) //阳光球处于飞跃状态
{
if (balls[i].y > 0 && balls[i].x > 262)
{
const float angle = atan((float)(balls[i].y - 0) / (float)(balls[i].x - 262)); //不断调整阳光球的位置坐标
balls[i].xOffset = 16 * cos(angle);
balls[i].yOffset = 16 * sin(angle);
balls[i].x -= balls[i].xOffset;
balls[i].y -= balls[i].yOffset;
}
else
{
balls[i].xOffset = 0; //阳光球飞至计分器位置, 则将 xOffset 置 0, 且加上 25 积分
balls[i].yOffset = 0;
sunShineVal += 25;
}
}
}
}
/* 创建僵尸接口, 主要用于初始化僵尸 */
void createZombie()
{
static int zombieCallCnt = 0; //延缓函数调用次数并增加些随机性
static int randZombieCallCnt = 500;
if (zombieCallCnt++ < randZombieCallCnt) return;
randZombieCallCnt = 300 + rand() % 200;
zombieCallCnt = 0;
for (int i = 0; i < MAX_ZOMBIE_NUM; ++i) //找一个未在界面的僵尸初始化
{
if (!zombies[i].used)
{
zombies[i].row = rand() % GRASS_GRID_ROW; //僵尸出现在第几行(从 0 开始)
zombies[i].x = WIN_WIDTH;
zombies[i].y = zombies[i].row * GRASS_GRID_HIGHT; //出现在草地的任意一格上
zombies[i].frameId = 0;
zombies[i].speed = 1; //僵尸的移动速度
zombies[i].used = true;
break; //结束循环
}
}
}
/* 更新僵尸接口, 主要用于处理僵尸图片帧, 实现僵尸行走 */
void updateZombie()
{
static int CallCnt = 0; //延缓函数调用次数
if (++CallCnt < 3) return;
CallCnt = 0;
for (int i = 0; i < MAX_ZOMBIE_NUM; ++i)
{
if (zombies[i].used)
{
zombies[i].x -= zombies[i].speed; //僵尸行走
zombies[i].frameId = ++zombies[i].frameId % MAX_ZOMBIE_PIC_NUM; //僵尸更换图片帧
if (zombies[i].x < 170) //目前先这样写待优化
{
printf("GAME OVER !");
MessageBox(NULL, "over", "over", 0);
exit(0);
}
}
}
}
/* 更新植物图片帧接口, 主要用于实现植物摇摆 */
void updatePlantsPic()
{
for (int i = 0; i < GRASS_GRID_ROW; ++i) //遍历二维指针数组
{
for (int j = 0; j < GRASS_GRID_COL; ++j)
{
if (plants[i][j]->type >= 0 && //找到非草地的植物
imgPlant[plants[i][j]->type][++plants[i][j]->frameId] == NULL) //将植物图片增加一, 判断是否到达图片帧末尾
plants[i][j]->frameId = 0; //重置图片帧为零
}
}
}
/* 豌豆射手发射子弹接口 */
void shoot()
{
PeaShooter* peaShooter = NULL;
int row = 0, peaX = 0, peaY = 0, pic_width = 0;
for (int i = 0; i < MAX_ZOMBIE_NUM; ++i) //遍历是否存在僵尸
{
if (zombies[i].used)
{
row = zombies[i].row;
for (int j = 0; j < GRASS_GRID_COL; ++j) //遍历当前行是否存在豌豆
{
if (plants[row][j]->type == PEA)
{
peaShooter = (PeaShooter*)plants[row][j];
if (peaShooter->shootSpeed++ == DEFAULT_SHOOT_TIME) //发射时机
{
for (int k = 0; k < MAX_BULLET_NUM; ++k) //从子弹夹里取一颗未使用的子弹(默认一颗)
{
if (!peaShooter->bullet[k].used) //该子弹未在使用中
{
peaX = GRASS_LEFT_MARGIN + j * GRASS_GRID_WIDTH + 5; //之前豌豆的 X Y 坐标
peaY = GRASS_TOP_MARGIN + row * GRASS_GRID_HIGHT + 10;
pic_width = imgPlant[0][0]->getwidth();
peaShooter->bullet[k].x = peaX + pic_width; //初始化子弹
peaShooter->bullet[k].y = peaY + 5;
peaShooter->bullet[k].used = true;
break; //结束当前循环
}
}
}
else if (peaShooter->shootSpeed > MAX_TIME_INTERVAL) //不到发射时机
peaShooter->shootSpeed = DEFAULT_SHOOT_TIME; //则将 timer 计时器增加 (默认一百帧)
}
}
}
}
}
/* 更新子弹图片帧接口 */
void updateBullets()
{
PeaShooter* peaShooter = NULL;
for (int i = 0; i < GRASS_GRID_ROW; ++i) //遍历植物二维指针数组
{
for (int j = 0; j < GRASS_GRID_COL; ++j)
{
if (plants[i][j]->type == PEA) //找到其中是豌豆的位置
{
peaShooter = (PeaShooter*)plants[i][j];
for (int k = 0; k < MAX_BULLET_NUM; ++k)
{
if (peaShooter->bullet[k].used) //找到在使用中的子弹
{
peaShooter->bullet[k].x += peaShooter->bullet[k].speed; //移动子弹位置
if (peaShooter->bullet[k].x >= WIN_WIDTH) //如果到达窗口最右端
peaShooter->bullet[k].used = false; //将子弹重置为未使用状态
}
}
}
}
}
}
/* 更新游戏属性的接口 */
void updateGame()
{
updatePlantsPic();
createSunshine();
updateSunshine();
createZombie();
updateZombie();
shoot();
updateBullets();
}
/* 游戏开始前的菜单界面 */
void startUI()
{
IMAGE imageBg, imgMenu1, imgMenu2;
loadimage(&imageBg, "res/menu.png");
loadimage(&imgMenu1, "res/menu1.png");
loadimage(&imgMenu2, "res/menu2.png");
bool mouseStatus = false; //0 表示鼠标未移动至开始游戏位置
while (1)
{
BeginBatchDraw(); //双缓冲解决闪屏
putimage(0, 0, &imageBg);
putimagePNG(UI_LEFT_MARGIN, UI_TOP_MARGIN, mouseStatus ? &imgMenu2 : &imgMenu1); //根据鼠标是否移动至游戏开始位置, 显示不同的图片
ExMessage msg;
if (peekmessage(&msg)) //监听鼠标事件
{
if (msg.x > UI_LEFT_MARGIN && msg.x < UI_LEFT_MARGIN + UI_WIDTH
&& msg.y > UI_TOP_MARGIN && msg.y < UI_TOP_MARGIN + UI_HIGHT) //当鼠标移动至开始游戏位置, 界面高亮
{
putimagePNG(UI_LEFT_MARGIN, UI_TOP_MARGIN, &imgMenu2);
mouseStatus = true; //表示鼠标移动至开始游戏位置, 如果一直不移动鼠标则一直高亮
if (msg.message == WM_LBUTTONDOWN) //当鼠标点击时, 进入游戏
return; //结束函数
}
else mouseStatus = false; //当鼠标未移动至开始游戏位置, 界面不高亮
}
EndBatchDraw();
}
}
/* 主函数 */
int main()
{
gameInit(); //不能把 startUI 放在 gameInit 前, gameInit 包含了创建游戏图形窗口
startUI();
updateWindow(); //窗口视图展示
int timer = 0; //用以计时 20 毫秒更新一次
while (1)
{
userClick(); //监听窗口鼠标事件
timer += getDelay();
if (timer > 20)
{
updateWindow(); //更新窗口视图
updateGame(); //更新游戏动画帧
timer = 0;
}
}
destroyPlants(); //释放内存
system("pause");
return 0;
}
效果展示
僵尸在出现在某条道路上,且存在豌豆射手,豌豆射手就会发射子弹,还没实现子弹和僵尸碰撞功能

小记录
imgPlant[i][j] = new IMAGE;
该行是 easyx
内部在 IMAGE 构造函数里加了一些初始化内容,所以没办法用 malloc
替代
C++ 中的 new 和 delete,通过父类指针释放子类对象,是通过虚函数表实现的,在还是用上述 C 的方式比较好(类似于用 C 实现面对对象代码)
不能把判断条件写入循环条件内部,除非是可以用以结束循环的条件
十一 实现子弹和僵尸碰撞
子弹结构体新增成员变量
c++
/* 子弹结构体 */
typedef struct Bullet {
int x; //当前 X 轴坐标
int y; //当前 Y 轴坐标
int speed; //子弹移动的速度
int frameIndex; //帧序号
bool blast; //子弹是否爆炸
bool used; //是否在使用
} Bullet;
游戏初始化接口 gameInit,加载子弹爆炸图片至内存
c++
loadimage(&peaExplode[PEA_EXPLODE_PIC_NUM - 1], "res/bullets/PeaNormalExplode/PeaNormalExplode_0.png"); //加载豌豆子弹爆炸图片
for (int i = 1; i < PEA_EXPLODE_PIC_NUM; ++i)
{
loadimage(&peaExplode[i - 1], "res/bullets/PeaNormalExplode/PeaNormalExplode_0.png",
peaExplode[PEA_EXPLODE_PIC_NUM - 1].getwidth() * 0.2 * i,
peaExplode[PEA_EXPLODE_PIC_NUM - 1].getheight() * 0.2 * i, true); //加载豌豆子弹爆炸缩小版图片
}
游戏更新窗口接口 updateWindow,渲染子弹爆炸图片至输出窗口
c++
PeaShooter* peaShooter = NULL; //渲染子弹
for (int i = 0; i < GRASS_GRID_ROW; ++i)
{
for (int j = 0; j < GRASS_GRID_COL; ++j)
{
if (plants[i][j]->type == PEA)
{
peaShooter = (PeaShooter*)plants[i][j];
for (int k = 0; k < MAX_BULLET_NUM; ++k) //默认只有一发子弹, 但可调整
{
if (peaShooter->bullet[k].used)
{
if (peaShooter->bullet[k].blast)
putimagePNG(peaShooter->bullet[k].x, peaShooter->bullet[k].y,
&peaExplode[peaShooter->bullet[k].frameIndex]); //渲染子弹爆炸图片
else putimagePNG(peaShooter->bullet[k].x, peaShooter->bullet[k].y, &peaNormal); //渲染子弹图片
}
}
}
}
}
豌豆射手发射子弹接口 shoot 中初始化新增成员
c++
void shoot()
{
PeaShooter* peaShooter = NULL;
int row = 0, peaX = 0, peaY = 0, pic_width = 0;
for (int i = 0; i < MAX_ZOMBIE_NUM; ++i) //遍历是否存在僵尸
{
if (zombies[i].used)
{
row = zombies[i].row;
for (int j = 0; j < GRASS_GRID_COL; ++j) //遍历当前行是否存在豌豆
{
if (plants[row][j]->type == PEA)
{
peaShooter = (PeaShooter*)plants[row][j];
if (peaShooter->shootSpeed++ == DEFAULT_SHOOT_TIME) //发射时机
{
for (int k = 0; k < MAX_BULLET_NUM; ++k) //从子弹夹里取一颗未使用的子弹(默认一颗)
{
if (!peaShooter->bullet[k].used) //该子弹未在使用中
{
peaX = GRASS_LEFT_MARGIN + j * GRASS_GRID_WIDTH + 5; //之前豌豆的 X Y 坐标
peaY = GRASS_TOP_MARGIN + row * GRASS_GRID_HIGHT + 10;
pic_width = imgPlant[0][0]->getwidth();
peaShooter->bullet[k].x = peaX + pic_width; //初始化子弹
peaShooter->bullet[k].y = peaY + 5;
peaShooter->shootSpeed = DEFAULT_SHOOT_TIME; //豌豆射击速度, 或者叫豌豆发射子弹的时间间隔, -1 表示可发射子弹
peaShooter->bullet[k].speed = 10; //默认只使用了第一枚子弹, 可更改
peaShooter->bullet[k].frameIndex = 0;
peaShooter->bullet[k].blast = false;
peaShooter->bullet[k].used = true;
peaShooter->bullet[k].blast = false;
break; //结束当前循环
}
}
}
else if (peaShooter->shootSpeed > MAX_TIME_INTERVAL) //不到发射时机
peaShooter->shootSpeed = DEFAULT_SHOOT_TIME; //则将 timer 计时器增加 (默认一百帧)
}
}
}
}
}
updateBullets 更新子弹图片帧接口
c++
void updateBullets()
{
PeaShooter* peaShooter = NULL;
for (int i = 0; i < GRASS_GRID_ROW; ++i) //遍历植物二维指针数组
{
for (int j = 0; j < GRASS_GRID_COL; ++j)
{
if (plants[i][j]->type == PEA) //找到其中是豌豆的位置
{
peaShooter = (PeaShooter*)plants[i][j];
for (int k = 0; k < MAX_BULLET_NUM; ++k)
{
if (peaShooter->bullet[k].used) //找到在使用中的子弹
{
peaShooter->bullet[k].x += peaShooter->bullet[k].speed; //移动子弹位置
if (peaShooter->bullet[k].x >= WIN_WIDTH) //如果到达窗口最右端
peaShooter->bullet[k].used = false; //将子弹重置为未使用状态
}
if (peaShooter->bullet[k].blast && //找到爆炸的子弹
++peaShooter->bullet[k].frameIndex >= PEA_EXPLODE_PIC_NUM) //子弹爆炸完成
peaShooter->bullet[k].used = false; //重置子弹状态
}
}
}
}
}
重点 僵尸和子弹碰撞检测接口 collsionCheck
c++
void collsionCheck()
{
PeaShooter* peaShooter = NULL;
int row = 0, peaX = 0, pic_width = 0;
for (int i = 0; i < MAX_ZOMBIE_NUM; ++i) //遍历是否存在僵尸
{
if (zombies[i].used && !zombies[i].isDead) //僵尸正在使用中, 且存活
{
row = zombies[i].row;
for (int j = 0; j < GRASS_GRID_COL; ++j) //遍历当前行是否存在豌豆
{
if (plants[row][j]->type == PEA)
{
peaShooter = (PeaShooter*)plants[row][j]; //找到对应的豌豆
for (int k = 0; k < MAX_BULLET_NUM; ++k) //从子弹夹找到一颗在使用的子弹(默认一颗)
{
if (peaShooter->bullet[k].used && !peaShooter->bullet[k].blast) //该子弹在使用中
{
peaX = peaShooter->bullet[k].x;
if (peaX > (zombies[i].x + 80) && peaX < (zombies[i].x + 110)) //子弹和僵尸碰撞
{
zombies[i].blood -= 10; //扣除僵尸血量
peaShooter->bullet[k].blast = true; //子弹开始爆炸
peaShooter->bullet[k].speed = 0; //将子弹速度降为 0
if (zombies[i].blood <= 0)
{
zombies[i].isDead = true; //僵尸死亡
zombies[i].speed = 0; //重置僵尸速度
zombies[i].frameId = 0; //此时更换为僵尸死亡图片帧
}
}
break; //结束当前循环
}
}
}
}
}
}
}
updateGame 中调用 collsionCheck
c++
/* 更新游戏属性的接口 */
void updateGame()
{
updatePlantsPic();
createSunshine();
updateSunshine();
createZombie();
updateZombie();
shoot();
updateBullets();
collsionCheck();
}
效果展示

十二 实现僵尸死亡
新增僵尸死亡相关变量和图片
c++
/* 僵尸相关结构和变量 */
#define MAX_ZOMBIE_NUM 10
#define MAX_ZOMBIE_DEAD_PIC_NUM 10
#define MAX_ZOMBIE_PIC_NUM 22
typedef struct Zombie {
int x; //当前 X 轴坐标
int y; //当前 Y 轴坐标
int frameId; //当前图片帧编号
int speed; //僵尸移动的速度
int row; //僵尸所在行
int blood; //默认僵尸血条为 100
bool isDead; //僵尸是否死亡
bool used; //是否在使用
} Zombie;
Zombie zombies[MAX_ZOMBIE_NUM];
IMAGE imgZombies[MAX_ZOMBIE_PIC_NUM];
IMAGE imgDeadZombies[MAX_ZOMBIE_DEAD_PIC_NUM];
游戏初始化接口 gameInit,加载僵尸死亡图片至内存
c++
for (int i = 0; i < MAX_ZOMBIE_DEAD_PIC_NUM; ++i) //加载僵尸死亡图片
{
sprintf(name, "res/zm_dead/%d.png", i + 1);
loadimage(&imgDeadZombies[i], name);
}
游戏更新窗口接口 updateWindow,渲染僵尸死亡图片至输出窗口
c++
for (int i = 0; i < MAX_ZOMBIE_NUM; ++i) //渲染僵尸
{
if (zombies[i].used)
{
if (zombies[i].isDead) putimagePNG(zombies[i].x, zombies[i].y + 30, &imgDeadZombies[zombies[i].frameId]);
else putimagePNG(zombies[i].x, zombies[i].y + 30, &imgZombies[zombies[i].frameId]);
}
}
创建僵尸接口,初始化僵尸新增成员
c++
void createZombie()
{
static int zombieCallCnt = 0; //延缓函数调用次数并增加些随机性
static int randZombieCallCnt = 500;
if (zombieCallCnt++ < randZombieCallCnt) return;
randZombieCallCnt = 300 + rand() % 200;
zombieCallCnt = 0;
for (int i = 0; i < MAX_ZOMBIE_NUM; ++i) //找一个未在界面的僵尸初始化
{
if (!zombies[i].used)
{
zombies[i].row = rand() % GRASS_GRID_ROW; //僵尸出现在第几行(从 0 开始)
zombies[i].x = WIN_WIDTH;
zombies[i].y = zombies[i].row * GRASS_GRID_HIGHT; //出现在草地的任意一格上
zombies[i].frameId = 0;
zombies[i].speed = 1; //僵尸的移动速度
zombies[i].blood = 100; //默认僵尸血条为 100
zombies[i].isDead = false; //僵尸存活
zombies[i].used = true;
break; //结束循环
}
}
}
更新僵尸接口,处理僵尸死亡图片帧
c++
void updateZombie()
{
static int CallCnt = 0; //延缓函数调用次数
if (++CallCnt < 3) return;
CallCnt = 0;
for (int i = 0; i < MAX_ZOMBIE_NUM; ++i)
{
if (zombies[i].used)
{
if (zombies[i].isDead)
{
if (++zombies[i].frameId >= MAX_ZOMBIE_DEAD_PIC_NUM) //僵尸死亡则更换死亡帧
zombies[i].used = false; //重置僵尸状态
}
else
{
zombies[i].x -= zombies[i].speed; //僵尸行走
zombies[i].frameId = ++zombies[i].frameId % MAX_ZOMBIE_PIC_NUM; //僵尸更换图片帧
}
if (zombies[i].x < 170) //目前先这样写待优化
{
printf("GAME OVER !");
MessageBox(NULL, "over", "over", 0);
exit(0);
}
}
}
}
僵尸和子弹碰撞检测接口 collsionCheck 同上,更新僵尸血量和状态
效果展示
