FairyGUIの使用と最適化

FairyGUIは、クロスプラットフォームのゲームGUIソリューションです。エディターとプラットフォームSDKで構成されています。クロスプラットフォームエディターは、UIマテリアルの管理および編集機能を提供し、各プラットフォームのSDKと合わせ、各プラットフォームに最適化されたUIインターフェイスをすばやく簡単に構築できます。

具体的な紹介と指導については、FairyGUIの公式ウェブサイトをご覧ください:http://fairygui.com

 

私たちのチームは、2017年にCocos2d-XからUnityに移行する際に、FairyGUIに触れたことがあります。当時、チームのアーティストは3DとUnityの開発の経験がなく、プログラマの半分はUnityの学習段階にいます。NGUI、UGUIはまったく新しいものであり、UnityでUI編集するのは理想的ではありません。以前のCocos2d-x開発では、UI作成プロセスのかなりの部分がアーティストによって設計と描画され、コードレイヤーがプログラマによってブリコラージュされ実装される必要がありました。アーティストの確認と調整は、開発効率を制限しました。これは新しいプロジェクトで最適化して解決したい問題です「。

 

アーティストディレクターがFairyGUIの導入を提案した後、テストや評価を経て、このソリューションが私たちのチームに最適であることがわかりました。Unityの3D環境と比較して、アーティストはFlashに似た編集インターフェイスに慣れています。アーティストは、エディターでUIインターフェイスの設計とテスト作業のほとんど、特にアニメーション部分を完了することができます。基本的に、設計作業の70%は、プログラマの介入なしにWYSIWYGで完了できます。プログラマは、インターフェイスのブリコラージュとアニメーション作成するプロセスを節約し、ビジネスレイヤーの作成に集中できます。

 

1年半の開発と学習を経て、製品への適用に成功しました。もちろん、私たちもたくさんの落とし穴に遭遇し、いくつかの使用スキルも要約しました。参考になるように、ここでいくつかを整理しました。

 

一、ピクセルのテスト機能をクリック際の注意点

 

エディターで作成すると、任意のパッケージの画像を現在のコンポーネントのクリックテストのMaskとして使用でき、エディターでプレビューしても問題ありません。ただし、実行では、Maskの画像をコンポーネントの現在のパッケージに含める必要があります。Commonパッケージなどの別のパッケージに含まれている場合、関連する検出用データファイルはエクスポートされず、ピクセルのテスクをリックトしたら無効になります。

たとえば、fishItem01のような画像は一般的な背景として使用され、BgやCommonなどのパッケージに配置され、他のパッケージのアセットによって参照される場合があります。List DemoパッケージのList1コンポーネントでは、bg画像はBgパッケージのfishItem01画像をボタンの背景として参照し、同時に画像はピクセルテストクリックのMaskクとして使用されます。プレビューは正常に動作しますが、実行ではテスト機能は無効になります。

FairyGUIのソースコードを調べたところ、理由はわかります。

 

バージョン3.0より前のソースコード:

バージョン3.0以降のソースコード:

ここでは、コンポーネントの初期化時にpackageItem.ownerを介してテストアセットが取得されていることがわかります。このownerはUIPackageタイプであり、現在のコンポーネント(packageItem)が配置されているパッケージです。 hitTestIdの指定アセットが現在のパッケージに見つからない場合、hitAreaの設定は無視され、自然なピクセルクリックテストは無効になります。外部パッケージから対応するコンポーネントが配置されているパッケージにアセットをコピーした後、問題は解決されます。

 

次に、hitTestの原理とアセットの管理をさらに分析します。(以下の分析は、3.0より前のバージョンに基づいています。3.0以降は、エクスポートされたアセットのバイナリデータ化のみが実行されており、基本的な原則は同じです)

 

前のコードを観察すると、hitAreaはPixelHitTestタイプであり、データはUIPackage.GetPixelHitData関数から取得されます。この関数は_hitTestDatasメンバーを通し、ItemIdをKeyとしてデータを取得し、_hitTestDatas内のデータは次のようなコードで初期化されます。

システムが「hittest.bytes」ファイルをロードしていることがわかります。次に、ピクセルテストパッケージを使用してエクスポートした後、「packagename@hittest.bytes」という名前のファイルを見つけることができます。実際、外部パッケージのアセットを使用してピクセルテストMaskとする場合、このファイルはまったくエクスポートされません。当然、ピクセルテストは不可能になります。

 

このファイルを開くと、テキスト形式で記録されたバイナリデータが見つかります。解析はwhileループであるため、複数のhitTestアセットがファイルに収集されると判断できます。

ba.ReadString関数の分析を続けます。最初のushortは、hitAreaDataのid名の長さ(ここでは6)を格納し、次の6byteは「7337 7578 6a74」がidです。変換後のASCIIエンコーディングは「s7uxjt」です。また、id名をkeyとして使用して、テストデータを「_hitTestDatas」フォントで保存します。Itemのidのみがあり、パッケージ名はないため、現在のパッケージ内しかアセットを見つけることができません。

 

hitTestを使用するコンポーネントファイルを開くと、hitTestが内部の「n9_ockd」コンポーネントを参照していることがわかります。これは、image,src=“s7uxjt”です。

このコンポーネントが配置されているパッケージファイル「package.xml」を開き、「s7uxjt」を検索すると、このファイルが見つかります。ピクセルテストに使用される画像です。

「hittest.bytes」ファイルに戻って分析を続行します。PixelHitTestData.Load関数によると、次のintは空白で未使用であり、さらに次のintはピクセル幅です。これは、ビグエンドストレージであるため、”0xaa” = 170、scale = 1 / “0x02″で、画像全体の幅は340pxになり、合計pixel数は0x00000f86(3974)バイトであり、以下は画像エンコーディングから変換されたテストデータです。使用する画像は340X374で、合計127160ピクセルです。これまでのところ、画像の実際のピクセルはhitTestDataデータと一致していません。

さらなる分析および検出方法:

クリックされたローカル座標ポイントlocalPointは、テスト領域のオフセットとscaleのプロパティに従って元の画像の1/2領域にマッピングされます。これは、通常、実際のポイントツーポイントの精度検出は必要なく、十分であるためです。元の画像精度検出の半分を取得します。また、色の値があるかどうかを検出するだけで十分なので、1ビットで十分です。1バイトで8ポイントの情報を格納できます。したがって、pos-> pos2、pos3を計算すると、_dataでこのポイントのビットを見つけてヒット判定を返します。(他の境界チェックは説明しません)

 

PS:テスト画像は127160ピクセルです。マッピング後、4割ると31790テストポイントになります。ビット単位のエンコードは8割ると、3973.75になり、整数なら3974バイトになります。

 

二、TimeScaleのアニメーション再生問題

 

FairyGUIバージョン2.4.0を使用する前に、アニメーションの再生は正常ですが。バージョン3.0.0にアップグレードした後、アニメーションの再生が不完全であり、最初の部分がまったく再生されなかったことがわかりました。チュートリアルを見た後、以下のようなこと発見しました。

アニメーションのignoreEngineTimeScaleをfalseに設定しようとすると、正常に再生されます。

 

次に、ignoreEngineTimeScale関連のコードをトレースして、関連問題を調査します。 Githubのコード更新を詳しく見ると、2018年8月15日からのコミットが見つかりました:

この変更により、dtつまりTime.unscaledDeltaTimeの上限設定が削除され、dtが0.1f秒を超える可能性があります。そこで、ここに時間をプリントしてみると、ゲーム開始時の値が1秒以上になり非常に高くなることがわかりました。したがって、後続のアニメーション再生の計算にこのdtを加えた後、この1秒をスキップするのと同じです。この変更は、最大dtが0.1fに強制されるため、0.1秒はスキップされますが、基本的に問題はありません。

 

コードの調査を続けると、ignoreEngineTimeScaleが8月2日に追加され、以前のバージョン2.3.1では外部DoTweenが使用され、2.4.0では内蔵のGTweenが使用始めました。したがって、2.4.0バージョンが影響を受け始めました。

 

そして、私の比較的長いTime.unscaledDeltaTime時間は、実際にはゲーム開始後にスクリプトを実行するStartです。通常の状況では、Startでの1回限りの初期化が多数発生すると、短期間のフリーズが発生します。また、多くの場合、Startの初期化が完了した後、エントリアニメーションが再生されるか、コントローラが設定され、コントローラが再生アニメーションを呼び出します。ただし、アニメーションが更新されると、値の大きいTime.unscaledDeltaTimeが読み込まれるため、この期間のアニメーションは「早送り」されます。Time.unscaledDeltaTimeは、2つのフレーム間の実際の消費値であり、Time.deltaTimeは、TimeManagerによって設定された範囲内で計算および制御されます。

 

いくつかの解決策:

1、FairyGUIソースコードのデフォルトのignoreEngineTimeScaleをfalseに直接変更します。 FairyGUIがデフォルトでTime.deltaTimeを使用してアニメーション時間を計算するようにします。

短所:ライブラリコードが変更されており、その後のバージョン更新でメンテナンスの問題が発生します。変更が作成者の設計アイデアに準拠していない可能性があり、他の潜在的なバグが発生する可能性があります。

2、特定のアニメーションのignoreEngineTimeScaleをfalseに設定します。

短所:この値はデフォルトでtrueに設定されています。多数のアニメーション効果が影響を受ける場合、変更するのは不便です。また、アニメーションがコントローラーによって呼び出される場合、変更はより面倒になります。

3、Startがアニメーションを実行すると、Play関数の導入が遅延になるため、アニメーションは1〜2フレーム遅延してから実行されます。

短所:コントローラーのデフォルト以外の状態に切り替える必要がある場合は、遅延が大きすぎるため、インターフェイスがデフォルト状態で複数のフレームを描画しやすくなり、画面が点滅します。

4、コルーチンまたはその他のプログラムロジックを使用して、アニメーションの再生またはコントローラーの切り替えをStartまたはその他の時間がかかる可能性のある関数から移動します。再生を遅延にします。

短所:コードロジックは複雑で、メンテンスが不便です。

5、アーティストは、FairyGUIエディターでがコントローラーによってアニメーションが再生されるところから、遅延再生を行います。

短所:遅延時間の制御が正確ではありません。3の欠陥を引き起こす可能性があります。

 

個人的には、UnityのTimeScaleを使用することでほとんどのプロジェクトに対応できると考えています。後続の改善を楽しみにしています。

 

三、リスト使用の最適化ケース

 

プロジェクトでは、次の図のようなItemリストを作成する必要があります。

2列の横列、Itemは上と下の順序で配置されます。また、Itemは斜めに表示する必要があります。斜めに表示する必要があるため、ピクセルクリックテストが必要です。そうしないと、4番目のItemの右上隅がクリックしたら、6番目のItemが応答する場合があります。

 

一般的な考え方に従い、最初のバージョンのItemを作りました。

実行後:

UIPanelのFairyBatchingが選択されている場合:

基本的に、半分のDrawCallが節約されました。引き続きOverDrawとFrameDebugを観察します。

FairyGUIは、左右のtitleと背景画像がオクルージョンされていると判断しますが、上下のオクルージョンがないため、バッチでレンダリングされ、3つのバッチを使用してそれぞれ背景図、iconとtitleをレンダリングします。現在までにレンダリングされたのはtitle文字で、次にレンダリングすべきなのは背景図です。材質が違ったのでバッチ処理することはできません。したがって、合計DrawCallは半分に減ります。

 

引き続き最適化手法を模索します。

元のlistを3つのlistに分割します。各listは、title、bg、iconの3つの部分に対応しています。listも3つに分割されて使用できます。list3のみがタッチ応答を受信し、list1とlist2はタッチ不可に設定され、コードを介してスクロール的に同期されます。

 

Titleセクション:

Iconセクション:

Bgセックション:

スクリプトを変更した後に実行します。

レイヤー化した後、合計3つの材質のDrawCallをマージできます。

 

階層化のため、コードを変更する必要があります。

その他の実装の詳細は、プロジェクトの実際のニーズに応じて微調整する必要があります。上記のコードをカスタムListクラスにカプセル化することも検討できます。

 

四、バッチについて

 

FairyGUIのバッチプロセスの簡単な説明は次のとおりです。この記事では、詳細について説明しません。興味のある方は自分でコードを読むことができます。

 

UIPanelのfairyBatchingメンバー変数を設定すると、UIコンテナー(CreateContrainer)の作成、とプロパティ変更の応用(ApplyModifiedProperties、エディターの場合)という2つの操作では、コンテナーのfairyBatchingプロパティに割り当てられます。プロパティのset操作内に “_fBatchingRequested”を設定し、さらにトラバースして、バッチ処理する必要のある子ノードがあることを親ノードに通知します。親ノードのバッチ属性もオンになっている場合は、「_ fBatchingRequested」フラグも再度設定されます。バッチ処理は、実行時にコンテナコンポーネントのfairyBatchingプロパティを直接設定することによってもアクティブ化できます。

 

更新操作を振り返ると、StageEngine.LateUpdate-> Stage.InternalUpdate-> Container.Updateを実行するコードを段階的に追跡します。Container.Updateに入った後に「_fBatching」がtrueの場合、コンテキストオブジェクトのbatchingDepth ++が子ノードコンポーネントで更新中に渡され、多くのコンポーネントがContaineから継承されます。したがって、SetRenderingOrder操作は、第1レベルの親コンテナでのみ実行され、次にUpdate呼び出しが行われる前batchingDepth—を行います。

 

Container.SetRenderingOrder関数で、以前に設定された「_fBatchingRequested」フラグがtrueの場合、バッチ処理DoFairyBatching()が実行されます。この関数では、最初に「_fBatchingRequested」フラグがfalseに設定されているため、各コンテナがバッチ属性を設定した後、バッチ操作は1回だけ実行されると判断できます。 「_fBatchingRequested」をtrueに設定した2つの関数(UpdateBatchingFlags、InvalidateBatchingState)をトレースし、次の条件がバッチ操作を再度トリガーする可能性があることを確認します。

 

コンテナノードのバッチ属性をtrueに設定する場合:

 

子ノードの追加、子ノードの削除、子ノードの順序の設定、子ノードの交換、子ノードのOrder変更およびその他のノード操作

表示するかどうかを設定する

ブレンドモードを設定する

ImageコンポーネントがTextureを更新するとき

MovieClipコンポーネントがアニメーションデータを設定するとき

GListコンポーネントがBoundsを更新するとき

GProgressBarが更新されたとき

GSliderが更新されたとき

アニメーションの停止、再生、更新の緩和

コンテナはクリッピングRectを設定する

コンテナはmaskを設定する

描画モードに入るとき、コンポーネントオブジェクトをRenderTextureに描画するとき

DoFairyBatching関数に戻りましょう。この関数は、_descendantsのリストを維持し、CollectChildren関数を再帰的に呼び出して子ノードを収集します。子ノードコンテナでも「_fBatchingRequested」がtrueに設定されている場合、子ノードコンテナのDoFairyBatchingがCollectChildren内で呼び出されます。子ノードを収集した後、2層ループは_descendantsリストをトラバースし、材質とboundsの情報に従って挿入ソートを実行します。ソートは、List のRemoveAtとInsert ofが呼び出され、Array.Copyが内部的に呼び出される場合があります。パフォーマンスの問題に注意してください。

 

ソート後、SetRenderingOrder関数に戻り、_descendantsリストを再度ループして、子ノード(DisplayObject)のrenderingOrderプロパティを順番に設定します。

 

DisplayObject.renderingOrderプロパティは、実際にはDisplayObject内のgraphicとpaintingGraphics2つのNGraphicsのsortingOrderに対応しています。

 

NGraphicsは、FairyGUIのレンダリング部分のコアであり、GameObject、MeshFilter、MeshRenderer、Shader、およびUnityシーンに対応するその他の情報を内部的に維持します。

 

また、NGraphics.sortingOrderを設定することは、MeshRenderer.sortingOrderを設定することです。これは、Unityでmeshのレンダリング順序を手動で調整する方法でもあります。

 

以上は、FairyGUIバッチ全体の一般的なプロセスです。全体として、アニメーション効果、GProgress、GSlider、およびペイントモードに注意してください。これらのモードは、フレームごとの更新に、連続的なバッチ操作を引き起こす可能性があります。

 

五、仮想リストの循環について

 

仮想リストを循環させることは、特にレベルの選択や武器の選択などを行うときに非常に便利です。しかし、注目に値するいくつかの詳細があります。

 

まず、Item数が10個のである循環仮想リストを作成します。

画面にItemのChild Index、ItemIndex、Hash値をそれぞれプリントします。Child Indexは、画面に実際に描画される子ノードのインデックスに対応します。デモでは、0〜5ですが、OverDrawを通して実際に6つのItemが描画されていることがわかります。これは、Listがトリミングされているため、5つしか表示されません。Item Indexは、仮想リスト内の実際のItem数に対応します。ここでは0〜9です。循環仮想リストのItemに対応するDisplayObjectオブジェクトが多重化されているためです。したがって、Hashによって追跡されます。

 

少し右に移動すると、Itemのインデックス情報が変更されました。

前のItemと比較すると、画面の左端にItemが追加されていることがわかります。このとき、画面の最初のItemのChildインデックスは1になり、他のItemは変更されていません。

 

リストを右にドラッグし続けると、Itemオブジェクト0x39EB990が画面の左端に再表示されるまでは、オブジェクトのChild Indexは1のままで、ItemIndexは3です。

右にドラッグするうちに、左端のItemが画面に完全に表示されると、左に別のItemが追加されることがわかります。このとき、Childをリセットする操作が発生し、すべてのChildの再描画をトリガーし、さらにItemRenderを呼び出します。関連する効率の問題もチュートリアルに記載されています。

 

まず、循環リストはまず仮想リストであり、Item情報は実際にはList内の_virtualItemsリストに格納されていることを理解する必要があります。リストの長さは、仮想リストのnumItems、循環リストのnumItems * 6と同じであり、増加するだけで減少しません。また、リストには、実際のGButtonのようなオブジェクトではなく、ItemInfoが保存されています。合計5つのItemがあるとすると、循環リストの場合、_virutalItemの長さは30になります。ただし、実際の描画に使用されるItemオブジェクトは10個ある可能性があり、それらが_virtualItemのどこに格納されているかは不明です。HandleScrollシリーズの関数はそれらを再配置します。実際には、_firstIndexから始まる_numItemsがあります。

 

selectedIndexプロパティのget操作を観察します。

循環仮想リストの場合、選択したものが_virtualItemsリストから検索され、Item数を取得されることがわかります。

そして、selectedIndexプロパティのset操作:

AddSelection関数が呼び出されます。Valueが直接渡されることに注意してください。つまり、選択する必要のあるItemIndexが渡されます。

AddSelection関数では、循環リストに対する判断はなく、着信ItemIndexを直接使用して、変換せずに_virtualItemsからItemInfoを取得し、情報に対応するobjボタンオブジェクトのselected属性を設定します。

実際、仮想リストは問題ないかもしれませんが、ループリストで選択したいオブジェクトがその位置にない可能性があり、ii.objが空の値である可能性があります。そのゆえ、選択したいボタンがselected属性を設定できなくなります。さらに、Itemの数がChildの数より少ない場合、画面上に同じItemIndexを持つ複数のオブジェクトがあります。では、selectedIndexによってどちらが選択されますか?

 

要約すると、リストをループするためにselectedIndexを使用しないでください。

 

六、DrawCallに影響を与えるTextの例

 

上記の例をとると、DrawCallについて引き続き議論することができます。

ボタンのtitleを「test」のような短い文字列に設定する場合:

DrawCallが2であることがわかります。

titleにもっと情報を書くとき:

DrawCallは7になります。 FrameDebugをチャックすると、Textを表示するために生成されたMesh頂点が300を超えるためです。

七、フィルターはDrawCallに影響する

 

背景図にフィルター効果を追加します。

バッチ処理が完全に消えました。

バッチ処理部分の以前の分析および追跡コードによると、すべての背景のMaterialが異なることがわかります。つまり、システムは各Imageに対し、Materialインスタンスを生成します。

 

調査を続けたところ、システムがフィルター付きの材質に「COLOR_FILTER」キーワードを設定していることがわかりました。システムは、設定された材質Keywordに従ってマテリアルを生成します。Material.GetMaterialManager関数には、

キーワード付きのマテリアルである限り、インスタンスが再生成されるため、バッチ処理を実行できません。これまでに知られている唯一のキーワードは「COLOR_FILTER」です。したがって、フィルターは注意して使用してください。


UWA公式サイト:https://jp.uwa4d.com

UWA公式ブログ:https://blog.jp.uwa4d.com

UWA公式Q&Aコミュニティ(中国語注意):https://answer.uwa4d.com