目录
1. 简介
项目背景
目标
2. 项目概述
游戏概述
技术栈
主要特性
3. 开发过程
项目架构
游戏主循环
1. 简介
项目背景
《植物大战僵尸》是一款经典的塔防类游戏,因其独特的策略性和趣味性深受玩家喜爱。为了提升自己的C语言编程能力,同时学习游戏开发的基础知识,我决定使用EasyX图形库制作一个简化版的《植物大战僵尸》小游戏,在B站程序员Rock的零基础教学的学习下完成了此项目开发,而且在教程基础上增加了火爆辣椒,寒冰豌豆射手,三重豌豆射手等植物,以及游戏结束判断,游戏音效等功能。(源码获取:文章最后)
【【程序员Rock】C语言项目:完整版植物大战僵尸!可能是B站最好的植物大战僵尸教程了!零基础手把手游戏开发】
目标
这篇博客旨在分享开发过程中的技术细节和心得,希望帮助其他对游戏开发和C语言感兴趣的读者。同时,通过这个项目进一步提升自己的编程和问题解决能力。
2. 项目概述
游戏概述
小游戏保留了《植物大战僵尸》的核心玩法:玩家需要种植各种植物来阻挡和消灭入侵的僵尸,防止它们到达终点,同时对部分原游戏的功能进行了简化处理,感兴趣读者在获取源码后可自行拓展。
技术栈
- 编程语言:C语言
- 图形库:EasyX(用于处理图形渲染和简单的用户输入)
- 开发工具:Visual Studio 2022
主要特性
- 植物种类:包括多种豌豆射手、向日葵和火爆辣椒等。
- 僵尸种类:普通僵尸、铁桶僵尸、路障僵尸、旗子僵尸等。
3. 开发过程
项目架构
游戏的开发架构主要由5个核心模块构成:游戏主循环、游戏图形渲染、游戏初始化、游戏状态更新以及人机交互。
游戏主循环
1.代码概述:
这个主循环代码使用了C语言和EasyX图形库来开发一款简化版的《植物大战僵尸》游戏。代码结构清晰,主要包括以下几个核心部分:
- 图像加载与初始化:使用了
IMAGE
类型的变量和数组来存储游戏所需的图片资源。 - 结构体数组的定义与初始化:用于管理游戏中的植物、僵尸、子弹和阳光球等元素。
- 主游戏循环:循环负责处理用户交互、控制游戏帧率、调用渲染和更新函数。
2.代码解析:
2.1 图像加载与初始化
游戏开始时,需要加载各类图片资源并初始化结构体:
int WIN_WIDTH = 900; int WIN_HIGHTH = 600; IMAGE imgBg, imgBar, imgEnd, imgWin; IMAGE imgCards[ZHI_WU_COUNT]; IMAGE* imgZhiWu[ZHI_WU_COUNT][20]; IMAGE imgsunshineBall[29]; ...
定义了多种
IMAGE
类型的变量来存储不同的游戏资源图片,例如背景、植物卡牌、阳光球、僵尸等。
2.2 结构体数组的定义与初始化
定义并初始化了各种结构体数组,这些数组用于存储游戏中的各种动态对象,例如植物、僵尸、子弹等:
struct zhiwu map[5][9]; struct sunshineBalls balls[10]; struct zm zms[10]; ...
通过
memset
函数对这些结构体数组进行初始化,确保所有状态在游戏开始时为默认值。
2.3 主游戏循环
主循环是游戏的核心,它不断运行,直到玩家退出游戏:
while (1) { UCInteraction(&msg, &sunshine, mapptr, ballsptr, &curZhiwu, &curX, &curY, imgsunshineBallptr, ballMax); timer += getDelay(); if (timer > 35) { ... } if (flag) { flag = false; drawWindow(...); updateGame(...); } }
- UI交互模块:
UCInteraction
函数处理用户的输入操作,如种植植物、收集阳光等。- 渲染与更新模块:
drawWindow
负责游戏的图像渲染,而updateGame
则更新游戏中的各类数据,包括植物生长、僵尸移动、子弹发射等。- 帧率控制:通过
timer
和getDelay
函数来控制游戏帧率,确保游戏运行的流畅性。
3. 代码的进一步优化建议
- 内存管理:确保所有动态分配的内存(例如
imgZhiWu[i][j]
)在游戏结束时正确释放,以避免内存泄漏。 - 函数拆分:考虑将主循环中的一些逻辑进一步拆分为更小的函数,以提高代码的可读性和可维护性。
游戏初始化模块
游戏初始化模块包括了游戏窗口的创建、图片资源的加载、游戏数据的初始化和字体设置等功能。以下是代码的整体结构和功能的逐步解释:
1. 游戏初始化函数
gameInit
该函数主要用于初始化游戏窗口、加载资源、初始化数据、设置字体和随机种子。
void gameInit(IMAGE* imgBg, IMAGE* imgBar, IMAGE* imgEnd, IMAGE* imgWin, IMAGE* (*imgZhiWu)[20], IMAGE* imgCards, IMAGE* imgsunshineBall, IMAGE* imgBulletNormal, IMAGE* imgZMNormal, IMAGE* imgZMFlag, IMAGE* imgZMConehead, IMAGE* imgZMBuckethead, IMAGE* imgZMSnow, IMAGE* imgZMFlagSnow, IMAGE* imgZMConeheadSnow, IMAGE* imgZMBucketheadSnow, IMAGE* imgZMEat, IMAGE* imgZMEatFlag, IMAGE* imgZMEatConehead, IMAGE* imgZMEatBuckethead, IMAGE* imgBulletBlast, IMAGE* imgJalapenoExplode, IMAGE* imgZMDead, IMAGE* imgZmStand, IMAGE* imgBulletSnow, struct zm* zms, struct zmFlag* zmFlags, struct zmConehead* zmConeheads, struct zmBuckethead* zmBucketheads, struct bulletsnow* bullet_snows, struct zhiwu(*map)[9], struct sunshineBalls* balls, struct bullet* bullets, int* curY, int* curZhiwu, int* sunshine) { int WIN_WIDTH = 900; int WIN_HIGHTH = 600; // 创建游戏的图形窗口 initgraph(WIN_WIDTH, WIN_HIGHTH, 1); // 加载照片 Loadimage(imgBg, imgBar, imgEnd, imgWin, imgZhiWu, imgCards, imgsunshineBall, imgBulletNormal, imgZMNormal, imgZMFlag, imgZMConehead, imgZMBuckethead, imgZMSnow, imgZMFlagSnow, imgZMConeheadSnow, imgZMBucketheadSnow, imgZMEat, imgZMEatFlag, imgZMEatConehead, imgZMEatBuckethead, imgBulletBlast, imgJalapenoExplode, imgZMDead, imgZmStand, imgBulletSnow); // 初始化数据 InitData(zms, zmFlags, zmConeheads, zmBucketheads, bullet_snows, map, imgZhiWu, imgCards, balls, bullets, curY, curZhiwu, sunshine); // 设置阳光值字体 SetSunValueFont(); // 配置随机种子 srand(time(NULL)); }
2. 图片加载函数
Loadimage
Loadimage
函数用于加载游戏中的所有图片资源。包括地图背景、植物栏、僵尸、阳光球和子弹等。void Loadimage(IMAGE* imgBg, IMAGE* imgBar, IMAGE* imgEnd, IMAGE* imgWin, IMAGE* (*imgZhiWu)[20], IMAGE* imgCards, IMAGE* imgsunshineBall, IMAGE* imgBulletNormal, IMAGE* imgZMNormal, IMAGE* imgZMFlag, IMAGE* imgZMConehead, IMAGE* imgZMBuckethead, IMAGE* imgZMSnow, IMAGE* imgZMFlagSnow, IMAGE* imgZMConeheadSnow, IMAGE* imgZMBucketheadSnow, IMAGE* imgZMEat, IMAGE* imgZMEatFlag, IMAGE* imgZMEatConehead, IMAGE* imgZMEatBuckethead, IMAGE* imgBulletBlast, IMAGE* imgJalapenoExplode, IMAGE* imgZMDead, IMAGE* imgZmStand, IMAGE* imgSnowBulletNormal) { // 加载背景图片 loadimage(imgBg, "res/map0.jpg"); // 地图图片 loadimage(imgBar, "res/bar5.png"); // 植物栏图片 loadimage(imgEnd, "res/fail2.png"); // 游戏结束界面 loadimage(imgWin, "res/gameWin.png"); // 游戏胜利界面 char name[64]; // 定义字符串数组存储照片所在文件路径 // 加载其他图片资源 // (后续省略了对多个僵尸、子弹、阳光等的图片加载) }
3. 数据初始化函数
InitData
InitData
函数负责初始化游戏中的各种数据结构,包括僵尸、植物、子弹、阳光球等的初始状态。void InitData(struct zm* zms, struct zmFlag* zmFlags, struct zmConehead* zmConeheads, struct zmBuckethead* zmBucketheads, struct bulletsnow* bullet_snows, struct zhiwu(*map)[9], IMAGE* (*imgZhiWu)[20], IMAGE* imgCards, struct sunshineBalls* balls, struct bullet* bullets, int* curY, int* curZhiwu, int* sunshine) { // 指针数组初始化置零 memset(imgZhiWu, 0, sizeof(imgZhiWu)); // 初始化植物的状态 memset(map, 0, sizeof(map)); // 初始化阳光球数据 memset(balls, 0, sizeof(balls)); // 初始化僵尸、子弹和阳光值等数据 // (省略了具体初始化过程) }
4. 字体设置函数
SetSunValueFon
SetSunValueFont
函数用于设置阳光值显示的字体和颜色。void SetSunValueFont() { LOGFONT f; gettextstyle(&f); f.lfHeight = 30; f.lfWeight = 15; strcpy(f.lfFaceName, "Segoe UI Black"); f.lfQuality = ANTIALIASED_QUALITY; // 抗锯齿效果 settextstyle(&f); setbkmode(TRANSPARENT); // 设置背景模式 setcolor(BLACK); }
5. 文件存在性判断函数
fileExist
fileExist
函数用于判断指定路径的文件是否存在。bool fileExist(const char* name) { FILE* fp = fopen(name, "r"); if (fp == NULL) { return false; } else { fclose(fp); return true; } }
6.总结
- 游戏初始化:创建图形窗口,加载图片资源,初始化数据结构。
- 资源管理:加载图片文件并检查文件是否存在。
- 数据初始化:重置游戏数据的初始状态,为游戏开始做好准备。
- 字体设置:确保显示阳光值的字体美观且易读。
这个模块是游戏启动时的核心部分,它确保了所有资源和数据结构在游戏开始前已经准备好,并且使用适当的随机种子保证游戏中的随机事件(如僵尸出现的时间和位置)能够正常工作。
游戏状态更新模块
1.部分代码展示
游戏状态更新模块组织清晰,功能分布合理,涵盖了各种游戏元素的更新逻辑,如植物状态、阳光球、各种僵尸、子弹等。
#include "AllHeader.h" void updateGame(struct zhiwu(*map)[9], struct sunshineBalls* balls, struct zm* zms, struct zmFlag* zmFlags, struct zmConehead* zmConeheads, struct zmBuckethead* zmBucketheads, struct bullet* bullets, struct bulletsnow* bullet_snows, int* sunshine, IMAGE* (*imgZhiWu)[20], IMAGE* imgEnd, IMAGE* imgsunshineBall, IMAGE* imgZMNormal, int ballMax, int zmCount, int zmCountFlag, int zmCountCone, int zmCountBucket, int countnormalMax, int countsnowMax, int dangerX, IMAGE* imgWin) { upodateZhiWuState(map, imgZhiWu);更新植物种植后的状态 /*阳光球*/ createSunshine(balls, map, imgZhiWu, imgsunshineBall, ballMax);//创建阳光球 updateSunshine(balls, sunshine, ballMax);//更新阳光球状态 /*普通僵尸*/ createZMNormal(zms, zmCount);//创建普通僵尸 updateZMNormal(zms, imgEnd, zmCount);//更新普通僵尸的状态 /*旗子僵尸*/ createZMFlag(zmFlags, zmCount);//创建旗子僵尸 updateZMFlag(zmFlags, imgEnd, zmCount);//更新旗子僵尸的状态 /*路障僵尸*/ createZMConehead(zmConeheads, zmCount);//创建路障僵尸 updateZMConehead(zmConeheads, imgEnd, zmCount);//更新路障僵尸的状态 /*铁桶僵尸*/ createZMBuckethead(zmBucketheads, zmCount);//创建铁通僵尸 updateZMBuckethead(zmBucketheads, imgEnd, zmCount);//更新铁桶僵尸的状态 /*普通豌豆射手*/ shoot_single(zms, zmFlags, zmConeheads, zmBucketheads, bullets, map, imgZMNormal, imgZhiWu, countnormalMax, zmCount, zmCountFlag, zmCountCone, zmCountBucket, dangerX);//发射普通豌豆子弹 updateSingleBullets(bullets, countnormalMax);//更新普通豌豆子弹 /*双重豌豆射手*/ shoot_double(zms, zmFlags, zmConeheads, zmBucketheads, bullets, map, imgZMNormal, imgZhiWu, countnormalMax, zmCount, zmCountFlag, zmCountCone, zmCountBucket, dangerX);//发射两连发(普通)豌豆子弹 updateDoubleBullets(bullets, countnormalMax);//更新两连发(普通)豌豆子弹 /*三重豌豆射手*/ shoot_three(zms, zmFlags, zmConeheads, zmBucketheads, bullets, map, imgZMNormal, imgZhiWu, countnormalMax, zmCount, zmCountFlag, zmCountCone, zmCountBucket, dangerX);//发射三连发(普通)豌豆子弹 updateThreeBullets(bullets, countnormalMax);//更新三连发(普通)豌豆子弹 /*寒冰豌豆射手*/ shoot_snow(zms, zmFlags, zmConeheads, zmBucketheads, bullet_snows, map, imgZMNormal, imgZhiWu, countsnowMax, zmCount, zmCountFlag, zmCountCone, zmCountBucket, dangerX);//发射寒冰豌豆子弹 updateSnowBullets(bullet_snows, countsnowMax);//更新寒冰豌豆子弹 /*辣椒*/ updateJalapeno(map);//更新辣椒 checkNormalBullet2Zm(bullets, zms, zmFlags, zmConeheads, zmBucketheads, countnormalMax, zmCount, zmCountFlag, zmCountCone, zmCountBucket);//普通子弹对僵尸的碰撞检测 checkSnowBullet2Zm(bullet_snows, zms, zmFlags, zmConeheads, zmBucketheads, countnormalMax, zmCount, zmCountFlag, zmCountCone, zmCountBucket);//实现寒冰子弹对僵尸的碰撞检测 checkFlame2Zm(map, zms, zmFlags, zmConeheads, zmBucketheads, zmCount, zmCountFlag, zmCountCone, zmCountBucket, dangerX);//实现辣椒火焰与僵尸的接触检测 checkZm2ZhiWu(zms, zmFlags, zmConeheads, zmBucketheads, map, zmCount, zmCountFlag, zmCountCone, zmCountBucket);//实现僵尸对植物的碰撞检测 checksuccess(zms, zmFlags, zmConeheads, zmBucketheads, zmCount, zmCountFlag, zmCountCone, zmCountBucket, imgWin);//判断是否挑战成功 } //创建阳光球 void createSunshine(struct sunshineBalls* balls, struct zhiwu(*map)[9], IMAGE* (*imgZhiWu)[20], IMAGE* imgsunshineBall, int ballMax) { static int count = 0; static int fre = 450; count++; if (count >= fre) { fre = 300 + rand() % 150; count = 0; //从阳光池中取一个可以使用的 int i; for (i = 0; i < ballMax && balls[i].used; i++); if (i >= ballMax)return; balls[i].used = true; balls[i].frameIndex = 0; balls[i].timer = 0; balls[i].status = SUNSHINE_DOWN; balls[i].t = 0; balls[i].p1 = vector2(260 - 112 + rand() % (900 - (260 - 140)), 60); balls[i].p4 = vector2(balls[i].p1.x, 200 + (rand() % 4) * 90); float off = 2; float distance = balls[i].p4.y - balls[i].p1.y; balls[i].speed = 1.0 / (distance / off); } //向日葵生产阳光 for (int i = 0; i < 5; i++) { for (int j = 0; j < 9; j++) { if (map[i][j].type == XIANG_RI_KUI + 1) { map[i][j].timer++; if (map[i][j].timer > 300) { map[i][j].timer = 0; int k; for (k = 0; k < ballMax && balls[k].used; k++); if (k >= ballMax)return; balls[k].used = true; balls[k].p1 = vector2(map[i][j].x, map[i][j].y); int w = (100 + rand() % 50) * (rand() % 2 ? 1 : -1); balls[k].p4 = vector2(map[i][j].x + w, map[i][j].y + imgZhiWu[XIANG_RI_KUI][0]->getheight() - imgsunshineBall[0].getheight()); balls[k].p2 = vector2(balls[k].p1.x + w * 0.3, balls[k].p1.y - 100); balls[k].p3 = vector2(balls[k].p1.x + w * 0.7, balls[k].p1.y - 100); balls[k].status = SUNSHINE_PRODUCT; balls[k].speed = 0.05; balls[k].t = 0; } } } } } //更新阳光球 void updateSunshine(struct sunshineBalls* balls, int* sunshine, int ballMax) { for (int i = 0; i < ballMax; i++) { if (balls[i].used) { balls[i].frameIndex = (balls[i].frameIndex + 1) % 29; if (balls[i].status == SUNSHINE_DOWN) { struct sunshineBalls* sun = &balls[i]; sun->t += sun->speed; sun->pCur = sun->p1 + sun->t * (sun->p4 - sun->p1); if (sun->t >= 1) { sun->status = SUNSHINE_GROUND; sun->timer = 0; } } else if (balls[i].status == SUNSHINE_GROUND) { balls[i].timer++; if (balls[i].timer > 100) { balls[i].used = false; balls[i].timer = 0; } } else if (balls[i].status == SUNSHINE_COLLECT) { struct sunshineBalls* sun = &balls[i]; sun->t += sun->speed; sun->pCur = sun->p1 + sun->t * (sun->p4 - sun->p1); if (sun->t >= 1) { sun->used = false; (*sunshine) += 25; } } else if (balls[i].status == SUNSHINE_PRODUCT) { struct sunshineBalls* sun = &balls[i]; sun->t += sun->speed; sun->pCur = calcBezierPoint(sun->t, sun->p1, sun->p2, sun->p3, sun->p4);//计算贝塞尔曲线上点的位置 if (sun->t >= 1) { sun->status = SUNSHINE_GROUND; sun->timer = 0; } } } } } //更新植物种植后的状态 void upodateZhiWuState(struct zhiwu(*map)[9], IMAGE* (*imgZhiWu)[20]) { for (int i = 0; i < 5; i++) { for (int j = 0; j < 9; j++) { if (map[i][j].type > 0) { map[i][j].frameIndex++; int zhiWuType = map[i][j].type - 1; int index = map[i][j].frameIndex; if (imgZhiWu[zhiWuType][index] == NULL) { map[i][j].frameIndex = 0; map[i][j].JalapenoframeIndex = 0; } } } } } ... /*三重射手豌豆子弹的碰撞检测*/ for (int i = 0; i < countnormalMax; i++) { if (bullets[i].used == false || bullets[i].triple == false || bullets[i].blast)continue; //与普通僵尸的碰撞检测 for (int k = 0; k < zmCount; k++) { if (zms[k].used == false) continue; int x3 = bullets[i].pCur.x; int y3 = bullets[i].pCur.y; int x1 = zms[k].x + 80; int x2 = zms[k].x + 110; int y1 = zms[k].y + 40; int y2 = zms[k].y + 120; //子弹斜射状态 if (zms[k].dead == false && bullets[i].row == zms[k].row && bullets[i].oblique && x3 > x1 && x3 < x2 && y3 > y1 && y3 < y2) { //PlaySound("res/splat1.wav", NULL, SND_FILENAME | SND_ASYNC); zms[k].blood -= 10; bullets[i].blast = true; bullets[i].speedoblique = 0; if (zms[k].blood <= 0) { zms[k].dead = true; zms[k].speed = 0; zms[k].frameIndex = 0; } } //子弹直射状态 if (zms[k].dead == false && bullets[i].row == zms[k].row && bullets[i].straight && x3 > x1 && x3 < x2) { //PlaySound("res/splat1.wav", NULL, SND_FILENAME | SND_ASYNC); zms[k].blood -= 10; bullets[i].blast = true; bullets[i].speedstraight = 0; if (zms[k].blood <= 0) { zms[k].dead = true; zms[k].speed = 0; zms[k].frameIndex = 0; } } } //与旗子僵尸的碰撞检测 for (int k = 0; k < zmCountFlag; k++) { if (zmFlags[k].used == false) continue; int x3 = bullets[i].pCur.x; int y3 = bullets[i].pCur.y; int x1 = zmFlags[k].x + 80; int x2 = zmFlags[k].x + 110; int y1 = zmFlags[k].y + 40; int y2 = zmFlags[k].y + 120; //子弹斜射状态 if (zmFlags[k].dead == false && bullets[i].row == zmFlags[k].row && bullets[i].oblique && x3 > x1 && x3 < x2 && y3>y1 && y3 < y2) { //PlaySound("res/splat1.wav", NULL, SND_FILENAME | SND_ASYNC); zmFlags[k].blood -= 10; bullets[i].blast = true; bullets[i].speedoblique = 0; if (zmFlags[k].blood <= 0) { zmFlags[k].dead = true; zmFlags[k].speed = 0; zmFlags[k].frameIndex = 0; } } //子弹直射状态 if (zmFlags[k].dead == false && bullets[i].row == zmFlags[k].row && bullets[i].straight && x3 > x1 && x3 < x2) { //PlaySound("res/splat1.wav", NULL, SND_FILENAME | SND_ASYNC); zmFlags[k].blood -= 10; bullets[i].blast = true; bullets[i].speedstraight = 0; if (zmFlags[k].blood <= 0) { zmFlags[k].dead = true; zmFlags[k].speed = 0; zmFlags[k].frameIndex = 0; } } } //与路障僵尸的碰撞检测 for (int k = 0; k < zmCountCone; k++) { if (zmConeheads[k].used == false) continue; int x3 = bullets[i].pCur.x; int y3 = bullets[i].pCur.y; int x1 = zmConeheads[k].x + 80; int x2 = zmConeheads[k].x + 110; int y1 = zmConeheads[k].y + 40; int y2 = zmConeheads[k].y + 120; //子弹斜射状态 if (zmConeheads[k].dead == false && bullets[i].row == zmConeheads[k].row && bullets[i].oblique && x3 > x1 && x3 < x2 && y3>y1 && y3 < y2) { //PlaySound("res/splat1.wav", NULL, SND_FILENAME | SND_ASYNC); zmConeheads[k].blood -= 10; bullets[i].blast = true; bullets[i].speedoblique = 0; if (zmConeheads[k].blood <= 0) { zmConeheads[k].dead = true; zmConeheads[k].speed = 0; zmConeheads[k].frameIndex = 0; } } //子弹直射状态 if (zmConeheads[k].dead == false && bullets[i].row == zmConeheads[k].row && bullets[i].straight && x3 > x1 && x3 < x2) { //PlaySound("res/splat1.wav", NULL, SND_FILENAME | SND_ASYNC); zmConeheads[k].blood -= 10; bullets[i].blast = true; bullets[i].speedstraight = 0; if (zmConeheads[k].blood <= 0) { zmConeheads[k].dead = true; zmConeheads[k].speed = 0; zmConeheads[k].frameIndex = 0; } } } //与铁桶僵尸的碰撞检测 for (int k = 0; k < zmCountBucket; k++) { if (zmBucketheads[k].used == false) continue; int x3 = bullets[i].pCur.x; int y3 = bullets[i].pCur.y; int x1 = zmBucketheads[k].x + 80; int x2 = zmBucketheads[k].x + 110; int y1 = zmBucketheads[k].y + 40; int y2 = zmBucketheads[k].y + 120; //子弹斜射状态 if (zmBucketheads[k].dead == false && bullets[i].row == zmBucketheads[k].row && bullets[i].oblique && x3 > x1 && x3 < x2 && y3>y1 && y3 < y2) { //PlaySound("res/splat1.wav", NULL, SND_FILENAME | SND_ASYNC); zmBucketheads[k].blood -= 10; bullets[i].blast = true; bullets[i].speedoblique = 0; if (zmBucketheads[k].blood <= 0) { zmBucketheads[k].dead = true; zmBucketheads[k].speed = 0; zmBucketheads[k].frameIndex = 0; } } //子弹直射状态 if (zmBucketheads[k].dead == false && bullets[i].row == zmBucketheads[k].row && bullets[i].straight && x3 > x1 && x3 < x2) { //PlaySound("res/splat1.wav", NULL, SND_FILENAME | SND_ASYNC); zmBucketheads[k].blood -= 10; bullets[i].blast = true; bullets[i].speedstraight = 0; if (zmBucketheads[k].blood <= 0) { zmBucketheads[k].dead = true; zmBucketheads[k].speed = 0; zmBucketheads[k].frameIndex = 0; } } } } } ...//完整代码在文章最后自取
2.代码优化建议
-
优化函数调用:可以考虑将功能相似的函数进行合并,例如
createZMNormal
和updateZMNormal
可以合并为一个handleZMNormal
函数,减少代码重复。 -
代码可读性:在
updateGame
函数中,逻辑块之间加上注释,可以提高代码的可读性,便于以后维护。 -
内存管理:如果游戏有内存分配的部分,注意释放内存,防止内存泄漏。
-
边界检测:对游戏中涉及到的位置(如子弹、僵尸的位置等)做边界检测,防止超出游戏窗口或数组越界。
图形渲染模块
1.部分代码展示
图形渲染模块非常详细,涵盖了多种不同的渲染场景和对象,从僵尸的渲染到植物和子弹的渲染,以及初始界面的绘制。
#include "AllHeader.h" void drawWindow(struct zm* zms, struct zmFlag* zmFlags, struct zmConehead* zmConeheads, struct zmBuckethead* zmBucketheads, struct bullet* bullets, struct bulletsnow* bullet_snows, struct zhiwu(*map)[9], int* curY, int* curX, int* curZhiwu, int* sunshine, struct sunshineBalls* balls, IMAGE* imgsunshineBall, IMAGE* imgZMDead, IMAGE* imgZMEat, IMAGE* imgZMEatFlag, IMAGE* imgZMEatConehead, IMAGE* imgZMEatBuckethead, IMAGE* imgZMSnow, IMAGE* imgZMFlagSnow, IMAGE* imgZMConeheadSnow, IMAGE* imgZMBucketheadSnow, IMAGE* imgZMNormal, IMAGE* imgZMFlag, IMAGE* imgZMConehead, IMAGE* imgZMBuckethead, IMAGE* imgBg, IMAGE* imgBar, IMAGE* imgCards, IMAGE* (*imgZhiWu)[20], IMAGE* imgBulletBlast, IMAGE* imgBulletNormal, IMAGE* imgSnowBulletNormal, IMAGE* imgJalapenoExplode, IMAGE* imgZmStand, int ballMax, int zmCount, int countnormalMax, int countsnowMax) { BeginBatchDraw();//开始缓冲(*解决闪屏问题) viewScence(imgBg, imgZmStand);//片头巡场 /*渲染初始界面界面*/ drawUI(imgBg, imgBar); /*渲染植物卡牌*/ drawZhiWuCards(imgCards); /*渲染僵尸*/ drawZMNormal(zms, imgZMDead, imgZMEat, imgZMSnow, imgZMNormal, zmCount);//渲染普通僵尸 drawZMFlag(zmFlags, imgZMDead, imgZMEatFlag, imgZMFlagSnow, imgZMFlag, zmCount);//渲染旗子僵尸 drawZMConehead(zmConeheads, imgZMDead, imgZMEatConehead, imgZMConeheadSnow, imgZMConehead, zmCount);//渲染路障僵尸 drawZMBuckethead(zmBucketheads, imgZMDead, imgZMEatBuckethead, imgZMBucketheadSnow, imgZMBuckethead, zmCount);//渲染铁桶僵尸 /*渲染拖动过程中的植物*/ drawZhiWuMoving(imgZhiWu, curZhiwu, curY, curX); /*渲染放置的植物*/ drawZhiWuSetting(map, imgZhiWu); /*渲染豌豆子弹*/ drawSingleBullets(bullets, imgBulletBlast, imgBulletNormal, countnormalMax);//渲染普通豌豆子弹 drawDoubleBullets(bullets, imgBulletBlast, imgBulletNormal, countnormalMax);//渲染两连发(普通)豌豆子弹 drawThreeBullets(bullets, imgBulletBlast, imgBulletNormal, countnormalMax);//渲染三连发(普通)豌豆子弹 drawSnowBullets(bullet_snows, imgSnowBulletNormal, countsnowMax);//渲染寒冰豌豆子弹 /*渲染阳光值*/ drawSunshineValue(sunshine); /*渲染辣椒火焰*/ drawJalapenoExplode(map, zms, imgZhiWu, imgJalapenoExplode, zmCount); /*渲染阳光球(随机降落,向日葵生成,收集阳光)*/ drawSunshines(balls, imgsunshineBall, ballMax); EndBatchDraw();//结束双缓冲(*解决闪屏问题) } //渲染普通僵尸 void drawZMNormal(struct zm* zms, IMAGE* imgZMDead, IMAGE* imgZMEat, IMAGE* imgZMSnow, IMAGE* imgZMNormal, int zmCount) { for (int i = 0; i < zmCount; i++) { if (zms[i].used) { IMAGE* img = NULL; if (zms[i].dead) { img = imgZMDead; } else if (zms[i].eating) { img = imgZMEat; } else if (zms[i].slow) { img = imgZMSnow; } else { img = imgZMNormal; } img += zms[i].frameIndex; putimagePNG(zms[i].x, zms[i].y - img->getheight(), img); } } } ... //渲染普通豌豆射手豌豆子弹 void drawSingleBullets(struct bullet* bullets, IMAGE* imgBulletBlast, IMAGE* imgBulletNormal, int countnormalMax) { for (int i = 0; i < countnormalMax; i++) { if (bullets[i].used && bullets[i].single) { if (bullets[i].blast) { IMAGE* img = NULL; img = &imgBulletBlast[bullets[i].frameIndex]; putimagePNG(bullets[i].pCur.x, bullets[i].pCur.y, img); } else { putimagePNG(bullets[i].pCur.x, bullets[i].pCur.y, imgBulletNormal); } } } } ... //渲染阳光球(随机降落,向日葵生成,收集阳光) void drawSunshines(struct sunshineBalls* balls, IMAGE* imgsunshineBall, int ballMax) { for (int i = 0; i < ballMax; i++) { if (balls[i].used) { IMAGE* img = NULL; img = &imgsunshineBall[balls[i].frameIndex]; putimagePNG(balls[i].pCur.x, balls[i].pCur.y, img); } } } //渲染拖动过程中的植物 void drawZhiWuMoving(IMAGE* (*imgZhiWu)[20], int* curZhiwu, int* curY, int* curX) { if (*curZhiwu > 0) { IMAGE* img = NULL; img = imgZhiWu[*curZhiwu - 1][0]; putimagePNG(*curX - img->getwidth() / 2, *curY - img->getheight() / 2, img);//将光标调整到植物中间 } } //渲染放置的植物 void drawZhiWuSetting(struct zhiwu(*map)[9], IMAGE* (*imgZhiWu)[20]) { for (int i = 0; i < 5; i++) { for (int j = 0; j < 9; j++) { if (map[i][j].type > 0) { int zhiWuType = map[i][j].type - 1; int index = map[i][j].frameIndex; putimagePNG(map[i][j].x, map[i][j].y, imgZhiWu[zhiWuType][index]); } } } } //渲染阳光值 void drawSunshineValue(int* sunshine) { char scoreText[80]; sprintf_s(scoreText, sizeof(scoreText), "%d", *sunshine); outtextxy(278, 67, scoreText); } //渲染植物卡牌 void drawZhiWuCards(IMAGE* imgCards) { for (int i = 0; i < ZHI_WU_COUNT; i++) { int x = 336 + i * 65; int y = 6; putimage(x, y, &imgCards[i]); } } //渲染UI界面 void drawUI(IMAGE* imgBg, IMAGE* imgBar) { putimage(-112, 0, imgBg); putimagePNG(250, 0, imgBar); } //渲染辣椒火焰 void drawJalapenoExplode(struct zhiwu(*map)[9], struct zm* zms, IMAGE* (*imgZhiWu)[20], IMAGE* imgJalapenoExplode, int zmCount) { for (int i = 0; i < 5; i++) { for (int j = 0; j < 9; j++) { if (map[i][j].JalapenoExplode) { map[i][j].JalapenoframeIndex++; if (map[i][j].JalapenoframeIndex > 35) { map[i][j].JalapenoframeIndex = -1; map[i][j].JalapenoExplode = false; } int JalapenoY = map[i][j].y; IMAGE* img; img = imgJalapenoExplode; img += map[i][j].JalapenoframeIndex; putimagePNG(245 - 112, JalapenoY - 47, img); } } } } //片头巡场 void viewScence(IMAGE* imgBg, IMAGE* imgZmStand) { static int times = 0; if (times == 0) { times++; int WIN_WIDTH = 900; int WIN_HIGHTH = 600; int xMin = WIN_WIDTH - imgBg->getwidth(); vector2 point[9] = { {550,80},{530,160},{630,170},{530,200},{515,270}, {565,370},{605,340},{705,280},{690,340} }; int index[9]; for (int i = 0; i < 9; i++) { index[i] = rand() % 11; } int count = 0; for (int x = 0; x >= xMin; x -= 2) { BeginBatchDraw(); putimage(x, 0, imgBg); count++; for (int k = 0; k < 9; k++) { putimagePNG(point[k].x - xMin + x, point[k].y, &imgZmStand[index[k]]); if (count >= 10) { index[k] = (index[k] + 1) % 11; } } if (count >= 10) count = 0; EndBatchDraw(); Sleep(20); } //停留1s左右 for (int i = 0; i < 100; i++) { BeginBatchDraw(); putimage(xMin, 0, imgBg); for (int k = 0; k < 9; k++) { putimagePNG(point[k].x, point[k].y, &imgZmStand[index[k]]); index[k] = (index[k] + 1) % 11; } EndBatchDraw(); Sleep(30); } for (int x = xMin; x <= -112; x += 2) { BeginBatchDraw(); putimage(x, 0, imgBg); count++; for (int k = 0; k < 9; k++) { putimagePNG(point[k].x - xMin + x, point[k].y, &imgZmStand[index[k]]); if (count >= 10) { index[k] = (index[k] + 1) % 11; } if (count >= 10) count = 0; } EndBatchDraw(); Sleep(5); } } }
2.代码优化建议
-
参数传递优化:
drawWindow
函数的参数列表比较长,建议使用结构体将相关参数打包传递,以减少函数签名的复杂性,提升可读性。 -
代码重用:
drawZMNormal
、drawZMFlag
、drawZMConehead
、drawZMBuckethead
四个函数的逻辑结构非常相似,可以考虑将相同逻辑提取到一个通用的渲染函数中,并通过传入不同的参数来实现特定僵尸的渲染。 -
资源管理:图像资源的管理可以考虑使用统一的资源管理类或模块来加载和释放资源,这样可以更好地管理内存和提高代码的组织性。
-
绘制循环优化:在渲染过程中,
putimagePNG
和BeginBatchDraw
/EndBatchDraw
调用频繁,可以检查这些调用是否有优化空间,特别是在大规模渲染时,避免不必要的绘制操作。 -
动画效果处理:像
viewScence
这样的片头动画,建议将动画逻辑与游戏主循环解耦,单独管理,使其更加灵活且易于控制。
3.drawWindow函数优化实例
以下是
drawWindow
函数优化的例子,使用结构体来包装相关参数:struct GameResources { struct zm* zms; struct zmFlag* zmFlags; struct zmConehead* zmConeheads; struct zmBuckethead* zmBucketheads; struct bullet* bullets; struct bulletsnow* bullet_snows; struct zhiwu(*map)[9]; int* curY; int* curX; int* curZhiwu; int* sunshine; struct sunshineBalls* balls; IMAGE* imgsunshineBall; IMAGE* imgZMDead; IMAGE* imgZMEat; IMAGE* imgZMEatFlag; IMAGE* imgZMEatConehead; IMAGE* imgZMEatBuckethead; IMAGE* imgZMSnow; IMAGE* imgZMFlagSnow; IMAGE* imgZMConeheadSnow; IMAGE* imgZMBucketheadSnow; IMAGE* imgZMNormal; IMAGE* imgZMFlag; IMAGE* imgZMConehead; IMAGE* imgZMBuckethead; IMAGE* imgBg; IMAGE* imgBar; IMAGE* imgCards; IMAGE* (*imgZhiWu)[20]; IMAGE* imgBulletBlast; IMAGE* imgBulletNormal; IMAGE* imgSnowBulletNormal; IMAGE* imgJalapenoExplode; IMAGE* imgZmStand; int ballMax; int zmCount; int countnormalMax; int countsnowMax; }; void drawWindow(struct GameResources* resources) { BeginBatchDraw(); // 开始缓冲(*解决闪屏问题) viewScence(resources->imgBg, resources->imgZmStand); // 片头巡场 // 渲染初始界面界面 drawUI(resources->imgBg, resources->imgBar); // 渲染植物卡牌 drawZhiWuCards(resources->imgCards); // 渲染僵尸 drawZMNormal(resources->zms, resources->imgZMDead, resources->imgZMEat, resources->imgZMSnow, resources->imgZMNormal, resources->zmCount); drawZMFlag(resources->zmFlags, resources->imgZMDead, resources->imgZMEatFlag, resources->imgZMFlagSnow, resources->imgZMFlag, resources->zmCount); drawZMConehead(resources->zmConeheads, resources->imgZMDead, resources->imgZMEatConehead, resources->imgZMConeheadSnow, resources->imgZMConehead, resources->zmCount); drawZMBuckethead(resources->zmBucketheads, resources->imgZMDead, resources->imgZMEatBuckethead, resources->imgZMSnow, resources->imgZMBuckethead, resources->zmCount); // 渲染拖动过程中的植物 drawZhiWuMoving(resources->imgZhiWu, resources->curZhiwu, resources->curY, resources->curX); // 渲染放置的植物 drawZhiWuSetting(resources->map, resources->imgZhiWu); // 渲染豌豆子弹 drawSingleBullets(resources->bullets, resources->imgBulletBlast, resources->imgBulletNormal, resources->countnormalMax); drawDoubleBullets(resources->bullets, resources->imgBulletBlast, resources->imgBulletNormal, resources->countnormalMax); drawThreeBullets(resources->bullets, resources->imgBulletBlast, resources->imgBulletNormal, resources->countnormalMax); drawSnowBullets(resources->bullet_snows, resources->imgSnowBulletNormal, resources->countsnowMax); // 渲染阳光值 drawSunshineValue(resources->sunshine); // 渲染辣椒火焰 drawJalapenoExplode(resources->map, resources->zms, resources->imgZhiWu, resources->imgJalapenoExplode, resources->zmCount); // 渲染阳光球 drawSunshines(resources->balls, resources->imgsunshineBall, resources->ballMax); EndBatchDraw(); // 结束双缓冲(*解决闪屏问题) }
这个优化不仅简化了代码,还提高了可维护性。如果有更多具体问题或需要进一步的优化,随时可以讨论。
人机交互模块
-
UCInteraction函数:包含了用户交互的主要逻辑。首先调用
startUI
显示游戏开局界面,之后调用UserClick
来处理植物的选中和放置,以及阳光的收集。 -
UserClick函数:根据用户点击的消息类型处理不同的交互逻辑。
WM_LBUTTONDOWN
:用户点击时,判断是否点击了植物选择区域,选择合适的植物,并判断阳光是否足够。如果点击了阳光球,调用collectSunshine
函数进行阳光收集。WM_MOUSEMOVE
:实时更新鼠标位置。WM_LBUTTONUP
:当用户松开鼠标时,判断是否在有效区域内放置植物,并根据选择的植物类型减少阳光。
-
collectSunshine函数:判断用户是否点击了阳光球,如果点击了,则收集阳光,播放收集音效,并调整阳光球的状态和移动速度。
-
startUI函数:显示游戏的开局界面,并等待用户点击“开始游戏”按钮。当用户点击按钮后,退出开局界面,开始游戏。
1.代码展示
#include "AllHeader.h"
void UCInteraction(ExMessage* msg, int* sunshine, struct zhiwu(*map)[9], struct sunshineBalls* balls,
int* curZhiwu, int* curX, int* curY, IMAGE* imgsunshineBall, int ballMax)
{
//游戏开局UI交互(用户点击后开始游戏)
startUI();
//植物的选中与放置,阳光的收集
UserClick(msg, sunshine, map, balls, curZhiwu, curX, curY, imgsunshineBall, ballMax);
}
//实现植物的选中与放置
void UserClick(ExMessage* msg, int* sunshine, struct zhiwu(*map)[9], struct sunshineBalls* balls,
int* curZhiwu, int* curX, int* curY, IMAGE* imgsunshineBall, int ballMax)
{
static int status = 0;
if (peekmessage(msg))//判断有没有消息
{
if (msg->message == WM_LBUTTONDOWN)
{
if (msg->x > 336 && msg->x < 336 + 65 * ZHI_WU_COUNT && msg->y < 96)
{
int index = (msg->x - 336) / 65;
*curZhiwu = index + 1;
if (*curZhiwu == 1 && *sunshine >= 100)
{
status = 1;
}
else if (*curZhiwu == 2 && *sunshine >= 50)
{
status = 1;
}
else if (*curZhiwu == 3 && *sunshine >= 175)
{
status = 1;
}
else if (*curZhiwu == 4 && *sunshine >= 200)
{
status = 1;
}
else if (*curZhiwu == 5 && *sunshine >= 125)
{
status = 1;
}
else if (*curZhiwu == 6 && *sunshine >= 325)
{
status = 1;
}
}
else
{
collectSunshine(msg, balls, imgsunshineBall, ballMax);
}
}
else if (msg->message == WM_MOUSEMOVE)
{
*curX = msg->x;
*curY = msg->y;
}
else if (msg->message == WM_LBUTTONUP)
{
if (msg->x > 256 - 112 && msg->y > 88 && msg->y < 580)
{
int row = (msg->y - 88) / 102;//计算鼠标的行坐标
int col = (msg->x - (256 - 112)) / 81;//计算鼠标的列坐标
if (map[row][col].type == 0)
{
if (status == 1)
{
map[row][col].type = *curZhiwu;
map[row][col].frameIndex = 0;
if (*curZhiwu == 1)
{
PlaySound("res/plant1.wav", NULL, SND_FILENAME | SND_ASYNC);
(*sunshine) -= 100;
if (*sunshine < 0)
{
*sunshine = 0;
}
}
else if (*curZhiwu == 2)
{
PlaySound("res/plant1.wav", NULL, SND_FILENAME | SND_ASYNC);
(*sunshine) -= 50;
if (*sunshine < 0)
{
*sunshine = 0;
}
}
else if (*curZhiwu == 3)
{
PlaySound("res/plant1.wav", NULL, SND_FILENAME | SND_ASYNC);
(*sunshine) -= 175;
if (*sunshine < 0)
{
*sunshine = 0;
}
}
else if (*curZhiwu == 4)
{
PlaySound("res/plant1.wav", NULL, SND_FILENAME | SND_ASYNC);
(*sunshine) -= 200;
if (*sunshine < 0)
{
*sunshine = 0;
}
}
else if (*curZhiwu == 5)
{
PlaySound("res/jalapeno.wav", NULL, SND_FILENAME | SND_ASYNC);
(*sunshine) -= 125;
if (*sunshine < 0)
{
*sunshine = 0;
}
}
else if (*curZhiwu == 6)
{
PlaySound("res/plant1.wav", NULL, SND_FILENAME | SND_ASYNC);
(*sunshine) -= 325;
if (*sunshine < 0)
{
*sunshine = 0;
}
}
}
map[row][col].x = 256 - 112 + col * 81;
map[row][col].y = 77 + row * 102 + 10;
}
}
*curZhiwu = 0;
status = 0;
}
}
}
//实现阳光的收集
void collectSunshine(ExMessage* msg, struct sunshineBalls* balls, IMAGE* imgsunshineBall, int ballMax)
{
int w = imgsunshineBall[0].getwidth();
int h = imgsunshineBall[0].getheight();
for (int i = 0; i < ballMax; i++)
{
if (balls[i].used)
{
int x = balls[i].pCur.x;
int y = balls[i].pCur.y;
if (msg->x > x && msg->x<x + w && msg->y>y && msg->y < y + h)
{
balls[i].status = SUNSHINE_COLLECT;
PlaySound("res/sunshine.wav", NULL, SND_FILENAME | SND_ASYNC);
balls[i].p1 = balls[i].pCur;
balls[i].p4 = vector2(262, 0);
balls[i].t = 0;
float distance = dis(balls[i].p1 - balls[i].p4);
float off = 10;
balls[i].speed = 1.0 / (distance / off);
break;
}
}
}
}
//游戏开局UI交互(用户点击后开始游戏)
void startUI()
{
static int times = 0;
if (times == 0)
{
times++;
IMAGE imgBg, imgMenu1, imgMenu2;
loadimage(&imgBg, "res/menu.png");
loadimage(&imgMenu1, "res/menu1.png");
loadimage(&imgMenu2, "res/menu2.png");
int flag = 0;
while (1)
{
BeginBatchDraw();
putimage(0, 0, &imgBg);
putimagePNG(474, 75, flag ? &imgMenu2 : &imgMenu1);
ExMessage msg;
if (peekmessage(&msg))
{
if (msg.message == WM_LBUTTONDOWN
&& msg.x > 474 && msg.x < 474 + 300
&& msg.y>75 && msg.y < 75 + 140)
{
flag = 1;
}
else if (msg.message == WM_LBUTTONUP && flag)
{
EndBatchDraw();
break;
}
}
EndBatchDraw();
}
}
}
2.代码优化建议
-
代码复用:在
UserClick
函数中,根据植物类型选择不同的处理逻辑时,出现了多次重复代码。可以考虑使用一个数组或结构体来存储不同植物的阳光消耗和音效路径,简化代码逻辑。 -
消息处理优化:在
UserClick
函数中,你对WM_LBUTTONDOWN
和WM_LBUTTONUP
做了分别处理,但代码逻辑看上去存在一定的耦合性,或许可以通过更清晰的状态管理优化点击和放置逻辑。 -
常量定义:对于植物的阳光消耗值和其它硬编码的数值,可以使用宏定义或
const
变量来定义,以便于维护和修改。
4. 遇到的挑战与解决方案
性能优化
在开发过程中,由于游戏对象的数量逐渐增多,导致帧率下降。我通过减少不必要的图形重绘,优化了渲染循环,并使用双缓冲技术来提高游戏的整体性能。
图形和动画处理
处理动画帧时遇到了同步问题,例如僵尸和植物的动画不一致。通过将动画帧与游戏主循环的时间步长进行绑定,解决了这一问题。以及游戏渲染时出现的闪屏问题,通过双缓冲函数得以很好的解决。
逻辑错误
在早期开发中,植物攻击僵尸的逻辑存在缺陷,导致有些僵尸无法被消灭。通过详细的调试和日志记录,我发现问题出在碰撞检测的边界条件上,并成功修复了这个Bug。
5. 游戏测试与调优
测试过程
我使用了多种测试方法,包括手动测试和自动化单元测试,确保各个功能模块的正确性和稳定性。特别是在关卡设计上,通过反复调整难度,确保游戏具有挑战性但不至于过于困难。
用户反馈与迭代
在朋友中进行了一次小范围测试,收集了大量反馈。根据反馈,我对游戏的操作体验、关卡难度、植物和僵尸的平衡性等方面进行了多次迭代。
6. 最终效果展示
游戏截图
演示视频
效果演示
GitHub链接(源码获取)
https://github.com/Undefined-M/Plants-VS-Zombies/tree/master
8. 参考文献
[1] 程序员Rock.【程序员Rock】C语言项目:完整版植物大战僵尸!可能是B站最好的植物大战僵尸教程了!零基础手把手游戏开发[EB/0L].(2023-0207)
[2] 绿駬. EasyX基础入门——这一篇就够啦[EB/0L].(2023-08-28)
[3] beijing_txr. 贝塞尔曲线(Bezier Curve)原理、公式推导及matlab代码实现[EB/0L].(2021-10-22)
[4] 岁月失语唯石能言. C语言如何生成随机数以及设置随机数的范围。(超详细)[EB/0L].(2023-12-09)