如何让Unity3D Mecanim动画系统Generic动画支持动画中的位移

由于目前我们正在做的这个游戏中可能暂时还没有太强烈的需求去使用Mecanim动画系统中最新的Humanoid类型动画,我们首选的还会使用Generic动画,所以呢,前路漫漫其修远,大坑前面等你栽。 确定要使用Generic动画之后,那就继续呗。因为之前项目中使用的都是旧的动画系统(Legacy),所有角色和NPC的动画都是在原点做动作,角色和NPC在播放动画的同时需要通过脚本来控制角色的位移,这个要求负责程序和负责动画的童鞋密切合作把角色动画位移调整到一个协调的状态,这个当然也不是什么特别困难的事情,要不你看现在App Store里的所有游戏不都好好的嘛。 不过如果能让动画直接带上位移,那么程序和美术童鞋都解放了,动画设计的时候就能更大胆了,毕竟角色动画的真实位移相对于程序控制的更协调,而且大家都省了不少的事情,何乐不为啊。废话说了一大堆,好吧,回到正题。 那就做一个连续的带位移的动画吧,导入Unity之后,创建Animator Controller之后,把动作设置好,点击Play按钮,看看效果吧。尼玛,发现单个动画在播放的过程中角色确实发生位移了,但是每个动画结束之后回到IDLE状态动画的时候,角色在场景中的位置会发生一次闪回,直接重新归位到动画开始播放时角色所在的位置,并且在整个动画播放的过程中,角色的位置从未发生过变化,角色身上绑的Capsule Collider也完全不会移动,完全落在角色身后了。这是啥问题呢?这个问题在我使用Humanoid动画的时候木有出现啊,尝试再次恢复到Humanoid动画类型,确认了在Humanoid动画类型下确实不会出现这个位移闪回的问题,每个动画结束后,角色所在的位置就是动画结束时位置。 Mecanim-Generic-Rig-Apply-Root-Motion-Error 搜索良久最终找到了解决方法,其实只需要在导入动画的时候指定好动画骨骼的根节点就OK了。  

Generic-FBX-Import-Rig-Setting Humanoid-FBX-Import-Rig-Setting

 

 

 

 

 

咱们对比一下这两个设置的选项,其中Humanoid动画类型导入时,根本就不需要设置Rig的根节点,而Generic动画就需要设置,而且默认设置是none,所以需要手动指定为模型对应骨骼的根节点。

看来Unity动画支持Apply Root Motion这个特性的实现机制就是根据骨骼在动画中的位移在引擎内进行计算滴,如果我们选择使用Humanoid动画的话呢,肯定需要设置一个人形的骨骼Avatar信息,这个Avatar信息中就会包含骨骼各个节点的信息,其中当然也包含了根节点(或者说已经不需要根节点了,因为所有关键的骨骼信息都会映射到Avatar信息上)。而对于Generic动画呢,Unity自身并不清楚骨骼之间的关系,而Apply Root Motion这个特性就是根据骨骼位移自行计算的,那么就需要我们手动地指定动画使用的骨骼的根节点了,根据根节点在动画中的位置变化就可以动态计算出来对象在场景中实时的位置了。

下图就是修改Root Node设置之后动画播放时的效果了,Capsule Collider也实时跟着动画的播放改变位置了。

Generic-Apply-Root-Motion-1 Generic-Apply-Root-Motion-2

Unity3D Mecanim动画系统骨骼动画问题解决方法

这几天开始做游戏中跟动画相关的部分了,此次新项目我们决定一次从新开始,就是能用新的东西就都用新的东西,没有必要总是把自己局限在之前的认知里头,所以此次我们大胆而又现实的采用了Unity 4.x版本新增的Mecanim动画系统,并且我们果断地又选择了使用Humanoid动画类型。好吧,一切从新开始。

一切从新开始就意味着会有很多新的坑需要自己一个个去填,我们一直都拥有一颗坚强受虐的心不是吗。在我们的动画设计师将骨骼动画调好了之后,给了我一组FBX文件,导入Unity之后,预览一下这个动画吧。

animation-1

animation-2

 

 

 

 

 

 

我们可以很明显得看出这两个动画中人物角色的右手和武器之间的位置关系是不对的,跟动画设计师在3DS Max中制作和预览的效果也不对,好吧,碰到第一个坑,上吧。

通常我们先做的第一件事情就是先查查文档,看看Unity官方是否有一些最佳实践的建议,可是我翻遍了Unity Manual中关于Mecanim Animation System的所有文档没有看到相关的内容,而游戏引擎通常对于开发者来说就是个黑盒,出了问题除了查查看看别人是否碰到同样的问题也就是直接找Support这条路了,或者就是自己各种尝试呗。好吧,那就先Google一下吧,找啊找,找啊找,找了将近两天看了很多跟Mecanim动画相关的问题,但是就是没有找到这个这么基础的问题相关的帖子啥的,简直没有天理啊。

既然Humanoid不行,那我们试试Generic动画吧,这下就都OK了,那么显然动画资源自身肯定木有问题了,这下就确定肯定是Unity按照Humanoid动画导入FBX资源的时候有某些设置我们没有按照要求来做,所以导致了这个武器完全不跟着右手骨骼运动的情况。自己纠结了又一天,又是各种查啊各种试,完全木有进展。最后,只能求助于其他有经验的人了,通过我们的动画设计师,找到了一个他一个做技术美术的朋友,该大牛对Unity非常熟悉,专门解决项目中碰到的技术问题,哪里有问题哪里就有他。直接远程协助一下,看着大神一点点地调整,先是检查了一下Avatar设置中骨骼对应的关系是否正确,确认无误之后,再次运行看看效果依然如此,大牛稍微思考了一下,马上打开了Unity导入动画的页面,找到了Animations标签页,然后展开Mask设置中的Transform节点,将所有未勾选的人物角色骨骼节点都勾选上,然后Apply一下,再次运行游戏,搞定。

animation-3

animation-4

 

 

 

 

 

 

再回顾一下完整的调整过程吧,首先我们要确定我们出问题的骨骼具体是哪根,因为目前看到的现象是右手拿的刀和挂在腰上的刀鞘出现了问题,打开Avatar设置页面,查看一下,确定是Bip001 Prop1和Bip001 Prop2这两根骨骼出了问题。

Bip001 Prop1

 

确定了是这根骨骼出现问题了,然后进入到出现问题的动画文件jin_wei_jun@run.FBX的导入设置页面,打开Animations标签页 =》展开Mask节点 =》 展开Transform子节点 =》 找到左右手对应的刀和刀鞘的骨骼,勾选上,然后点击Apply按钮就好了:

import setting

QQ20140816-6@2x

QQ20140816-7@2x

 

 

 

 

 

 

 

 

 

 

虽然大神帮我们解决了这个困扰了我将近4天的问题,那么究竟为啥捏?其实在被困扰的4天里头,其实也发现了一些蛛丝马迹的,就是每次我们导入动画jin_wei_jun@run.FBX的时候,Unity总会提示以下的警告:

MuscleClip ‘jin_wei_jun@run’ conversion warning: Bone position is different in avatar and animation
‘Bip001 L Thigh’ : position error = 31.473890 mm
‘Bip001 R Thigh’ : position error = 8.405679 mm
‘Bip001 L Clavicle’ : position error = 37.673038 mm
‘Bip001 R Clavicle’ : position error = 76.461647 mm

UnityEditor.DockArea:OnGUI()

和这样的一个警告:

MuscleClip ‘jin_wei_jun@run’ conversion warning: ‘Bip001/Bip001 Pelvis’ is between humanoid transforms and has rotation animation. This might lower retargeting quality.
MuscleClip ‘jin_wei_jun@run’conversion warning: ‘Bip001/Bip001 Pelvis/Bip001 Spine’ has translation animation. It is not supported.
MuscleClip ‘jin_wei_jun@run’ conversion warning: ‘Bip001/Bip001 Pelvis/Bip001 Spine’ is between humanoid transforms and has rotation animation. This might lower retargeting quality.
MuscleClip ‘jin_wei_jun@run’conversion warning: ‘Bip001/Bip001 Pelvis/Bip001 Spine/Bip001 L Thigh’ has translation animation. It is not supported.
MuscleClip ‘jin_wei_jun@run’conversion warning: ‘Bip001/Bip001 Pelvis/Bip001 Spine/Bip001 R Thigh’ has translation animation. It is not supported.
MuscleClip ‘jin_wei_jun@run’conversion warning: ‘Bip001/Bip001 Pelvis/Bip001 Spine/Bip001 Spine1/Bip001 Neck/Bip001 L Clavicle’ has translation animation. It is not supported.
MuscleClip ‘jin_wei_jun@run’conversion warning: ‘Bip001/Bip001 Pelvis/Bip001 Spine/Bip001 Spine1/Bip001 Neck/Bip001 R Clavicle’ has translation animation. It is not supported.

UnityEditor.DockArea:OnGUI()

这两个警告大体的意思是啥呢,刚开始自己没有太注意,后来总是找不到原因就想着会不会还就真是这俩警告给弄的呢,自己仔细看了一下。第一个警告的意思,应该说的是动画中有几个骨骼的位置跟这个动画实际使用的Avatar中骨骼的位置不相符,这个原因我大概能理解的原因是这样的。我们在制作的过程中会将模型文件和动画文件分开,我在导入模型文件的时候会创建一个属于这个模型的Avatar,其他的动画都会直接引用这个Avatar文件而不会每个动画都创建自己的Avatar,而不同的动画中人物可以会有一些不同的动作,这就会让动画中角色的一些骨骼位置和模型(模型默认姿态就是站姿)站立姿态时的骨骼的位置会出现一些不匹配的情况,所以这个可以理解了。那么第二个警告呢,这个警告都是说某根骨骼有位移动画和旋转动画,而这些动画会降低Retargeting的质量,同时Humanoid动画不支持在这些骨骼上使用位移动画。虽然这个警告中提到的带有位移和旋转动画的骨骼不少,但是涉及的就是角色躯干、胸部、臀部、左右手、左右脚这几个非常关键的骨骼,并没有提到影响到刀和刀鞘的骨骼Bip001 Prop1和Bip001 Prop2啊。

综合大神提供的解决方案,应该可以初步得出结论,导入FBX动画的时候,选择Humanoid动画类型进行导入的时候,Unity会自行进行计算和判断,然后根据Retargeting最佳原则,设置Animations选项卡中Mask节点下Transform子节点中的骨骼是否需要在Humanoid动画中应用位移动画,而这个时候Bip001 Prop1和Bip001 Prop2这俩骨骼就被忽略了,所以我们需要手动的去勾选,如果有必要的话,那么就把所有的未勾选的骨骼都勾选上,然后再Apply一下吧,如果出现Apply一次不生效的话,重新重复设置遍,然后再Apply一下吧(我自己碰到了一次把所有的都勾选,然后Apply之后无效,重新展开之后发现只有部分被勾选上了,重新再全勾选Apply一次才好的情况)。

刚才我们提到了使用Generic动画的时候就不会出现这个问题对吧,那么为了印证这个结论是否正确,我们可以检查一下Generic动画导入设置页面中Animations标签页中Mask节点下Transform子节点中的骨骼节点是神马情况就好了。如下图,所有的节点默认都是勾选上的。

QQ20140816-10@2x

鉴于此,我们可以得出结论就是,在我们使用Humanoid动画类型导入FBX文件时,Unity会以最佳匹配Retargeting规则的方式自行计算,看看那些骨骼是需要勾选Transform动画选项的,而默认使用Generic动画就会将所有的骨骼节点Transform动画选项都勾选上,所以效果是正确的。OK,所以问题到这里就彻底明了了。

我是怎么科学上网的

最近碰到不少朋友问我怎么科学上网,刚好自己也有一些科学上网的经验,记录一下吧。

从2008年开始,我真正开始学会使用互联网,那个时候Twitter也才刚刚兴起,国内还有饭否和叽歪这样的追随者。刚开始Twitter是可以正常访问的,但是后来逐渐就不能访问了,为了追随国际潮流,作为程序员的我们,当然得想办法了。那个时候网络上有各种各样的免费代理以及Twitter的第三方服务,找一个还算稳定的代理或者直接使用某个第三方的服务就好了。但是好景不长,GFW越来越牛逼,众多代理倒下了,那些第三方服务作者前期对Twitter疯狂的热情也慢慢消融了,Twitter对于API的政策也慢慢缩紧了,终于有一天我发现了一个叫Puff的服务,当然也是在Twitter上发现的。

Puff是我第一次花钱购买的一个科学上网服务,Puff最开始是有提供免费版本的,我在持续使用了近半年免费版本之后,毕业开始上班了。我工作的第一家公司『北京超图软件股份有限公司』针对研发中心的所有小伙伴们都做了访问外网的限制,所有的工作机器都是无法连接外网的,那个时候自己还住在公司集体宿舍,也木有自己的电脑,所以差不多有半年的时间里头,很少能有机会接触到互联网,作为一个对世界充满了爱的少年,我们肯定是无法忍受的。所以我忍痛向朋友借了5000块钱,自己再刷信用卡刷了2500,耗资7500购买了一台当时很牛逼的ThinkPad T400,从此非工作时间我就可以拿着我的电脑在办公区使用Wi-Fi网络畅游互联网了,感谢借钱给我的同学!就在我购买电脑之后,我购买了人生中的第一个正版软件『Nod32』为期1年半的服务,从此开始了自己数字消费之路。在我离开『超图』加入『喜讯』之后的第一个月,我购买了Puff的商业版。Puff有个特性让我很感动,因为那时候自己每天都是在Ubuntu下进行开发,所以有跨平台的需求,Puff可以在Wine的托管下非常好的运作,这样我就可以在家里和办公室都顺畅地使用Puff了。其实这一年使用Puff的主要需求也就是上上Twitter,看看别人每天都在刷什么,看看自己能不能跟上潮流,就这么Puff帮助了我科学上网了一年时间。

AWS EC2 + SSH在Puff服务到期之际非常及时的补上了缺,当时AWS在做活动,免费赠送1年的EC2服务时间,办公室里头4只程序猿果断掏出信用卡绑定上,免费的优质服务必须体验一下嘛。当时选择的是旧金山机房,貌似还不错,刚刚好当时自己的这个博客站点所托管的主机服务器也要到期了,所以正好把博客也迁移到了EC2上(这个要感谢我们的振警梁同学),EC2的稳定让我这一年几乎从未因为无法科学上网而发愁,简简单单『ssh -CfNg -D 127.0.0.1:7070 [your_user_name]@[your_host_name_or_ip_address]』一句话,配合浏览器Firefox的AutoProxy + gfwlist简直就是倚天屠龙啊,感谢AWS免费提供了一年高效稳定的服务。

免费的午餐抵不了成天的肚饿啊,EC2到期之后,跟同事一起合租了一个VPS,把大家自己的博客站点都挂上去之后,平时依然当做科学上网工具来使用,但是试过了多个VPS,稳定性都实在无法跟EC2相提并论,多方寻觅,最后选择了一个42区的VPS,价格也合适,刚刚使用的一段时间也很不错,不过后期经常宕机的毛病,让我们实在无法继续忍受。最终我们把VPS迁移到了一个SSD Cloud VPS平台了,就是现在持续在使用的Digital Ocean,我们选择的是最基础的套餐,20G SSD Disk,512M RAM,1TB Transfer,每个月$5,一年下来不到400人民币,还算划算,也还算稳定,重启机器也很方便。在购买Digital Ocean的服务之前,因为无法忍受其他VPS龟速的网络以及各种宕机的问题。我已经购买了VPN Tech的服务,这家VPN的网速还是相当不错的,配置也简单,可以直接使用域名进行连接设置,区分了各大地区,还区分了下载服务器,在提供付费VIP服务的同时,也提供了免费的试用服务。如果家里的网络稳定的话,看Youtube视频完全不是问题,而且最多支持5台设备同时在线,对于有多设备需求的人来说非常合适,其实这一个帐号都完全能够支撑一家子人使用了。

持续了使用了VPN Tech的服务之后,因为一直都是使用v.avpn.us这个域名进行连接,而有时候就是会连不上,最后发现使用hk.avpn.us连接会更快也更稳定,但是偶尔还是会出现掉线或者偶尔出现无法连接上VPN服务器的情况,而通常自己想用VPN的时候已经是比较着急的时候了,这么折腾几回之后,也尝试了一家香港的VPN服务Green网络加速器,网速非常强劲,稳定性也非常不错,只是我只用了一个月,因为对同时连接设备数限制太多,我又通常需要在我的电脑和手机上同时使用,不太适合我。

连续使用了两年多的VPN服务,感觉也还可以,但是总会出现一些不如人意的事情。其实我们自己现在使用的VPS上也有搭建VPN服务,偶尔也会使用一下,但是也偶尔会出现无法连接的情况,当然也可以使用SSH的方式,还是那句话,太折腾又时有不如人意的事情发生。折腾这些事情呢又总是很费时间,而且很容易让人心情不好,特别是在你需要科学上网的时候,喀给你来那么一下,你能舒服吗?

所以,我把目光投向了更专业的一些服务,当然价格也稍微贵一些,类似于『曲径』和『轻云』(貌似已经被墙了),最终我选择了MacTalk一直推荐的曲径,正所谓『曲径通幽处』我想你懂的,别想歪了啊。曲径这类服务其实本质上就是一个Http/Https代理,配合PAC(Proxy Auto-Config)文件进行分流,让你需要科学上网的时候通过曲径的服务器进行加密访问,不需要科学上网的时候直接访问,这样一来也不再需要担心流量问题(我每天都用Spotify听音乐,流量都能足够),而且配置极其简单,移动设备配置也非常方便。只是目前Android设备需要有Root权限才可以使用曲径的服务,这个让我感觉还是蛮蛋疼的,其他的都还好。当然曲径不能帮你解决所有的问题,她只能帮你解决使用系统默认代理设置,或者可以手动设定Http/Https代理的软件科学上网的问题,比如某些软件不提供代理设置方式又不使用系统默认的代理设置,这个时候VPN还是有作用的。

科学上网是一个渴望自由访问互联网内容的程序员的必修课,修这么课的同时就能让我了解到很多技术上的东西,也拓展了视野,最终成为了一个可以随时随地自由访问互联网的人。对此,我很开心,很感谢这些为我们提供各种科学上网服务的人们所做的各种努力。

什么叫创业心态?

从坚持了整整4年的喜讯离开了,肩上的担子一下轻了下去,自己角色转变还蛮快的,倒也没有觉得有什么不适,感谢我们武雪同学在离职的当天晚上还给我做思想工作,让我别想太多不要太难过,要快乐地度过这段离职缓冲期。

在喜讯四年多,从一个什么都不懂的愣头青小子到如今依然懵懵懂懂的一个程序员,四年里头自己坚持得最多的可能概括起来就是『用创业的心态去做事情』这么几个字吧。

现在有很多的企业都在宣称要去中心化,要精英团队,在大公司内部提倡小团队在公司内部创业,例如『金山软件』将所有的大部分拆分成各个小的子公司,让各个自己公司『重新创业』,也确实看到了『猎豹移动』这样快速成长的上市子公司,成绩非常斐然。同时我们也看到了『腾讯广研』团队创造出来的『微信』神话,这些都是非常好的大公司小团队创业成功的例子。包括百度也在提倡要狼性,无非也就是要求大家要忘却自己是大公司的身份,要重新给自己定位,要有创业的心态。

这些公司和团队都提倡一个『创业心态』,那么『创业心态』是啥?我自己的理解就是『把公司分配给你的任务当成自己的事情来做』。这句话读起来非常简单,感觉也挺好做到的,但是在自己跟着公司创业的这4年多时间里,委实觉得要做到实在不易。

『把公司分配给你的任务当成自己的事情来做』要求自己做到两点:

1. 要有责任感,只有拥有强烈的责任感,才可能把事情做到位,未来才有可能会有更多的任务和责任分配到自己的头上。例如公司让你完成某个项目中的A功能模块,那么怎么才能顺利把A功能完成呢?这个时候可能会碰到以下问题:

  • A功能是自己之前从未遇到过的一个技术难题,需要自己苦心钻研几天才有可能找到解决问题的方法,但是项目进度要求非常紧张,如果花费几天搞定这个问题,成本太高;
  • A功能跟某个B功能是有非常强关联的,如果需要做A的话,需要调整B功能,B功能一直是其他的某个同事在负责,但是目前该同事也有较重的任务在身上,无法脱身来协助调整B功能;
  • 等等类似的问题都可能会出现,举例只是想说明在我们处理任何问题的时候都可能不只是一个简单的花费多长时间,通过工作量就可以搞定的。

那么在这些情况下我们要怎么才能按时按质按量完成A功能呢?好吧,其实我也不知道具体的解决方法是啥啦,因为大家碰到的问题可能千奇百怪的,所谓『家家有本难念的经』,谁知道尼玛你会碰到什么狗逼问题啊。但是,在我们碰到类似的困难的时候,只要认定这个事情交到自己手上,自己一定要想办法搞定他,遇到技术难题迎难而上,加班加点肯定不是什么稀奇的事情,遇到需要其他同事协同处理,努力跟对方沟通协调时间,对方实在没空可以考虑跟对方沟通确实调整方案自行修改,如果实在自己能力有限,确实无法在指定时间内搞定该问题,或者自己根本就完全没有办法搞定这个问题,那么及时反馈到团队负责人那儿,大家一起来想办法。

公司给大家发薪水,大家需要体现自己的价值,解决公司的问题是每个团队成员必须具备的基本素质。在创业团队中更是如此,因为团队建制较小,每个人都需要独当一面,甚至独当多面,那么公司交给你的事情,一定要尽最大的努力完成,因为团队中其他的成员默认这个事情交给你就是需要你来搞定,团队中其他环节的安排都会以你默认可以搞定这个事情为前提去开展,如果出现自己无法搞定而又未能及时反馈到团队,最终造成团队内部消耗,有个几次团队其他人便会认为你完全没有责任心,或者也可以说没有能力,交给你的事情完全搞不定,还不及时跟团队沟通。一来二往,你在团队中会失去队友对你的信任,那么未来不太可能再有什么责任交给你,只会给你一些鸡毛蒜皮的事情。只有你给团队其他人一种踏实可靠的感觉,大家在分配重要任务的时候,才会想到你,就这样,做一个『靠谱』的人很重要。

2. 要有主人翁精神,说白了就是在有责任心之外,还要多管闲事。为什么要多管闲事呢?其实创业团队最主要的一个资源限制就是人员不够,很多配备上是不完善的,那么你作为一个客户端程序员是否就只关注客户端程序的实现呢?当然关注客户端程序的实现是本职工作,一定要踏踏实实做好,做一个『靠谱』的客户端程序员,除此之外,其实还有很多的事情需要你来参与,正所谓『众人拾柴火焰高』啊,你不负责生火,但是你可以捡柴火啊。

  • 产品设计,虽然客户端程序员不需要承担产品设计的工作,但是作为一个用户,你也可以提出自己的看法啊,认为有可以改进的地方,当然需要主动提出来,供大家参考了,不要觉得不好意思,尼玛产品做砸了,你一样也被用户骂,你一样也没有奖金;
  • 服务端架构,虽然你只是个客户端程序,服务器神马的完全看不懂啦,不要给这么多鸭梨好不好啊?好吧,是有点强人所难,可是难道你真的没有想法吗?如果服务器的架构设计完全不考虑与客户端如何通信交互,这难道也可以?好吧,显然不行,那么就把自己能想到的可能出现问题的地方,尽早说出来吧,这样大家才能一起想办法来搞定这些事儿;
  • 团队文化,感觉好高深的样子,其实尼玛你就是团队成员,你也是这个文化的缔造者啊。所谓团队文化,无非就是团队所有成员在做事情的时候形成的一些共识的做事的方式和方法,那么你自己做事情的方式和方法无疑会给整个团队的文化刻上你的印记。那么自己写代码的时候是不是要考虑把方法名起得简单易懂有意义呢,是否每次提交代码的时候需要写清楚此次提交的注释呢,是否在下班的时候要让自己的电脑关机呢,都是些鸡毛蒜皮的小事情,可就是这些小事情会逐渐形成为整个团队独有的风格,随后加入的成员会慢慢融入到这些风格中来的。

公司有很多岗位,每个人负责的事情可能也不一样,这里也只是简单的举了几个例子,其实每个人在做完自己的本职工作之外,有很多的事情都是可以参与进来的,充分发挥自己主人翁的精神,把公司当成自己的公司,珍惜身边的每个人,认真对待每件事情,其实也可以侧面地让自己更有责任感,既然自己已经是公司的主人了,那么还有什么责任承担不了呢,是吧。

如果自己认为自己只需要把自己的事情做好,其他的事情就应该由别人来搞定,这当然没有什么错,但是这个并不适合在创业团队还没有足够的资源配备的情况,确实就是有很多的事情需要你来参与,一起努力。自己搞定自己的一亩三分地之后,事不关己高高挂起,如此这般,团队中其他人慢慢地就会认为你根本没有把这个团队当成自己的团队,慢慢地你就边缘了,慢慢地你就离团队中心越来越远了,最后你就会觉得自己就像是个外人,说话都插不上嘴,自己也不被团队认同了,你说这能是啥好事吗?

 

Unity3D游戏在iOS上因为trampolines闪退的原因与解决办法

崩溃的情况

进入游戏一会儿,神马都不要做,双手离开手机,盯着屏幕看吧,游戏会定时从服务器那儿读取一些数据,时间一长,闪退了。尼玛问题是神马呢?完全没有头绪,不过大体猜测是因为网络请求导致的,那么好,先排查服务器返回结果是否有问题,最终确认每次客户端崩溃的时候,服务器都成功的返回了格式正确的数据,没有任何异常。那么可以确定问题是出在客户端部分了。 先检查代码,确认逻辑上没有任何问题之后,也倍感无力啊,问题依然在重现。肿么办呢?

确定具体原因

那么好吧,打一个测试版本再来看,然后再等着崩溃,查看崩溃日志吧,最终看到的崩溃日志中,崩溃线程输出信息如下:

Thread 27 Crashed:

0 libsystem_kernel.dylib 0x38e671fc __pthread_kill + 8

1 libsystem_pthread.dylib 0x38ecea4e pthread_kill + 54

2 libsystem_c.dylib 0x38e18028 abort + 72

3 gowonline 0x0178a0c0 mono_handle_native_sigsegv + 312

4 gowonline 0x01779a30 mono_sigsegv_signal_handler + 256

5 libsystem_platform.dylib 0x38ec9720 _sigtramp + 40

6 gowonline 0x00114f48 m_RestSharp_Http_ExecuteCallback_RestSharp_HttpResponse_System_Action_1_RestSharp_HttpResponse + 52

7 gowonline 0x001142b4 m_RestSharp_Http_RequestStreamCallback_System_IAsyncResult_System_Action_1_RestSharp_HttpResponse + 900

8 gowonline 0x00329c60 m_2be7 + 48

9 gowonline 0x00a39d08 m_System_Net_WebAsyncResult_DoCallback + 76

10 gowonline 0x00a29628 m_System_Net_HttpWebRequest_SetWriteStream_System_Net_WebConnectionStream + 536

11 gowonline 0x00a46f84 m_System_Net_WebConnection_InitConnection_object + 708

12 gowonline 0x0101ffac m_wrapper_runtime_invoke_object_runtime_invoke_dynamic_intptr_intptr_intptr_intptr + 200

13 gowonline 0x017792d4 mono_jit_runtime_invoke + 2152

14 gowonline 0x0181b324 mono_runtime_invoke + 132

15 gowonline 0x01820118 mono_runtime_invoke_array + 1448

16 gowonline 0x01820510 mono_message_invoke + 444

17 gowonline 0x018444a8 mono_async_invoke + 124

18 gowonline 0x01844174 async_invoke_thread + 312

19 gowonline 0x0184c580 start_wrapper + 496

20 gowonline 0x018695b4 thread_start_routine + 284

21 gowonline 0x01885750 GC_start_routine + 92

22 libsystem_pthread.dylib 0x38ecdc5a _pthread_body + 138

23 libsystem_pthread.dylib 0x38ecdbca _pthread_start + 98

好的,那么已经确定是在我们使用的一个第三方类库RestSharp中出现的问题,问题是出现在一个Action回调的地方。那么这种问题为什么会出现呢,那我们就得好好得来找找原因了。

关于如何查看iOS崩溃日志,让崩溃日志更加友好,我们可以参考这篇文章,iOS应用崩溃日志揭秘,主要就是要确保你的设备上跑着的这个App的编译和打包的二进制文件要在你用于查看日志的Mac上,这样的话,当我们查看崩溃日志的时候,Xcode会自动将那些无法阅读的函数调用的堆栈信息转化成可读性较强的日志信息,帮助还是很大的。

那么这个时候我们可以通过将设备连接到Mac上,直接通过Xcode将程序编译并运行,多尝试着玩一段时间,当程序再次出现崩溃的时候,我们就能看到更清楚的函数调用关系了,同时也能看到更多的日志提示。

最终能确定每次崩溃的函数就是这个mono_convert_imt_slot_to_vtable_slot,这个看上去就是Mono Runtime在将接口声明的方法指针指向实际实现这个接口的对方的方法,我们可以找到mono_convert_imt_slot_to_vtable_slot这个方法所在的文件查看一下,这个方法就在Mono项目的目录mono/mini/mini-trampolines.c中可以找到。

在Xcode中崩溃时,会输出类似” SIGABRT (ERROR:mini-trampolines.c:183:mono_convert_imt_slot_to_vtable_slot: code should not be reached) “的日志,看着很像是原本是要执行某个方法,但是不知道因为什么原因这个方法就无法访问到了,好奇葩啊。

解决方案

现在虽然已经知道了问题出现的地方,但是貌似完全看不明白的样子,尼玛trampoline都还是第一次听说耶,那么先请教一个我大Google吧,我们总是相信自己不是那第一个吃螃蟹的人,所以我们找到了一位大神的解决方案就在这里,大神的文章写得非常言简意赅,大体意思就是如果你在做Unity3D开发时,特别是在针对iOS和Android平台的时候,你很有可能会碰到比较杯具的就是程序会莫名其妙地闪退哦,不过不要着急,这个通常就是因为你的程序编译的时候给trampoline分配的空间太小,而你的程序中又大量使用了泛型、泛型方法调用和接口实现导致的。然后给出了具体的解决方法,那就是在Unity3D的编译选项Player Setting中有一个AOT Compilation Options条目,在这个选项条目中加上以下编译参数就好了

nrgctx-trampolines=8096,nimt-trampolines=8096,ntrampolines=4048

然后再重新一下,多多测试吧,骚年。关于这三个参数的意思呢,大神也给出了解释,分别如下:

  1. nrgctx-trampolines=8096 这是留给递归泛型使用的空间,默认是1024
  2. nimt-trampolines=8096 这是留给接口使用的空间,默认是128
  3. ntrampolines=4048 这是留给泛型方法调用使用的空间,默认是1024

Mono Runtime AOT机制剖析

虽然问题貌似已经得到解决了,而且我们貌似也搞清楚了具体原因就是因为默认Mono Runtime在AOT编译的时候给的trampoline配置太小,不适合我们这种设计优良,大量使用interface,设计绝对遵照OO思想的稍大一些的项目呢。那么我们以后是不是在做Unity3D开发的时候就尽量少用接口呢?是不是我们就尽量少用泛型和泛型方法呢?

既然这么感兴趣,想问个究竟,那么我们就来好好看看这个AOT到底是个神马东西吧,尼玛为什么就这么复杂,这么隐蔽,这么折腾人,《铁血战神》在App Store上线都5个月了有木有,尼玛这个问题碰到也不是一次两次了有木有,作为程序猿的我们被玩家吐槽了很多次,我们的客服XDJM们为我们背了多少黑锅啊,我勒个去啊。

首先,还是先搞定这个trampoline吧,毕竟问题的根源是在它身上的,那么我们就好好来看看这是个神马东西。我们找到Mono Runtime的官方文档中关于trampoline的描述来看看吧。

Trampolines are small, hand-written pieces of assembly code used to perform various tasks in the mono runtime. They are generated at runtime using the native code generation macros used by the JIT. They usually have a corresponding C function they can fall back to if they need to perform a more complicated task. They can be viewed as ways to pass control from JITted code back to the runtime.

翻译一下吧:

Trampoline是一些手写的非常短小的用来在mono运行时中执行很多操作的组件代码。主要是通过JIT使用到的本地代码宏在运行时动态生成的。它们通常都有与之相对应的C方法,在某些较为复杂的场景中,当trampoline无法胜任时,mono运行时就会将这些复杂的操作交回给这些对应的C方法来执行。这也可以看作是将JIT代码的执行权交回给runtime的一种方式。

好吧,貌似还没有太明白,那么这个Trampoline为什么会导致出现闪退的问题的,这看起来明显是为了提高mono runtime在执行C#代码时候的效率啊。

那么我们再来看看官方文档关于JIT Trampolines和AOT Trampolines的介绍吧,杯具的IMT Trampolines介绍还在//TODO状态中。

JIT Trampolines These trampolines are used to JIT compile a method the first time it is called. When the JIT compiles a call instruction, it doesn’t compile the called method right away. Instead, it creates a JIT trampoline, and emits a call instruction referencing the trampoline. When the trampoline is called, it calls mono_magic_trampoline () which compiles the target method, and returns the address of the compiled code to the trampoline which branches to it. This process is somewhat slow, so mono_magic_trampoline () tries to patch the calling JITted code so it calls the compiled code instead of the trampoline from now on. This is done by mono_arch_patch_callsite () in tramp-.c.

好吧,再翻译一下吧。

JIT Trampolines 这些Trampoline主要是JIT在首次调用某个方法的时候编译方法用的。当JIT在编译一个方法调用指令时,它并不会立刻就编译这个被调用到的方法。实际上,它会先创建一个JIT Trampoline,同时创建一个指向这个trampoline的调用指令。当这个JIT Trampoline在调用到的时候,它会再调用mono_magic_trampoline()方法来编译这个trampoline实际指向的目标方法,然后将编译后的方法的指针地址返回给这个指向它的trampoline。这个过程呢稍微有点慢,所以呢,mono_magic_trampoline()方法会优化调用JIT代码的过程,它会先尝试调用已经通过JIT编译过的方法而不是立即通过trampoline直接进行调用。这些都是通过在tramp-.c文件中的mono_patch_callsiete()方法来完成的。

这就是JIT Trampolines的机制,接下来我们看看AOT Trampolines又是怎么一回事呢。

AOT Trampolines

These are similar to the JIT trampolines but instead of receiving a MonoMethod to compile, they receive an image+token pair. If the method identified by this pair is also AOT compiled, the address of its compiled code can be obtained without loading the metadata for the method.

再翻译一下。

AOT Trampolines AOT Trampolines和JIT Trampolines非常相似,但是AOT Trampolines接受的编译参数不是一个Mono方法而是一个image+token对。如果传入的用于编译的image+token对所指向的方法已经经过AOT编译过了,那么再次编译这个image+token对时,就会直接返回这个已编译方法的指针地址而不需要再次加载这个方法的元数据进行再次编译了。

好吧,看了这么多关于Trampoline相关的内容,貌似只是了解到了非常有限的内容,那就依然是Trampolines存在的价值就是为了减少C#代码在mono runtime中运行时的性能损耗,提高C#代码的执行效率。

还有那个没有出场的IMT Trampolines应该也就是用于优化接口调用效率的小『蹦床』吧。

那么我们在开发Unity3D游戏的时候通常都会发布到iOS设备和Android设备上,而Unity3D在iOS和Android设备上的发布都选择了使用AOT编译机制来实现。那么显然我们碰到的Trampolines问题都是跟AOT Trampolines有关,那么AOT又是神马呢?

AOT就是区别于JIT(Just In Time)的另一个编译机制,全称是Ahead Of Time,就是预先编译好,而不是在代码执行到了某个方法再进行编译,这样的话会有一些好处。

通过查看Mono官方AOT介绍文档,使用AOT编译的有点有以下优点: 1. 加快程序启动速度 2. 更强的内存共享机制 3. 潜在的性能提升

当然也会有一些限制,例如支持平台的有限,支持AOT的Mono版本有限等等,具体信息可以参考Mono官方AOT介绍文档

那么回到我们最开始的问题,为什么我们的游戏就会出现崩溃呢?好吧,现在一点点回顾吧。

我们出现的问题是偶尔会出现闪退,根据崩溃日志我们能定位到是mono_convert_imt_slot_to_vtable_slot这个方法导致的,然后我们再通过Xcode跟踪到了是trampoline无法被访问到的问题。

那么这么高端大气上档次的问题是肿么出现的呢?貌似Mono还算是个不错的产品啊,还是很活跃的啊,也有专门的公司Xamarin在支撑着,怎么就会出现这种问提呢?

好吧,程序都是人写的,有问题也是很正常的。上面的分析已经很清楚了,大体的原因就是因为Mono在iOS/Android等移动设备上使用了AOT这种机制,为什么选择这种机制?原因非常简单,那就是可以针对特定平台编译成在平台优化的字节码,在资源比较紧缺的移动平台上还是有着明显优势的。而使用AOT编译就需要为Trampolines这些小东西留足足够的空间,当然这个肯定是硬编码的某个常数啦,在整个程序加载成功运行之后,该常数就成为了Trampolines运行时的配置。AOT默认编译时给Trampolines的参数有点低:

nrgctx-trampolines 默认为1024

nimt-trampolines 默认为128

ntrampolines 默认为1024

这对于小一些的项目可能是够用的,因为整体项目的结构不会太复杂,使用到的接口、泛型、递归相对也不会太多,但是对于一个稍大一些的项目来说,特别是采用了某些设计良好的第三方库的项目来说,这就比较纠结了。

其实我们在项目中就使用了两个第三方的库,一个是CodeTitan.JSon库,一个是RestSharp,分别用于JSON解析和HTTP请求处理,可是这两个库实在是设计得太好了,各种使用接口,各种抽象,没个两三天我都没法说完全理解了整个库的结构。

就是因为这些设计良好,完全遵循OOP原则,高度抽象的类库将Mono默认的Trampolines的配置耗尽了,所以捏,我们就把这个编译选项开大就好了,解决方案就是上面咱们提到的咯。