分类目录归档:Unity3D

Unity3D中的碰撞检测

在游戏制作过程中,碰撞检测肯定是经常需要使用到的,例如子弹飞行之后与可攻击目标之间的碰撞,子弹飞行之后与环境中物体的碰撞等等,都需要用到碰撞检测。那么在Unity3D中Collider肿么使呢,又有啥需要注意的捏?

Unity3D中用于碰撞检测的组件叫Collider,由这个货派生出来的又有Box Collider,Sphere Collider,Capsule Collider,Mesh Collider和Wheel Collider,这里我们主要说一下我自己用过的几个货啊,我目前就使用过Box Collider,Capsule Collider和Mesh Collider,其实Sphere Collider应该也跟Box Collider、Capsule Collider没啥实质性的区别,只是适用于不同的物体罢了。不废话了,进正题。

Unity3D中使用碰撞需要注意些什么呢?

  1. 确保物体所处的Layer之间存在碰撞。在Unity3D中,如果需要两个物体之间发生碰撞,首先要确认这两个物体所在的层(Layer)之间是会有物理碰撞的,这个可以通过Edit->Project Settings->Physics打开PhysicsManager进行查看和编辑,编辑面板最下方有一个跟9×9乘法表一样的Mask(掩码)开关面板,如果想让A层和B层之间的物体有物理碰撞,那就找到A和B交叉的那个点,确认勾选上就可以了,如果想去除两个层之间的物理碰撞检测,那么就把勾选去掉就OK了,两个处于同一层的物体碰撞设置也同理哦。
  2. 确保用于碰撞检测的两个物体至少有一个物体上有Rigidbody组件,Unity3D的物理引擎实际上使用的是Nvidia的PhysX系统,这个引擎的所有计算都是通过Unity3D中的Rigidbody来进行表现的,所以这个Rigidbody是碰撞检测的必要条件。
  3. OnTriggerEnter和OnCollisionEnter的区别,理解这两个概念之前需要先了解Collider和Trigger的区别。在Unity3D中我们添加的组件都是Collider但是每种Collider组件都有一个属性『Is Trigger』,只要将这个属性设置为True,那么这个Collider就成为了Trigger,这两者的区别在于:
    • Collider之间(两个都没有设置Is Trigger属性为True)碰撞(接触)之后会回调Collider所在GameObject上绑定的脚本中的OnCollisionEnter方法,碰撞之后物理引擎会根据碰撞物体的质量,运动速度等等信息进行计算,计算碰撞之后物体的运动参数,会产生真实的物体碰撞效果。
    • Collider和Trigger之间(任意一个Collider设置了Is Trigger属性为True)碰撞(接触)之后回回调Trigger所在GameObject上绑定的脚本中的OnTriggerEnter方法,碰撞之后的物体依然会按照原有的运动状态继续在世界中运动,两者之间不会相互影响对方的状态。
    • Trigger之间(两个Collider都设置了Is Trigger属性为True)碰撞的情况同Collider和Trigger之间的碰撞。
    • OnTriggerEnter和OnCollisionEnter不会同时回调,永远都只会调用这两个方法中的其中一个。

Unity3D中Generic动画导入设置和Root Motion之间的关系

Unity3D的Mecanim动画系统可以直接复用3DS MAX中制作的动画文件中的位移,这个就是通过applyRootMotion来达成的,我们只需要在使用Animator控制动画播放的同时,设置Animator的applyRootMotion字段为True就OK了。

那么怎么来利用这个特性达成我们想要的一些效果呢?这个applyRootMotion到底指的是啥呢?

ApplyRootMotion,从字面上理解来看,是『应用根节点的运动』,听起来貌似像那么一回事。可是我们可以从官方文档上看到这样一段话:

The Root Transform is a projection on the Y plane of the Body Transform and is computed at runtime. At every frame, a change in the Root Transform is computed. This change in transform is then applied to the Game Object to make it move.

翻译过来的意思,应该是这样的:

根节点的运动变换其实就是整个物体运动变换通过Y轴垂直在水平面上的一个投影。根节点的运动变换在动画的每一帧中都会进行计算。计算出来的根节点变换结果都会应用在播放动画的对象上,让该对象按照根节点的运动变换进行移动。

这段话大体的意思就是,RootMotion这个玩意就是作用于动画物体在X轴和Z轴上的位移的,而且这个位移是根据实际播放的动画中每一帧物体的位移在X和Z轴上投影计算出来的。

这个特性非常赞特别是对于某些技能动画,整个动画是有一定位移的,但是动画的位移是动作设计师在设计时根据动作需要调出来的,位移是跟动作的幅度直接相关和匹配的。

那么在释放技能的时候就只需要直接播放动画,并且应用这个Root Motion的特性就可以很好的完成角色在播放攻击动作的同时进行移动,动作播放完毕之后就在动画结束帧角色所在的位置,切换为待机动作就OK了。

看起来很牛逼的样子对不对?是的,确实很牛逼。但是还有很多事情需要我们都一一了解以后,我们才能做出我们想要的东西的。

下面我们先岔开一下话题,好好说说这个Animation Import Settings中『Animations』Tab页中各项设置的作用。

  • Import Animation,勾选这个才可以导入动画到Unity工程中;
  • Bake Animations,这个选项只在使用Humanoid动画并且使用到了IK特性的时候才可用;
  • Anim.Compression,这个是关于动画压缩选项的,默认会选择Keyframe Reduction这个是『压缩关键帧』,就是Unity会自行重采样动画的关键帧,还有两个选项『Off和Optimal』,一个是关闭动画压缩,一个是最优化压缩(应该是压缩效率最高,动画效果失真度可能也较高)
  • 选择了Keyframe Reduction或者Optimal压缩选项,就会有三个用于控制压缩选项的系数配置, Rotation Error,Position Error和Scale Error,这个三个参数默认都是0.5,越小呢精度就越高也就是说动画的失真度越小。
  • Clips,这个下面列出了这个FBX文件下包含的所有动画,我们在默认的动画文件基础上新建和删除动画片段(Animation Clip),当然每个动画片段都是可以指定起始帧和结束帧的; 以下的设置都是针对单个动画片段滴:
    • Loop Time,勾选这个选项之后,如果Animator处于播放这个动画状态时,在播放完第一遍这个动画片段之后,会自动循环从起始帧再次开始播放动画,如此循环往复。如果我们不勾选这个选项,例如Animator一直处于播放这个动画的状态,那么动画会定格在动画的结束帧,直到我们通过Animator切换这个Animator状态机的状态,切换到其他的动画;
      • Loop Pose和Cycle Offset,在勾选了Loop Time之后生效的两个选项,Loop Pose用于控制动画循环播放时,从结束帧切换到起始帧时,动画的动作可以无缝的衔接上,Cycly Offset就是用于控制循环的时候起始帧偏移用的;
    • Root Transform Rotation,根节点的旋转信息
      • Bake Into Pose,勾选后会将根节点每一帧的旋转方向信息烘焙到动画的骨骼运动中,在整个动画播放的过程中,根节点的旋转信息就不会在通过Root Motion作用到播放该动画的GameObject上了,这就意味着这个动画播放的过程中,该物体的Transform中的Rotation值不会因为动画中物体做了任何旋转而发生改变,而是会保持一个恒定的值,和该动画播放之前的旋转值保持一致;
      • Based Upon (at Start)或者Based Upon,根节点旋转的参考基准,有两个选项『Original和Root Node Rotation』这两个分别指的是动画文件中指定的旋转值和根节点旋转信息,其实我更愿意将Original理解为动画中原点的旋转值,因为在整个动画播放的过程中,所有骨骼肯定都会有旋转和位移的变换,但是动画的原点其实一定都是确定的,这样理解感觉更简单也更形象一些,勾选了Bake Into Pose之后,就会变成Based Upon而不勾选Bake Into Pose就会保持为Based Upon (at Start),这个目前还木有理解为啥;
      • Offset,旋转角度与参考基准的偏移(以度为单位);
    • Root Transform Position(Y),根节点位移信息(Y轴)
      • Bake Into Pose,勾选后会将根节点每一帧在垂直Y轴方向上的运动信息烘焙到动画的骨骼运动中,在整个动画播放的过程中,根节点在Y轴方向的所有位移信息不会通过Root Motion作用到播放该动画的GameObject上,这就意味着我们在场景中看到物体在Y轴上有位移,例如向上或者向下移动,但是该物体的Transform中的Position信息不会发生改变,会跟动画播放之前的Position信息保持一致;
      • Based Upon或者Based Upon (at Start),这个貌似有点不一样哦,在选中Bake Into Pose之后会变成Based Upon (at Start),不勾选的时候是Based Upon,不过这个就能理解了。不烘焙的话,那么Root Motion中Y轴的变化就依赖于选择的『Original或者Root Node Position』的Y轴位移变化,如果选择烘焙的话,那么就以这个动画的起始帧的Y轴作为整个动画Root Motion的Y轴位移,在整个动画播放的过程中,Y轴的位移都是恒定不变的;
      • Offset,垂直方向上的偏移;
    • Root Transform Position(XZ),根节点位移信息(水平面,XZ轴)
      • Bake Into Pose,勾选后会将根节点每一帧在水平面(X和Z轴)方向上的运动信息烘焙到动画的骨骼运动中,在整个动画播放的过程中,根节点在X和Z轴方向的所有位移信息不会通过Root Motion作用到播放该动画的GameObject上,这就意味着我们在场景中看到物体在水平面上移动,但是该物体的Transform中的Position信息不会发生改变,会跟动画播放之前的Position信息保持一致,假如动画中物体会向前移动3米,我们会看到物体在整个动画播放过程中确实在向前移动,播放到最后一帧时确实向前移动了3米,但是当这个动画播放完毕之后,切换到任何其他的动画时,物体会直接闪回这个动画播放前物体所在的位置,所以通常我们需要保留动作位移的动画都不会勾选这个选项。那这个选项有神马用捏?例如某些待机动画,我们其实希望物体只是做一个待机动作,但是实际上不想让物体在水平方向上有位移,这个时候就可以勾选这个选项了,到时候看起来物体就像是钉在水平面上了;
    • Mask,这个掩码主要是用于控制动画播放过程中,各个骨骼之间的运动变换的
      • Definition,可以选择从动画文件创建也可以选择使用其他动画文件中已经创建好的配置;
      • Transform,这个就是动画文件中所有骨骼的层级关系,可以选择勾选那些需要应用动画中运动变换的骨骼;
    • Curves,这个主要用于设置某些跟动画相关的参数用,例如控制整个动画播放过程中的速度参数之类的,在动画播放的过程中可以通过Animator.GetFloat(ParamName)函数来读取曲线的值,曲线的X轴为动画的时间轴,Y轴为曲线的值,曲线可以通过曲线编辑器进行增加关键点,调整曲线斜率进行编辑,读取时默认会根据当前动画播放的进度作为X轴的值进行读取,一个动画片段可以有多个曲线;
    • Events,这个是用于在动画播放的过程中触发事件的,例如整个动画中有起跳和落地两个事件需要在准确的时间点触发并通知到游戏中其他的对象,那么就可以在Events时间轴上新增事件通知,设置好触发的方法名称和参数,在播放该动画的GameObject上确保有某个脚本中有与该事件通知的方法签名一致的方法就好了,当动画播放到触发通知时间时,就会向GameObject广播该时间通知,脚本中方法签名一致的方法就会被回调了,那我们就可以做我们需要做的事情了。

说了这么多貌似跟Root Motion不是很相关的东西,那么究竟我们今天的主题是啥呢?肯定还是Root Motion这货。主要因为动画导入时的设置对于Root Motion的应用影响非常直接,所以前面絮絮叨叨地把这个动画导入设置都罗列了一遍。

回到正题,Generic动画应用Root Motion有以下几个特点:

  1. Root Motion仅仅作用于GameObject在X和Z轴上的位移变换,不影响Y轴上的位移。例如现在播放一个从地上向前空翻之后落地的动画,设置Animator的applyRootMotion为True,也就是应用Root Motion,那么动画在播放过程中,物体会在水平方向和垂直方向上都按照实际动画的运动轨迹进行运动,如果将applyRootMotion设置为False,那么我们就只能看到动画在原地起跳然后再落地,动画中原本应有的在水平方向的位移就没有了;
  2. Root Motion与导入动画时设置Root Transform Position(XZ)是直接相关的,如果我们选择了将X和Z轴方向上根节点的位移烘焙到动画骨骼运动中的话,那么动画播放过程中不论我们是否将Animator的applyRootMotion设置为True还是False,动画播放过程中物体在X和Z上的移动是一定的,因为这个已经被烘焙到骨骼动画中,只要动画播放,物体就会移动,但是在动画播放的过程中GameObject的Position值不会改变,在动画结束后我们切换到其他动画的时候,其他动画开始播放时的GameObject的位置会回到这个动画播放前的位置,所以如果我们需要对某个动画应用Root Motion的话,那么这个动画在导入的时候就不要烘焙其在X和Z轴方向上的Root Transform Position,让Unity自行根据动画中根节点的位移进行位移计算GameObject的位置信息;
  3. 注意Root Motion与Rigidbody.Velocity属性的关系,如果有两个动画A和B,播放A动画的时候,希望A动画应用Root Motion,而在播放B动画的时候不想应用Root Motion,那么就直接在切换到动画B的时候,将Animator的applyRootMotion设置为False就OK了。但是如果播放动画的GameObject带有Rigidbody组件,那么需要注意一点,在播放A动画时Rigidbody的Velocity并不会在切换到B动画时清零,也就是说如果A动画的运动速度较快,那么切换到B动画的时候,如果希望B动画播放的时候GameObject按照自己的设定轨迹运动,就需要自行手动在切换到B动画之前将Rigidbody的Velocity属性清零,防止GameObject按照A动画的运动惯性继续运动。这个问题在没有Rigidbody组件的GameObject上不会存在;

这边再岔开一下,说说这个动画跟Rigidbody之间的关系:

  1. 如果我们没有将Root Transform Position的Y和XZ轴进行烘焙的话,那么在动画播放的过程中,Rigidbody将会自动获得动画中物体运动的速度信息,直接通过Rigidbody.Velocity属性就可以获得;
  2. 如果我们将Y轴进行烘焙,那么Rigidbody.Velocity在Y轴上的值将会一直为0,对于XZ轴也是一样的,如果烘焙了XZ轴的位移,那么整个动画播放过程中,Rigidbody.Velocity在X和Z轴上的值都会为0;
  3. 如果播放动画的物体没有Rigidbody组件,那么动画的运动都会仅仅按照动画实际的位移来进行逐帧播放,不会出现上文中提到的动画播放切换之后还存在的运动惯性问题,因为物理引擎依赖于Rigidbody组件,如果没有该组件,所有动画的播放都只是逐帧播放动画,不会存在速度的概念只有移动位移。
  4. Rigidbody使用使用重力对于动画在Y轴上的位移没有任何影响,不论是否对Root Transform Position的Y轴进行了烘焙。

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,代码如下:

[code lang=”csharp”]
mAnimator.SetTrigger ("KickFloat");
rigidbody.velocity = velocity; // NPC斜向上方飞行的初速度
[/code]

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

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

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

尼玛这下清净了吧,哥哥我半个下午的时间就是在不断的怀疑人生和自我啊,不过看在最终还是搞定的份上,我也就不再说啥了。然后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动画中的位移,可以移步这里查看。

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

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

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

技能编辑器

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

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

我们想要的是,特效路径这个Label后边跟着的这个可输入的TextField文本输入框可以接受拖拽文件的方式来进行路径的输入。那么我们需要做的就是在这个『技能编辑器』的Window中监听鼠标拖拽的事情,同时判断鼠标拖拽时鼠标的位置是否处于这个可输入的TextField文本输入框之中就OK了,上代码。
[code lang=”csharp”]
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 ();
[/code]

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

 

 

 

 

 

 

============== 更新于 2015-01-13 11:56:41 ==============

另外,所有Mecanim的Generic动画都会有一个依赖的Avatar Definition信息(不知道是不是翻译成蒙皮信息),在这个Avatar中指定指定Root node就好了。

在跟我们的动画设计师沟通之后,发现其实在3DS MAX中制作的动画都会自动生成一个Bip001 Footsteps的骨骼节点,该节点直接绑在Bip001根节点上,在3DS MAX中预览动画播放的全程中,这个节点一直都是Bip001节点在水平面上的投影,也就意味着这个点一直只在水平面上移动,是非常适合用来做Root Motion应用的Root node的,但是在将动画导入到Unity中之后,我们再查看Bip001 Footsteps这个节点的位移和在3DS MAX中并不一样,并不是一直处于模型质心的正下方,所以目前还无法使用Bip001 Footsteps这个节点作为Root Motion要使用的Root node,只能选择Bip001这个节点。

在我们通过设置Animator.applyRootMotion字段为True以及对动画依赖的Avatar Definition设置合适的Root node之后,动画在播放的过程中,会自动调整播放该动画的GameObject的Transform的位置信息(仅限X和Z轴)。那么在这个过程中我们能否再通过其他的方法,例如Rigidbody.MovePosition和Transform.position等方法来调整GameObject的Position呢?显然是可以的,在Animator应用Root Motion调整GameObject的Position同时,我们也可以通过其他的方法调整GameObject的Position,效果是叠加的。