一个基于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

image-20230516193420778

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

image-20230516201817194

此时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

中间这个是阈值

image-20230516204959726

通过transition.State.Parameter设置

资源序列化

可以通过这里创建序列化资源

image-20230516205416668

image-20230516205430193

然后直接在脚本里引用

LinearMixerTransitionAsset.UnShared _Mixer

注意,需要用UnShared

当然也有2D的混合器MixerTransition2D

Event

事件点

和Unity原生的基本没区别,不过更好用

image-20230516221025480

当然也可以在代码里设置

Swing.Events.SetCallback(HitEventName, HitBall);

甚至你可以给Transition加上标签

[EventNames("Start","End")] 
private ClipTransition _Swing;

然后就能在面板上直接选择了

image-20230516221605289

在事件中获取当前状态

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;
}