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

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

十 实现豌豆子弹

原设计

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

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

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

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

c++ 复制代码
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) 地方创建子弹和更新子弹数据

c++ 复制代码
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 中加载图片

c++ 复制代码
//加载子弹图片
loadimage(&peaNormal, "res/bullets/PeaNormal/PeaNormal_0.png");

在 updateWindow 中渲染子弹

c++ 复制代码
//渲染子弹
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() 函数

c++ 复制代码
void createBullets()
{
    int peaX = 0, peaY = 0, pic_width = 0;
    //遍历是否存在僵尸
    for (int i = 0; i < MAX_ZOMBIE_NUM && zombies[i].used; ++i)
    {
        //printf("%s zombies i = %d  row = %d \n", __FUNCTION__ , i, zombies[i].row);
        
        //遍历当前行是否存在豌豆
        for (int j = 0; j < GRASS_GRID_COL && 
            plants[zombies[i].row][j].type == (int)PEA; ++j)
        {
            //printf("%s pea i = %d  j = %d \n", __FUNCTION__, zombies[i].row, j);

            //找到一颗未使用的子弹
            for (int k = 0; k < (GRASS_GRID_ROW * GRASS_GRID_COL)
                && !bullets[k].used; ++k)
            {
                //printf("%s bullet k = %d \n", __FUNCTION__,k);
                //之前豌豆的 X Y 坐标
                peaX = GRASS_LEFT_MARGIN + j * GRASS_GRID_WIDTH + 5;
                peaY = GRASS_TOP_MARGIN + zombies[i].row * GRASS_GRID_HIGHT + 10;
                pic_width = imgPlant[0][0]->getwidth();

                //初始化子弹
                bullets[k].x = peaX + pic_width;
                bullets[k].y = peaY + 5;
                bullets[k].speed = 7;
                bullets[k].frameId = 0;
                bullets[k].used = true;
                break;
            }
        }
    }
}

void updateBullets() 
{
    for (int k = 0; k < (GRASS_GRID_ROW * GRASS_GRID_COL)
        && bullets[k].used; ++k)
    {
        bullets[k].x += bullets[k].speed;
        if (bullets[k].x >= WIN_WIDTH)
        {
            bullets[k].used = false;
        }
    }
}

上述 createBullets 函数存在问题点,因为没有判断该该豌豆是否已经发射了子弹(假设豌豆射击的时间间隔为,在子弹爆炸前不会再发射子弹),所以子弹会瞬间用完

这时可以在 Plant 结构体中增加成员变量来记录,用 index 来记录子弹数组的下标,通过在上述第三层循环中增加 plants[zombies[i].row][j].index = k; 就可以判断 该豌豆是否已经发射了子弹

c++ 复制代码
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

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

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

现设计

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

设计结构体如下

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

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

c++ 复制代码
#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[i][j] = new IMAGE; 该行是 easyx 内部在 IMAGE 构造函数里加了一些初始化内容,所以没办法用 malloc 替代

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

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

十一 实现子弹和僵尸碰撞

子弹结构体新增成员变量

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

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

c++ 复制代码
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,渲染子弹爆炸图片至输出窗口

c++ 复制代码
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 中初始化新增成员

c++ 复制代码
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 更新子弹图片帧接口

c++ 复制代码
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

c++ 复制代码
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

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

效果展示

十二 实现僵尸死亡

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

c++ 复制代码
/* 僵尸相关结构和变量 */
#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,加载僵尸死亡图片至内存

c++ 复制代码
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,渲染僵尸死亡图片至输出窗口

c++ 复制代码
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]);
    }
}

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

c++ 复制代码
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; //结束循环
        }
    }
}

更新僵尸接口,处理僵尸死亡图片帧

c++ 复制代码
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 同上,更新僵尸血量和状态

效果展示

相关推荐
Biomamba生信基地8 分钟前
两天入门R语言,周末开讲
开发语言·r语言·生信
RAN_PAND9 分钟前
STL介绍1:vector、pair、string、queue、map
开发语言·c++·算法
Bio Coder12 分钟前
R语言安装生物信息数据库包
开发语言·数据库·r语言
Tiger Z12 分钟前
R 语言科研绘图第 27 期 --- 密度图-分组
开发语言·程序人生·r语言·贴图
fai厅的秃头姐!2 小时前
C语言03
c语言·数据结构·算法
life_time_3 小时前
C语言(22)
c语言·开发语言
Minner-Scrapy3 小时前
DApp 开发入门指南
开发语言·python·web app
孤雪心殇3 小时前
简单易懂,解析Go语言中的Map
开发语言·数据结构·后端·golang·go
庸俗今天不摸鱼3 小时前
Canvas进阶-4、边界检测(流光,鼠标拖尾)
开发语言·前端·javascript·计算机外设