• Siki_Unity_3-6_UI框架 (基于UGUI)


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

    任务1&2&3&4:介绍 && 创建工程

    UI框架:
      管理场景中所有UI面板
      控制面板之间的跳转

      如果没有UI框架,会通过面板之间的交叉访问来实现这些功能,管理混乱

    创建工程UIFrameWork:
      创建工程目录
        

      导入素材,将素材放入Images文件夹下
        将所有素材的Texture Type修改为Sprite(2D and UI)

    任务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,位于屏幕右下角,并设置Anchor

    新建Image: SourceImage 任务 TastButton
      添加Button组件
      新建Text:颜色黄白色,大小,字体,对齐等
        给Text添加Shadow组件
        取消Raycast Target的勾选

    在Project的Images文件夹下新建UI Prefab文件夹,将上述按钮制作成prefab

    通过上述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);

    总结:

     

     

     

  • 相关阅读:
    generator
    JS 中 apply 、call 、bind的详解
    前端面试题(24-js)
    JS原型链深入了解
    Java12新特性
    Java11-ZGC
    Java11新特性
    Java10新特性
    Java9新特性
    CF1385E【Directing Edges】 (拓扑排序)
  • 原文地址:https://www.cnblogs.com/FudgeBear/p/8950088.html
Copyright © 2020-2023  润新知