Unity笔记(十一)——换装、Spine骨骼动画、3D动画相关
写在前面:
写本系列(自用)的目的是回顾已经学过的知识、记录新学习的知识或是记录心得理解,方便自己以后快速复习,减少遗忘。这里只有代码相关部分,没有面板部分。
五、换装
1、换装准备
在制作人物系统时,会涉及到换装。首先为对象创建骨骼,生成蒙皮,调整权重(这一步不作细致说明):
接下来,创建一个Sprite Library Asset,创建方式是在Project窗口右键—Create—2D—Sprite Library Asset。创建后点击:
打开后即可在界面左侧创建分类,右侧为分类关联该类别的子对象。创建完成后,点击Save。
创建完成后,在场景上需要换装的对象身上创建Sprite Library脚本,并为其关联先前创建的Sprite Library Asset。
为需要换装的子对象都添加脚本:Sprite Resolver,选择相应的类别。
这样,就做好了换装准备,可以进行代码换装了。
2、代码换装
换装可以采用的API是
public SpriteResolver sr;
sr.SetCategoryAndLabel(sr.GetCategory(), "CASK 1"); 第一个参数是需要换装的类别,第二个参数是需要换为的对象名。当想要换装的部件很多时,需要关联多个SpriteResolver对象,较为麻烦。可以使用如下代码,先存储换装信息为一个字典,再换装:
//public SpriteResolver sr;private Dictionary<string, SpriteResolver> equipDic = new Dictionary<string, SpriteResolver>();void Start()
{//sr.SetCategoryAndLabel(sr.GetCategory(), "CASK 1");SpriteResolver[] srs = this.GetComponentsInChildren<SpriteResolver>();for(int i = 0; i < srs.Length; i++){equipDic.Add(srs[i].GetCategory(), srs[i]);}ChangeEquip("Cask", "CASK 1");
}public void ChangeEquip(string category, string equipName)
{if (equipDic.ContainsKey(category)){equipDic[category].SetCategoryAndLabel(category, equipName);}
}
六、Spine骨骼动画
Spine动画系统与Unity自带的动画系统不同。将从Spine导出的文件直接拖入Hierarchy中,会弹出如下窗口
选择第一个,即可创建骨骼动画。对象身上会自动创建如下脚本:
可以通过获取SkeletonAnimation脚本来使用代码控制动画播放。
1、动画播放
修改播放的动画有两种方式:
(1)直接改变SkeletonAnimation中的参数
若想要修改动画的循环方式,需要先改循环再改动画,否则循环不会生效。
直接使用sa.AnimationName = "idle",修改播放的动画名,即可修改播放的动画
private SkeletonAnimation sa;void Start()
{sa = this.GetComponent<SkeletonAnimation>();sa.loop = false;sa.AnimationName = "idle";
}
(2)使用动画状态改变函数
直接使用API,sa.AnimationState.SetAnimation(0, "jump", false)。括号内传入了3个参数,第一个参数为索引值,填0即可;第二个参数是需要播放的动画名,第三个是是否循环播放。
此外,还可以使用排队播放:sa.AnimationState.AddAnimation(0, "walk", true, 0)。会等待上一个运行完,前三个参数和前者相同,第四个参数是延迟播放时间。
最后,如果需要动画转向,可以使用API,sa.skeleton.ScaleX。当sa.skeleton.ScaleX = -1时表示在X轴上进行镜像翻转。
private SkeletonAnimation sa;void Start()
{sa = this.GetComponent<SkeletonAnimation>();sa.AnimationState.SetAnimation(0, "jump", false);sa.AnimationState.AddAnimation(0, "walk", true, 0);//转向sa.skeleton.ScaleX = -1;
}
2、动画事件
共有4种动画事件,这四种事件函数都必须传入参数,该参数能得到动画相关信息:
在动画开始时进行的事件:sa.AnimationState.Start
在动画中断or清除时进行的事件:sa.AnimationState.End
在动画完成时进行的事件:sa.AnimationState.Complete
为动画添加自定义事件:sa.AnimationState.Event,注意,需要传入两个参数,示例中的参数e是具体发生的事件,该事件的添加在Spine中制作动画时,而非Unity中,因此这里只是事件的监听。
private SkeletonAnimation sa;void Start()
{sa = this.GetComponent<SkeletonAnimation>();//动画开始sa.AnimationState.Start += (t) =>{print(sa.AnimationName+"动画开始");};//动画中断or清除sa.AnimationState.End += (t) =>{print(sa.AnimationName + "动画中断or清除");};//动画完成sa.AnimationState.Complete += (t) =>{print(sa.AnimationName + "动画完成");};//动画添加自定义事件sa.AnimationState.Event += (t, e) =>{print(sa.AnimationName + "自定义事件");};
}
3、便捷特性
设置特性,可以让我们Inspector面板及代码的使用更加便捷。例如,添加了动画特性[SpineAnimation]后,Inspector窗口上就会出现你声明的AnimationName变量,并且打开下拉列表可以为其选择动画。这样,代码后续想要使用该动画时,动画名直接传入AnimationName即可,不用再去看动画名是什么。
[SpineAnimation]
public string AnimationName;
常用的特性有这几个:
动画特性[SpineAnimation]
骨骼特性[SpineBone]
插槽特性[SpineSlot]
附件特性[SpineAttachment]
这里,插槽指的是身体上的一个装备栏位(比如“右手”这个栏位)
附件指的是可以放进该栏位的具体装备(比如“木剑”、“铁剑”、“火把”或“空手”)
private SkeletonAnimation sa;[SpineBone]
public string boneName;[SpineSlot]
public string slotName;[SpineAttachment]
public string attachmentName;
void Start()
{sa = this.GetComponent<SkeletonAnimation>();//获取骨骼Bone b = sa.skeleton.FindBone(boneName);//设置插槽附件sa.skeleton.SetAttachment(slotName, attachmentName);
}
七、3D动画相关
1、动画遮罩
假如我们需要制作瞄准动画,人物在蹲下和站起分别进行瞄准时,上半身的动作都为举枪瞄准动作,有区别的只是下半身的动作,即是站立还是蹲下。为了节省制作成本,可以使用动画遮罩,即我们只修改下半身的动作,用同一套上半身动作即可。
(1)动画层次
在Animator窗口新建层次,新建后点击右上角的齿轮,在Blending处可以选择混合模式:Override为覆盖,Additive为叠加。在Weight处可设置权重,权重越大该层造成的影响越大,例如:新层次new Layer模式选择Override,Weight设为1,就会完全覆盖Base Layer。
(2)遮罩
若想只覆盖下半身,需要创建遮罩:在Project窗口右键Create Avatar Mask即可创建动画遮罩。
可点击肢体进行取消选中,动画就不会影响到红色取消部位。
最后将动画遮罩与对应层次关联即可。
(3)代码修改层次权重
可通过animator.SetLayerWeight()来设置对应层次权重。括号内第一个参数传入层次索引,可通过animator.GetLayerIndex("层次名")利用层次名获得层次索引。第二个参数是设置的层次大小。
private Animator animator;void Start()
{animator = this.GetComponent<Animator>();animator.SetLayerWeight(animator.GetLayerIndex("MyLayer1"), 1);
}
2、动画IK控制
(1)开启IK
动画IK控制可以实现开枪瞄准目标时,人物身体跟随目标移动;人物眼神跟随指定物体移动等效果。若想要通过代码设置动画IK,可以在Animator窗口的Layers窗口齿轮处,点击勾选IK pass。之后就可以在代码中进行动画IK控制 。
(2)代码相关
①OnAnimatorIK和OnAnimatorMove
OnAnimatorIK和OnAnimatorMove是两个和动画相关的特殊生命周期函数。它们会在Update之后和LateUpdate之前调用。
IK相关代码需要写在函数OnAnimatorIK中。
OnAnimatorMove主要处理动画移动以修改根运动的回调逻辑。若你的模型中原本就带有移动而你又想继续添加移动,就可以在这个函数中写逻辑。
②头部IK相关
设置头部IK使用两个API,分别是animator.SetLookAtWeight()和animator.SetLookAtPosition()。后者是设置看向的位置,这里拖入了一个物体的位置。前者是设置各部分的权重,最多可以传入5个参数。这5个参数从左到右分别是:
weight:LookAt全局权重0~1;
bodyWeight:LookAt时身体权重0~1;
headWeight:LookAt时头部权重0~1;
eyesWeight:LookAt时眼睛权重0~1;
clampWeight:0表示角色运动时不受限制,1表示角色完全固定无法执行LookAt,0.5表示只能够移动一半;
下例中只设置了全局、身体、头部权重。
private Animator animator;public Transform pos;void Start()
{animator = this.GetComponent<Animator>();
}private void OnAnimatorIK(int layerIndex)
{animator.SetLookAtWeight(1,0.5f,0.5f);animator.SetLookAtPosition(pos.position);
}
③四肢IK相关
四肢IK使用四个API,分别是:
SetIKPositionWeight(),设置对应肢体位置的权重,第一个参数是需要控制的肢体,使用AvatarIKGoal点出,例如右手是AvatarIKGoal.RightHand,下同;第二个参数为设置的权重大小。
SetIKRotationWeight(),设置对应肢体角度的权重,第一个参数是需要控制的肢体;第二个参数为设置的权重大小。
SetIKPosition(),设置对应肢体的位置,第一个参数是需要控制的肢体;第二个参数是肢体的位置。
SetIKRotation(),设置对应肢体的角度,第一个参数是需要控制的肢体;第二个参数是肢体的角度。
private Animator animator;public Transform pos2;void Start()
{animator = this.GetComponent<Animator>();
}private void OnAnimatorIK(int layerIndex)
{animator.SetIKPositionWeight(AvatarIKGoal.RightHand, 1);animator.SetIKRotationWeight(AvatarIKGoal.RightHand, 1);animator.SetIKPosition(AvatarIKGoal.RightHand, pos2.position);animator.SetIKRotation(AvatarIKGoal.RightHand, pos2.rotation);
}
3、动画目标匹配
(1)作用
当游戏中角色要以某种动作移动,该动作播放完毕后,人物的手或者脚必须落在某一个地方。比如角色需要跳过踏脚石或者跳跃并抓住房梁。这时候就需要动作目标匹配来达到想要的效果。
使用步骤是:
1、找到动作关键点位置信息,比如起跳点,落地点
2、将关键信息传入MatchTargetAPI中
(2)代码相关
如图所示人物脚底为起跳点,立方体上的空物体即为选取的落地点。需要将落地点targetPos关联到代码中。
实例中想要实现的是:人物在待机时播放待机动画,按下空格键播放跳跃动画并跳至平台上。构建如下状态机并与人物模型关联:
添加Trigger参数,触发时切换到跳跃动作。
①注意事项
调用匹配动画的时机有一些限制。
1、必须保证动画已经切换到了目标动画上。例如,我们需要在跳跃时设置动画匹配,若此时没有切换到跳跃动画上而一直是待机动画,就肯定不会进行动画匹配。
2、必须保证调用时动画并不是出于过渡阶段而真正在播放目标动画。例如下图:
待机动画切换到跳跃动画有0.25s的过渡时间,如果在该过渡时间内进行动画匹配就会出现很奇怪的效果。
3、需要开启Apply Root Motion。在模型的Animator脚本处勾选即可。
②代码控制
在注意事项中我们知道,必须保证调用时动画并不是出于过渡阶段而真正在播放目标动画。我们制作的动画中,从Idle状态到Jump的状态中有0.25s的过渡时间,在该时间内不能进行动画匹配。因此,可以在动作Jump的Inspector窗口中在0.25s后为其添加一个事件,利用事件来进行动画目标匹配:
只要在对象挂载的脚本中书写了同名函数MatchTarget,该函数就会自动执行。
动画目标匹配使用的API是: animator.MatchTarget(),其中,可传入6个参数
参数一:目标位置
参数二:目标角度
参数三:匹配的骨骼位置,可以通过AvatarTarget来点出需要匹配的肢体。例如AvatarTarget.LeftFoot匹配左脚。
参数四:位置角度权重
参数五:开始位移动作的百分比
参数六:结束位移动作的百分比
关于参数5,6需要在动画预览窗口查看。例如下图,在动画播放到40%处人物准备起跳(开始位移),结束位移动作百分比也同理。
private Animator animator;
public Transform targetPos;void Start()
{animator = this.GetComponent<Animator>();
}void Update()
{if(Input.GetKeyDown(KeyCode.Space)){animator.SetTrigger("Jump");}
}private void MatchTarget()
{animator.MatchTarget(targetPos.position, targetPos.rotation, AvatarTarget.LeftFoot, new MatchTargetWeightMask(Vector3.one, 1), 0.4f, 0.64f);
}
4、状态机行为脚本
(1)作用
状态机行为脚本是一类特殊的脚本,可以实现一些特殊功能:进入或退出某一状态时播放声音、仅在某些状态下检测一些逻辑等。
(2)代码
新建一个脚本继承StateMachineBehaviour基类
选择自己需要的特定方法进行重写:
OnStateEnter:进入状态时,第一个Update中调用
OnStateExit:退出状态时,最后一个Update中调用
OnStateIK:OnAnimatorIK后调用
OnStateMove:OnAnimatorMove后调用
OnStateUpdate:除第一帧和最后一帧,每个Update上调用
OnStateMachineEnter:子状态机进入时调用,第一个Update中调用
OnStateMachineExit:子状态机退出时调用,最后一个Update中调用
public class lession10_StateMachineBehaviour : StateMachineBehaviour
{public override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex){if(stateInfo.IsName("HumanoidIdle"))Debug.Log("进入HumanoidIdle状态");}public override void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex){if (stateInfo.IsName("HumanoidIdle"))Debug.Log("退出HumanoidIdle状态");}public override void OnStateIK(Animator animator, AnimatorStateInfo stateInfo, int layerIndex){base.OnStateIK(animator, stateInfo, layerIndex);}public override void OnStateMove(Animator animator, AnimatorStateInfo stateInfo, int layerIndex){base.OnStateMove(animator, stateInfo, layerIndex);}public override void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex){if (stateInfo.IsName("HumanoidIdle"))Debug.Log("处于HumanoidIdle状态");}public override void OnStateMachineEnter(Animator animator, int stateMachinePathHash){base.OnStateMachineEnter(animator, stateMachinePathHash);}public override void OnStateMachineExit(Animator animator, int stateMachinePathHash){base.OnStateMachineExit(animator, stateMachinePathHash);}
}