当前位置: 首页 > news >正文

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

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

C 实现植物大战僵尸,完结撒花(还有个音频稍卡顿的性能问题,待有空优化解决)。目前基本的功能模块已经搭建好了,感兴趣的友友可自行尝试编写后续游戏内容

因为 C 站不能上传动图,所以游戏实际效果可看后续文章更新,插一条试玩视频

后面项目全部源代码会上传至 C 站(待上传)

十三 实现僵尸吃植物

实现和原 UP 有差异,僵尸捕获植物感觉很奇怪,不如设计成植物同样有血量,当植物血量为 0 时,植物死亡

调整植物和僵尸结构体,以及增加变量

/* 僵尸相关结构和变量 */
#define MAX_ZOMBIE_NUM 10
#define MAX_ZOMBIE_DEAD_PIC_NUM 10
#define MAX_ZOMBIE_EAT_PIC_NUM 21
#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 isEating;      //僵尸是否在吃植物, 这些状态改用枚举更好, 待优化bool used;          //是否在使用
} Zombie;
Zombie zombies[MAX_ZOMBIE_NUM];
IMAGE imgZombies[MAX_ZOMBIE_PIC_NUM];
IMAGE imgDeadZombies[MAX_ZOMBIE_DEAD_PIC_NUM];
IMAGE imgZombiesEat[MAX_ZOMBIE_EAT_PIC_NUM];/* 植物相关结构和变量 */
typedef struct Plant // 植物结构体
{int type;     //植物类型, -1 表示草地int frameId;  //表示植物摆动帧int blood;    //植物血量
} Plant;

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

for (int i = 0; i < MAX_ZOMBIE_EAT_PIC_NUM; ++i) //加载僵尸吃植物图片
{sprintf(name, "res/zm_eat/0/%d.png", i + 1);loadimage(&imgZombiesEat[i], name);
}

游戏更新窗口接口,渲染图片至输出窗口

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 if (zombies[i].isEating) putimagePNG(zombies[i].x, zombies[i].y + 30, &imgZombiesEat[zombies[i].frameId]);else putimagePNG(zombies[i].x, zombies[i].y + 30, &imgZombies[zombies[i].frameId]);}
}

更新游戏属性的接口,增加 eatPlants

/* 更新游戏属性的接口 */
void updateGame() 
{updatePlantsPic();createSunshine();updateSunshine();createZombie();updateZombie();shoot();updateBullets();collsionCheck();eatPlants();
}
/* 移除死亡的植物 */
Plant* plantDeath(Plant* plant)
{assert(plant);if (plant->type == PEA)  //释放对应种植植物内存free((PeaShooter*)plant);else if (plant->type == SUNFLOWER)free((SunFlower*)plant);Grass* grassPtr = (Grass*)calloc(1, sizeof(Grass)); //重置为草地assert(grassPtr);grassPtr->plant.type = -1;return (Plant*)grassPtr;
}/* 僵尸吃植物接口 */
void eatPlants()
{PeaShooter* peaShooter = NULL;int row = 0, plantX = 0, zombieCurrX = 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) {plantX = GRASS_LEFT_MARGIN + j * GRASS_GRID_WIDTH + 5; zombieCurrX = zombies[i].x + 80;if (zombieCurrX > plantX + 10 && zombieCurrX < plantX + 60) //当僵尸已经到达植物附近{zombies[i].isEating = true;plants[row][j]->blood -= 1; //植物扣血if (plants[row][j]->blood <= 0) //植物被杀死{plants[row][j] = plantDeath(plants[row][j]); //移除死亡的植物zombies[i].frameId = 0;zombies[i].isEating = false; //僵尸解除吃植物状态}}}}}}
}

最后更新僵尸状态,在这里进行帧处理

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 if (zombies[i].isEating){zombies[i].frameId = ++zombies[i].frameId % MAX_ZOMBIE_EAT_PIC_NUM; //僵尸更换图片帧}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);}}}
}

效果展示

僵尸会对一条道路上的植物进行啃食,在啃食期间会正常受到豌豆射手的攻击,啃食结束后,植物死亡

image-20241230220759638

十四 向日葵生成阳光

实现和原 UP 有差异,想保留原随机阳光球逻辑,所以这里是做了兼容处理逻辑,具体实现如下

向日葵结构体增加变量

enum SUN_SHINE_STATUS { UNUSED, PRODUCE, GROUND, COLLECT };/* 向日葵结构体 */
typedef struct SunFlower
{Plant plant;/* 这里也可以使用数组, 一个向日葵有多个阳光球成员*/SunShineBall sunShine; //向日葵生产的阳光球int timeInterval;             //向日葵生产阳光的计时器int status;       //向日葵生产的阳光球状态float t; //贝塞尔曲线时间点float speed; //阳光球移动速度vector2 p1, p2, p3, p4; //贝塞尔曲线位置点vector2 pCurr; //当前阳光球的位置
} SunFlower;

实现向日葵生产阳光的接口

需要注意的是在收集向日葵生产太阳球时,需要重置贝塞尔曲线

/* 实现向日葵生产太阳球 */
void produceSunShine()
{SunFlower* sunFlower = NULL;for (int i = 0; i < GRASS_GRID_ROW; ++i) //遍历二维指针数组{for (int j = 0; j < GRASS_GRID_COL; ++j){if (plants[i][j]->type == SUNFLOWER){sunFlower = (SunFlower*)plants[i][j];switch (sunFlower->status){case COLLECT:sunFlower->t += sunFlower->speed; //设置贝塞尔曲线开始时间sunFlower->pCurr = sunFlower->p1 +sunFlower->t * (sunFlower->p4 - sunFlower->p1); //构建贝塞尔曲线if (sunFlower->t > 1) { sunShineVal += 25;sunFlower->status = UNUSED;resetVecotrVal(sunFlower, i, j);}break;case GROUND:if (--sunFlower->timeInterval <= 0) //超时则阳光消失{sunFlower->status = UNUSED; //重置状态sunFlower->timeInterval = MAX_TIME_INTERVAL * (4 + rand() % 5);}break;case PRODUCE:sunFlower->t += sunFlower->speed; //设置贝塞尔曲线开始时间sunFlower->pCurr = calcBezierPoint(sunFlower->t,sunFlower->p1, sunFlower->p2, sunFlower->p3, sunFlower->p4); //构建贝塞尔曲线if (sunFlower->t > 1){sunFlower->t = 0;sunFlower->status = GROUND;sunFlower->timeInterval = MAX_TIME_INTERVAL * (4 + rand() % 5);}break;case UNUSED:if (--sunFlower->timeInterval <= 0){sunFlower->status = PRODUCE;sunFlower->timeInterval = MAX_TIME_INTERVAL * (4 + rand() % 5);}break;default:printf("ERROR");break;}}}}
}
/* 重置贝塞尔曲线坐标值 */
void resetVecotrVal(SunFlower* sunFlower, int x, int y)
{assert(sunFlower);if (sunFlower->status == COLLECT){sunFlower->p1 = sunFlower->pCurr;sunFlower->p4 = vector2(262, 0);sunFlower->t = 0;const float distance = dis(sunFlower->p1 - sunFlower->p4);sunFlower->speed = 1.0 / (distance / 16.0);}else if (sunFlower->status == UNUSED){const int distance = (50 + rand() % 50); //只往右抛即可const int currPlantX = GRASS_LEFT_MARGIN + y * GRASS_GRID_WIDTH + 5;const int currPlantY = GRASS_TOP_MARGIN + x * GRASS_GRID_HIGHT + 10;sunFlower->t = 0;sunFlower->timeInterval = MAX_TIME_INTERVAL * (4 + rand() % 5);sunFlower->speed = 0.05;sunFlower->p1 = vector2(currPlantX, currPlantY);sunFlower->p2 = vector2(sunFlower->p1.x + distance * 0.3, sunFlower->p1.y - 100);sunFlower->p3 = vector2(sunFlower->p1.x + distance * 0.7, sunFlower->p1.y - 100);sunFlower->p4 = vector2(currPlantX + distance, currPlantY +imgPlant[SUNFLOWER][0]->getheight() - imgSunShineBall[0].getheight());}
}

在更新游戏属性的接口中调用

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

其次,在种植向日葵的时候需要进行新增成员的初始化

/* 种植植物接口, 主要释放草格子内存, 二维指针数组对应位置,指向初始化的植物 */
Plant* growPlants(Plant* plant, int type, int x, int y)
{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->plant.blood = 100;return (Plant*)peaShooter;}else if (type == SUNFLOWER) //根据类型初始化 SunFlower{SunFlower* sunFlower = (SunFlower*)calloc(1, sizeof(SunFlower));assert(sunFlower);sunFlower->plant.type = 1;sunFlower->plant.blood = 100;sunFlower->timeInterval = MAX_TIME_INTERVAL * (4 + rand() % 5); //增加游戏随机性/* 初始化贝塞尔曲线 */const int distance = (50 + rand() % 50); //只往右抛即可const int currPlantX = GRASS_LEFT_MARGIN + y * GRASS_GRID_WIDTH + 5;const int currPlantY = GRASS_TOP_MARGIN + x * GRASS_GRID_HIGHT + 10;sunFlower->t = 0;sunFlower->speed = 0.05;sunFlower->p1 = vector2(currPlantX, currPlantY);sunFlower->p2 = vector2(sunFlower->p1.x + distance * 0.3, sunFlower->p1.y - 100);sunFlower->p3 = vector2(sunFlower->p1.x + distance * 0.7, sunFlower->p1.y - 100);sunFlower->p4 = vector2(currPlantX + distance, currPlantY +imgPlant[SUNFLOWER][0]->getheight() - imgSunShineBall[0].getheight());return (Plant*)sunFlower;}
}

在更新阳光球接口,添加新增更新向日葵生产阳光球帧的逻辑

/* 更新随机阳光球接口, 主要更新随机阳光球的图片帧和处理飞跃状态时的 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; //每次移动两个像素else //当阳光下落至目标位置时, 停止移动{if (balls[i].timer < MAX_TIME_INTERVAL) ++balls[i].timer;else balls[i].used = false;}balls[i].frameId = ++balls[i].frameId % SUM_SHINE_PIC_NUM; //修改当前图片帧编号, 并在到达 SUM_SHINE_PIC_NUM 时重置图片帧为 0}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;}}}/* 更新向日葵生产的日光 */SunFlower* sunFlower = NULL;for (int i = 0; i < GRASS_GRID_ROW; ++i) //遍历二维指针数组{for (int j = 0; j < GRASS_GRID_COL; ++j){if (plants[i][j]->type == SUNFLOWER){sunFlower = (SunFlower*)plants[i][j];if (sunFlower->status == GROUND || sunFlower->status == PRODUCE)sunFlower->sunShine.frameId = ++sunFlower->sunShine.frameId % SUM_SHINE_PIC_NUM; //修改当前图片帧编号, 并在到达 SUM_SHINE_PIC_NUM 时重置图片帧为 0 }}}
}

在收集随机阳光接口中添加上收集向日葵生产的日光 新增逻辑

/* 收集随机阳光接口 */
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 轴偏移}}}/* 收集向日葵生产的日光 */SunFlower* sunFlower = NULL;for (int i = 0; i < GRASS_GRID_ROW; ++i) //遍历二维指针数组{for (int j = 0; j < GRASS_GRID_COL; ++j){if (plants[i][j]->type == SUNFLOWER){sunFlower = (SunFlower*)plants[i][j];imgSunShine = &imgSunShineBall[sunFlower->sunShine.frameId]; //找到对应的阳光球图片if (sunFlower->status == GROUND) {if (msg->x > sunFlower->pCurr.x && msg->x < sunFlower->pCurr.x + imgSunShine->getwidth()&& msg->y > sunFlower->pCurr.y && msg->y < sunFlower->pCurr.y + imgSunShine->getheight()) //判断鼠标移动的位置是否处于当前阳光球的位置{PlaySound("res/audio/sunshine.wav", NULL, SND_FILENAME | SND_ASYNC); //异步播放收集阳光球音效sunFlower->status = COLLECT;resetVecotrVal(sunFlower, i, j); //更改曲线坐标}}}}}
}

最后只需要在 updateWindow 接口中渲染一下向日葵生产的阳光即可

SunFlower* sunFlower = NULL;
for (int i = 0; i < GRASS_GRID_ROW; ++i) //渲染向日葵阳光
{for (int j = 0; j < GRASS_GRID_COL; ++j){if (plants[i][j]->type == SUNFLOWER){sunFlower = ((SunFlower*)plants[i][j]);if (sunFlower->status > UNUSED){putimagePNG(sunFlower->pCurr.x, sunFlower->pCurr.y,&imgSunShineBall[sunFlower->sunShine.frameId]);}}      }
}

效果展示

向日葵可以生产阳光,生产阳光球后会以类似抛物线的形式(贝塞尔曲线)随机掉落在右一格的位置。鼠标移动至阳光球处,阳光将会被收集,阳光值增加 25

image-20241231115916738

十五 片头僵尸展示

优化片头效果,实现函数如下,开局会先展示路边的僵尸

/* 展示界面的僵尸相关变量 */
#define VIEW_ZOMBIE_NUM 9
#define VIEW_ZOMBIE_PIC_NUM 11
IMAGE imgViewZombies[VIEW_ZOMBIE_PIC_NUM];/* 游戏开始前展示僵尸 */
void viewScence()
{int Xmin = WIN_WIDTH - imgBg.getwidth(); //-500vector2 zombieVec[VIEW_ZOMBIE_NUM] = {   //展示场景中, 僵尸初始位置{550,80},{530,160},{630,170},{530,200},{515,270},{565,370},{605,340},{705,280},{690,340}};int frameIndexArr[VIEW_ZOMBIE_NUM];for (int i = 0; i < VIEW_ZOMBIE_NUM; ++i)frameIndexArr[i] = rand() % VIEW_ZOMBIE_PIC_NUM;int cycleNum = 0; //利用循环计数, 解决僵尸抖动过快for (int x = 0; x >= Xmin; x -= 2) //缓慢移动展示僵尸{BeginBatchDraw(); //双缓冲解决闪屏putimage(x, 0, &imgBg);++cycleNum; //当循环十次后, 更换每只僵尸的帧图片for (int i = 0; i < VIEW_ZOMBIE_NUM; ++i) //循环僵尸个数{putimagePNG(zombieVec[i].x - Xmin + x, zombieVec[i].y, &imgViewZombies[frameIndexArr[i]]); //渲染僵尸图片if (cycleNum > 2)frameIndexArr[i] = (++frameIndexArr[i]) % VIEW_ZOMBIE_PIC_NUM; //更换帧图}if (cycleNum > 2) cycleNum = 0; //重置循环计数EndBatchDraw();Sleep(5);}//停留 3 S 展示for (int k = 0; k < MAX_TIME_INTERVAL / 2; ++k){BeginBatchDraw(); //双缓冲解决闪屏putimage(Xmin, 0, &imgBg); //相当于把图片向左移动 500 个像素for (int i = 0; i < VIEW_ZOMBIE_NUM; ++i) //循环僵尸个数{putimagePNG(zombieVec[i].x, zombieVec[i].y, &imgViewZombies[frameIndexArr[i]]); //渲染僵尸图片frameIndexArr[i] = (++frameIndexArr[i]) % VIEW_ZOMBIE_PIC_NUM; //更换帧图}EndBatchDraw();Sleep(30);}//移动回主界面cycleNum = 0;for (int x = Xmin; x <= 0; x += 2){BeginBatchDraw(); //双缓冲解决闪屏putimage(x, 0, &imgBg);++cycleNum; //当循环十次后, 更换每只僵尸的帧图片for (int i = 0; i < VIEW_ZOMBIE_NUM; ++i) //循环僵尸个数{if (zombieVec[i].x - Xmin + x > 0){putimagePNG(zombieVec[i].x - Xmin + x, zombieVec[i].y, &imgViewZombies[frameIndexArr[i]]); //渲染僵尸图片if (cycleNum > 2)frameIndexArr[i] = (++frameIndexArr[i]) % VIEW_ZOMBIE_PIC_NUM; //更换帧图}}if (cycleNum > 2) cycleNum = 0; //重置循环计数EndBatchDraw();Sleep(5);}
}

在主函数中调用

image-20241231191236192

效果展示

游戏开场会缓慢的移动窗口至马路边,停顿观察路边僵尸(僵尸会一摇一摇的抖动),然后游戏镜头会再缓慢移动至原界面

十六 植物栏滑动

在上述游戏界面拉回主界面过程中,植物菜单栏会缓慢滑动出现,具体实现如下

/* 植物栏滑动 */
void barsDown()
{int imgBarHeight = imgBar.getheight();for (int i = -imgBarHeight; i <= 6; ++i) //这里因为微调了植物卡片位置为 6{BeginBatchDraw();putimage(0, 0, &imgBg);             //渲染地图if (i <= 0) putimagePNG(250, i, &imgBar); //但植物栏的位置为 0else putimagePNG(250, 0, &imgBar); //渲染植物栏for (int j = 0; j < PLANT_CNT; ++j) //遍历植物卡牌putimage(PIC_LEFT_MARGIN + j * PIC_WIDTH, i, &imgCards[j]); //渲染植物卡牌    EndBatchDraw();Sleep(10);}Sleep(1000);
}

在主函数中调用

image-20241231191614388

效果展示

在上述开场游戏界面拉回主界面过程中,植物菜单栏会缓慢滑动出现

image-20241231191802974

十六 判断游戏结束

相关结构和变量

/* 游戏输赢相关的结构和变量 */
enum { GAMEING, WIN, FAIL };
#define INGAME_ZOMBIE_NUM 15
int killZombies = 0;
int gameStatus = GAMEING;

创建僵尸接口时判断杀死的僵尸是否满足该局僵尸的数目了,如果是则不再创建

/* 创建僵尸接口, 主要用于初始化僵尸 */
void createZombie()
{if (killZombies >= INGAME_ZOMBIE_NUM) return;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].isEating = false;zombies[i].used = true;break; //结束循环}}
}

在原子弹和僵尸碰撞接口 collsionCheck 中 ,若杀死僵尸数大于或等于该局游戏僵尸数目,则改变游戏状态

image-20241231192309251

原更新僵尸接口中,若僵尸已移动至最左端,则游戏失败

image-20241231192447971

最后在 main 函数中调用检验游戏状态的函数,即可判断游戏输赢

checkGameOver 会用到 在线 MP3 音频转 WAV

/* 判断游戏输赢 */
IMAGE imgGameOver; //工具栏图片
bool checkGameOver()
{if (gameStatus == WIN){Sleep(500);PlaySound("res/audio/win.wav", NULL, SND_FILENAME | SND_ASYNC); //异步播放音效loadimage(0, "res/gameWin.png");return true;}else if (gameStatus == FAIL){Sleep(500);PlaySound("res/audio/lose.wav", NULL, SND_FILENAME | SND_ASYNC); //异步播放音效loadimage(&imgGameOver, "res/gameFail.png");putimagePNG(300, 140, &imgGameOver);return true;}return false;
}/* 主函数 */
int main()
{gameInit(); //不能把 startUI 放在 gameInit 前, gameInit 包含了创建游戏图形窗口startUI();viewScence();barsDown();updateWindow(); //窗口视图展示int timer = 0; //用以计时 20 毫秒更新一次while (1){userClick(); //监听窗口鼠标事件timer += getDelay();if (timer > 20){updateWindow(); //更新窗口视图updateGame(); //更新游戏动画帧if (checkGameOver()) break; //判断游戏输赢timer = 0;}}destroyPlants(); //释放内存system("pause");return 0;
}

效果展示

image-20241231203102248

一些游戏体验优化

① 豌豆不能太提前射击僵尸

在射击接口 shoot 里,校验僵尸和窗口右端的距离即可

② 卡牌太阳值不够不能选取

如果阳光值不够选取植物,则渲染为灰色,阳光值不够不能种植该植物;且植物有冷却时间,在冷却时间内植物不能种植

/* 游戏体验优化, 阳光值不足或植物冷却时不能种植 */
IMAGE imgBlackCards[PLANT_CNT]; //植物不能种植卡片
IMAGE imgFreezeCards[PLANT_CNT]; //植物冷却卡片
#define PEA_FREEZE_TIME 500
#define SUMFLOWER_FREEZE_TIME 200
static int peaPlantInterval = 500;
static int sumFlowerPlantInterval = 200;enum PLANT_CARD_STATUS { BRIGHT, GREY, FREEZE };
int plantCardStatus[PLANT_CNT]; //植物卡片状态数组

更新植物卡牌状态函数代码

/* 更新植物卡牌状态 */
void updatePlantCardStatus()
{for (int i = 0; i < PLANT_CNT; ++i) //判断植物卡牌状态{if (i == PEA){if (sunShineVal < 100)                  //阳光值不够plantCardStatus[i] = GREY;          //卡片灰色else if (sunShineVal >= 100 && peaPlantInterval < PEA_FREEZE_TIME) //阳光值够但在冷却时间内plantCardStatus[i] = FREEZE;        //卡片冻结elseplantCardStatus[i] = BRIGHT;        //卡片原色}else if (i == SUNFLOWER){if (sunShineVal < 50)plantCardStatus[i] = GREY;else if (sunShineVal >= 50 && sumFlowerPlantInterval < SUMFLOWER_FREEZE_TIME)plantCardStatus[i] = FREEZE;elseplantCardStatus[i] = BRIGHT;}}
}

修改植物栏滑动逻辑

/* 植物栏滑动 */
void barsDown()
{int imgBarHeight = imgBar.getheight();updatePlantCardStatus();for (int i = -imgBarHeight; i <= 6; ++i) //这里因为微调了植物卡片位置为 6{BeginBatchDraw();putimage(0, 0, &imgBg);             //渲染地图if (i <= 0) putimagePNG(250, i, &imgBar); //但植物栏的位置为 0else putimagePNG(250, 0, &imgBar); //渲染植物栏for (int j = 0; j < PLANT_CNT; ++j) //遍历植物卡牌{if (plantCardStatus[j] == BRIGHT)putimage(PIC_LEFT_MARGIN + j * PIC_WIDTH, i, &imgCards[j]);  //渲染植物卡牌else if (plantCardStatus[j] == GREY)putimage(PIC_LEFT_MARGIN + j * PIC_WIDTH, i, &imgBlackCards[j]);elseputimage(PIC_LEFT_MARGIN + j * PIC_WIDTH, i, &imgFreezeCards[j]);}  EndBatchDraw();Sleep(10);}Sleep(1000);
}

种植植物时记得扣除太阳值和重置冷却

/* 种植植物接口, 主要释放草格子内存, 二维指针数组对应位置,指向初始化的植物 */
Plant* growPlants(Plant* plant, int type, int x, int y)
{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->plant.blood = 100;//扣除太阳值和重置冷却sunShineVal -= 100;peaPlantInterval = 0;updatePlantCardStatus();return (Plant*)peaShooter;}else if (type == SUNFLOWER) //根据类型初始化 SunFlower{SunFlower* sunFlower = (SunFlower*)calloc(1, sizeof(SunFlower));assert(sunFlower);sunFlower->plant.type = 1;sunFlower->plant.blood = 100;sunFlower->timeInterval = MAX_TIME_INTERVAL * (4 + rand() % 5);/* 初始化贝塞尔曲线 */const int distance = (50 + rand() % 50); //只往右抛即可const int currPlantX = GRASS_LEFT_MARGIN + y * GRASS_GRID_WIDTH + 5;const int currPlantY = GRASS_TOP_MARGIN + x * GRASS_GRID_HIGHT + 10;sunFlower->t = 0;sunFlower->speed = 0.05;sunFlower->p1 = vector2(currPlantX, currPlantY);sunFlower->p2 = vector2(sunFlower->p1.x + distance * 0.3, sunFlower->p1.y - 100);sunFlower->p3 = vector2(sunFlower->p1.x + distance * 0.7, sunFlower->p1.y - 100);sunFlower->p4 = vector2(currPlantX + distance, currPlantY +imgPlant[SUNFLOWER][0]->getheight() - imgSunShineBall[0].getheight());sunShineVal -= 50;  //扣除太阳值和重置冷却sumFlowerPlantInterval = 0;updatePlantCardStatus();return (Plant*)sunFlower;}
}

原 updatePlantsPic 接口中更新 peaPlantInterval 和 sumFlowerPlantInterval

/* 更新植物图片帧接口, 主要用于实现植物摇摆 */
void updatePlantsPic()
{++peaPlantInterval;++sumFlowerPlantInterval;updatePlantCardStatus();for (int i = 0; i < GRASS_GRID_ROW; ++i) //遍历二维指针数组{for (int j = 0; j < GRASS_GRID_COL; ++j){if (plants[i][j]->type >= PEA && //找到非草地的植物imgPlant[plants[i][j]->type][++plants[i][j]->frameId] == NULL) //将植物图片增加一, 判断是否到达图片帧末尾            plants[i][j]->frameId = 0; //重置图片帧为零}}
}

最后修改渲染卡片窗口的 updateWindow 函数

image-20241231205401264

效果展示

如果阳光值不够选取植物,则渲染为灰色,阳光值不够不能种植该植物;且植物有冷却时间,在冷却时间内植物不能种植

image-20241231205535580

③ 添加各种音乐

加上音效

初始背景音乐

/* 游戏开始前的菜单界面 */
void startUI()
{IMAGE imageBg, imgMenu1, imgMenu2;loadimage(&imageBg, "res/menu.png");loadimage(&imgMenu1, "res/menu1.png");loadimage(&imgMenu2, "res/menu2.png");PlaySound("res/audio/bg.wav", NULL, SND_FILENAME | SND_ASYNC); //异步播放音效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) //当鼠标点击时, 进入游戏{PlaySound(0, 0, SND_FILENAME);EndBatchDraw();return; //结束函数}}else mouseStatus = false; //当鼠标未移动至开始游戏位置, 界面不高亮}EndBatchDraw();}
}

片头背景音乐

image-20241231233818079

僵尸来了背景音乐

在 createZombie 接口中,添加如下代码

if (createZombies == 1) PlaySound("res/audio/zombiescoming.wav", NULL, SND_FILENAME | SND_ASYNC); //异步播放音效

image-20241231233933248

选取植物背景音乐

image-20241231234142778

种植物音乐,种到不合适地方的音乐

image-20241231234225477

豌豆射击的音乐

image-20241231234315880

花了两块大洋买了原曲,支持一下(其实是为了游戏背景曲,哈哈)

image-20241231233515466

遗留问题

音频播放同时播放两个音频,可以实现功能就是没用到其它音频库,导致游戏试玩时当有大量音频需要加载播放时,会稍有卡顿,待有空找个 Win 音频三方库优化一下吧

全部源代码和资源文件待后续把项目上传

相关文章:

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

C 实现植物大战僵尸&#xff08;四&#xff09; C 实现植物大战僵尸&#xff0c;完结撒花&#xff08;还有个音频稍卡顿的性能问题&#xff0c;待有空优化解决&#xff09;。目前基本的功能模块已经搭建好了&#xff0c;感兴趣的友友可自行尝试编写后续游戏内容 因为 C 站不能…...

Tailwind CSS:现代 CSS 框架的优雅之选

Tailwind CSS&#xff1a;现代 CSS 框架的优雅之选 在现代前端开发中&#xff0c;CSS 的灵活性和复杂性让开发者在设计与实现之间寻找平衡。而 Tailwind CSS 的出现&#xff0c;重新定义了 CSS 框架的使用方式。它是一种原子化的 CSS 工具库&#xff0c;提供了丰富的类名以快速…...

MyBatis 使用的设计模式详解

MyBatis 是一个优秀的持久层框架&#xff0c;它简化了 Java 应用程序与数据库之间的交互。为了实现高效、灵活且易于维护的代码&#xff0c;MyBatis 内部使用了多种设计模式。本文将详细介绍 MyBatis 中应用到的设计模式及其作用。 工厂模式&#xff08;Factory Pattern&#x…...

LabVIEW 中 NI Vision 模块的IMAQ Create VI

IMAQ Create VI 是 LabVIEW 中 NI Vision 模块&#xff08;NI Vision Development Module&#xff09;的一个常用 VI&#xff0c;用于创建一个图像变量。该图像变量可以存储和操作图像数据&#xff0c;是图像处理任务的基础。 ​ 通过以上操作&#xff0c;IMAQ Create VI 是构建…...

2024 年度总结

时光荏苒&#xff0c;2024 年即将画上句号&#xff0c;回顾这一年的写博历程&#xff0c;有付出、有收获、有成长&#xff0c;也有诸多值得回味与反思的瞬间。 一、内容创作 主题涉猎&#xff1a;这一年&#xff0c;我致力于探索多样化的主题&#xff0c;以满足不同读者群体的…...

STM32 高级 物联网通讯之LoRa通讯

目录 LoRa通讯基础知识 常见的3种通讯协议 远距离高速率的传输协议 近距离高速率传输技术 近距离低功耗传输技术 低功耗广域网 采用授权频段技术 非授权频段 LoRa简介 LoRa的特点 远距离 低功耗 安全 标准化 地理定位 移动性 高性能 低成本 LoRa应用 LoRa组…...

【笔记】在虚拟机中通过apache2给一个主机上配置多个web服务器

&#xff08;配置出来的web服务器又叫虚拟主机……&#xff09; 下载apache2 sudo apt update sudo apt install apache2 &#xff08;一&#xff09;ip相同 web端口不同的web服务器 进入 /var/www/html 创建站点一和站点二的目录文件&#xff08;目录文件名自定义哈&#x…...

数据库的创建与删除:理论与实践

title: 数据库的创建与删除:理论与实践 date: 2024/12/31 updated: 2024/12/31 author: cmdragon excerpt: 在当今的数字时代,数据的管理和存储变得尤为重要。数据库作为数据存储的结构化方案,为数据的增删改查提供了系统化的方法。在一个典型的数据库管理系统中,创建和…...

如何解决Eigen和CUDA版本不匹配引起的错误math_functions.hpp: No such file or directory

Apollo9针对RTX40的docker环境里的Eigen库版本是3.3.4&#xff0c;CUDA是11.8: 编译我们自己封装模型的某些component代码时没问题&#xff0c;编译一个封装occ模型的component代码时始终报错: In file included from /usr/include/eigen3/Eigen/Geometry:11:0, …...

Mybatis 01

JDBC回顾 select 语句 "select *from student" 演示&#xff1a; 驱动包 JDBC 的操作流程&#xff1a; 1. 创建数据库连接池 DataSource 2. 通过 DataSource 获取数据库连接 Connection 3. 编写要执⾏带 ? 占位符的 SQL 语句 4. 通过 Connection 及 SQL 创建…...

前端页面展示本电脑的摄像头,并使用js获取摄像头列表

可以通过 JavaScript 使用 navigator.mediaDevices.enumerateDevices() 获取电脑上的摄像头列表。以下是一个示例代码&#xff0c;可以展示摄像头列表并选择进行预览。 HTML JavaScript 实现摄像头列表展示和预览 <!DOCTYPE html> <html lang"zh-CN">…...

HTML5实现喜庆的新年快乐网页源码

HTML5实现喜庆的新年快乐网页源码 前言一、设计来源1.1 主界面1.2 关于新年界面1.3 新年庆祝活动界面1.4 新年活动组织界面1.5 新年祝福订阅界面1.6 联系我们界面 二、效果和源码2.1 动态效果2.2 源代码 源码下载结束语 HTML5实现喜庆的新年快乐网页源码&#xff0c;春节新年网…...

Excel文件恢复教程:快速找回丢失数据!

Excel文件恢复位置在哪里&#xff1f; Excel是微软开发的电子表格软件&#xff0c;它为处理数据和组织工作提供了便捷。虽然数据丢失的问题在数字时代已经司空见惯&#xff0c;但对于某些用户来说&#xff0c;恢复未保存/删除/丢失的Excel文件可能会很困难&#xff0c;更不用说…...

计算机网络-L2TP Over IPSec基础实验

一、概述 上次我们进行了标准L2TP的配置&#xff0c;但是在最后我们在进行业务流量访问时看到流量是没有进行加密的&#xff0c;这就导致可能得安全风险&#xff0c;所以这里其实可以退像GRE那样调用IPSec框架来进行加密保护。 拓扑 数据不加密 现在需要配置IPSec&#xff0c;然…...

一个最简单的ios程序(object_c)的编写

前言 如何在苹果系统MacOS创建一个简单的ios&#xff08;iphone&#xff09;程序&#xff0c;貌似非常的简单。但是&#xff0c;作为习惯了Windows开发的程序员来说&#xff0c;有时候还觉得有点麻烦&#xff0c;至少开始有点很不习惯。 本博文试着把这个过程展现一下&#xff…...

使用Clion在ubuntu上进行交叉编译,并在Linux上远程编译五子棋

目录 1.工具以及概念介绍 &#xff08;1&#xff09;Clion软件简介 &#xff08;2&#xff09;交叉编译 &#xff08;3&#xff09;远程编译 2.操作原理 3.详细操作步骤 &#xff08;1&#xff09;配置Clion与虚拟机ubuntu的ssh连接 CLion远程开发Ubuntu&#xff0c;并显…...

《QDebug 2024年12月》

一、Qt Widgets 问题交流 1. 二、Qt Quick 问题交流 1.QQuickPaintedItem 或者 QQuickItem 绘制的图片 dpi 缩放后模糊 启用 Qt 自带的缩放后&#xff0c;界面会跟随系统设置的 dpi 进行放大缩小&#xff1a; #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)QCoreApplicat…...

3---杭州工作三年半

2021-07-06来杭——2025-01-01元旦 1滨江2021-07-06——2022-11-25&#xff08;一年零四个月&#xff09; 2下沙2023-01-01——2023-04-27&#xff08;五个月&#xff09; 3苏州2023-06-07——2023-06-27&#xff08;一个月&#xff09;厦门2023-06-29——2023-07-06&#xff…...

从2024看2025前端发展趋势

前言 又至年关&#xff0c;回顾整个2024年&#xff0c;前端行业仍旧百废待兴&#xff0c;IT业界同样也未见有所起色&#xff0c;AI风潮也从狂热兴奋逐步走向了冷静稳定阶段&#xff0c;造成此形势感观并非单一行业或者某一企业之特例&#xff0c;实为政经等综合影响之结果。因…...

网络渗透测试实验三:SQL注入

1.实验目的和要求 实验目的:了解SQL注入的基本原理;掌握PHP脚本访问MySQL数据库的基本方法;掌握程序设计中避免出现SQL注入漏洞的基本方法;掌握网站配置。 系统环境:Kali Linux 2、Windows Server 网络环境:交换网络结构 实验工具: SqlMAP;DVWA 2.实验步骤 实验目…...

ElasticSearch7.8快速入门

文章目录 1.基本概念1.数据格式2.倒排索引 2.HTTP使用1.创建索引&#xff08;相当于创建数据库&#xff09;2.索引-查询 & 删除1.查询索引2.查询所有索引3.删除索引 3.创建文档1.创建文档&#xff08;不指定id&#xff09;2.创建文档&#xff08;指定id&#xff09; 4.文档…...

【YashanDB知识库】hive初始化崖山报错YAS-04209

本文内容来自YashanDB官网&#xff0c;原文内容请见 https://www.yashandb.com/newsinfo/7849008.html?templateId1718516 【问题分类】功能使用 【关键字】hadoop&#xff0c;hive&#xff0c;YAS-02058 【问题描述】hive初始化崖山报错&#xff1a; 0: jdbc:yasdb://192…...

2024年度总结:保持正念 延迟满足

总结&#xff1a; 时光荏苒&#xff0c;很快就到了年底&#xff0c;2024年也就悄悄的过去了&#xff0c;回顾这一年&#xff0c;有很多的感触&#xff0c;在此做一个总结&#xff0c;留下自己的脚印 CTF&#xff1a; 要总结的第一件事那当然是CTF&#xff0c;回顾这一年&#…...

VScode SSH 错误:Got bad result from install script 解決

之前vscode好好的&#xff0c;某天突然连接报错如下 尝试1. 服务器没有断开,ssh可以正常连接 2. 用管理员权限运行vscode&#xff0c;无效 3. 删除服务器上的~/.vscode-server 文件夹&#xff0c;无效 试过很多后&#xff0c;原来很可能是前一天anaconda卸载导致注册表项 步…...

Logo设计免费生成器工具:轻松创建独特标志

在当今的商业世界中&#xff0c;一个独特且引人注目的Logo是任何企业或品牌的身份象征。它不仅代表了公司的形象&#xff0c;还传达了公司的价值观和使命。然而&#xff0c;对于许多初创企业或小型企业来说&#xff0c;聘请专业设计师来设计一个Logo可能是一笔不小的开销。这时…...

[算法] [leetcode-349] 两个数组的交集

349 两个数组的交集 给定两个数组 nums1 和 nums2 &#xff0c;返回 它们的 交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序 。 示例 1&#xff1a; 输入&#xff1a;nums1 [1,2,2,1], nums2 [2,2] 输出&#xff1a;[2] 示例 2&#xff1a; …...

Type c系列接口驱动电路·内置供电驱动电路使用USB2.0驱动电路!!!

目录 前言 Type c常见封装类型 Type c引脚功能详解 Type c常见驱动电路详解 Type c数据手册 ​​​​​​​ ​​​​​​​ 编写不易&#xff0c;仅供学习&#xff0c;请勿搬运&#xff0c;感谢理解 常见元器件驱动电路文章专栏连接 LM7805系列降压芯片驱动电路…...

第7章 程序流程控制 - 条件分支

汇编语言是一种低级编程语言&#xff0c;它与特定计算机架构的机器码有着直接对应关系。条件分支是程序流程控制的一部分&#xff0c;允许根据某些条件来决定执行哪一段代码。在汇编中&#xff0c;这通常通过比较指令和跳转指令来实现。 以下是一些经典的汇编语言源代码示例&a…...

Edge如何获得纯净的启动界面

启动Edge会出现快速链接&#xff0c;推广链接&#xff0c;网站导航&#xff0c;显示小组件&#xff0c;显示信息提要&#xff0c;背景 ●复杂页面 ●精简页面 点击页面设置按钮 关闭快速链接 关闭网站导航 关闭小组件 关闭信息提要 关闭背景 关闭天气提示 精简页面看起来十分舒…...

gitlab的搭建及使用

1、环境准备 服务器准备 CentOS Linux release 7.9.2009 (Core)&#xff0c;内存至少4G。 修改主机名和配置ip地址 hostnamectl set-hostname <hostname> 关闭主机的防火墙 # 关闭防火墙 systemctl stop firewalld #临时关闭防火墙 systemctl disable firewalld …...