【飞桨开发者说】韩磊,飞桨开发者技术专家(PPDE),台湾清华大学资讯工程学系硕士,创业公司算法工程师。
又菜又爱玩,是我对自己游戏生活的总结。玩网游怕被队友喷,玩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即可完成。我们需要给小黑点一个偏移量作为初始速度,然后每帧去更新这个小黑点的新坐标并重新绘制。
图1:人脸分割
现在,我们有了Player的mask(坐标信息),也有了所有的Enemy的坐标信息。当Enemy的点运动到ace2p中的mask为1的位置时,即可判定发生了碰撞,游戏结束。这里可以制作一张只有Enemy的mask2,将mask2和Player的mask相乘(相当于取交集)并统计结果,就可以知道碰撞的结果。
图2 碰撞检测
由于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。
图3 人脸定位
以下代码将人脸检测模型重新封装了一下,通过一些后处理,使得检测的结果更鲁棒,解决了一些漏检和误检的问题。
classdetUtils():
def__init__(self)
:

super
(detUtils, 
self
).__init_
_
()

self
.lastres = None

# 对人脸检测模型进行封装,之后调用dodet拿到后处理过的检测结果
self
.
module
 = hub.Module(name=
"ultra_light_fast_generic_face_detector_1mb_320"
)


defdistance(self, a, b)
:

# 计算两个点的欧式距离,这里主要用于两个bbox的中心点距离
return
 math.sqrt(math.pow(a[
0
]-b[
0
], 
2
) + math.pow(a[
1
]-b[
1
], 
2
))


defiou(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 <= 
0or
 h <= 
0
:

return0
, dis


        iou = w * h / (area1 + area2 - w * h)

return
 iou, dis



defdodet(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
:

ifself
.lastres is 
notNone:
                    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的基本的位置信息,运动信息,以及技能信息等,并提供了更新函数来完成绘制和碰撞检测。
classBall():
# 谭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      


defmove(self, screen, checkimg)
:

# 处理运动
        global GM

        global llock

# 在没有时停的时候可以更新位置
ifnotllock:
self
.x += 
self
.speed_x

self
.y += 
self
.speed_y


ifself
.x > W - 
self
.w/
2orself
.x < 
self
.w/
2
:

self
.speed_x = -
self
.speed_x


ifself
.y > H - 
self
.h/
2orself
.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()

ifself
.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,所以学习的门槛很低。详细阅读本文后,大家也可以制作出一款类似的游戏。
如果您想详细了解更多飞桨的相关内容,请参阅以下文档。
·飞桨官网地址·
https://www.paddlepaddle.org.cn/
·飞桨开源框架项目地址·
GitHub: https://github.com/PaddlePaddle/Paddle 
Gitee: https://gitee.com/paddlepaddle/Paddle
觉得不错,请点个在看
继续阅读
阅读原文