FairyGUI原始碼解讀

匹夫无名發表於2024-10-29

  初識FairyGUI是在公司的專案中,覺得用起來很舒服。最近在學習MotionFramework框架,想擴充其UI框架支援FairyGUI,所以小小地研究了一下FairyGUI。這裡寫一下自己的感悟。

一、用程式碼動態啟動FairyGUI

  編寫程式碼動態載入FairyGUI包,它將提供包內的元件和資源,之後再根據需求,建立出其中包含的元件,加入到FairyGUI在unity中建立的根節點下,即可顯示。

UIPackage.AddPackage("UI/Common");
GComponent winHome = UIPackage.CreateObject("Common", "main").asCom;
GRoot.inst.AddChild(winHome);

在呼叫GRoot.inst時,會自動建立Stage物體,GRoot物體,Stage Camera物體。並且切換場景時不會被銷燬。

public static GRoot inst
{
    get
    {
        if (_inst == null)
            Stage.Instantiate();

        return _inst;
    }
}

public static void Instantiate()
{
    if (_inst == null)
    {
        _inst = new Stage();
        GRoot._inst = new GRoot();
        GRoot._inst.ApplyContentScaleFactor();
        _inst.AddChild(GRoot._inst.displayObject);

        StageCamera.CheckMainCamera();
    }
}

public static void CheckMainCamera()
{
    if (GameObject.Find(Name) == null)
    {
        int layer = LayerMask.NameToLayer(LayerName);
        CreateCamera(Name, 1 << layer);
    }

    HitTestContext.cachedMainCamera = Camera.main;
}

public static Camera CreateCamera(string name, int cullingMask)
{
    GameObject cameraObject = new GameObject(name);
    Camera camera = cameraObject.AddComponent<Camera>();
    camera.depth = 1;
    camera.cullingMask = cullingMask;
    camera.clearFlags = CameraClearFlags.Depth;
    camera.orthographic = true;
    camera.orthographicSize = DefaultCameraSize;
    camera.nearClipPlane = -30;
    camera.farClipPlane = 30;
    camera.stereoTargetEye = StereoTargetEyeMask.None;
    camera.allowHDR = false;
    camera.allowMSAA = false;
    cameraObject.AddComponent<StageCamera>();
    return camera;
}

二、包管理

  主要類是UIPackage類,使用全域性的UIPackage.AddPackage()方法,可以載入fairy包內的所有專案。該方法有多個過載方法,基本的流程都是,載入FairyGUI匯出來的二進位制檔案,然後進行解析。解析過程是建立一個UIPackage類的物件,呼叫此物件私有的LoadPackage()方法,遍歷主資料檔案的內容,載入其中所有涉及到的資源。

對於全域性的UIPackage.AddPackage()方法,FairyGUI提供了多種傳參型別,根據傳參型別可以將這些方法分為以下3類:

  • 使用UnityEngine.AssetBundle包
  • 直接使用FairyGUI包名
  • 直接使用二進位制資料

使用UnityEngine.AssetBundle包

由FairyGUI匯出的檔案有兩類,.bytes資料檔案,資原始檔。打包的時候可以將所有檔案打成一個包,也可以將資料檔案打成一個包,資原始檔打成一個包。但這兩種方式都是走一個包載入邏輯。

// 從 assetbundle 新增 UI 包。
public static UIPackage AddPackage(AssetBundle bundle)
{
    return AddPackage(bundle, bundle, null);
}
// 從兩個 assetbundle 新增一個 UI 包。desc 和 res 可以相同。
public static UIPackage AddPackage(AssetBundle desc, AssetBundle res)
{
    return AddPackage(desc, res, null);
}

  資料包作為desc引數傳進來,資源包作為res引數傳進來,mainAssetName是資料檔名,是_fui.bytes檔案的字首名。如果desc引數和res引數相同,說明資料檔案和資原始檔打成了一個包,如果不相同,說明資料檔案和資原始檔打成了兩個包。在AddPackage方法中均會記錄下資源所在的AB包。
  mainAssetName如果為空,則會遍歷desc包內容去查詢它,查詢條件是查詢_fui結尾名字的檔案,此時如果所有的_fui.bytes檔案都打成了一個包,則mainAssetName最終會是desc包內的第一個_fui檔案,所以一般不會是空的。

public static UIPackage AddPackage(AssetBundle desc, AssetBundle res, string mainAssetName)
{
    byte[] source = null;
    if (!string.IsNullOrEmpty(mainAssetName))
    {
        TextAsset ta = desc.LoadAsset<TextAsset>(mainAssetName);
        if (ta != null)
            source = ta.bytes;
    }
    else
    {
        string[] names = desc.GetAllAssetNames();
        string searchPattern = "_fui";
        foreach (string n in names)
        {
            if (n.IndexOf(searchPattern) != -1)
            {
                TextAsset ta = desc.LoadAsset<TextAsset>(n);
                if (ta != null)
                {
                    source = ta.bytes;
                    mainAssetName = Path.GetFileNameWithoutExtension(n);
                    break;
                }
            }
        }
    }

    if (source == null)
        throw new Exception("FairyGUI: no package found in this bundle.");

    if (unloadBundleByFGUI && desc != res)
        desc.Unload(true);

    ByteBuffer buffer = new ByteBuffer(source);

    UIPackage pkg = new UIPackage();
    pkg._resBundle = res;
    pkg._fromBundle = true;
    int pos = mainAssetName.IndexOf("_fui");
    if (pos != -1)
        mainAssetName = mainAssetName.Substring(0, pos);
    if (!pkg.LoadPackage(buffer, mainAssetName))
        return null;

    _packageInstById[pkg.id] = pkg;
    _packageInstByName[pkg.name] = pkg;
    _packageList.Add(pkg);

    return pkg;
}

直接使用FairyGUI包名

直接使用包名,最終呼叫的是AddPackage(string assetPath, LoadResource loadFunc)方法。loadFunc委託方法主要是用來載入AB包,然後從AB包中載入資源返回TextAsset 類物件的。
這裡首先判斷了一下所載入的包是否已在列表中,存在則直接返回;否則就呼叫loadFunc委託函式,獲得一個TextAsset 類物件,這個類是UnityAPI中自帶的,表示文字檔案資源,儲存的內容有.bytes格式內容以及.txt格式的文字內容。然後建立一個UIPackage類物件,呼叫此物件的方法LoadPackage(buffer, assetPath),該方法的主要功能是用來解析二進位制檔案的、讀取FairyGUI包的資料,將其解析為記憶體中的物件,這些物件包括影像、動畫、字型、元件、圖集、聲音、骨骼動畫等,可以在FairyGUI中使用。

/// <summary>
/// 使用自定義的載入方式載入一個包。
/// </summary>
/// <param name="assetPath">包資源路徑。</param>
/// <param name="loadFunc">載入函式</param>
/// <returns></returns>
public static UIPackage AddPackage(string assetPath, LoadResource loadFunc)
{
    if (_packageInstById.ContainsKey(assetPath))
        return _packageInstById[assetPath];

    DestroyMethod dm;
    TextAsset asset = (TextAsset)loadFunc(assetPath + "_fui", ".bytes", typeof(TextAsset), out dm);
    if (asset == null)
    {
        if (Application.isPlaying)
            throw new Exception("FairyGUI: Cannot load ui package in '" + assetPath + "'");
        else
            Debug.LogWarning("FairyGUI: Cannot load ui package in '" + assetPath + "'");
    }

    ByteBuffer buffer = new ByteBuffer(asset.bytes);

    UIPackage pkg = new UIPackage();
    pkg._loadFunc = loadFunc;
    pkg._assetPath = assetPath;
    if (!pkg.LoadPackage(buffer, assetPath))//用來解析UI的二進位制檔案資訊
        return null;

    _packageInstById[pkg.id] = pkg;
    _packageInstByName[pkg.name] = pkg;
    _packageInstById[assetPath] = pkg;
    _packageList.Add(pkg);
    return pkg;
}

直接使用二進位制資料

這個不在贅述,和前面兩種方法後面的載入邏輯一樣。


三、建立FairyGUI元件物件

  在包載入完後,即可建立FairyGUI元件的物件,建立FairyGUI元件的方法主要分為兩類,同步建立和非同步建立。
  使用FairyGUI生成的指令碼程式碼去建立元件,就是使用同步建立。其中pkgName是包名,resName是元件名或元件名。過程是,透過包名獲取包UIPackage,然後最終呼叫CreateObject(PackageItem item, System.Type userClass)方法即可建立。

public static UI_MainWindow CreateInstance()
{
    return (UI_MainWindow)UIPackage.CreateObject("WindowTest", "MainWindow");
}

public static GObject CreateObject(string pkgName, string resName)
{
    UIPackage pkg = GetByName(pkgName);
    if (pkg != null)
        return pkg.CreateObject(resName);
    else
        return null;
}

public GObject CreateObject(string resName)
{
    PackageItem pi;
    if (!_itemsByName.TryGetValue(resName, out pi))
    {
        Debug.LogError("FairyGUI: resource not found - " + resName + " in " + this.name);
        return null;
    }

    return CreateObject(pi, null);
}

GObject CreateObject(PackageItem item, System.Type userClass)
{
    Stats.LatestObjectCreation = 0;
    Stats.LatestGraphicsCreation = 0;

    GetItemAsset(item);

    GObject g = UIObjectFactory.NewObject(item, userClass);
    if (g == null)
        return null;

    _constructing++;
    g.ConstructFromResource();
    _constructing--;

    return g;
}

四、FairyGUI元件和Unity物體GameObject的關聯

  例如已經獲取到了一個按鈕元件m_btn,可以透過呼叫m_btn.displayObject.gameObject去獲取元件在場景中對應的GameObject。

m_btn.displayObject.gameObject.SetActive(true);

  FairyGUI.DisplayObject透過其CreateGameObject方法來建立UnityEngine.GameObject,並將這個物件放置到unity的DontDestroyOnLoad場景。FairyGUI.GObject中有個CreateDisplayObject虛方法,由它的子類去複寫這個虛方法,根據子類不同的需求去建立FairyGUI.DisplayObject,從而建立UnityEngine.GameObject。

override protected void CreateDisplayObject()
{
    rootContainer = new Container("GComponent");
    rootContainer.gOwner = this;
    rootContainer.onUpdate += OnUpdate;
    container = rootContainer;

    displayObject = rootContainer;
}

public Container(string gameObjectName)
    : base()
{
    CreateGameObject(gameObjectName);
    Init();
}

protected void CreateGameObject(string gameObjectName)
{
    gameObject = new GameObject(gameObjectName);
    cachedTransform = gameObject.transform;
    if (Application.isPlaying)
    {
        UnityEngine.Object.DontDestroyOnLoad(gameObject);

        DisplayObjectInfo info = gameObject.AddComponent<DisplayObjectInfo>();
        info.displayObject = this;
    }
    gameObject.hideFlags = DisplayObject.hideFlags;
    gameObject.SetActive(false);
}

  例如GComponent元件繼承自GObject,重寫了GObject的虛方法CreateDisplayObject,從而給GObject.displayObject賦上了值。而重寫的虛方法CreateDisplayObject中的例項化處物件ContainerContainer繼承自DisplayObject,在Container的建構函式里呼叫了CreateGameObject(gameObjectName),從而給DisplayObject.gameObject賦上了值。

渲染邏輯

  在State物體被建立的時候加上了StageEngine指令碼,在StageEngine這個繼承自UnityEngine.MonoBehaviour的類中,有LateUpdate()方法,呼叫了根目錄下所有DisplayObject子節點的Update方法。在DisplayObject.Update()方法中,呼叫了NGraphics.Update()方法。

相關文章