Unity3D RPG Core | 35 场景转换的渐入渐出

这节我们使用 Unity Timeline 制作游戏开始时的开场动画。同时,之前的场景传送功能在视觉效果上有点突兀。我们增加一个场景切换的渐入渐出功能。

1. Timeline 创建动画

我们点击 Window - Sequencing - Timeline,打开如图 1 所示的 Timeline 窗体。

图1 Timeline 窗体

Timeline 窗体中我们可以看到 "To start creating a timeline, select a GameObject"。为此我们新建一个空对象,用于承载 Timeline 组件。接着点击 Timeline 窗体上的 Create 按钮,创建并保存 Timeline。

记得先要选中想要的对象,再在其下创建 Timeline。

1.1 为相机创建动画

我们首先为相机创建动画,让相机位置逐渐靠近传送门。在 Timeline 窗体左侧右击选择新建 Animation Track

接着如图 2 所示,我们把相机组件拖拽到动画轨道组件上。并且右上角选择 Timecode 模式。

图2 动画轨道

注意要切换到 Timecode 模式。此处视频中没有说。

设置好后点击录制按钮。选择相机组件下的 Position 位置,右击选择 Add Key 添加开始关键帧。

图3 添加关键帧

我们想要动画为 3 秒,因此如图 4 所示,将拖拽进度条到 3 秒的位置。接着修改相机的终点位置,这时会自动添加关键帧。

图4 添加结尾关键帧

注意添加第一帧关键帧之后,如果位置信息变化了会自动添加关键帧。所以需要先移动进度条到结尾。

至此相机的动画就制作好了,我们再次点击录制按钮结束录制。可以点击播放按钮,预览一下效果。

1.2 为角色人物创建动画

如图 5 所示,我们同样为人物角色创建一条 Animation Track。因为人物组件上已经有了动画控制器,所以我们可以将奔跑的动画拖拽到时间条上。

图5 角色人物动画帧和覆盖帧

但是默认奔跑动画只能播放一遍,为此我们如图 6 所示,将 Post-Extrapolate 设置为 Loop。同时将动画条放置最左端,然后拉到 3 秒处(即覆盖整个动画周期)。

图6 设置动画

添加了动画,角色人物的坐标会变到零的默认值。为此我们点击图 5 箭头处位置,选择 Add Override Track。我们就在这条新的轨道上设置角色位置变化动画,设置的方式和设置相机时一样。

如果想要首帧的位置和原先显示的一样,记得记录一下角色人物的起始位置。

1.3 禁用 EventSystem

在播放动画期间,我们还是可以和 UI 进行交互(比如键盘选择、按 Enter 键等)。这会影响到程序逻辑,因此我们新建一条 Activation Track,并拖拽指定 EventSystem。

默认 EventSystem 是处于 Active 状态的,我们需要在 Timeline 时间轴上将其删除。

2. 控制动画播放

动画默认情况下在程序运行的时候就会执行。我们需要在点击新游戏的时候再进行播放,所以如图 7 所示,我们取消勾选 Play On Awake,在代码中控制播放时机。

图7 播放设置

如代码清单 1 所示,我们改变之前主界面控制器的逻辑。首先通过 FindObjectOfType() 获取到动画播放器(第 11 行)。之前的新游戏点击事件 OnNewGameClick() 改为播放动画,即点击新游戏会播放动画。

最后需要考虑的是,我们何时运行之前的场景切换功能:我们为动画播放器设置播放完成的 stopped 事件(第 12 行),即在动画播放完后进行场景切换。

代码清单 1 控制动画播放
  1. public class MainMenuController : MonoBehaviour
  2. {
  3.     PlayableDirector m_director;
  4.  
  5.     private void Awake()
  6.     {
  7.         m_newGameButton.onClick.AddListener(OnNewGameClick);
  8.         m_continueButton.onClick.AddListener(OnContinueClick);
  9.         m_QuitButton.onClick.AddListener(OnQuitClick);
  10.  
  11.         m_director = FindObjectOfType<PlayableDirector>();
  12.         m_director.stopped += OnDirectorStopped;
  13.     }
  14.  
  15.     private void OnDirectorStopped(PlayableDirector obj)
  16.     {
  17.         PlayerPrefs.DeleteAll();
  18.         SceneController.GetInstance().TransferToDestinationInDifferentScene("Land", "地图入口点");
  19.     }
  20.  
  21.     void OnNewGameClick()
  22.     {
  23.         m_director.Play();
  24.     }
  25. }

3. 实现渐入渐出

首先我们创建一个新的画布,并在其下添加 Image UI 组件。如图 8 所示,我们按 Alt + Shift 键,再点击矩阵框出的按钮,以平铺 Image。

图8 平铺图片

接着我们在 Canvas 下添加 Canvas Group 组件,如图 9 所示,我们就是控制其 Alpha 值来实现渐入渐出。

图9 Canvas Group

3.1 代码控制

我们新建 C# 脚本,并命名为 FadeController。如代码清单 2 所示,我们首先获取到 CanvasGroup 组件(第 7 行);因为渐入渐出涉及不同场景,所以我们设置其场景切换时不销毁(第 8 行);定义渐入 FadeIn() 和渐出 FadeOut() 这两个协程格式的函数,因为需要外部指定调用,我们需要将其定义为公有方法。

代码清单 2 渐入渐出控制
  1. public class FadeController : MonoBehaviour
  2. {
  3.     CanvasGroup m_canvasGroup;
  4.  
  5.     private void Awake()
  6.     {
  7.         m_canvasGroup = GetComponent<CanvasGroup>();
  8.         DontDestroyOnLoad(this);
  9.     }
  10.  
  11.     public IEnumerator FadeOut(float time)
  12.     {
  13.         while (m_canvasGroup.alpha < 1)
  14.         {
  15.             m_canvasGroup.alpha += Time.deltaTime / time;
  16.             yield return null;
  17.         }
  18.     }
  19.  
  20.     public IEnumerator FadeIn(float time)
  21.     {
  22.         while (m_canvasGroup.alpha > 0)
  23.         {
  24.             m_canvasGroup.alpha -= Time.deltaTime / time;
  25.             yield return null;
  26.         }
  27.  
  28.         Destroy(gameObject);
  29.     }
  30. }

我们实现渐入渐出的方式是,在场景切换前通过预制体创建出新的 FadeController 实例,然后通过协程调用其中的渐入渐出函数。因此我们先将 FadeController 脚本拖拽到之前的画布组件上,并将其指定为预制体,然后原先场景中的画布组件就可以删除了。

因为渐入渐出画布是预制体实例化出来的,所以务必记得销毁。在代码清单 2 的第 28 行,当渐入到新场景完成后,就可以销毁组件了。

实现完 FadeController 并制作好预制体之后,我们就可以在 SceneController 中调用了。如代码清单 3 所示,我们首先指定预制体(第 3 行);接着在原先不同场景传送的协程前后,增加渐出(第 8 行)和渐入(第 28 行)。

代码清单 3 调用渐入渐出
  1. public class SceneController : Singleton<SceneController>
  2. {
  3.     public FadeController m_fadePrefab = null;
  4.  
  5.     IEnumerator TransferDifferentScene(string sceneName, string destPortalName)
  6.     {
  7.         FadeController fadeController = Instantiate(m_fadePrefab);
  8.         yield return StartCoroutine(fadeController.FadeOut(2.5f));
  9.  
  10.         AsyncOperation asyncLoad = SceneManager.LoadSceneAsync(sceneName);
  11.  
  12.         // Wait until the asynchronous scene fully loads
  13.         while (!asyncLoad.isDone)
  14.         {
  15.             yield return null;
  16.         }
  17.  
  18.         PortalController destPortal = GetDestinationPortal(destPortalName);
  19.         Instantiate(m_playerPrefab,
  20.             destPortal.m_point.position,
  21.             destPortal.m_point.rotation);
  22.  
  23.         while (GameManager.GetInstance().m_player == null)
  24.         {
  25.             yield return null;
  26.         }
  27.  
  28.         yield return StartCoroutine(fadeController.FadeIn(2.5f));
  29.     }
  30. }

返回主界面的不同场景加载接口函数有所不同。如代码清单 4 所示,加上渐入渐出也是同样的操作。

代码清单 4 其他渐入渐出调用
  1. public class SceneController : Singleton<SceneController>
  2. {
  3.     IEnumerator TransferDifferentScene(string sceneName)
  4.     {
  5.         FadeController fadeController = Instantiate(m_fadePrefab);
  6.         yield return StartCoroutine(fadeController.FadeOut(2.5f));
  7.  
  8.         AsyncOperation asyncLoad = SceneManager.LoadSceneAsync(sceneName);
  9.  
  10.         // Wait until the asynchronous scene fully loads
  11.         while (!asyncLoad.isDone)
  12.         {
  13.             yield return null;
  14.         }
  15.  
  16.         yield return StartCoroutine(fadeController.FadeIn(2.5f));
  17.     }
  18. }

4. 角色死亡逻辑完善

以上已经完成了渐入渐出的功能了。在此基础上,注意到人物角色死亡时也可以回到主界面,以完善角色死亡这块的逻辑。

实现的方式之前也已经有过了,如代码清单 5 所示,继承 IEndGameObserver,实现其中的响应接口,响应的措施为返回主界面。同时需要在 Start 函数中注册观察者。

代码清单 5 观察者接口
  1. public class SceneController : Singleton<SceneController>, IEndGameObserver
  2. {
  3.     // Start is called before the first frame update
  4.     void Start()
  5.     {
  6.         GameManager.GetInstance().AddEndGameObserver(this);
  7.     }
  8.  
  9.     public void BackToMainMenu()
  10.     {
  11.         if (SceneManager.GetActiveScene().name != "MainMenu")
  12.         {
  13.             StartCoroutine(TransferDifferentScene("MainMenu"));
  14.         }
  15.     }
  16.  
  17.     void IEndGameObserver.OnEndGameEvent()
  18.     {
  19.         BackToMainMenu();
  20.     }
  21. }

我们还需要更改一下之前的通知机制。因为死亡是一个状态更改,只需要通知一次就可以了,每帧通知会造成负载过大,且影响以上的场景加载逻辑。如代码清单 6 所示,我们新增一个变量,指示是否通知过,以确保只通知过一次。

代码清单 6 被观察者通知
  1. public class GameManager : Singleton<GameManager>
  2. {
  3.     bool m_endGameHasNotified = false;
  4.  
  5.     // Update is called once per frame
  6.     void Update()
  7.     {
  8.         if (m_endGameHasNotified == false &&
  9.             m_player != null &&
  10.             m_player.m_characterData.CurrentHealth == 0)
  11.         {
  12.             m_endGameHasNotified = true;
  13.             NotifyEndGame();
  14.         }
  15.     }
  16. }

5. 测试

如果测试的时候,想要修改动画逻辑,比如想要人物先跑一段时间,相机再跟上。我们不需要重新制作动画,可以直接在现有 Timeline 上修改:如图 10 所示,我们双击相机时间轴上的动画。进入编辑界面后,可以拖拽移动第一帧关键帧的位置,让它延后一点。

图10 修改关键帧

图 11 所示的是进入新游戏时的渐入渐出效果。

图11 渐入渐出效果

图 12 所示的是角色死亡时返回主界面的渐入渐出效果。

图12 角色死亡效果