Unity——#19 举盾
在RPG类游戏中,除了有攻击,也有和攻击同等重要的动作,那就是防御。本节将讨论如何实现防御。不过在有了实现攻击的先例经验,我想实现防御的难度已经降低了。
先着手把动画机(Animator)里的动画准备好:添加防御动画,采取与攻击动画同样的命名方式,命名为defense1h;添加闲置(idle)动画链接Entry;新增动画机bool型参数(Prameters),命名为defense;两个动画间互拉Transition,都消勾Has Exit Time ,添加触发条件(Conditions)为defense参数,其中idle→defense1h的defense为true,defense1h→idle的defense为false。
现在我打算为这个模型添加一块盾牌和木棍,说是这么说,其实添加Cube做盾牌。Cylinder做木棍。为此我们需要找到这个模型的右手手掌,和左手肘关节处。在模型的右手底下新增empty改名为WeaponHandle,再在WeaponHandle底下新增一3D Oeject Cylinder命名为sword:
然后调整Cylinder的大小和旋转角和WeaponHandle的位置,使其刚好在模型右手手掌的虎口处,这样角色在握拳时能刚好握住Cylinder而不穿模:
现在来看看挥起来顺不顺手:
看起来Ok。现在来添加一块盾牌,同样找到左手肘关节处,在其底下创建empty,命名为WeaponHandle,再在WeaponHandle底下新增一3D Oeject Cube,命名为Shield:
为什么不找左手(LeftHand),手背放盾牌不是更好吗?因为我们的二段攻击动画有一个左手往外拐的动作:
如果戴上盾牌后,有可能会造成穿模(手掌穿出盾牌),所以打算把盾牌放在肘关节以下的位置,通过偏斜一点盾牌的角度来避免这个问题。
通过调整WeaHandle的位置和shield的大小,使其掌心不穿出Cube:
现在来看看效果如何:
因为闲置动画的左手就是摆在胸前,导致其放上盾牌后有点变成像防御状态的样子。但是这并不是我想要的,不能让玩家产生闲置状态能挡住敌人攻击的误解。所以我们要改变在闲置动画下左手手臂的位置。但是这个动画是是别人做好的,我不会改啊,怎么办?不怕,Unity提供能用代码改变动画中人物关节位置的方法,那就是
OnAnimatorIK()
,这个函数有点像我们之前用过的OnAnimatorMove()
,前者是改变动画关节位置,后者处理动画的位移量。我们可以看看它的Signature和官方描述MonoBehaviour.OnAnimatorIK(int)
:
Parameters | Means |
---|---|
layerIndex | The index of the layer on which the IK solver is called. |
Description
Callback for setting up animation IK (inverse kinematics).OnAnimatorIK() is called by the Animator Component immediately before it updates its internal IK system. This callback can be used to set the positions of the IK goals and their respective weights.
在解释这个函数之前,我想先简单讨论一下何为IK。IK,即Inverse kinematics,反向动力学。引用一处IK学习笔记中关于反向动力学的解释:
反向动力学与前向动力学相反,先给出骨骼点的位置,然后要求出运动骨骼以什么样的位移、旋转、缩放可以使得骨骼点到达指定的位置。
用加法来做比喻的话,前向动力学是已知算式1+1+3,求结果;反向动力学是已知结果5,求算式。可以看出,反向动力学计算得到的结果往往不是的,因此比前向动力学要复杂许多。
能看出其实IK是一门非常复杂的学科,如果日后有机会必定深入学习。但今天我们仅仅是简单地使用一下OnAnimatorIK()
,不做深入讨论。
回到OnAnimatorIK()
上,这个函数会在动画组件更新它内部的IK系统前被调用。用途是能够用来设置IK目标的位置和它们各自的权重。说白了就是这个函数能在IK更新前,对动画的IK做最后的调整,这个调整就是对骨骼点的运动做位移、旋转和缩放。那么究竟怎么做调整,那就要用到另外的API了。
我们想要的是把闲置动画里抬起来的左手给放到左大腿边上,所以要对骨骼点进行旋转,这要用到两个函数:Animator.GetBoneTransform()
和Animator.SetBoneLocalRotation
,接下来对这两个函数分别做出解释:
public Transform GetBoneTransform(HumanBodyBones humanBoneId)
:
Parameters | Means |
---|---|
humanBoneId | The human bone that is queried, see enum HumanBodyBones for a list of possible values. |
Description
Returns Transform mapped to this human bone id.
如果想对某个骨骼点进行一些操作,那就先获得这个骨骼点的位置;要想获得这个骨骼点的位置,就要用这个骨骼点对应的枚举值(enum)去查询(queried)。等会用到这个函数时我们就知道怎么查询左下臂。
public void SetBoneLocalRotation(HumanBodyBones humanBoneId, Quaternion rotation)
:
Parameters | Means |
---|---|
humanBoneId | The human bone Id. |
rotation | The local rotation. |
Description
Sets local rotation of a human bone during a IK pass.Can be used to create rotation IK goals for any human bone. Ex: Control lower and upper body independantly by setting Hips and Spine local rotation during an IK pass.
这个函数就是对某指定的骨骼点进行旋转。不难理解。描述给的例子是通过髋部和脊柱(Hips and Spine)的局部旋转来独立控制上下身体。
我再重申一下我的需求:把闲置动画里抬起来的左手给放到左大腿边上,但是在举盾(进入defense1h动画)时,要取消这个修改不然这个举盾动画也会被一并改掉。
在准备Add Component前,需注意的是要想这个OnAnimatorIK()
生效,第一个要做的就是在defense Layer层级上把IK Pass上勾上,宣告我这个OnAnimatorIK()
是作用于此Layer的,在之前实现一段攻击时也有提到过这个。
第二个就是把你写的这个函数放在有Animator组件的对象的兄弟层级上,这里我就在Ybot上直接Add Component,命名为LeftArmAnimFix:
把其中的
Start()
和Update()
函数给砍了,这里用不上。然后宣告一个Animator对象,在Awake()
获得Ybot上的Animator组件:
private Animator anim;
void Awake(){
anim = GetComponent<Animator> ();
}
然后实现OnAnimatorIK()
函数,思路是先在里面获得左前臂的位置,然后宣告一个Vector3
变量,把它曝露出去,方便我们对其作出修改(在改到合适位置后在把它写死),然后再把这个Vector3
变量转换为四元数喂给Animator.SetBoneLocalRotation()
。
public class LeftArmAnimFix : MonoBehaviour {
private Animator anim;
public Vector3 tempLocalRotation;
void Awake(){
anim = GetComponent<Animator> ();
}
void OnAnimatorIK()
{
if (anim.GetBool("defense") == false) {
Transform leftArm = anim.GetBoneTransform (HumanBodyBones.LeftLowerArm);
leftArm.localEulerAngles = tempLocalRotation;
anim.SetBoneLocalRotation (HumanBodyBones.LeftLowerArm, Quaternion.Euler (leftArm.localEulerAngles));
}
}
}
这个HumanBodyBones
的枚举值有很多,我们要的是左前臂,所以是LeftLowerArm:
由于我们设置的欧拉角,所以要把它转换为四元数(函数要求),转换的方法就是
Quaternion.Euler()
。现在我们就可在外部调整我们的手臂了,记住先在Play mode下调整好,然后Copy Component ,取消Play mode,再Paste Component Values。这是调好的角度,已经基本实现了我的需求:Last but not least, 实现举盾动作。这里我只给出手柄的实现,键盘就不再另说了。首先在IUserInput.cs里宣告一bool变量命名为defense:
[Header("===== State =====")]
...
public bool defense;
然后在JoystickInput.cs里用RB键举盾:
//角色举盾
defense = Input.GetButton(keyButRB);
在ActorController.cs里,将输入信号defense与动画机参数defense联系起来:
anim.SetBool ("defense",pi.defense);
现在代码方面完成了,但是我差点了忘了讲两件事情,就是给defense Layer灌个Avatar Mask和它的权重。这里我把新建一个Avatar Mask,只覆盖左手,命名为lefthand,然后灌给defense Layer:
因为这层只覆盖其他层动画的左手,所以可以不考虑权重变化的问题,直接拉满就行:
现在应该就大功告成了,来看看效果: