Unity3D中useGravity和isKinematic以及applyRootMotion的备忘

在最近几天的开发中,碰到一个比较有意思(恶心)的问题,需要达到的效果如下:

  1. 有一个NPC,在受到攻击时,会播放一个从站立姿势到击飞浮空的动画;
  2. 在NPC切换击飞浮空动画的同时,NPC需要同时向斜上方飞行,飞行到指定高度待机。

通常我们有几个方案可以选择:

  1. 通过Unity3D自带的物理引擎模拟,给NPC一个斜上方的速度或者作用力,这个通过Rigidbody.velocity设置一个速度向量或者通过Rigidbody.AddFore/Rigidbody.AddRelativeForce进行设置就OK了;
  2. 自己根据需要的速度自行计算每一帧NPC的位移,然后通过直接设置NPC对象Transform.position属性来达成NPC斜向上飞的效果。

这两个方案从理论上来说肯定都是OK的,其实方案一最终在Unity3D内部的PhysicX引擎中的实现也是通过对Transform的position进行设置来完成的,只是额外还会进行碰撞监测和阻力计算等等。

实际开发的过程中,我分别使用了这两种方案来进行测试,对比之后发现:

  1. 方案一模拟效果确实更真实,而且省去了很多的计算过程,但是简单的设置速度和作用力可能无法达到策划设计的受击效果,因为我们制作的是格斗游戏,对于受击对象最终浮空的位置有相对严格的要求,需要为各种连续攻击留足设计空间,这个就不展开说了。总之,如果完全交给物理引擎来演算模拟的话,会出现一些不可控的结果,而那是我们不想要的,所以需要通过一些其他的手段来进行反算,使得我们设置了某些参数之后,物理引擎模拟最终的结果就是我们想要的(最终我就是这么做的);
  2. 方案二简单,而且结果是完全可控的,NPC飞行的轨迹完全是可以预计的,但是由于直接控制Transform的position是完全无视在整个游戏场景中的物理碰撞的,在做position设置之前每次都需要自行检测NPC是否会与场景中可碰撞对象之间的关系,这个性能的开销如何控制以及如何做到有效的检测对于目前的我来说是一个较难的课题,所以最终我放弃了。

那么按照方案一来执行的过程中,又碰到了什么有意思的问题呢?

NPC在受击之后,通过Animator设置了一个Trigger,触发NPC从站立到击飞浮空的动画切换,同时给NPC对象的Rigidbody一个Velocity,代码如下:

mAnimator.SetTrigger ("KickFloat");
rigidbody.velocity = velocity; // NPC斜向上方飞行的初速度

可是最终执行的效果是NPC只会往上垂直飞行,在水平方向上根本就没有速度,这是为啥呢?就在我快要把头发拽下来的刹那,我决定先去泡一杯劣质的越南『中原G6』速溶咖啡,等我把咖啡泡好了回到座位上,突然意识到其实在很早之前碰到过另一个问题,当时就是因为Animator的applyRootMotion变量给惹的祸,卧槽啊~

于是,我很精灵的在刚刚的代码基础上加上了一个前缀处理,代码如下:

mAnimator.applyRootMotion = false;
mAnimator.SetTrigger ("KickFloat");
rigidbody.velocity = velocity; // NPC斜向上方飞行的初速度

尼玛这下清净了吧,哥哥我半个下午的时间就是在不断的怀疑人生和自我啊,不过看在最终还是搞定的份上,我也就不再说啥了。然后NPC在做从站立过渡到空中浮空待机的动作的同时,终于按照我设定的速度的方向移动了,如释重负啊。

说了这么老些题外话(什么?这些竟然都是题外话?尼玛这已经很丰富了喂,可以做单独一篇文章了喂,那你到底要说的是啥啊?),我的目的只有一个,我就是想说明一下我为啥要针对这三个变量来说道说道,因为它们真的蛮有意思的(恶心了我这个Unity3D傻逼)。为了让自己以后不再傻逼,所以还是记录一下,做个备忘吧。

Rigidbody.isKinematic,这个属性用于控制物理引擎效果是否会在生效,如果这个属性为True,那么所有通过物理引擎给物体施加外力和设置速度等等就不会再有任何效果了。例如如果想做一个让NPC受击浮空之后,在空中停留的效果,就可以通过设置这个变量来实现,只要该变量为true,物理的重力效果就不会生效,NPC并不会自动往下做自由落体运动,而会在空中保持之前的动画状态,位置也不会发生改变。

Rigidbody.useGravity,这个属性就是用来控制物理引擎模拟时是否加入重力影响,例如我们在做击飞效果的时候,通常会通过给受击对象一个击飞后方向的作用力或者运动速度,如果需要考虑重力因素的话,可能会更真实一些,但是有的某些情况下,我们可能不想因为重力的作用对受击对象的飞行轨迹产生影响,那么就可以通过将该变量设置为false就可以关闭重力在屋里引擎模拟过程中对物体整体运动速度和轨迹的影响了。

Animator.applyRootMotion,这个属性是用来控制物体在播放骨骼动画的时候是否应用骨骼根节点的运动参数,也就是说在动画播放的过程中是否使用动画中物体的位移。在实际制作的过程中,总会有某些动画自身带位移,而某些动画是不带位移的,对于那些动画中自带位移的,那么就可以通过设置这个变量为true就可以让对象在播放动画的过程中按照动画中的位移进行移动,这样动画的效果也会更逼真一些而不是通过程序来控制对象的位移(程序大部分情况下都是简单的做匀速或者加速运动,无法做到完全与动画中动作移动幅度完全吻合,可能会出现一些失真的情况)。但是如果一旦设置了这个变量为true,那么请一定注意,这个会对物理引擎在模拟对象的运动轨迹时产生直接的影响,例如在某个动画A中,对象只向Y轴方向进行了移动,在X和Z轴是静止的,那么我们在播放A动画的时候,如果使用Rigidbody设置速度或者施加外力,还是不会让物体在X和Z轴上发生位移的。这是因为,在整个动画播放的过程中(例如0.5秒),Animator会根据动画中物体的位移信息对物体的速度进行赋值,这样达到使用骨骼根节点的位移的效果,也就是说,我在播放动画过程中的任意时间给物体设置了X或者Z轴方向上的运动速度,后续的动画播放帧中,速度又会被Animator强制赋值为跟动画文件中的位移信息一致。

综上,咱们来说说碰到哪些情况,我们需要检查哪些变量?

  1. 如果出现物体无论设置运动速度还是施加作用力,愣是没有任何效果,赶紧检查一下isKinematic变量是否为true,如果是的话,那么肯定是不会有效果的,当然也不会自由落体了;
  2. 如果出现物体往某个一直运动,根本停不下来,那很有可能是useGravity这个变量为false,如果真的不想要这个重力效果,那么设置为false刚好可以从此摆脱地心引力了;
  3. 如果出现物体无论设置运动速度还是施加作用力,而且也没有设置isKinematic变量为true,那么先别怀疑人生,赶紧确认该物体的Animator组件是否设置了applyRootMotion变量,否则头发揪没了也无济于事啊。

关于Mecanim动画系统中,如何让物体应用Generic动画中的位移,可以移步这里查看。

你想做什么样的老板

这两天各种媒体上都是关于陌陌IPO的新闻,以及陌陌CEO唐岩前雇主网易在陌陌IPO前夕发布一些关于唐岩个人的负面新闻,大家众说纷纭,被大家戏称为又一次『撕逼』,甚至有人臆想为陌陌上市之后会对易信造成更大的市场阻力等等。

作为一个局外人,其实我永远都只是一个不明真相,多少又有点愤慨的群众。莫说易信实际的竞争力远不如陌陌来得猛烈,就说陌陌平台通过游戏给陌陌带来的变现能力也远不是如今的易信可以望其项背的。作为一个观众,其实我们只看到了舞台的一角,也可能幕布还没有打开,我们就已经对着幕布在不断地YY,想象着幕布打开之后会是啥景象,也许幕布还没有打开,观众们凭着自己丰富的想象力以及各种阴谋论,足以让自己达到高潮,然后匆匆散场,而实际的演出都木有开始,也许这番乱象才是真相。

回到文题,实际上对于这个热点新闻,我完全木有任何想法也木有观点,跟我半毛钱关系都木有,去他大爷的。不过关于前东家在自己前雇员上市的前夕做出如此举动,自己心里头有点莫名的想说点啥的冲动。

什么样的老板会这么干呢?这么干的目的是啥呢?干了这个事情之后,能给自己带来什么呢?

问出这三个问题之后,其实也很难回答,因为我自己不会这么干,其实很难去分析别人这么干的个中缘由,也因为我们不知道当事人之间的各种利益纠葛和陈年旧事,所以我们回到自己想做什么样的老板这个话题上吧。其实谈自己想做什么样的老板,也意味着自己想跟着什么样的老板。

在我看来,老板需要具备以下一些比较优良的品质,方可让自己的雇员感觉到比较的舒服。

  • 能带领大家赚到钱,赚到钱之后愿意与大家分享;
  • 自己犯的错误要自己承担,不能把自己的屎盆子扣到别人头上;
  • 栽培员工是自己的职责,不能因此站在道德的高地;
  • 能坦然面对团队中能力突出者,并及时做出肯定和认可,不吝啬赞美之词;
  • 疑人不用,用人不疑,适当放权,相信别人会比自己做得好;
  • 面对团队成员离开,保持平静的态度,宽容地对待这些与自己想法不再相同的前伙伴;
  • 珍惜所有一起做事情的人们,有缘才聚到一起,别弄得像掉进钱眼里头去了一般。

说得不够抽象也不够简洁,也没有谈到类似于『克己,以身作则,思路开阔』等等优秀品质,这倒不是因为我认为这些特质不够重要,这些也非常重要。其实我们可以回头看看自己的老板,他们身上都有着很多不错的品质,通常都很能拼,能打硬仗,业务能力即便不算超一流肯定也是行业里头顶尖的家伙。可是真的只需要这些就可以算是一个好老板吗?

我一个人能写别人10个人都写不来的好程序,我一个人1年能卖出别人10年都卖不出的业绩,我一个人能每天工作18个小时,我可以一个月都不陪老婆逛街,天天就说工作。这些是不是很牛逼?是,确实很牛逼,很多人都做不到这些牛逼的事情,这些人也确实有很大部分就是现在的某些老板,他们都很优秀。

不过作为一个员工,其实每个人对于老板也是有要求的,员工都希望老板能给自己涨工资,希望老板能比较友善,希望老板给自己发展机会和空间。可能是因为自己之前碰到过一些不太如意的事情,也跟很多朋友聊到过类似的事情,心中有些郁积之气,如此这般道出来之后,心中也稍微畅快一些。

写下这些字,目的是提醒自己,如果有一天自己选择老板,或者自己也成为了老板,要记住除了自身要牛逼之外,还要记住这些自己曾经作为员工的时候,期待自己的老板会有的一些优良品质,不要自己最终成为了自己最初讨厌的人。那样的话,幸甚!

Unity3D编辑器通过拖拽获取文件路径

Unity3D项目开发过程中咱们难免会碰到一些需要设置Prefab路径的时候,例如某个攻击动作的特效,虽然我们最终都是通过填表来完成,如果完全手动填表那实在让人崩溃啊,碰到有的特效命名不小心填错了,那就更加无聊了,所以作为程序猿的我们肯定是要通过更加友好的方式来获取各种目录下各种文件的路径了。

我们早已经习惯了在各种地方通过拖拽来进行文件路径设置了,那么显然拖拽是非常简单又人性化的设定啊。假设我们现在有一个技能设置的编辑器,如下图:

技能编辑器

特效路径就是我们想设置的,而且可能会在平时做一些修改,每次都自己手动输入显然不是我们想要的,那么怎么让这个TextField支持将Project视图下的文件拖拽到这个文本输入框中,并且让它自动获取我拖拽的文件的路径呢?

『技能编辑器』这整个Window其实就是通过各种小控件给拼起来的,Unity3D中默认所有控件都是支持拖拽事件的,所以我们不需要做任何设置,直接针对我们的需求开始就好了。

我们想要的是,特效路径这个Label后边跟着的这个可输入的TextField文本输入框可以接受拖拽文件的方式来进行路径的输入。那么我们需要做的就是在这个『技能编辑器』的Window中监听鼠标拖拽的事情,同时判断鼠标拖拽时鼠标的位置是否处于这个可输入的TextField文本输入框之中就OK了,上代码。

EditorGUILayout.BeginHorizontal ();
EditorGUILayout.LabelField ("特效路径", GUILayout.Width (80));
// 获得特效路径文本输入框的位置和大小参数,用于后续判断鼠标在文本输入框内使用
Rect sfxPathRect = EditorGUILayout.GetControlRect(GUILayout.Width (250));
// 用刚刚获取的文本输入框的位置和大小参数,创建一个文本输入框,用于输入特效路径
string sfxPathText = EditorGUI.TextField (sfxPathRect, meleeAttackSection.sfxPath);
// 判断当前鼠标正拖拽某对象或者在拖拽的过程中松开了鼠标按键
// 同时还需要判断拖拽时鼠标所在位置处于文本输入框内
if ((Event.current.type == EventType.DragUpdated
	|| Event.current.type == EventType.DragExited)
    && sfxPathRect.Contains (Event.current.mousePosition)) {
        // 判断是否拖拽了文件
    if (DragAndDrop.paths != null && DragAndDrop.paths.Length > 0) {
        string sfxPath = DragAndDrop.paths [0];
            // 拖拽的过程中,松开鼠标之后,拖拽操作结束,此时就可以使用获得的sfxPath变量了
		if (!string.IsNullOrEmpty (sfxPath) && Event.current.type == EventType.DragExited) {
            DragAndDrop.AcceptDrag ();
		// 好了,这下想用这个sfxPath变量干嘛就干嘛吧
        }
    }
}
EditorGUILayout.EndHorizontal ();

VPS再次搬家

这次搬家搬到了Linode上,从最早的免费1年AWS EC2,搬到后来的42区,再到DigitalOcean,然后到Linode,折腾的次数也不算少了,所幸每次搬家耀华同学都非常快的就完成了所有的工作。我只需要把域名解析改一下就OK了,其他的一切照旧。

此次注册Linode帐号的时候还遭遇了一次必须上传身份证扫描件和信用卡扫描件的事情,有点小意外,不过还好,前后两个小时左右,帐号开通,一切正常。Tokyo机房的速度简直有点小残暴啊,直接使用VPN访问国内所有站点竟然不卡,简直就是禽兽啊。

此次逃离DigitalOcean的主要原因呢,就是在大陆地区经常无法Ping通主机,也就无法顺畅地访问博客站点了。之前差不多每次重启实例就OK了,这次多次重启均无效,貌似IP被封了,懒得发Ticket申请换IP,而且对于DigitalOcean旧金山机房的速度也不是特别满意,索性直接切换到了Linode上,目前使用情况良好,希望能保持。

如何让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