目录
3.5 Solo and Mute functionality
3.7.3.1 Root Transform Rotation
3.7.3.2 Root Transform Position (Y)
3.7.3.3 Root Transform Position (XZ)
3.7.4 Tutorial: Scripting Root Motion for “in-place” humanoid animations
1. 简介
Unity 有两个具有不同功能和性能特征的动画系统:
- Unity’s animation system, 也就是 Mecanim, 使用 Animator component, Animation window, Animator window
- Unity’s Legacy Animation system
Unity’s animation system 在大多数情况下推荐使用,如果动画包含很多 animation curves
blending.
The Legacy Animation system 对于简单的场景更好,比如简单的 UI 动画
1.2 Animation的工作流程
Unity’s animation system 基于 Animation Clips,每一个clip 都可以看作是一个单一的线性的记录,比如对旋转,位置,缩放等。它可以在unity内创建也可以从外部导入。
Animation Clips 然后导入Animator Controller,它就类似一个状态机 “State Machine”,它跟踪当前应该播放哪个剪辑,以及动画何时应该更改或混合在一起。
Unity’s Animation system 也提供了很多处理人物动画的功能,比如可以把各种来源的动画和人物做绑定,其中可以调节muscle definitions,这些功能是通过 Unity’s Avatar系统完成的
Animation Clips、 Animator Controller、 Avatar通过Animator Component 绑定到一起. 它引用了Animator Controller,Avatar(如果需要),Avatar 只有人形动画才需要,其他类型的动画,只有controller就够了。
1.3 在Animation中旋转物体
Unity使用插值来计算GameObject在动画中如何从一个方向移动到另一个方向。
不同的 interpolation 方法,旋转的方向不一样,但是结果是一样的,Unity提供了三种方法:
- Euler Angles interpolation和理解中的旋转一致,比如旋转超过了360°,那就是一圈多。
- Euler Angles (Quaternion) interpolation 把旋转的信息烘焙保存成quaternion曲线. 此方法使用更多内存,但运行时速度略快
- Quaternion 将GameObject在最短的距离上旋转到一个特定的方向. 例如,不管旋转值是5度还是365度,GameObject都会旋转5度.
1.3.1 从外部导入的动画注意事项
Animation from external sources 通常包含Euler角格式的关键帧, Unity 会对这些动画重采样,并生成新的关键帧,避免旋转不一致的情况。
例如,如果你有两个相隔6帧的关键帧,x值从0到270度,那么GameObject就会向相反方向旋转90度,因为这是获得相同结果的最短方法。相反,Unity重新采样并在每一帧上添加一个关键帧,所以关键帧之间的旋转只有45度,旋转是正确的。
如果导入动画的四元数重采样不能满足您的需求,您可以关闭动画重采样,并在运行时使用原始的Euler动画关键帧. For more information, see Euler curve resampling.
2. Animation Clips
Animation Clip 在Untiy中有两种方式创建:一种是从外部导入,另一种是在unity内部创建
从外部导入的动画
外部导入的animation的种类可以有:
- Humanoid animations 人形动画
- Animations 通过3D软件创建的动画
- 来自第三方库的动画集 (eg, from Unity’s asset store)
- 从一个导入的时间轴上剪切和切片多个剪辑 timeline
内部创建的动画
Unity’s 动画窗口可以创建的动画类别包括:
- 位置,缩放,旋转
- 或者是一些组件的属性,比如材质球的颜色,灯光的强度,声音的大小
- 或者是脚本里的一些属性 scripts,比如float,integer, enum, vector and Boolean
2.1 外部导入的动画
在查看导入的动画关键帧时,动画窗口提供动画数据的只读视图。要编辑这个数据,需要在Unity中创建一个新的空白Clip,然后把动画数据粘贴到新的里面。
2.1.1 Humanoid Avatars
因为人形角色在游戏中很常见,Unity为此提供了专门的工作流和一个处理人形动画( humanoid animations)的工具集。
The Avatar system 能够定义动画与人身体的映射关系。
因为人的身体结构都是相似的,所以一个人的动画也可以映射到另一个人身上,允许retargeting 和 inverse kinematics(IK).
2.2 内部创建动画
动画窗口有两种编辑预览模式:, Dopesheet and Curves.
从左至右:
- Preview mode (toggle on/off)
- Record mode (toggle on/off) Note: Preview mode is always on if record mode is on
- 将播放头移动到剪辑的开头
- 移动播放头到前一个关键帧
- Play Animation
- 移动播放头到下一个关键帧
- 将播放头移动到剪辑的末端
快捷键:
- 按逗号(,)转到前一帧.
- 按周期(.)转到下一帧.
- 按住Alt键并按逗号(,)转到前面的关键帧
- 按住Alt键并按下句点(.)进入下一个关键帧。
- 在curve状态下,按F键能够聚焦曲线,以最佳视图查看曲线
锁定动画编辑器窗口:
添加关键帧的方法有三种:
第一种:右键
第二种:
第三种:快捷键
K为所有的动画都添加关键帧
这里有一些例子:
- 使光的颜色和强度动起来,使它闪烁、闪烁或颤动.
- 使一个循环音源的音高和音量产生动画效果,为吹起的风、运转的引擎或流动的水带来生机,同时将声音资产的大小保持在最小.
- 使材料的纹理偏移产生动画效果,以模拟移动的带或轨迹、流水或特殊效果.
- 动画发射状态和速度的多个椭球粒子发射器创建壮观的烟花或喷泉显示.
- 给脚本的变量创建动画,使事情随着时间的推移产生不同的行为.
2.3 使用Animation Events
- 把脚本挂载到动画物体上,然后声明一个方法
- 在动画的关键帧上面,添加一个动画事件
- 在触发该关键帧的时候,可以触发该动画,该方法接受的参数可以是基本类型、object,AnmationEvent对象
3. Animator Controllers
3.1 Animation Parameters
Default parameter 它们可以有四种基本类型:
- Integer - 整数
- Float - 小数
- Bool - bool 值,以方块表示
- Trigger -触发值,触发完成后,回到初始状态,以圆圈表示
可以通过方法: SetFloat, SetInteger, SetBool, SetTriggerand ResetTrigger.设置其值,
ResetTrigger:是重置当前正在活跃(Settrigger是播放完自动重置)的动画的状态,让它回到初始状态,比如按上键往上跳的时候,有可能正在下蹲,所以要重置下蹲的动画,回到站立状态,再往上跳:
void Update () {
float h = Input.GetAxis("Horizontal");
float v = Input.GetAxis("Vertical");
bool fire = Input.GetButtonDown("Fire1");
animator.SetFloat("Forward",v);
animator.SetFloat("Strafe",h);
animator.SetBool("Fire", fire);
}
void OnCollisionEnter(Collision col) {
if (col.gameObject.CompareTag("Enemy"))
{
animator.SetTrigger("Die");//只设置值,在完成后,自动重置
}
}
3.2 State Machine Behaviours
继承自State Machine Behaviour类, 把它添加到状态机中的某个状态上,当状态机enter,update,exit的时候,执行代码中的方法 。
原理:执行的时候,是调用脚本类的实例,去执行方法,所以大量的挂载脚本也会增加内存开销
如果多个状态有相同逻辑的,可以通过添加属性,实现共用,这样只生成一个实例:
using UnityEngine;
[SharedBetweenAnimators]
public class AttackBehaviour : StateMachineBehaviour
{
public override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
Debug.Log("OnStateEnter");
}
}
比如:
- 进入或退出状态时播放声音
- 在适当的状态下进行某些测试(如地面检测)
- 激活、控制与特定状态相关的特殊效果
using UnityEngine;
public class AttackBehaviour : StateMachineBehaviour
{
public GameObject particle;
public float radius;
public float power;
protected GameObject clone;
override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
clone = Instantiate(particle, animator.rootPosition, Quaternion.identity) as GameObject;
Rigidbody rb = clone.GetComponent<Rigidbody>();
rb.AddExplosionForce(power, animator.rootPosition, radius, 3.0f);
}
override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
Destroy(clone);
}
override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
Debug.Log("On Attack Update ");
}
override public void OnStateMove(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
Debug.Log("On Attack Move ");
}
override public void OnStateIK(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
Debug.Log("On Attack IK ");
}
}
3.3 Animator.StringToHash的优化点
- **性能**:整数比较比字符串比较要快得多,因为整数比较通常是单个指令,而字符串比较则需要逐字符比较直到找到不匹配或到达字符串末尾。
- **内存**:整数占用的内存空间小于字符串,特别是对于长字符串,这可以减少内存的使用和管理开销。
3.4 Sub-State Machines
类似组,如果多个动画,比如走路,跑步,跳跃,这几个总是一起出现,可以把它们作为合成sub-state,单独打成一个组
sub-state是以六边形表示的:
从外面连接的时候,可以选择要连接里面的那个state machine
(Up)表示组外面的state,和它连接时,也能选择连接外面的那个state machine
3.4 Animation Layers
Animation Layers 用来制作身体不同部位的动画,通过weight来管理各个层之间的关系,举个例子:一个人物下身用于跳跃行走,而上身用于投掷物体/射击,
点击层左上方的齿轮,来设置层的属性
Mask: 表示该层动画应用于哪些部位
BlendType:
- Override 表示覆盖其他层的动画,显示本层的动画
- Additive 表示当前层的动画叠加到其他层的上面,两者共同播放,通过weight管理,为了混合能成功,Additive层上的动画必须包含与前几层相同的属性
‘M’ 符号在图层侧边栏中可见,表示该图层应用了遮罩。
3.4.1 Animation Layer syncing
能够在不同的层中重用相同的状态机,Sync 同步层就是让该层复刻其他层的动画,它们的结构是相同的,但是各个状态的animation是可以不一样的,其他层可以从Source Layer 选择。
Timing:当前层和Source层同一个状态使用的动画时间长度不一致时,因为两个的动画可能不一致,不勾选timing那么复制的层按Source层的时间播放,否则Source层按复制层的时间播放,只有勾选sync,timing才可以勾选
比如:“Fatigued” 层同步base层. 状态机的结构布局和 base layer一样,
每个状态中使用的独立动画被替换为不同但适当的等效动画。
一个' S '是符号是可见的层侧边栏,以表明该层是一个同步层。
3.5 Solo and Mute functionality
在复杂状态机中,单独预览机器的某些部分的操作是有用的. 可以使用 Mute and Solo functionality:
- Mute 禁用一个Transition.
- Solo 只播放某一个Transition.
一个state可以设置多个Solo Transition,表示只能进入到设置为Solo 的Transition,
Solo 和 Mute 都打开 ,Mute 优先。
状态机上显示Solo 为绿色, Mute为红色
上面的图中,如果从State 0进入
, 只能进入 State A
和State B
.
3.6 Target Matching
通常在游戏中,会出现一种情况,角色可能需要跳过垫脚石或跳过并抓住头顶的横梁。
通过 Animator.MatchTarget来处理这种情况,例如:角色跳到一个平台上,现在有一个Jump Up的动画,首先,需要在动画剪辑中找到角色开始跳起的地方, 它在动画里面是在14.1%,然后在78.0%的时候将要落地
using UnityEngine;
using System;
[RequireComponent(typeof(Animator))]
public class TargetCtrl : MonoBehaviour {
protected Animator animator;
//the platform object in the scene
public Transform jumpTarget = null;
void Start () {
animator = GetComponent<Animator>();
}
void Update () {
if(animator) {
if(Input.GetButton("Fire1"))
animator.MatchTarget(jumpTarget.position, jumpTarget.rotation, AvatarTarget.LeftFoot,
new MatchTargetWeightMask(Vector3.one, 1f), 0.141f, 0.78f);
}
}
}
3.5.1 Animator.MatchTarget
自动应用角色的position、rotation,且同时只能发生一个match target,多的排队处理,如果设置的normallized time 大于clip的时间,按循环处理, Animator.applyRootMotion 必须打开才有效果。
using UnityEngine;
public class TargetMatchingManager : MonoBehaviour
{
public void MatchTarget(Vector3 matchPosition, Quaternion matchRotation, AvatarTarget target, MatchTargetWeightMask weightMask, float normalisedStartTime, float normalisedEndTime)
{
var animator = GetComponent<Animator>();
if (animator.isMatchingTarget)
return;
//Repeat 类似模运算,只不过它是浮点数的,比如(3,2.5)就等于3对2.5求模=0.5,
//表示在0-2.5之间徘徊
float normalizeTime = Mathf.Repeat(animator.GetCurrentAnimatorStateInfo(0).normalizedTime, 1f);
if (normalizeTime > normalisedEndTime)
return;
animator.MatchTarget(matchPosition, matchRotation, target, weightMask, normalisedStartTime, normalisedEndTime);
}
}
3.6 Inverse Kinematics
大多数动画是通过旋转骨骼中的关节产生的, 子关节的位置会随着父关节的旋转而改变, 这个叫做: forward kinematics(前向运动学)
但是在实践中通常有用的是,给定一个位置,向后决定关节的位置,比如:想要一个角色的脚在一个不平整的路面上,它的关节是怎么样的,这种从后往前倒推关键位置的方式叫做: Inverse Kinematics (IK) 反向运动学,即子物体决定父物体的位置
如果为角色设置IK ,通常有一个与角色交互的物体对象,在脚本中通过下面的方法设置 IK:
比如: SetIKPositionWeight, SetIKRotationWeight, SetIKPosition, SetIKRotation,SetLookAtPosition, bodyPosition, bodyRotation
using UnityEngine;
public class Example : MonoBehaviour
{
Transform objToPickUp;
Animator animator;
void Start()
{
animator = GetComponent<Animator>();
}
void OnAnimatorIK(int layerIndex)
{
float reach = animator.GetFloat("RightHandReach");
animator.SetIKPositionWeight(AvatarIKGoal.RightHand, reach);
animator.SetIKPosition(AvatarIKGoal.RightHand, objToPickUp.position);
}
}
在上面的插图中,我们展示了一个人物抓着一个圆柱形的物体。我们如何才能做到这一点?
using UnityEngine;
using System;
using System.Collections;
[RequireComponent(typeof(Animator))]
public class IKControl : MonoBehaviour {
protected Animator animator;
public bool ikActive = false;
public Transform rightHandObj = null;
public Transform lookObj = null;
void Start ()
{
animator = GetComponent<Animator>();
}
//a callback for calculating IK
void OnAnimatorIK()
{
if(animator) {
//if the IK is active, set the position and rotation directly to the goal.
if(ikActive) {
// Set the look target position, if one has been assigned
if(lookObj != null) {
animator.SetLookAtWeight(1);
animator.SetLookAtPosition(lookObj.position);
}
// Set the right hand target position and rotation, if one has been assigned
if(rightHandObj != null) {
animator.SetIKPositionWeight(AvatarIKGoal.RightHand,1);
animator.SetIKRotationWeight(AvatarIKGoal.RightHand,1);
animator.SetIKPosition(AvatarIKGoal.RightHand,rightHandObj.position);
animator.SetIKRotation(AvatarIKGoal.RightHand,rightHandObj.rotation);
}
}
//if the IK is not active, set the position and rotation of the hand and head back to the original position
else {
animator.SetIKPositionWeight(AvatarIKGoal.RightHand,0);
animator.SetIKRotationWeight(AvatarIKGoal.RightHand,0);
animator.SetLookAtWeight(0);
}
}
}
}
选中弹出的菜单中的IK Pass复选框
3.7 Root Motion
3.7.1 Body Transform
Body Transform 就是胸部,是角色的中心。
它的位置和旋转存储在 Animation Clip 中(using the Muscle definitionsset up in the Avatar). 它们是唯一存储在clip中的世界空间曲线。其它的,比如 muscle curves 和 IK goals (Hands and Feet) 是相对于Body Transform进行存储的
3.7.2 Root Transform
它是Body Transform在Y轴上面的投影面,每一帧都会计算Root Transform,它会改变游戏物体的Transform,从而让游戏物体移动。
下面的圈代表root transform
3.7.3 Animation Clip属性面板
Root Transform Rotation, Root Transform Position (Y) and Root Transform Position (XZ) -用来控制从Body Transform映射到Root Transform. Body Transform 可以转到 Root Transform.
3.7.3.1 Root Transform Rotation
- Bake into Pose:方向和Body Transform保持一致,表示clip不会改变游戏对象的旋转,即角色不会因为动画旋转而旋转,只有root 的转向,在clip开始和停止时的差距很小,才使用这个选项, 右边的灯为绿色表示 clip的方向和面板上的参数匹配的很好,比如径直走路或跑步
- Based Upon: 设置Root的方向是基于什么的. Body Orientation: 默认选项,表示Root的方向基于胸部的方向,大多数动画都可以使用,比如 walks, runs, jumps, 但是当身体的旋转和Root 方向不一致时,就不能使用这个选项了,比如扫射,身体的方向是变化的,而Root的方向不变,这时可以使用Offset 选项;当美术已经在模型中嵌入了旋转,使用Original选项自动找到偏移量.
- Offset: 当 Based Upon 为Offset 选项时旋转的偏移
3.7.3.2 Root Transform Position (Y)
- Bake Into Pose: 运动的Y分量和Body Transform 保持一致. 即Clip不会改变游戏物体的高度。大多数都是都选上的,除了一些特定的动画,比如跳跃和跳下。Note: 当勾选时,
Animator.gravityWeight = 1,
当disabled = 0
. gravityWeight 表示两个state之间切换的权重值 - Based Upon: Original、Mass Center (Body)、 Feet, 当Bake Into Pose 不勾选时,Feet 选项可以很方便的更改角色的高度,Root Transform Position Y 始终和最低脚的高度保持一致。
- Offset:当 Based Upon 为Offset 选项时高度的偏移
3.7.3.3 Root Transform Position (XZ)
和前两个一样
3.7.4 Tutorial: Scripting Root Motion for “in-place” humanoid animations
有时候场景中人物的动画,并不会移动角色的位置,或者说需要在脚本中修改 “root motion”,下面是一个方法:
- 打开模型导入的属性面板,转到 Animation tab
- 确保角色有 Muscle Definition Avatar,比如有一个叫 Dude的Avatar
- 选择几个animation clip
- 勾选上 Loop Pose
- 在当前面板的curve下,创建一个速度的曲线 ,命名为“Runspeed”
- 创建一个Animator Controller,命名为RootMotionController,至少包含一个state
- 添加一个和curve名字一样的参数,这样在脚本中访问参数值的时候,会从曲线中获取 (in this case, “Runspeed”)
-
- 把avatar Dude,RootMotionController 拖到Animator 组件上.
-
using UnityEngine; using System.Collections; [RequireComponent(typeof(Animator))] public class RootMotionScript : MonoBehaviour { //属于mono behavior的方法,用于更改root transform的回调,就是在这里面设置动画影响角色的移动 void OnAnimatorMove() { Animator animator = GetComponent<Animator>(); if (animator) { Vector3 newPosition = transform.position; newPosition.z += animator.GetFloat("Runspeed") * Time.deltaTime; //Runspeed的值会从之前定义的曲线中获取 transform.position = newPosition; } } }