项目背景介绍
上大学之后我一直在学习游戏开发,最开始是直接使用Easyx这个绘图库做Dos下的游戏,当时学习了C++和数据结构之后正巧有个数据结构课程设计,就心生了要做这个游戏的想法。我算是那种有想法就想着去做的人(有时候也算是缺点,因为只注重了实现),于是在那次课程设计中完成了本项目的初始版本。
在初始版本中还是实现了挺多我对游戏功能的想法,不过代码的结构、规范化、性能存在着很大的问题,在经历了一些面试之后,我觉得我是得往这方面更深入研究。在还没有拿到offer的情况下,我打算使用已经学习和将要学习的知识来改进自己写过的代码,而这个弹幕射击游戏是第一个。我对项目改进所关注的点主要在于:代码的结构、规范性,程序的性能。
版本对比
截图对比:
原始版本截图
改进版本截图
说明:从图中看,改进版本没有原始版本那么乱(其实是功能删减了很多)。
功能的改变:
想在新版本实现的功能:
插值计算 ok
轨迹管理 ok
存储记录 ok
全局信息 false
关卡管理 false
运动管理 ok
协程 false
敌人 子弹 导弹 技能 ok
游戏暂停 优化
相比原始版本未实现功能:
保存数据 false
debuff状态 false
攻击特效 false
溅射 false
敌人特性 false
玩家的升级 经验 hp 技能获取等 false
游戏提示 false
这些功能没有重新在新版本中实现是我觉得和一些其他功能有所重复,时间紧迫再实现也没多大意义。
项目结构的改变:
这次重新编写代码,仿照了Unity引擎脚本运行逻辑来设计对象模型结构,这样子游戏中出现的对象完全继承于基类方便了统一管理。相对比原始版本,消除了全局变量的做法,消除了动态对象分配的做法。
改进版本结构图1
改进版本结构图2(优化了一丢丢)
项目性能的改变:
原始版本性能参数
改进版本性能参数
说明:改进版本的性能上CPU占用减低了一丢丢,似乎没有多大影响,而且内存占用变大了,这是因为使用了对象池的原因。好了,这个只是暂时的,进一步的改进请看下面的性能优化过程。
改进遇到的问题
//遇到问题:2018年10月31日15:25:58 线段和圆的相交 感觉网上给出的方法是错误的
//解决方法:在网上实现的方法基础上进行了改进以达到要求。
//遇到问题的思考 内存池(其实应该是对象池,当时不懂) 先生成一定数量的某一类对象 但是我想生成一个敌人的内存池 敌人有不同种类
//即很多敌人类型是基础敌人基类的 该如何分配内存
//以下是解决方法:使用对象池 加定位new的方式分配对象空间 然后在使用到具体对象时在空间内创建类对象
//使用定位new在buff创建A类的对象a后会使用A类大小等同的字节来保存对象a的数据(包括虚基表)
//如果调用了显式析构函数 则虚基表会被释放 而其他的数据如果没有释放则不变
//即调用派生类的函数会调用到基类的函数 调用类成员变量则值不变
//遇到问题:2018年10月31日10:36:18 关于STL容器的迭代器尾++ 头-- 操作的问题
//那么如何在遍历的过程中进行增加删除呢
//做法:将操作转移到管理者中统一操作,因为管理者拥有迭代器成员变量记录链表操作对象,通过迭代器进行删除
//遇到的问题 从一个正在遍历的链表中转移一个元素到另外一个链表导致的链表遍历出错
//解决方法 类定义一个迭代器进行遍历操作 当回收时使迭代器--
//遇到问题 2018年10月30日20:46:33 将指向容器头的迭代器做减操作时发生错误
//包括子弹的回收 特效的回收 怪物的回收
//解决方法:添加一个迭代器成员变量,在做减操作前进行判断。
//更复杂的一个问题:2018年10月31日11:07:42 在一个容器遍历的时候可能删除自身元素 同时遍历的操作过程中
//遍历别的容器的元素并且可能把它删除 寻求解决的方法:如何在遍历时删除元素并确保遍历可以继续进行
//思路:如果没有删除 则一直遍历完成 如果删除了元素 则迭代器需要指向正确位置并判断是否可以进行步进操作
//难点:在遍历过程中是否删除元素无法告知遍历操作
//那么一个方法是可以让遍历中的执行操作返回是否删除元素 另外将遍历处理的工作移交给具备容器迭代器的对象进行
//处理 如此一来 它在处理过程中如果删除了元素 不会出现迭代器的++ --操作异常的情况
//最终选择的做法是将带有可能删除元素遍历操作专业到管理者中进行
//遇到问题的 不同的char类型之间的转换问题
//这个需要仔细看下博客。
//遇到问题:2018年10月30日18:03:45 EnemyIncubator的构造函数没有执行
//解决问题:其实是有执行 但敌人孵化器没有先构造 添加玩家时会将场景 敌人孵化器添加给子弹 空指针异常
//遇到问题:2018年10月31日19:46:16 函数执行之后不知道跳到哪里去了
//解决:2018年10月31日19:55:19 迭代器忘记了操作遍历
//遇到问题:2018年10月31日20:02:02 lambda表达式传递过来的值没有发生过变化
//解决:不是传过来的值没有变化,而是if语句之后添加了;导致每次返回了相同值
//特效的管理 无论是玩家管理还是场景特效管理者管理 都需要让特效获取指针执行管理
//为了让职责划分更清晰 所以才有场景特效管理者管理
//另一种思考:将子弹和特效绑定在一起管理 不过这种的话职责不清晰 而且管理不方便
//比如在特效产生前特效该干什么 子弹消除后子弹该干什么 假如什么都不干是否需要清除出队列
//遇到问题:如何将lambda表达式存储起来,供后面调用使用
//已经解决: 发现编译器的缺陷问题 类的模板函数目前还无法分文件实现
//又产生了和Bullet的相互调用
//进行尝试:2018年11月1日11:11:54 将会追踪的子弹添加到游戏中
//先测试数学几何模型
//问题:在写好场景转换之后遇到了Scene类无法识别的问题,导致gamemanager也出现报错
//A B相互包含 C继承B 然后在C中报错 C没有定义基类
//解决 使用指针 在需要调用GameManager类方法的源文件中添加GameManager的头文件
//遇到问题List的释放 后来发现其实不是不是这个问题 而且切换场景后没有重新调用GameManager的Run方法
//导致了继续执行原来场景的Updatae方法出现错误
性能优化过程
在完成改进版本功能编码之后对其进行性能优化,首先我们来看它现在的性能。首先设定:循环延时17ms(即保持每秒59帧这样子),CPU占用10.5%左右,内存25m左右。然后我们将循环延时去掉看最强的帧率能达到多少,帧率142左右滑动,CPU占用24.5%左右滑动,内存占用还是25m左右。
然后我们开始对在循环延时17ms的情况下进行优化:在主循环中sleep函数的调用占到了30%的时间,在绘制界面的函数中putimage函数是个大的性能花费,但是这里对它的改进不是我的主要工作,所以把背景给出掉。然后CPU占用下降到了4.2%左右(一个函数就这么夸张)。接下来我们看下我们最多能将CPU占用下降到多少,我在主循环中只保留了延时函数(17ms),好的,如果程序什么都不做,那么CPU占用为0.15这样子,说明我们现在还有很大的优化空间...接下里我们主要研究战斗场景的各函数花费占比,然后优化他们。我们先将开始游戏场景设置为战斗场景,这样子性能分析工具只测试部分调用的函数,避开了由于lambda表达式调用无法显示正确调用函数占用时间。好了,现在在游戏主循环中sleep函数的占比已经占到了42%了(几乎就用于睡觉了),其他函数调用微乎其微(就是我写的对象逻辑执行占用时间太少了)。Ok,那我的性能优化到此结束....好吧,开个玩笑,虽然现在逻辑运行占用时间少,但是还是有优化空间,当我们增大游戏中的怪物数量时,这个占用时间会慢慢升上去。把子弹,导弹,敌人的最大数量都调到10000000,每次产生的数量增大到1000(原本是1),我们现在再来看下各函数调用所占用时间...好吧,这玩笑开得有点过头了,我运行之后然后就去玩手游了...等了几分钟,终于等到了游戏分配好内存进入战斗,然后电脑游戏卡得实在是不显示了,而且性能分析工具在结束分析之后的很长一段时间没有给出分析结果...可能我得往下调整一下数量。我们还是把基数增长为100倍吧,不然这个对象池分配内存一开始耗费的时间太长了。对象数量设置为10000,每次产生的子弹、导弹、敌人为100,再进行测试。好吧,这样子也是界面卡得很慢,觉得不应该啊,这样子就承受不住了,先来看下性能分析的报表内容。这一次sleep函数调用占用为0.17%,说明根本没有时间睡觉了,进一步发现在玩家更新中,玩家所发射的子弹、导弹跟敌人的碰撞检测非常耗费时间。然后我直接运行程序发现是可以运行,但是随着敌人数量的不断增加,FPS逐渐减低到了1,然后基本上就是每2s移动的画面。为什么性能分析工具不用运行呢,带着疑问我继续进行测试,原因可能是性能分析工具本身需要占用一些资源,用于进行性能分析有点困难。照目前情况来看,100倍是ok的,稳定59帧率。重新搞成1000倍试一下,好吧,还是卡死,我想,如果能够在1000倍都正常运行,那可能是不错的。先回到10倍进行一下性能分析测试,发现在敌人的绘制函数中,名字的绘制是挺耗费时间的(相比其他几何图形绘制)。现在有了个疑惑,因为,发现了在游戏中子弹触发的碰撞检测要比导弹触发的碰撞检测要多,大约是子弹的四倍,但是在场景中导弹的数量要比子弹多,我们先来看触发碰撞检测部分代码。子弹的移动时间间隔也就是进行检测的时间间隔,但是子弹的移动是直线的,所以可以将移动时间间隔改大一些,然后发现导弹的移动时间间隔更小,这就更加让我疑惑,究竟是咋回事...然后修改一下倍数之后发现变回来了,导弹的碰撞要比子弹的多,但在碰撞检测中,调用敌人的碰撞检测函数,函数内部调用时间过长,得想办法消除部分花费时间,想法是直接获取敌人的位置信息然后使用碰撞函数判断。这样子修改之后确实有了较大的提升,并且我还将敌人的名字给隐藏了(这个绘图的相关优化我就不搞了)。然后将倍数改为了200发现是运行ok,但是FPS一跳一跳的,这个可能跟每2s发生一次导弹有关系,如果想稳定应该是去除发射导弹(当然游戏是需求的),我们先这么试试看FPS是否稳定,ok,不发射导弹FPS的跳动不大,上下跳动大概为2-3帧这样子,加导弹跳动不稳定,因为这导弹的数量基数挺大的。接下来设定最大对象为10000(比较小),但是产生倍数设置为500倍,然后,好吧,很快就万人同屏了,根本跑不动。还是设置个比较合理些的数字吧,设置2000这样子最大数量(更加小了),然后产生倍数还是为500吧(很快就可以达到上限了),接下来看效果,发现敌人数量产生了太多不太对劲,好吧,操作过程有点问题,没有修改到敌人的最大数量,现在设置它为2000,咦...敌人的数量还是有点迷,不会是哪里写错了吧...我决定要显示一下他们的数量出来。妈呀,居然产生了一万多的敌人在同一屏幕,怎么可能不卡呀...奇怪的是都没有设置那么大的对象池呀,这些敌人究竟是怎么产生的呢。带着这样子的疑惑先来看下子弹和导弹的产生是否会超越限制吧,emmm....经过排查发现其实敌人是没有超过2000的限制的(原来我使用的计数方法错误了)。那么目前来说就是,程序无法支撑2000个怪物同屏(起码来说很卡很卡),那么经过优化是否可以达到效果呢?应该如何进一步优化呢?我们可能还是得查看性能优化工具,发现调小了倍数之后发现,其实游戏逻辑执行已经很ok了,倒是渲染方面占用了不少的CPU(这块我就不做优化了)。
接下里进行的是循环无延时的优化,我首先将循环延时给去掉,设定最大对象2000,倍数20,然后CPU占用为24.5%,内存占用25m,FPS:240左右这样子。改回来到倍数1看看,CPU、内存都差不多,FPS为380左右。现在把倍数改成20然后使用性能分析工具进行分析,结果发现,其实优化空间不大,主要还是受到了渲染的限制。
所以得出结论是,目前本项目可以容纳的游戏对象数量主要还是取决于渲染。假设减少渲染速率(就是添加循环延时,一般设置为60FPS每帧),循环中主要占用时间的是sleep函数和绘制函数,当敌人数量增加到一定程度时就会卡,如果想容纳更多的敌人,则需要优化渲染(这个我就不深入研究了)。其余的和敌人的碰撞检测也是非常耗费时间,想要减少这部分时间耗费可以减少检测或者优化检测。
资料以及请求
项目github地址:https://github.com/huanlingkeji/BarrageGame
项目gitee地址:https://gitee.com/huanlingkeji/BarrageGame
其他的参考学习资料
Git的学习推荐以下两个网站:
https://www.liaoxuefeng.com/wiki/0013739516305929606dd18361248578c67b8067c8c017b000
https://learngitbranching.js.org/?NODEMO