Siki_Unity_3-6_UI框架 (基於UGUI)

Unity 3-6 UI框架 (基於UGUI)

任務1&2&3&4:介紹 && 建立工程

UI框架:
  管理場景中全部UI面板
  控制面板之間的跳轉php

  若是沒有UI框架,會經過面板之間的交叉訪問來實現這些功能,管理混亂html

建立工程UIFrameWork:
  建立工程目錄
    json

  導入素材,將素材放入Images文件夾下
    將全部素材的Texture Type修改成Sprite(2D and UI)canvas

任務5:主菜單面板

建立 UI->Panel,命名MainMenuPanel,
  Image->SourceImage: BG數組

將Canvas的Canvas Scaler.UI Scale Mode = Scale with Screen Size
  將Match設置爲Height
  -- 即自適應:
    若是UI Scale Mode爲默認的 Constant Pixel Size時,文字等的大小不會基於屏幕大小而改變
    設置爲Scale With Screen Size, 控件的大小會基於Height或Width(只有一個)的變化而變化安全

新建空物體,命名IconPanel,位於屏幕右下角,並設置Anchorapp

新建Image: SourceImage 任務 TastButton
  添加Button組件
  新建Text:顏色黃白色,大小,字體,對齊等
    給Text添加Shadow組件
    取消Raycast Target的勾選框架

在Project的Images文件夾下新建UI Prefab文件夾,將上述按鈕製做成prefabide

經過上述prefab製做其餘按鈕,依次爲揹包、戰鬥、技能、商店、系統函數

在MainMenuPanel中建立Image,命名PersonalInfoPanel,SourceImage: pic_我的信息
  取左上角爲Anchor,位置也移到左上角

  新建Image,命名portrait,SourceImage: 頭像底板女性

將整個MainMenuPanel製做成Prefab,放入Resources->UIPanel文件夾

任務6:任務面板

新建Image,命名TaskPanel,SourceImage: bg-任務
  位置居中,anchor居中

  新建Text,內容"任務",位於標題欄處,修改字體、顏色等

  新建Button,命名CloseButton,SourceImage:  btn_關閉1
    刪除自帶的Text
    Button.Transition選擇Sprite Swap (默認ColorTint)
      分別選擇:
      Hightlighted Sprite: btn_關閉2
      Pressed Sprite: btn_關閉3
      Disabled Sprite: btn_關閉4

製做成Prefab,放入Resources.UIPanel文件夾

任務7:揹包和物品彈框信息面板

揹包面板:
新建Image,命名KnapsackPanel,SourceImage: bg_揹包,居中

  新建關閉按鈕,使用上一節中的便可

  新建Image,命名ItemSlot,SourceImage: bg_道具
    新建Image,命名Item,SourceImage: 大致力丹
  由於KnapsackPanel和ItemSlot不須要點擊,所以將Image的Raycast Target取消勾選
    在Item中添加Button組件

物品彈框信息面板:
新建Image,命名ItemInfoPanel,SourceImage: bg_彈框,取消RaycastTarget的勾選

  新建關閉按鈕

任務8:其餘的UI面板

點擊戰鬥按鈕,會直接跳轉到戰鬥場景,所以不須要面板
技能、商城、系統的面板,都和TaskPanel差很少,賦值TaskPanel,修改便可

技能面板:SkillPanel

商城面板:ShopPanel

系統面板:SettingPanel

修改完名字,製做成Prefab,進行更改
  將Text的內容分別修改成技能、商城、系統設置便可

  系統面板的寬度修改小一些

任務9:經過Json和枚舉保存全部面板的信息

在進行面板切換以前,須要知道當前項目有哪些面板、各個面板的加載路徑
  將這些信息傳遞給UI框架後,再由UI框架進行UI面板的加載操做
  -- 使用Json信息來保存全部的面板和路徑
     每個面板都定義一個枚舉類型

新建腳本UIPanelType.cs

枚舉類型不須要繼承自MonoBehaviour
一個Panel對應一個UIType類型

public enum UIPanelType {
    TaskPanel,
    KnapsackPanel,
    ItemInfoPanel,
    SkillPanel,
    ShopPanel,
    SettingPanel
}

新建Json文件(在電腦中UIFramework文件夾下建立文本文檔)

UIPanelType.json -- 用來保存全部面板對應的存儲路徑
  -- bug!!! 任務11中發現不能這麼寫,不能被JsonUtility解析

[
  {
    "panelType": "MainMenuPanel",
    "path": "UIPanel/MainMenuPanel"
  },
  {
    "panelType": "TaskPanel",
    "path": "UIPanel/TaskPanel"
  },
  {
    "panelType": "KnapsackPanel",
    "path": "UIPanel/KnapsackPanel"
  },
  {
    "panelType": "ItemInfoPanel",
    "path": "UIPanel/ItemInfoPanel"
  },
  {
    "panelType": "SkillPanel",
    "path": "UIPanel/SkillPanel"
  },
  {
    "panelType": "ShopPanel",
    "path": "UIPanel/ShopPanel"
  },
  {
    "panelType": "SettingPanel",
    "path": "UIPanel/SettingPanel"
  }
]

經過百度,進行Json文件的校驗

使用Unity自帶工具 JsonUtility進行Json數據的解析

任務10&11&12:開發UIManager解析Json信息 && UIManager單例模式

將Json中的信息讀取並存儲到Dictionary中,key爲type,value爲path

在UIFramework文件夾下建立UIManager.cs類
  UIManager是一個管理器,不是組件,所以不須要繼承自MonoBehaviour
  UIManager是單例模式

private Dictionary<UIPanelType, string> panelPathDict; // 來存儲全部面板prefab的路徑

解析Json信息的方法:

在UIFramework文件夾下建立Resources文件夾,將.json文件放入(由於須要使用Resources.Load()加載數據
TextAsset ta = Resources.Load<TextAsset>("path");
  // 這裏的path是UIPanelType,見Unity的project中,是沒有json後綴的,因此不用寫

JsonUtility API:
  FromJson() -- Create an Object from its Json representation
  ToJson() -- Generate a Json representation of the public fields of object
  首先須要定義一個類,用於存儲Json文件中的數據,類中成員變量須要和Json數據徹底對應

  注意點:
    JsonUtility支持的數據格式是對象的格式,必須符合:{ "key": ["", "", "", ... ] }
    若是是 json 數組會提示錯誤 JSON must represent an object type
    被轉換的對象必須是可被序列化的,須要標記 [System.Serializable] 屬性

新建腳本UIPanelData.cs
  相同的,UIPanelData用於存儲信息,不須要繼承自MonoBehaviour
  [Serializable] -- 可序列化的 -- using System;
    序列化:將內存的數據存儲到硬盤上
    反序列化:從文本信息到對象的過程
    這裏須要把文本文件的內容讀取到內存中,而這個類就是用於傳遞數據的。
    因此這個類須要用 [Serializable] 標識

  變量名徹底相同:
    public UIPanelType panelType;
    public string path;

[System.Serializable] public class UIPanelInfo {
    public UIPanelType panelType;
    public string path;
}

-- 注意,這裏的兩個Serializable都是必須的,UIPanelInfo和UIPanelInfoList

經過JsonUtility將數據讀取並存儲到UIPanelInfo的對象中
  -- (這裏是有bug的,下面詳述)

JsonUtility.FromJson< List<UIPanelInfo> >(textAsset.text);
// 返回值即List<UIPanelInfo>,新建變量存儲
List<UIPanelInfo> panelInfoList = ...;

將List中的信息傳遞到Dictionary中
foreach( var element in panelInfoList) {
  panelPathDict.Add(element.panelType, element.path);
}

將UIManager寫成單例模式
  -- 構造方法私有化,防止在外界實例化/ 調用構造方法
     內部進行實例化

  在構造方法中調用上述獲取數據的方法
  private UIManager() {
    ParsePanelTypeJson();
  }

public class UIManager {
    private Dictionary<UIPanelType, string> panelPathDict;

    // 單例模式 -- 構造函數爲private
    private UIManager () {
        ParsePanelTypeJson();
    }

    private void ParsePanelTypeJson() {
        // 用Resources.Load()從Json文件中讀取text數據
        TextAsset textAsset = Resources.Load<TextAsset>("UIPanelType");
        // 將Json格式的text數據轉換成List<>數據
        List<UIPanelInfo> panelInfoList = 
            JsonUtility.FromJson<List<UIPanelInfo>>(textAsset.text);
        // 把List的存儲到Dictionary中
        foreach(var element in panelInfoList) {
            panelPathDict.Add(element.panelType, element.path);
}}}

定義一個靜態私有變量
  private static UIManager _instance;
  提供這個靜態私有變量的構造方法

public static UIManager Instance {
    // get方法公有,由於須要在外界訪問該實例
    get{
        if(_instance == null) {
            // 第一次訪問Get時,就會建立一個UIManager,且只有在這裏可以建立
            _instance = new UIManager();
            // UIManager構造函數爲私有,所以只能在內部調用
            // 構造的時候,對Json數據進行解析讀取
        }
        // 以後訪問時,就會直接獲得以前建立的UIManager,保證了單例
        return _instance;
    }
}

新建GameRoot.cs腳本,用於控制遊戲啓動,至關於開始腳本
  把GameRoot掛載在Canvas上,由於他是UI的啓動器

UIManager.Instance.Test();

報錯:ArgumentException: JSON must represent an object type.

https://answers.unity.com/questions/1148632/jsonutility-and-arrays-error-json-must-represent-a.html
https://blog.csdn.net/kenight/article/details/78787259
緣由:JSON須要轉爲一個Object,而不能是一個List的Object
在UIManager中建立類UIPanelInfoList,用來存儲List<UIPanelInfo>
  [System.Serializable]
  class UIPanelInfoList {
    public List<UIPanelInfo> panelInfoList;
  }

將UIPanelType.json中的數據改成 -- 一個對象,對象裏只有一個屬性
  {
    "panelTypeList" : [
    {...}, {...}, {...}, ...
    ]
  }

此時,從JsonUtility.FromJson<UIPanelInfoList>(textAsset.text)返回的爲UIPanelInfoList類型的對象

進行foreach (UIPanelInfo panelInfo in panelInfoList.panelInfoList) { }
  如果Debug.Log出每個panelInfo.panelType,會發現輸出了7個MainMenuPanel

-- 爲何呢???
緣由:JsonUtility解析時發生了錯誤:返回的都是枚舉類型UIPanelType的默認值MainMenuPanel
  詳細說明:Json解析對於UIPanelInfo中的成員變量UIPanelType沒法解析

  解決方案:
    在public UIPanelType panelType;上加上 [NonSerialized],表示不須要解析
    添加成員變量 public sting panelTypeString;
    相對應的,將json文件中的key從"panelType"改成"panelTypeString"

    如今,Json數據就能被正常解析了

可是:UIPanelInfo.panelType並未賦值

讓UIPanelInfo繼承自ISerializationCallbackReceiver接口

須要實現兩個方法

public void OnAfterDeserialize() {
  // 在反序列化以後自動調用 -- 即從Json文本數據解析至對象以後,進行調用
  panelType = (UIPanelType) System.Enum.Parse(typeof(UIPanelType, panelTypeString);
}

public void OnBeforeSerialize() {
  // 在序列化以前自動調用 -- 這裏沒有牽涉到序列化
  // 將對象的數據寫成Json文本文件
}

// System.Enum.Parse(System.Type enumType, string value);
// 返回類型爲object,將object強制轉換爲UIPanelType,賦值給panelType
// 即完成了賦值操做 -- 將string的值賦給了相應的枚舉類型

[System.Serializable]
public class UIPanelInfo : ISerializationCallbackReceiver {

    [System.NonSerialized]
    public UIPanelType panelType;
    public string panelTypeString; // Json解析賦值給string,不能給Enum
    public string path;

    public void OnAfterDeserialize() {
        // 反序列化以後調用,即從Json文本數據解析至對象以後,會進行調用
        // Debug.Log(panelTypeString);
        panelType = (UIPanelType)System.Enum.Parse(typeof(UIPanelType), panelTypeString);
    }

    public void OnBeforeSerialize() {
    }
}


成功輸出每一個UIPanelType,和最後一個從Dictionary中取得的path值

任務13:面板基類BasePanel

獲得了panel的路徑之後,就能夠經過路徑來加載panel的prefab
加載出來的panel就是一個遊戲物體,每一個panel都須要一個對應的腳原本實現它的功能

BasePanel.cs -- 實現全部面板共有的功能
  其餘全部面板的腳本都須要繼承自BasePanel,如 ItemInfoPanel : BasePanel
  由於其餘面板是可變的,不屬於UI框架的,所以將這些腳本放入Scripts->Panel,而BasePanel.cs放入UIFramework->Base

任務14&15:建立和管理UI面板的Prefab的實例化 && Dictionary的擴展方法

UIManager.cs中管理全部UI面板

建立字典 panelDict -- 保存全部的實例化後的面板上所掛載的BasePanel腳本組件
  private Dictionary<UIPanelType, BasePanel> panelDict;

方法GetPanel -- 根據panelType來獲得實例化面板上所掛載的BasePanel腳本組件
  private BasePanel GetBasePanel(UIPanelType panelType) {
    // 判斷字典是否實例化
    if(panelDict == null) {
      // 建立字典
      panelDict = new Dictionary<UIPanelType, BasePanel>();
    }
    BasePanel panel;
    panelDict.TryGetValue(panelType, out panel);

// 若是panel爲空,則該類型panel未實例化 -- 須要實例化該panel
if(panel == null) {
  string path;
  panelPathDict.TryGetValue(panelType, out path);
  GameObject instantPanel = GameObject.Instantiate(Resource.Load(path)) as GameObject;

  // 須要將panel放在Canvas下做爲子物體
  -- private Transform canvasTrans;
  -- private Transform CanvasTrans {
    get {
      if(canvasTrans == null) {
        canvasTrans = GameObject.Find("Canvas").transform;
      }
      return canvasTrans;
     }}
  instantPanel.transform.SetParent(CanvasTrans);

  // 以後運行的時候會發現,若是就這麼寫,由於canvas的scale不是1,因此panel的位置信息會錯亂,
  // 須要修改爲instantPanel.transform.SetParent(CanvasTrans, false);
  // -- 重載方法SetParent(Transform, bool worldPositionStays): 是否保持物體在世界座標中的位置
    若是爲true,則物體的局部座標會爲了使物體在世界中的位置不變而改變
    若是爲false,則物體的局部座標就是本來的世界座標

  // 將剛實例化的panel存入panelDict
  panelDict.Add(panelType, instantPanel.GetComponent<BasePanel>());
  return instantPanel.GetComponent<BasePanel>();

} else { // panel已經存在
  return panel;
}

-- 由於Dictionray.TryGetValue(key, out value),所以每次都須要定義一個BasePanel panel,顯得很麻煩
  定義一個Dictionary的擴展方法,簡化操做(放在UIFramework->Extension文件夾)
  -- 如何給系統內之類擴展方法
    擴展方法的用處:當一個類的源碼沒法修改,可是又想後期添加方法的時候,就可使用擴展方法

  思路:
  1. 類應該爲static
  2. 擴展的方法爲static
  3. 返回值類型爲泛型,由Dictionary的value類型而定
  4. 須要傳入Dictionary和key -- 在使用的時候不須要傳遞Dictionary,詳見下
    這裏的Dictionary須要加修飾符this,表示這個方法是Dictionary類的擴展方法,經過這個類的對象進行調用
    由於this就表示了當前對象

DictionaryExtension.cs

public static class DictionaryExtension {
    public static TValue TryGetValueExtension<TKey,TValue>(
            this Dictionary<TKey,TValue> dict, TKey key) {
        TValue value;
        dict.TryGetValue(key, out value);
        return value;
    }
}

    使用:
      在UIManager中,有兩個地方替換
      BasePanel basePanel處:
        BasePanel basePanel = panelDict.TryGetValueExtension(panelType);
      string path處:
        string path = panelPathDict.TryGetValueExtension(panelType);

任務16&:用棧Stack存儲在場景中顯示的Panel
17&18&19:面板之間的跳轉

Panel的三種狀態:
  prefab -- 完成 -- panelPathDict
  在場景中被實例化 -- 完成 -- panelDict
  在場景中顯示 -- 未完成 -- 經過棧來管理

爲何適合用棧來實現呢?
  面板是一個一個出現,一個一個關閉的
  先打開的最後關閉 -- 先入後出

代碼實現:UIManager.cs中

private Stack<BasePanel> panelDisplayedStack;

何時入棧?
  顯示頁面的時候
  public void PushPanel(UIPanelType panelType) {
    // 獲得該類型的panel
    BasePanel panel = GetBasePanel(panelType);
    // 入棧
    if(panelDisplayedStack == null) {
      panelDisplayedStack = new Stack<BasePanel>();
    }
    panelDisplayedStack.Push(panel);
  }

MainMenuPanel是須要存在的
在GameRoot.cs中
Start() {
  UIManager.Instance.PushPanel(UIPanelType.MainMenuPanel);
}

Panel之間的切換 -- 經過點擊MainMenuPanel上的按鈕進行切換

切換Panel的代碼在每個繼承自BasePanel的子類 (這裏是MainMenuPanel.cs)中實現

  public void OnPushPanel(UIPanelType panelType) {
  }

  在Inspector中,將這個方法註冊到全部按鈕上,這時,發現找不到這個方法
  緣由多是識別不了UIPanelType類型的參數,將其換成string類型,就能夠成功註冊了

  在OnPushPanel中:
    // 將string轉換成枚舉類型UIPanelType
    UIPanelType panelType = (UIPanelType)System.Enum.Parse(typeof(UIPanelType), panelTypeString);
    // 入棧
    UIManager.Instance.PushPanel(panelType);

  如今,當咱們點擊按鈕時,就會進行入棧
  (出棧在任務22中講解)

  運行,發現點擊按鈕,相應的panel就會顯示出來
  1. battle按鈕沒有對應的panel,瞭解一下
  2. 若重複點擊一個按鈕,會報錯:
    ArgumentException: An element with the same key already exists in the dictionary.

  1. 刪除battle按鈕的OnPushPanel()註冊
  2. 發現是由於在GetBasePanel()實例化一個panel後,出現判斷錯誤
    如:
    
    前三句:在GameRoot中執行PushPanel(MainMenuPanel);
    中間三局:點擊TaskButton
    最後一句:再次點擊TaskButton,報錯
      緣由很明顯:在進行panelDict.Add()的時候沒有判斷是否panel已在dict中存在
      可是這個緣由並不能成爲緣由,由於panelDict.Add()的調用前提就是沒有找到panel,才進行實例化的
      所以報錯的緣由在與basePanel == null的判斷上
    發現第3句和第6句的區別:
      第一次push時,將MainMenuPanel push進去了,可是第二次push時,push的爲空物體

    解決方案:
      在PushPanel中,將panel輸出,結果爲空
      而在上面實例化的instantPanel是沒問題的,可是instantPanel.GetComponent<BasePanel>()爲空
      找了半天,媽蛋,是由於沒有將TaskPanel等的TaskPanel類聲明爲BasePanel的子類
      就像大司馬說的同樣:哇,好煩

3. 爲何會直接顯示呢,並且
  由於入棧會GetBasePanel()
  -- 第一次調用,在panelDict中找不到對應panel,會進行Instantiate(prefab)
    所以,第一次調用的時候,會按照點擊按鈕的順序直接在Game視圖中顯示出panel
    可是,以後再點擊,就沒有反應了。
  並且,再次點擊雖然不會Instantiate新的Prefab,可是會對該prefab進行push stack操做

  解決方法:是要進行在Stack中是否存在的判斷嗎?
    不,直接將後臺禁用。
    正常的遊戲設計是,只有在Stack棧頂的panel才能進行操做
      好比,打開TaskPanel後,MainMenuPanel的功能就不能實現了

代碼實現:

給每一個panel添加自身的生命週期,經過生命週期來控制panel當前的狀態
生命週期狀態:
  進入顯示 -- 點擊相關按鈕打開該panel,push入棧
  暫停 -- 在該panel中點擊按鈕,打開其餘panel,不在棧頂
  繼續 -- 關閉了其餘panel,回到棧頂
  移出顯示 -- 關閉了自身,從棧中pop出去


由於每一個頁面都有這個生命週期,所以將這些定義在BasePanel中
在UIManager中進行調用

在BasePanel中
  public virtual void OnEnter() {}
  public virtual void OnPause() {}
  public virtual void OnResume() {}
  public virtual void OnExit() {}

任務20&21&22:實現上述出棧入棧和對應生命週期功能

上一節中在基類BasePanel中定義了虛函數

這些函數的觸發在UIManager中實現

UIManager.cs中:

入棧 -- 

在Push入棧以前,調用新入棧panel的OnEnter()
同時判斷,若以前棧不爲空,須要調用以前的棧頂panel的OnPause()

在PushPanel()中
  panel.OnEnter();
  if(stack.Count > 0) {
    stack.Peek().OnPause();
  }

MainMenuPanel中的override:

public override void OnPause() {
  // 讓MainMenuPanel再也不跟鼠標進行交互
  // -- 在MainMenuPanel物體中添加組件Canvas Group
  // -- 若將CanvasGroup.BlocksRaycasts設爲false時,就不會進行鼠標射線檢測了
  // -- private CanvasGroup canvasGroup = GetComponent<CanvasGroup>();
  canvasGroup.blocksRaycasts = false;
}

出棧 --

當點擊關閉按鈕時,須要註冊給關閉按鈕一個方法:
  以TaskPanel爲例,由於MainMenuPanel沒有關閉按鈕
  TaskPanel.OnClosePanel();
  {
    UIManager.Instance.PopPanel(panelType);
  }

在UIManager.PopPanel()中
  {
    if(panelDisplayedStack == null) { ... = new ...; }
    if(panelDisplayedStack.Count > 0) {
      // 我的認爲通常狀況下不會出現爲空,由於點擊關閉按鈕就表示當前有panel顯示
      // 獲得棧頂panel,即即將被關閉的panel
      BasePanel panel = panelDisplayedStack.Pop();
      // 關閉
      panel.OnExit();
      // 激活下一層的panel -- 若是有的話(沒有的話就不操做)
      if(panelDisplayedStack.Count > 0) {
        panelDisplayedStack.Peek().OnResume();
  } } }

在TaskPanel中,須要override OnExit:

  public override void OnExit() {
    // CanvasGroup.alpha = 0 表示不顯示
    // 注意,這裏只是不顯示,所以還需暫停鼠標交互
    canvasGroup.alpha = 1;
    canvasGroup.blocksRaycasts = false;
  }

在MainMenuPanel中,須要override OnResume:

public override void OnResume() {
  canvasGroup.blocksRaycasts = true;
}

運行:
  顯示主菜單,點擊任務,顯示任務菜單,關閉,任務菜單消失,再點擊任務菜單,沒有反應
  緣由:由於任務面板的canvasGroup.alpha = 0;不顯示(固然,也一樣不能交互)
  解決方法:
    由於再次點擊任務,會觸發MainMenuPanel.OnPushPanel()
      -> UIManager.PushPanel() -> TaskPanel.OnEnter()
      在OnEnter()中加上
        canvasGroup.alpha = 1;
        canvasGroup.blocksRaycasts = true;
解決了嗎?
運行:報錯 --
  NullReferenceException: Object reference not set to an instance of an object
  TaskPanel.OnEnter () (at Assets/Scripts/Panel/TaskPanel.cs:18)
  在canvasGroup.alpha = 1;處報錯
緣由:
  當第一次實例化TaskPanel時
  即點擊任務按鈕->MainMenuPanel.OnPushPanel()->UIManager.PushPanel()->GetBasePanel()時
    實例化完TaskPanel,返回taskPanel給PushPanel(),就當即調用了taskPanel.OnEnter()
    此時,taskPanel.Start()還未被調用???爲何???
解決方法:
  在OnEnter()中進行判斷
  if(canvasGroup == null) {
    canvasGroup = GetComponent<CanvasGroup>();
  }
  ...

  在Start()中也進行相同判斷

// UIManager.cs中
public void PushPanel(UIPanelType panelType) {
    BasePanel panel = GetBasePanel(panelType);
    // Debug.Log(panel);
    if(panelDisplayedStack == null) {
        panelDisplayedStack = new Stack<BasePanel>();
    }
    // 進行生命週期的調用
    // 由於新Push入棧,因此須要調用OnEnter()
    panel.OnEnter();
    // 若是以前的棧不爲空,則以前的棧頂panel須要調用OnPause()
    if(panelDisplayedStack.Count > 0) {
        // Debug.Log("peek on pause: " + panelDisplayedStack.Peek());
        panelDisplayedStack.Peek().OnPause();
    }
    panelDisplayedStack.Push(panel);
    // Debug.Log("Push " + panel + " 進入Stack");
}
public void PopPanel() {
    if(panelDisplayedStack == null) {
        // 不會吧?
        panelDisplayedStack = new Stack<BasePanel>();
    }
    if(panelDisplayedStack.Count > 0) {
        // 通常狀況不會爲空,但仍是作一下安全校驗
        BasePanel panel = panelDisplayedStack.Pop();
        panel.OnExit();
        // Resume新的棧頂panel
        if (panelDisplayedStack.Count > 0) {
            panelDisplayedStack.Peek().OnResume();
}}}
public class MainMenuPanel : BasePanel {
    private CanvasGroup canvasGroup;
    private void Start() {
        canvasGroup = GetComponent<CanvasGroup>();
    }
    public void OnPushPanel(string panelTypeString) {
        // 將string轉換成UIPanelType
        UIPanelType panelType = (UIPanelType)
            System.Enum.Parse(typeof(UIPanelType), panelTypeString);
        UIManager.Instance.PushPanel(panelType);
    }
    public override void OnPause() {
        // 該面板再也不和鼠標進行交互
        canvasGroup.blocksRaycasts = false;
    }
    public override void OnResume() {
        canvasGroup.blocksRaycasts = true;
        canvasGroup.alpha = 1;
}}
public class TaskPanel : BasePanel {
    private CanvasGroup canvasGroup;
    private void Start() {
        if (canvasGroup == null) {
            canvasGroup = GetComponent<CanvasGroup>();
    }}
    public void OnClosePanel() {
        UIManager.Instance.PopPanel();
    }
    public override void OnEnter() {
        if (canvasGroup == null) {
            canvasGroup = GetComponent<CanvasGroup>();
        }
        canvasGroup.alpha = 1;
        canvasGroup.blocksRaycasts = true;
    }
    public override void OnExit() {
        canvasGroup.alpha = 0;
        canvasGroup.blocksRaycasts = false;
}}

總結:UI框架的優勢:
  後期,當UI框架開發完,只須要對新面板進行override上述生命週期的四個事件方法便可。

任務23:其餘剩下的面板之間的跳轉

Panel之間的跳轉:
  MainMenu -> Knapsack; Knapsack close
    Knapsack -> ItemInfo; ItemInfo close
  MainMenu -> Skill; Skill close
  MainMenu -> Shop; Shop close
  MainMenu -> Setting; Setting close

代碼實現:

在Knapsack/ ItemInfo/ Skill/ Shop/ Setting 中實現
  Start() // 判斷並賦值canvasGroup
  OnClosePanel() // 調用UIManager的PopPanel()便可
  OnEnter() // 判斷canvasGroup是否爲空,爲空就賦值,以後設置alpha和blocksRaycasts
  OnExit() // 設置canvasGroup的alpha和blocksRaycasts

  註冊關閉按鈕

Knapsack <-> ItemInfo
  實現Knapsack子物體ItemSlot->Item的按鈕點擊註冊
    OnItemButtonClick() {
      UIManager.Instance.PushPanel(UIPanelType.ItemInfoPanel);
    }
  如今,能夠實現物品信息面板的開啓和關閉了
  可是,開啓物品信息面板時,與Knapsack仍是能夠交互的,點擊knapsack的關閉按鈕,會關閉物品信息面板
    由於Stack的存儲機制
  實現Knapsack的OnPause() -- 暫停交互 // canvasGroup.blocksRaycasts = false;
  實現Knapsack的OnResume() -- 從新開始交互 // canvasGroup.blocksRaycasts = true;

任務24:給面板切換添加動畫DoTween

在OnEnter()和OnExit()時作一些動畫處理

導入插件DoTween
  素材中有Pro版,AssetStore中能夠下載到普通版

使用DoTween製做動畫
http://dotween.demigiant.com/getstarted.php
  以KnapsackPanel爲例 -- 平移動畫
    引入命名空間 using DG.Tweening;

在OnEnter()的最後,實現DoTween動畫
  // Tweener Transform.DOLocalMove(float endValue, float duration, [bool snapping = false])
  Vector3 temp = transform.localPosition;
  temp.x = -Screen.width *3/4;  // 屏幕外面 (自己寬度大概在屏幕的一半)
  transform.localPosition = temp;
  // 目標x位置爲0,用時0.5f
  transform.DOLocalMoveX(0, 0.5f);

相同的,在OnExit()最開始
  transform.DOLocalMoveX(Screen.width *3/4, 0.5f);
  這時,就不須要改變alpha了

  但,若是想要改變alpha呢?
  直接放在下面,會致使剛開始進行DOLocalMoveX的時候就已經設置好了Alpha

  -- OnComplete()
  transform.DOLocalMoveX(..., ...).OnComplete(()=>canvasGroup.alpha =  0);
  這樣,在進行完動畫時,就會回調設置alpha值

以TaskPanel爲例 -- 透明度動畫
  canvasGroup.DoFade(float endvalue, float duration);

  在OnEnter()最後
    先把alpha設置爲0,而後執行動畫
    canvasGroup.DoFade(1, 0.5f);

  在OnExit()最後
    canvasGroup.DoFade(0, 0.5f);

以ItemInfoPanel爲例 -- 縮放動畫
  transform.DOScale(float endValue, float duration);

  在OnEnter()最後
    先把scale設置爲0
    transform.localScale = Vector3.zero;
    實現縮放
    transform.DOScale(1, 0.5f);

  在OnExit()最後
    transform.DOScale(0, 0.5f).OnComplete(() => canvasGroup.alpha = 0);

總結:

相關文章
相關標籤/搜索