Timeline|03 Animancer
一个基于Playable的扩展插件(当然你也可以基于Playable自己写一套规则)
Basic
最简单的播放一个动画,Play有多个参数,主要看前两个
- 目标 AnimationClip / AnimancerState / ITransition
- FadeTime 过渡时间/秒
- FadeMode 过度模式
public sealed class PlayAnimationOnEnable : MonoBehaviour
{
[SerializeField]
private AnimancerComponent _Animancer;
[SerializeField]
private AnimationClip _Animation;
private void OnEnable()
{
_Animancer.Play(_Animation);
}
}
动画结束事件
public class PlayAnimationOnClick : MonoBehaviour
{
[SerializeField] private AnimancerComponent _Animancer;
[SerializeField] private AnimationClip _Idle;
[SerializeField] private AnimationClip _Action;
private void OnEnable()
{
_Animancer.Play(_Idle);
}
private void Update()
{
if (ExampleInput.LeftMouseUp)
{
AnimancerState state = _Animancer.Play(_Action);
// 结束事件
state.Events.OnEnd = OnEnable;
}
}
}
动画过度
可以直接使用Play里的参数
_Animancer.Play(_Action, 0.25f);
public sealed class PlayTransitionOnClick : MonoBehaviour
{
[SerializeField] private AnimancerComponent _Animancer;
[SerializeField] private AnimationClip _Idle;
[SerializeField] private AnimationClip _Action;
private void OnEnable()
{
_Animancer.Play(_Idle, 0.25f);
}
private void Update()
{
if (ExampleInput.LeftMouseUp)
{
AnimancerState state = _Animancer.Play(_Action, 0.25f);
// 重置时间
state.Time = 0;
state.Events.OnEnd = OnEnable;
}
}
}
每次播放我们都需要重置state的时间为0,这是由我们的FadeMode决定的
FadeMode
-
FixedSpeed 固定速度,默认
-
FixedDuration 固定时间
当weight从0~1,过度时间为0.25时,过渡速度是1/0.25=4,所需时间是0.25
假设我们现在的weight是从0.5~1
如果使用固定速度,那么过渡速度还是4,此时过渡时间会变成0.125
如果使用固定时间,那么过度时间还是0.25,则过渡速度变成了2
因此,如果某个动画正在播放中,此时再次使用固定速度播放他,那么不会有任何变化,因为weight从1-1,过渡时间为0
如果使用固定时间,那么又会从头播放
-
FromStart 从头开始,如果我们从一个动画切换到自身,那他会创建一个动画状态副本,假装有2个动画状态在切换
或者使用ClipTransition
private ClipTransition _Idle
public sealed class PlayTransitionOnClick : MonoBehaviour
{
[SerializeField] private AnimancerComponent _Animancer;
[SerializeField] private ClipTransition _Idle;
[SerializeField] private ClipTransition _Action;
private void OnEnable()
{
_Action.Events.OnEnd = OnActionEnd;
_Animancer.Play(_Idle);
}
private void OnActionEnd()
{
_Animancer.Play(_Idle);
}
private void Update()
{
if (ExampleInput.LeftMouseUp)
{
_Animancer.Play(_Action);
}
}
}
- StartTime 不启用的话FadeMode=FixeSpeed,启用的话等于FromStart,后面的时间是动画开始时间
- EndTime 结束事件,动画的结束时间
一个基础的角色状态机
public sealed class BasicCharacterAnimations : MonoBehaviour
{
[SerializeField] private AnimancerComponent _Animancer;
[SerializeField] private ClipTransition _Idle;
[SerializeField] private ClipTransition _Move;
[SerializeField] private ClipTransition _Action;
private enum State
{
NotActing,
Acting,
}
private State _CurrentState;
private void Awake()
{
_Action.Events.OnEnd = OnActionEnd;
}
private void OnActionEnd()
{
_CurrentState = State.NotActing;
UpdateMovement();
}
private void Update()
{
switch (_CurrentState)
{
case State.NotActing:
UpdateMovement();
UpdateAction();
break;
case State.Acting:
UpdateAction();
break;
}
}
private void UpdateMovement()
{
_CurrentState = State.NotActing;
float forward = ExampleInput.WASD.y;
if (forward > 0)
{
_Animancer.Play(_Move);
}
else
{
_Animancer.Play(_Idle);
}
}
private void UpdateAction()
{
if (ExampleInput.LeftMouseUp)
{
_CurrentState = State.Acting;
_Animancer.Play(_Action);
}
}
}
Fine Control
推荐直接播一个AnimationClip,当然也可以给Clip设置一个名字
NamedAnimancerComponent
此时Clip会关联一个string,默认就是Clip的名字
然后可以通过TryPlay来播放
_Animancer.TryPlay("Humanoid-Idle");
当然,如果没有这个动画,就无事发生,此时需要你主动去创建这个动画状态
_Animancer.States.Create(_Walk);
你也可以自定义一个名字,这里的key其实是一个obj
_Animancer.States.Create("walk",_Walk);
倒放
通过控制Speed实现
var state = _Animancer.Play(_WakeUp);
state.Speed = -1;
注意,正方和倒放都会触发EndEvent
编辑器模拟
#if UNITY_EDITOR
// Unity自带的生命周期
private void OnValidate()
{
// _Open是一个动画
if (_Open != null)
AnimancerUtilities.EditModeSampleAnimation(_Open, this, _Openness * _Open.length);
}
#endif
更新频率
可以再最开始暂停Graph
_Animancer.Playable.PauseGraph();
这样动画就不会被采样了,然后可以合适的时机调用
` _Animancer.Evaluate(…);`
当然这个时机需要自己写逻辑控制……
Locomotion
运动控制
自己实现一个ITransition
比如某些动画需要RootMotion,某些不需要,每次都指定很麻烦
可以自己写一个类
[Serializable]
public class MotionTransition : ClipTransition
{
[SerializeField]
private bool _ApplyRootMotion;
public override void Apply(AnimancerState state)
{
base.Apply(state);
state.Root.Component.Animator.applyRootMotion = _ApplyRootMotion;
}
}
这里最好继承ClipTransition,当然继承ITransition完全自己重写也是可以的,但是对于这个功能来说没必要啊
混合树
可以播放Unity原生的混合树,不过这里不讲这种方式
可以使用LinearMixerTransition
中间这个是阈值
通过transition.State.Parameter
设置
资源序列化
可以通过这里创建序列化资源
然后直接在脚本里引用
LinearMixerTransitionAsset.UnShared _Mixer
注意,需要用UnShared
当然也有2D的混合器MixerTransition2D
Event
事件点
和Unity原生的基本没区别,不过更好用
当然也可以在代码里设置
Swing.Events.SetCallback(HitEventName, HitBall);
甚至你可以给Transition加上标签
[EventNames("Start","End")]
private ClipTransition _Swing;
然后就能在面板上直接选择了
在事件中获取当前状态
public static readonly Action LogCurrentEvent = () =>
{
Debug.Log(
$"An {nameof(AnimancerEvent)} was triggered:" +
$"\n- Event: {AnimancerEvent.CurrentEvent}" +
$"\n- State: {AnimancerEvent.CurrentState.GetDescription()}",
AnimancerEvent.CurrentState.Root?.Component as Object);
};
甚至可以重置动画
public static readonly Action RestartCurrentState = () =>
{
AnimancerEvent.CurrentState.Time = 0;
};
或者暂停
public static readonly Action PauseAtCurrentEvent = () =>
{
AnimancerEvent.CurrentState.IsPlaying = false;
AnimancerEvent.CurrentState.NormalizedTime = AnimancerEvent.CurrentEvent.normalizedTime;
};
State Machine
在最前面的例子中,我们通过一个脚本就可以控制一个角色的行为了,但实际情况是一个角色的行为往往非常、非常负责,全部写在一个类中,很明显是违背设计模式原则的———单一职责原则
所以我们首先需要抽象一下我们的角色控制器
抽象出“状态”
// 继承StateBehaviour
public abstract class CharacterState : StateBehaviour
{
// 状态节点
[Serializable]
public class StateMachine : StateMachine<CharacterState>.WithDefault
{
}
// 用于描述当前状态的优先级
public virtual CharacterStatePriority Priority => CharacterStatePriority.Low;
// 能否切换到自身
public virtual bool CanInterruptSelf => false;
// 用于判断能否切换到下一状态
public override bool CanExitState
{
get
{
var nextState = _Character.StateMachine.NextState;
if (nextState == this)
return CanInterruptSelf;
if (Priority == CharacterStatePriority.Low)
return true;
return nextState.Priority > Priority;
}
}
}
通过这个状态节点,我们可以实现一些具体状态,比如
public sealed class IdleState : CharacterState
{
[SerializeField] private ClipTransition _Animation;
// Animancer对于状态的管理是通过Enable/Disable进行的
private void OnEnable()
{
Character.Animancer.Play(_Animation);
}
}
再比如
public sealed class ActionState : CharacterState
{
[SerializeField] private ClipTransition _Animation;
private void Awake()
{
// 动画结束后重置状态机到默认状态
_Animation.Events.OnEnd = Character.StateMachine.ForceSetDefaultState;
}
private void OnEnable()
{
Character.Animancer.Play(_Animation);
}
// 这种状态的优先级是中等
public override CharacterStatePriority Priority => CharacterStatePriority.Medium;
// 这个状态可以切换到自身
public override bool CanInterruptSelf => true;
}
然后是我们的角色控制器
public sealed class Character : MonoBehaviour
{
[SerializeField]
private AnimancerComponent _Animancer;
public AnimancerComponent Animancer => _Animancer;
[SerializeField]
private CharacterState.StateMachine _StateMachine;
public CharacterState.StateMachine StateMachine => _StateMachine;
[SerializeField]
private CharacterState _Move;
[SerializeField]
private CharacterState _Action;
private void Awake()
{
// 初始化
StateMachine.InitializeAfterDeserialize();
}
private void Update()
{
UpdateMovement();
UpdateAction();
}
private void UpdateMovement()
{
float forward = ExampleInput.WASD.y;
if (forward > 0)
{
StateMachine.TrySetState(_Move);
}
else
{
StateMachine.TrySetDefaultState();
}
}
private void UpdateAction()
{
if (ExampleInput.LeftMouseUp)
StateMachine.TryResetState(_Action);
}
}
我们还可以定义一个输入参数类
public sealed class CharacterParameters
{
[SerializeField]
private Vector3 _MovementDirection;
public Vector3 MovementDirection
{
get => _MovementDirection;
set => _MovementDirection = Vector3.ClampMagnitude(value, 1);
}
[SerializeField]
private bool _WantsToRun;
public ref bool WantsToRun => ref _WantsToRun;
}