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

news2025/3/12 19:35:50

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

音频稍卡顿问题,用了 SFML 三方库已优化解决

安装 SFML
资源下载 https://www.sfml-dev.org/download/sfml/2.6.2/

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

因为 C 站不能上传动图,所以游戏实际效果可看后续文章更新,插一条试玩视频(https://live.csdn.net/v/441805)

后面项目全部源代码会上传至 C 站(https://gitcode.com/qq_44868502/PlantsAndZombiesBattle),
音频图片等因为 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;          //默认僵尸血条为 100
    bool isDead;        //僵尸是否死亡
    bool isEating;      //僵尸是否在吃植物, 这些状态改用枚举更好, 待优化
    bool used;          //是否在使用
} Zombie;
Zombie zombies[MAX_ZOMBIE_NUM];
IMAGE imgZombies[MAX_ZOMBIE_PIC_NUM];
IMAGE imgDeadZombies[MAX_ZOMBIE_DEAD_PIC_NUM];
IMAGE imgZombiesEat[MAX_ZOMBIE_EAT_PIC_NUM];


/* 植物相关结构和变量 */
typedef struct Plant // 植物结构体
{
    int type;     //植物类型, -1 表示草地
    int frameId;  //表示植物摆动帧
    int blood;    //植物血量
} Plant;

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

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

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

for (int i = 0; i < MAX_ZOMBIE_NUM; ++i) //渲染僵尸
{
    if (zombies[i].used) 
    {
        if (zombies[i].isDead) putimagePNG(zombies[i].x, zombies[i].y + 30, &imgDeadZombies[zombies[i].frameId]);
        else if (zombies[i].isEating) putimagePNG(zombies[i].x, zombies[i].y + 30, &imgZombiesEat[zombies[i].frameId]);
        else putimagePNG(zombies[i].x, zombies[i].y + 30, &imgZombies[zombies[i].frameId]);
    }
}

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

/* 更新游戏属性的接口 */
void updateGame() 
{
    updatePlantsPic();
    createSunshine();
    updateSunshine();
    createZombie();
    updateZombie();
    shoot();
    updateBullets();
    collsionCheck();
    eatPlants();
}
/* 移除死亡的植物 */
Plant* plantDeath(Plant* plant)
{
    assert(plant);
    if (plant->type == PEA)  //释放对应种植植物内存
        free((PeaShooter*)plant);
    else if (plant->type == SUNFLOWER)
        free((SunFlower*)plant);

    Grass* grassPtr = (Grass*)calloc(1, sizeof(Grass)); //重置为草地
    assert(grassPtr);
    grassPtr->plant.type = -1;
    return (Plant*)grassPtr;
}

/* 僵尸吃植物接口 */
void eatPlants()
{
    PeaShooter* peaShooter = NULL;
    int row = 0, plantX = 0, zombieCurrX = 0;
    for (int i = 0; i < MAX_ZOMBIE_NUM; ++i) //遍历是否存在僵尸
    {
        if (zombies[i].used && !zombies[i].isDead) //僵尸正在使用中, 且存活
        {
            row = zombies[i].row;
            for (int j = 0; j < GRASS_GRID_COL; ++j) //遍历当前行是否存在植物
            {
                if (plants[row][j]->type >= PEA) 
                {
                    plantX = GRASS_LEFT_MARGIN + j * GRASS_GRID_WIDTH + 5; 
                    zombieCurrX = zombies[i].x + 80;
                    if (zombieCurrX > plantX + 10 && zombieCurrX < plantX + 60) //当僵尸已经到达植物附近
                    {
                        zombies[i].isEating = true;
                        plants[row][j]->blood -= 1; //植物扣血
                        if (plants[row][j]->blood <= 0) //植物被杀死
                        {
                            plants[row][j] = plantDeath(plants[row][j]); //移除死亡的植物
                            zombies[i].frameId = 0;
                            zombies[i].isEating = false; //僵尸解除吃植物状态
                        }
                    }
                }
            }
        }
    }
}

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

void updateZombie() 
{
    static int CallCnt = 0; //延缓函数调用次数
    if (++CallCnt < 3) return;
    CallCnt = 0;

    for (int i = 0; i < MAX_ZOMBIE_NUM; ++i)
    {
        if (zombies[i].used)
        {
            if (zombies[i].isDead)
            {
                if (++zombies[i].frameId >= MAX_ZOMBIE_DEAD_PIC_NUM) //僵尸死亡则更换死亡帧
                    zombies[i].used = false; //重置僵尸状态
            }
            else if (zombies[i].isEating)
            {
                zombies[i].frameId = ++zombies[i].frameId % MAX_ZOMBIE_EAT_PIC_NUM; //僵尸更换图片帧
            }
            else
            {
                zombies[i].x -= zombies[i].speed; //僵尸行走
                zombies[i].frameId = ++zombies[i].frameId % MAX_ZOMBIE_PIC_NUM; //僵尸更换图片帧
            }

            if (zombies[i].x < 170) //目前先这样写待优化
            {
                printf("GAME OVER !");
                MessageBox(NULL, "over", "over", 0);
                exit(0);
            }
        }
    }
}

效果展示

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

image-20241230220759638

十四 向日葵生成阳光

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

向日葵结构体增加变量

enum SUN_SHINE_STATUS { UNUSED, PRODUCE, GROUND, COLLECT };

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

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

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

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

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

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

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

/* 种植植物接口, 主要释放草格子内存, 二维指针数组对应位置,指向初始化的植物 */
Plant* growPlants(Plant* plant, int type, int x, int y)
{
    assert(plant);
    free((Grass*)plant); //释放该位置草格子内存
    if (type == PEA) //根据类型初始化 PeaShooter
    {
        PeaShooter* peaShooter = (PeaShooter*)calloc(1, sizeof(PeaShooter)); //calloc 函数替代 malloc, 省略 memset
        assert(peaShooter);
        peaShooter->shootSpeed = DEFAULT_SHOOT_TIME; //豌豆射击速度, 或者叫豌豆发射子弹的时间间隔, -1 表示可发射子弹
        peaShooter->plant.blood = 100;
        return (Plant*)peaShooter;
    }
    else if (type == SUNFLOWER) //根据类型初始化 SunFlower
    {
        SunFlower* sunFlower = (SunFlower*)calloc(1, sizeof(SunFlower));
        assert(sunFlower);
        sunFlower->plant.type = 1;
        sunFlower->plant.blood = 100;
        sunFlower->timeInterval = MAX_TIME_INTERVAL * (4 + rand() % 5); //增加游戏随机性

        /* 初始化贝塞尔曲线 */
        const int distance = (50 + rand() % 50); //只往右抛即可
        const int currPlantX = GRASS_LEFT_MARGIN + y * GRASS_GRID_WIDTH + 5;
        const int currPlantY = GRASS_TOP_MARGIN + x * GRASS_GRID_HIGHT + 10;
        sunFlower->t = 0;
        sunFlower->speed = 0.05;
        sunFlower->p1 = vector2(currPlantX, currPlantY);
        sunFlower->p2 = vector2(sunFlower->p1.x + distance * 0.3, sunFlower->p1.y - 100);
        sunFlower->p3 = vector2(sunFlower->p1.x + distance * 0.7, sunFlower->p1.y - 100);
        sunFlower->p4 = vector2(currPlantX + distance, currPlantY +
            imgPlant[SUNFLOWER][0]->getheight() - imgSunShineBall[0].getheight());

        return (Plant*)sunFlower;
    }
}

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

/* 更新随机阳光球接口, 主要更新随机阳光球的图片帧和处理飞跃状态时的 X Y 轴偏移 */
void updateSunshine()
{
    for (int i = 0; i < MAX_BALLS_NUM; ++i) 
    {
        if (balls[i].used)
        {
            if (balls[i].y < balls[i].destination)
                balls[i].y += 2; //每次移动两个像素
            else //当阳光下落至目标位置时, 停止移动
            {
                if (balls[i].timer < MAX_TIME_INTERVAL) ++balls[i].timer;
                else balls[i].used = false;
            }
            balls[i].frameId = ++balls[i].frameId % SUM_SHINE_PIC_NUM; //修改当前图片帧编号, 并在到达 SUM_SHINE_PIC_NUM 时重置图片帧为 0
        }
        else if (balls[i].xOffset) //阳光球处于飞跃状态
        {
            if (balls[i].y > 0 && balls[i].x > 262)
            {
                const float angle = atan((float)(balls[i].y - 0) / (float)(balls[i].x - 262)); //不断调整阳光球的位置坐标
                balls[i].xOffset = 16 * cos(angle);
                balls[i].yOffset = 16 * sin(angle);
                balls[i].x -= balls[i].xOffset;
                balls[i].y -= balls[i].yOffset;
            }
            else
            {
                balls[i].xOffset = 0;  //阳光球飞至计分器位置, 则将 xOffset 置 0, 且加上 25 积分
                balls[i].yOffset = 0;
                sunShineVal += 25;
            }
        }
    }

    /* 更新向日葵生产的日光 */
    SunFlower* sunFlower = NULL;
    for (int i = 0; i < GRASS_GRID_ROW; ++i) //遍历二维指针数组
    {
        for (int j = 0; j < GRASS_GRID_COL; ++j)
        {
            if (plants[i][j]->type == SUNFLOWER)
            {
                sunFlower = (SunFlower*)plants[i][j];
                if (sunFlower->status == GROUND || sunFlower->status == PRODUCE)
                    sunFlower->sunShine.frameId = ++sunFlower->sunShine.frameId % SUM_SHINE_PIC_NUM; //修改当前图片帧编号, 并在到达 SUM_SHINE_PIC_NUM 时重置图片帧为 0 
            }
        }
    }
}

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

/* 收集随机阳光接口 */
void collectSunShine(ExMessage* msg)
{
    IMAGE* imgSunShine = NULL;
    for (int i = 0; i < MAX_BALLS_NUM; ++i) //遍历阳光球
    {
        if (balls[i].used) //阳光球在使用中
        {
            imgSunShine = &imgSunShineBall[balls[i].frameId]; //找到对应的阳光球图片
            if (msg->x > balls[i].x && msg->x < balls[i].x + imgSunShine->getwidth()
                && msg->y > balls[i].y && msg->y < balls[i].y + imgSunShine->getheight()) //判断鼠标移动的位置是否处于当前阳光球的位置
            {
                PlaySound("res/audio/sunshine.wav", NULL, SND_FILENAME | SND_ASYNC); //异步播放收集阳光球音效
                balls[i].used = false;  //将阳光球状态更改为未使用 (飞跃状态, 因为 xOffset 赋值了)
                const float angle = atan((float)(balls[i].y - 0) / (float)(balls[i].x - 262)); //使用正切函数
                balls[i].xOffset = 16 * cos(angle); //计算 X 轴偏移
                balls[i].yOffset = 16 * sin(angle); //计算 Y 轴偏移
            }
        }
    }

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

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

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

效果展示

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

image-20241231115916738

十五 片头僵尸展示

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

/* 展示界面的僵尸相关变量 */
#define VIEW_ZOMBIE_NUM 9
#define VIEW_ZOMBIE_PIC_NUM 11
IMAGE imgViewZombies[VIEW_ZOMBIE_PIC_NUM];

/* 游戏开始前展示僵尸 */
void viewScence()
{
    int Xmin = WIN_WIDTH - imgBg.getwidth(); //-500
    vector2 zombieVec[VIEW_ZOMBIE_NUM] = {   //展示场景中, 僵尸初始位置
        {550,80},{530,160},{630,170},{530,200},{515,270},
        {565,370},{605,340},{705,280},{690,340}
    };

    int frameIndexArr[VIEW_ZOMBIE_NUM];
    for (int i = 0; i < VIEW_ZOMBIE_NUM; ++i)
        frameIndexArr[i] = rand() % VIEW_ZOMBIE_PIC_NUM;

    int cycleNum = 0; //利用循环计数, 解决僵尸抖动过快
    for (int x = 0; x >= Xmin; x -= 2) //缓慢移动展示僵尸
    {
        BeginBatchDraw(); //双缓冲解决闪屏
        
        putimage(x, 0, &imgBg);
        ++cycleNum; //当循环十次后, 更换每只僵尸的帧图片
        for (int i = 0; i < VIEW_ZOMBIE_NUM; ++i) //循环僵尸个数
        {
            putimagePNG(zombieVec[i].x - Xmin + x, zombieVec[i].y, &imgViewZombies[frameIndexArr[i]]); //渲染僵尸图片
            if (cycleNum > 2)
                frameIndexArr[i] = (++frameIndexArr[i]) % VIEW_ZOMBIE_PIC_NUM; //更换帧图
        }

        if (cycleNum > 2) cycleNum = 0; //重置循环计数
        EndBatchDraw();
        Sleep(5);
    }

    //停留 3 S 展示
    for (int k = 0; k < MAX_TIME_INTERVAL / 2; ++k)
    {
        BeginBatchDraw(); //双缓冲解决闪屏

        putimage(Xmin, 0, &imgBg); //相当于把图片向左移动 500 个像素
        for (int i = 0; i < VIEW_ZOMBIE_NUM; ++i) //循环僵尸个数
        {
            putimagePNG(zombieVec[i].x, zombieVec[i].y, &imgViewZombies[frameIndexArr[i]]); //渲染僵尸图片
            frameIndexArr[i] = (++frameIndexArr[i]) % VIEW_ZOMBIE_PIC_NUM; //更换帧图
        }
        EndBatchDraw();
        Sleep(30);
    }

    //移动回主界面
    cycleNum = 0;
    for (int x = Xmin; x <= 0; x += 2)
    {
        BeginBatchDraw(); //双缓冲解决闪屏

        putimage(x, 0, &imgBg);
        ++cycleNum; //当循环十次后, 更换每只僵尸的帧图片
        for (int i = 0; i < VIEW_ZOMBIE_NUM; ++i) //循环僵尸个数
        {
            if (zombieVec[i].x - Xmin + x > 0)
            {
                putimagePNG(zombieVec[i].x - Xmin + x, zombieVec[i].y, &imgViewZombies[frameIndexArr[i]]); //渲染僵尸图片
                if (cycleNum > 2)
                    frameIndexArr[i] = (++frameIndexArr[i]) % VIEW_ZOMBIE_PIC_NUM; //更换帧图
            }
        }

        if (cycleNum > 2) cycleNum = 0; //重置循环计数
        EndBatchDraw();
        Sleep(5);
    }
}

在主函数中调用

image-20241231191236192

效果展示

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

十六 植物栏滑动

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

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

        EndBatchDraw();
        Sleep(10);
    }
    Sleep(1000);
}

在主函数中调用

image-20241231191614388

效果展示

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

image-20241231191802974

十六 判断游戏结束

相关结构和变量

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

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

/* 创建僵尸接口, 主要用于初始化僵尸 */
void createZombie()
{
    if (killZombies >= INGAME_ZOMBIE_NUM) return;
    static int zombieCallCnt = 0; //延缓函数调用次数并增加些随机性
    static int randZombieCallCnt = 500;
    if (zombieCallCnt++ < randZombieCallCnt) return;
    randZombieCallCnt = 300 + rand() % 200;
    zombieCallCnt = 0;

    for (int i = 0; i < MAX_ZOMBIE_NUM;  ++i) //找一个未在界面的僵尸初始化
    {
        if (!zombies[i].used)
        {
            zombies[i].row = rand() % GRASS_GRID_ROW; //僵尸出现在第几行(从 0 开始)
            zombies[i].x = WIN_WIDTH;
            zombies[i].y = zombies[i].row * GRASS_GRID_HIGHT; //出现在草地的任意一格上
            zombies[i].frameId = 0;
            zombies[i].speed = 1;  //僵尸的移动速度
            zombies[i].blood = 100; //默认僵尸血条为 100
            zombies[i].isDead = false; //僵尸存活
            zombies[i].isEating = false;
            zombies[i].used = true;
            break; //结束循环
        }
    }
}

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

image-20241231192309251

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

image-20241231192447971

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

checkGameOver 会用到 在线 MP3 音频转 WAV

/* 判断游戏输赢 */
IMAGE imgGameOver; //工具栏图片
bool checkGameOver()
{
    if (gameStatus == WIN)
    {
        Sleep(500);
        PlaySound("res/audio/win.wav", NULL, SND_FILENAME | SND_ASYNC); //异步播放音效
        loadimage(0, "res/gameWin.png");
        return true;
    }
    else if (gameStatus == FAIL)
    {
        Sleep(500);
        PlaySound("res/audio/lose.wav", NULL, SND_FILENAME | SND_ASYNC); //异步播放音效
        loadimage(&imgGameOver, "res/gameFail.png");
        putimagePNG(300, 140, &imgGameOver);
        return true;
    }
    return false;
}


/* 主函数 */
int main()
{
    gameInit(); //不能把 startUI 放在 gameInit 前, gameInit 包含了创建游戏图形窗口
    startUI();
    viewScence();
    barsDown();
    updateWindow(); //窗口视图展示

    int timer = 0; //用以计时 20 毫秒更新一次
    while (1)
    {
        userClick(); //监听窗口鼠标事件
        timer += getDelay();

        if (timer > 20)
        {
            updateWindow(); //更新窗口视图
            updateGame(); //更新游戏动画帧
            if (checkGameOver()) break; //判断游戏输赢
            timer = 0;
        }
    }

    destroyPlants(); //释放内存
    system("pause");
    return 0;
}

效果展示

image-20241231203102248

一些游戏体验优化

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

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

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

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

/* 游戏体验优化, 阳光值不足或植物冷却时不能种植 */
IMAGE imgBlackCards[PLANT_CNT]; //植物不能种植卡片
IMAGE imgFreezeCards[PLANT_CNT]; //植物冷却卡片
#define PEA_FREEZE_TIME 500
#define SUMFLOWER_FREEZE_TIME 200
static int peaPlantInterval = 500;
static int sumFlowerPlantInterval = 200;

enum PLANT_CARD_STATUS { BRIGHT, GREY, FREEZE };
int plantCardStatus[PLANT_CNT]; //植物卡片状态数组

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

/* 更新植物卡牌状态 */
void updatePlantCardStatus()
{
    for (int i = 0; i < PLANT_CNT; ++i) //判断植物卡牌状态
    {
        if (i == PEA)
        {
            if (sunShineVal < 100)                  //阳光值不够
                plantCardStatus[i] = GREY;          //卡片灰色
            else if (sunShineVal >= 100 && peaPlantInterval < PEA_FREEZE_TIME) //阳光值够但在冷却时间内
                plantCardStatus[i] = FREEZE;        //卡片冻结
            else
                plantCardStatus[i] = BRIGHT;        //卡片原色
        }
        else if (i == SUNFLOWER)
        {
            if (sunShineVal < 50)
                plantCardStatus[i] = GREY;
            else if (sunShineVal >= 50 && sumFlowerPlantInterval < SUMFLOWER_FREEZE_TIME)
                plantCardStatus[i] = FREEZE;
            else
                plantCardStatus[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); //但植物栏的位置为 0
        else 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]);
            else
                putimage(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, 省略 memset
        assert(peaShooter);
        peaShooter->shootSpeed = DEFAULT_SHOOT_TIME; //豌豆射击速度, 或者叫豌豆发射子弹的时间间隔, -1 表示可发射子弹
        peaShooter->plant.blood = 100;

        //扣除太阳值和重置冷却
        sunShineVal -= 100;
        peaPlantInterval = 0;
        updatePlantCardStatus();
        return (Plant*)peaShooter;
    }
    else if (type == SUNFLOWER) //根据类型初始化 SunFlower
    {
        SunFlower* sunFlower = (SunFlower*)calloc(1, sizeof(SunFlower));
        assert(sunFlower);
        sunFlower->plant.type = 1;
        sunFlower->plant.blood = 100;
        sunFlower->timeInterval = MAX_TIME_INTERVAL * (4 + rand() % 5);

        /* 初始化贝塞尔曲线 */
        const int distance = (50 + rand() % 50); //只往右抛即可
        const int currPlantX = GRASS_LEFT_MARGIN + y * GRASS_GRID_WIDTH + 5;
        const int currPlantY = GRASS_TOP_MARGIN + x * GRASS_GRID_HIGHT + 10;
        sunFlower->t = 0;
        sunFlower->speed = 0.05;
        sunFlower->p1 = vector2(currPlantX, currPlantY);
        sunFlower->p2 = vector2(sunFlower->p1.x + distance * 0.3, sunFlower->p1.y - 100);
        sunFlower->p3 = vector2(sunFlower->p1.x + distance * 0.7, sunFlower->p1.y - 100);
        sunFlower->p4 = vector2(currPlantX + distance, currPlantY +
            imgPlant[SUNFLOWER][0]->getheight() - imgSunShineBall[0].getheight());

        sunShineVal -= 50;  //扣除太阳值和重置冷却
        sumFlowerPlantInterval = 0;
        updatePlantCardStatus();
        return (Plant*)sunFlower;
    }
}

原 updatePlantsPic 接口中更新 peaPlantInterval 和 sumFlowerPlantInterval

/* 更新植物图片帧接口, 主要用于实现植物摇摆 */
void updatePlantsPic()
{
    ++peaPlantInterval;
    ++sumFlowerPlantInterval;
    updatePlantCardStatus();

    for (int i = 0; i < GRASS_GRID_ROW; ++i) //遍历二维指针数组
    {
        for (int j = 0; j < GRASS_GRID_COL; ++j)
        {
            if (plants[i][j]->type >= PEA && //找到非草地的植物
                imgPlant[plants[i][j]->type][++plants[i][j]->frameId] == NULL) //将植物图片增加一, 判断是否到达图片帧末尾            
                    plants[i][j]->frameId = 0; //重置图片帧为零
        }
    }
}

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

image-20241231205401264

效果展示

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

image-20241231205535580

③ 添加各种音乐

加上音效

初始背景音乐

/* 游戏开始前的菜单界面 */
void startUI()
{
    IMAGE imageBg, imgMenu1, imgMenu2;
    loadimage(&imageBg, "res/menu.png");
    loadimage(&imgMenu1, "res/menu1.png");
    loadimage(&imgMenu2, "res/menu2.png");
    PlaySound("res/audio/bg.wav", NULL, SND_FILENAME | SND_ASYNC); //异步播放音效

    bool mouseStatus = false; //0 表示鼠标未移动至开始游戏位置
    while (1) 
    {
        BeginBatchDraw(); //双缓冲解决闪屏
        putimage(0, 0, &imageBg);
        putimagePNG(UI_LEFT_MARGIN, UI_TOP_MARGIN, mouseStatus ? &imgMenu2 : &imgMenu1); //根据鼠标是否移动至游戏开始位置, 显示不同的图片

        ExMessage msg;
        if (peekmessage(&msg)) //监听鼠标事件
        {
            if (msg.x > UI_LEFT_MARGIN && msg.x < UI_LEFT_MARGIN + UI_WIDTH
                && msg.y > UI_TOP_MARGIN && msg.y < UI_TOP_MARGIN + UI_HIGHT) //当鼠标移动至开始游戏位置, 界面高亮
            {
                putimagePNG(UI_LEFT_MARGIN, UI_TOP_MARGIN, &imgMenu2);
                mouseStatus = true; //表示鼠标移动至开始游戏位置, 如果一直不移动鼠标则一直高亮

                if (msg.message == WM_LBUTTONDOWN) //当鼠标点击时, 进入游戏
                {
                    PlaySound(0, 0, SND_FILENAME);
                    EndBatchDraw();
                    return; //结束函数
                }
            }
            else mouseStatus = false; //当鼠标未移动至开始游戏位置, 界面不高亮
        }
        EndBatchDraw();
    }
}

片头背景音乐

image-20241231233818079

僵尸来了背景音乐

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

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

image-20241231233933248

选取植物背景音乐

image-20241231234142778

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

image-20241231234225477

豌豆射击的音乐

image-20241231234315880

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

image-20241231233515466

遗留问题

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2271777.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

编程入门(2)-2024年 RAD Studio version 12发布综述

随着2024年即将画上句号&#xff0c;我想借此机会回顾一下我们在这一年中发布的一些Embarcadero产品、行业趋势&#xff0c;并感谢我们尊贵的客户们对我们的产品一如既往的支持。这一年对我们来说充满了激动人心的变化和发展&#xff0c;我们非常高兴能与您一起踏上这段旅程。 …

[Day 12]904.水果成篮

今天给带来的题目是滑动窗口的另一种题目&#xff0c;之前我们讲了滑动窗口题目中长度最小的子数组&#xff0c;今天这个题目实际上是求长度最长的子数组 题目描述&#xff1a;力扣链接 904.水果成篮 你正在探访一家农场&#xff0c;农场从左到右种植了一排果树。这些树用一个整…

SpringBoot 2.6 集成es 7.17

引言 在现代应用开发中&#xff0c;Elasticsearch作为一个强大的搜索引擎和分析引擎&#xff0c;已经成为许多项目不可或缺的一部分。Spring Boot作为Java生态中最受欢迎的微服务框架之一&#xff0c;其对Elasticsearch的支持自然也是开发者关注的焦点。本文将详细介绍如何在S…

【数据仓库】hive on Tez配置

hive on Tez 搭建 前提是hive4.0hadoop3.2.2数仓已搭建完成&#xff0c;现在只是更换其执行引擎 为Tez。搭建可参考【数据仓库】hive hadoop数仓搭建实践文章。 Tez 下载 下载地址 https://archive.apache.org/dist/tez/ 官网地址 https://tez.apache.org/releases/apac…

云备份项目--客户端编写

文章目录 10. 客户端工具类10.1 整体的类10.2 测试 11 客户端数据管理类11.1 整体的类11.2 测试 12. 客户端业务处理12.1 整体的类 完整的代码–gitee链接 10. 客户端工具类 10.1 整体的类 在windows平台下进行开发&#xff0c;Util.hpp实际上是客户端FileUtil.hpp和JsonUtil…

MySQL 的事务与多版本并发控制(MVCC)的那些事

什么是事务原子性:一致性隔离性 问题1: 为什么MySQL要使用mvcc实现隔离性而不使用 锁 解决并发问题?持久性 问题2: MySQL 不是磁盘数据库吗,持久化为什么是 redo log 保证的?问题 3: redo log 储存了什么东西,持久化(崩溃恢复是怎么做的?)问题 4 : MySQL 的 bing log (二进制…

Eplan 项目结构(高层代号、安装地点、位置代号)

Eplan中的项目结构分为3个层次&#xff1a; &#xff08;1&#xff09;功能面结构。指明这个系统的功能&#xff0c;有什么用途。在EPlan中&#xff0c;指的就是"高层代号&#xff08;&#xff09;"。 一般指的是线体。 &#xff08;2&#xff09;位置面结构。指明该…

OWASP ZAP之API 请求基础知识

ZAP API 提供对 ZAP 大部分核心功能的访问,例如主动扫描器和蜘蛛。ZAP API 在守护进程模式和桌面模式下默认启用。如果您使用 ZAP 桌面,则可以通过访问以下屏幕来配置 API: Tools -> Options -> API。 ZAP 需要 API 密钥才能通过 REST API 执行特定操作。必须在所有 …

短信通知在 IOS 17/18 中不起作用?这是修复方法

问题 “我最近将 iPhone 更新到了 iOS 17/18。我真的很兴奋&#xff0c;直到我发现 iOS 17/18 中没有 iMessage 文本通知。此后我的兴奋变成了失望。请告诉我如何解决这个问题&#xff1f; ” 我们知道这可能是一个大问题&#xff0c;因为我们通常不会打开消息应用程序&#…

从0开始的opencv之旅(1)cv::Mat的使用

目录 Mat 存储方法 创建一个指定像素方式的图像。 尽管我们完全可以把cv::Mat当作一个黑盒&#xff0c;但是笔者的建议是仍然要深入理解和学习cv::Mat自身的构造逻辑和存储原理&#xff0c;这样在查找问题&#xff0c;或者是遇到一些奇奇怪怪的图像显示问题的时候能够快速的想…

机场安全项目|基于改进 YOLOv8 的机场飞鸟实时目标检测方法

目录 论文信息 背景 摘要 YOLOv8模型结构 模型改进 FFC3 模块 CSPPF 模块 数据集增强策略 实验结果 消融实验 对比实验 结论 论文信息 《科学技术与工程》2024年第24卷第32期刊载了中国民用航空飞行学院空中交通管理学院孔建国, 张向伟, 赵志伟, 梁海军的论文——…

《Rust权威指南》学习笔记(二)

枚举enum 1.枚举的定义和使用如下图所示&#xff1a; 定义时还可以给枚举的成员指定数据类型&#xff0c;例如&#xff1a;enum IpAddr{V4(u8, u8, u8, u8),V6(String),}。枚举的变体都位于标识符的命名空间下&#xff0c;使用::进行分隔。 2.一个特殊的枚举Option&#xff0…

OSI模型的网络层中产生拥塞的主要原因?

&#xff08; 1 &#xff09;缓冲区容量有限&#xff1b;&#xff08; 1.5 分&#xff09; &#xff08; 2 &#xff09;传输线路的带宽有限&#xff1b;&#xff08; 1.5 分&#xff09; &#xff08; 3 &#xff09;网络结点的处理能力有限&#xff1b;&#xff08; 1 分…

linux上安装MySQL教程

1.准备好MySQL压缩包&#xff0c;并进行解压 tar -xvf mysql-5.7.28-1.el7.x86_64.rpm-bundle.tar -C /usr/local 2.检查是否有mariadb数据库 rpm -aq|grep mariadb 关于mariadb:是MySQL的一个分支&#xff0c;主要由开源社区在维护&#xff0c;采用GPL授权许可 MariaDB的目…

R语言中的时间序列分析·

1 数据集说明 AirPassengers 1949~1960年每月乘坐飞机的乘客数 JohnsonJohnson Johnson&Johnson每股季度收入 nhtemp 康涅狄格州纽黑文地区从1912年至1971年每年的平均气温 Nile 尼罗河的流量 sunspots 1749年~1983年月平均太阳黑子数 2 相关包 xts、forecast、tser…

LookingGlass使用

背景 Looking Glass 是一款开源应用程序&#xff0c;可以直接使用显卡直通的windows虚拟机。 常见环境是Linux hostwindows guest&#xff0c;基本部署结构图&#xff1a; 编译 git clone --recursive https://github.com/gnif/LookingGlass.git编译client mkdir client/b…

HCIA-Access V2.5_7_3_XG(S)原理_关键技术

为什么需要测距 因为上行链路只有一根纤,而且每一个ONU到OLT的距离是不一样的,虽然上行通过TDMA技术,让每一个ONU在不同的时间段发送数据,但是仍然有可能在同一时刻到达分光器,产生数据冲突。 有测距的信元传输 所以为了避免碰撞冲突,通过ONU在注册的时候就会启动测距…

四、VSCODE 使用GIT插件

VSCODE 使用GIT插件 一下载git插件与git Graph插件二、git插件使用三、文件提交到远程仓库四、git Graph插件 一下载git插件与git Graph插件 二、git插件使用 git插件一般VSCode自带了git&#xff0c;就是左边栏目的图标 在下载git软件后vscode的git插件会自动识别当前项目 …

RISC-V学习笔记

1.RISC ISA1个基本整数指令集多个可选的扩展指令集&#xff0c;如RV32I表示支持32位整数指令集。I表示基本指令集&#xff0c;M表示整数乘法与除法指令集&#xff0c;A表示存储器原子指令集&#xff0c;F表示单精度浮点指令集&#xff0c;D表示双精度浮点指令集等&#xff0c;C…

strapi中使用Documentation插件

Swagger UI 自动生成并展示了 API 的文档&#xff0c;这些文档是根据 OpenAPI Specification (OAS) 格式编写的。它提供了对 API 端点、请求方法&#xff08;GET, POST, PUT, DELETE 等&#xff09;、参数、响应格式等详细信息的描述 安装 npm run strapi install documentat…