五、创建游戏启动菜单界面
在主函数中,启动菜单放在开始
我们开始创建这个函数,开始加载菜单界面和俩个明暗菜单按钮,因为这个界面是要等待用户点击,所以是一个循环,像用户点击函数一样,我们接受鼠标信息,若鼠标左键按下状态在开始按钮区域中,则渲染高亮开始按钮图片,若鼠标左键弹起状态在开始按钮区域中,则结束循环。
void startUI() { IMAGE imgBj, imgMenun1, imgMenun2; loadimage(&imgBj, "res/menu.png"); loadimage(&imgMenun1, "res/menu1.png"); loadimage(&imgMenun2, "res/menu2.png"); int flag = 0; while (1) { BeginBatchDraw(); putimage(0, 0, &imgBj); putimagePNG(475, 75, flag ? &imgMenun2 : &imgMenun1); ExMessage msg; if (peekmessage(&msg, EX_MOUSE))//判断鼠标消息,有返回真,无返回假 { 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 && msg.x > 474 && msg.x < 474 + 300 && msg.y>75 && msg.y < 75 + 140) { //return; EndBatchDraw();//点击到开始按钮,在跳出循环之前结束缓冲 break; } } EndBatchDraw();//没有点击开始按钮,需要结束缓冲 } }
六、创建阳光球
创建阳光球结构体和一个结构体数组,表示10个阳光球,阳光球分为俩个部分,一种是自由落体,另一种是向日葵产生的抛物线运动的阳光球,这里引入一个工具文件vector2.cpp,将该文件添加到现有项里面去。
这个可以理解成c语言的结构体类型,红色框代表结构体成员,可以运用这个结构体进行直线,抛物线运动(贝塞尔曲线)
贝塞尔曲线函数:通过四个点和t(起始位置t=0,终点位置t=1)确定t时刻在曲线位置坐标
(斜)直线运动:通过俩个点(p1和p4)以及t,确定直线运动的t时刻位置坐标,t=t+speed,pcur=p1+t*(p4-p1)
enum{SUNSHINE_DOWN,SUNSHINE_GROUND,SUNSHINE_COLLECT,SUNSHINE_PRODUCT}; //0.阳光下降 1.阳光着陆 2.阳光收集 3.阳光生产 struct sunshineball { int frameIndex;//当前显示图片帧的序号 //int destY;//飘落的目标位置的y坐标 bool use;//是否在使用(0代表没有被使用) int timer;//计时器,目的使阳光停留几秒 float t;//贝塞尔曲线时间点0-1 vector2 p1, p2, p3, p4;//贝塞尔曲线的四个点 vector2 pcur;//当前时刻阳光球位置 float speed; int status;//阳光球四个状态 }; struct sunshineball balls[10]; IMAGE imgSunshineball[29]; int sunshine;//阳光值
然后在初始化函数中初始化(加载)阳光球
memset(balls, 0, sizeof(balls)); for (int i = 0; i < 29; i++) { sprintf_s(plantname, sizeof(plantname), "res/sunshine/%d.png", i + 1); loadimage(&imgSunshineball[i], plantname); }
因为阳光球在下落过程中,数据是不断变化的,我们在更新数据函数updategame()写创建阳光球函数和修改阳光球函数
void updategame()//修改游戏数据 { creatsunshine();//创建阳光 updatesunshine();//修改阳光状态 }
创建阳光球函数怎么写呢?
阳光球分为俩个部分,一种是自由落体阳光球,另一种是向日葵产生的抛物线运动的阳光球。
6.1 创建自由落体阳光球
先遍历10个阳光球,找到一个没有使用过的,对该阳光球初始化,阳光球状态初始化下降状态,下降只要设置起始点和落地点即可,起始点的横坐标在260-800位置随机出现(随机函数可以看这篇博文随机数的生成),降落的纵坐标在4行草坪高度随机出现,dis()是计算俩个坐标之间的距离(可以看成俩个向量相减的模长),阳光球从起始位置时t=0,到目标位置t=1,所以可以以此计算阳光球的运动快慢,若speed=1.0/2,t=t+speed,则需要俩次阳光才能到目的地。
void creatsunshine() { //创建自由落体阳光 static int count = 0; static int fre = 200; count++; if (count >= fre)//每400帧获取一个阳光值 { fre = rand() % 200;//200-399的随机值 count = 0; //从阳光池中取一个可以使用的 int ballmax = sizeof(balls) / sizeof(balls[0]); int i = 0; for (i = 0; i < ballmax && balls[i].use; i++);//找到未被使用的 if (i >= ballmax)//未找到 { return; } balls[i].use = true;//使用 balls[i].frameIndex = 0;//第0个序列图片 //balls[i].x = rand() % (800 - 260) + 260; //balls[i].destY = (rand() % 5) * 90 + 170;//目标位置 //balls[i].y = 60; balls[i].timer = 0; //balls[i].xoff = 0; //balls[i].yoff = 0; balls[i].status = SUNSHINE_DOWN; balls[i].t = 0; balls[i].p1 = vector2(rand() % (800 - 260) + 260, 60);//把vector2看成一个结构体 balls[i].p4 = vector2(balls[i].p1.x, (rand() % 5) * 90 + 170); int off = 2; float distance = dis(balls[i].p4.y - balls[i].p1.y); balls[i].speed = 1.0 / (distance / off);//下落速度 } //创建向日葵生产阳光 }
6.2 创建向日葵生产阳光
遍历4行8列的植物数组,若是向日葵,则找一个一个没有用过的太阳球,对太阳球结构体修改成员状态,状态改为生产阳光,起始点为向日葵坐标,终点坐标是向日葵左右100-150作用,中间俩个坐标自己调节,
//向日葵生产阳光 int ballmax = sizeof(balls) / sizeof(balls[0]); for (int i = 0; i < 4; i++) { for (int j = 0; j < 8; j++) { if (map[i][j].type == SUNFLOWER + 1) { map[i][j].timer++;//计时器 if (map[i][j].timer > 100) { map[i][j].timer = 0; int k; for (k = 0; k < ballmax && balls[k].use; k++); if (k >= ballmax) { return; } balls[k].use = 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+imgplant[SUNFLOWER][0]->getheight()- imgSunshineball[0].getheight()+10);//喷射下落位置 balls[k].p2 = vector2(balls[k].p1.x + 0.3 * w, balls[k].p1.y - 50); balls[k].p3 = vector2(balls[k].p1.x + 0.7 * w, balls[k].p1.y - 50); balls[k].status = SUNSHINE_PRODUCT; balls[k].speed = 0.05; balls[k].t = 0; } } } }
七、收集阳光
收集阳光属于用户操作,收集阳光函数应在用户操作函数中,若鼠标弹起状态位置不是卡牌区域,判断是否在阳光球位置,若是在阳光球位置,改变阳光球的状态(阳光球有四种状态)为收集状态,阳光球的起始位置为点击位置,终点位置为工具栏的阳光图片。
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) { } else { collectsunshine(&msg);//收集阳光 } } else if (msg.message == WM_MOUSEMOVE && status == 1)//鼠标移动 { } else if (msg.message == WM_LBUTTONDOWN)//鼠标左键按下 { curplant = 0; status = 0; } } }
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 < count; i++)//若鼠标左键弹起的位置在阳光里面,则阳光消失,且阳光值加25 { if (balls[i].use) { //int x = balls[i].x; //int y = balls[i].y; int x = balls[i].pcur.x; int y = balls[i].pcur.y; if (msg->x > (x + 8) && msg->x<(x + w - 8) && msg->y >(y + 8) && msg->y < (y + h - 8))//鼠标左键弹起的位置在阳光里面 { //balls[i].use = false; balls[i].status = SUNSHINE_COLLECT; //sunshine += 25; //mciSendString("play res/sunshine.mp3", 0, 0, 0);//播放音效(这个音乐需要上一个播放完才能播放下一个) PlaySound("res/sunshine.wav", NULL, SND_FILENAME |SND_ASYNC); //设置阳光偏移量 //float destY = 0; //float destX = 262; //float angle = atan((balls[i].y - destY) / (balls[i].x - destX)); //balls[i].xoff = 4 * cos(angle); //balls[i].yoff = 4 * sin(angle); balls[i].p1 = balls[i].pcur;//直线运动不用p2,p3. balls[i].p4 = vector2(262,0); balls[i].t = 0; float distance = dis(balls[i].p1 - balls[i].p4); float off = 8; balls[i].speed = 1.0 / (distance / off);//实际distance/off帧回到原点 break;//收集该阳光球后,判断下次点击 } } } }
八、更新阳光球状态
前面我们已经有了四种阳光球状态,然后对这四种状态阳光球进行数据修改,
(1)如果正在使用阳光球处于下降状态,t=t+speed,pcur=p1+t*(p4-p1),通过t就可以得到下降状态的位置,此时阳光球状态变为落地,计时器归0;
(2)如果正在使用阳光球处于落地状态,使太阳球停顿一会再消失;
(3)如果正在使用阳光球处于收集状态,此时运动是斜直线,用疑似贝塞尔曲线确定阳光位置,t=1时,阳光球消失,计数器加25
(4)如果正在使用阳光球处于生产状态,此时运动状态是曲线,用贝塞尔曲线计算位置,t=1,到终点位置,变落地状态
void updatesunshine() { int ballmax = sizeof(balls) / sizeof(balls[0]); for (int i = 0; i < ballmax; i++) { if (balls[i].use)//被使用状态 { balls[i].frameIndex = (balls[i].frameIndex + 1) % 29; if (balls[i].status == SUNSHINE_DOWN) { struct sunshineball* 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 > 25) { balls[i].use = false; balls[i].timer = 0; } } else if (balls[i].status == SUNSHINE_COLLECT)//此时运动是斜直线 { struct sunshineball* sun = &balls[i]; sun->t += sun->speed; sun->pcur = sun->p1 + sun->t * (sun->p4 - sun->p1);//向量相减 if (sun->t >= 1) { sun->use = false; sun->timer = 0; sunshine += 25; } } else if (balls[i].status == SUNSHINE_PRODUCT) { struct sunshineball* 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; } } } } }
上面我们设计,当阳光求回到工具栏中,阳光值增加25,我们的目的想使阳光值显示再工具栏上。
有俩方法,一种用图片呈现,另一种直接输出字体
这里我们输出文本,先在初始化函数中设置字体
//设置字体 LOGFONT f;//当前字体结构体变量 gettextstyle(&f);//获取当前字体 f.lfHeight = 30; f.lfWidth = 15; strcpy(f.lfFaceName, "Segoe UI Black");//修改字体类型 f.lfQuality = ANTIALIASED_QUALITY;//抗锯齿效果 settextstyle(&f);//设置字体文本 setbkmode(TRANSPARENT);//设置字体背景透明 setcolor(BLACK);
然后渲染阳光值数字,要把十进制数字转化为字符串,再去渲染
char scoretext[8]; sprintf_s(scoretext, sizeof(scoretext), "%d", sunshine);//将阳光值存储到字符数组 outtextxy(276, 67, scoretext);//输出字符数组里面的字符
接下来我们将阳光球渲染出来就可以看到效果了
//渲染阳光球 int ballmax = sizeof(balls) / sizeof(balls[0]); for(int i=0;i<ballmax;i++) { //if (balls[i].use||balls[i].xoff) if(balls[i].use) { IMAGE* img = &imgSunshineball[balls[i].frameIndex]; //putimagePNG(balls[i].x, balls[i].y, img); putimagePNG(balls[i].pcur.x, balls[i].pcur.y, img); } }
后续精彩内容,请点击 植物大战僵尸(下) 进行跳转
完整的代码及素材链接: