Slate轨道工具使用(一)

您所在的位置:网站首页 unity编辑器扩展控制动作播放 Slate轨道工具使用(一)

Slate轨道工具使用(一)

2024-02-03 01:04| 来源: 网络整理| 查看: 265

文章目录 轨道型工具SlateTrack动画Track(自定义Track)Clip(自定义Clip)动画播放ClipSlate的播放

轨道型工具

一款游戏我认为离不开两种类型的工具,一种是节点型工具,处理游戏整体逻辑、人物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 { [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; /// /// 默认长度为整个动画的时长 /// public override float length { get { return _length; }//将默认长度变为当前动画长度 set { _length = value; } } private List GetString() { List ClipsNames = new List(); 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 //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 : 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() : 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($"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(); 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($"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(); 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(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 continue; } if (string.IsNullOrEmpty(cutsceneClip.CutsceneClipName)) { continue; } if (cutsceneClip.CutsceneClipName == CutsceneClipName) { return cutsceneClip as T; } } } } return null; } public static List GetCutsceneClip(this Cutscene cutscene) where T : CutsceneClipBase { List cutsceneClips = new List(); //通过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 cutsceneClips.Add(curClip as T); } } } } return cutsceneClips; } }

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



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3