此教程來自siki學院的<<暗黑戰神>>課程
這次需要記錄的是動態顯示窗口的制作方式,它的效果是彈出一條游戲Tips,上面可以顯示你想顯示的內容,隨后消失。
顯然,我們只需要制作一個動畫,動畫中改變Text組件的位置即可實現此效果。
然而現在的問題是,不能讓這個動畫立刻播放,我們需要在特定的時候去播放它,並在特定的時刻停止。
那么怎么實現呢?
我們可以在控制動態顯示窗口的腳本中,設置一個方法,當要顯示動態Tips時,設置改變Text內容,並激活對應的游戲物體,手動控制動畫播放。注意,游戲物體不激活時即使動畫是自動播放的,也不會播放,只有激活了,才會從頭開始播放。
動畫播放后需要停止,因此需要把游戲物體取消激活,但我們在代碼中怎么准確地控制它能延時取消激活呢?
這個問題和之前說的異步加載資源服務有點類似,傳送門如下:
https://www.cnblogs.com/czw52460183/p/11044456.html
當時的思路是在Update中用委托實時查看加載進度,但這里就不行,因為我們沒有什么方法可以實時獲取到動畫播放進度,怎么辦呢?
答案是可以使用協程,我們只需要在開啟協程后,讓它在固定等待一段時間后調用回調方法即可,在這個回調方法中去關閉物體的激活狀態即可,而這個等待的時間,就設置成動畫的持續時長即可,這樣,即使我們沒有辦法掌握動畫的實際播放進度,也可以實現延時關閉。
參考代碼如下:(由於只是講述思路,這里代碼中的細節就不詳細敘述了,因為用到的一些方法都是之前制作的,這里沒給出具體信息,只看一下思路即可)
/**************************************************** 文件:DynamicWnd.cs 作者:czw52460183 郵箱: czw52460183@163.com 日期:2019/6/22 10:8:16 功能:動態UI元素界面 *****************************************************/ using System; using System.Collections; using UnityEngine; using UnityEngine.UI; public class DynamicWnd : WindowRoot { public Text txtTips; public Animation tipsAni; //本窗口初始化時需要: //1.使txtTips所屬的游戲物體不激活 //注意,游戲物體不激活時即使動畫是自動播放的,也不會播放,只有激活了,才會從頭開始播放 protected override void InitWnd() { base.InitWnd(); SetActive(txtTips,false); } /// <summary> /// 設置界面的展示內容 /// </summary> public void SetTips(string tips) { //激活txtTips所屬的游戲物體 SetActive(txtTips, true); //設置展示內容 SetText(txtTips,tips); //播放動畫 tipsAni.Play(); //利用協程實現延時關閉激活狀態 AnimationClip clip = tipsAni.clip; StartCoroutine(AniPlayDone(clip.length,()=>{ //延時到動畫結束后關閉激活狀態 SetActive(txtTips,false); })); } //協程,延時后調用回調方法 private IEnumerator AniPlayDone(float sec, Action cb) { yield return new WaitForSeconds(sec); if(cb != null) { cb(); } } }
Unity中是單線程的,我的理解是,開啟協程后,系統會在每幀檢查yield return的條件是否滿足,滿足則執行yield return后面的方法,不滿足則繼續執行開啟協程語句后面的指令,這就使得雖然Unity是單線程的,但協程會給人一種多線程的感覺。
但使用協程也會帶來新的問題,就是由於它模擬出了多線程的效果,自然也會出現多線程有的問題,假如我們的系統在兩個不同的地方同時調用了SetTips方法,會導致第一個SetTips在開啟協程后繼續執行下面的指令,此時第一個動畫在播放,但是還沒到進入yield return的時間,這時系統在執行后面的指令,即開始執行第二個SetTips指令,此時又會設置展示內容,因此就把第一個動畫設置的展示內容覆蓋掉了,導致的結果就是只展示了第二個SetTips的指令。
怎么辦呢?
可以使用一個隊列,當每一條SetTips請求進來時,把它加入到隊列中,而在每一幀中對隊列中的元素數量進行檢查,若隊列中有元素,就取出對應內容並進行展示內容的設置。注意,在向隊列中添加或取出元素時,由於可能出現競爭,因此要加鎖。
代碼如下:
/**************************************************** 文件:DynamicWnd.cs 作者:czw52460183 郵箱: czw52460183@163.com 日期:2019/6/22 10:8:16 功能:動態UI元素界面 *****************************************************/ using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class DynamicWnd : WindowRoot { public Text txtTips; public Animation tipsAni; //本窗口初始化時需要: //1.使txtTips所屬的游戲物體不激活 //注意,游戲物體不激活時即使動畫是自動播放的,也不會播放,只有激活了,才會從頭開始播放 protected override void InitWnd() { base.InitWnd(); SetActive(txtTips,false); } //用隊列來存放要展示的內容 private Queue<string> tipsQueue = new Queue<string>(); /// <summary> /// 把展示的內容添加到隊列中 /// </summary> public void AddTips(string tips) { //添加前要為隊列加鎖,防止沖突 lock(tipsQueue) { tipsQueue.Enqueue(tips); } } //每一幀檢測隊列中是否有元素,有就取出並展示 private void Update() { if(tipsQueue.Count > 0) { //取出前也要為隊列加鎖,防止沖突 lock (tipsQueue) { string tips = tipsQueue.Dequeue(); SetTips(tips); } } } /// <summary> /// 設置界面的展示內容 /// </summary> private void SetTips(string tips) { //激活txtTips所屬的游戲物體 SetActive(txtTips, true); //設置展示內容 SetText(txtTips,tips); //播放動畫 tipsAni.Play(); //利用協程實現延時關閉激活狀態 AnimationClip clip = tipsAni.clip; StartCoroutine(AniPlayDone(clip.length,()=>{ //延時到動畫結束后關閉激活狀態 SetActive(txtTips,false); })); } //協程,延時后調用回調方法 private IEnumerator AniPlayDone(float sec, Action cb) { yield return new WaitForSeconds(sec); if(cb != null) { cb(); } } }
紅色的是要修改的地方,這里相當於是把協程帶來的沖突問題轉移到了Update中。
但這樣的修改是否能解決問題?顯然不行,雖然沖突問題轉移到了Update中,但Update是每一幀執行一次的,速度很快,所以面臨的狀況和之前其實一樣,第一個設置內容請求(這里是AddTips)使協程開啟后,由於Update執行間隔很短,第二個請求依然會在動畫播放完成前進入SetTips。
我們可以在這基礎上再加一個標志變量,標記當前是否有Tips在展示,只有沒有展示時才從隊列中取元素並展示,當動畫播放完,即協程返回時,才將標記變量重新恢復為無展示狀態。
代碼如下:
/**************************************************** 文件:DynamicWnd.cs 作者:czw52460183 郵箱: czw52460183@163.com 日期:2019/6/22 10:8:16 功能:動態UI元素界面 *****************************************************/ using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class DynamicWnd : WindowRoot { public Text txtTips; public Animation tipsAni; //本窗口初始化時需要: //1.使txtTips所屬的游戲物體不激活 //注意,游戲物體不激活時即使動畫是自動播放的,也不會播放,只有激活了,才會從頭開始播放 protected override void InitWnd() { base.InitWnd(); SetActive(txtTips,false); } //標志變量,代表當前是否有Tips在展示 private bool isTipsShow = false; //用隊列來存放要展示的內容 private Queue<string> tipsQueue = new Queue<string>(); /// <summary> /// 把展示的內容添加到隊列中 /// </summary> public void AddTips(string tips) { //添加前要為隊列加鎖,防止沖突 lock(tipsQueue) { tipsQueue.Enqueue(tips); } } //每一幀檢測 private void Update() { //只有當隊列中有元素,且當前沒有Tips在展示時才從隊列中取元素展示 if (tipsQueue.Count > 0 && isTipsShow == false) { //取出前也要為隊列加鎖,防止沖突 lock (tipsQueue) { string tips = tipsQueue.Dequeue(); //標記當前為正在展示狀態 isTipsShow = true; SetTips(tips); } } } /// <summary> /// 設置界面的展示內容 /// </summary> private void SetTips(string tips) { //激活txtTips所屬的游戲物體 SetActive(txtTips, true); //設置展示內容 SetText(txtTips,tips); //播放動畫 tipsAni.Play(); //利用協程實現延時關閉激活狀態 AnimationClip clip = tipsAni.clip; StartCoroutine(AniPlayDone(clip.length,()=>{ //延時到動畫結束后關閉激活狀態 SetActive(txtTips,false); //標記當前為無展示狀態 isTipsShow = false; })); } //協程,延時后調用回調方法 private IEnumerator AniPlayDone(float sec, Action cb) { yield return new WaitForSeconds(sec); if(cb != null) { cb(); } } }
紅色為添加內容,具體不再介紹。
完畢。