FairyGUI源码解读

  初识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()方法。

posted @ 2024-10-29 00:24  匹夫无名  阅读(527)  评论(0)    收藏  举报