2022.3 C++植物大战僵尸项目总结

2022.3.15:

目录

1.对于上一次飞机大战项目的复盘

2.此次项目项目设计

3.实现思路

4.项目架构详述

5.学到的东西

6.未来展望

1.对于上一次项目飞机大战的复盘

上次做完项目之后其实没有仔细去思考,也没有仔细看当时的答辩和总结的视频。等到做这次项目的时候才知道一次项目的经验真的可以融合到多个地方,所以我又重新对上次项目做了一个总结,希望能让它变成一个对我有很大价值的事情。

①个人总结/学到的东西:

Ⅰ.关于新函数的学习方法:函数的本质即是接受参数,实现一些行为,再返回值。但返回有时不是仅仅通过return来实现的。从一个特别的例子来看:学习Easyx中RGBtoHSL的函数时,我观察到这个函数功能上是实现对于某种颜色模型的获取,然后返回该模型的对应值,但函数类型却是void--由此:既然不能通过return来返回,则此时应当思考是否传值为指针或引用来变相实现一种返回的操作。然后顺着这个思路可以发现RGBtoHSL函数的传参确实是三个float型的指针。

Ⅱ.关于文件的相对路径:应当在.sln文件同名的文件夹下建立res文件夹,在加载资源时路径写作./res/xxxx的形式,这样统一代码比较好整合。而且路径应当在书写架构时通过宏定义来事先写好,比如#define IMGShooter L"./res/shooter.img",这样后续做界面的时候也能方便很多。

Ⅲ.关于游戏设计:首先,从产品角度,游戏的核心是带给用户视听的体验,所以做一款好的游戏的界面、音乐、皮肤功能都很重要。比如,多在菜单上下功夫,让界面的色彩对比更加明显鲜艳。即使逻辑方面稍逊,也能够通过界面来补。再从用户的角度来说,及时的反馈是非常必要的——及时给予用户操作上的反馈,比如修改各种设置时去给用户反映到数值上,以此让用户的操作落到实处。同时及时地给予刺激,比如提示用户距离下一步还有多少分等等的,来通过不断地反馈去吸引用户继续进行游戏。或者是像商城系统之类的项目,可以有根据用户的常选商品作商品推荐的模块,可以增加与用户的交互性。

Ⅳ.关于头文件:首先,头文件应当杜绝使用中文名。其次,头文件应当写清注释。

Ⅶ.关于学习:核心思想:最有价值的开发者应当是专精某个方面,如架构,同时也熟悉其他方面,如界面。所以首先,学习时要思考学习每个技术,每个工具的方法,要有精确的方向性。简单的对于知识的堆积是没有任何意义的,要去提炼知识点,多去问为什么。我们该记住的不单单是语法公式设计模式,也不应该靠背去学习编程。因此,我们应当培养的,是解决问题与更好的解决问题的能力。所以未来学习时,应当常问自己这些问题:这个知识点存在的意义是什么?它有什么用?它怎么用?为什么是这样?还能怎么样?对于我个人而言,前面几个问题我是会经常问的,但最后两个我总是会忽视它。而它正是设计、创新和优化的关键点,在以后学习知识点时我会多从最后两个问题的角度来审视知识,来进一步提高自己的能力。比如,为什么要这么安排进程,为什么要这么去书写架构,如何能更好的书写架构。要做到东东哥说的“比你的后辈更懂业务,比你的同辈更懂技术”。最后代码方面,对于一些题目如果实在不会,可以采用“三遍代码”策略:抄一遍,去掉源码保留注释写一遍,全部去掉再写一遍来确保学会。

②关于设计/项目的开发与架构方面学到的经验:

Ⅰ.模块化思想,对于大功能,注意去进行尽可能的拆解,应当想出分哪几步来实现之后再去看每一步怎么实现。亦即拆分成一个个函数或结构体/类。

Ⅱ.一些头文件:conio.h (con==console i==in o==out,控制台输入输出),主要是用getch()。

getch()系列:getch的原型:int getch(void);getch()会等待你按下任意键之后在执行下面的语句;而ch=getch()会等待你按下任意键,再把ASCII赋给ch,再执行下句。它最重要的功能即是不需回车即可接受输入信息,实现游戏中的键盘操作。

#include<mmsystem.h>与#pragma comment(lib,"winmm.lib"):mmsystem.h包含了windows中大多数与多媒体有关的接口,而“winmm.lib”相当于告诉编译器要导入winmm库(多媒体库)。这俩主要是为了后续的mciSendString函数服务的。

#pragma的别的用途:主要用于检查是否有某些宏定义,比如后跟参数message来发送消息:

#define SUM_NUM 5
#ifdef SUM_NUM
#pragma message("SUM_NUM get defined!")
#endif

注意这里发送的消息是在编译阶段发送到窗口的:

Ⅲ:一些小点:注意引用文件路径时,如果传参是宽字符串类型的,要注意使用L""或_T("")!同时,建议一些对于窗口等的基础定义,放在一个enum类中,比如高和宽等,可以让宏定义更清晰。

Ⅳ.游戏实现方面(1):譬如对于图片跟随鼠标功能的实现:

initgraph(500, 500);
	IMAGE img1;
	ExMessage ex;
	ex = getmessage(EM_MOUSE | EM_KEY);
	while (true)
	{
		BeginBatchDraw();
		cleardevice();
		loadimage(&img1, _T("background.jpg"));
		putimage(ex.x,ex.y, &img1);
		FlushBatchDraw();
	}
    EndBatchDraw();
	getchar();
	closegraph();

在不书写cleardevice()或者加载背景图片等操作的时候,产生过的图片是不会消失的。这就会导致我们的图片虽然是跟随着鼠标挪动了,但最终呈现的效果像是多张图片的叠加。所以我们这个功能的实现实际上需要先清除之前画的图,再画上新图,以此来不断的循环。但是如果单纯在循环中加入一个cleardevice()之后,整个程序运行起来,我们发现是全黑的。也就是说,cleardevice()这个操作在putimage之后瞬间就执行了,然后接着putimage,再瞬间cleardevice()掉,导致图片根本无法存在于界面中。我们可以理解为cleardevice()执行的速度远快于putimage,举个夸张点的例子,比如你花了10秒钟才完成了putimage才放上了图片,cleardevice()用了0.001秒就把绘图界面清空了,这自然会造成图片存在时间极短导致人眼根本看不到它的情况。

所以这个问题没有得以实现,可以理解为循环没有跟着我们希望的方向去进行。

我们希望的方向是什么呢?是一次完整的循环执行完再执行下一次循环吧!我们书写循环时单从代码来看,一次循环先cleardevice再putimage,如此进行,似乎很合理。可是加之上面的分析我们不难发现循环并不是那样走的。

比如,我们希望的执行过程是:

cleardevice()

putimage()

cleardevice()

putimage()

这样以单次循环为单位画图。

但实际上它的执行过程: cleardevice()

putimage()

cleardevice()

putimage()

...

是这样以函数为单位的不断交替进行的过程,那么它再加之上文提到过的速度问题,自然导致了功能实现的失败。

但是我们有办法让它以循环为单位来进行画图。这里的核心是BeginBatchDraw()与FlushBatchDraw的使用,我们来看一下easyx官方文档的定义:

BeginBatchDraw

这个函数用于开始批量绘图。执行后,任何绘图操作都将暂时不输出到绘图窗口上,直到执行 FlushBatchDraw 或 EndBatchDraw 才将之前的绘图输出。

FlushBatchDraw

这个函数用于执行未完成的绘制任务。

所以如果我们启用了BeginBatchDraw(),单次循环的画图操作会暂存起来,等到单次循环结束,他们就会同时进行。注意,是同时进行!如果把cleardevice()理解成一块黑布,那么同时进行相当于是把putimage操作的图片贴到黑布上之后,一起糊到窗口上。

这样,对于图片的不断消失与新建就完成了。

同样地,这个思想的用途极广,比如一枚子弹打出去,你要不断清除再重新根据坐标绘制它的图片

Ⅴ.游戏实现方面(2):关于界面切换时鼠标判定的重置:

使用while循环即可,在循环内不断判断鼠标操作,切换界面时直接使循环条件为0,再将切换后的界面的循环条件置为true。

Ⅵ.游戏实现方面(3):碰撞检测:考虑将两个物体看作矩形或圆。若为矩形且是横向碰撞,设两矩形物体的边长为r1,r2,那么当两矩形的横坐标差值<=(r1+r2)/2时即为碰撞;若为圆形且为横向碰撞,设两圆的半径为r1,r2,那么当两圆形的横坐标差值<=(r1+r2)/2时即为碰撞。

Ⅶ.游戏实现方面(4):飞机移动与子弹发射异步进行:

static DWORD t1 = 0, t2 = 0; //让子弹均匀发射
	if (GetAsyncKeyState(VK_SPACE) && t2 - t1 > 200) //按下空格键且子弹cd好了
	{
		createBullet();//创建子弹对象
		mciSendString("close BGM1 ", 0, 0, 0);
		mciSendString("open ./images/f_gun.mp3 alias BGM1", 0, 0, 0);
		mciSendString("play BGM1 ", 0, 0, 0);//加载音效
		t1 = t2;
	}
	t2 = clock();

同样的,也可以通过计时函数来定时创建子弹或者敌机对象,再调用move函数使他们移动即可。

2.此次项目项目设计

①.需求文档:整理需要的数据对象与操作

②.功能流程图:

③.产品原型图:

项目设计阶段我又重新下载植物大战僵尸玩了10关,大概捋了需要的数据对象与操作,但是在现在再看的话其实是不全面的,尤其是流程图部分,直接会影响到架构的清晰度,应当尝试从用户的角度来拟一份,再从游戏剧情的角度拟一份。“大概”永远是不够的。对于不是那么容易实现的项目,每个环节都要做到极致。

3.实现思路

①核心功能模块:

Ⅰ.僵尸、子弹、阳光产生与移动的异步进行,参考上文飞机大战中的异步实现,调用计时函数

Ⅱ.植物僵尸、子弹僵尸的碰撞检测,参考上文飞机大战中的实现思路,判断坐标

Ⅲ.贴图等的不断刷新,图片与鼠标的跟随,参考上文飞机大战中的刷新思路

Ⅳ.对象的创建,使用全局变量对象指针型数组,创建具体对象即使对应元素指向它

Ⅴ.草地块有无植物的判断,根据草地块五行九列创建全局变量植物指针型数组。如果植物种在了相应的草地块,则该块对应的数组元素的指针即指向该植物对象。这样对应的数组元素为NULLPTR的草地块即为没有植物的草地块,同时也建立的Location结构体用于存储植物所在草地块的行列数。

Ⅵ.僵尸的异步出现,可以给僵尸类添加数据对象用于存储出场时间,初值为负值,值随着时间++,等到值==0时,僵尸就入场。于是还应当给僵尸添加一个变量用于判断是否位于场景中。

Ⅶ.种植物、挖植物正如上文所提到的,是一系列动作!所以应当作拆分,比如选中植物,植物跟随鼠标移动,点击地面种下植物。

Ⅷ.游戏结束判断:游戏结束判断:如果死亡僵尸数==应产生僵尸数,游戏胜利;当僵尸进入”房间“,如僵尸左横坐标<小车所在位置的左横坐标,游戏失败。死亡僵尸数使用全局变量来实现,当调用ZombieDead函数时,应使得死亡僵尸数++

②全局变量设计:

int SUM_SUNSHINE = 0;//目前的总阳光数,为了与植物所需阳光作比判断“买不买得起”对应植物 int ZOMBIES_DEAD_NUM = 0;//死亡僵尸总数,为了与已产生僵尸作比判断游戏是否胜利 Location tempLocation;//用于返回临时的位置值,传入坐标与草块间相互转换的函数,再返回出来

Plant* arr[5][9] = { NULL }; //标记每块地图可否被种植,通过是否为NULLPTR来看 //创建新植物对象时,使得对应位置指针指向对应对象

/*创建铲子对象:*/ Basemodel Shovel(0x3f3f3f); /*创建僵尸对象组:*/ Zombie* zombies[SUM_ZOMBIES]; /*创建阳光对象组:*/ Sunshine* sunshine[500]; /*创建子弹对象组:*/ Basemodel* bullet[1000]; /*为了需要用到的对象贴图创建的IMAGE类对象:*/ extern IMAGE img1, img2, img3, img4, img5, img6, img7, img8, img9;

③宏定义设计:

#define WIN_HEIGHT 640//窗口高 #define WIN_WIDTH 1040//窗口宽 #define INIT_SUN 50//初始阳光数 #define PRO_SPEED_EN 0.25//环境产生阳光的速度,数值代表一秒几坨 #define SUN_PERNUM 25//每次产生的阳光数(一个阳光的值是多少) #define SUN_LIVESEC 11//产出的阳光存活时间,秒,与阳光存在时间作比决定阳光死不死 #define SUN_SPEED 5//阳光产生后落地所需时间,秒,落地后即停止移动 #define ZOMBIES_SPEED 50//僵尸的移动速度,一秒移动几个像素点 #define Car_NUM 5//手推车数量 #define SUM_ZOMBIES 50//这一局会产生的僵尸的总数,与僵尸死亡总数作比判断是否游戏结束

④类的设计:

Basemodel基本物体类:

①包含属性:

int blood;//血量 int ATK;//攻击力 double IAS;//攻速 一秒攻击几下 Location location;//位置 moreLocation morelocation;//详细坐标位置 IMAGE image;//物体本身贴图 bool isAlive;//是否存活 double MoveSpeed;//移动速度

②成员函数:均是Set/Get型函数

※其中血量、位置、详细坐标位置、贴图、是否存活是每个物体都需要的属性。

※攻击力、攻速因为僵尸、射手都需要,故放在了基类

※移动速度僵尸、子弹、手推车都需要,而子弹手推车直接使用基类创建对象,故也加入此成员

Sunshine阳光类:继承于Basemodel基本物体类

①包含属性:

double staytime;//阳光在场时间(进入场景多久了) double starttime;//开始下落的时间(什么时候开始下落)

②成员函数:均是Set/Get型函数

※其中开始下落时间初值为负,随着时间++,当==0时阳光开始下落

※在场时间用于和全局变量阳光存活的最大时间作比,判断阳光是否应当消失

Zombie僵尸类:继承于Basemodel基本物体类

①包含属性:

bool isOnScene;//僵尸是否位于场景中 double AppearTime;//僵尸什么时候出现在场景中 int death;//僵尸是怎么死的。

※是否位于场景中,初值为负,当AppearTime==0时置为true

※僵尸什么时候出现在场景中,倒计时。初值为负,不断++,等到==0时僵尸入场

※death值为1,射死;2,炸死;3,坐死

②成员函数:均是Set/Get型函数

Plant植物类:继承于Basemodel基本物体类

①包含属性:

int needsunshine;//生产该植物所需阳光数 double cd;//冷却时间 int sign;//植物标识

※:植物标识即为标志自己是哪一类植物。因后续会有传参为Plant类型的函数。向日葵1豌豆2坚果3

②成员函数:均是Set/Get型函数

Shooter类:继承于Basemodel基本物体类

①包含属性:

较于一般植物比较特别的就是攻击力和攻速。

攻击力也决定了每个子弹的伤害以及子弹对象发射的频率

②成员函数:均是Set/Get型函数

Sunflower类:继承于Basemodel基本物体类

①:包含属性:

double PRO_SPEED_SF;//生产阳光的速度

※向日葵的产阳光速度也可以通过宏定义定义好,然后向日葵对象的创建直接基于普通植物类也可。但感觉区分植物类型的时候不如这样清晰

②成员函数:均是Set/Get型函数

Nut坚果类:继承于Basemodel基本物体类

①:包含属性:

没有任何特别的属性,只是血厚,但没有新的类型的成员。创建此类也是觉得在种植物选植物的时候便于区分。

②成员函数:均是Set/Get型函数

⑤分层架构设计:

先从基本架构谈起,应当涉及全局变量、类、函数。前俩一般放dao,功能函数就分离到service。

同时,非常重要的一点,也是这次我忽略的一点,就是架构时应当确立的编码规范问题。比如全局变量有哪些,比如接受鼠标参数统一用ExMessage,比如命名采取单词首字母大写式,如GetMouseInput,比如需要的数据对象的格式、名字,反馈的数据对象的格式、名字,这些都是要规范的。这次交的函数其实也有自己另加了全局变量了的,调试起来会很麻烦,这也是编码规范没有规定好的缘故。

dao层:存放实体类、宏定义、数据,以及完成文件操作/数据库操作(此次没有涉及)。

下文列出了我在dao层存放的东西,上文给出了类的设计思路。

service层:存放功能函数,涉及逻辑判断,数据的判断修改

※当涉及从库中读取数据/写入数据至库时,调用dao

view层:负责与用户的交互、展示内容。接收用户输入,绘制相应界面

※当涉及判断/数据修改时,调用service

这两层函数的具体功能我都放在了下面的代码块处,并且补充了不少注释。

于是遵照这service和view的分层,我把贴图类函数全部分离到了view层,也就是Create_xxx系列的函数,虽然叫Create,但他们实现的功能都是贴图跟随物体坐标传值移动,也就是完成物体的贴图。

但是对于鼠标输入,我觉得我的实现做颠倒了。包括书写PutPlant等函数的同学也搞颠倒了。比如我在view写了一个GetMouseInput函数,想要通过view层接受鼠标输入再返回给service层,这样做不现实。因为我在view.h中包含了service.h,就不可以再在service.h中包含view.h了,会使得service.h中显示调用的view层的函数是不存在的。再比如PutPlant这方面,这个函数的书写者在Put后调用了Create系列的函数来传入贴图,同样肯定也是不可以的。

所以这个事情到现在我觉得唯一的解决方案就是把关于鼠标坐标的所有操作全放到view层,然后给service层的对应函数传ExMessage型的参数,然后在view层的函数中来调用它们。目前仍然没有实现,等到我做完这次大创之后再尝试这样的代码调整吧。

所以,三层架构的关键,就是view包含service,service包含dao,最终的根据输入完成的各种调用是在view层实现的,而不是在service做好所有的工作。service是需要做好各种的逻辑判断以及值的修改等,某些自动实现的比如僵尸移动也许会最终是在service做,但对于那种接受输入,完成一系列操作的,最终还是要在view中完成对service调用的。

4.项目架构详述:

Ⅰ.dao层架构详述:

#pragma once
//dao层:存放实体类、存放数据;进行数据库/文件操作


//1.没事别在头文件写#include<iostream>,一般用不到,
//而且这种重复声明在某些编译器可能会报错
//2.头文件里永远不要写using namespace std
//3.头文件应当只包含函数声明,同时应建立对应的
//.cpp文件写实现
//4.全局变量也只是在.cpp文件中定义即可


#include<graphics.h>
//宏定义:(窗口大小等)
#define WIN_HEIGHT 640//窗口高度
#define WIN_WIDTH 1040//窗口宽度
#define INIT_SUN 50//初始阳光数
#define PRO_SPEED_EN 0.25//环境产生阳光的速度,数值代表一秒几坨
#define SUN_PERNUM 25//每次产生的阳光数(一个阳光的值是多少)
#define SUN_LIVESEC 11//产出的阳光存活时间 秒
#define SUN_SPEED 5//阳光产生后落地所需时间 秒
#define ZOMBIES_SPEED 50//僵尸的移动速度  一秒移动几个像素点
#define Car_NUM 5//手推车数量
#define SUM_ZOMBIES 50//这一局会产生的僵尸的总数


//位置坐标结构体
struct Location
{
    int i, j;
    //i存放所在行数,j存放所在列数
};
//这个结构体用来存放物体(主要是植物)在哪个草地块

//可用来判断能否种植/僵尸是否吃植物/是否打子弹


struct moreLocation
{
    int x1, y1, x2, y2;
};

//左上角坐标(x1,y1)右下角坐标(x2,y2)
//存放更详细的坐标值:可判断手推车是否发射
//同时用于子弹和僵尸的碰撞检测、move函数书写等


class Basemodel//基本物体类
//手推车、子弹都基于此类创建对象
{
public:
    Basemodel();
    Basemodel(int blood);
    //用于创建对象时给物体血量赋值
    void SetLocation(int x, int y);
    //设置物体所在格数
    //x表示物体所在行,y表示物体所在列
    void SetMoreLocation(int XL, int YL, int XR, int YR);
    //设置物体具体位置
    
    Location GetLocation();
    //获得该物体当前所在格数

    moreLocation GetMoreLocation();
    //获得该物体的具体位置坐标
    int GetBlood();//血量获取
    void SetBlood(int a);//设置血量
    bool GetAlive();//存活状态获取
    double GetMoveSpeed();//移动速度获取(子弹等常用)
    void SetAlive(bool judge);//设置存活状态
    int GetATK();//获取物体攻击力
    void SetImage(IMAGE image1);//设置物体贴图
protected:
    int blood;//血量
    int ATK;//攻击力
    double IAS;//攻速  一秒攻击几下
    Location location;//位置
    moreLocation morelocation;//详细坐标位置
    IMAGE image;//物体本身贴图    
    bool isAlive;//是否存活 
    double MoveSpeed;//移动速度
};

class Sunshine :public Basemodel
{
private:
    double staytime;//阳光在场时间(进入场景多久了)
    double starttime;//开始下落的时间(什么时候开始下落)
    //初值为负值,随着时间++,直到为0时即进入场景
public:
    void set_staytime(double a);//设置在场时间
    double get_staytime();//获取在场时间
    void set_starttime(double a);//设置开始下落时间
    double get_starttime();//获取开始下落时间
};

class Plant :public Basemodel//植物类
{
public:
    Plant();
    Plant(int blood, int need_sunshine, int a1, int a2);
    //通过构造函数调用,分别赋血量、需阳光数、所在格数
    int GetNeedSunshine(); //获取此植物所需要的阳光数
    double getCD();//获取该植物的冷却时间
    int GetSign();//获取植物标识
protected:
    int needsunshine;//生产该植物所需阳光数 
    double cd;//冷却时间
    int sign;//植物标识,标志自己是哪一类植物
    //因后续会有传参为Plant类型的函数。向日葵1豌豆2坚果3
};
class Sunflower :public Plant
{
public:
    Sunflower();
    Sunflower(int sfblood, int sfneed, int a1, int a2);
    //通过构造函数调用,分别赋血量、需阳光数、所在格数
protected:
    double PRO_SPEED_SF;//生产阳光的速度
};
//向日葵类

class Shooter :public Plant
{
public:
    Shooter();
    Shooter(int stblood, int stneed, int sfATK, int sfIAS, int a1, int a2);
    //通过构造函数调用,分别赋血量、需阳光数、攻击力、攻速、所在格数
};

//射手类

class Nut:public Plant
{
public:
    Nut();
    Nut(int ntblood, int ntneed, int a1, int a2);
    //通过构造函数调用,分别赋血量、需阳光数、所在格数
};
//坚果类
class Zombie :public Basemodel//僵尸类
{
public:
    double GetAppearTime();//获取出场时间
    void SetisOnScene(bool judge);//设置是否位于场景内
    bool GetisOnScene();//获取是否在场景内
    Zombie();//直接赋给僵尸它的攻速攻击力血量出场时间
    Zombie(int zmblood, int timez);
    //也会直接赋给僵尸他的攻速攻击力,可手动指定血量出场时间
    ~Zombie();
protected: 
    bool isOnScene;//僵尸是否位于场景中,初值false
    double AppearTime;//僵尸什么时候出现在场景中,倒计时
    //初值为负,不断++,等到==0时僵尸入场
    int death;//僵尸是怎么死的。值为1,射死;2,炸死;3,坐死
};
//目前先实现一种僵尸,即普通僵尸

2.service层架构详述:

#pragma once
#include "dao.h"
#include <time.h>
//为了实现鼠标操作与逻辑判断的分离:获取鼠标消息的函数应位于view层
//注意所有传参为ExMessage的函数都要调用GetMouseInput()

//进行所有的逻辑判断操作:
class System
{
public:
	System();
    ~System();
	
	void InitGame();
    //最初设想的有问题  因为创建对象得在外边搞全局变量。
	//否则直接这样InitGame的话出来的对象作用域只在函数内部
	//于是这个函数应该做的工作是对象数组创建完成后让他们指向具体的对象
    //再做一些基本的初始化,比如把向日葵应有的血量通过构造函数给向日葵对象


/*阳光操作相关:*/
	void CreateSunshine(Sunshine** sunshine);
    //通过取随机数主要为每个阳光对象赋予出场时间以及初始坐标
	void SunshineMove(Sunshine Sunshine);
    //根据阳光对象的运动速度来不断更新阳光对象的坐标
	void PickSunshine(Sunshine sunshine, ExMessage exmessage);
    //传入鼠标坐标,进行判断是否点击到阳光。若点击到,全局变量阳光值增加
	bool isSunshineDead(Sunshine sunshine);
    //根据阳光的在场时间与全局变量的阳光应存活的时间判断阳光是否存活


/*植物操作相关:*/


/*种植植物:*/
	int PickWhichPlant(ExMessage ex);
    //根据鼠标坐标与战斗界面上方植物栏处各个植物卡片坐标的关系
    //返回对应的植物的标识值
    //同时还需要有阳光是否足够、CD是否结束的判断
	bool PlantFollow(Plant plant, ExMessage ex);
    //判断是否还要实现植物图片与鼠标的跟随
    //
	void PutPlant(Plant plant,ExMessage ex,int a);
    //
	/*挖植物:拆解为3个过程  选中铲子,铲子跟随,选中植物*/
	bool PlantAbility(ExMessage ex);
    //根据arr判断一下鼠标选中的对应格子是否可种植植物
	void PickShovel(ExMessage ex);
    //如果鼠标坐标对应着铲子的坐标范围,选中铲子
	void DigPlant(Plant plant, ExMessage ex);
    //选中要挖的植物,先通过arr判断选中的格子有没有植物
    //若有植物,相当于该格子指针指向的植物对象死亡,调用PlantDead函数
	void PlantHurt(Plant plant, Zombie zombie);
    //获取植物和僵尸的坐标,若“碰撞”,则植物以一定速率掉血
    //若血量为0,调用PlantDead
	void PlantDead(Plant plant);
    //植物血量为0,坐标设为非常大的值,便于view层对相应对象贴图做处理。
    //同时,存活态置为false,使得arr对应格子的指针置为nullptr
	bool isSunshineEnough(Plant plant);
    //根据全局变量存储的当前阳光数量与传入的植物对象所需要的阳光数量
    //判断阳光是否足够来“购买”此植物,足够则返回1


/*僵尸操作相关:*/
	void CreateZombie(Zombie** zombies);
    //通过取随机数为每个僵尸赋予应当位于的行,设置出场时间
	void ZombieMove(Zombie zombie);
    //根据僵尸的移动速度改变对应僵尸对象的坐标值
	void ZombieHurt(Zombie zombie, Basemodel bullet);
    //内部调用Collision_Check_Bullet函数判断僵尸是否掉血
    //若应当掉血,则僵尸血量对应减少。血量==0时,调用ZombieDead
	void ZombieDead(Zombie zombie);
    //血量==0时存活状态设为false,坐标设为一个非常大的值
    //全局变量中僵尸死亡数++


/*子弹操作相关:*/
	void CreateBullet(Basemodel** Bullet, Shooter** shooter, Zombie zombie);
    //如果射手对象与僵尸对象位于同一列,且均存活,定时创造子弹对象
	bool IsShoot(Shooter shooter, Zombie zombie);
    //如果植物和僵尸对象位于同一行,返回1,即应当射击
	void BulletMove(Basemodel Bullet, Zombie zombie, Shooter shooter);
    //内部调用CreateBullet函数,根据移速不断改变子弹对象的坐标
	bool Collision_Check_Bullet(Zombie zombie, Basemodel Bullet);
    //子弹与僵尸的碰撞检测,碰撞返回1,实现思路见上文
	bool Collision_Check_Plant(Plant plant, Zombie zombie);
    //僵尸与植物的碰撞检测,碰撞则返回1
/*手推车操作相关:*/
	bool isCarMove(Zombie zombie, Basemodel Car);
    //传入僵尸对象,若僵尸左横坐标值==手推车右横坐标值返回1,即手推车应当发射
	void CarMove(bool judge, Basemodel Car);
    //调用isCarMove函数,如果应发射,根据手推车速度改变手推车坐标值

/*游戏总操作相关:*/
	Location TransToLocation(int x, int y);
    //接收x,y值,根据x,y值返回相应的草地块的行列数
    //主要用于点击草地时判断点击的是哪块草地
	moreLocation TransToMoreLocation(Location location);
    //根据传入的草地的行列数,返回这块草地的左上角、右下角坐标
    //主要获取种下的植物坐标与发射出来的子弹的坐标
	void GameControl();
    //游戏总操控
	bool GameOver(Zombie** zombies);
    //游戏结束判断:如果死亡僵尸数==应产生僵尸数,游戏胜利
    //调用游戏胜利界面
    //当僵尸进入房间,如僵尸左横坐标<小车所在位置的左横坐标,游戏失败
    //调用游戏失败界面
};

3.view层架构详述:

#pragma once
#include"service.h"
class GameDraw
{
public:
/*接受鼠标输入函数:*/
	ExMessage GetMouseInput();
    //返回鼠标消息

/*界面绘制函数:*/
void MainTheme();//初始界面
LPTSTR EnterName();//键入玩家姓名界面 
void MainMenu();//主菜单界面
void Settings();//设置界面
void MapChoose(); //选择游戏界面 
void Store();//存档界面
void Shop();//商店界面
void OtherGames();//玩玩小游戏界面 
void Exit();//退出界面 
void Battle();//战斗界面背景地图界面
void CardChoose();//选择植物界面 
void GamePause();//游戏暂停界面
void GameMenu();//游戏内菜单界面
void WinGame();//游戏胜利界面
void LoseGame();//游戏失败界面


void Game_Draw(int a);
//a取值为1-15,分别对应上面1-15界面的绘制	


/*游戏内对象绘制函数:*/

//这些函数比较特殊,Create系列的函数分别完成对应种类的对象的全部绘图操作
//不只是对象贴图的创建,所有的图的移动都由他们完成
//根据对象的坐标值不断刷新图片创建

void Create_Plant(Plant plant, bool judge);
void Create_Sunshine(Basemodel sunshine);
void Create_Bullet(Basemodel Bullet);
void Create_Car(Basemodel Car);
void Create_Shovel(Basemodel Shovel);
void Create_Boom(Basemodel Boom);

/*对象消失函数:均为移除贴图*/
void Bullet_Dead(bool judge);
void Plant_Dead(Plant plant);
void Zombie_Dead(Zombie zombie);
void Sunshine_Dead(bool judge);
};

4.源文件中的全局变量定义:

//全局变量:
int SUM_SUNSHINE = 0;//目前的总阳光数
int ZOMBIES_DEAD_NUM = 0;//死亡僵尸总数
Location tempLocation;//用于返回临时的位置值



Plant* arr[5][9] = { NULL };
//标记每块地图可否被种植,通过是否为NULLPTR来看
//创建新植物对象时,使得对应位置指针指向对应对象

/*创建铲子对象:*/
Basemodel Shovel(0x3f3f3f);
/*创建僵尸对象组:*/
Zombie* zombies[SUM_ZOMBIES];
/*创建阳光对象组:*/
Sunshine* sunshine[500];
/*创建子弹对象组:*/
Basemodel* bullet[1000];
/*为了需要用到的对象贴图创建的IMAGE类对象:*/
extern IMAGE img1, img2, img3, img4, img5, img6, img7, img8, img9;

5.头文件中的宏定义:

#define WIN_HEIGHT 640//窗口高
#define WIN_WIDTH 1040//窗口宽
#define INIT_SUN 50//初始阳光数
#define PRO_SPEED_EN 0.25//环境产生阳光的速度,数值代表一秒几坨
#define SUN_PERNUM 25//每次产生的阳光数(一个阳光的值是多少)
#define SUN_LIVESEC 11//产出的阳光存活时间,秒,与阳光存在时间作比决定阳光死不死
#define SUN_SPEED 5//阳光产生后落地所需时间,秒,落地后即停止移动
#define ZOMBIES_SPEED 50//僵尸的移动速度,一秒移动几个像素点
#define Car_NUM 5//手推车数量
#define SUM_ZOMBIES 50//这一局会产生的僵尸的总数,与僵尸死亡总数作比判断是否游戏结束

5.学到的东西

①“设计”思想,也就是安排与计划思想。最重要的一点。

从最开始的设计就要做到完善,每个环节哪怕多花时间也要做到完善,否则非常影响后续的环节。在立项时,就要做好可行性的分析。依照会有的功能考虑技术的实现难度,这个往往被忽略。同样的技术也应当分成局部技术和全局技术,前者是部分人会就可以的技术,后者是每个人都必须会的技术。比如前者,QT/EasyX界面实现,或者mysql数据库存储;后者则是Git、类的特性,函数书写等。可行性分析时要把需要的技术这样做分类,然后剖析是否可行。如果不可,尽早更换!不要头铁!

然后,设计环节。首先,需求文档必须非常精细的设计好核心部分,比如此次游戏的核心部分其实只有战斗界面的植物发射子弹,子弹移动,僵尸移动,子弹击中僵尸这几部分。剩下的像是选择各种植物,种植物,挖植物,收取阳光等都算作非核心部分,亦即不至于那么核心但没有他们会不合理的部分。总之,捋清楚需要的功能与数据对象。

然后,在需求文档的基础上,以完全还原的级别去设计游戏界面的原型图。像这次,我的原型图首先并不完全契合最终的游戏界面,有些背景图因为赶时间都是从网上随手扒的或者是从游戏里截取的。这样没法在界面设计分工的时候分给组员对应的图片去做分函数实现,使得大家需要自己从网上搜罗资源,最后会有坐标数值不契合的各种问题。而且我在设计战斗界面的时候没有完全还原应有的界面,包括上方的植物栏、铲子图标、手推车图标等,只是按照自己的理解觉得放一个背景就差不多能有那样的意思,导致后续的界面设计等都需要现摸索现调整。不过,原型图最核心的,还是体现界面的跳转逻辑,重点展示出用户点击哪里会跳转到哪里的操作。

再之后,在流程图设计方面,整个流程图必须完完全全与项目应有的剧情流程或用户操作相同。这样架构才可以完全依照流程图等来设计,而不至于脱离于前面的环节来做这个环节。前后的环节都是有很强的延续性的,如果每个环节都为了赶时间做的很粗糙很独立的话,最终一定会导致整个项目的大问题。同样的,产品原型图也应确定需要展示哪些数据。

最后,也就是架构,必须完全设计好才能分工。这次因为太想做出这个项目,拿了一个充满缺口的架构分工、整合,最后的结果也必然是表面看起来没有语法错误而内部充满问题。于是架构完成的意思应当是dao、service、view三层架构的头文件均很完善,注释用统一的风格写好,每个函数的写法技术官或组长应当有很成熟的思路,各种函数的调用与转化关系全部设计好并在源文件给出。这样,再去分工函数,才是正确的。这样分工的函数完成后直接填到对应的函数定义处即可,而不是像现在一样收到了函数我却还在考虑实现问题。

总之,一定要做好当前环节之后,再进行下一个环节。也就是在所有东西都设计好之后再着手写代码。即使要设计四五天,只要能做完善了,最后分工开发填写都会是非常顺利的。

总之,别急着开发,每个人都确定好功能之后,再开始开发。

②作为组长的一些做法:

譬如云海讲,当征求某一问题的意见时,只要那个问题是你能给出几种答案的,那就尽量把这样一个开放问题变成一个选择题。比如询问开会时间,不要直接问大家觉得几点开会好。有两种做法是可取的——可以问,9点半可以吗大家?或者,我们是9点还是9点半开会?

因为开放问题往往不容易得到答案,大家在发言的时候会感觉没有抓手,往往会不去发言。而且就算得到答案往往几个答案之间相差的也不小,协调起来也比较麻烦,所以最好去问有限定条件的问题。

再一个,每天的会议是很必要的,尤其是开发阶段。即使看起来好像真的没什么可说的,但是组织起来开个会起码能了解大家的进度与问题,也能让大家一直处在项目开发的这个状态里。

然后,每日总结非常重要,但这次我忽略了。每天填写项目的开发进度表是非常必要的,这样能量化出进度来,而不是凭着感觉觉得“快了”之类的,这很容易导致进度的快慢设置不合理。

③技术方面:

学会了gitee的相关操作,也捡起来了很多很多很久不用快要忘掉的东西,比如头文件的书写,类的继承机制,指针数组,初始化列表。还有上文提到的所有东西。更重要的,我觉得起码能对架构的设计有了一定的认识了,不至于像以前一样完全没有想法,盲人摸象一般前进。

头文件书写方面,头文件是一个“公共的函数声明处”,所以很需要#pragma once来防止多次包含引发的重定义问题。而且注意.h文件中放函数声明,宏定义,全局变量声明。并且在对应的.cpp文件(尽量同名)中放函数的定义部分以及全局变量的定义。注意头文件最好不要写#Include<iostream>和using namespace std;之类的语句,上文也提到过,iostream太大了,std命名空间又包括了太多的东西,这会使得包含头文件的文件中的一些错误难以寻找。

同时,头文件不可以相互包含。也就是如果在service.h中包含了view.h的话,即不可以再在view.h中包含service.h。这也是我写架构的时候被卡住的点之一。

#include<>用于引用库头文件,而""用于引用自定义的头文件。""的意思是头文件与当前的sln文件位于同一目录下。

switch语句case完事一定要break掉!!!!!!!

指针数组在需要创建多个某类的对象且需要调用有参构造时非常有用。因为创建指针数组时不会像直接创建对象数组一样会实例化出对象来,导致必须有无参构造可以调用。创建指针数组时是没有实例化对象的,等到指针数组中存放的指针指向new出来的对象时才会实例化,这时即可使用有参构造了。

6.未来展望

①技术上的遗憾:这次没有用出类的多态性,没有能使用类的析构函数,只能通过一些重复的代码和Dead系列的函数来实现相同功能,没有利用好类的特性,应当要补充这方面的知识。

②继续读C++ primer,去GitHub找优质开源项目的源码来看,学习架构的思想以及好的实现方式。

③大一下要完成十篇博客,再完成一个项目,学完数据结构与算法、操作系统,读完C艹Primer,熟练Mysql,争取能搞会计网。

2022.5.4更新:两个星期前重拾了这个项目,又做了几天,但是又因为接了远程实习助教&准备PAT&进修Cpp而搁置了,希望这学期学完设计模式之后暑假能腾出时间来做完这个战线拉的老长老长的项目吧……

现在码上重整之后的架构:

从分dao&service&view变为基本一个类一个头文件

头文件一、BaseDefinition.h 存放基本的定义

#pragma once
#pragma comment(lib, "Winmm.lib ")
#include<graphics.h>
//位置坐标结构体
struct Location
{
    int i, j;
    //i存放所在行数,j存放所在列数
};
//这个结构体用来存放物体(主要是植物)在哪个草地块

//可用来判断能否种植/僵尸是否吃植物/是否打子弹

struct moreLocation
{
    int x1, y1, x2, y2;
};


extern wchar_t s[20];

extern IMAGE img1, img2, img3, img4, img5, img6, img7, img8, img9;



#define WIN_HEIGHT 640//窗口高
#define WIN_WIDTH 1040//窗口宽
#define INIT_SUN 50//初始阳光数
#define PRO_SPEED_EN 0.25//环境产生阳光的速度,数值代表一秒几坨
#define SUN_PERNUM 25//每次产生的阳光数(一个阳光的值是多少)
#define SUN_LIVESEC 11//产出的阳光存活时间,秒,与阳光存在时间作比决定阳光死不死
#define SUN_SPEED 5//阳光产生后落地所需时间,秒,落地后即停止移动
#define ZOMBIES_SPEED 5//僵尸的移动速度,一秒移动几个像素点
#define Car_NUM 5//手推车数量
#define SUM_ZOMBIES 50//这一局会产生的僵尸的总数,与僵尸死亡总数作比判断是否游戏结束

头文件二、ClassBasemodel.h 基本物体类

#pragma once
#include"BaseDefinition.h"
class Basemodel//基本物体类
//手推车、子弹都基于此类创建对象
{
public:
    Basemodel();
    Basemodel(int blood);
    //用于创建对象时给物体血量赋值
    void SetLocation(int x, int y);
    //设置物体所在格数
    //x表示物体所在行,y表示物体所在列
    void SetMoreLocation(int XL, int YL, int XR, int YR);
    //设置物体具体位置

    Location GetLocation();
    //获得该物体当前所在格数

    moreLocation GetMoreLocation();
    //获得该物体的具体位置坐标
    int GetBlood();//血量获取
    void SetBlood(int a);//设置血量
    bool GetAlive();//存活状态获取
    int GetMoveSpeed();//移动速度获取(子弹等常用)
    void SetMoveSpeed(int a);//设置移动速度
    void SetAlive(bool judge);//设置存活状态
    int GetATK();//获取物体攻击力
    void SetImage(IMAGE image1);//设置物体贴图
protected:
    int blood;//血量
    int ATK;//攻击力
    double IAS;//攻速  一秒攻击几下
    Location location;//位置
    moreLocation morelocation;//详细坐标位置
    IMAGE image;//物体本身贴图    
    bool isAlive;//是否存活 
    int MoveSpeed;//移动速度
};

头文件三、ClassPlant.h

#pragma once
#include"ClassBasemodel.h"
class Plant :public Basemodel//植物类
    //坚果类也可调用基础植物类  因为没什么新的属性可言
{
public:
    Plant();
    Plant(int blood, int need_sunshine, int a1, int a2);
    //通过构造函数调用,分别赋血量、需阳光数、所在格数
    int GetNeedSunshine(); //获取此植物所需要的阳光数
    double getCD();//获取该植物的冷却时间
    int GetSign();//获取植物标识 
    void SetSign(int);//设置植物标识
    void SetCD(int);//设置冷却时间
    ~Plant();
protected:
    int needsunshine;//生产该植物所需阳光数 
    double cd;//冷却时间
    int sign;//植物标识,标志自己是哪一类植物
    //因后续会有传参为Plant类型的函数。向日葵1豌豆2坚果3
};

头文件四、ClassShooter.h

#pragma once
#include"ClassPlant.h"
class Shooter :public Plant
{
public:
    Shooter();
    Shooter(int stblood, int stneed, int sfATK, int sfIAS, int a1, int a2);
    //通过构造函数调用,分别赋血量、需阳光数、攻击力、攻速、所在格数
};

//射手类

头文件五、ClassSunflower.h

#pragma once
#include"ClassPlant.h"
class Sunflower :public Plant
{
public:
    Sunflower();
    Sunflower(int sfblood, int sfneed, int a1, int a2);
    //通过构造函数调用,分别赋血量、需阳光数、所在格数
protected:
    double PRO_SPEED_SF;//生产阳光的速度
};
//向日葵类#pragma once

头文件六、ClassSunshine.h

#pragma once
#include"ClassBasemodel.h"
class Sunshine :public Basemodel
{
private:
    double staytime;//阳光在场时间(进入场景多久了)
    double starttime;//开始下落的时间(什么时候开始下落)
    //初值为负值,随着时间++,直到为0时即进入场景
    int leftsec;//阳光的剩余存活时间
public:
    Sunshine();
    void set_staytime(double a);//设置在场时间
    double get_staytime();//获取在场时间
    void set_starttime(double a);//设置开始下落时间
    double get_starttime();//获取开始下落时间
    void set_leftsec(int a);//设置阳光的剩余存活时间
    double get_leftsec();//获取阳光的剩余存活时间
};

头文件七、ClassZombie.h

#pragma once
#include"ClassBasemodel.h"
class Zombie :public Basemodel//僵尸类
{
public:
    double GetAppearTime();//获取出场时间
    void SetisOnScene(bool judge);//设置是否位于场景内
    bool GetisOnScene();//获取是否在场景内
    Zombie() = delete;//后边有默认值有参构造,为了防止歧义,删除默认构造
    Zombie(int zmblood, int timez);
    //也会直接赋给僵尸他的攻速攻击力,可手动指定血量出场时间
    Zombie(int a=-10);//传入的int值只用来指定出场时间 默认值-10
    ~Zombie();
    void ChangeAppearTime(int);//改变出场时间
protected:
    bool isOnScene;//僵尸是否位于场景中,初值false
    double AppearTime;//僵尸什么时候出现在场景中,倒计时
    //初值为负,不断++,等到==0时僵尸入场
    int death;//僵尸是怎么死的。值为1,射死;2,炸死;3,坐死
};
//目前先实现一种僵尸,即普

头文件八、DrawPics.h

#pragma once
#include"BaseDefinition.h"
#include"ClassPlant.h"
#include"ClassZombie.h"


	/*界面绘制函数:*/
	void MainTheme();//初始界面
	bool EnterName();//键入玩家姓名界面 
	void MainMenu();//主菜单界面
	void Settings();//设置界面
	void MapChoose(); //选择游戏界面 
	void Store();//存档界面
	void Shop();//商店界面
	void OtherGames();//玩玩小游戏界面 
	void Exit();//退出界面 
	void Battle();//战斗界面背景地图界面
	void CardChoose();//选择植物界面 
	void GamePause();//游戏暂停界面
	void GameMenu();//游戏内菜单界面
	void WinGame();//游戏胜利界面
	void LoseGame();//游戏失败界面




	//使用Switch语句  根据不同的GetResult值来判断调用哪个绘制界面的函数

	void Game_Draw(int a);




	/*游戏内对象绘制函数:*/

	//这些函数比较特殊,Create系列的函数分别完成对应种类的对象的全部绘图操作
	//不只是对象贴图的创建,所有的图的移动都由他们完成



	//1-9分别为阳光、豌豆、向日葵、坚果、僵尸、铲子、手推车、子弹、子弹爆炸
	void Create_Sunshine(Basemodel sunshine);

	void Create_Bullet(Basemodel Bullet);

	void Create_Car(Basemodel Car);
	void Create_Shovel(Basemodel Shovel);
	void Create_Boom(Basemodel Boom);

	/*对象消失函数:*/


	void Bullet_Dead(bool judge);
	void Plant_Dead(Plant plant);
	void Zombie_Dead(Zombie zombie);
	void Sunshine_Dead(bool judge);

	/*游戏结束界面绘制:*/

	void Game_Over(bool judge);

头文件九、GameControl.h

#pragma once
#include"ClassBasemodel.h"
#include"ClassZombie.h"
#include"ClassPlant.h"
#include"ClassShooter.h"
#include"ClassSunflower.h"
#include"ClassSunshine.h"
#include"DrawPics.h"
#include <time.h>

//为了实现鼠标操作与逻辑判断的分离:获取鼠标消息的函数应位于view层
//注意所有传参为ExMessage的函数都要调用GetMouseInput()

//进行所有的逻辑判断操作:
    void InitGame();
    //最初设想的有问题  因为创建对象得在外边搞全局变量。
    //否则直接这样InitGame的话出来的对象作用域只在函数内部
    //于是这个函数应该做的工作是对象数组创建完成后让他们指向具体的对象
    //再做一些基本的初始化,比如把向日葵应有的血量通过构造函数给向日葵对象


/*阳光操作相关:*/
    void CreateSunshine(Sunshine** sunshine);
    //通过取随机数主要为每个阳光对象赋予出场时间以及初始坐标
    void SunshineMove(Sunshine Sunshine);
    //根据阳光对象的运动速度来不断更新阳光对象的坐标
    void PickSunshine(Sunshine sunshine, ExMessage exmessage);
    //传入鼠标坐标,进行判断是否点击到阳光。若点击到,全局变量阳光值增加
    bool isSunshineDead(Sunshine sunshine);
    //根据阳光的在场时间与全局变量的阳光应存活的时间判断阳光是否存活


/*植物操作相关:*/


/*种植植物:*/
    //根据鼠标坐标与战斗界面上方植物栏处各个植物卡片坐标的关系
    //返回对应的植物的标识值
    //同时还需要有阳光是否足够、CD是否结束的判断
    bool PlantFollow(Plant plant, ExMessage ex);
    //判断是否还要实现植物图片与鼠标的跟随
    //
    void PutPlant(Plant plant, ExMessage ex, int a);
    //
    /*挖植物:拆解为3个过程  选中铲子,铲子跟随,选中植物*/
    bool PlantAbility(ExMessage ex);
    //根据arr判断一下鼠标选中的对应格子是否可种植植物
    void PickShovel(ExMessage ex);
    //如果鼠标坐标对应着铲子的坐标范围,选中铲子
    void DigPlant(Plant plant, ExMessage ex);
    //选中要挖的植物,先通过arr判断选中的格子有没有植物
    //若有植物,相当于该格子指针指向的植物对象死亡,调用PlantDead函数
    void PlantHurt(Plant plant, Zombie zombie);
    //获取植物和僵尸的坐标,若“碰撞”,则植物以一定速率掉血
    //若血量为0,调用PlantDead
    void PlantDead(Plant plant);
    //植物血量为0,坐标设为非常大的值,便于view层对相应对象贴图做处理。
    //同时,存活态置为false,使得arr对应格子的指针置为nullptr
    void Create_Plant(Plant* plant, bool judge);
    //植物对象绘图
    bool isSunshineEnough(Plant plant);
    //根据全局变量存储的当前阳光数量与传入的植物对象所需要的阳光数量
    //判断阳光是否足够来“购买”此植物,足够则返回1


/*僵尸操作相关:*/
    void CreateZombie(Zombie** zombie);
    //通过取随机数为每个僵尸赋予应当位于的行,设置出场时间
    void real_create_zombies();
    void ZombieAppear(Zombie* zombie);//改出场时间
    void ZombieMove(Zombie &zombie,Shooter*);
    //根据僵尸的移动速度改变对应僵尸对象的坐标值
    void ZombieHurt(Zombie&zombie, Basemodel&bullet);
    //内部调用Collision_Check_Bullet函数判断僵尸是否掉血
    //若应当掉血,则僵尸血量对应减少。血量==0时,调用ZombieDead
    void ZombieDead(Zombie* zombie);
    //血量==0时存活状态设为false,坐标设为一个非常大的值
    //全局变量中僵尸死亡数++
    void ifShowZombies(Zombie* zombie);
    //僵尸是否入场
    void Create_Zombies(Zombie& zombie);
    //僵尸对象绘图

/*子弹操作相关:*/
    void CreateBullet(Shooter* shooter, Zombie&zombie);
    //如果射手对象与僵尸对象位于同一列,且均存活,定时创造子弹对象
    bool IsShoot(Shooter&shooter, Zombie*zombie);
    //如果植物和僵尸对象位于同一行,返回1,即应当射击
    void BulletMove(Zombie&zombie, Shooter*shooter);
    //内部调用CreateBullet函数,根据移速不断改变子弹对象的坐标
    bool Collision_Check_Bullet(Zombie& zombie, Basemodel& Bullet);
    //子弹与僵尸的碰撞检测,碰撞返回1,实现思路见上文
    bool Collision_Check_Plant(Plant plant, Zombie zombie);
    //僵尸与植物的碰撞检测,碰撞则返回1
/*手推车操作相关:*/
    bool isCarMove(Zombie zombie, Basemodel Car);
    //传入僵尸对象,若僵尸左横坐标值==手推车右横坐标值返回1,即手推车应当发射
    void CarMove(bool judge, Basemodel Car);
    //调用isCarMove函数,如果应发射,根据手推车速度改变手推车坐标值

/*游戏总操作相关:*/
    Location* TransToLocation(int x, int y);
    //接收x,y值,根据x,y值返回相应的草地块的行列数
    //主要用于点击草地时判断点击的是哪块草地
    moreLocation *TransToMoreLocation(Location location);
    //根据传入的草地的行列数,返回这块草地的左上角、右下角坐标
    //主要获取种下的植物坐标与发射出来的子弹的坐标
    void real_create_plant();
    //不实现鼠标跟随情况下的种植植物
    void GameControl();
    //游戏总操控
    bool GameOver(Zombie** zombies);
    //游戏结束判断:如果死亡僵尸数==应产生僵尸数,游戏胜利
    //调用游戏胜利界面
    //当僵尸进入房间,如僵尸左横坐标<小车所在位置的左横坐标,游戏失败
    //调用游戏失败界面
经验分享 程序员 微信小程序 职场和发展