CanvasDirector

自動でSortingOrderを調整したい!

 各UIのSortingOrder設定を覚えていられない! そのUIの配下にあるParticleSystemや子CanvasのSortingOrder設定なんてもっと覚えていられない! ということでCanvasDirectorにUIを登録/削除すると各UIのSortingOrderを自動調整する機能を入れました。

 現在の自作プロジェクトではCamera.Depth・Canvas.SortingOrder・Hierarchyを使ってUIの前後関係を制御しています。UIの前後関係は以下の要素で決まります(Shaderは今回無視しています)。上の方が優先順位が高いです。

  • Canvas.RenderMode → UIは全部ScreenSpaceCameraモードにする予定(Overlayは使わない)
  • Camera.Depth → 使う
  • Canvas.SortingLayer → SortingOrderで頑張れば使わなくても良さそうなので何かのときのために取っておく
  • Canvas.SortingOrder → 使う
  • Hierarchy → 使う

 Hierarchyだけで制御するようにするとわかりやすいのですが、Canvasで適宜UIを分割した方がパフォーマンスが良くなるのでCanvasの設定を制御する必要がでてきました。

www.slideshare.net

 また、CameraのDepthはUI上で3Dオブジェクトを表示するために制御が必要です。3DオブジェクトはCanvasの制御を受けないので、表示の前後関係を操ろうとするならCameraの力を借りる必要があります。

SortingOrderの設定を覚えていられない

 通常UIは100、ヘッダーは200、ポップアップは300、ローディング表示は400と決めていくと、いずれ設定した箇所が多くなりすぎて覚えてられなくなります。設定をドキュメントに残しても、設定を増やしたときに更新されるとは限りません!

 また、UI内でParticleSystemを使っていると、このParticleSystemに設定してあるSortingOrderの管理も必要になります。何かの拍子にCanvasの設定を変更すると前後関係がメチャクチャになって地獄を見ます。

 さらに、そのParticleSystemよりも上にUIを表示したい、となるとUI内にさらにCanvas追加+OverrideSortingしてSortingOrderを設定する必要があり、手動で管理し切るのはなかなか難しい作業になります。

CanvasDirectorがSortingOrderを自動で管理するようにしよう

 Prefabに設定しておくと設定が散らばって収拾がつかなくなります。そこで、コード上に各UIの設定を記述して、その記述に従ってCanvasDirectorがSortingOrderを自動管理するようにしました。

 よくよく考えるとPrefabに持たせていた設定がコード側に来ただけなのですが、前後関係の変更・追加はかなり楽です。数値でなく列挙型で管理できるのもわかりやすさの向上につながっていると思います。例えば、現在のプロジェクトはFrontmost(エラー通知など最前面に出るUI)・Overlay(ローディング系)・Popup・Resident(ヘッダーなどの常駐UI)・Core(メインUI)・BackgroundというUI層を定義しています。

UI内のSortingOrderもある程度自動管理するようにしよう

qiita.com

のSortingOrderUpdaterの項に詳細を書きました。各UI・ParticleSystemのSortingOrderを相対値で設定しようというものです。UI内部のSortingOrderだけ気にすれば良いので、アプリ全体のSortingOrderを管理するよりかはだいぶマシになるはずです。

SceneをAssetBundle化する

github.com

 自己流SceneManagerの作成と同時にSceneのAssetBundle化に取り組みました。

AssetBundleからSceneを読み込みたい

 シーンもAssetBundleにすることが可能です。これによりアプリ本体に含まれるAssetを思いっきり減らすことができます。前回の自己流AssetBundleManagerはSceneを扱えなかったので、自己流SceneManagerの作成と同時に対応しました。

kema8351.hatenablog.com

ローカルモードとビルドを両立する

 問題となったのはローカルモードとビルドの両立です。ローカルモードとはAssetの変更をすぐに確認するためのモードです。変更のたびにAssetBundleをビルドするのは時間がかかるので、それを避けるためのモードを自己流AssetBundleManagerでは用意しました。

 この2つはBuildSettingで衝突します。ローカルモードを実現するためにはシーンをすべてBuildSettingに登録しておく必要があります。対して、ビルド時にはアセットバンドル化したシーンはBuildSettingから外す必要があります。BuildSettingに登録されているシーンはアプリに含まれてしまうからです。そのシーンからリンクされている画像や音声はすべてアプリに入ります。

 そこで両立させるために、通常時はすべてのシーンをBuildSettingに登録しておき、ビルド時だけアセットバンドル化したシーンを削除することにしました(※削除は必要ないことが判明。追記参照)。そのままですね。手動で実行するのは面倒くさいのでCIツールのサポートが必須です。

※2018-06-26 追記

ビルドスクリプトで含めるSceneを指定できるので削除する必要はありません。

docs.unity3d.com

自己流SceneManager

github.com

Sceneに渡す引数をキャストしたくない

 今回は自己流のSceneManagerを作りました。今回一番重視した点は「Sceneに渡す引数をキャストしない」ことです。

 例えば、キャラクター詳細シーンがあった場合、どのキャラクターについて表示するかはキャラクター詳細シーンの呼び出し側からキャラクターのIDを渡すと思います。これをint型で渡すの難しく、大抵はobject型にキャストして渡すことになると思います。難しい理由は次の2点です。

  • シーンの読み込みは非同期なので呼び出し時点では呼び出されるシーンは存在しない
  • 他のシーンの引数がint型とは限らない

 呼び出し時点でシーンが存在しないので、シーン引数は一時的に保存しておく必要があります。また、シーン引数はシーンによって変わるため、どんな型が来ても大丈夫なようにobject型で保存することになります。

 ただ自分はどうしてもobject型へのキャストを避けたいと思っていました。その理由は2つです。

  • ダウンキャストは危険なので行いたくない
  • 引数のヒントが無くなる

 渡される引数がobject型なのでどんな型でも来る可能性があるということです。 int型を要求するシーンでも絶対にint型をもらえるとは限りません。

 また、どのような引数を渡せばよいか、object型ではひと目ではわかりません。引数の型については受け取るシーンのソースコードを読む必要があります。シーンを作成した直後は覚えているでしょうが、1ヶ月後にもなればシーンを作った本人も忘れています。ドキュメントにして残しても、変更があった場合にドキュメントが変更される保証はありません。

Sceneに渡す引数をActionで保存する

 この問題の解決策としてAction<T>として保存することにしました。Tはシーンが読み込まれたときにSceneDirecotr(自己流SceneManagerの名前)にアクセスするクラスです。このクラスにはわかりやすいように○○SceneStarterという名前を付けるようにしました。

 例えばTitleシーンの読み込みには

sceneDirector.LoadFromBuiltinAsset<TitleSceneStarter>("Title", (TitleSceneStarter ss) => ss.SetArgument("User1"));

という形で呼び出し、(TitleSceneStarter ss) => ss.SetArgument("User1")の部分をActionとして保存します。保存先はSceneDirectorの中に

class LoadedActionCache<T>
{
    public static Queue<Action<T>> Queue { get; } = new Queue<Action<T>>();
}

というCacheクラスを作って保存します。Queueなのは複数回同時にScene読み込みが行われた場合を想定しています。

参考:http://engineering.grani.jp/entry/2017/07/28/145035

 Titleシーンが読み込まれたらTitleSceneStarter.Start()がSceneDirectorにアクセスして保存してあったActionを受け取り、自分を引数にして実行します。これでシーン引数の受け取りが完了します。

懸念点

 Actionをstaticな領域に保存していますが、これを初期化する方法を考える必要があります。エラーで「タイトルに戻る」という処理を実装する場合、このstaticな領域にActionが残ったまま再開されてしまう可能性があります。

DestroyされたComponentはinterface越しにnullチェックできない

 Componentをinterfaceを使って扱っていたら困った話。困った原因を端的に表すコードが以下のようなものです。

using System.Collections;
using UnityEngine;

public class Root : MonoBehaviour
{
    [SerializeField]
    Test test;

    IEnumerator Start()
    {
        ITest itf = test;
        Destroy(test.gameObject);
        yield return null;

        Debug.Log($"Component: {test} {test == null}");
        Debug.Log($"Interface: {itf} {itf == null}");
    }
}

public class Test : MonoBehaviour, ITest
{
}

public interface ITest { }

 これを実行すると

Component: null True
Interface: null False

 interface側は、ToStringするとnullと出るのに、==nullによるチェックではFalseと出る、という不思議な状況です。自分はこの現象により意図しない挙動になって困りました。「ComponentがDestroyされたらある処理を実行しよう」とそのComponentをInterface型変数に収めてnullチェックして待っていたのですが、いつまで経ってもTrueにならず空振りに終わりました。

 とりあえずの対策はinterfaceにnullチェック関数を持たせることです。

using UnityEngine;

public class Test : MonoBehaviour, ITest
{
    bool ITest.IsNullComponent => this == null;
}

public interface ITest
{
    bool IsNullComponent { get; }
}

 あまり上手い解決方法ではないので、もっと良い方法は無いかな、と思います。

Conditional Attribute

qiita.com

 プリプロセッサの代わりに使えるConditionalAttribute。「自動編集用のinterfaceをクラスに付けるたびに#if UNITY_EDITORを記述するのが面倒くさい・・・」と思っていたところで発見したので「これで勝つる!」と思ったのですが、そう甘くはなかったようで。interfaceには使えません。残念。

AtCoder ARC094 D問題

 解き方は正しかったのですが、long doubleの可能性に思い当たらなかったためにWAし続けたのでメモ。WAになったのは次の2つが原因です。

1. sqrt関数にdouble型の引数を渡した

 この問題では最大10の18乗の平方根を取る必要があります。平方根を算出する関数sqrtは浮動小数点を引数としています。この際、floatやdoubleで値を渡すと情報落ちが発生して正しい解が得られない場合があります。

 

qiita.com

 doubleの仮数部は52bitなので約4.5×10^15までしか正確に表現できないのが原因です。

倍精度浮動小数点数 - Wikipedia
 10^18を浮動小数点で正確に表現するにはlong doubleを用います。long doubleは仮数部が64bitなので約1.8×10^19まで正確に表現可能です。

qiita.com

2. cmathをincludeしていなかった または sqrtl関数を使用しなかった

 long doubleの平方根を得るためには次の2つの方法があります。

  • math.hのsqrtl関数を用いる
  • cmathのsqrt関数にlong doubleを渡す

 math.hのsqrt関数を使用すると、引数がdoubleにキャストされてしまいます。math.hにはsqrtlという別の名前の関数がlong double用に用意されているのでそちらを使います。
 またcmathのsqrt関数を用いる方法もあります。C++では関数がオーバーロードできる(引数が異なる同名の関数を作れる)ようになったので、cmathの平方根を求める関数はsqrtという名前に統一されています。long doubleを明示的に渡すことにより、long double版のsqrt関数を使うことができます。

Ex. bits/stdc++.h

 沢山includeを並べている解答例をよく見る中、次の一行だけincludeしている解答例を見かけました。

#include <bits/stdc++.h>

qiita.com

 標準ライブラリを全部includeしてくれます。今回必要だったcmathも含まれています。AtCoderでは使えるようなので、次回からはこれを使おうと思います。

Componentに必要な設定を自動で守らせるようにしたい

qiita.com

github.com

 Inspector上でResetすれば自動で必要な設定が揃うようにする、ということは今までやっていました。しかし、それでも面倒くさいのでもっと自動化しようとした取り組んだのが今回の記事。これでプロジェクトの最初からチェックルールを記述するようにしていけばかなり楽ができそうです。実際の説明はGitHubやQiitaに任せるとして、実装で躓いたことをメモ。

SceneをUnityEditor上でOpenせずに中身をいじる方法はないものか

 あるんですかね。

 今回の処理は、SceneとPrefabを全部チェック→必要があれば変更して保存する、という処理です。PrefabはAssetDatabase.LoadAssetAtPathを使って調査するだけでOK。しかし、Sceneに関しては上手な方法が見つかりませんでした。

 結局、UnityEditor上でSceneをOpenしてFindObjectsする方法を使いました。この方法の問題は、Scriptを実行したときにSceneを編集中だった場合に「編集内容を破棄するか」or「編集内容を強制保存するか」をしなくてはならないこと。とりあえず強制保存で実装しましたが、できれば避けたかったです。

既に開いているシーンを改めて開くとScriptが停止する

 エラーも出さずに中途半端なところで止まってしまいます。EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single)で空シーンを開いて対応。

Scriptによる変更が保存されない

 Scriptで変更した値が保存されない事態に遭遇しました。保存されてるように見えてもUnityを閉じて再度開くと元の値に戻ってしまう状態になりました。

 検索した結果「Undoクラスを使えば良い」や「SerializePropertyを使う」といった対応策が出てきましたが上手くいかず。結局EditorUtility.SetDirtyを使ったら上手く行きました。以下の記事が参考になりました。

www.urablog.xyz

 Componentに対してはEditorUtility.SetDirtyして、Sceneに対してはEditorSceneManager.MarkAllScenesDirtyしてから保存すると、Scriptによる変更が反映されました。

Scene内に配置されているPrefabに対する変更が保存されない

 前記の対応を行っても想定している動きにならなかったのが、Scene内に配置されたPrefabです。Scene内に配置されたPrefabは「元Prefabの値」に「変更値」分を上書きした状態になっています。この「変更値」分とScriptによる変更が重なっていると上手く行かないようでした。

 これは(1)一度Prefabのリンクを切って、(2)Scriptによる値の変更をして、(3)再度Prefabのリンクを繋げることで解決しました。リンクを切るのはPrefabUtility.DisconnectPrefabInstance、リンクを繋げるのPrefabUtility.ConnectGameObjectToPrefabです。リンクを繋げる方法はPrefabUtility.ReconnectToLastPrefabもあったのですが、こちらは上手く行きませんでしたね。