植物大战僵尸,是一个非常经典的小游戏,初学者从零开始,开发一个自己的植物大战僵尸,还是非常值得期待的!可以作为自己的课设,也可以用来快速提升自己的项目开发能力。
项目效果(详细视频教程点这里)
说明:因为完整动图提交后提示违规,所以这里仅截图示意。如果需要演示视频,在评论中回复即可。
![](https://img-blog.csdnimg.cn/img_convert/9694a4fa90754ac39b6a1de8be38a1da.png)
项目准备
安装Visual Studio的任意版本(推荐VS2019社区版、VS2022社区版)
安装easyx图形库(官网下载地址)
领取项目素材(回复“植物大战僵尸”,即可领取)
创建项目
使用VS创建项目,使用空项目模板:
![](https://img-blog.csdnimg.cn/img_convert/b5d15bc5a5d24890ab46a8878299198b.png)
导入素材:在项目目录下,创建res文件夹,把解压后的素材拷贝到res目录下。
![](https://img-blog.csdnimg.cn/img_convert/aa5c4084a4244b92a7912b92170b74b0.png)
![](https://img-blog.csdnimg.cn/img_convert/7914ee91b80445bab3ea6ba4d69a16c9.png)
![](https://img-blog.csdnimg.cn/img_convert/c910147eae74448eb0df2f50c22c0456.png)
实现游戏初始场景
代码如下(需要逐行代码视频讲解,可回复“代码讲解“)。
#include <stdio.h>
#include <graphics.h>
#include "tools.h"
#include <mmsystem.h>
#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 < ZHI_WU_COUT; i++) {
sprintf_s(name, sizeof(name), "res/Cards/card_%d.png", i + 1);
loadimage(&imgCards[i], name);
for (int j = 0; j < 20; j++) {
sprintf_s(name, sizeof(name), "res/zhiwu/%d/%d.png", i, j + 1);
imgZhiWu[i][j] = new IMAGE;
loadimage(imgZhiWu[i][j], name);
if (imgZhiWu[i][j]->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 < ZHI_WU_COUT; i++) {
int x = 338 + i * 64;
int y = 6;
putimage(x, y, &imgCards[i]);
}
if (curZhiWu > 0) { // 绘制正在移动的植物
IMAGE* img = imgZhiWu[curZhiWu - 1][0];
putimagePNG(curX - img->getwidth() * 0.5, curY - img->getheight() * 0.5, img);
}
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 9; j++) {
if (map[i][j].type > 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 < 338 + 64 * ZHI_WU_COUT && msg.y>6 && msg.y < 96) {
int index = (msg.x - 338) / 64;
printf("%d\n", index);
status = 1;
curZhiWu = index + 1; // 1, 2
curX = msg.x;
curY = msg.y;
}
}
else if (msg.message == WM_MOUSEMOVE && status == 1) {
curX = msg.x;
curY = msg.y;
}
else if (msg.message == WM_LBUTTONUP && status == 1) {
printf("up\n");
if (msg.x > 260 && msg.y < 995 && msg.y > 180 && msg.y < 491) {
if (sunshine >= 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 < 3; i++) {
for (int j = 0; j < 9; j++) {
if (map[i][j].type > 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 < 474 + 300 && msg.y > 75 && msg.y < 75 + 140) {
flag = 1;
EndBatchDraw();
}
else if (msg.message == WM_LBUTTONUP && flag) {
return;
}
}
EndBatchDraw();
}
}
在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 < 29; i++) {
sprintf_s(name, sizeof(name), "res/sunshine/%d.png", i + 1);
loadimage(&imgSunshineBall[i], name);
}
创建阳光,代码如下。
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 && balls[i].used; i++);
if (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 < ballMax; i++) {
if (balls[i].used) {
balls[i].frameIndex = (balls[i].frameIndex + 1) % 29;
if(balls[i].timer == 0) balls[i].y += 2;
if (balls[i].y >= balls[i].destY) {
balls[i].timer++;
if (balls[i].timer > 100) balls[i].used = false;
}
}
}
}
在updateGame函数中调用以上两个函数 ,以创建阳光并更新阳光的状态。
createSunshine();
updateSunshine();
在updateWindow函数中,渲染阳光。
for (int i = 0; i < 10; i++) {
if (balls[i].used) {
putimagePNG(balls[i].x, balls[i].y, &imgSunshineBall[balls[i].frameIndex]);
}
}