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.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 140
- int currX = 0, currY = 0, currIndex = -1; //当前拖动植物的坐标和类型
- enum PLANT_CARDS { PEA, SUNFLOWER, PLANT_CNT }; //使用 PLANT_CNT 统计 PLANT 总数
- IMAGE imgBg; //背景图片
- IMAGE imgBar; //工具栏图片
- IMAGE imgCards[PLANT_CNT]; //植物卡片
- IMAGE* imgPlant[PLANT_CNT][MAX_PICTURE_NUM]; //动态植物素材 (也可使用二维数组, 但存在浪费空间问题)
- /* 植物相关结构和变量 */
- typedef struct Plant // 植物结构体
- {
- int type; //植物类型, -1 表示草地
- int frameId; //表示植物摆动帧
- }Plant;
- Plant* plants[GRASS_GRID_ROW][GRASS_GRID_COL];
- /* 草地结构体 */
- typedef struct Grass
- {
- Plant plant;
- } Grass;
- /* 向日葵结构体 */
- typedef struct SunFlower
- {
- Plant plant;
- } SunFlower;
- /* 豌豆射手相关结构和变量 */
- #define MAX_BULLET_NUM 1 //默认同一豌豆只有上次发射子弹爆炸后才发射下一颗, 也可更改
- IMAGE peaNormal;
- IMAGE peaNormalExplode;
- /* 子弹结构体 */
- typedef struct Bullet {
- int x; //当前 X 轴坐标
- int y; //当前 Y 轴坐标
- int speed; //子弹移动的速度
- bool used; //是否在使用
- }Bullet;
- #define DEFAULT_SHOOT_TIME -1
- #define MAX_TIME_INTERVAL 100
- /* 豌豆射手结构体 */
- typedef struct PeaShooter
- {
- Plant plant;
- int shootSpeed; //豌豆射击速度, 或者叫豌豆发射子弹的时间间隔, -1 表示可发射子弹
- Bullet bullet[MAX_BULLET_NUM]; //子弹夹
- } PeaShooter;
- /* 阳光球相关结构和变量 */
- typedef struct SunShineBall
- {
- int x; //当前 X 轴坐标, 阳光球在飘落过程中 X 坐标不变
- int y; //当前 Y 轴坐标
- int frameId; //当前图片帧编号
- int destination; //飘落目标位置 Y 坐标
- bool used; //是否在使用
- int timer; //统计飘落目标位置后的帧次数
- float xOffset; //阳光球飞跃过程中每次 X 轴偏移量
- float yOffset; //阳光球飞跃过程中每次 Y 轴偏移量
- }SunShineBall;
- #define MAX_BALLS_NUM 10
- #define SUM_SHINE_PIC_NUM 29
- SunShineBall balls[MAX_BALLS_NUM];
- IMAGE imgSunShineBall[SUM_SHINE_PIC_NUM];
- int sunShineVal = 50; //全局变量阳光值
- /* 僵尸相关结构和变量 */
- #define MAX_ZOMBIE_NUM 10
- #define MAX_ZOMBIE_PIC_NUM 22
- typedef struct Zombie {
- int x; //当前 X 轴坐标
- int y; //当前 Y 轴坐标
- int frameId; //当前图片帧编号
- int speed; //僵尸移动的速度
- int row; //僵尸所在行
- bool used; //是否在使用
- };
- Zombie zombies[MAX_ZOMBIE_NUM];
- IMAGE imgZombies[MAX_ZOMBIE_PIC_NUM];
- /* 判断文件是否存在接口 */
- bool fileExist(const char* name)
- {
- FILE* file = NULL;
- if (file = fopen(name,"r"))
- fclose(file);
- return file == NULL ? false : true;
- }
- /* 游戏初始化接口, 主要加载游戏图片至内存 */
- void gameInit()
- {
- loadimage(&imgBg, "res/map0.jpg"); //加载背景图片
- loadimage(&imgBar, "res/bar5.png");
- char name[64];
- memset(imgPlant, 0, sizeof(imgPlant)); //将二维指针数组内存空间置零
- memset(balls, 0, sizeof(balls));
- memset(zombies, 0, sizeof(zombies));
- memset(plants, 0, sizeof(plants));
- Grass* grassPtr = NULL;
- for (int i = 0; i < GRASS_GRID_ROW; ++i) //将植物数组全初始化为草地
- {
- for (int j = 0; j < GRASS_GRID_COL; ++j)
- {
- grassPtr = (Grass*)calloc(1, sizeof(Grass));
- assert(grassPtr);
- plants[i][j] = (Plant*)grassPtr;
- plants[i][j]->type = -1;
- }
- }
- for (int i = 0; i < PLANT_CNT; ++i)
- {
- sprintf(name, "res/Cards/card_%d.png", i + 1); //获取植物卡片相对路径名称
- loadimage(&imgCards[i], name);
- for (int j = 0;i < MAX_PICTURE_NUM; ++j)
- {
- sprintf(name, "res/Plants/%d/%d.png", i, j + 1); //获取动态植物素材相对路径名称
- if (fileExist(name)) {
- imgPlant[i][j] = new IMAGE;
- loadimage(imgPlant[i][j], name);
- }
- else break;
- }
- }
- for (int i = 0; i < SUM_SHINE_PIC_NUM; ++i) //加载阳光图片
- {
- sprintf(name, "res/sunshine/%d.png", i + 1);
- loadimage(&imgSunShineBall[i], name);
- }
- for (int i = 0; i < MAX_ZOMBIE_PIC_NUM; ++i) //加载僵尸图片
- {
- sprintf(name, "res/zm/0/%d.png", i + 1);
- loadimage(&imgZombies[i], name);
- }
- loadimage(&peaNormal, "res/bullets/PeaNormal/PeaNormal_0.png"); //加载子弹图片
- srand(time(NULL)); //配置随机种子
- initgraph(WIN_WIDTH, WIN_HIGHT, 1); //创建游戏图形窗口
- LOGFONT f; //设置字体
- gettextstyle(&f);
- f.lfHeight = 30;
- f.lfWidth = 15;
- strcpy(f.lfFaceName, "Segoe UI Black");
- f.lfQuality = ANTIALIASED_QUALITY; //抗锯齿化效果
- settextstyle(&f);
- setbkmode(TRANSPARENT); //设置背景透明
- setcolor(BLACK); //设置字体颜色
- }
- /* 游戏更新窗口接口, 主要渲染游戏图片至输出窗口 */
- void updateWindow()
- {
- BeginBatchDraw(); //使用双缓冲, 解决输出窗口闪屏
- putimage(0, 0, &imgBg); //渲染背景图至窗口
- putimagePNG(250, 0, &imgBar);
- for (int i = 0;i < PLANT_CNT;++i) //渲染植物卡牌
- putimage(PIC_LEFT_MARGIN + i * PIC_WIDTH, 6, &imgCards[i]);
- for (int i = 0; i < GRASS_GRID_ROW; ++i) //渲染种植植物
- {
- for (int j = 0; j < GRASS_GRID_COL; ++j)
- {
- if (plants[i][j]->type >= 0)
- putimagePNG(GRASS_LEFT_MARGIN + j * GRASS_GRID_WIDTH + 5, //微调植物种植位置
- GRASS_TOP_MARGIN + i * GRASS_GRID_HIGHT + 10,
- imgPlant[plants[i][j]->type][plants[i][j]->frameId]);
- }
- }
- for (int i = 0; i < MAX_BALLS_NUM; ++i) //渲染随机阳光
- {
- if (balls[i].used || balls[i].xOffset)
- putimagePNG(balls[i].x, balls[i].y, &imgSunShineBall[balls[i].frameId]);
- }
- if (currIndex >= 0) //渲染当前拖动的植物
- {
- IMAGE* currImage = imgPlant[currIndex][0];
- putimagePNG(currX - currImage->getwidth() / 2,
- currY - currImage->getheight() / 2, currImage);
- }
- char scoreText[8]; //渲染阳光值
- sprintf(scoreText, "%d", sunShineVal);
- outtextxy(277, 67, scoreText);
- for (int i = 0; i < MAX_ZOMBIE_NUM; ++i) //渲染僵尸
- {
- if (zombies[i].used)
- {
- IMAGE* img = &imgZombies[zombies[i].frameId];
- putimagePNG(zombies[i].x, zombies[i].y + 30, img);
- }
- }
- PeaShooter* peaShooter = NULL; //渲染子弹
- for (int i = 0; i < GRASS_GRID_ROW; ++i)
- {
- for (int j = 0; j < GRASS_GRID_COL; ++j)
- {
- if (plants[i][j]->type == PEA)
- {
- peaShooter = (PeaShooter*)plants[i][j];
- for (int k = 0; k < MAX_BULLET_NUM; ++k) //默认只有一发子弹, 但可调整
- {
- if (peaShooter->bullet[k].used)
- putimagePNG(peaShooter->bullet[k].x, peaShooter->bullet[k].y, &peaNormal);
- }
- }
- }
- }
- EndBatchDraw(); //结束双缓冲
- }
- /* 收集随机阳光接口 */
- void collectSunShine(ExMessage* msg)
- {
- IMAGE* imgSunShine = NULL;
- for (int i = 0; i < MAX_BALLS_NUM; ++i) //遍历阳光球
- {
- if (balls[i].used) //阳光球在使用中
- {
- imgSunShine = &imgSunShineBall[balls[i].frameId]; //找到对应的阳光球图片
- if (msg->x > balls[i].x && msg->x < balls[i].x + imgSunShine->getwidth()
- && msg->y > balls[i].y && msg->y < balls[i].y + imgSunShine->getheight()) //判断鼠标移动的位置是否处于当前阳光球的位置
- {
- PlaySound("res/audio/sunshine.wav", NULL, SND_FILENAME | SND_ASYNC); //异步播放收集阳光球音效
- balls[i].used = false; //将阳光球状态更改为未使用 (飞跃状态, 因为 xOffset 赋值了)
- const float angle = atan((float)(balls[i].y - 0) / (float)(balls[i].x - 262)); //使用正切函数
- balls[i].xOffset = 16 * cos(angle); //计算 X 轴偏移
- balls[i].yOffset = 16 * sin(angle); //计算 Y 轴偏移
- }
- }
- }
- }
- /* 种植植物接口, 主要释放草格子内存, 二维指针数组对应位置,指向初始化的植物 */
- Plant* growPlants(Plant* plant, int type)
- {
- assert(plant);
- free((Grass*)plant); //释放该位置草格子内存
- if (type == PEA) //根据类型初始化 PeaShooter
- {
- PeaShooter* peaShooter = (PeaShooter*)calloc(1, sizeof(PeaShooter)); //calloc 函数替代 malloc, 省略 memset
- assert(peaShooter);
- peaShooter->shootSpeed = DEFAULT_SHOOT_TIME; //豌豆射击速度, 或者叫豌豆发射子弹的时间间隔, -1 表示可发射子弹
- peaShooter->bullet[0].speed = 10; //默认只使用了第一枚子弹, 可更改
- return (Plant*)peaShooter;
- }
- else if (type == SUNFLOWER) //根据类型初始化 SunFlower
- {
- SunFlower* sunFlower = (SunFlower*)calloc(1, sizeof(SunFlower));
- assert(sunFlower);
- sunFlower->plant.type = 1;
- return (Plant*)sunFlower;
- }
- }
- /* 销毁植物接口, 主要释放草格子和种植植物的内存 */
- void destroyPlants()
- {
- for (int i = 0; i < GRASS_GRID_ROW; ++i)
- {
- for (int j = 0; j < GRASS_GRID_COL; ++j)
- {
- if (plants[i][j]->type == PEA)
- free ((PeaShooter*)plants[i][j]);
- else if (plants[i][j]->type == SUNFLOWER)
- free ((SunFlower*)plants[i][j]);
- else
- free ((Grass*)plants[i][j]);
- }
- }
- memset(plants, 0, sizeof(plants)); //将指针全部置 NULL
- }
- /* 用户点击接口, 主要监听鼠标事件并调用相应的函数 */
- void userClick()
- {
- ExMessage msg; //创建消息体
- static int status = 0; //种植植物必须先选中再拖动(拖动需先左键点击再拖动)
- if (peekmessage(&msg)) //该函数用于获取一个消息,并立即返回
- {
- collectSunShine(&msg);
- if (msg.message == WM_LBUTTONDOWN) //鼠标点击
- {
- if (msg.x > PIC_LEFT_MARGIN &&
- msg.x < PIC_LEFT_MARGIN + PLANT_CNT * PIC_WIDTH &&
- msg.y < 96)
- {
- currX = msg.x, currY = msg.y;
- currIndex = (msg.x - PIC_LEFT_MARGIN) / PIC_WIDTH;
- status = 1;
- }
- }
- else if (msg.message == WM_MOUSEMOVE && status == 1) //鼠标拖动
- {
- currX = msg.x, currY = msg.y; //记录当前拖动位置
- }
- else if (msg.message == WM_LBUTTONUP) //鼠标抬起
- {
- if (msg.x >= GRASS_LEFT_MARGIN &&
- msg.x <= GRASS_LEFT_MARGIN + GRASS_GRID_COL * GRASS_GRID_WIDTH &&
- msg.y >= GRASS_TOP_MARGIN &&
- msg.y <= GRASS_TOP_MARGIN + GRASS_GRID_ROW * GRASS_GRID_HIGHT) //当植物拖到至草地位置终止, 则种植植物
- {
- int x = (msg.y - GRASS_TOP_MARGIN) / GRASS_GRID_HIGHT; //计算第几行
- int y = (msg.x - GRASS_LEFT_MARGIN) / GRASS_GRID_WIDTH; //计算第几列
- if (plants[x][y]->type < 0 && status == 1) //未点击植物或当前位置已种植过植物,则不种植植物
- plants[x][y] = growPlants(plants[x][y], currIndex); //种植植物
- }
- status = 0, currIndex = -1; //停止拖动当前植物
- }
- }
- }
- /* 创建随机阳光球接口, 主要初始化随机阳光球 */
- void createSunshine()
- {
- static int sunCallCnt = 0; //延缓函数调用次数并增加些随机性
- static int randSunCallCnt = 400;
- if (++sunCallCnt < randSunCallCnt) return;
- randSunCallCnt = 200 + rand() % 200;
- sunCallCnt = 0;
- for (int i = 0; i < MAX_BALLS_NUM; ++i) //从阳光池中取一个可用阳光
- {
- if (!balls[i].used && balls[i].xOffset == 0) //找到一个未使用的阳光, 则进行初始化
- {
- balls[i].x = GRASS_LEFT_MARGIN + GRASS_GRID_WIDTH //只允许阳光掉落在草地范围内(不允许左一格)
- + (rand() % GRASS_GRID_COL) * GRASS_GRID_WIDTH; //因为左一格的位置可能在上方阳光栏图片左边
- balls[i].y = GRASS_TOP_MARGIN;
- balls[i].frameId = 0;
- balls[i].destination = GRASS_TOP_MARGIN
- + GRASS_GRID_HIGHT + (rand() % (3 * GRASS_GRID_HIGHT)); //目标点在中间三行
- balls[i].used = true;
- balls[i].timer = 0;
- balls[i].xOffset = 0;
- balls[i].yOffset = 0;
- break;
- }
- }
- }
- /* 更新随机阳光球接口, 主要更新随机阳光球的图片帧和处理飞跃状态时的 X Y 轴偏移 */
- void updateSunshine()
- {
- for (int i = 0; i < MAX_BALLS_NUM; ++i)
- {
- if (balls[i].used)
- {
- if (balls[i].y < balls[i].destination)
- {
- balls[i].y += 2; //每次移动两个像素
- balls[i].frameId = ++balls[i].frameId % SUM_SHINE_PIC_NUM; //修改当前图片帧编号, 并在到达 SUM_SHINE_PIC_NUM 时重置图片帧为 0
- }
- else //当阳光下落至目标位置时, 停止移动
- {
- if (balls[i].timer < MAX_TIME_INTERVAL) ++balls[i].timer;
- else balls[i].used = false;
- }
- }
- else if (balls[i].xOffset) //阳光球处于飞跃状态
- {
- if (balls[i].y > 0 && balls[i].x > 262)
- {
- const float angle = atan((float)(balls[i].y - 0) / (float)(balls[i].x - 262)); //不断调整阳光球的位置坐标
- balls[i].xOffset = 16 * cos(angle);
- balls[i].yOffset = 16 * sin(angle);
- balls[i].x -= balls[i].xOffset;
- balls[i].y -= balls[i].yOffset;
- }
- else
- {
- balls[i].xOffset = 0; //阳光球飞至计分器位置, 则将 xOffset 置 0, 且加上 25 积分
- balls[i].yOffset = 0;
- sunShineVal += 25;
- }
- }
- }
- }
- /* 创建僵尸接口, 主要用于初始化僵尸 */
- void createZombie()
- {
- static int zombieCallCnt = 0; //延缓函数调用次数并增加些随机性
- static int randZombieCallCnt = 500;
- if (zombieCallCnt++ < randZombieCallCnt) return;
- randZombieCallCnt = 300 + rand() % 200;
- zombieCallCnt = 0;
- for (int i = 0; i < MAX_ZOMBIE_NUM; ++i) //找一个未在界面的僵尸初始化
- {
- if (!zombies[i].used)
- {
- zombies[i].row = rand() % GRASS_GRID_ROW; //僵尸出现在第几行(从 0 开始)
- zombies[i].x = WIN_WIDTH;
- zombies[i].y = zombies[i].row * GRASS_GRID_HIGHT; //出现在草地的任意一格上
- zombies[i].frameId = 0;
- zombies[i].speed = 1; //僵尸的移动速度
- zombies[i].used = true;
- break; //结束循环
- }
- }
- }
- /* 更新僵尸接口, 主要用于处理僵尸图片帧, 实现僵尸行走 */
- void updateZombie()
- {
- static int CallCnt = 0; //延缓函数调用次数
- if (++CallCnt < 3) return;
- CallCnt = 0;
- for (int i = 0; i < MAX_ZOMBIE_NUM; ++i)
- {
- if (zombies[i].used)
- {
- zombies[i].x -= zombies[i].speed; //僵尸行走
- zombies[i].frameId = ++zombies[i].frameId % MAX_ZOMBIE_PIC_NUM; //僵尸更换图片帧
- if (zombies[i].x < 170) //目前先这样写待优化
- {
- printf("GAME OVER !");
- MessageBox(NULL, "over", "over", 0);
- exit(0);
- }
- }
- }
- }
- /* 更新植物图片帧接口, 主要用于实现植物摇摆 */
- void updatePlantsPic()
- {
- for (int i = 0; i < GRASS_GRID_ROW; ++i) //遍历二维指针数组
- {
- for (int j = 0; j < GRASS_GRID_COL; ++j)
- {
- if (plants[i][j]->type >= 0 && //找到非草地的植物
- imgPlant[plants[i][j]->type][++plants[i][j]->frameId] == NULL) //将植物图片增加一, 判断是否到达图片帧末尾
- plants[i][j]->frameId = 0; //重置图片帧为零
- }
- }
- }
- /* 豌豆射手发射子弹接口 */
- void shoot()
- {
- PeaShooter* peaShooter = NULL;
- int row = 0, peaX = 0, peaY = 0, pic_width = 0;
- for (int i = 0; i < MAX_ZOMBIE_NUM; ++i) //遍历是否存在僵尸
- {
- if (zombies[i].used)
- {
- row = zombies[i].row;
- for (int j = 0; j < GRASS_GRID_COL; ++j) //遍历当前行是否存在豌豆
- {
- if (plants[row][j]->type == PEA)
- {
- peaShooter = (PeaShooter*)plants[row][j];
- if (peaShooter->shootSpeed++ == DEFAULT_SHOOT_TIME) //发射时机
- {
- for (int k = 0; k < MAX_BULLET_NUM; ++k) //从子弹夹里取一颗未使用的子弹(默认一颗)
- {
- if (!peaShooter->bullet[k].used) //该子弹未在使用中
- {
- peaX = GRASS_LEFT_MARGIN + j * GRASS_GRID_WIDTH + 5; //之前豌豆的 X Y 坐标
- peaY = GRASS_TOP_MARGIN + row * GRASS_GRID_HIGHT + 10;
- pic_width = imgPlant[0][0]->getwidth();
- peaShooter->bullet[k].x = peaX + pic_width; //初始化子弹
- peaShooter->bullet[k].y = peaY + 5;
- peaShooter->bullet[k].used = true;
- break; //结束当前循环
- }
- }
- }
- else if (peaShooter->shootSpeed > MAX_TIME_INTERVAL) //不到发射时机
- peaShooter->shootSpeed = DEFAULT_SHOOT_TIME; //则将 timer 计时器增加 (默认一百帧)
- }
- }
- }
- }
- }
- /* 更新子弹图片帧接口 */
- void updateBullets()
- {
- PeaShooter* peaShooter = NULL;
- for (int i = 0; i < GRASS_GRID_ROW; ++i) //遍历植物二维指针数组
- {
- for (int j = 0; j < GRASS_GRID_COL; ++j)
- {
- if (plants[i][j]->type == PEA) //找到其中是豌豆的位置
- {
- peaShooter = (PeaShooter*)plants[i][j];
- for (int k = 0; k < MAX_BULLET_NUM; ++k)
- {
- if (peaShooter->bullet[k].used) //找到在使用中的子弹
- {
- peaShooter->bullet[k].x += peaShooter->bullet[k].speed; //移动子弹位置
- if (peaShooter->bullet[k].x >= WIN_WIDTH) //如果到达窗口最右端
- peaShooter->bullet[k].used = false; //将子弹重置为未使用状态
- }
- }
- }
- }
- }
- }
- /* 更新游戏属性的接口 */
- void updateGame()
- {
- updatePlantsPic();
- createSunshine();
- updateSunshine();
- createZombie();
- updateZombie();
- shoot();
- updateBullets();
- }
- /* 游戏开始前的菜单界面 */
- void startUI()
- {
- IMAGE imageBg, imgMenu1, imgMenu2;
- loadimage(&imageBg, "res/menu.png");
- loadimage(&imgMenu1, "res/menu1.png");
- loadimage(&imgMenu2, "res/menu2.png");
- bool mouseStatus = false; //0 表示鼠标未移动至开始游戏位置
- while (1)
- {
- BeginBatchDraw(); //双缓冲解决闪屏
- putimage(0, 0, &imageBg);
- putimagePNG(UI_LEFT_MARGIN, UI_TOP_MARGIN, mouseStatus ? &imgMenu2 : &imgMenu1); //根据鼠标是否移动至游戏开始位置, 显示不同的图片
- ExMessage msg;
- if (peekmessage(&msg)) //监听鼠标事件
- {
- if (msg.x > UI_LEFT_MARGIN && msg.x < UI_LEFT_MARGIN + UI_WIDTH
- && msg.y > UI_TOP_MARGIN && msg.y < UI_TOP_MARGIN + UI_HIGHT) //当鼠标移动至开始游戏位置, 界面高亮
- {
- putimagePNG(UI_LEFT_MARGIN, UI_TOP_MARGIN, &imgMenu2);
- mouseStatus = true; //表示鼠标移动至开始游戏位置, 如果一直不移动鼠标则一直高亮
- if (msg.message == WM_LBUTTONDOWN) //当鼠标点击时, 进入游戏
- return; //结束函数
- }
- else mouseStatus = false; //当鼠标未移动至开始游戏位置, 界面不高亮
- }
- EndBatchDraw();
- }
- }
- /* 主函数 */
- int main()
- {
- gameInit(); //不能把 startUI 放在 gameInit 前, gameInit 包含了创建游戏图形窗口
- startUI();
- updateWindow(); //窗口视图展示
- int timer = 0; //用以计时 20 毫秒更新一次
- while (1)
- {
- userClick(); //监听窗口鼠标事件
- timer += getDelay();
- if (timer > 20)
- {
- updateWindow(); //更新窗口视图
- updateGame(); //更新游戏动画帧
- timer = 0;
- }
- }
- destroyPlants(); //释放内存
- system("pause");
- return 0;
- }
复制代码 效果展示
僵尸在出如今某条门路上,且存在豌豆射手,豌豆射手就会发射子弹,还没实现子弹和僵尸碰撞功能
小记录
不能将 X 范例的值分配到 X范例的实体问题
imgPlant[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; //将子弹速度降为 0
- if (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();
- }
复制代码 效果展示
十二 实现僵尸殒命
新增僵尸殒命相干变量和图片
- /* 僵尸相关结构和变量 */
- #define MAX_ZOMBIE_NUM 10
- #define MAX_ZOMBIE_DEAD_PIC_NUM 10
- #define MAX_ZOMBIE_PIC_NUM 22
- typedef struct Zombie {
- int x; //当前 X 轴坐标
- int y; //当前 Y 轴坐标
- int frameId; //当前图片帧编号
- int speed; //僵尸移动的速度
- int row; //僵尸所在行
- int blood; //默认僵尸血条为 100
- bool isDead; //僵尸是否死亡
- bool used; //是否在使用
- } Zombie;
- Zombie zombies[MAX_ZOMBIE_NUM];
- IMAGE imgZombies[MAX_ZOMBIE_PIC_NUM];
- IMAGE imgDeadZombies[MAX_ZOMBIE_DEAD_PIC_NUM];
复制代码 游戏初始化接口 gameInit,加载僵尸殒命图片至内存
- 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; //默认僵尸血条为 100
- zombies[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 同上,更新僵尸血量和状态
效果展示
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |