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中的实例化处对象Container,Container继承自DisplayObject,在Container的构造函数里调用了CreateGameObject(gameObjectName),从而给DisplayObject.gameObject赋上了值。
渲染逻辑
在State物体被创建的时候加上了StageEngine脚本,在StageEngine这个继承自UnityEngine.MonoBehaviour的类中,有LateUpdate()方法,调用了根目录下所有DisplayObject子节点的Update方法。在DisplayObject.Update()方法中,调用了NGraphics.Update()方法。

浙公网安备 33010602011771号