C 实现植物大战僵尸(三)

devtools/2025/1/8 19:04:40/

C 实现植物大战僵尸(三)

十 实现豌豆子弹

原设计

这里的设计思路和原 UP 主思路差异比较大,罗列如下

原作中只要僵尸在出现在某条道路上,且存在豌豆射手,豌豆射手就会发射子弹,(这里是网页在线版的链接 4399 在线玩植物大战僵尸 H5 )

可以看到正常情况下,同一豌豆射手只有上次发射的子弹爆炸后才会发射下一颗(也就是豌豆射击是有时间间隔的)

如果按照原 UP 主思路设计一个子弹类

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) 地方创建子弹和更新子弹数据

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 中加载图片

//加载子弹图片
loadimage(&peaNormal, "res/bullets/PeaNormal/PeaNormal_0.png");

在 updateWindow 中渲染子弹

//渲染子弹
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() 函数

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; 就可以判断 该豌豆是否已经发射了子弹

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

typedef struct Bullet {int currPeaX;		//新增 豌豆 X 坐标int currPeaY;		//新增 豌豆 Y 坐标int x;              //当前 X 轴坐标int y;              //当前 Y 轴坐标int frameId;        //当前图片帧编号int speed;          //子弹移动的速度bool used;          //是否在使用
};

当然新增其它数据数据,豌豆和子弹间利用新增结构体之间联系也可以。但豌豆和子弹本身应该是从属关系,所以无论是新增结构体或是用上面新增成员变量的方式(在 Plant 结构体中加上专属于 Pea 的成员也很奇怪),代码都会有一种割裂感(原本一个整体却被割开了)

现设计

因此感觉这里需要的应该是豌豆射手结构体(用结构体嵌套方式),上述中的豌豆射击的时间间隔,可以把它定义为豌豆的射击速度

设计结构体如下

/* 植物相关结构和变量 */
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 更新子弹图片帧接口,和之前逻辑基本一致

改动位置较多,无法一一说明,贴当前项目全部代码如下,所有代码内容均有注释

#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 140int 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, 省略 memsetassert(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]);elsefree ((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;
}

效果展示

僵尸在出现在某条道路上,且存在豌豆射手,豌豆射手就会发射子弹,还没实现子弹和僵尸碰撞功能

image-20241230151227993

小记录

不能将 X 类型的值分配到 X类型的实体问题

imgPlant[i][j] = new IMAGE; 该行是 easyx 内部在 IMAGE 构造函数里加了一些初始化内容,所以没办法用 malloc 替代

C++ 中的 new 和 delete,通过父类指针释放子类对象,是通过虚函数表实现的,在还是用上述 C 的方式比较好(类似于用 C 实现面对对象代码)

不能把判断条件写入循环条件内部,除非是可以用以结束循环的条件

十一 实现子弹和僵尸碰撞

子弹结构体新增成员变量

/* 子弹结构体 */
typedef struct Bullet {int x;              //当前 X 轴坐标int y;              //当前 Y 轴坐标int speed;          //子弹移动的速度int frameIndex;     //帧序号bool blast;         //子弹是否爆炸bool used;          //是否在使用
} Bullet;

游戏初始化接口 gameInit,加载子弹爆炸图片至内存

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,渲染子弹爆炸图片至输出窗口

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 中初始化新增成员

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 更新子弹图片帧接口

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

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; //将子弹速度降为 0if (zombies[i].blood <= 0) {zombies[i].isDead = true; //僵尸死亡zombies[i].speed = 0;     //重置僵尸速度zombies[i].frameId = 0;   //此时更换为僵尸死亡图片帧}}break; //结束当前循环}} }}}}
}

updateGame 中调用 collsionCheck

/* 更新游戏属性的接口 */
void updateGame() 
{updatePlantsPic();createSunshine();updateSunshine();createZombie();updateZombie();shoot();updateBullets();collsionCheck();
}

效果展示

image-20241230182322320

十二 实现僵尸死亡

新增僵尸死亡相关变量和图片

/* 僵尸相关结构和变量 */
#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;          //默认僵尸血条为 100bool isDead;        //僵尸是否死亡bool used;          //是否在使用
} Zombie;
Zombie zombies[MAX_ZOMBIE_NUM];
IMAGE imgZombies[MAX_ZOMBIE_PIC_NUM];
IMAGE imgDeadZombies[MAX_ZOMBIE_DEAD_PIC_NUM];

游戏初始化接口 gameInit,加载僵尸死亡图片至内存

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,渲染僵尸死亡图片至输出窗口

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]);}
}

创建僵尸接口,初始化僵尸新增成员

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; //默认僵尸血条为 100zombies[i].isDead = false; //僵尸存活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){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 同上,更新僵尸血量和状态

效果展示

image-20241230175955220


http://www.ppmy.cn/devtools/148969.html

相关文章

人工智能AI学习路径

一、打好基础 1. 数学基础 - 线性代数&#xff1a;这是 AI 的基石之一。矩阵和向量的运算在神经网络等许多 AI 算法中无处不在。例如&#xff0c;在深度学习中&#xff0c;图像可以被表示为一个矩阵&#xff0c;通过矩阵乘法等操作进行特征提取。你需要理解矩阵的基本运算&…

(安卓无线调试)ADB 无法连接及 Scrcpy 问题排查指南

问题描述 在使用 ADB 和 Scrcpy 时遇到以下问题&#xff1a; 无法连接到 ADB 服务。 即使连接成功&#xff0c;Scrcpy 显示以下错误&#xff1a; INFO: scrcpy 1.10 <https://github.com/Genymobile/scrcpy> D:\.....\scrcpy\scrcpy-server.jar: 1 file pushed. 0.2 …

30分钟学会HTML

HTML 基本语法 HTML&#xff08;HyperText Markup Language&#xff09;是构成网页内容的基础。它使用一系列的标签来描述网页的结构&#xff0c;包括文本、图片、链接等元素。浏览器会解析这些标签并渲染成我们看到的网页。 在线体验一下 CodePen (在线 HTML 编辑器)。 千万不…

H5通过URL Scheme唤醒手机地图APP

1.高德地图 安卓URL Scheme&#xff1a;baidumap:// 官方文档&#xff1a;https://lbs.amap.com/api/amap-mobile/guide/android/navigation IOS URL Scheme&#xff1a;iosamap:// 官方文档&#xff1a;https://lbs.amap.com/api/amap-mobile/guide/ios/navi HarmonyOS NEXT U…

Unity自定义编辑器:基于枚举类型动态显示属性

1.参考链接 2.应用 target并设置多选编辑 添加[CanEditMultipleObjects] using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEditor;[CustomEditor(typeof(LightsState))] [CanEditMultipleObjects] public class TestInspector :…

C语言——字符函数和内存函数

目录 前言 字符函数 1strlen 模拟实现 2strcpy 模拟实现 3strcat 模拟实现 4strcmp 模拟实现 5strncpy 模拟实现 6strncat 模拟实现 7strncmp 模拟实现 8strstr 模拟实现 9strtok 10strerror 11大小写字符转换函数 内存函数 1memcpy 模拟实现 2…

什么是 ERP?

目录 企业资源计划&#xff08;ERP&#xff09;的定义 ERP与财务管理的区别 ERP基础知识 ERP的业务价值 ERP简史 ERP部署模式&#xff1a;从本地部署到云端 ERP云 — 新的ERP交付模式 迁移至ERP云技术解决方案的7个原因 企业资源计划&#xff08;ERP&#xff09;的定义 …

线程-8-日志_线程池

主要看code 日志&#xff1a; 日志指标/属性 设计模式&#xff1a;策略模式 日志格式&#xff1a; [可读性很好的时间] [⽇志等级] [进程pid] [打印对应⽇志的⽂件名][⾏号] - 消息内容&#xff0c;⽀持可 变参数 [2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [16] - h…