Slate轨道工具使用(一)—Track,Clip

轨道型工具

一款游戏我认为离不开两种类型的工具,一种是节点型工具,处理游戏整体逻辑、人物ai等。

如UE的蓝图,Unity的Bot,Playmaker,Xnode…

除此之外还有一种类型的工具也是必不可少的,那就是轨道型工具,这种工具处理细节,如战斗,在挥剑时,在全动作不同位置通过轨道细微控制 被击对象的受击,钝帧打击感,音效,特效,等等,这些全部可以在轨道型工具的配合下,在最合适的时机触发

Slate

Slate是Unity的轨道型工具,是付费插件,比起TimeLine,更加轻量,同时Slate提供源码,可以根据项目进行合适的改造

Track

Slate 自带很多Track,这些Track可以播放动画,更换图片,添加碰撞体等等

Slate创建之后需要拖入一个Gameobject ,然后各个轨道都可以获取此物体,从而对物体进行操作
在这里插入图片描述
例如播放动画,选择对应的Track 选择clip 即可在编辑模式下 查看动画的播放,Cutscene自带的动画甚至包括动画位移,动画混合,都可以在编辑模式下实时看到效果,从而在对应的动画位置加入一些细微操作,

比如音效,特效与动画的配合
在这里插入图片描述
在这里插入图片描述

动画Track(自定义Track)

我们以动画播放为例创建自定义的Track

using Slate;
using UnityEngine;

[Name("模型动画轨道")]
[Icon(typeof(Animator))]
[Attachable(typeof(ActorGroup))]
public class AnimTrack : CutsceneTrack
{
    protected override void OnCreate()
    {
        base.OnCreate();

        this.name = "模型动画轨道";
    }

    protected override void OnEnter()
    {
        base.OnEnter();
    }
}

Clip(自定义Clip)

Clip是Track的具体内容,在Clip脚本中,具有一个带Time参数的OnUpdate函数,其中的Time即是当前Slate 播放到的时间长度,这也是轨道型工具的核心,根据Time的不同 脚本会随Time执行,通过Time去控制各个轨道的组件进行配合

同理,我们如果要编写自己的Clip,可以参考Slate自带的Clip与Track脚本,

动画播放Clip

例如动画播放,Slate原本的动画播放是通过Playable实现的,我们可以写一个通过Animator实现的动画播放,我们把自定义的动画播放放到自定义的轨道上,通过继承ActorActionClip 即可实现自定义的Clip

比如我们的Animator 可以通过人物身上的状态机,自动找到所有状态名,下拉选择,同时自动刷新到动画长度 (刷新与中文支持 是对slate的扩展,详见)
在这里插入图片描述

在这里插入图片描述

参考代码:

using Sirenix.OdinInspector;
using Slate;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

[Name("播放动画Animator")]
[Attachable(typeof(AnimTrack))]
public class PlayAnimClip : CutsceneClip<Animator>
{
    [LabelText("动作名")]
    [ValueDropdown(nameof(GetString))]
    [SerializeField]
    [OnValueChanged("Refresh")]
    public string AnimName;

    [LabelText("播放速度")]
    [SerializeField]
    public float PlaySpeed = 1;

    [LabelText("循环播放")]
    [SerializeField]
    public bool Loop = false;

    private float _usedBlendAnimTime;

    [HideInInspector]
    [SerializeField] private float _length = 1 / 30f;

    private AnimationClip CurClip;

    [HideInInspector]
    public bool IsCrossing = false;

    /// <summary>
    /// 默认长度为整个动画的时长
    /// </summary>
    public override float length
    {
        get { return _length; }//将默认长度变为当前动画长度
        set { _length = value; }
    }

    private List<string> GetString()
    {
        List<string> ClipsNames = new List<string>();
        ClipsNames.Clear();
        var Animclips = ActorComponent.runtimeAnimatorController.animationClips;
        //todo 如果双击 无人物,可以增加一个默认角色
        foreach (var animatorClipInfo in Animclips)
        {
            ClipsNames.Add(animatorClipInfo.name);
        }

        return ClipsNames.ToList();
    }

    protected override void OnCreate()
    {
        Refresh();
    }

    protected override void OnEnter()
    {
        ActorComponent.speed = PlaySpeed;
        var playClips = ActorComponent.runtimeAnimatorController.animationClips.Where(p => p.name == AnimName);
        if (playClips.ToList().Count <= 0)
        {
            Debug.LogError("没有对应的动画可以播放");
        }
        CurClip = playClips.First();
    }

    protected override void OnUpdate(float time)
    {
        //todo 测试time与真实时间的换算

        //编辑模式预览动画
        if (IsCrossing)//动作融合中,动画不播放
        {
            Debug.Log("处于动作融合");
            return;
        }

        var curClipLength = CurClip.length;
        float normalizedBefore = time * PlaySpeed;
        if (Loop && time > curClipLength)
        {
            //要跳转到的动画时长 ,根据Update Time 取余 ,需要归一化时间
            normalizedBefore = time * PlaySpeed % curClipLength;
        }
        //normalzedTime,0-1 表示开始与 播放结束,
        ActorComponent.Play(AnimName, 0, normalizedBefore / curClipLength);
        ActorComponent.Update(0);
    }

    protected override void OnExit()
    {

        base.OnExit();
    }

    public override void Refresh()
    {   //设置Length 为对应_animName的长度 与播放速度成比例
        length = ActorComponent.runtimeAnimatorController.animationClips.Where(p => p.name == AnimName).First().length;
        length = length / PlaySpeed;
    }

    //Todo OnGui 红色表示动画长度
using Slate;
using Slate.ActionClips;
using System;
using System.Linq;
using Sirenix.OdinInspector;
using UnityEngine;

[Serializable]
public abstract class CutsceneClip<T> : CutsceneClipBase, IRefresh, IClipRefresh where T : Component
{
    private T _actorComponent;

    public T ActorComponent
    {
        get
        {
            if (_actorComponent != null && _actorComponent.gameObject == base.actor)
            {
                return _actorComponent;
            }

            return _actorComponent = base.actor != null ? base.actor.GetComponent<T>() : null;
        }
    }

    public abstract void Refresh();

    public override bool isValid
    {
        get { return actor != null && base.isValid; }
    }

}

public class CutsceneClipBase:ActorActionClip
{
    [LabelText("Clip片段名")]
    public string CutsceneClipName;
}

public interface IRefresh
{
    public void Refresh();
}

Slate的播放

Slate编辑完之后,保存为Prefab,但是示例化出来,当时只作为prefab时放入的主角不见了,因为Slate无法保存主角,需要在代码里设置

Slate实例化参考脚本 其中传入的Gameobject 即是表演主角(单主角情况)

public static class CutsceneHelper
{
    public static Cutscene InstateAndPlay(GameObject player, string CutsceneName)
    {
        GameObject RoleActionCutscene = player.transform.Find("RoleActionCutscene")?.gameObject;
        if (RoleActionCutscene == null)
        {
            RoleActionCutscene = new GameObject("RoleActionCutscene");
            RoleActionCutscene.transform.SetParent(player.transform, false);
        }
        else
        {
            GameObject.Destroy(RoleActionCutscene.transform.GetChild(0).gameObject);
        }
        //播放动画
        GameObject slateRes = Resources.Load<GameObject>($"Slate/Player/{CutsceneName}");
        if (slateRes == null)
        {
            Debug.LogError("找不到对应的Cutscene");
        }
        var slate = GameObject.Instantiate(slateRes);

        slate.transform.position = RoleActionCutscene.transform.position;
        slate.transform.SetParent(RoleActionCutscene.transform, false);
        var cutscene = slate.GetComponent<Cutscene>();
        foreach (var cutsceneGroup in cutscene.groups)
        {
            cutsceneGroup.actor = player;
        }
        cutscene.Play();
        return cutscene;
    }

    public static Cutscene Instate(GameObject player, string CutsceneName)
    {
        GameObject RoleActionCutscene = player.transform.Find("RoleActionCutscene")?.gameObject;
        if (RoleActionCutscene == null)
        {
            RoleActionCutscene = new GameObject("RoleActionCutscene");
            RoleActionCutscene.transform.SetParent(player.transform, false);
        }
        else
        {
            GameObject.Destroy(RoleActionCutscene.transform.GetChild(0).gameObject);
        }

        //播放动画
        GameObject slateRes = Resources.Load<GameObject>($"Slate/Player/{CutsceneName}");
        if (slateRes == null)
        {
            Debug.LogError("找不到对应的Cutscene");
        }
        var slate = GameObject.Instantiate(slateRes);

        slate.transform.position = RoleActionCutscene.transform.position;
        slate.transform.SetParent(RoleActionCutscene.transform, false);
        var cutscene = slate.GetComponent<Cutscene>();
        foreach (var cutsceneGroup in cutscene.groups)
        {
            cutsceneGroup.actor = player;
        }

        return cutscene;
    }

    public static Cutscene Instate(GameObject player, Cutscene inCutscene)
    {
        GameObject RoleActionCutscene = player.transform.Find("RoleActionCutscene")?.gameObject;
        if (RoleActionCutscene == null)
        {
            RoleActionCutscene = new GameObject("RoleActionCutscene");
            RoleActionCutscene.transform.SetParent(player.transform, false);
        }
        else
        {
            GameObject.Destroy(RoleActionCutscene.transform.GetChild(0).gameObject);
        }

        //播放动画
        Cutscene slate = GameObject.Instantiate(inCutscene);
        slate.transform.position = RoleActionCutscene.transform.position;
        slate.transform.SetParent(RoleActionCutscene.transform, false);
        foreach (var cutsceneGroup in slate.groups)
        {
            cutsceneGroup.actor = player;
        }

        return slate;
    }

    public static T GetCutsceneClip<T>(this Cutscene cutscene, string CutsceneClipName) where T : CutsceneClipBase
    {
        //通过cutscene 对象找到所有的Clips,调用带有ClipRefresh 接口的函数
        foreach (var group in cutscene.groups)
        {
            foreach (var track in group.tracks)
            {
                var clips = track.clips.ToList();

                for (int i = 0; i < clips.Count; i++)
                {
                    var curClip = clips[i];
                    var cutsceneClip = curClip as T;
                    if (cutsceneClip == null)
                    {
                        continue;
                    }

                    if (string.IsNullOrEmpty(cutsceneClip.CutsceneClipName))
                    {
                        continue;
                    }

                    if (cutsceneClip.CutsceneClipName == CutsceneClipName)
                    {
                        return cutsceneClip as T;
                    }
                }
            }
        }

        return null;
    }

    public static List<T> GetCutsceneClip<T>(this Cutscene cutscene) where T : CutsceneClipBase
    {
        List<T> cutsceneClips = new List<T>();
        //通过cutscene 对象找到所有的Clips,调用带有ClipRefresh 接口的函数
        foreach (var group in cutscene.groups)
        {
            foreach (var track in group.tracks)
            {
                var clips = track.clips.ToList();

                for (int i = 0; i < clips.Count; i++)
                {
                    var curClip = clips[i];
                    if (curClip is T)
                    {
                        cutsceneClips.Add(curClip as T);
                    }
                }
            }
        }

        return cutsceneClips;
    }
}

Slate被示例化之后,是否循环播放取决于其脚本的选项Once or Loop
在这里插入图片描述

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值