又菜又爱玩,是我对自己游戏生活的总结。玩网游怕被队友喷,玩3A又打不过BOSS,还是安安静静的玩一玩小游戏吧(当然,我玩小游戏也菜......)。我喜欢小游戏的另一个原因是学习成本比较低,你可以很快地了解游戏规则并上手,而不用去阅读大量的规则,然后探索打怪升级。除了玩游戏,我也热衷于开发游戏,做过一些小demo自娱自乐。我觉得制作游戏是一个很有趣的创造过程,你可以制定自己的规则,在自己制作的小小游戏里,你就是控制一切的主宰(然后被自己制作的游戏虐哭......),我想有谁不喜欢这种感觉呢?(等等,不会只有我吧)。今天给大家分享我使用飞桨预训练模型应用工具PaddleHub制作了一款体感小游戏——《决战二仙桥》。在游戏中,我们不再使用键盘鼠标,只使用头部来控制主角二仙桥大爷来躲避谭SIR的追踪。当然,这样一款游戏也可以让我们活动起来。闲暇时间来一局,轻轻松松预防颈椎病。
效果展示
详细介绍
第一版: 人像细粒度分割版本
在一开始,我的想法是做一个类似于“是男人就坚持一百秒”的小游戏,实质上就是人脸在场景里躲避子弹,而人脸的部分则是通过摄像头实时获取的图像,然后进行人像分割的结果。(对,就是这么懒,就是不想用手来玩游戏)。分析一下场景里只有两个元素——我们控制的Player以及飞散在场景中的Enemy,需要处理的事件则只有Enemy与边界、Enemy与Player之间的碰撞检测。
秉持着一切从简的原则,项目没有使用Pygame,而单纯地使用OpenCV。用OpenCV显示图像是最基础的功能,而碰撞检测可以通过各种mask的叠加来判断,从而避免了使用OpenCV和Pygame之间各种格式转换的麻烦,也降低了学习的门槛。
定位与绘制
Player:游戏的Player是通过人像细粒度分割模型分割出的人脸,这里使用PaddleHub中的ace2p,这个模型可以分割出人体的不同部位。我们通过ace2p可以得到想要部位的mask,这个mask不仅可以用于绘制我们的人脸,也可以用于碰撞检测。人脸的绘制只需要将输入的图像和mask相乘就可以得到。
Enemy:Enemy的绘制仅仅是n*n的小黑点,通过把指定位置的像素置0即可完成。我们需要给小黑点一个偏移量作为初始速度,然后每帧去更新这个小黑点的新坐标并重新绘制。
现在,我们有了Player的mask(坐标信息),也有了所有的Enemy的坐标信息。当Enemy的点运动到ace2p中的mask为1的位置时,即可判定发生了碰撞,游戏结束。这里可以制作一张只有Enemy的mask2,将mask2和Player的mask相乘(相当于取交集)并统计结果,就可以知道碰撞的结果。
由于ace2p模型对电脑的硬件要求有一点高,通过几个玩家的反应,我又制作了硬件需求低的版本——决战二仙桥。
第二版: 人脸检测版本
为了让游戏更有故事性和热点性,第二版使用了“谭谈交通”(成都本土一档寓教于乐的交通警示类节目)的一些素材,制作一个二仙桥主题的游戏。游戏中我们控制二仙桥大爷,来躲避谭SIR“追捕”。另外,我还引入了两个新的NPC——气球眼镜哥和“强人锁男”哥,这二位的出现,让整个游戏更有趣味性,也让整个“追捕”过程更加惊心动魄。同时,我增加了开始动画和不同结局的结束动画,并增加了语音特效、优化了UI。
人脸检测使用的是PaddleHub中ultra_light_fast_generic_face_detector_
1mb_320这个模型(以下简称face_detector模型),主要也是为了降低模型的硬件需求。通过测试,这个模型在CPU上也可以在很高的帧率下运行。相较于前一版本,这一版只需要定位人脸的中心点坐标,然后把我们想要的贴图文件画上去即可。当然,我们还是要处理碰撞检测这个问题,这里同样通过mask的方式。
定位与绘制
Player:Player的定位即是我们拍摄画面中的人脸的位置,这里通过face_detector模型获得。为了程序能够鲁棒地运行,我对该模型进行封装,加入更多的后处理以解决检测失败、误检等问题。Player的绘制仍然使用mask的方式,我用一些软件处理了程序用到的所有贴图资源,让它们全是具有通明通道的png图片,这样我可以在程序中很轻松地通过通道值来获取贴图的mask。
以下代码将人脸检测模型重新封装了一下,通过一些后处理,使得检测的结果更鲁棒,解决了一些漏检和误检的问题。
class detUtils():
def __init__(self):
super(detUtils, self).__init__()
self.lastres = None
# 对人脸检测模型进行封装,之后调用dodet拿到后处理过的检测结果
self.module = hub.Module(name="ultra_light_fast_generic_face_detector_1mb_320")
def distance(self, a, b):
# 计算两个点的欧式距离,这里主要用于两个bbox的中心点距离
return math.sqrt(math.pow(a[0]-b[0], 2) + math.pow(a[1]-b[1], 2))
def iou(self, bbox1, bbox2):
# 计算两个bbox 的IOU
b1left = bbox1['left']
b1right = bbox1['right']
b1top = bbox1['top']
b1bottom = bbox1['bottom']
b2left = bbox2['left']
b2right = bbox2['right']
b2top = bbox2['top']
b2bottom = bbox2['bottom']
area1 = (b1bottom - b1top) * (b1right - b1left)
area2 = (b2bottom - b2top) * (b2right - b2left)
w = min(b1right, b2right) - max(b1left, b2left)
h = min(b1bottom, b2bottom) - max(b1top, b2top)
dis = self.distance([(b1left+b1right)/2, (b1bottom+b1top)/2],[(b2left+b2right)/2, (b2bottom+b2top)/2])
if w <= 0 or h <= 0:
return 0, dis
iou = w * h / (area1 + area2 - w * h)
return iou, dis
def dodet(self, frame):
# 后处理bbox,尽量保持人脸框稳定,解决一些误检漏检的情况
result = self.module.face_detection(images=[frame], use_gpu=False)
result = result[0]['data']
if isinstance(result, list):
if len(result) == 0:
return None, None
if len(result) > 1:
if self.lastres is not None:
maxiou = -float('inf')
maxi = 0
mind = float('inf')
mini = 0
for index in range(len(result)):
tiou, td = self.iou(self.lastres, result[index])
if tiou > maxiou:
maxi = index
maxiou = tiou
if td < mind:
mind = td
mini = index
if tiou == 0:
return result[mini], result
else:
return result[maxi], result
else:
self.lastres = result[0]
return result[0], result
else:
self.lastres = result[0]
return result[0], result
else:
return None, None
Enemy及特殊NPC:这两者的位置与运动和第一版的相同。在绘制方面使用了和绘制Player时同样的方法来获取mask等步骤,就不再重复说明。
以下代码为Enemy和NPC的类,其中包含了Enemy的基本的位置信息,运动信息,以及技能信息等,并提供了更新函数来完成绘制和碰撞检测。
class Ball():
# 谭sir和特殊NPC的类
def __init__(self, x, y, speed_x, speed_y, img, skill, mask=None):
# 位置信息、运动信息、贴图及对应的mask、技能
self.x = x
self.y = y
self.speed_x = speed_x
self.speed_y = speed_y
self.img = img
if mask is None:
self.mask = np.zeros_like(img)
self.mask[img > 0] = 1
else:
self.mask = np.repeat(mask[:,:,np.newaxis], 3, 2)
self.h, self.w = img.shape[:2]
self.skill = skill
def move(self, screen, checkimg):
# 处理运动
global GM
global llock
# 在没有时停的时候可以更新位置
if not llock:
self.x += self.speed_x
self.y += self.speed_y
if self.x > W - self.w/2 or self.x < self.w/2:
self.speed_x = -self.speed_x
if self.y > H - self.h/2 or self.y < self.h/2:
self.speed_y = -self.speed_y
t, l, b, r, tt, tl, tb, tr = getPIXEL(self.x, self.y, self.w/2, self.h/2)
ctimg = checkimg[t:b,l:r]
stimg = screen[t:b,l:r]
# 检测碰撞检测,发生碰撞检测则触发技能,播放音效
if np.sum(ctimg[self.mask[tt:tb,tl:tr]>0]) > 0:
self.skill.trigger()
if self.skill.finish is False:
GM.appendskill(self.skill)
if isinstance(self.skill, Balloon):
_thread.start_new_thread(sound.thread_playsound, ("sound1",RES.getballoonmusic()))
elif isinstance(self.skill, Lock):
_thread.start_new_thread(sound.thread_playsound, ("sound1",RES.getlockmusic()))
else:
_thread.start_new_thread(sound.thread_playsound, ("sound1",RES.gettmusic()))
return True
else:
screen[t:b,l:r] = screen[t:b,l:r] * (1 - self.mask[tt:tb,tl:tr]) + self.mask[tt:tb,tl:tr] * self.img[tt:tb,tl:tr]
return False
技能与碰撞检测
第二版中增加了两个特殊的NPC——气球眼镜哥和强人锁男哥。当我们控制二仙桥大爷碰撞这两个NPC时,就会触发他们的技能。气球眼镜哥的技能是让我们的二仙桥大爷增加一次游戏的机会;强人锁男哥则会让除了二仙桥大爷外的所有角色无法动弹,时间随机。
碰撞检测的方法和第一版基本相同,不过这里的子弹变成了一张张小的贴图,因此判定方法需要做出改变,直接判定每个谭SIR、特殊NPC的mask和二仙桥大爷的mask是否有重叠。一旦重叠,则判定为发生了碰撞。因为不同的碰撞会触发不同的效果,现在也不再绘制Enemy的mask,这里会为每个NPC增加对应的碰撞检测的成员方法。
为了降低程序的耦合性,我把技能单独制作了一个类别,这里称作Class Skill。为了统一管理,谭SIR则被赋予了与气球哥相反效果的技能:让二仙桥大爷失去一次游戏的机会。每种技能都是Class Skill的一个子类。在制作NPC的类别时,技能类会作为一个NPC类的成员变量。在发生碰撞的时候,通过一个继承的多态方法来调用各自的技能类别。
Manager
为了解耦以及方便管理,游戏中包含了几个Manager——控制所有NPC生成和更新的NPC Manager,负责贴图、视频、音频资源加载及相应后处理的Resource Manager,负责控制游戏进程的Game Manager等。这些Manager让项目的逻辑更加清晰,代码更加简洁。
后记
以上便是这个游戏制作过程的介绍,游戏的制作都秉持着一切从简的原则,通过构建array,按照一定的顺序依次贴上贴图,并使用OpenCV来完成游戏的绘制。同时,保存一张大的mask地图,用于碰撞检测。除此之外,结合了一些热点元素,让游戏更有趣味性。由于只使用了OpenCV,所以学习的门槛很低。详细阅读本文后,大家也可以制作出一款类似的游戏。
直播预告
今晚7点,作者在飞桨B站直播间与你分享:自制体感小游戏《决战二仙桥》!
飞桨(PaddlePaddle)以百度多年的深度学习技术研究和业务应用为基础,是中国首个开源开放、技术领先、功能完备的产业级深度学习平台,包括飞桨开源平台和飞桨企业版。飞桨开源平台包含核心框架、基础模型库、端到端开发套件与工具组件,持续开源核心能力,为产业、学术、科研创新提供基础底座。飞桨企业版基于飞桨开源平台,针对企业级需求增强了相应特性,包含零门槛AI开发平台EasyDL和全功能AI开发平台BML。EasyDL主要面向中小企业,提供零门槛、预置丰富网络和模型、便捷高效的开发平台;BML是为大型企业提供的功能全面、可灵活定制和被深度集成的开发平台。
END