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);}}}
}
效果展示
僵尸会对一条道路上的植物进行啃食,在啃食期间会正常受到豌豆射手的攻击,啃食结束后,植物死亡
十四 向日葵生成阳光
实现和原 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
十五 片头僵尸展示
优化片头效果,实现函数如下,开局会先展示路边的僵尸
/* 展示界面的僵尸相关变量 */
#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);}
}
在主函数中调用
效果展示
游戏开场会缓慢的移动窗口至马路边,停顿观察路边僵尸(僵尸会一摇一摇的抖动),然后游戏镜头会再缓慢移动至原界面
十六 植物栏滑动
在上述游戏界面拉回主界面过程中,植物菜单栏会缓慢滑动出现,具体实现如下
/* 植物栏滑动 */
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);
}
在主函数中调用
效果展示
在上述开场游戏界面拉回主界面过程中,植物菜单栏会缓慢滑动出现
十六 判断游戏结束
相关结构和变量
/* 游戏输赢相关的结构和变量 */
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 中 ,若杀死僵尸数大于或等于该局游戏僵尸数目,则改变游戏状态
原更新僵尸接口中,若僵尸已移动至最左端,则游戏失败
最后在 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;
}
效果展示
一些游戏体验优化
① 豌豆不能太提前射击僵尸
在射击接口 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 函数
效果展示
如果阳光值不够选取植物,则渲染为灰色,阳光值不够不能种植该植物;且植物有冷却时间,在冷却时间内植物不能种植
③ 添加各种音乐
加上音效
初始背景音乐
/* 游戏开始前的菜单界面 */
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();}
}
片头背景音乐
僵尸来了背景音乐
在 createZombie 接口中,添加如下代码
if (createZombies == 1) PlaySound("res/audio/zombiescoming.wav", NULL, SND_FILENAME | SND_ASYNC); //异步播放音效
选取植物背景音乐
种植物音乐,种到不合适地方的音乐
豌豆射击的音乐
花了两块大洋买了原曲,支持一下(其实是为了游戏背景曲,哈哈)
遗留问题
音频播放同时播放两个音频,可以实现功能就是没用到其它音频库,导致游戏试玩时当有大量音频需要加载播放时,会稍有卡顿,待有空找个 Win 音频三方库优化一下吧
全部源代码和资源文件待后续把项目上传
相关文章:

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

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

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

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

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

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

【笔记】在虚拟机中通过apache2给一个主机上配置多个web服务器
(配置出来的web服务器又叫虚拟主机……) 下载apache2 sudo apt update sudo apt install apache2 (一)ip相同 web端口不同的web服务器 进入 /var/www/html 创建站点一和站点二的目录文件(目录文件名自定义哈&#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,CUDA是11.8: 编译我们自己封装模型的某些component代码时没问题,编译一个封装occ模型的component代码时始终报错: In file included from /usr/include/eigen3/Eigen/Geometry:11:0, …...

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

前端页面展示本电脑的摄像头,并使用js获取摄像头列表
可以通过 JavaScript 使用 navigator.mediaDevices.enumerateDevices() 获取电脑上的摄像头列表。以下是一个示例代码,可以展示摄像头列表并选择进行预览。 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实现喜庆的新年快乐网页源码,春节新年网…...

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

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

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

使用Clion在ubuntu上进行交叉编译,并在Linux上远程编译五子棋
目录 1.工具以及概念介绍 (1)Clion软件简介 (2)交叉编译 (3)远程编译 2.操作原理 3.详细操作步骤 (1)配置Clion与虚拟机ubuntu的ssh连接 CLion远程开发Ubuntu,并显…...

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

3---杭州工作三年半
2021-07-06来杭——2025-01-01元旦 1滨江2021-07-06——2022-11-25(一年零四个月) 2下沙2023-01-01——2023-04-27(五个月) 3苏州2023-06-07——2023-06-27(一个月)厦门2023-06-29——2023-07-06ÿ…...

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

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

ElasticSearch7.8快速入门
文章目录 1.基本概念1.数据格式2.倒排索引 2.HTTP使用1.创建索引(相当于创建数据库)2.索引-查询 & 删除1.查询索引2.查询所有索引3.删除索引 3.创建文档1.创建文档(不指定id)2.创建文档(指定id) 4.文档…...

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

2024年度总结:保持正念 延迟满足
总结: 时光荏苒,很快就到了年底,2024年也就悄悄的过去了,回顾这一年,有很多的感触,在此做一个总结,留下自己的脚印 CTF: 要总结的第一件事那当然是CTF,回顾这一年&#…...

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

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

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

Type c系列接口驱动电路·内置供电驱动电路使用USB2.0驱动电路!!!
目录 前言 Type c常见封装类型 Type c引脚功能详解 Type c常见驱动电路详解 Type c数据手册 编写不易,仅供学习,请勿搬运,感谢理解 常见元器件驱动电路文章专栏连接 LM7805系列降压芯片驱动电路…...

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

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

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