2013年3月17日日曜日

Missing Scriptsエラーの解消(後編)

前回記事に続いて、Missing Scripts エラーをエディタで自動的に解消するスクリプトの解説だ。

関連記事


Missing Scriptsエラーの解消(前編)
http://gamesonytablet.blogspot.com/2013/03/missing-scripts.html

ではいつもどおり、Unity Gemsからの翻訳をどうぞ!

----------
http://unitygems.com/lateral1/
November 14, 2012

データを利用可能なスクリプトのフィールドと比較


ここからが難所だ。振り返ると、Missing Script が出たゲームオブジェクトで GetComponent を行うと null が戻る。とすると null しか戻らないものに対してどうやってプロパティの一覧を取得するのか、ということが課題となる。

さらに問題なのが、Unity 自身が C++ バックエンドの中でスクリプトの取得およびエディタ割り当てを内部で実行していて、Editor クラスでも手が届かないのでアクセス権もないことだ。

これで私はかなり悩まされた(実は始める前からこの辺りは相当問題になるだろうことは理解していたが、まあ話をわかり易くするため、その辺りの前後関係は省略する)。

事実関係を整理すると:

  • Missing Script で GetComponent を行うと null が戻る
  • シリアライズされたデータは、それに対する参照がないとアクセスする方法はない
  • インスペクタではどのスクリプトか分からなくともプロパティの一覧を表示する
  • 明白なことは、最後のポイントがこの問題の鍵だということだ。C# で利用可能なものは、そのデータストリームに対するアクセスが必要だ。さらに、それはインスペクタで表示される Editor でなければならない

このため、やるべきことはそのシステムに適切に統合して、Editor の中で Missing Object  を検知できるようにすべきだ。つまり MonoBehaviour 自身のカスタム エディタを記述してみよう。

必然的に、これを行うためにはデータストリームをインスペクタ上の serializedObject で受け渡しをしないといけない。これができれば一歩前に進める。次にやるべきことはMonoBehaviourが Missing Script かどうかの判定だ。このために取得したオブジェクトの SerializedProperties をみる必要がある。

さて、次はデバッグの出番だ。データストリームには何が含まれているか、確認が必要だ。

SerializedObjectの値を見ていくには GetIterator() をコールする。すると SerializedProperty が戻るので、さらに Next または NextVisible をコールして、値の間を遷移していく。これら二つのコールは共に boolean 型の引数を指定してデータのドリルダウンが出来る。最初のコールでは true でないといけないが、逆にその後のコールでは false でないといけない。
var property = serializedObject.GetIterator():
var first = true;
while(property.NextVisible(first))
{
     first = false;
     Debug.Log(property.name);
}
シリアライズ (インスペクタで表示) されたデータの各メンバーに対して Debug.Log を使用することで、現在 null 格納されている m_Script 変数に格納されていることが分かる。そうすると、ストリームの残りはデフォルトのインスペクタが描画しているパブリックのプロパティを保有している。これで解決策は完了で、データストリームはアクセスが可能だ。

後やるべきことは、これらの変数を候補となるスクリプトで整理して、全部が m_Script とマッチングするかチェックして、それさえ出来れば変数としてセット出来る。
// シリアライズ プロパティのスクリプトのコピーを作成
// して、後に参照
var script = iterator.Copy();
// 全てのスクリプトの一覧を取得
var candidates = scripts.ToList();
// 残りの引数をチェック
// して、マッチングするものがあるか確認
while(iterator.NextVisible(false) && candidates.Count>0)
{
    // 候補のスクリプトを現在のプロパティを保有する
    // サブセットに設定
    candidates = candidates.Where(c=>c.properties.ContainsKey(iterator.name)).ToList();
}
// もし候補が一つだけの場合は
// それを使用
if(candidates.Count==1)
{
    // スクリプトの参照をセット
    script.objectReferenceValue = candidates[0].script;
    // データストリームを更新
    serializedObject.ApplyModifiedProperties();
    serializedObject.UpdateIfDirtyOrScript();
}
// もし複数マッチングした場合は
// ユーザに選択肢を提供
else if(candidates.Count > 0)
{
    foreach(var candidate in candidates)
    {
        if(GUILayout.Button("Use " + candidate.script.name))
        {
            // スクリプト設定を編集
            script.objectReferenceValue = candidate.script;

            serializedObject.ApplyModifiedProperties();
            serializedObject.UpdateIfDirtyOrScript();
        }
    }
}
// それ以外はエラーメッセージ表示
else
{

    GUILayout.Label("> 適切なスクリプトは見つかりませんでした");
}

既存の壊れたオブジェクトを修正して正しいスクリプトを参照させる


前述のスクリプトでこれを確認することが出来る。m_Script の SerializeProperty である、objectReferenceValue を新しいスクリプトの値と同じにして、親の SerializeObject で ApplyModifiedProperties() をコールする。

Bodging に関するテクニック


さて賢明な方はすでにお気付きと思うが、欠点として、スクリプトを一つづつしか修正出来ない。どうやって全てのスクリプトをチェックして、一つづつ選択せずに直せるのだろうか。

その解決策が Bodging だ。二段論法でいうと:


  • 実際のデータ上でアクセスできる場所はインスペクタ
  • インスペクタはエディタ上で選択しないと修正できない


このためエディタをうまく騙して Missing Objects を各々選択させて、前述のコード実行を待機して、次のスクリプトに進む処理が必要だ。

これは Update 関数で工夫を凝らす必要があり、インスペクタ上でコードを追加して、どのコードが作業完了か認識させる必要がある。
        if(GUILayout.Button("Fix Now", GUILayout.Width(80)))
{
    FixMissingScripts.tried = false;
    EditorPrefs.SetBool("Fix", true);
    processList.AddRange(
        Resources.FindObjectsOfTypeAll(typeof(GameObject)).Cast().Where(c=>c.GetComponents().Any(o=>o==null))
        );
}

fix ボタンをクリックすると壊れたスクリプトは processList に移動され、カスタムエディタの Update 関数で処理される。ここではインスペクタのコードで作業済みの static 変数に false を渡していることに注目して欲しい。

次に Update で行うことは
if(!trying)
{
    if(processList.Count > 0)
    {
        FixMissingScripts.tried = false;
        var first = processList[0];
        FixMissingScripts.tryThisObject = first;
        processList.RemoveAt(0);
        Selection.activeObject = first;
        if(processList.Count==0)
        {
            nextTime = 10;
        }
        Repaint();
        trying = true;

    }
}
if(trying && FixMissingScripts.tried)
    trying = false;
何か作業中かチェックして、そうでなければ次をキューに入れて、完了時にインスペクタに作業済みの変数を更新するように指示する。

もし作業済みならば、現在のスクリプトの処理が完了したか確認をして、完了していれば次のアイテムへと進む。

結論


Bodging は有効に活用できる。これは時に水平思考の一種ぎ必要だ。どうすれば特定のデータストリームを取得出来るだろうか。しばらく画面を眺めたうえでアハ体験に遭遇するようなことご必要で、インスペクタで取得出来るならばインスペクタ自身になるためにはどうすれば良いだろうか。残りは残念ながら Bodging により、最初の回避策を上手に活用して 100 個長のスクリプトでも正しい動作をさせる必要がある。

このプログラムは次のリンク先からダウンロード出来る。

プログラムをダウンロード


メニューから Window > Fix Missing Scripts とすることで修正用のウィンドウが開く。
--------

やったぜ!これでMissing Scriptsの解消が出来る!と安心するのは早計。

実際使ってみた感想としては:

  • 進捗状況が分からない
  • Missing がスクリプト以外の場合はみつけてくれない

など課題はある。考えようによっては上記スクリプトをカスタマイズすればそのあたりは解消できそうだし、ダメ元でチャレンジというぐらいのスタンスが良いのかも。

エディタプログラミングも勉強すると便利だぜ!

0 件のコメント:

コメントを投稿

ブックマークに追加

このエントリーをはてなブックマークに追加

自己紹介

自分の写真
Unity3D公式マニュアル翻訳やってる人がスマホ(iPhone, Android)のゲーム開発しています。気軽に面白く初心者が遊べる内容がモットー。Blogでは開発情報をひたすら、Twitterではゲーム作成の過程で参考にしている情報を中心につぶやきます

ページビューの合計

過去7日間の人気投稿