序言:
这个游戏的实现主要是纯c语言+easyx库(c++输出图片的工具),所以在编写该项目前要安装easyx库,我使用的vs2019专业版,若是社区版的vs2019,easyx可j检测到,直接安装;而专业版话需要手动安装easyx(点击手动安装教程);还需要注意的一点是在创建新项目时,代码后缀保持.cpp不变,因为c++才能支持easyx。
一、实现最开始游戏场景
按照序言要求,创建好.cpp的新项目,然后打开该项目所在文件夹导入素材,如图所示
#include<stdio.h> #include <graphics.h>//easyx图形库头文件,需要安装easyx图形库 int main(void) { gameInit();//初始化 updatewindow();//更新(渲染)界面 system("pause"); return 0; }
gameInit()用来初始化游戏,updatewindow()更新游戏界面。
#define WIN_WIDTH 900 #define WIN_HEIFHT 600 IMAGE imgBg;//表示背景图片变量 IMAGE imgbar;//表示工具栏图片变量
void gameInit() { //加载有背景的图片,把字符集修改成”多字符集“ loadimage(&imgBg, "res/Background_2.jpg"); loadimage(&imgbar, "res/bar5.png"); //创建游戏的图形窗口 initgraph(WIN_WIDTH, WIN_HEIFHT,1);//同时显示控制台窗口和图形窗口,不需要控制台去掉1即可 }
创建俩个全局图片变量用来存储背景、工具栏图片,在初始化函数中将文件图片加载(储存)到图片变量中,initgraph创建一个900*600大小的游戏窗口(因为没有渲染图片,所以打印出来是900*600大小的黑框。
注意:在使用loadimage函数会报错,处理办法将把字符集修改成”多字符集“)
此处是上面出现的easyx库中函数的用法链接(loadimage函数,initgraph函数)
下一步将代码与显示屏交互
void updatewindow() { putimage(0, 0, &imgBg);//渲染图片 }
此处便完成最开始游戏场景布置。
二.实现游戏顶部工具栏
像上面一样,先加载工具栏图片,在渲染工具栏图片,因为工具栏图片是叠加到背景图片上,所以需要去除工具栏图片黑色边框,所以渲染函数用putimagePNG(此函数来源于一个写好代码)
用之前将写好的代码复制在 该项目所在文件夹 ,然后点击项目名,选择添加——>现有项——>选中俩个代码文件。
用putimagePNG函数需要包含该文件头文件
#include"tools.h"
在 gameInit()初始化函数中加载工具栏图片
loadimage(&imgbar, "res/bar5.png");
在updatewindow()函数渲染工具栏图片
putimagePNG(250, 0, &imgbar);//渲染工具栏(透明背景)图片
三.实现游戏顶部工具栏的植物卡牌
有多个卡牌,我们就用图片数组储存卡牌图片,用枚举来表示卡牌种类
enum {PEA,SUNFLOWER,PLANT_COUNT};//枚举所有卡牌种类 IMAGE imgcards[PLANT_COUNT];//表示植物卡牌图片数组变量
发现PLANT_COUNT表示2,也就是卡牌种类个数,我们在枚举中插入别的卡牌类型,那PLANT_COUNT依然表示卡牌种类个数,这是一个小技巧,后面需要添加其他卡牌就容易了
如法炮制,在初始化函数中加载卡牌图片,在更新界面函数中渲染图片
在gameInit()函数中
//初始化(加载)植物卡牌图片 char plantname[64] = { 0 }; for (int i = 0; i < PLANT_COUNT; i++) { //生成植物卡牌文件名 sprintf_s(plantname, sizeof(plantname), "res/Cards/card_%d.png", i + 1);//将植物卡牌文件名存储到字符数组 loadimage(&imgcards[i], plantname); }
在updatewindow()函数中
//渲染卡牌图片 for (int i = 0; i < PLANT_COUNT; i++) { int x = 338 + i * 65;//卡牌左上角坐标 int y = 6; putimage(x, y, &imgcards[i]); }
四、实现植物的种植
这个操作属于用户操作,需要在主函数中增加用户点击函数,用来处理接受的鼠标信息,每一个游戏都是死循环,赢或者输都会跳出循环,进入下一个关卡或者结束游戏
int main(void) { gameInit(); while (1) { userclick(); updatewindow();//更新(渲染)界面 } system("pause"); return 0; }
这样处理后,界面会出现闪烁,因为在死循环中会不停打印变换界面,这里我们在更新界面函数中用一个双缓冲,它的作用是将所有图片准备好在一片内存中,在一起打印出来,就不会闪烁了
void updatewindow() { BeginBatchDraw();//双缓冲(先打印在一片内存内,然后一起打印出来) putimage(0, 0, &imgBg);//渲染图片 putimagePNG(250, 0, &imgbar);//渲染(透明背景)图片 //渲染卡牌图片 for (int i = 0; i < PLANT_COUNT; i++) { int x = 338 + i * 65;//卡牌左上角坐标 int y = 6; putimage(x, y, &imgcards[i]); } EndBatchDraw();//结束双缓冲 }
我们先判断界面有没有鼠标消息,用peekmessage这个函数(函数是bool类型,有消息则返回true,没有消息返回为false,第一个参数是ExMessage类型的结构体指针,第二个参数是获取消息的范围,因为我们获取的鼠标消息,所以写EX_MOUSE)
对于ExMessage类型结构体里面有message成员,该成员值如下
介绍完这个函数和结构体,我们就设计植物的种植。在这里我们和原版一样实现的效果是我们点击卡牌图片一下,然后移动鼠标,植物跟着鼠标移动,然后在草坪上点击,即种植完成。
首先接受鼠标信息
(1)因为鼠标点击一下瞬间,既有左键按下,又有左键抬起,那么在卡牌区域检测到左键抬起,说明是选中该植物
(2)因为需要有植物图片跟随鼠标移动,那么,设置一个静态变量status=0,选中植物卡牌即便为1,鼠标在选中植物卡牌后且左键抬起状态的鼠标坐标位置即植物图片位置,定义一个全局变量接收鼠标坐标,然后在该移动坐标位置渲染植物图片,就有跟随效果了
(3)移动之后要种植,创建一个植物结构体和4行8列的结构体数组(为了方便对应草坪4行8列区域种植植物情况),检测到鼠标左键按下状态在草坪区域,根据鼠标坐标确定所在草坪具体的哪行哪列,分析该位置有无植物,当没有植物即种下鼠标之前选中的植物curplant,确定种植植物的位置,方便后面渲染种植植物
int curx, cury;//当前选中植物在移动过程中的位置 int curplant=0;//0表示没有选中,1表示选中第一种植物,2表示选中第二种植物 struct plant { int type;//0表示没有植物,1表示选中第一种植物,2表示选中第二种植物 int frameIndex;//序列帧的序号 int timer;//喷射阳光的计时器 int x, y;//植物坐标 }; struct plant map[4][8];//4行8列的植物数组,每个元素是一个植物结构体 void userclick() { ExMessage msg;//这个结构体变量用于保存鼠标消息 static int status= 0; if (peekmessage(&msg,EX_MOUSE))//判断鼠标消息,有返回真,无返回假 { if (msg.message == WM_LBUTTONUP)//鼠标左键弹起 { if (msg.x > 338 && msg.x < 338 + 65 * PLANT_COUNT && msg.y < 96) { int index = (msg.x - 338) / 65;//植物卡片,0.代表第一张,1.代表第二张 //printf("%d ", index); status = 1; curplant = index + 1;//植物,1.代表豌豆,2.代表向日葵 } } else if (msg.message == WM_MOUSEMOVE && status == 1)//鼠标移动 { curx = msg.x; cury = msg.y; } else if (msg.message == WM_LBUTTONDOWN)//鼠标左键按下 { if (msg.x > 261 && msg.y > 167 && msg.y < 531)//鼠标落在草坪区域 { int row = (msg.y - 167) / 91; int col = (msg.x - 261) / 81; if (map[row][col].type == 0) { map[row][col].type = curplant; map[row][col].frameIndex = 0; //int x = 261 + 81 * j; //int y = 167 + 91 * i + 14; map[row][col].x = 261 + 81 * col; map[row][col].y = 167 + 91 * row + 14; map[row][col].eated=false; } //printf("%d,%d\n", row, col); } curplant = 0;//在任意区域点击即可取消选则 status = 0; } }
初始化植物图片
然后将植物图片加载到植物图片数组里,豌豆有17张图片,向日葵有20张图片,我们设置一个2行10列图片指针数组来加载植物图片,将植物图片加载到植物数组中去
注意的是在加载过程中需要判断plantname中的文件地址是否存在,存在才加载,不存在则跳过该行植物文件。
IMAGE* imgplant[PLANT_COUNT][20];//表示存放植物图片地址的指针数组 bool fileExist(const char* name)//打开成功返回真,打开失败返回假 { FILE* fp = fopen(name, "r");//若错误,则点击代码文件名选中属性,关掉c/c++中的SDL检查 if (fp == NULL) { return false; } else { fclose(fp); return true; } } void gameInit() { memset(imgplant, 0, sizeof(imgplant));//将指针数组内容置为NULL,目的防止野指针 memset(map, 0, sizeof(map));//将植物数组初始化,使里面的成员都为0.包括type也为0(表示没有植物) for (int i = 0; i < PLANT_COUNT; i++) { for (int j = 0; j < 20; j++) { sprintf_s(plantname, sizeof(plantname), "res/zhiwu/%d/%d.png", i ,j+1);//将植物文件名存储到字符数组 //判断文件是否存在 if (fileExist(plantname)) { imgplant[i][j] = new IMAGE; //C++中分配内存 loadimage(imgplant[i][j], plantname); } else { break; } } } }
接下来渲染拖动过程的植物和渲染种下之后的植物
(1)在渲染拖动过程中植物图片,为了使鼠标箭头位于植物图片中央,需要对渲染位置做一点修改
img->getwidth() 表示获取该图片的宽度
(2)渲染草坪上的植物时,要注意的点的时,植物是随时动的,我们渲染的是植物第一帧图片.所以我们要改变frameIndex的值,需要一个改变数据的函数
void updatewindow() { BeginBatchDraw();//双缓冲(先打印在一片内存内,然后一起打印出来) //渲染拖动过程中的植物 if (curplant==PEA+1) { IMAGE* img = imgplant[curplant - 1][0];//表示存放豌豆植物图片地址的指针 putimagePNG(curx - img->getwidth() / 2, cury - img->getwidth() / 2, img); } else if (curplant == SUNFLOWER+1 ) { IMAGE* img = imgplant[curplant - 1][0];//表示存放向日葵植物图片地址的指针 putimagePNG(curx - img->getwidth() / 2, cury - img->getwidth() / 2, img); } //渲染草坪上植物 for (int i = 0; i < 4; i++) { for (int j = 0; j < 8; j++) if (map[i][j].type > 0) { //int x = 261 + 81 * j; //int y = 167 + 91 * i + 14; //putimagePNG(x, y, imgplant[map[i][j].type - 1][map[i][j].frameIndex]); putimagePNG(map[i][j].x,map[i][j]. y, imgplant[map[i][j].type - 1][map[i][j].frameIndex]); } }
接下来我们需要改变frameIndex图片帧的数据,创建一个更改数据的函数
int main(void) { gameInit(); while (1) { userclick(); updatewindow();//更新(渲染)界面 updategame();//改变游戏数据 } system("pause"); return 0; }
void updategame()//修改游戏数据 { for (int i = 0; i < 4; i++) { for (int j = 0; j < 8; j++) { if (map[i][j].type > 0) { map[i][j].frameIndex++; if (imgplant[map[i][j].type - 1][map[i][j].frameIndex] == NULL) { map[i][j].frameIndex = 0; } } } } }
注:因为植物指针数组不是每个都有文件,所以需要判断,若该数组内容为NULL,则从头再开始帧序号
解决植物摇摆状态变换过快
运行文件我们发现植物运动很快,因为在main()函数中没有加延时,会循环的非常快,不断改变帧序号,所以我们需要优化循环,我们就想到Sleep,发现这样后植物跟不上鼠标移动,会有迟滞感,
我们选择计时来解决这个问题
这里有一个写在我们之前tool.cpp函数中延时程序,从第二次调用以后,返回值都是当前调用和上一次调用的时间差
当时间将大于50ms,我们才跟新画面和数据,但是处理用户操作时随时的,而用Sleep时,中间停顿的时间是处理不了用户点击的
bool flag = true; while (1) { userclick(); timer = timer + getDelay(); if (timer > 50) { flag = true; timer = 0; } if (flag) { flag = false; updatewindow();//更新(渲染)界面 updategame();//改变游戏数据 //Sleep(10); } }
当然上面觉得比较啰嗦的话,也可以这样
while (1) { userclick(); timer = timer + getDelay(); if (timer > 50) { timer = 0; updatewindow();//更新(渲染)界面 updategame();//改变游戏数据 //Sleep(10); } }
后面中小型游戏都可以这样用,后面也会经常用到。(该技巧再下面成为控制频率技巧)
如果还觉比较快,当然也可以单独对种植植物里面,植物图片帧进行频率控制
后面大家再调试时觉的阳光球或者僵尸等移动过快,都可以再更新阳光球数据或者更新僵尸数据等函数中进行控制频率处理