チリペヂィア

リンクフリー。サンプルコードなどは関連記事内でライセンスについて明示されない限り商用利用なども自由に行って構いませんが、自己責任でお願いします。またこれら日記内容の著作権自体は放棄していません。引用部分については引用元の権利に従ってください。

昨日のビルボードサンプルのエラー対応版

前回の最後にちょろっと書いてる「GameObject.hideFlags = DontSaveでの問題」について。

さらに追試。

Component.hideFlags

Component.hideFlags≠GameObject.hideFlagsなので、コンポーネントだけDontSaveにしてみました。でもやはりロード時に

Component MeshFilter could not be loaded when loading game object. Cleaning up!

なーんて赤警告で表示されてしまいますた。

トップレベルのGameObjectがDontSaveならまったく問題ない模様

試行錯誤の結果、シーンの直下(親ナシ)の時だけはちゃんとフラグチェックして保存対象から除外しているようでした。そもそもUnityのDontSaveフラグの説明を見てみると「んー?単にシーン越境するためだけのフラグ?」ともとれかねないのですが、この状況を見るに、親子ノードを辿って再帰的にシリアライズ保存する時はチェックが抜けているっぽい…*1

プラン1(オススメ!)

隠しオブジェクトは単独でシーンに配置する」つまりオモテのオブジェクトの子にしない。transformが再帰的に反映されないので、オモテのオブジェクトが操作されたらウラのオブジェクトには逐一手動で反映します。削除もしないとリークします。日記の最後に、この方法で修正した版を書いておきます。

プラン2

このHideFlags.DontSave causes "CheckConsistency: Transform child can't be loaded" (Unity Bug?) / UnityAnswersに書いてあるんですが、UnityEditor.AssetModificationProcessorを使ってシーン保存イベントを拾って、保存前に親子関係を切ったり削除してしまったりする手が考えられます。ただ、この方法で問題になったのが2点。

課題1.いつ絶縁状態や削除状態から復帰させるか

これはまぁ、Editorクラスを継承してCustomEditor属性をつけてあげれば、選択時にOnSceneGUIが飛んでくるようになります。例えば保存時に消しても、ヒエラルキで選択すればOnSceneGUIが実行されてそこでプレビューオブジェクトの復帰処理がコールされる、というShurikenライクな仕様とか。背景用だとイマイチなものの、エフェクト用のビルボードならじゅうぶんです。

なら解決?と思いきや。

課題2.親オブジェクトに隠し子をつなげると「シーン変更」としてバレてしまう!

ところが、親子関係の復帰処理としてhiddenChild.transform.parent = ownerObject;って書いちゃうと、「シーンの保存対象であるownerObjectが変更された!」って事になって、そのままUnityを閉じると「変更を保存しますか?」の画面が出ちゃうんですね。コレ解決できませんでした。先述した「選択で再表示」の仕様とすると「保存時にプレビューメッシュが消えちゃうのはともかくとして、クリックしてビルボードを再表示させただけで変更として検知される」というのはちょっとわかりにくい気がします。小さなシーンならともかく、大きなシーンで全選択した中にこういうコンポーネントが混ざっちゃってると、「アレ?俺なんか変更したっけ?」ってなりそう。

プラン2もなんか使い道がありそうなんですが、とりあえず今回はプラン1で解決します。

プラン1方式の修正版

Billboard.cs
using UnityEngine;

namespace BillboardSystem
{
    [ExecuteInEditMode()]
    public class Billboard : MonoBehaviour
    {
        /// <summary>レンダリング時に使用するマテリアル</summary>
        public Material material;

        private static System.String hiddenChildName = "BillboardHiddenRenderer";
        private GameObject renderObject;

        /// <summary>状態を隠しレンダオブジェクトにコピーします(位置とレイヤーを転写)。</summary>
        void CopyState()
        {
            if (renderObject != null)
            {
                Transform childTrans = renderObject.transform;
                childTrans.position = transform.position;
                childTrans.localScale = transform.lossyScale;
                childTrans.rotation = transform.rotation;
                renderObject.layer = gameObject.layer;
            }
        }

        void OnEnable()
        {
            if (renderObject == null)
            {
                renderObject = new GameObject(hiddenChildName);
                renderObject.hideFlags = HideFlags.HideAndDontSave;
                renderObject.AddComponent<BillboardRenderer>().ownerBillboard = this;
                CopyState();
            }
            else renderObject.SetActive(true);
        }
        
        void OnDisable()
        {
            if (renderObject != null) renderObject.SetActive(false);
        }
        
        void OnDestroy()
        {
            if (Application.isEditor) DestroyImmediate(renderObject); else Destroy(renderObject);
        }

        void Update() { CopyState(); }
    }
}

BillboardRenderer.cs は昨日の記事のままで大丈夫です。

*1:保存するGameObjectの子に非保存GameObjectがぶら下がってるってそんなに異常な状態でもないと思うんだけどなぁ…非保存で保存をスキップする時に子配列のインデックス番号がズレると、エンドプログラマスクリプトバグる可能性が微粒子レベルで存在?それならそれでMissing扱いにしちゃえば…うぅーむ。