找回密码
 立即注册
查看: 433|回复: 3

【游戏开发心得】C++游戏《飞机大战》开发心得

[复制链接]
发表于 2022-6-28 07:28 | 显示全部楼层 |阅读模式
【摘要:】

前段时间用C++制作了一个游戏《飞机大战》,本文介绍一下这款游戏的制作心得、经验和感悟;
这款游戏是从0开始写代码的,并没有依赖于第三方库,其实真正要开发这种2D游戏,通常是需要基于第三方库,比如easy2d,这样更节省时间、写的程序也更不容易出Bug。而这款游戏主要是为了训练自己的C++的驾驭能力、常用技巧的使用,所以是从0开始全手写;
本游戏制作过程中参考了网上的一些案例,但是比网上所有已知的案例功能更丰富,大部分功能是自己增加的;
由于代码太多,本文涉及到代码的地方都用截图展示,不直接展示代码;
全文约10000字,有大量图片,可能是截图格式的原因,图片可能有点模糊,还请见谅;
【游戏基本信息:】

游戏名:飞机大战
游戏版本:V1.0
作者:CODspielen
开发日期:2022/05/29
开发环境:Windows 10 Visual Studio 2022
开发设备:联想Y50-70
【游戏玩法:】

操控主角飞机,击杀敌机,达到一定数量后进入Boss战,击杀Boss,游戏通关,击杀数被记录到积分榜中;
如果中途被击杀,则减一条命,如果所有命都用完后仍被击杀,则游戏失败;
游戏失败后,击杀数被记录到积分榜中,可选择重新开始或退出游戏;
【游戏功能:】

1,主菜单功能:开场界面、鼠标点击按钮、多层菜单、背景音乐、查看记录、操作说明、关于游戏、退出游戏、关卡选择(目前只做了一个关卡)、难度选择,其中查看记录会按照击杀数排序;
2,可选择手动控制或者自动驾驶,手动控制就是玩家自己控制主角飞机进行游戏,自动驾驶就是让主角飞机自动进行游戏;
3,可选择自动开火或者手动开火;
4,主角飞机可用鼠标或者键盘进行操控,键盘灵敏度可调节;
5,可选择敌机开火模式为随机单架开火或者全部开火;
6,火力等级随着击杀数增加而增加,一共有三个火力等级;
7,击杀数达到一定值时,会进入Boss战;
8,主角和Boss有血量设定,而普通敌机设定为一击必杀;
9,根据难度不同,主角、Boss血量、攻击力、敌机数量都有不同;
10,有暂停菜单,暂停时可选择重新开始、继续游戏或退出游戏;
11,主角、敌机、Boss被击杀时有动画效果;
12,游戏非Boss战阶段和Boss战阶段,有对应的背景音乐,主角、敌机、Boss被击杀时有对应的音效;
13,有UI功能,UI显示击杀数、血量、生命数等信息;
【游戏操作:】

主菜单:鼠标点击
游戏中:
暂停:ESC键
移动:方向键
开火:空格键
暂停菜单:
继续游戏:ESC键
重新开始:Enter键
返回主菜单:Backspace键
【引用图片和音乐:】

主菜单背景图:《杀手6》
主菜单音乐:《最终幻想7重制版》Tifa's theme
战斗音乐:《最终幻想7重制版》战斗BGM
Boss战音乐:《最终幻想7重制版》战斗竞技场BGM
主角阵亡音乐:《超级马里奥》BGM
通关音乐:《最终幻想6》胜利号角
//这些游戏是本人最喜欢的游戏之一
【代码的整体架构:】

将整体代码设置为几个模块:游戏进行、主菜单、背景图片、主角、敌机、子弹,其中主角、敌机和Boss写成继承,由一个飞机类基类派生;子弹分为主角子弹、敌机子弹、Boss子弹,也由一个子弹基类派生;增加一个配置文件,用于调整游戏窗口大小、开火模式、键盘灵敏度、自动驾驶开关、按键设置等;
在这个架构下,模块之间耦合程度相对不高,游戏功能由简单到复杂,都比较容易制作,增加功能不容易造成混乱,修改Bug也比较容易定位问题;
模块与模块之间的关系如下:
1,头文件和CPP文件:



2,头文件包含关系:



3,说明:

iostream:输入输出流,C++头文件;
algorithm:算法头文件;
graphics.h:图形库头文件;
drawImg.h:图片处理头文件,自己写的,非标准库,用途是对图片进行无背景贴图,标准库graphics.h中没有这项功能;
mmsystem.h:播放音乐的头文件;
winmm.lib:多媒体设备接口库文件;
ctime:计时用的头文件;
conio.h:键盘输入操作用到的头文件;
vector:vector容器头文件;
vkCode.h:虚拟键码,程序中并未被包含,只是为了方便使用虚拟键码;
config.h:配置文件,用于调整游戏窗口大小、开火模式、键盘灵敏度、自动驾驶开关、按键设置等;
PlaneBase.h:飞机类的基类,派生类是主角飞机、敌机和Boss;//子弹的基类直接写在Bullet.h中,不单独写一个头文件;
MainMenu.h:主菜单;
BK.h:背景图片;
Hero.h:主角飞机;
Bullet.h:子弹,包括子弹基类、子弹的派生类(主角子弹、敌机子弹和Boss子弹);
Enemy.h:敌机,包括普通敌机和Boss;
FlyingGame.h:游戏流程控制,包括游戏初始化、敌机生成、主角敌机与Boss开火生成、游戏进行、游戏结束、游戏通关、游戏暂停、UI显示;
main.cpp:程序主线程;
【游戏素材:】

素材为jpg或png格式的图片,以及mp3格式的音乐。其中jpg图片用于主菜单背景,为静态显示;png图片为无背景图片,但是如果要在游戏中实现无背景贴图,不能直接用putimage(),要用自己写的贴图函数drawImg();
由于只制作了一个关卡,所以并不是所有素材都被用到;


【主函数main.cpp:】

进入main()函数中,首先是创建游戏窗口,有两种创建方式,一种是不带控制台窗口的,另一种是带控制台窗口的,在开发调试过程中,需要用到控制台窗口,会加一些打印数据。在正式发布时,不需要用到控制台窗口;
整个程序由两层while循环控制,外层是一直循环,直到游戏中触发了退出程序才退出,而内层循环的条件是游戏运行的标志位m_bplay是否为true,为true则进行内层循环,说明游戏正在运行,为false则退出内层循环,说明游戏结束,但是会进入主菜单,因为外层循环还在运行。通过这两层循环,可以控制整个游戏,是运行游戏还是运行在主菜单界面;


【配置文件config.h:】

这里设置了一些可以配置的参数,以满足不同玩家的需求,包括窗口大小,自动驾驶开关,主角开火模式,敌机开火模式,键盘灵敏度,按键设置,这些参数在后续的程序中被调用;


【无背景贴图drawImg.h, drawImg.cpp:】

这里涉及到C++的图片处理算法,不是本游戏开发的重点,不进行阐述。只展示.h文件。后续代码中需要无背景贴图的地方,用drawImg()替代putimage()就可以;


以主角飞机为例,图片背景在看图软件中打开是这样的,能看出本身是无背景的,但是如果直接用putimage()贴图,在游戏中这些无背景的地图会显示为黑色;


使用drawImg()后就不存在黑色了,如图,在游戏中是这样的:


【主菜单MainMenu.h, MainMenu.cpp:】

主菜单代码主要分为四部分,先是播放背景音乐和载入开场界面,然后是文字和按钮区域的设置,最后是跟踪鼠标动作,判断鼠标是否点击在某个位置,根据点击的位置和菜单层级进行下一步动作;


1,播放背景音乐:

需要注意的是只要音乐资源放在同级文件夹的指定位置,并且格式、命名都正确,那么这里可以不使用绝对路径,直接写./res/menu_music.mp3就可以,这样在客户把游戏更换了路径后,也能直接读取到资源,不需要在代码里改绝对路径。后面的alias menu_music是起别名,在后续对这个音乐文件操作的时候可以直接使用这个别名,更加方便,比如下面的play menu_music。后面的repeat是让这产音乐单曲循环播放的意思;
在其他载入图片的地方,也是基于这个方法;
2,开场界面:

由一个标志位beginInfo控制是否进入开场界面,退出后标志置为false,只有在启动游戏时才需要进入,后续都不需要;
在这里需要加BeginBatchDraw()和EndBatchDraw(),不然会出现闪烁或者不显示;


开场界面:


3,文字和按钮区域的设置:

这里主要是调整好文字、按钮在整个窗口中的位置,同样也要加BeginBatchDraw()和EndBatchDraw();在设置获取矩形长和宽之前应该先settextstyle(),把字体大小确认,不然长和宽会一直变化;
主菜单界面:


4,跟踪鼠标动作:



用一个while (true)持续获取鼠标输入,并且屏蔽触发暂停的按键,如果这里不屏蔽,后续再次进入游戏中会直接暂停住;
用枚举变量区分菜单层级,在不同层级点击需要做出不同的响应,并更新层级变量;


在切换层级时,一开始会出现连续点击多次,导致连续切换多个层级的现象,后面加上这个延时函数,并且在其中屏蔽鼠标动作,就解决了。每次切换层级时调用,就不会判定鼠标连续点了多次;


查看记录、操作说明、关于游戏,点进去后只有一层,只能点返回回到主菜单,这几个分支主要是显示相应的文字:






其中查看记录会按击杀数的降序排列,用到的是vector的sort()函数:


并且要调整好显示的位置,如果超出了屏幕下方就不再显示:


降序排列:


如果点击的是退出游戏,则会让用户再确认一次:




如果点了开始游戏,那么先进行关卡选择,当然这里只有关卡1,后面两个关卡没有制作,点击了也无效:




点击关卡1,进入难度选择界面:


点击想要的难度,然后会让用户再确认一下是否用这个难度开始游戏:


如果点击开始战斗,就会进入游戏,如果点击取消,则返回上一步,点击开始战斗后需要更新两个标志位,m_bplay为真,后面return,就会进入main()函数中的内层循环,也就是游戏正式开始;同时也要停止主菜单音乐:


主菜单部分就介绍到这里,还有一点要注意,就是创建文本变量尽量都写到.h文件中的类中,作为成员变量,尽量不要写在成员函数中,这样可以让成员函数的代码更简洁。同时大量重复的动作,尽量封装成函数,比如显示文字、显示矩形框,也可以减少重复代码量;
【背景BK.h, BK.cpp:】

背景部分主要实现的功能有两个,一是背景的静态显示,二是动态滚动;
由于背景图片设置为拉满整个窗口,所以不需要无背景贴图,直接用putimage()即可;
对于主菜单背景,用静态显示;对于游戏内的背景,需要滚动,循环显示,这里的y+=4就是让每一次循环图片向窗口下方移动4个像素,同时背景图片要做成高度为两位于窗口高度的图片,这样滚动起来就不会产生空白,看起来就是连续的;
游戏中的背景,在不同的关卡中通常会用不同的贴图;




【子弹Bullet.h, Bullet.cpp:】

先写一个基类,因为派生类要在堆区new出数据,所以加上了纯虚析构,让派生类在析构时可以释放堆区的数据。这里把主角、敌机和Boss的子弹中通用的部分放在了基类中,通过继承的方式使用:


1,主角子弹:



构造函数中传入主角飞机的矩形,因为主角子弹是由主角矩形位置决定的;
成员变量都写成私有,对外提供接口,防止误更改;
主角子弹有火力等级的设定,1就是只发射一列子弹,2就是发射3列子弹,3就是发射5列子弹;
左右的子弹也在构造函数中进行位置初始化;
这里还有一点要注意,&img用的是引用传递,因为要改变实参img里的值;
引用其实就是起别名,操作的还是同一块内存空间里的数据,用指针传递也是同样的效果;如果是值传递,会拷贝一份数据作为形参,在函数里操作的是另一块内存里拷贝出来的数据,原内存里的实参就不会被改变;在本代码中,其他需要改变实参的地方,都用引用传递;
游戏设定为根据击杀数自动更新火力等级,所以需要把击杀数传入函数进行判断;
这里传进来的kill用于给this->m_menu.m_kill赋值,不需要改变实参,所以不用引用传递;因为this->m_menu.m_kill和传进来的kill是两个不同的类中的对象,所以需要通过赋值来更新this->m_menu.m_kill;


三个火力等级下的子弹形态:


主角子弹的速度会根据游戏难度进行调整,难度越高速度越慢;
如果子弹已经超过屏幕上方,要return掉,不然一会浪费系统资源,二会导致敌机刚生成就被子弹干掉了。因为敌机是在屏幕窗口上方生成的,再向下移动进入窗口,如果主角子弹超出屏幕不return,那么会击中窗口上方刚生成的敌机,会导致击杀数快速增大,不符合常理;
这里同样需要传入diff进行赋值,因为是两个不同的类中的对象;


2,敌机子弹:

同样由敌机矩形位置决定,需要传入敌机矩形范围:


敌机子弹速度固定,不与难度相关;但是要注意,敌机子弹速度方向和主角是反的,是向屏蔽下方移动的,同时还要加上敌机自己移动的速度;
同样,超出屏幕下方的子弹要return掉;


敌机子弹的形态:


3,Boss子弹:

Boss子弹的设定是左右两排,并且速度与难度无关:


需要注意的是,Boss子弹的也要加上Boss的纵向移动速度,Boss的设定是在窗口的一定范围内随机移动,方向涉及上下左右,如果是向下移动,则Boss速度保持默认,如果是向上移动,Boss速度改为负值,如果是左右移动,Boss速度改为0;


Boss子弹的形态:


【飞机类PlaneBase.h, Hero.h, Hero.cpp, Enemy.h, Enemy.cpp:】

先写一个飞机的基类,因为三个派生类(主角、敌机、Boss)都有爆炸动画,所以这个函数写成纯虚函数,用多态的技术进行调用,同时也加上纯虚析构,让派生类在堆区的数据可以被释放,其他可以继承的变量和函数都写进基类中:


1,主角:

通过构造函数,初始化位置为窗口下方正中央;
主要的成员函数有载入图片、主角爆炸、控制、碰撞检测,其中控制分为手动控制和自动驾驶;
生命值初始化为3条,后续会根据难度进行赋值;


载入图片,使用无背景贴图:


主角爆炸动画:
这里要连续载入几张爆炸的图片,覆盖上一帧的图片,形成爆炸效果,这里使用了一个技巧,在一个for循环里,使用wsprintf()函数,来载入爆炸图片,可以使代码简洁,不用写重复的载入图片的代码;
每次drawImg前后要加上BeginBatchDraw()和EndBatchDraw(),不然不会显示图片;
每次循环后加上一个延时,防止显示太快看不到爆炸过程;


两个矩形碰撞检测:


手动控制:
主角的控制支持手动控制和自动驾驶,手动控制支持鼠标控制和键盘控制;
鼠标控制就是直接获取鼠标的xy坐标,鼠标移到哪,主角飞机就移到哪;
键盘控制就是用上下左右来控制飞机,这里要用到配置文件里的键盘灵敏度,越大移动越快。


自动驾驶:
自动驾驶暂时只采用较为简单的算法,后续如有需要再开发更为复杂和精确的算法;
设定为让主角飞机只进行左右方向移动,暂不进行上下方向移动,目标是攻击敌机并尽量躲避子弹;
由于此部分代码较多,不进行截图展示,只介绍算法原理:
先根据敌机的位置,计算出目标位置,也就是主角要移动到的位置,这个位置是用于攻击敌机的;
如果是非Boss战,就在出现的敌机中随机选择敌机,选择其中间位置进行追踪;如果是Boss战,就选Boss宽度的中间位置进行追踪;
然后定义左中右三个矩形区域,用于和敌机、Boss、子弹进行碰撞检测;


然后根据目标位置与主角的相对位置,决定是向左还是向右移动;
然后根据三个矩形反馈的碰撞检测结果,决定是否要避让敌机或子弹;
这里用到了几个状态机:
追踪状态有三个:向左追踪、向右追踪、保持不动;
避让状态有三个:向左避让、向右避让、保持不动;
追踪状态的移动速度较高,因为需要快速去攻击敌机;而避让状态的移动速度较低,因为只需要避开子弹或敌机就行了;
综合状态机,让主角的移动;
经过参数调试,主角的攻击欲望较高,可以快速去追踪并攻击敌机;同时能较为灵敏地躲避敌机和子弹;
参数的调试非常重要,直接影响着主角飞机的行为,可能会出现攻击欲望不高、或者躲避较为迟钝、过于“勇敢”或者过于“谨慎”的情况。
无论是手动控制还是自动驾驶,超出屏幕后要进行修正:
这里设定为不超过上方10个像素的位置,不超过左右边界半个主角宽度的位置;


至于为什么是半个主角宽度,因为这样可以让主角子弹击中靠近窗口左右边界的敌机,增大攻击范围:


2,敌机:

敌机在本关卡中只设定了一种,如果游戏有后续其他关卡,可以增加种类,种类越多用继承和多态的优势就越能体现出来;
敌机的行为非常简单,就是向屏幕下方移动,并且发射一列子弹;
设定为一击必杀,和主角一样也有爆炸动画、也是用多态来写;


敌机飞出屏幕也需要return,和子弹一样;敌机的速度设定为和游戏难度相关,越难移动越快;


3,Boss:

Boss的行为和敌机类似,区别是移动方式;
增加4个状态机,对应4个方向的移动;


定义Boss的移动范围为上半个窗口区域,同时当Boss触碰到此区域的四个边界中的任意一个时,给其随机地赋值一个移动状态机,这样Boss的移动行为就是在这个区域内进行随机的上下左右移动;
当然,这是比较简单的设定方式,如果后续有需要,可以开发出更加复杂的移动行为;
Boss的移动速度也和游戏难度相关:


【游戏流程控制FlyingGame.h, FlyingGame.cpp:】

是整个游戏的核心部分,包括游戏初始化、敌机生成、主角敌机与Boss开火生成、游戏进行、游戏结束、游戏通关、游戏暂停、UI显示;上述所有模块,除了主菜单,都是为这个模块服务的;
类中定义了一些标志位,用于控制游戏的流程;


1,初始化游戏:

用于初始化游戏数据,调用时机是在游戏进行函数刚开始后;
先设置背景颜色,用于后续的暂停、游戏结束和游戏通关画面的文字显示;
然后载入游戏背景音乐;
然后是根据难度进行配置,包括子弹速度、同屏敌机数量、主角血量、Boss血量、主角掉血量、Boss掉血量、主角生命数等;


最后这里把生命值赋值给函数参数InitHealth,这里函数参数都用引用传递,保证实参也能被修改;这个InitHealth在这函数里被赋值后,会输出到UI显示函数中,用于实时显示主角生命值;


2,生成敌机:

用一个vector容器储存同屏中生成的敌机,这里把数据开辟在堆区;
//由于一开始没有写成多态,所以这里没有用基类指针指向派生类对象的方式,后面改成多态后其实这里也应该改的,但由于改动量较大,这里暂时不改,只记录一下; 而且在本游戏中,多态只用到了一个成员函数Down (),多态的优势体现的并不多,其实在本游戏中主角、敌机、Boss可共用的成员函数并不多;
//如果我将来开发下个作品,一开始就可以写成多态;
这里使用随机数种子,让生成的敌机随机地出现在屏幕的位置;
为了避免敌机出场时互相有重叠,这里需要判断一下敌机之间是否会有重叠,需要用到两个矩形碰撞检测的函数;
同时,这里使用了C++11的写法,基于范围的for循环,使代码更简洁;


3,开火:

主角开火:
bsing是个计数器,用于限制开火频率的,每次达到一定值时触发开火,然后重新归零;
如果是自动开火,就创建一列子弹,存储到一个子弹容器里;由于主角只有一个,所以创建的子弹只有一列,从主角飞机前方发射;
如果是手动开火,还需要先检测一下键盘是否按下开火键;
关于开火的音效的处理,如果用mciSendString函数open了音乐,那么只会播放一次。在open之前先close一下,就可以实现每发射一颗子弹,就播放一次音效;


单架敌机开火:
思路和主角开火一致,由于这里传进来的es是生成的敌机中的随机一架,所以游戏效果就是随机一架敌机开火,这样可以显著降低游戏难度;




全部敌机开火:
与单架开火的区别是,这里传入的敌机是一个vector<Enemy*>容器,也就是每一帧同屏的所有敌机;
同时,这里也要对这个es进行for循环遍历,让每一架敌机都创建一列子弹,效果就是所有敌机都开火;




Boss开火:
与主角开火类似,给Boss创建一列子弹;


4,游戏结束:

游戏结束意味着游戏失败,没有通关,触发时机为主角耗尽了所有生命数和血量;
函数内部,第一部分为音乐的处理,先停止背景音乐、子弹音效、击打音效等,然后播放主角阵亡的音效;
第二部分是把传入的生命值传到UI函数中,因为被击杀后还要显示准确的生命值为0,如果不传,显示的不是0;


第三部分是文字显示,把一些信息显示到屏幕上,注意需要加上BeginBatchDraw()和EndBatchDraw(),不然不会显示任何东西;


第四部分是把击杀数存入积分榜,把标志位归零和复位;


第五部分是等待用户的键盘输入,分为重新开始和返回主菜单,需要停止相应的音乐和音效;
重新开始和返回主菜单的区别是,m_bplay这个标志位,在重新开始里要重新设置为true,这样在退出这次大循环后又会进入这层循环,否则就会退出这层循环,进入主菜单函数;
注意,需要屏蔽掉ESC键,ESC键是暂停键,不屏蔽的话暂停功能会异常;


游戏结束画面:


5,游戏通关:

游戏结束意味着游戏完成整个关卡,触发时机为主角击败了Boss;
函数内部和游戏结束差不多,这里只说一下有区别的地方;
在文字显示方面,如果主角的生命值是满的,则显示“恭喜无伤通关”;


游戏通关画面(这里只是为了快速调出通关画面,所以击杀数为4,正常设定为根据难度决定击杀数):


6,游戏暂停:

游戏暂停意味着游戏中的所有可视元素都停住,但是背景音乐要继续播放,同时要求能够接收玩家的选择,根据选择执行是继续游戏、重新开始还是回到主菜单;
游戏暂停触发的时机是在游戏进行中任意时间,玩家按下了ESC键;
暂停函数和游戏结束以及通关内容差不多,因为要实时接收玩家的按键,所以要用_kbhit()和_getch()的组合;
游戏暂停画面:


7,显示UI:

UI是实时显示游戏信息的,包括主角生命数、血量、击杀数、Boss生命值、是否是自动驾驶等信息;
所以需要传入主角对象以及Boss的生命值(传Boss对象也可以);
调用的时机是每一次循环里调用一次,以保证实时更新数据;
需要注意的是调整好各个信息的位置,设置好字体大小和颜色;


另外在主角生命值低于20时,让生命值显示为红色:


8,游戏进行:

这部分是整个游戏最核心、最主要的部分,涉及到整个游戏流程的控制,游戏机制,玩法,关卡设计等待,放在最后讲;
第一部分是准备工作,载入图片,定义变量,调用游戏初始化函数,初始化游戏参数;
注意,这里在创建主角和Boss时,尝试使用了智能指针,也可以用普通指针或者不用指针;智能指针的好处在于会自动释放数据,不用手动释放;
然后是添加敌机,让敌机在屏蔽上方生成;


然后是进入一层while()循环,循环的条件是m_bplay标志位为true;这里会展示整个循环内部的架构;
暂停菜单的调用放在一开始的位置,然后是显示背景、载入主角飞机、控制主角飞机,显示UI,都放在这里;
然后是主角开火、所有敌机开火;


然后是遍历所有敌机,再放一层while()循环,用一个迭代器去遍历敌机的容器;
首先是让每个敌机和主角的子弹进行碰撞检测,实现击杀敌机的过程;
然后是单架敌机开火,放在这里;
然后是对飞出屏幕的敌机进行处理;
退出这层while()循环后,如果还没进入Boss战,就补充被击杀的敌机,让同屏的敌机数量一致;
如果击杀数达到200,就进入Boss战环节;这个200也可以设定成随着难度而变化,这里简化为固定值200;
最后部分,一个是调用游戏结束函数,一个是调用游戏通关函数;
这样的架构,在前面的循环中通过标志位的更新,只要退出循环,一定能进入后面的通关或者结束分支,也就是如果玩家不手动退出,游戏进行下去,一定会要么通关、要么结束,不会有无限循环或者其他可能;


整体框架描述完后,对一些重点地方谈谈心得:
首先是暂停菜单:
_kbhit() 和 _getch()结合使用,监测玩家是否按下了ESC键;
然后调用暂停函数,暂停函数中会更新游戏状态标志位gameStatue,也就是根据玩家的选择,决定是要继续游戏、重新开始还是返回主菜单,后两者都需要先关闭音乐和相应的音效,存入积分榜,更新标志位,只不过退出游戏需要让m_bplay置为false,达到退出游戏的目的;


另外,在这里手动判断一下各个容器,如果不为空,则delete里面new到堆区的变量,同时清空容器;这里手动释放是为了防止容器vector溢出报错;


主角开火部分,需要把超出屏蔽的子弹delete掉,这里很容易造成容器溢出,vector容器使用erase后,迭代器会指向下一个元素,如果正好这时候已经指向end()了,这一次循环后迭代器再++,那么就会溢出;所以delete后要写成i = bs.erase(i),并且如果i != bs.end(),再让i++,这样就不会溢出;


在全部敌机开火部分,直接进行敌机子弹和主角碰撞检测,每击中一次,播放一次被击中音效,同时让主角掉血,同时释放这颗子弹;
如果主角血量已经掉到了0,就减一条命,调用主角爆炸函数,如果生命数不为0,就重置主角的血量为满值;
如果命也减到了0,说明主角被击杀,就让m_bplay等于false,这样就会进入后面的游戏结束函数的调用;
这里同样要注意避免容器溢出;
后面单架敌机开火部分逻辑类似;


同样,如果主角和敌机碰撞了,也要进行类似的逻辑,只不过碰撞后敌机直接删除,判定为一击必杀,而主角掉血量为被子弹击中的两倍;


还要把每一个敌机和主角的子弹进行碰撞检测,设定为一击必杀,逻辑与上述类似;
只不过主角的子弹分三个等级,每个等级都要考虑到;


对飞出屏幕敌机的释放也要避免容器溢出;


补充敌机,这里是AddEnemy()函数唯一被调用的地方;


进入Boss战后,首先是更新标志位,同时切换背景音乐为Boss战的音乐;
然后清空屏幕上现有的敌机和子弹,需要手动释放,不能只clear容器;


然后是主角与Boss碰撞检测、主角子弹与Boss碰撞检测、主角与Boss子弹碰撞检测,逻辑和注意事项与上面类似,就不再阐述;
最后是游戏通关和游戏结束的函数调用,为了保证程序的稳定性,在Finish()函数或Over()函数调用后,再次手动释放一遍堆区的数据,因为程序运行到这里,这些数据在这一轮循环中肯定不需要再用了;
【总结与感悟:】

本人是一名单机游戏爱好者,因为转行学习了C++,这也是第一次编写游戏项目,水平有限,如果有不正确或者不完善之处,欢迎纠正和指出;
整个工程写了将近两周,利用业余时间和周末,整个代码量大概是2860行,虽然看上去是个小游戏,并且关卡只有一关,但是要加入如此多的功能,还是很复杂的;
整个游戏编写过程中,遇到了很多问题,一开始只是写了个简单的框架,没有主菜单,只有很单一的功能,但是后续加各种功能的过程中并没有感觉到太混乱或者太难,我觉得架构的搭建也起到了关键的作用,包括游戏进行函数play()内部的架构搭建,为后续调试程序、解决问题节省了很多时间,让模块与模块之间尽可能解耦的好处就是定位问题和修改代码比较方便,后续扩展内容也很方便;
游戏编写过程中,用到了C++11里的一些特性,还有一些要大量重复使用的代码,尽量封装成函数,同时还使用了一些技巧,目的都是尽量让代码简洁、结构清晰,同时强化了一下C++11的语法的应用;
当然,整个代码还是有很多可以优化的空间,比如从一开始就可以引入多态,而不是后面再改,等等;
感觉难度最大的地方不是分析或修改某个问题,而是设计自动驾驶算法部分,虽然这个游戏中的自动驾驶用的是较为简单的逻辑,但处理自动驾驶的代码量仍然相当可观;当然这部分也是成就感最大的;
下次如果还有机会写游戏,我会尝试基于easy2d三方库进行开发,这样更节省时间、写的程序也更稳定;
如果有机会,下次可能会写一个赛车游戏,加入更加真实和复杂的自动驾驶算法,甚至如果有条件,可以考虑引入虚幻引擎进行开发;
下个游戏项目中再见。
<hr/>//by CODspielen

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
发表于 2022-6-28 07:31 | 显示全部楼层
[捂脸]我用的unity,c++学起来有亿点难度
发表于 2022-6-28 07:35 | 显示全部楼层
好厉害,也挺费精力的
发表于 2022-6-28 07:41 | 显示全部楼层
挺好啊 用c加加写游戏
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-26 05:32 , Processed in 0.134278 second(s), 23 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表