基于NGUI的unity界面管理的討論

寫在前面

剛剛做的項目,由于界面管理做的不太好,所以在開發(fā)的過程中出現(xiàn)了很多奇怪或難纏的bug,搞得我們幾個寫UI邏輯的越寫越覺得沒意思,想方設(shè)法的到處打補丁,后來也就是在這樣的情況下,一直在總結(jié)開發(fā)中關(guān)于界面上遇到的坑,寫了一年多的UI邏輯,針對那些由于界面架構(gòu)上導(dǎo)致的問題,自己琢磨了一個簡易的UI框架,只是簡單的跑了一下沒什么問題。

好了正式開始吧。

關(guān)于界面的問題(我開發(fā)時遇到的)

這個可是太多,總結(jié)了幾個重要的點
1、界面的管理
2、界面生命周期
3、界面的顯示和隱藏
4、界面邏輯的管理
5、邏輯代碼和view分離
6、界面之間傳值問題
7、界面穿插和界面層級管理
8、引用關(guān)系
9、腳本該不該掛在gameobject上

那么下面我就圍繞以上幾點寫了。

界面管理

界面的資源全部都是打在AssetBundle中,然后通過底層函數(shù)把prefab load起來,給它掛上一個腳本,這個腳本就包含著該界面的邏輯,有一個WindowManager來管理這些window,每個window之間有父子引用關(guān)系,在WindowManager中還維護了一個棧來管理,每次界面打開或關(guān)閉都與該界面的父或子有關(guān)系。

例如,當(dāng)打開一個新界面時,會把父界面的gameobject傳進去,把界面顯示出來,把父界面隱藏,關(guān)閉界面的時候,把當(dāng)前界面隱藏,父界面顯示,這樣會出現(xiàn)一個問題,當(dāng)兩個界面同時在最上面時,當(dāng)它們無論關(guān)閉時都會下面的界面顯示出來,有時候就會出現(xiàn)穿插。

正常情況下,在同一時刻應(yīng)該只允許一個界面是可操作的。

又是維持父子關(guān)系,一方面又用棧來保存,這樣讓我真的不知道應(yīng)該怎么獲取父界面,因為有可能在界面中父的引用不是棧里面的“父”。

界面生命周期

界面的生命周期可說是個比較重要的問題,提醒一下?。?!
千萬不要把兩個不同的生命周期順序?qū)懺谝黄?,如果真要寫一起,請一定一定注意它們之間的順序。

自己的界面生命周期函數(shù)的調(diào)用時機一定要很清楚。

說說我們項目,掛在界面上的那個腳本里面就存在兩套生命周期函數(shù),一個是Mono的那一套,另一個是底層框架維護的一套。這東西當(dāng)開始的時候沒什么問題,越往后寫越改就發(fā)現(xiàn)很多時候的bug,都是由于生命周期順序造成的。例如:NGUI里面很多東西都是在Start做的,所以只要用NGUI,所有設(shè)置界面顯示都最好是在Start之后去調(diào)用,不然可能會出現(xiàn)ScrollView的Item錯位的情況。

我們界面幾個狀態(tài),可見、可操作、不可見。轉(zhuǎn)圈的進度條也被用界面來管理了,所以當(dāng)時每次轉(zhuǎn)圈完了之后,就會調(diào)用一次“可操作”的周期函數(shù),有時候遇到斷線重連,就會不停的轉(zhuǎn)圈,當(dāng)然也會不停的調(diào)用函數(shù)。

界面的顯示和隱藏

有很多種方法
1、gameObject.SetActive(true or false)
2、把界面移到UI攝像機外面
3、改變界面的Layer到UI相機不照的層
4、設(shè)置為透明
5、用不透明的背景遮擋
6、每個界面都放在不同位置上,這樣移動UI相機到相應(yīng)界面也實現(xiàn)顯示隱藏了。
7、也可采用多相機的方式

其中1、4兩種方法對于NGUI并不好,因為那樣操作會導(dǎo)致panel的所有“頂點重建”,重新生成drawcall。這也是NGUI消耗性能的地方,過段時間我會整理一下對NGUI的分析。
其中5,要看具體需求(自己腦補)
其中2、3、6、7都是可取的,但具體細(xì)節(jié)還得認(rèn)真考慮,我用了改變Layer的方式。

界面邏輯的管理

我們直接在上掛了一個腳本,剛開始做unity的時候,把界面的邏輯全部寫在這個腳本里面。一般簡單界面還好,但遇到復(fù)雜界面就完蛋了,有時候這一個腳本就得上千行,可讀寫性很差,過一段時間修改原代碼很費勁,而且很多邏輯狀態(tài)放在一起非常容易出現(xiàn)bug,有一段時間bug特別多。

遇到了一個狀態(tài)非常多的界面,腳本里面放了很多狀態(tài)變量,有些變量是互斥的,有些可以共存的,然后就這樣沒有規(guī)劃的寫了,結(jié)果這個界面很亂,都不敢做太大改動,出了bug改好了又引發(fā)其他的bug。所以后來就用有限狀態(tài)機來管理這些,把每個狀態(tài)和狀態(tài)對應(yīng)的邏輯拆分,這樣每個腳本行數(shù)變少了,邏輯得到很大的改善,后來改bug都不費腦子了,呵呵。(后來在知乎上看到一個人說用行為樹。。。后面再嘗試吧)

邏輯代碼和view分離

為什么?
1、當(dāng)業(yè)務(wù)代碼越復(fù)雜時,修改代碼就成了費腦的事情。
2、當(dāng)時間越來越久,理解代碼就非常困難。
3、同一個邏輯不能復(fù)用,在很多地方復(fù)制粘貼,如果出現(xiàn)錯誤就會修改很多地方。
4、測試變得非常麻煩,沒都要整體測一次才能確保一切完好。

怎么做?
使用MVC或MVP等架構(gòu)模式,使代碼達(dá)到低耦合、高復(fù)用、易測試、好維護、易擴展。

記得剛剛學(xué)習(xí)網(wǎng)站開發(fā)的時候,MVC是首先接觸到的設(shè)計思想,應(yīng)該滾瓜爛熟的東西。有一段時間我研究了一下MVC,發(fā)現(xiàn)和之前的認(rèn)識不一樣,比如View需要觀察Model,MVC實際是UI框架的一種模式,可并不是整個系統(tǒng)。下面就看看那些模式:

MVC
是一種使用Model View Controller設(shè)計創(chuàng)建web應(yīng)用程序的程序。它強制性的使應(yīng)用程序的輸入、處理和輸出分開。使用MVC應(yīng)用程序被分成三個核心部件:模型、視圖、控制器。它們各自處理自己的任務(wù)。最典型的MVC就是jsp + servlet + javabean的模式。

Model - 表示應(yīng)用的程序的核心,提供數(shù)據(jù)和數(shù)據(jù)相關(guān)的邏輯,通知View數(shù)據(jù)變化
View - 顯示數(shù)據(jù),觀察Model變化,可以從Model取得數(shù)據(jù)進行顯示
Controller - 處理輸入,調(diào)用model處理業(yè)務(wù)邏輯,邏輯處理完之后,修改Model,并選擇View顯示結(jié)果
注意:這里所說的是經(jīng)典MVC模式,后來發(fā)展了很多版本,它們之間無非就是這三者關(guān)系的變化,具體可以看看相關(guān)的文章和論文。

clipboard.png

MVP
它是從MVC演變而來,其中Presenter處理業(yè)務(wù)邏輯,Model提供數(shù)據(jù)和數(shù)據(jù)的邏輯,View負(fù)責(zé)顯示。
作為一種新模式,和MVC的重大區(qū)別就是在MVP中View不直接使用Model,它們之間通過Presenter來進行的,所有交互發(fā)生在Presenter內(nèi)部,Presenter代替了Controller的角色,在處理業(yè)務(wù)邏輯的基礎(chǔ)上還要負(fù)責(zé)幫View從Model中取數(shù)據(jù)。而在MVC中,View會直接從Model中讀取數(shù)據(jù)。

clipboard.png

MVVM
對MVVM不了解,也沒有使用過,看了一些網(wǎng)上的文章,最重要的概念應(yīng)該就是:數(shù)據(jù)綁定。把Presenter換成了ViewModel,換湯不換藥,最終發(fā)生改變就是三者之間的關(guān)系和三者所負(fù)責(zé)的事情。了解更多就去網(wǎng)上搜一搜。

以上對一些模式的簡介,總結(jié)起來,雖然有這些模式的存在,但需求是萬變的,沒有哪個模式能適用于一切情況,所以一切都要以實際項目、實際需求為主,吸收那些模式的思想,應(yīng)用于各個開發(fā)場景。一句話就是,怎樣讓開發(fā)簡單、代碼好看、易于維護就怎么做嘍。

界面之間傳值問題

不管是使用哪種開發(fā)模式。在實際開發(fā)中應(yīng)該都會遇到一個問題,對于界面管理,界面之間的傳值是一個重要的問題。
在Android中,兩個Activity之間傳值使用了一個叫Intent的組件,Activity持有Intent的引用。
在unity開發(fā)中,需要注意傳值的時機,在界面邏輯腳本中用成員變量保存該值。

界面穿插和界面層級管理

影響渲染順序的因素:

clipboard.png

在NGUI中,panel之間的層級,weight之間的層級都是用depth屬性控制的。雖然有以上幾個方面都可以控制渲染順序,但還是建議使用depth吧,畢竟這是NGUI提供的最正規(guī)的方式。

注意,panel和weight的depth是不交叉的,先是panel和panel深度排序,然后再是同一個panel下的weight進行深度排序。而且即使panel在hierarchy視圖中有層次關(guān)系,也不會影響depth的排序。

當(dāng)然關(guān)于層級關(guān)系還有一個重要的方面:3D模型和粒子特效的裁剪問題,有些游戲有這樣的需求,比如在界面上顯示一個英雄的模型,有些界面需要在模型上面,有些則在模型下面。我現(xiàn)在的做法是用多個相機,一個界面對應(yīng)一個相機,模型相機也是分開,利用相機的depth達(dá)到效果。

引用關(guān)系

取決于具體開發(fā)的框架了,建議使用MVC或MVP,各個層次的引用關(guān)系就是這些模式所描述的,能使代碼結(jié)構(gòu)清晰,減少bug的出現(xiàn),利于后期維護。

腳本該不該掛在gameobject上

關(guān)于這個問題就看項目的框架了,有些框架是把界面的腳本直接掛在gameObject上,有些則是通過腳本內(nèi)持有g(shù)ameObject引用關(guān)聯(lián)的。

經(jīng)過上面的討論,已經(jīng)把遇到過關(guān)于界面比較重要一些地方了解了,然后自己寫了一個簡單的UI框架。

在Unity開發(fā)中,客戶端UI框架的腳本有兩種方式:

1、如果每個界面都有單獨處理業(yè)務(wù)邏輯的腳本掛在自己身上,這種是通過Unity自身來驅(qū)動界面,把兩個生命周期放在一個腳本中。

首先需要知道,寫邏輯的腳本不能靜態(tài)綁定的,因為網(wǎng)絡(luò)游戲都需要資源熱更新,所以我們要把幾乎所有的美術(shù)資源打成AssetBundle的形式(這是Unity美術(shù)資源的一種存在形式),unity中資源結(jié)構(gòu)的組織及管理通過.meta文件完成的,unity會為工程中每個文件和文件夾創(chuàng)建一個.meta文件,里面記錄著一個GUID,每個電腦生成的GUID不一樣,而且資源只要變化了就會重新生成GUID,在開發(fā)時要不停往這些腳本中寫代碼,腳本變化對應(yīng)的GUID也會變化,這會導(dǎo)致已經(jīng)打好的AssetBundle里通過記錄的GUID找不到掛的腳本,也就是腳本丟失。

那么邏輯腳本也就只能動態(tài)的掛上去了:
TestScript test = gameObject.AddComponent<TestScript>();
test.SetParams(param); //傳值
test.Init(); //初始化
這段代碼是很多時候是這樣的,但需要注意,此時的TestScript只執(zhí)行了Awake,還沒有執(zhí)行Start就調(diào)用了初始化,如果界面是NGUI的,那么NGUI很多初始化工作都在Start中完成,也就是說UI本身都還沒有初始化完成,就開始執(zhí)行顯示邏輯了,這是不對的。所以Init里面不能寫讓UI顯示數(shù)據(jù)的代碼,只能寫在TestScript 的Start中,這樣才能保證所有UI控件已經(jīng)初始化完成了。

2、如果整個框架是有某個腳本來驅(qū)動的,也就是界面的邏輯不直接掛在gameObject上的,而是通過代碼中存在的引用關(guān)聯(lián)的,這樣腳本中沒有mono相關(guān)的生命周期,只有自己底層維護的周期了,所有腳本都完全自己把控。但還是得注意,自己的周期也一定要合理,NGUI中一定要保證UI全部初始化完成了才能執(zhí)行顯示邏輯。

UI框架部分

整體的類圖

clipboard.png

我直接在gameObject上掛腳本,但是掛的一個通用的腳本:Window,這個類繼承自MonoBehaviour,用來驅(qū)動我的邏輯。

Window.cs

using System;
using System.Collections.Generic;
using UnityEngine;

public class Window : MonoBehaviour
{
    private IPresenter _presenter = null;
    private bool _isStart = false;

    void Start()
    {
        _isStart = true;
        gameObject.layer = UnityLayer.ShowUILayer;
        _presenter.OnStart();
        this.Show();
    }

    void OnDestroy()
    {
        _presenter.OnDestroy();
    }

    public void AddPresenter(IPresenter presenter)
    {
        this._presenter = presenter;
    }

    public void Show()
    {
        if (_isStart)
        {
            _presenter.OnEnter();
        }
    }

    public void Hide()
    {
        _presenter.OnLeave();
    }

    public void OnStop()
    {
        _presenter.OnStop();
    }

    //重用界面時調(diào)用

    public void ReStart(IIntent intent)
    {
        _presenter.SetIntent(intent);
        _presenter.OnStart();
        this.Show();
    }
}

IPresenter是定義的處理界面邏輯的接口

public interface IPresenter
{
    void OnStart();
    void OnEnter();
    void OnLeave();
    void OnStop();
    void OnDestroy();

    void BindView(GameObject go); //這就是綁定gameObject到邏輯
    void SetIntent(IIntent intent); //傳遞界面參數(shù)
}

IView是定義的界面接口

public interface IView
{
    void Init(GameObject view); //在Presenter中會把傳遞的界面gameObject綁定到View上,Presenter持有View的引用,而不直接持有g(shù)ameObject
}

IIntent是參數(shù)傳遞的接口

public interface IIntent { }

結(jié)構(gòu)可以理解為一個界面對一個IPresenter,對應(yīng)一個IView。IPresenter中負(fù)責(zé)業(yè)務(wù)邏輯、設(shè)置界面,IView中負(fù)責(zé)寫界面設(shè)置函數(shù)和事件監(jiān)聽,這樣把UI和邏輯分開了。

接著看看實現(xiàn)IPresenter的一個基礎(chǔ)類:Presenter<T>,它接受一個泛型,用來把IView和它聯(lián)系起來,并實現(xiàn)了一些函數(shù)。

using System;
using System.Collections.Generic;
using UnityEngine;

public abstract class Presenter<T> : IPresenter where T : IView
{
    protected FSM _fsm = null;
    protected IIntent _intent = null;
    protected T _view = default(T);

    public void SetIntent(IIntent intent)
    {
        this._intent = intent;
    }

    //每次壓棧都會調(diào)用
    public abstract void OnEnter();
    //{
    //    //_view.Show();
    //}

    //每次退棧都會調(diào)用
    public abstract void OnLeave();
    //{
    //    //_view.Hide();
    //}

    //在mono start和時調(diào)用
    public virtual void OnStart() { }

    public virtual void OnStop() { }

    public virtual void OnDestroy() { }

    public void BindView(GameObject view)
    {
        _view = Activator.CreateInstance<T>();
        _view.Init(view);
    }
}

當(dāng)然IView也有基本實現(xiàn):View

public abstract class View : IView
{
    protected GameObject _view = null;

    public virtual void Init(GameObject view)
    {
        this._view = view;
    }

    public void Show()
    {
        _view.layer = UnityLayer.ShowUILayer;
    }

    public void Hide()
    {
        _view.layer = UnityLayer.HideUILayer;
    }
}

其中UnityLayer是定義的通過UnityEditor創(chuàng)建的Layer,之前也說過,我是通過改變layer來顯示和隱藏界面的。

public class UnityLayer
{
    public const int HideUILayer = 8;
    public const int ShowUILayer = 5;
}

還有一個類負(fù)責(zé)管理界面:WindowManager,它維護了一個棧的結(jié)構(gòu)(雖然我是用List裝的),每次打開界面的時候 - 進棧,每次關(guān)閉界面的時候 - 出棧。

界面IPresenter的生命周期:

clipboard.png

WindowManager 對外提供兩個函數(shù),一個打開一個關(guān)閉,并且還對無用的界面做了緩存,限制cache容器的大小,并用一個定時器定期去檢查cache,超過限制就把前面的釋放掉,滿足先進先出的規(guī)則。

public class WindowManager
{
    private List<Window> win = new List<Window>();
    private List<Window> cache = new List<Window>();

    private static WindowManager ins = null;

    private WindowManager()
    {
        //運行檢查緩存的定時器
    }

    public static WindowManager GetInstance()
    {
        if (ins == null)
        {
            ins = new WindowManager();
        }

        return ins;
    }

    public void OpenWin(string name, IIntent intent)
    {
        List<Window>.Enumerator etor = cache.GetEnumerator();
        Window old = null;
        while (etor.MoveNext())
        {
            if (etor.Current.gameObject.name.Equals(name))
            {
                old = etor.Current;
            }
        }

        if (old != null)
        {
            cache.Remove(old);
            win.Add(old);
            //手動調(diào)用,表示重用
            old.ReStart(intent);
        }
        else
        {
            //為了簡單,所以這里就直接使用Resources加載了
            UnityEngine.Object obj = Resources.Load(name);
            GameObject go = GameObject.Instantiate(obj) as GameObject;

            //通過配置,關(guān)聯(lián)界面和Presenter
            Type type = PresenterCfg.pconfig[name];
            IPresenter p = Activator.CreateInstance(type) as IPresenter;

            Window w = go.AddComponent<Window>();
            w.AddPresenter(p);

            if (win.Count > 0)
            {
                win[win.Count - 1].Hide();
            }
            win.Add(w);

            p.SetIntent(intent);
            p.BindView(go);
        }
    }

    public void CloseWin(GameObject go)
    {
        int i = 0;
        for (i = 0; i < win.Count; ++i)
        {
            if (win[i].gameObject == go)
            {
                //把當(dāng)前最上面的窗口hide
                win[win.Count - 1].Hide();
                break;
            }
        }

        //沒有找到相應(yīng)的窗口
        if (i >= win.Count)
        {
            return;
        }

        for (int j = win.Count - 1; j >= i; --j)
        {
            win[j].OnStop();
            //緩存界面
            cache.Add(win[j]);
        }

        //彈出棧之后,需要銷毀資源
        win.RemoveRange(i, win.Count);

        if (win.Count > 0)
        {
            win[win.Count - 1].Show();
        }
    }

    //檢查并清理緩存
    private void _Examine()
    {
        if(cache.Count > 0)
        {
            //先進先出
            Window w = cache[0];
            cache.Remove(w);

            //釋放資源
        }
    }
}

至此,一個簡單的界面框架就完成了,那么在開發(fā)的時候只需要寫一個Presenter和一個View:

public class ViewPresenter : Presenter<MainView>
{
    public override void OnStart()
    {
        //listen click
        Debug.Log("view presenter start");

        //get model data
        //set view data

    }

    public override void OnEnter()
    {
        Debug.Log("view presenter enter");

        _view.Show();

        //set attr
        //set icon
        //set name
        //set level
        //set quality
        //set ...
    }

    public override void OnLeave()
    {
        Debug.Log("view presenter leave");
        _view.Hide();
    }

    public override void OnStop()
    {
        Debug.Log("view presenter stop");
        //unlisten
    }

    public override void OnDestroy() { Debug.Log("view presenter destroy"); }

    //some
}
public class MainView : View
{
    //private event EventHandler Clicked;

    public override void Init(GameObject view)
    {
        base.Init(view);

        //UISprite sp = _view.transform.Find("").GetComponent<UISprite>();
        //UILabel label = _view.transform.Find("").GetComponent<UILabel>();
        //Transform test = _view.transform.Find("");
    }
}

總結(jié):

在做項目的時候就一直琢磨,要自己寫一個UI框架,不然對不起自己寫了這么久界面。最近終于完成了第一版,里面還存在很多問題,比如多個界面的層次關(guān)系怎么管理、有兩處代碼使用了反射可以想辦法改進,當(dāng)然還有沒有考慮到的問題,所以后續(xù)還要陸續(xù)修改。

寫在最后:

花了一周時間整理了這些東西,整理自己的思路,這次一定印象深刻,可能寫的不太好,有什么問題請直接指出,一起討論,不斷總結(jié),不斷學(xué)習(xí),不斷提升。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 原文: iOS應(yīng)用架構(gòu)談 view層的組織和調(diào)用方案 iOS應(yīng)用架構(gòu)談 開篇 iOS應(yīng)用架構(gòu)談 網(wǎng)絡(luò)層設(shè)計方案 i...
    難卻卻閱讀 1,386評論 0 7
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,160評論 25 708
  • 前言 看了下上篇博客的發(fā)表時間到這篇博客,竟然過了11個月,罪過,罪過。這一年時間也是夠折騰的,年初離職跳槽到鵝廠...
    西木柚子閱讀 21,425評論 12 183
  • 踏入社會以后,C小姐開始習(xí)慣別人把她當(dāng)做大人來看。沒人會因為她初來乍到就遷就她,她學(xué)著為自己說的每句話、做的每件事...
    栗子的月亮船閱讀 346評論 0 0
  • 最近總想著怎么能跳出溫水一樣的生活,畢竟我不是一只青蛙。最近開始跳出生活審視自己,還是學(xué)不會少說一些心里的話,所以...
    TTTTERRY閱讀 155評論 0 0

友情鏈接更多精彩內(nèi)容