Page 1 of 1

NodeGraph等のInstantiateの負荷について

Posted: 2020/05/12 02:53
by tonkotu
いつもお世話になっております。
非常に便利に使わせていただいております。

早速、件名の内容についてですが、
主にReflectionによるFieldを使ってシリアライズをしている箇所になるのでが、
例として NodeGraph.cs の以下のメソッドについて
```
void ISerializationCallbackReceiver.OnAfterDeserialize()
{
RegisterNodes();
}

void RegisterNodes()
{
ClearNodes();

EachField<Node>.Find(this, this.GetType(), (node) =>
{
RegisterNode(node);
});
}
```
これらの処理が、Instantiate時に高負荷となっておりかなりのボトルネックとなっております。
どうにかReflectionを使用しないように手を加えられないか、コードをトレースもしてみましたが、
方法が浮かばず、フォーラムにて相談させていただいた次第です。

私ごとではありますが、プロジェクトの終盤を迎え、取り急ぎ解決したい問題でもありまして、
こちらどうにか回避する策などがあればご教授いただけないでしょうか?
よろしくお願いいたします。

Re: NodeGraph等のInstantiateの負荷について

Posted: 2020/05/12 04:10
by caitsithware
ご要望ありがとうございます。

NodeGraph生成時における負荷についてですね。

ObjectPoolによる事前生成

まず、ゲーム途中に生成が頻発し高負荷になってしまうのが問題の場合、ObjectPoolingを使用して必要な数を予め生成しておきオブジェクトを使いまわす方法があります。

主に以下の流れでObjectPoolingを使用します。
  1. AdvancedPoolingでGameObjectを必要数プールする。
    生成には若干時間がかかるため、生成完了まで待つReadyリンクより遷移する。
  2. InstantiateGameObjectのUse Poolにチェックを入れて生成する。
  3. 不要になったらDestroyGameObjectで破棄する(この時プール可能であればプールへ戻される)
注意点としては、再利用された場合にコンポーネントの各フィールドやプロパティは以前のままとなっています。
もし残ってしまうと困る場合は、削除前に初期値に戻すなどの処理も行ってください。
※自動的に初期値に戻す対応を行うとなるとこちらもReflectionを使用せざるを得なくなりObjectPoolingによる負荷軽減の意味がなくなってしまうため、各自対応をお願いいたします。

自作するスクリプトでもArbor内のObjectPooling機能を使用する場合は、ObjectPooling名前空間に各種クラスがありますので参考にしてみてください。

RegisterNodesを各グラフでReflectionを使用せずに行う

ArborFSMInternalBehaviourTreeInternalにNodeGraph.RegisterNodes()を移植し、各ノードリストを直接登録することでReflection使用を回避する方法です。

例えば以下のような移植の流れとなります。
  1. NodeGraphクラスにOnRegisterNodes()を追加(中身は不要)。

    Code: Select all

    protected virtual void OnRegisterNodes()
    {
    }
    
  2. NodeGraph.RegisterNodes()を以下のように変更。

    Code: Select all

    void RegisterNodes()
    {
    	ClearNodes();
    
    	foreach (var node in _Calculators)
    	{
    		RegisterNode(node);
    	}
    
    	foreach (var node in _Comments)
    	{
    		RegisterNode(node);
    	}
    
    	foreach (var node in _Groups)
    	{
    		RegisterNode(node);
    	}
    
    	for (int i = 0; i < _DataBranchRerouteNodes.count; i++)
    	{
    		RegisterNode(_DataBranchRerouteNodes[i]);
    	}
    
    	OnRegisterNodes();
    }
    
  3. ArborFSMInternalクラスにOnRegisterNodes()追加。

    Code: Select all

    protected override void OnRegisterNodes()
    {
    	foreach (var node in _States)
    	{
    		RegisterNode(node);
    	}
    
    	for (int i = 0; i < _StateLinkRerouteNodes.count; i++)
    	{
    		RegisterNode(_StateLinkRerouteNodes[i]);
    	}
    }
    
  4. BehaviourTreeInternalクラスにOnRegisterNodes()追加。

    Code: Select all

    protected override void OnRegisterNodes()
    {
    	RegisterNode(_RootNode);
    
    	for (int i = 0; i < _CompositeNodes.count; i++)
    	{
    		RegisterNode(_CompositeNodes[i]);
    	}
    
    	for (int i = 0; i < _ActionNodes.count; i++)
    	{
    		RegisterNode(_ActionNodes[i]);
    	}
    }
    
※もしforeachの使用も削減する方針でしたら、forへの置き換えはご自由に行ってください。

変更箇所が多岐にわたる点および対象ファイルがシステムの根幹の部分になるため、変更後のファイルの暫定的な配布は行っておりません。
上記移植の流れを参考に変更していただくようお願いいたします。

この変更は、今後の更新で対応する方向で検討いたします。

その他、Reflectionの使用について

今から全体的にReflectionの使用をほぼ禁止とすると、ユーザー様の作成するスクリプトにも変更が必要になってしまうような大幅な仕様変更となってしまうため、現状でReflectionの使用を削減するのは難しいです。
よって現状では全体的なReflection使用削減を行う予定はございません。
ご理解のほどよろしくお願いいたします。

Instantiate時の負荷について

そもそものUnityの仕様としてInstantiate自体も高負荷であるため、頻繁に呼び出すようなことがある場合は、ObjectPoolingを使用するようにお願いいたします。
とはいえ、Arbor内部の処理負荷も高くなっているのも事実であるため、可能な限り負荷軽減できないかも今後の課題として検討いたします。


取り急ぎ、まず全体としてObjectPoolingに対応できそうかやRegisterNodesの変更でどうなるかご検証ください。

以上となります。
ご不便おかけして申し訳ございませんがよろしくお願いいたします。

Re: NodeGraph等のInstantiateの負荷について

Posted: 2020/06/06 03:14
by tonkotu
お世話になっております。

FiniteStateMachineやBehaviourのリフレクションの件について、無事解決できました!
ありがとうございます。

同様にNodeBehaviour.csのシリアライズについても、似たような対応ができないか、検討しております
できる限りメモリ消費も抑えたいため、ObjectPoolingを使用しない、何か良い方法ありますでしょうか?

上記、よろしくお願いいたします

Re: NodeGraph等のInstantiateの負荷について

Posted: 2020/06/06 04:58
by caitsithware
ご確認ありがとうございます。

NodeGraph側のOnAfterDeserializeについては無事解決ということで良かったです。

NodeBehaviour側のOnAfterDeserializeについては、DataSlotを一括してリストアップしているのですが、以下のような理由によりリフレクションによるリストアップが最適であると考えております。
  • DataSlotは属性で型制限ができるものがありFieldInfo経由でその型を得る必要があるためリフレクションでFieldInfoを取るほかない。
  • DataSlotはユーザーが作成されたスクリプトでも宣言できるため、手動で登録する場合に、登録コードを記述する手間が発生。
  • 全て手動で書くとなると、ユーザースクリプトを作るハードル(学習コスト)が上がってしまう。
以上からDataSlot周りでのリフレクション使用を禁止するのは現システムの根幹から変える必要があるため、リフレクションを一切使用しないような対応は現状しない方針です。
DataSlotフィールドのリストアップ(検索)負荷そのものについては、事前にリストアップしておけないか等は今後の課題として検討していきたいと思います。

もし現状でNodeBehaviourのデシリアライズでDataSlot周りのリフレクション使用をなくしたい場合、DataSlot類、FlexibleField類、DataLink属性付きフィールド、それらを使用している組み込みスクリプト類を一切使用しないようにグラフを組んだうえで、NodeBehaviour.RebuildFieldsの呼び出し部(もしくはRebuildFieldsの中身そのもの)を削除してください。
ただしこの対応は非推奨でありサポート外となります。
行う場合は自己責任でお願いいたします。

メモリ消費等も考慮しての良い方法についてですが、tonkotu様のプロジェクトにおけるターゲットプラットフォームやスペック、何をInstantiateしているか等にもよるため、どうすれば良いかは一概には答えられません。
Unityの仕様上もとからInstantiateの負荷は高いものであり、シビアなゲーム中のInstantiate負荷軽減のためのObjectPooling実装でもあるため、こちらからはObjectPoolingを推奨するという形となっております。

以上です。
ご期待に沿えず申し訳ございませんがよろしくお願いいたします。