
在上节中,遗留的问题有很多,我们将在这节逐一击破。
第一个就是冲量问题,我打算在角色使出一段攻击时,给它一段曲线的位移量,这招我们已经用过两次了(在实现翻滚和后跳的过程中),现在再来实现一次应该也不成问题。
编辑动画本身,在Animation的Curves点+号新增一条曲线,命名为attack1hAVelocity:

然后编辑曲线,我调整的曲线为:

然后这条曲线的数值应该给到动画的进行时作为向前冲量,所以应该在动画上新增组件FSMCallOnState:

然后在ActorController.cs里接收其发送的信息:
void OnAttack1hAState()
{
thrustVec = model.transform.forward * anim.GetFloat ("attack1hAVelocity");
}

能看到现在平A会有一段向前的小位移。
第二个想要解决的问题就是实现attack这层Layer权重值的动态变化。对于这个问题,我的想法是:在进入到一段攻击的动画后(键入j触发转换),应该在攻击动画的进行时逐渐升高该层的权重值,直至拉满;然后攻击动画播放完毕,转回闲置状态之后,在其动画播放时逐渐降低该层的权重值,直至为零。换个说法就是这两个过程都应该在
OnStateUpdate()
里完成,至于怎么完成,我们应该心中有数,那就是用Mathf.Lerp()
。对于在attack1hA动画,应该逐渐拉满权重值:

我的思路是,应该在每次进入该动画时,设置目标权重值为1;在每次进入闲置动画时,设置目标权重值为0。
private float layerWeightLerpTarget;
void OnAttack1hAEnter() //进入Attack1hA动画
{
pi.InputEnabled = false;
//anim.SetLayerWeight (anim.GetLayerIndex ("attack"),1.0f);
layerWeightLerpTarget = 1.0f;
}
void OnAttackIdleEnter() //进入AttackIdle动画
{
pi.InputEnabled = true;
//anim.SetLayerWeight (anim.GetLayerIndex ("attack"), 0);
layerWeightLerpTarget =0;
}
然后分别在各动画的进行时对当前权重值和目标权重值进行插值处理,获取当前权重值用的函数是Animator.GetLayerWeight(int layerIndex)
,用法与上节提到过的SetLayerWeight()
一致,区别只不过一个是Set,一个是Get而已,不再赘述。
void OnAttack1hAState()
{
thrustVec = model.transform.forward * anim.GetFloat ("attack1hAVelocity");
int layerIndex = anim.GetLayerIndex ("attack");
float currentLayerWeight = anim.GetLayerWeight(layerIndex); //获得当前权重值
currentLayerWeight = Mathf.Lerp(currentLayerWeight,layerWeightLerpTarget,0.2f); //与目标权重值进行插值处理,每次追20%
anim.SetLayerWeight (anim.GetLayerIndex ("attack"),currentLayerWeight);
}
void OnAttackIdleState()
{
int layerIndex = anim.GetLayerIndex ("attack");
float currentLayerWeight = anim.GetLayerWeight(layerIndex);
currentLayerWeight = Mathf.Lerp(currentLayerWeight,layerWeightLerpTarget,0.2f);
anim.SetLayerWeight (anim.GetLayerIndex ("attack"),currentLayerWeight);
}
这段代码其实可以简化处理,取消currentLayerWeight
和layerIndex
这两个临时变量,把其右值直接放在参数的位置,不过这样可读性变差了,不适合在这里展示。

可以看到现在这个动画的切换就很流畅,令人赏心悦目。而attack权重值的渐变也很明显,可喜可贺。
现在来关注最后一个问题,那就是attack信号与其他动画的的冲突问题,两个动画碰撞在一起会产生什么火花?上图直接告诉你:

在下落途中按攻击键,那么原本会播放的下落动画就会被攻击动画所覆盖。

这个是我在奔跑状态下键入j后立马键入空格的结果,这个输入对应这个结果那么原因其实不难猜到,那就是两个Trigger都触发了,但由于攻击动画覆盖Base Layer的原因,所以只看到了攻击动画,没看到跳跃动画。且我们的动画安排是跳跃接翻滚,在攻击动画播放完毕后,该层权重值归0(而Base Layer的权重值始终为1),刚好也在进行跳跃到翻滚的转换,所以最后就播放了翻滚的动画。
下面这个动图也是同样原因引起,只不过是我键入j跟空格的时机跟上图不一样,这次我是j跟空格同时键入,能看到是跳劈接翻滚:

看起来还蛮帅的这个动作,可惜这并不合理。这是跳跃动画没播放(被覆盖),但跳跃动画的OnJumpEnter()执行了,产生了往上的冲量造成的。
我们应该让attack的触发条件变得更为苛刻,以减少这些冲突的可能性。
对于第一个,我们可以做一个状态检查,规定当只有在播放ground这个Blend Tree里的动画时才能攻击,空中或下落图中则不能。于此,我们应该需要一个能得知当前在状态机的哪个状态的函数,但是Unity并没有与之相符的API,仅有一个能查询当前状态是不是某个指定状态的函数
public AnimatorStateInfo GetCurrentAnimatorStateInfo(int layerIndex);
这个函数接收一个Layer的索引值,然后返回当前状态信息的
AnimatorStateInfo
,而至于AnimatorStateInfo
包含了状态的哪些信息,我们可以来简单看一下,这并没有什么坏处:
Properties | Mean |
---|---|
fullPathHash | The full path hash for this state. |
length | Current duration of the state. |
loop | Is the state looping. |
normalizedTime | Normalized time of the State. |
shortNameHash | The hash is generated using Animator.StringToHash. The hash does not include the name of the parent layer. |
speed | The playback speed of the animation. 1 is the normal playback speed. |
speedMultiplier | The speed multiplier for this state. |
tagHash | The Tag of the State. |
Public Methods | Mean |
---|---|
IsName | Does name match the name of the active state in the statemachine? |
IsTag | Does tag match the tag of the active state in the statemachine. |
这么多信息当中,我们要用到就是AnimatorStateInfo.IsName()
,它的功能是:检查指定的状态名是否是当前在活动(active)状态的状态名,返回的结果是一bool值。
于是乎,我觉得应该把状态检查做为一个函数,这样即使别的地方要用到,直接调用函数即可。那么这个函数应该要实现的是:通过给与的Layer名字和状态名字去查询当前是不是在某个状态上,用bool变量反映结果。显然,它需要两个string变量做参数,分别是layerName和stateName,并返回一个bool变量。
bool CheckState(string stateName, string layerName = "Base Layer")
{
int layerIndex = anim.GetLayerIndex (layerName);
bool result = anim.GetCurrentAnimatorStateInfo (layerIndex).IsName (stateName);
return result;
}
记得把layerName转回索引值。然后增加触发attack Trigger的条件:只有在ground状态时才能攻击。
if (pi.attack && CheckState ("ground")) {
anim.SetTrigger ("attack");
}
现在在空中是A不出来了。
然后对于attack与jump两个Trigger的冲突问题,这个比较棘手,试过很多方法都不奏效,反而有个简单粗暴的方法效果还好一点,那就是当玩家键入攻击按钮时,我不管此时的jump信号是true or false,一律都设为false。
if (attack) {
jump = false;
}
这样当玩家同时键入攻击键和跳跃键时,只会当做键入攻击键处理,这个做法让跳劈的成功率下降了很多,虽然偶尔会有一两次是能劈出来,但大部分时候是使不出来的。当然这可能不是最好的解决方法。
最后还有一个小细节别忘了,我们这个attack是个Trigger信号,所以记得要Reset Trigger,与实现jump信号一样。在attck层的idle动画添加FSMClearSignal:

网友评论