找回密码
 立即注册
查看: 686|回复: 0

[简易教程] 【Unity】TimeLine&Cinemachine系列教程——动态赋值,我要打十个!

[复制链接]
发表于 2020-12-18 19:16 | 显示全部楼层 |阅读模式
前言
TimeLine在上一篇介绍了TimeLine的各个功能特性,已经用TimeLine做了一个固定的特写镜头。但是要用好TimeLine还得去学会如何去掌控它,因此今天主要内容之一是用代码赋值修改TimeLine中轨道的参数(为了能更好的做出效果,几乎快做了一个格斗游戏了,想做动作游戏想的热血沸腾~,最后想着还要出教程,还是停下来老老实实做功能)。
前前后后遇到了很多坑点,TimeLine有许多没法力所能及的地方,在这篇文章结尾会将主要的问题写出来,避免大家继续踩坑。


最终效果




Track赋值
创建了TimeLine的物体会有一个绑定列表




绑定列表对应我们做TimeLine各个控制轨道




我们是可以对轨道修改名字的,这个修改后的名字就是Bindings的key值。




脚本中可以获取到PlayableDirector类,通过SetGenericBinding函数赋值物体。
      var mDirector = new PlayableDirector;
       mDirector.SetGenericBinding("轨道名字","轨道绑定的物体");这一步是比较简单的,完成这一步后,我们可以创建两个动画,在游戏中触发TimeLine后,动态给轨道赋值。比如给攻击动画轨道赋值玩家模型,给受击动画轨道赋值怪物模型。


/*有的读者可能会问,我们如果做攻击和被击效果的话可以直接把动作放在角色控制器里面,没必要用TimeLine实现。的确是这样,但是如果游戏中模型动作资源太多,比如战神这款游戏,每一个精英怪都会有一个动作特写,如果一开始全部放在动画控制器中,会造成动画十分混乱。而使用TimeLine,相当于我们动态给模型加载动画,当使用到这个动画的时候再加载它,这样就会让动画控制器非常的干净简洁,也节约了动画占用的内存*/


TimeLineClip赋值
接下来我们会动态给轨道内的Clip赋值,比如说Cinemachine的摄像机,和摄像机观察的物体、跟随的物体。










获取到Clip的代码我是做了一个字典来在playableAsset.outputs输出信息中储存了所有轨道信息。
   public void Init()
    {
        ……
        ……
        foreach (var at in mDirector.playableAsset.outputs)
        {
            if (!bindingDict.ContainsKey(at.streamName))
            {
                bindingDict.Add(at.streamName, at);
            }
        }

       var CinemachineTrack = bindingDict["Cinemachine"].sourceObject as Cinemachine.Timeline.CinemachineTrack;

        foreach (var clip in CinemachineTrack.GetClips())
        {
            //TODO
        }
    }而没有用GetGenericBinding函数是因为传入的值是Object类型,目前没找到一个有效传入数据。




给Clip赋值有几个重要的地方,一是Clip的名字是TimeLineClip的displayName,TimeLineClip.asset是片段资源,二是赋值的时候得创建ExposedReference类型的结构体,不然直接赋值是没法生效的。
   public void Init()
    {
        ……
        ……

        foreach (var info in CinemachineTrack.GetClips())
        {
            if (info.displayName == "CinemachineShot")
            {
                var cameraInfo = info.asset as Cinemachine.Timeline.CinemachineShot;
                var vcam1 = GameObject.Find("CM vcam1").GetComponent<CinemachineVirtualCameraBase>();
                var setCam = new ExposedReference<CinemachineVirtualCameraBase>();
                setCam.defaultValue = vcam1;
                cameraInfo.VirtualCamera = setCam;
            }
        }
    }Clip如果赋值完毕后,要修改Clip的参数,必须解析playableGraph,不然修改的目标并不是实际游戏中的物体。
   public void Init()
    {
        ……
        ……

        var cameraInfo = info.asset as Cinemachine.Timeline.CinemachineShot;
        var vcam2 = cameraInfo.VirtualCamera.Resolve(mDirector.playableGraph.GetResolver());
        vcam2.LookAt = mControl.transform.Find("Tran_Chest").transform;
        vcam2.Follow = mControl.transform;
    }
这里有坑点:playableGraph是TimeLine运行时才会创建生存,否则为空。因此我们修改的这些值实际是 TimeLine已经播放的时候修改的。如果TimeLine在OnGraphStart函数中用了的值,我们这里修改后是无效的。


编写触发脚本
做这个之前有一个小插曲,本来我是想用TimeLine的自定义脚本轨道来搞定被抓取后怪物的位置修正的,结果发现不能用TimeLine来做,因为TimeLine的赋值修改会直接对所有用到这个TimeLine资源的物体一起赋值(这个bug目前没有找到解决方法),所以用自定义脚本轨道上处理位置修正,就会保留当前的目标信息,下次抓另一个怪物的时候就会两个物体一起运动(神奇的bug)。
所以我直接把抓取的时候位置修正代码和触发TimeLine的代码一起了,代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.Timeline;
using UnityEngine.Playables;
using Cinemachine.Timeline;
using Cinemachine;

public class KillControl : MonoBehaviour
{
   public GameObject mKillInfo;
   [HideInInspector]
   public PlayableDirector mDirector;
   Dictionary<string, PlayableBinding> bindingDict = new Dictionary<string, PlayableBinding>();
   public CharaControl playerControl;
   private CharaControl mControl;
   public Vector3 offsetPos;
   public Vector3 offsetRot;
   public UnityAction OnFinishEvent;


   public void Start()
   {
       mControl = GetComponent<CharaControl>();

       mKillInfo = Instantiate(mKillInfo);

       mKillInfo.transform.SetParent(mControl.transform);

       mDirector = mKillInfo.GetComponent<PlayableDirector>();

       foreach (var at in mDirector.playableAsset.outputs)
       {
           if (!bindingDict.ContainsKey(at.streamName))
           {
               bindingDict.Add(at.streamName, at);
           }
       }
   }

   public void Play()
   {
       mControl.transform.position = playerControl.transform.position + playerControl.transform.rotation * offsetPos;
       mControl.transform.rotation = Quaternion.LookRotation(playerControl.transform.position - mControl.transform.position);
       mControl.transform.localEulerAngles += offsetRot;


       mKillInfo.gameObject.SetActive(true);
       mDirector.Play();

       mDirector.SetGenericBinding(bindingDict["Player"].sourceObject, playerControl.mAim);
       mDirector.SetGenericBinding(bindingDict["Enemy"].sourceObject, mControl.mAim);
       mDirector.SetGenericBinding(bindingDict["Cinemachine"].sourceObject, Camera.main.GetComponent<Cinemachine.CinemachineBrain>());
       var CinemachineTrack = bindingDict["Cinemachine"].sourceObject as Cinemachine.Timeline.CinemachineTrack;

       foreach (var info in CinemachineTrack.GetClips())
       {
           if (info.displayName == "CinemachineShot")
           {
               var cameraInfo = info.asset as Cinemachine.Timeline.CinemachineShot;
               var vcam1 = GameObject.Find("CM vcam1").GetComponent<CinemachineVirtualCameraBase>();
               var setCam = new ExposedReference<CinemachineVirtualCameraBase>();
               setCam.defaultValue = vcam1;
               cameraInfo.VirtualCamera = setCam;
           }

           if (info.displayName == "CM vcam2")
           {
               var cameraInfo = info.asset as Cinemachine.Timeline.CinemachineShot;
               var vcam2 = cameraInfo.VirtualCamera.Resolve(mDirector.playableGraph.GetResolver());
               vcam2.LookAt = mControl.transform.Find("Tran_Chest").transform;
               vcam2.Follow = mControl.transform;
           }
       }
   }

   private void Update()
   {
       if (mDirector.gameObject.activeInHierarchy)
       {
           if (mDirector.state == PlayState.Paused)
           {
               mDirector.gameObject.SetActive(false);
               if (OnFinishEvent != null)
               {
                   OnFinishEvent();
               }
           }
       }
   }
}脚本功能很简单,Start里面创建好资源,并做了一个索引字典,Play的时候动态赋值。唯一要注意的是目前没找到TimeLine的结束回调事件,因此只能用判断运行状态来处理结束逻辑。为了精确时间,我将判断函数放在了Update中。
将这个脚本挂载到怪物身上,设置好绑定的TimeLine和修正的位置、旋转信息。




接下来只要我们特殊攻击打倒了怪物,我们就直接播放TimeLine:




这里有必要说一下,在编辑状态里面,有时候会发现镜头里面动画位置信息效果不对。这是正常的,因为我们没有去Key位置,TimeLine没有记录位置信息的时候,编辑模式位置就会是一个位置点和方向。




我们可以修改动画的偏移,这样就可以在编辑模式下看到正常效果了






攻击判定
对于角色如何控制的由于是测试代码,想做了解的同学可以去下载工程源码。这里对如何触发攻击做一个简单讲解。
如下,角色攻击的骨骼会绑定攻击盒子(触发器),当攻击动画进入判定攻击时间的时候将盒子激活,在收招的时候盒子判定会关闭。在此之间如果检测到了怪物的受击盒子(怪物会有一个受击判定盒子),就提前关闭攻击触发器,进入TimeLine播放阶段。






附上主要触发代码,根据不同的index索引ID判断身体哪个部位击中了怪物。攻击和被击我都是用一个代码来做判定的,这样方便我们直接可以得到我们打的是谁的被攻击部位。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

public class HitTrigger : MonoBehaviour
{
   /// <summary>
   /// 触发器
   /// </summary>
   public Collider mCol;
   /// <summary>
   /// 控制该触发器的角色
   /// </summary>
   public CharaControl mParent;
   /// <summary>
   /// 碰撞器Id
   /// </summary>
   public string index;
   /// <summary>
   /// 是否激活
   /// </summary>
   public bool isActive;
   /// <summary>
   /// 当前碰撞到的物体列表
   /// </summary>
   public List<GameObject> colList = new List<GameObject>();
   /// <summary>
   /// 碰撞事件
   /// </summary>
   public UnityAction<HitTrigger> TriggerFuncEvent;


   public void Start()
   {
       mCol = gameObject.GetComponent<Collider>();
       if (mCol.isTrigger)
       {
           mCol.enabled = false;
       }
   }

   public void OnTriggerEnter(Collider other)
   {
       SetHitTrigger(other);
   }

   public void OnTriggerStay(Collider other)
   {
       SetHitTrigger(other);
   }


   public void SetHitTrigger(Collider other)
   {
       if (!isActive)
       {
           return;
       }

       var hitObj = other.gameObject;

       if (!colList.Contains(hitObj))
       {
          var triggerScr=   hitObj.GetComponent<HitTrigger>();

           if (triggerScr==null||triggerScr.mParent == mParent)
           {
               return;
           }

           colList.Add(hitObj);

           if (TriggerFuncEvent != null)
           {
               TriggerFuncEvent(triggerScr);
           }
       }
   }



   public void SetColActive(bool rActive)
   {
       isActive = rActive;
       if (mCol.isTrigger)
       {
           mCol.enabled = rActive;
       }

       if (!isActive)
       {
           colList.Clear();
       }
   }
}


TimeLine做动画控制的问题
Bake Into Pose选项作用:
再次提及一个Humanoid动画设置,之前有点模糊的地方,Bake Into Pose如果勾选,这个动画将会只将动画信息放在动画中,不会修改根节点位置。而取消勾选的话,将会将动画位移信息保存到根骨骼中。




勾与不勾效果对比:
勾选:TimeLine播放完后动画转身




取消勾选:播放完毕后,位置信息保存到了根节点上。






TimeLine无法很好处理两个根节点动画融合
实际上再非编辑模式下,是正常的,感觉是在编辑模式下,Animtor的App Root Motion选项功能没有激活。如果有这个问题,可以用Override动画修改位置,或者使用动画偏移修改位置












TimeLineClip名字自动修改
比如我将未赋值的相机轨道Clip赋值为01




当拖到层级窗口时,会自动修改名字成CinemachineShot




这让动态修改Clip的难度直线上线,目前没有找到很好的办法,只能赋值一个不用的初始物体解决。


总结
TimeLine是Unity的一个很好的功能,但是功能不完美,还是存在许多坑的地方。比如赋值Clips会需要PlayableGraph类,比如自定义脚本赋值问题,动画编辑模式下无法很好查看根节点动画问题。但是总的来说避免了坑点后加功能的时候是非常方便的。
有兴趣的同学可以下载工程源码看看:
https://github.com/chs71371/TimeLineBattle


有朋友吐槽GIT下载下来文件无效,补上网盘下载
链接:https://pan.baidu.com/s/1kVMQ9JD 密码:cpgy






对游戏开发感兴趣的同学,欢迎围观我们:【皮皮关游戏开发教育】 ,会定期更新各种教程干货,更有别具一格的线下小班教育。在你学习进步的路上,有皮皮关陪你!~
我们的官网地址:http://levelpp.com/
我们的游戏开发技术交流群:610475807
我们的微信公众号:皮皮关

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-23 11:23 , Processed in 0.093751 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表