当前位置:   article > 正文

【C语言课设】经典植物大战僵尸丨完整开发教程+笔记+源码_植物大战僵尸c语言源代码

植物大战僵尸c语言源代码

前言

植物对抗僵尸是一款经典小游戏,初学者可以从零开始开发自己的版本,这将是一次令人期待的经历!它可以作为课程设计的一部分,也可以用来快速提升项目开发技能。

视频教程+素材资源

点击进入B站:(视频教程+素材资源
说明:项目演示视频,在上面链接里的第一个视频,下面只展示项目截图..

项目准备

  • 安装Visual Studio的任意版本(推荐VS2019社区版、VS2022社区版)

  • 领取项目素材(回复“植物大战僵尸”,即可领取)

创建项目

使用VS创建项目,使用空项目模板:

导入素材 :

在项目目录下,创建res文件夹,把解压后的素材拷贝到res目录下。

实现游戏初始场景

代码如下(需要逐行代码视频讲解,可回复“代码讲解“)。

  1. #include <stdio.h>
  2. #include <graphics.h>
  3. #include "tools.h"
  4. #include <mmsystem.h>
  5. #pragma comment(lib, "winmm.lib")
  6. #define WIN_WIDTH 900
  7. #define WIN_HEIGHT 600
  8. enum { WAN_DOU, XIANG_RI_KUI, ZHI_WU_COUT };
  9. IMAGE imgBg;
  10. IMAGE imgBar;
  11. IMAGE imgCards[ZHI_WU_COUT];
  12. IMAGE* imgZhiWu[ZHI_WU_COUT][20];
  13. int curZhiWu;
  14. int curX, curY; //当前选中植物在移动过程中的坐标
  15. struct zhiWu {
  16. int type; // >=1 0:没有植物
  17. int frameIndex;
  18. };
  19. struct zhiWu map[3][9];
  20. int sunshine;
  21. int sunshineTable[ZHI_WU_COUT] = { 100, 50 };
  22. void gameInit() {
  23. loadimage(&imgBg, "res/bg.jpg");
  24. loadimage(&imgBar, "res/bar.png");
  25. sunshine = 150;
  26. curZhiWu = 0;
  27. memset(imgZhiWu, 0, sizeof(imgZhiWu));
  28. memset(map, 0, sizeof(map));
  29. char name[64];
  30. for (int i = 0; i < ZHI_WU_COUT; i++) {
  31. sprintf_s(name, sizeof(name), "res/Cards/card_%d.png", i + 1);
  32. loadimage(&imgCards[i], name);
  33. for (int j = 0; j < 20; j++) {
  34. sprintf_s(name, sizeof(name), "res/zhiwu/%d/%d.png", i, j + 1);
  35. imgZhiWu[i][j] = new IMAGE;
  36. loadimage(imgZhiWu[i][j], name);
  37. if (imgZhiWu[i][j]->getwidth() == 0) {
  38. delete imgZhiWu[i][j];
  39. imgZhiWu[i][j] = NULL;
  40. }
  41. }
  42. }
  43. initgraph(WIN_WIDTH, WIN_HEIGHT, 1);
  44. // 设置字体:
  45. LOGFONT f;
  46. gettextstyle(&f); // 获取当前字体设置
  47. f.lfHeight = 30; // 设置字体高度为 48
  48. f.lfWidth = 15;
  49. strcpy(f.lfFaceName, "Segoe UI Black");
  50. f.lfQuality = ANTIALIASED_QUALITY; // 设置输出效果为抗锯齿
  51. settextstyle(&f); // 设置字体样式
  52. setbkmode(TRANSPARENT);
  53. setcolor(BLACK);
  54. mciSendString("play res/bg.mp3 repeat", 0, 0, 0);
  55. }
  56. void updateWindow() {
  57. BeginBatchDraw();
  58. putimage(0, 0, &imgBg);
  59. putimagePNG(250, 0, &imgBar);
  60. for (int i = 0; i < ZHI_WU_COUT; i++) {
  61. int x = 338 + i * 64;
  62. int y = 6;
  63. putimage(x, y, &imgCards[i]);
  64. }
  65. if (curZhiWu > 0) { // 绘制正在移动的植物
  66. IMAGE* img = imgZhiWu[curZhiWu - 1][0];
  67. putimagePNG(curX - img->getwidth() * 0.5, curY - img->getheight() * 0.5, img);
  68. }
  69. for (int i = 0; i < 3; i++) {
  70. for (int j = 0; j < 9; j++) {
  71. if (map[i][j].type > 0) {
  72. int x = 260 + j * 81.6; // (msg.x - 260) / 81.6;
  73. int y = 180 + i * 103.6 + 14; // (msg.y - 210) / 103.6;
  74. int zhiWuIndex = map[i][j].type;
  75. int frameIndex = map[i][j].frameIndex;
  76. putimagePNG(x, y, imgZhiWu[zhiWuIndex - 1][frameIndex]);
  77. }
  78. }
  79. }
  80. char scoreText[8];
  81. sprintf_s(scoreText, sizeof(scoreText), "%d", sunshine);
  82. outtextxy(282 - 10 + 4, 50 + 15 + 2, scoreText);
  83. EndBatchDraw();
  84. }
  85. void userClick() {
  86. ExMessage msg;
  87. static int status = 0;
  88. if (peekmessage(&msg)) {
  89. if (msg.message == WM_LBUTTONDOWN) {
  90. if (msg.x > 338 && msg.x < 338 + 64 * ZHI_WU_COUT && msg.y>6 && msg.y < 96) {
  91. int index = (msg.x - 338) / 64;
  92. printf("%d\n", index);
  93. status = 1;
  94. curZhiWu = index + 1; // 1, 2
  95. curX = msg.x;
  96. curY = msg.y;
  97. }
  98. }
  99. else if (msg.message == WM_MOUSEMOVE && status == 1) {
  100. curX = msg.x;
  101. curY = msg.y;
  102. }
  103. else if (msg.message == WM_LBUTTONUP && status == 1) {
  104. printf("up\n");
  105. if (msg.x > 260 && msg.y < 995 && msg.y > 180 && msg.y < 491) {
  106. if (sunshine >= sunshineTable[curZhiWu - 1]) {
  107. sunshine -= sunshineTable[curZhiWu - 1];
  108. int col = (msg.x - 260) / 81.6;
  109. int row = (msg.y - 210) / 103.6;
  110. printf("[%d,%d]\n", row, col);
  111. if (map[row][col].type == 0) {
  112. map[row][col].type = curZhiWu;
  113. map[row][col].frameIndex = 0;
  114. }
  115. }
  116. }
  117. status = 0;
  118. curZhiWu = 0;
  119. }
  120. }
  121. }
  122. void updateGame() {
  123. for (int i = 0; i < 3; i++) {
  124. for (int j = 0; j < 9; j++) {
  125. if (map[i][j].type > 0) {
  126. map[i][j].frameIndex++;
  127. if (imgZhiWu[map[i][j].type - 1][map[i][j].frameIndex] == NULL) {
  128. map[i][j].frameIndex = 0;
  129. }
  130. }
  131. }
  132. }
  133. }
  134. int main(void) {
  135. gameInit();
  136. int timer = 0;
  137. bool flag = true;
  138. while (1) {
  139. userClick();
  140. timer += getDelay();
  141. if (timer > 20) {
  142. timer = 0;
  143. flag = true;
  144. }
  145. if (flag) {
  146. flag = false;
  147. updateWindow();
  148. updateGame();
  149. }
  150. }
  151. return 0;
  152. }

添加启动菜单

创建菜单界面,代码如下:

  1. void startUI() {
  2. IMAGE imgBg, imgMenu1, imgMenu2;
  3. loadimage(&imgBg, "res/menu.png");
  4. loadimage(&imgMenu1, "res/menu1.png");
  5. loadimage(&imgMenu2, "res/menu2.png");
  6. int flag = 0;
  7. while (1) {
  8. BeginBatchDraw();
  9. putimage(0, 0, &imgBg);
  10. putimagePNG(474, 75, flag ? &imgMenu2 : &imgMenu1);
  11. ExMessage msg;
  12. if (peekmessage(&msg)) {
  13. if (msg.message == WM_LBUTTONDOWN &&
  14. msg.x > 474 && msg.x < 474 + 300 && msg.y > 75 && msg.y < 75 + 140) {
  15. flag = 1;
  16. EndBatchDraw();
  17. }
  18. else if (msg.message == WM_LBUTTONUP && flag) {
  19. return;
  20. }
  21. }
  22. EndBatchDraw();
  23. }
  24. }

在main函数中调用菜单,代码如下:

  1. int main(void) {
  2. gameInit();
  3. startUI();
  4. int timer = 0;
  5. bool flag = true;
  6. while (1) {
  7. userClick();
  8. timer += getDelay();
  9. if (timer > 20) {
  10. timer = 0;
  11. flag = true;
  12. }
  13. if (flag) {
  14. flag = false;
  15. updateWindow();
  16. updateGame();
  17. }
  18. }
  19. return 0;
  20. }

生产阳光


熟悉植物大战僵尸的同学都知道,种植植物才能消灭僵尸,但是种植植物,需要先具备一定数量的阳光值。初始的阳光值很小。

有两种方式生成阳光:

第一种,随机降落少量的阳光;

第二种,通过种植向日葵,让向日葵自动生产阳光。我们先实现第一种方式。

定义一个结构体,来表示阳光球。因为阳光是以旋转的方式运动的,所以定义一个图片帧数组,通过循环播放图片帧来实现旋转效果。

  1. IMAGE imgSunshineBall[29];
  2. struct sunshineBall {
  3. int x, y;
  4. int frameIndex;
  5. bool used;
  6. int destY;
  7. int timer = 0;
  8. };
  9. struct sunshineBall balls[10];

在gameInit函数中,初始化阳光帧数组。

  1. memset(balls, 0, sizeof(balls));
  2. for (int i = 0; i < 29; i++) {
  3. sprintf_s(name, sizeof(name), "res/sunshine/%d.png", i + 1);
  4. loadimage(&imgSunshineBall[i], name);
  5. }

创建阳光,代码如下。

  1. void createSunshine() {
  2. int ballMax = sizeof(balls) / sizeof(balls[0]);
  3. static int frameCount = 0;
  4. static int fre = 400;
  5. frameCount++;
  6. if (frameCount >= fre) {
  7. fre = 200 + rand() % 200;
  8. frameCount = 0;
  9. int i;
  10. for (i = 0; i < ballMax && balls[i].used; i++);
  11. if (i >= ballMax) return;
  12. balls[i].used = true;
  13. balls[i].frameIndex = 0;
  14. balls[i].x = 260 + rand() % (905 - 260);
  15. balls[i].y = 60;
  16. balls[i].destY = 180 + (rand() % 4) * 90 + 20;
  17. balls[i].timer = 0;
  18. }
  19. }

修改阳光的位置和帧序号,代码如下。

  1. void updateSunshine() {
  2. int ballMax = sizeof(balls) / sizeof(balls[0]);
  3. for (int i = 0; i < ballMax; i++) {
  4. if (balls[i].used) {
  5. balls[i].frameIndex = (balls[i].frameIndex + 1) % 29;
  6. if(balls[i].timer == 0) balls[i].y += 2;
  7. if (balls[i].y >= balls[i].destY) {
  8. balls[i].timer++;
  9. if (balls[i].timer > 100) balls[i].used = false;
  10. }
  11. }
  12. }
  13. }

在updateGame函数中调用以上两个函数 ,以创建阳光并更新阳光的状态。

  1. createSunshine();
  2. updateSunshine();

在updateWindow函数中,渲染阳光。

  1. for (int i = 0; i < 10; i++) {
  2. if (balls[i].used) {
  3. putimagePNG(balls[i].x, balls[i].y, &imgSunshineBall[balls[i].frameIndex]);
  4. }
  5. }

收集阳光

当“阳光球”出现的时候,用户点击阳光球,就可以“收集”这个阳光,当前总的阳光值就会增加25点。在原版的植物大战僵尸游戏中,阳光球被收集后,会慢慢移动到顶部的“工具栏”的左侧。这个阳光球的“移动过程”,我们后续再实现。

定义一个全局变量,表示当前总的阳光值。

int sunshine;

在初始化gameInit中,设置一个初始值。

sunshine = 150;

创建收集阳光的函数,如下:

  1. void collectSunshine(ExMessage* msg) {
  2. int count = sizeof(balls) / sizeof(balls[0]);
  3. int w = imgSunshineBall[0].getwidth();
  4. int h = imgSunshineBall[0].getheight();
  5. for (int i = 0; i < count; i++) {
  6. if (balls[i].used) {
  7. int x = balls[i].x;
  8. int y = balls[i].y;
  9. if (msg->x > x && msg->x < x + w && msg->y > y && msg->y < y + h) {
  10. balls[i].used = false;
  11. sunshine += 25;
  12. mciSendString("play res/sunshine.mp3", 0, 0, 0);
  13. }
  14. }
  15. }
  16. }

在用户点击处理中,调用收集阳光的函数。

  1. #include <mmsystem.h>
  2. #pragma comment(lib, "winmm.lib")
  3. void userClick() {
  4. ExMessage msg;
  5. static int status = 0;
  6. if (peekmessage(&msg)) {
  7. if (msg.message == WM_LBUTTONDOWN) {
  8. if (msg.x > 338 && msg.x < 338 + 65 * ZHI_WU_COUNT && msg.y < 96) {
  9. int index = (msg.x - 338) / 65;
  10. status = 1;
  11. curZhiWu = index + 1;
  12. } else {
  13. collectSunshine(&msg);
  14. }
  15. }
  16. // ......
  17. }
  18. }

显示当前总的阳光值

在gameInit初始化中,设置字体。

  1. LOGFONT f;
  2. gettextstyle(&f); // 获取当前字体设置
  3. f.lfHeight = 30; // 设置字体高度为 48
  4. f.lfWidth = 15;
  5. strcpy(f.lfFaceName, "Segoe UI Black");
  6. f.lfQuality = ANTIALIASED_QUALITY; // 设置输出效果为抗锯齿
  7. settextstyle(&f); // 设置字体样式
  8. setbkmode(TRANSPARENT);
  9. setcolor(BLACK);

在updateWindow中绘制阳光值。

  1. char scoreText[8];
  2. sprintf_s(scoreText, sizeof(scoreText), "%d", sunshine);
  3. outtextxy(276, 67, scoreText);

创建僵尸

创建僵尸的数据模型。这里一共创建了10个僵尸,这10个僵尸全部被消灭后,这个关卡就胜利了。

  1. struct zm {
  2. int x, y;
  3. int frameIndex;
  4. bool used;
  5. int speed;
  6. };
  7. struct zm zms[10];
  8. IMAGE imgZM[22];

僵尸数组,以及僵尸序列帧图片数组,在gameInit函数中进行初始化,如下。(注意:把僵尸的素材图片保存到src/zm目录下。)

  1. memset(zms, 0, sizeof(zms));
  2. srand(time(NULL));
  3. for (int i = 0; i < 22; i++) {
  4. sprintf_s(name, sizeof(name), "res/zm/%d.png", i + 1);
  5. loadimage(&imgZM[i], name);
  6. }

创建僵尸,代码如下:

  1. void createZM() {
  2. static int zmFre = 500;
  3. static int count = 0;
  4. count++;
  5. if (count > zmFre) {
  6. zmFre = rand() % 200 + 300;
  7. count = 0;
  8. int i;
  9. int zmMax = sizeof(zms) / sizeof(zms[0]);
  10. for (i = 0; i < zmMax && zms[i].used; i++);
  11. if (i < zmMax) {
  12. zms[i].used = true;
  13. zms[i].x = WIN_WIDTH;
  14. zms[i].y = 180 + (1 + rand() % 3) * 100 - 8;
  15. zms[i].speed = 1;
  16. }
  17. }
  18. }

更新僵尸的数据(僵尸的图片帧序号、僵尸的位置),代码如下:

  1. void updateZM() {
  2. int zmMax = sizeof(zms) / sizeof(zms[0]);
  3. static int count1 = 0;
  4. count1++;
  5. if (count1 > 2) {
  6. count1 = 0;
  7. for (int i = 0; i < zmMax; i++) {
  8. if (zms[i].used) {
  9. zms[i].x -= zms[i].speed;
  10. if (zms->x < 236 - 66) {
  11. printf("GAME OVER!\n");
  12. MessageBox(NULL, "over", "over", 0); //TO DO
  13. break;
  14. }
  15. }
  16. }
  17. }
  18. static int count2 = 0;
  19. count2++;
  20. if (count2 > 4) {
  21. count2 = 0;
  22. for (int i = 0; i < zmMax; i++) {
  23. if (zms[i].used) {
  24. zms[i].frameIndex = (zms[i].frameIndex + 1) % 22;
  25. }
  26. }
  27. }
  28. }

在updateGame函数中,创建僵尸并更新僵尸数据,如下:

  1. createZM();
  2. updateZM();

创建绘制僵尸的接口, 如下:

  1. void drawZM() {
  2. int zmCount = sizeof(zms) / sizeof(zms[0]);
  3. for (int i = 0; i < zmCount; i++) {
  4. if (zms[i].used) {
  5. IMAGE* img = &imgZM[zms[i].frameIndex];
  6. int x = zms[i].x;
  7. int y = zms[i].y - img->getheight();
  8. putimagePNG(x, y, img);
  9. }
  10. }
  11. }

在updateWindow函数中,绘制僵尸,如下:

drawZM();

实现阳光球的飞跃
现在的实现效果是,阳光被点击后,阳光球直接消失了!而原版的植物大战僵尸中,阳光被点击后,阳光会自动飞向左上角的位置,飞到终点后,阳光值才增加25点。我们的实现方式是,阳光球每次飞跃4个点,直到飞到终点,如下图:


给阳光的结构体添加两个成员,表示飞跃过程中的偏移量:

  1. struct sunshineBall {
  2. int x, y;
  3. int frameIndex;
  4. bool used;
  5. int destY;
  6. int timer;
  7. //添加以下两个成员
  8. float xOff;
  9. float yOff;
  10. };

在阳光被创建时,把变异量设置为0, 如下:

  1. void createSunshine() {
  2. int ballMax = sizeof(balls) / sizeof(balls[0]);
  3. static int frameCount = 0;
  4. static int fre = 200;
  5. frameCount++;
  6. if (frameCount >= fre) {
  7. //...略
  8. balls[i].xOff = 0;
  9. balls[i].yOff = 0;
  10. }
  11. }

阳光被点击后,马上修改阳光球的xoff和yoff:

  1. #include <math.h>
  2. void collectSunshine(ExMessage* msg) {
  3. int count = sizeof(balls) / sizeof(balls[0]);
  4. int w = imgSunshineBall[0].getwidth();
  5. int h = imgSunshineBall[0].getheight();
  6. for (int i = 0; i < count; i++) {
  7. if (balls[i].used) {
  8. int x = balls[i].x;
  9. int y = balls[i].y;
  10. if (msg->x > x && msg->x < x + w &&
  11. msg->y >y && msg->y < y + h) {
  12. balls[i].used = false;
  13. sunshine += 25;
  14. mciSendString("play res/sunshine.mp3", 0, 0, 0);
  15. // 设置初始偏移量
  16. float destX = 262;
  17. float destY = 0;
  18. float angle = atan((y - destY) / (x - destX));
  19. balls[i].xOff = 4 * cos(angle);
  20. balls[i].yOff = 4 * sin(angle);
  21. }
  22. }
  23. }
  24. }

在阳光飞跃过程中更新阳光的位置,如下:(注意是在飞跃过程中,不断计算偏移量,效果更好。)

  1. void updateSunshine() {
  2. int ballMax = sizeof(balls) / sizeof(balls[0]);
  3. for (int i = 0; i < ballMax; i++) {
  4. if (balls[i].used) {
  5. //略...
  6. }
  7. else if (balls[i].xOff) {
  8. float destX = 263;
  9. float destY = 0;
  10. float angle = atan((balls[i].y - destY) / (balls[i].x - destX));
  11. balls[i].xOff = 4 * cos(angle);
  12. balls[i].yOff = 4 * sin(angle);
  13. balls[i].x -= balls[i].xOff;
  14. balls[i].y -= balls[i].yOff;
  15. if (balls[i].y < 0 || balls[i].x < 262) {
  16. balls[i].xOff = 0;
  17. balls[i].yOff = 0;
  18. sunshine += 25;
  19. }
  20. }
  21. }
  22. }

删除原来被点击后,立即更新阳光值的代码。

//sunshine += 25;

修改渲染阳光的判断条件,如下:

  1. for (int i = 0; i < ballMax; i++) {
  2. if (balls[i].used
  3. || balls[i].xOff) { //添加这个条件
  4. IMAGE* img = &imgSunshineBall[balls[i].frameIndex];
  5. putimagePNG(balls[i].x, balls[i].y, img);
  6. }
  7. }

此时已经能够实现阳光的飞跃了,但是飞跃动作太慢了,后期我们再优化。

发射豌豆

僵尸靠近时,已经种植的植物豌豆就会自动发射“子弹”,我们先为子弹定义数据类型,如下:

  1. struct bullet {
  2. int x, y;
  3. int row;
  4. bool used;
  5. int speed;
  6. };
  7. struct bullet bullets[30];
  8. IMAGE imgBulletNormal;

在gameInit函数中,初始化“豌豆子弹池”和子弹的图片,如下:

  1. loadimage(&imgBulletNormal, "res/bullets/bullet_normal.png");
  2. memset(bullets, 0, sizeof(bullets));

在僵尸结构体中,添加成员row, 表示该僵尸所在的“行”,方便后续的判断。也可以不加,直接根据僵尸的y坐标来计算。

  1. struct zm {
  2. int x, y;
  3. int frameIndex;
  4. bool used;
  5. int speed;
  6. int row; //0..2
  7. };

在createZM函数中,创建僵尸的时候,设置row成员的值,如下:

  1. ......
  2. if (i < zmMax) {
  3. zms[i].used = true;
  4. zms[i].x = WIN_WIDTH;
  5. zms[i].row = rand() % 3; // 0..2;
  6. zms[i].y = 172 + (1 + zms[i].row) * 100;
  7. zms[i].speed = 1;
  8. }
  9. ......

创建shoot函数,实现豌豆发射子弹,如下:

  1. void shoot() {
  2. int zmCount = sizeof(zms) / sizeof(zms[0]);
  3. int directions[3] = { 0 };
  4. int dangerX = WIN_WIDTH - imgZM[0].getwidth();
  5. for (int i = 0; i < zmCount; i++) {
  6. if (zms[i].used && zms[i].x < dangerX) {
  7. directions[zms[i].row] = 1;
  8. }
  9. }
  10. for (int i = 0; i < 3; i++) {
  11. for (int j = 0; j < 9; j++) {
  12. if (map[i][j].type == WAN_DOU+1 && directions[i]) {
  13. static int count = 0;
  14. count++;
  15. if (count > 20) {
  16. count = 0;
  17. int k;
  18. int maxCount = sizeof(bullets) / sizeof(bullets[0]);
  19. for (k = 0; k < maxCount && bullets[k].used; k++);
  20. if (k < maxCount) {
  21. bullets[k].row = i;
  22. bullets[k].speed = 4;
  23. bullets[k].used = true;
  24. int zwX = 260 + j * 81.6; // (msg.x - 260) / 81.6;
  25. int zwY = 180 + i * 103.6 + 14; // (msg.y - 210) / 103.6;
  26. bullets[k].x = zwX + imgZhiWu[map[i][j].type - 1][0]->getwidth()-10;
  27. bullets[k].y = zwY + 5;
  28. }
  29. }
  30. }
  31. }
  32. }
  33. }

更新子弹的位置,如下:

  1. void updateBullets() {
  2. int countMax = sizeof(bullets) / sizeof(bullets[0]);
  3. for (int i = 0; i < countMax; i++) {
  4. if (bullets[i].used) {
  5. bullets[i].x += bullets[i].speed;
  6. if (bullets[i].x > WIN_WIDTH) {
  7. bullets[i].used = false;
  8. }
  9. }
  10. }
  11. }

在updateGame函数中,发射子弹并更新子弹的位置,如下:

  1. shoot();
  2. updateBullets();

在updateWindow中绘制子弹,如下:

  1. int bulletMax = sizeof(bullets) / sizeof(bullets[0]);
  2. for (int i = 0; i < bulletMax; i++) {
  3. if (bullets[i].used) {
  4. putimagePNG(bullets[i].x, bullets[i].y, &imgBulletNormal);
  5. }
  6. }

子弹和僵尸的碰撞

子弹碰到僵尸之后,子弹会“爆炸”,同时僵尸会“掉血”。我们先给僵尸添加血量成员。

  1. struct zm {
  2. //略...
  3. int blood;
  4. };

并在创建僵尸的时候,把血量初始化为100,如下:

  1. //...
  2. zms[i].speed = 1;
  3. zms[i].blood = 100;

子弹在碰到僵尸之后才会爆炸,并显示爆炸图片:

所以,我们在子弹的结构体中添加两个成员,分别表示当前是否已经爆炸,以及爆炸的帧图片序号,如下:

  1. struct bullet {
  2. //...
  3. bool blast;
  4. int frameIndex;
  5. };
  6. IMAGE imgBulletBlast[4];

在gameInit函数中对子弹帧图片数组,进行初始化,如下:

  1. loadimage(&imgBulletBlast[3], "res/bullets/bullet_blast.png");
  2. for (int i = 0; i < 3; i++) {
  3. float k = (i + 1) * 0.2;
  4. loadimage(&imgBulletBlast[i], "res/bullets/bullet_blast.png",
  5. imgBulletBlast[3].getwidth()*k,
  6. imgBulletBlast[3].getheight()*k, true);
  7. }

在发射子弹shoot函数中,对子弹的blast和帧序号frameIndex进行初始化,如下:

  1. bullets[k].row = i;
  2. bullets[k].speed = 4;
  3. bullets[k].used = true;
  4. bullets[k].blast = false;
  5. bullets[k].blastTime = 0;

在更新子弹的updateBullets函数中,更新子弹爆炸的帧序号,如下:

  1. bullets[i].x += bullets[i].speed;
  2. if (bullets[i].x > WIN_WIDTH) {
  3. bullets[i].used = false;
  4. }
  5. if (bullets[i].blast) {
  6. bullets[i].blastTime++;
  7. if (bullets[i].blastTime >= 4) {
  8. bullets[i].used = false;
  9. }
  10. }

进行碰撞检测,检查子弹和僵尸是否发生碰撞,如下:

  1. void collisionCheck() {
  2. int bCount = sizeof(bullets) / sizeof(bullets[0]);
  3. int zCount = sizeof(zms) / sizeof(zms[0]);
  4. for (int i = 0; i < bCount; i++) {
  5. if (bullets[i].used == false || bullets[i].blast)continue;
  6. for (int k = 0; k < zCount; k++) {
  7. int x1 = zms[k].x + 80;
  8. int x2 = zms[k].x + 110;
  9. if (bullets[i].row == zms[k].row && bullets[i].x > x1 && bullets[i].x < x2) {
  10. zms[i].blood -= 20;
  11. bullets[i].blast = true;
  12. bullets[i].speed = 0;
  13. }
  14. }
  15. }
  16. }

在updateGame函数中,调用碰撞检测函数,如下:

collisionCheck();

渲染子弹的爆炸效果,如下:

  1. int bulletMax = sizeof(bullets) / sizeof(bullets[0]);
  2. for (int i = 0; i < bulletMax; i++) {
  3. if (bullets[i].used) {
  4. if (bullets[i].blast) {
  5. IMAGE* img = &imgBulletBlast[bullets[i].blastTime];
  6. int x = bullets[i].x + 12 - img->getwidth() / 2;
  7. int y = bullets[i].y + 12 - img->getheight() / 2;
  8. putimagePNG(x, y, img);
  9. /*bullets[i].used = false;*/
  10. }
  11. else {
  12. putimagePNG(bullets[i].x, bullets[i].y, &imgBulletNormal);
  13. }
  14. }
  15. }

僵尸死亡

僵尸被豌豆子弹击中后,会“掉血”,血量掉光了,就直接KO了,同时变成一堆“黑沙”。

给僵尸结构体添加dead成员,表示是否已经死亡,另外添加一个图片帧数组,用来表示变成成黑沙的过程。

  1. struct zm {
  2. ......
  3. bool dead;
  4. };
  5. IMAGE imgZmDead[20];

在gameInit中对这个图片帧数组进行初始化。

  1. for (int i = 0; i < 20; i++) {
  2. sprintf_s(name, sizeof(name), "res/zm_dead/%d.png", i + 1);
  3. loadimage(&imgZmDead[i], name);
  4. }

在碰撞检测中对僵尸的血量做检测,如果血量降到0,就设置为死亡状态。如下:

  1. void collisionCheck() {
  2. int bCount = sizeof(bullets) / sizeof(bullets[0]);
  3. int zCount = sizeof(zms) / sizeof(zms[0]);
  4. for (int i = 0; i < bCount; i++) {
  5. if (bullets[i].used == false || bullets[i].blast)continue;
  6. for (int k = 0; k < zCount; k++) {
  7. int x1 = zms[k].x + 80;
  8. int x2 = zms[k].x + 110;
  9. if (zms[k].dead==false && //添加这个条件
  10. bullets[i].row == zms[k].row && bullets[i].x > x1 && bullets[i].x < x2) {
  11. zms[k].blood -= 20;
  12. bullets[i].blast = true;
  13. bullets[i].speed = 0;
  14. //对血量进行检测
  15. if (zms[k].blood <= 0) {
  16. zms[k].dead = true;
  17. zms[k].speed = 0;
  18. zms[k].frameIndex = 0;
  19. }
  20. break;
  21. }
  22. }
  23. }
  24. }

僵尸死亡后,在updateZM中,更新僵尸的状态(变成黑沙发)。如下:

  1. static int count2 = 0;
  2. count2++;
  3. if (count2 > 4) {
  4. count2 = 0;
  5. for (int i = 0; i < zmMax; i++) {
  6. if (zms[i].used) {
  7. //判断是否已经死亡
  8. if (zms[i].dead) {
  9. zms[i].frameIndex++;
  10. if (zms[i].frameIndex >= 20) {
  11. zms[i].used = false;
  12. }
  13. }
  14. else {
  15. zms[i].frameIndex = (zms[i].frameIndex + 1) % 22;
  16. }
  17. }
  18. }
  19. }

绘制僵尸的黑沙状态,如下:

  1. void drawZM() {
  2. int zmCount = sizeof(zms) / sizeof(zms[0]);
  3. for (int i = 0; i < zmCount; i++) {
  4. if (zms[i].used) {
  5. //选择对应的渲染图片
  6. IMAGE* img = (zms[i].dead) ? imgZmDead : imgZM;
  7. img += zms[i].frameIndex;
  8. int x = zms[i].x;
  9. int y = zms[i].y - img->getheight();
  10. putimagePNG(x, y, img);
  11. }
  12. }
  13. }

后续的内容,点击这里看完整的实现

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/笔触狂放9/article/detail/571939
推荐阅读
相关标签
  

闽ICP备14008679号