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