SpriteStudio / SS5PlayerForUnity

OPTPiX SpriteStudio 5 Player for Unity
http://www.webtech.co.jp/spritestudio/
MIT License
39 stars 15 forks source link

v1.2.8 マテリアルが開放されない #195

Open sagat64 opened 7 years ago

sagat64 commented 7 years ago

お世話になります。

Instantiate した SS5 オブジェクトを Destroy しても、 表示に使われたマテリアルが開放されません。

全てのマテリアルを明示的に破棄する方法をご教示いただけないでしょうか。

古いバージョンで恐縮ですが、 1.2.8 を使っています(データの再コンバートがむずかしいため)。

よろしくお願いします。

sagat64 commented 7 years ago

<こちらで試したこと>

・Script_SpriteStudio_PartsRoot クラスの TableMaterial を個別に開放を試みました。  →Unity の以下のエラーで許可されませんでした。  "Destroying assets is not permitted to avoid data loss.    If you really want to remove an asset use DestroyImmediate (theObject, true);"

・Unity 5.4.2 を使用しています。

sagat64 commented 7 years ago

SS5PlayerForUnity は、 マテリアルをどのような方法で開放する設計になっているか ご教示いただけるとありがたいです。

MasamiYitsuse commented 7 years ago

sagat64 様

お世話になっております。

どのような目的で描画に使用したマテリアルの解放を(明示的に)行おうとしておりますでしょうか?

といいますのも、Unityの場合(そしてC#やJavaなどの言語的な側面でも)、原則として 「使用していたメモリやリソース(ここでは一旦、以降「データ」と呼称します)などは、使われていないと認識された時点で自動的に解放される」 という前提があります。 ※データの自動解放方法には複数の手法がありますが、Unityでは主に「ガベージコレクト」という方法を使って自動解放が行われています。

この自動解放は……非常に乱暴ではありますが、大枠は下記のような感じの動作になっています。 ・一定の条件が整った時点(例えば、一定回数以上newが行われた時点……など)で、自動的に、現在メモリ上に存在しているデータをチェックしはじめる。 ・使用しているデータへの参照を保持している処理がない場合、「使用されていない」と判断される。 ・「使用されていない」データが解放される。

そのため、Unityで明示的な解放を行わなくてはならないものというのは、非常に限られたものとなります(例えばアセットバンドルやシーンそのもの・動的に作成したメッシュの頂点など……です)。 ※そして、そういったものの場合、Unityだと「Destroy」という名前以外の、明示的な解放関数が用意されています(例えば「Unload」「Clear」といった名前の関数など)。

それらに加え、(特にUnity5系が顕著ですが)マルチスレッドレンダリングなどの関係もあり、 プログラムを組んでいる人が「必要がなくなった」と思っても、実はUnityのシステムなどがそのデータを使用している場合も少なくありません。

また、

"Destroying assets is not permitted to avoid data loss.    If you really want to remove an asset use DestroyImmediate (theObject, true);"

このエラーが出る場合の多くの原因としては、明示的に消してはならないデータ(例えばシーンの上にinstantiateされているオブジェクトの元になっているPrefab)や他の処理がまだ使用している可能性が高いデータなどを消そうとしているため「データ(下手をするとプロジェクト)が壊れてしまうかもしれないけど良いのでしょうか?」と警告を出してきていることが多いです。 それでも「一応DestroyImmediateを使用すれば消せますよ」とアドバイスしてくれてはいるのですが……DestroyImmediateで消してしまった場合、どのようなことになるか保障できません。 消した後の結果が「解っている」場合以外では、DestroyImmediateを使用することも、あまり推奨できません。 ※現実的には、DestroyImmediateを使用しなくてはならない状況というのも存在はするのですが、その大半は「やむなく使用せざるをえない」場合です。

念のため……Destroy関数とDestroyImmediate関数の違いですが、名前の通りDestroyImmediate関数は「とにかくその場で消す」ことをします。消した結果何が起こるかわからない(大半の場合は良くないことが起こります)けれど、(他の処理で使っているかどうかはお構いなしに)とにかく消してしまいます。 一方Destroy関数は「そのデータの使用状態を考慮して、できるだけ安全なタイミングで消そう」とします。

かつ、上で書いております「ガベージコレクト」がありますため、Destroyしなくても、Unityが「使用していない」と判断したデータなどについては、順次解放されていきます。

ですので、C++などでゲームを作ったりする際に行うような、 「new → (使用しおわったら)delete」 というような明示的解放を行う必要性は殆どありません(Unityだけでなく、C#にもnewはあってもdeleteがないことからも、C#そのものが自動解放を前提とした言語であることがお分かりいただけるかと思います)。

こと、Materialについては明示的な解放が必要になるシチュエーションが、(私には)思いつきません。 ※むしろ、経験上、無理矢理やってもほぼ無駄なのと、余程危険な状態になる場合が殆どです。

ただ、上記の通り「やむなく」行わなくてはならない場合が存在していることも事実ではありますので……何かしらの特殊な・もしくは限定的なご事情で、明示的に解放する必要が生じていらっしゃいましたら、そのご事情を頂ければ、何か手法のご提案をできるかもしれません。

お忙しい中お手数とは存じますが、何卒ご検討とご確認下さいますようお願い致します。


付記: 上記で「DestroyImmediateを使用しなければならない場合」と書いている場合も、結構な割合で 「明示的にガベージコレクタ(ガベージコレクトをする処理)を実行してやる」 ことで収拾できたりすることも少なくありませんことを追記させていただきます。

sagat64 commented 7 years ago

お返事ありがとうございます。

こと、Materialについては明示的な解放が必要になるシチュエーションが、(私には)思いつきません。

シーンを移動しても、SS5 のマテリアルがメモリから消えなくて困っております。

具体的には、 関数は Application.LoadLevelAsync()でシーンを移動、 Resources.UnloadUnusedAssets() を実行しています。

※メモリの確認方法ですが、 Android 実機でアプリを実行して、 PC 上の UnityEditor の Profiler で確認しています。

Material の参照が残っているのを疑ったのですが、見つかっていません。

なので、明示的に開放したい、と考えています。

「ガベージコレクト」がありますため、Destroyしなくても、Unityが「使用していない」と判断した オブジェクトやリソースなどについては、順次解放されていきます。

SS5PU の仕様は、 「Material の開放は、特に何もせず、ガベージコレクトされるのを待つ。」 ということでしょうか?

MasamiYitsuse commented 7 years ago

sagat64 様

お世話になっております。 ご返信ありがとうございます。

いただいたコメント順と前後しますが……

SS5PU の仕様は、 「Material の開放は、特に何もせず、ガベージコレクトされるのを待つ。」 ということでしょうか?

端的にはその仕様になります。 正確には、「Unityのデータ群の解放タイミングに任せる」となります(必ずしもUnityが各種データ群の解放をガベージコレクトのみで行っているとは限らないためです)。 また、「SS5PUの仕様」というわけではなく、大半のアセットの仕様が同じ形であるかと思われます。 ※前の当方からのコメントにあります通り、各種データの解放タイミングは、Unity本体側での利用の都合もあるため、アセット側の都合だけでリリースしてよいものでもないためです(また、シーン上にinstantiateしたObjectを消去する場合など以外で、特定のデータ等をDestroyしなくてはならないシチュエーションというのは、レアといえばレアケースかと思われます)。


シーンを移動しても、SS5 のマテリアルがメモリから消えなくて困っております。

まず、Ver.1.2.0から現状までで、Materialの解放不全については、症状としてもレポートでも、初見になります。

その上で、コメントいただいた情報の範囲で、解放不全が起こる可能性がある部分に言及していきたいと思います。

まず、SS5PUの1.2.x系(及び1.3系・1.4系)の基本的な構造では、(記憶にある限りで)マテリアルの解放を阻害するような参照構造はなかった記憶がございます。 Script_SpriteStudio_Root.csのTableBasedMaterialに定義されているマテリアル群を参照しているとすると、 ・描画マネージャの最適化処理 ・画面マネージャに付いている、MeshRenderer の2つが考えられますが、これらについては、1回分の画面描画が終了してしまうと、参照がされなくなるはずなので、長期間メモリ内から消えないということはないと思います(自動的にガベージコレクタが走ったら削除されるはずです)。

LoadLevelAsyncでシーンを変更していらっしゃるとのことで、その際に、UnliadUnusedAssetsなどでリソースをアンロードするタイミングによっては、一部のアセットが「そのタイミングでは」解放しきれない場合があります。 理由としては、 LoadLevelAsyncは非同期処理(LoadLevelAsyncが実行されたそのタイミングで、シーンの切り替えが行われるわけではなく、主な処理は遅延して行われる)であることが理由です。 LoadLevelAsyncの時に、乱暴に言うと、大体下記の順序で処理がすすみます。

  1. 元のシーン
  2. LoadLevelAsyncを実行
  3. 新しいシーンを読み込む
  4. 新しいシーンの初期起動を行う
  5. (新しいシーンの初期起動が終った時点で)古いシーンが捨てられる
  6. 新しいシーンに完全に移行する

この3~5の部分で、古いシーンと新しいシーンのデータは、メモリ中に同時に存在していて、かつ、一部のデータ群は利用中になっている可能性があります。 ここで肝になるのが、2~5の間で「古いシーンは動き続けている」ということです。 そのため、5のタイミング以前にUnloadUnusedAssetsを実行しても、古いシーンで使用中のデータ群については「使用中である」とみなされて、「そのタイミングでは解放されない」場合があります(その後、どこかのタイミングで「使用していない」とみなされた時に自動解放されます)。


もう一つ、よくやってしまうデータ群の自動解放の阻害なのですが…… 常駐シーンや、常駐のプログラム部分でそのデータを参照していた場合には、そのデータは「使用状態である」と判断されて、解放されません。 常駐シーンについては、わかりやすいのですが、常駐のプログラム部分については、私も忘れていて見落とす場合があります。 例えば、 public static class AAA { public static Material bbb; }; とあった時に AAA::bbb = Material001; / Material001は何かのマテリアルのインスタンス / などとしていると、このマテリアルは使用状態であるとみなされてしまいます。

これがさらに複雑になってくると、見落とす場合が少なくなくなってきます。 public static class AAA { public static Script_SpriteStudio_Root ccc; }; AAA::ccc = Root01; / Root01は何かのScript_SpriteStudio_Rootのインスタンス / などとなっている場合、一見すると問題なさそうな気もするのですが……Root01で使っている各種データ(その中には当然Materialもあります)も、「使用状態である」とみなされてしまいます。 (Root1は参照されているのだから、Root01の中から参照しているものも使用状態だよね……とガベージコレクタから判断されます)

static変数やクラスは、「シーンに関わらず、アプリケーションが実行されている間、常にメモリの中にある」という点が見落としがちなところになります(しかもUnityの場合、staticクラスなどはMonoBehaviourを継承した形では作らないので、インスペクタなどが存在していないため、見つけ辛い……という側面もあります)。

アプリケーションで、SS5PUのマテリアルテーブルをプログラム側で作成しなおしていたり・一時的に動的に作ったマテリアルを割り当てていたりとかするような処理を行っている場合に、元々設定されていたマテリアルを保存していたりして・その保存変数をクリアしわすれていたりすると、ずっとそのマテリアルが参照状態になってしまう……などが考えられます。


以上が、現在パッと思いつく要因となります。

若干抽象的に過ぎる部分もあって申し訳ございませんが、何卒ご検討等お願い致します。

sagat64 commented 7 years ago

MasamiYitsuse さま

丁寧なご回答、ありがとうございます!

あれからいろいろ試したところ、 Script_SpriteStudio_PartsRoot.cs の以下の箇所をコメントアウトするとリークが解消されました。

void Awake()
{
    //var root = gameObject.GetComponent<Script_SpriteStudio_PartsRoot>();
    //foreach(var MaterialNow in root.TableMaterial)
    //{
    //  MaterialNow.shader = Shader.Find(MaterialNow.shader.name);
    //}

    Status = ~BitStatus.MASK_INITIAL;
}

なぜこのコメントアウトでリークしなく生るのか、きちんと理解できていないのですが、 現象としては間違いなく解消されましたので、取り急ぎご報告いたします。

ありがとうございました!

MasamiYitsuse commented 7 years ago

sagat64様

お世話になっております。

ご検証いただきありがとうございます。

当該処理は、実行状態によっては、マテリアルに設定されているシェーダが失われることがあるためについている復旧処理(#176)なのですが、本処理でのリークが発見されたのは初見となります。 こちらでも当該事象の詳細検証を行い、今後のバージョンアップの際の参考にさせていただきたく思います。

重ねてありがとうございます。