行业知识
【C语言课设】经典植物大战僵尸丨完整开发教程+笔记+源码
前言
植物对抗僵尸是一款经典小游戏,初学者可以从零开始开发自己的版本,这将是一次令人期待的经历!它可以作为课程设计的一部分,也可以用来快速提升项目开发技能。
视频教程+素材资源
点击进入B站:(视频教程+素材资源)
说明:项目演示视频,在上面链接里的第一个视频,下面只展示项目截图..
项目准备
-
安装Visual Studio的任意版本(推荐VS2019社区版、VS2022社区版)
-
安装easyx图形库(官网下载地址)
-
领取项目素材(回复“植物大战僵尸”,即可领取)
创建项目
使用VS创建项目,使用空项目模板:
导入素材 :
在项目目录下,创建res文件夹,把解压后的素材拷贝到res目录下。
实现游戏初始场景
代码如下(需要逐行代码视频讲解,可回复“代码讲解“)。
#include #include #include "tools.h"#include #pragma comment(lib, "winmm.lib") #define WIN_WIDTH 900#define WIN_HEIGHT 600 enum { WAN_DOU, XIANG_RI_KUI, ZHI_WU_COUT };IMAGE imgBg;IMAGE imgBar;IMAGE imgCards[ZHI_WU_COUT];IMAGE* imgZhiWu[ZHI_WU_COUT][20];int curZhiWu;int curX, curY; //当前选中植物在移动过程中的坐标 struct zhiWu { int type; // >=1 0:没有植物 int frameIndex;};struct zhiWu map[3][9];int sunshine;int sunshineTable[ZHI_WU_COUT] = { 100, 50 }; void gameInit() { loadimage(&imgBg, "res/bg.jpg"); loadimage(&imgBar, "res/bar.png"); sunshine = 150; curZhiWu = 0; memset(imgZhiWu, 0, sizeof(imgZhiWu)); memset(map, 0, sizeof(map)); char name[64]; for (int i = 0; i getwidth() == 0) { delete imgZhiWu[i][j]; imgZhiWu[i][j] = NULL; } } } initgraph(WIN_WIDTH, WIN_HEIGHT, 1); // 设置字体: LOGFONT f; gettextstyle(&f); // 获取当前字体设置 f.lfHeight = 30; // 设置字体高度为 48 f.lfWidth = 15; strcpy(f.lfFaceName, "Segoe UI Black"); f.lfQuality = ANTIALIASED_QUALITY; // 设置输出效果为抗锯齿 settextstyle(&f); // 设置字体样式 setbkmode(TRANSPARENT); setcolor(BLACK); mciSendString("play res/bg.mp3 repeat", 0, 0, 0);} void updateWindow() { BeginBatchDraw(); putimage(0, 0, &imgBg); putimagePNG(250, 0, &imgBar); for (int i = 0; i 0) { // 绘制正在移动的植物 IMAGE* img = imgZhiWu[curZhiWu - 1][0]; putimagePNG(curX - img->getwidth() * 0.5, curY - img->getheight() * 0.5, img); } for (int i = 0; i 0) { int x = 260 + j * 81.6; // (msg.x - 260) / 81.6; int y = 180 + i * 103.6 + 14; // (msg.y - 210) / 103.6; int zhiWuIndex = map[i][j].type; int frameIndex = map[i][j].frameIndex; putimagePNG(x, y, imgZhiWu[zhiWuIndex - 1][frameIndex]); } } } char scoreText[8]; sprintf_s(scoreText, sizeof(scoreText), "%d", sunshine); outtextxy(282 - 10 + 4, 50 + 15 + 2, scoreText); EndBatchDraw();} void userClick() { ExMessage msg; static int status = 0; if (peekmessage(&msg)) { if (msg.message == WM_LBUTTONDOWN) { if (msg.x > 338 && msg.x 6 && msg.y 260 && msg.y 180 && msg.y = sunshineTable[curZhiWu - 1]) { sunshine -= sunshineTable[curZhiWu - 1]; int col = (msg.x - 260) / 81.6; int row = (msg.y - 210) / 103.6; printf("[%d,%d]\n", row, col); if (map[row][col].type == 0) { map[row][col].type = curZhiWu; map[row][col].frameIndex = 0; } } } status = 0; curZhiWu = 0; } }} void updateGame() { for (int i = 0; i 0) { map[i][j].frameIndex++; if (imgZhiWu[map[i][j].type - 1][map[i][j].frameIndex] == NULL) { map[i][j].frameIndex = 0; } } } }} int main(void) { gameInit(); int timer = 0; bool flag = true; while (1) { userClick(); timer += getDelay(); if (timer > 20) { timer = 0; flag = true; } if (flag) { flag = false; updateWindow(); updateGame(); } } return 0;}添加启动菜单
创建菜单界面,代码如下:
void startUI() { 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 75 && msg.y在main函数中调用菜单,代码如下:
int main(void) { gameInit(); startUI(); int timer = 0; bool flag = true; while (1) { userClick(); timer += getDelay(); if (timer > 20) { timer = 0; flag = true; } if (flag) { flag = false; updateWindow(); updateGame(); } } return 0;}生产阳光
熟悉植物大战僵尸的同学都知道,种植植物才能消灭僵尸,但是种植植物,需要先具备一定数量的阳光值。初始的阳光值很小。
有两种方式生成阳光:
第一种,随机降落少量的阳光;
第二种,通过种植向日葵,让向日葵自动生产阳光。我们先实现第一种方式。
定义一个结构体,来表示阳光球。因为阳光是以旋转的方式运动的,所以定义一个图片帧数组,通过循环播放图片帧来实现旋转效果。
IMAGE imgSunshineBall[29]; struct sunshineBall { int x, y; int frameIndex; bool used; int destY; int timer = 0;};struct sunshineBall balls[10];在gameInit函数中,初始化阳光帧数组。
memset(balls, 0, sizeof(balls)); for (int i = 0; i
创建阳光,代码如下。
void createSunshine() { int ballMax = sizeof(balls) / sizeof(balls[0]); static int frameCount = 0; static int fre = 400; frameCount++; if (frameCount >= fre) { fre = 200 + rand() % 200; frameCount = 0; int i; for (i = 0; i = ballMax) return; balls[i].used = true; balls[i].frameIndex = 0; balls[i].x = 260 + rand() % (905 - 260); balls[i].y = 60; balls[i].destY = 180 + (rand() % 4) * 90 + 20; balls[i].timer = 0; }}修改阳光的位置和帧序号,代码如下。
void updateSunshine() { int ballMax = sizeof(balls) / sizeof(balls[0]); for (int i = 0; i = balls[i].destY) { balls[i].timer++; if (balls[i].timer > 100) balls[i].used = false; } } }}在updateGame函数中调用以上两个函数 ,以创建阳光并更新阳光的状态。
createSunshine();updateSunshine();
在updateWindow函数中,渲染阳光。
for (int i = 0; i
收集阳光
当“阳光球”出现的时候,用户点击阳光球,就可以“收集”这个阳光,当前总的阳光值就会增加25点。在原版的植物大战僵尸游戏中,阳光球被收集后,会慢慢移动到顶部的“工具栏”的左侧。这个阳光球的“移动过程”,我们后续再实现。
定义一个全局变量,表示当前总的阳光值。
int sunshine;
在初始化gameInit中,设置一个初始值。
sunshine = 150;
创建收集阳光的函数,如下:
void collectSunshine(ExMessage* msg) { int count = sizeof(balls) / sizeof(balls[0]); int w = imgSunshineBall[0].getwidth(); int h = imgSunshineBall[0].getheight(); for (int i = 0; i x > x && msg->x y > y && msg->y在用户点击处理中,调用收集阳光的函数。
#include #pragma comment(lib, "winmm.lib") void userClick() { ExMessage msg; static int status = 0; if (peekmessage(&msg)) { if (msg.message == WM_LBUTTONDOWN) { if (msg.x > 338 && msg.x显示当前总的阳光值
在gameInit初始化中,设置字体。
LOGFONT f;gettextstyle(&f); // 获取当前字体设置f.lfHeight = 30; // 设置字体高度为 48f.lfWidth = 15;strcpy(f.lfFaceName, "Segoe UI Black");f.lfQuality = ANTIALIASED_QUALITY; // 设置输出效果为抗锯齿 settextstyle(&f); // 设置字体样式setbkmode(TRANSPARENT);setcolor(BLACK);
在updateWindow中绘制阳光值。
char scoreText[8];sprintf_s(scoreText, sizeof(scoreText), "%d", sunshine);outtextxy(276, 67, scoreText);
创建僵尸
创建僵尸的数据模型。这里一共创建了10个僵尸,这10个僵尸全部被消灭后,这个关卡就胜利了。
struct zm { int x, y; int frameIndex; bool used; int speed;};struct zm zms[10];IMAGE imgZM[22];僵尸数组,以及僵尸序列帧图片数组,在gameInit函数中进行初始化,如下。(注意:把僵尸的素材图片保存到src/zm目录下。)
memset(zms, 0, sizeof(zms));srand(time(NULL)); for (int i = 0; i
创建僵尸,代码如下:
void createZM() { static int zmFre = 500; static int count = 0; count++; if (count > zmFre) { zmFre = rand() % 200 + 300; count = 0; int i; int zmMax = sizeof(zms) / sizeof(zms[0]); for (i = 0; i更新僵尸的数据(僵尸的图片帧序号、僵尸的位置),代码如下:
void updateZM() { int zmMax = sizeof(zms) / sizeof(zms[0]); static int count1 = 0; count1++; if (count1 > 2) { count1 = 0; for (int i = 0; i x 4) { count2 = 0; for (int i = 0; i在updateGame函数中,创建僵尸并更新僵尸数据,如下:
createZM();updateZM();
创建绘制僵尸的接口, 如下:
void drawZM() { int zmCount = sizeof(zms) / sizeof(zms[0]); for (int i = 0; i getheight(); putimagePNG(x, y, img); } }}在updateWindow函数中,绘制僵尸,如下:
drawZM();
实现阳光球的飞跃
现在的实现效果是,阳光被点击后,阳光球直接消失了!而原版的植物大战僵尸中,阳光被点击后,阳光会自动飞向左上角的位置,飞到终点后,阳光值才增加25点。我们的实现方式是,阳光球每次飞跃4个点,直到飞到终点,如下图:
给阳光的结构体添加两个成员,表示飞跃过程中的偏移量:
struct sunshineBall { int x, y; int frameIndex; bool used; int destY; int timer; //添加以下两个成员 float xOff; float yOff;};在阳光被创建时,把变异量设置为0, 如下:
void createSunshine() { int ballMax = sizeof(balls) / sizeof(balls[0]); static int frameCount = 0; static int fre = 200; frameCount++; if (frameCount >= fre) { //...略 balls[i].xOff = 0; balls[i].yOff = 0; } }阳光被点击后,马上修改阳光球的xoff和yoff:
#include void collectSunshine(ExMessage* msg) { int count = sizeof(balls) / sizeof(balls[0]); int w = imgSunshineBall[0].getwidth(); int h = imgSunshineBall[0].getheight(); for (int i = 0; i x > x && msg->x y >y && msg->y在阳光飞跃过程中更新阳光的位置,如下:(注意是在飞跃过程中,不断计算偏移量,效果更好。)
void updateSunshine() { int ballMax = sizeof(balls) / sizeof(balls[0]); for (int i = 0; i删除原来被点击后,立即更新阳光值的代码。
//sunshine += 25;
修改渲染阳光的判断条件,如下:
for (int i = 0; i
此时已经能够实现阳光的飞跃了,但是飞跃动作太慢了,后期我们再优化。
发射豌豆
僵尸靠近时,已经种植的植物豌豆就会自动发射“子弹”,我们先为子弹定义数据类型,如下:
struct bullet { int x, y; int row; bool used; int speed;};struct bullet bullets[30];IMAGE imgBulletNormal;在gameInit函数中,初始化“豌豆子弹池”和子弹的图片,如下:
loadimage(&imgBulletNormal, "res/bullets/bullet_normal.png");memset(bullets, 0, sizeof(bullets));
在僵尸结构体中,添加成员row, 表示该僵尸所在的“行”,方便后续的判断。也可以不加,直接根据僵尸的y坐标来计算。
struct zm { int x, y; int frameIndex; bool used; int speed; int row; //0..2};在createZM函数中,创建僵尸的时候,设置row成员的值,如下:
......if (i
创建shoot函数,实现豌豆发射子弹,如下:
void shoot() { int zmCount = sizeof(zms) / sizeof(zms[0]); int directions[3] = { 0 }; int dangerX = WIN_WIDTH - imgZM[0].getwidth(); for (int i = 0; i 20) { count = 0; int k; int maxCount = sizeof(bullets) / sizeof(bullets[0]); for (k = 0; k getwidth()-10; bullets[k].y = zwY + 5; } } } } }}更新子弹的位置,如下:
void updateBullets() { int countMax = sizeof(bullets) / sizeof(bullets[0]); for (int i = 0; i WIN_WIDTH) { bullets[i].used = false; } } }}在updateGame函数中,发射子弹并更新子弹的位置,如下:
shoot();updateBullets();
在updateWindow中绘制子弹,如下:
int bulletMax = sizeof(bullets) / sizeof(bullets[0]);for (int i = 0; i
子弹和僵尸的碰撞
子弹碰到僵尸之后,子弹会“爆炸”,同时僵尸会“掉血”。我们先给僵尸添加血量成员。
struct zm { //略... int blood;};并在创建僵尸的时候,把血量初始化为100,如下:
//...zms[i].speed = 1;zms[i].blood = 100;
子弹在碰到僵尸之后才会爆炸,并显示爆炸图片:
所以,我们在子弹的结构体中添加两个成员,分别表示当前是否已经爆炸,以及爆炸的帧图片序号,如下:
struct bullet { //... bool blast; int frameIndex; };IMAGE imgBulletBlast[4];在gameInit函数中对子弹帧图片数组,进行初始化,如下:
loadimage(&imgBulletBlast[3], "res/bullets/bullet_blast.png");for (int i = 0; i
在发射子弹shoot函数中,对子弹的blast和帧序号frameIndex进行初始化,如下:
bullets[k].row = i;bullets[k].speed = 4;bullets[k].used = true; bullets[k].blast = false;bullets[k].blastTime = 0;
在更新子弹的updateBullets函数中,更新子弹爆炸的帧序号,如下:
bullets[i].x += bullets[i].speed;if (bullets[i].x > WIN_WIDTH) { bullets[i].used = false;} if (bullets[i].blast) { bullets[i].blastTime++; if (bullets[i].blastTime >= 4) { bullets[i].used = false; }}进行碰撞检测,检查子弹和僵尸是否发生碰撞,如下:
void collisionCheck() { int bCount = sizeof(bullets) / sizeof(bullets[0]); int zCount = sizeof(zms) / sizeof(zms[0]); for (int i = 0; i x1 && bullets[i].x在updateGame函数中,调用碰撞检测函数,如下:
collisionCheck();
渲染子弹的爆炸效果,如下:
int bulletMax = sizeof(bullets) / sizeof(bullets[0]);for (int i = 0; i getwidth() / 2; int y = bullets[i].y + 12 - img->getheight() / 2; putimagePNG(x, y, img); /*bullets[i].used = false;*/ } else { putimagePNG(bullets[i].x, bullets[i].y, &imgBulletNormal); } }}僵尸***亡
僵尸被豌豆子弹击中后,会“掉血”,血量掉光了,就直接KO了,同时变成一堆“黑沙”。
给僵尸结构体添加dead成员,表示是否已经***亡,另外添加一个图片帧数组,用来表示变成成黑沙的过程。
struct zm { ...... bool dead;};IMAGE imgZmDead[20];在gameInit中对这个图片帧数组进行初始化。
for (int i = 0; i
在碰撞检测中对僵尸的血量做检测,如果血量降到0,就设置为***亡状态。如下:
void collisionCheck() { int bCount = sizeof(bullets) / sizeof(bullets[0]); int zCount = sizeof(zms) / sizeof(zms[0]); for (int i = 0; i x1 && bullets[i].x = 20) { zms[i].used = false; } } else { zms[i].frameIndex = (zms[i].frameIndex + 1) % 22; } } }}绘制僵尸的黑沙状态,如下:
void drawZM() { int zmCount = sizeof(zms) / sizeof(zms[0]); for (int i = 0; i getheight(); putimagePNG(x, y, img); } }}后续的内容,点击这里看完整的实现
-
-
下一篇:服务器怎么做路由模式?