7080 + 1

ゲームプログラミングの記事を書いてます。

【Unity】UIToolKitの機能だけでスマホみたいなフッターUIを作ってみる

UIToolKit の勉強中です。
UIToolKit は HTML+CSS を参考に作られています。
なので「既存の HTML+CSS の動画とか参考にすればいい感じのUIを作れるのでは?」と思いました。
できたのがこんな感じのスマホのようなフッターUIです。

参考動画は HTML+CSS で作られているものを見ながら作りました。
Magic Navigation Menu Indicator using Html CSS & Javascript | Curve Outside Effects - YouTube

アイコンはすべて icons8 からダウンロードしています。

改善点

添付 Gif の青丸のカーソルをちゃんと動かすことができなかったです。
EditorWindow のサイズが変わったときに追従させられなかった...

CSS との違いで苦戦したところ

PseudoClass(疑似クラス)

CSS に比べて対応しているものがかなり少ないです。今後増えてくれると嬉しいなと思いました。
USS (8種類)
Unity - Manual: Pseudo-classes

CSS (5,60種類くらい)
擬似クラス - CSS: カスケーディングスタイルシート | MDN

BorderRadius

USS と CSS で挙動が違います。ので、参考動画のような滑らかな曲線を作ることができませんでした。

参考

【Unity】タブUIを実装してみる【UIToolkit】

最近 UIToolKit を勉強しています。
PlayMode 中での実装例もちょこちょこ聞くようになってきて、そろそろ IMGUI から卒業するための準備をしないとまずいかもと思っているからです。
これとか→UI Toolkitを使用したランタイムツールの開発事例紹介 - YouTube

(まだ2,3年は大丈夫だと思いますが...)

というわけで、タブを実装できる TabView の仕組みを見様見真似で作ってみました。
実装例はこんな感じです。

GitHub にサンプルと一緒にあげてあります。
僕も UIToolkit を初めて間もないので、同じく入門する方の参考になれば幸いです。
github.com

概要

TabView はタブを簡単に追加できます。
また、そのタブで何を表示するかは、自分で作った uxml を使えるようになっています。
上の動画サンプルは以下のコードで実装できます。

public class TabWindow : EditorWindow
{
    [MenuItem("Samples/TabWindow")]
    public static void Open()
    {
        GetWindow<TabWindow>("タブサンプル");
    }

    private void CreateGUI()
    {
        var tab = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("[TabViewを持っているuxmlのパス]");
        tab.CloneTree(rootVisualElement);
        var tabView = rootVisualElement.Q<TabView>();

        var tab1Content = new TabContentBase(AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("[Tab1で表示したいUIのuxmlのパス]"));
        var tab2Content = new TabContentBase(AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("[Tab2で表示したいUIのuxml]のパス"));
        tabView.AddTab("Tab1", tab1Content);
        tabView.AddTab("Tab2", tab2Content);

        // Tab1を選択
        tabView.SelectTab(0);
    }
}

また、以下の uxml が必要です。

  1. TabView を含んで、かつ TabItem.uss を適用した uxml
  2. 各タブで表示する中身の uxml

1 の作り方ですが、TabView は UxmlFactory を実装しているので、UIBuilder から追加することができます。
UIBuilder の Library に表示されるので、D&D で追加してください。

タブのコンテンツ表示は ITabContent を実装できるので、タブの表示時、非表示時の処理を独自で実装できます。

...とはいえまだまだ分からないことだらけなので、もっとうまい方法があるかもです。
だれかコメント求む~~~といった感じです...

所感

IMGUI よりも少しだけリッチな見た目のエディタ用UIを実装できるのが楽しいです
何よりもレイアウトを UIBuilder で見ながら作れるので、めちゃくちゃ楽ですし、何より楽しいです。(HTML,CSSの知識が乏しいので苦戦はしています...)
まだまだ IMGUI、uGUI に比べて機能は少ないので、基礎から学ぶには今くらいが絶好なんじゃないかなと思います。

【Unity】配列のラベルを型名に変更するPropertyAttribute

Unity では、配列や List をシリアライズ可能な形で定義すると、インスペクタ上で配列の要素を追加したり削除したりできるようになって便利です。
Unity2021 あたりからは、自動的に ReorderableList になったので並び替えも簡単になり、さらに便利になりました。

しかし、配列の各要素のタイトルが「Element 0」などといった表記になって、その要素が何なのかがわからないことがあります。
特に SerializeReference で配列を定義しているときに顕著に出るかなと思いました。
例えば、以下のように IExampleInterface を実装した3つのクラスがあったとします。

public interface IExampleInterface
{
}

[Serializable]
public class ExampleClassA : IExampleInterface
{
    [SerializeField]
    int fieldA;
}

[Serializable]
public class ExampleClassB : IExampleInterface
{
    [SerializeField]
    int fieldB;
}

[Serializable]
public class ExampleClassC : IExampleInterface
{
    [SerializeField]
    int fieldC;
}

これを SerializeReference 属性のリストで定義すると、インスペクタ上ではこのような見た目になります。

public class Example : MonoBehaviour
{
    [SerializeReference]
    List<IExampleInterface> intefaces = new List<IExampleInterface> {
        new ExampleClassA(), 
        new ExampleClassB(),
        new ExampleClassC(),
    };
}

ここでは、できれば Elelement 0 などという形ではなく、ExampleClassA などどいった型名が出てくれたほうが作業がしやすいと思います。
以下のような PropertyAttribute を定義することで、型名を表示することができます。

この属性を適用してみると、これで以下のような見た目になります。

public class Example : MonoBehaviour
{
    [SerializeReference, LabelTypeName]
    List<IExampleInterface> intefaces = new List<IExampleInterface> {
        new ExampleClassA(), 
        new ExampleClassB(),
        new ExampleClassC(),
    };
}

蛇足

上記の例では SerializeReference になっているときのことしか考慮していませんが、SerializedProperty.propertyType で分岐して、任意の名前に変えることができます。

【Unity】エディタのデバッグモードを常に有効にする

問題点

Unity で作業をしていると、当然ですが、デバッグ作業が必要になってくると思います。
VisualStudio や Riderなどの IDE でデバッガーを起動して、ブレイクポイントを仕込んだりして挙動を見ると思います。

Unity2020 か 2021 あたりから、エディタ上で ReleaseMode と DebugMode が分かれるようになりました。
Unity の画面右下に虫みたいなアイコンが出ていたら該当するかと思います。

このマークだとエディタが ReleaseMode ということになります。
この状態で VisualStudio などで F5 を押してデバッガーを起動すると、Unity側でこんな画面が出てきます。

で、DebugMode に切り替えると、スクリプトコンパイルが始まって少し待たされます。

僕はしょっちゅうデバッガーを使うので、Unity を起動する度にこれをして待たされていました。
いろいろ調べていると、どうやら起動時に最初から DebugMode で起動しておく方法を見つけました。
(よく見たら上記の画像に思い切り設定変更出来るよって書いてありました…)

解決方法

設定は簡単で、 Unityのメニューバー→Edit→PreferencesのGeneralタブの、添付画像の設定を変えるだけでできました。

これでデバッガーを起動するときにコンパイルが走ることはなくなります。
ただ、 DebugModeはReleaseModeと比べてパフォーマンスが若干悪くなります。
ケースバイケースで使い分けるのが良いかもしれません。

蛇足

画面右下の虫マークを押せば、任意のタイミングで二つを切り替えることもできます。

【Unity】シーンの変更の破棄をスクリプトから実行する

Unity で変更のあったシーンファイルは、シーン名の横にアスタリスクがついて、変更があることを教えてくれます。

この時、右クリックで出るメニューの「Discard Changes」を実行することで、保存されていない変更をすべて破棄することができます。

スクリプトからこれを実行したいのですが、残念ながら public に公開されたメソッドとして用意されているものがありません。(見つからなかった)
が、 Unity がメニューから操作できるようになっているなら、それ用のスクリプトがあるはずだと思って探しました。
EditorSceneManager.ReloadScene() というメソッドを発見したので、リフレクションを使って無理やり実行します。

ソースコードとしては以下です。

DiscardChangeSceneAsset.DiscardChange() を呼べば、引数のシーンの変更が破棄されます。
一応 PlayMode 中は処理しないようにしています。

また、メニューから実行できる DiscardChanges は確認ダイアログなどを出したりしていますが、これは問答無用で破棄するので注意してください。
具体的な処理は以下辺りを参照ください。
github.com

【Unity】特定のシーンなど、保存させるアセットを限定する

Unity で作業をしているとき、基本的には Ctrl+S で作業内容を保存するかと思います。
これは編集中のアセットから今開いているシーンまですべて保存します。
これだと困ることがたまにあって、例えば特定の ScriptableObject を編集していて保存したとき、知らないうちに編集していたシーンまで保存されてしまったりなんてことがあるかと思います。

この問題は、AssetModificationProcessor.OnWillSaveAssets() を使うことで解決できます。

私が開発している中でやりたかったこととして、「特定の作業をしている間、関連しているシーンの変更は保存させたくない」
というものがありました。

特定のシーンを保存させない処理はこちら

SaveSceneAssetBlocker.SetDontSaveScene()、ResetDontSaveScene() を任意のタイミングで呼べば、その間だけ設定したシーンが保存されなくなります。
パッと見だと不具合のように感じるので、ダイアログを出してあげたりするのが親切かもしれません。

注意点

AssetModificationProcessor.OnWillSaveAssets() はアセットのパス名でしか判定できないため、シーン名と同じ名前のアセットがほかにあったら、それも保存されなくなるのでご注意ください。
もし同名の別アセットがある場合、AssetDataBase.LoadAssetAtPath() で一度ロードして、型で分岐する処理をするのが良いと思います。

蛇足

AssetModificationProcessor を使えば、ほかにも「特定のアセットを削除させない」などもできるみたいです。
docs.unity3d.com

また、併せてシーンの変更を破棄したい場合はこちら
7081.hatenablog.com

【Unity】Timelineのヘッドを手動で動かしたときだけ処理を仕込む【Timeline】

やりたいこと

Timeline の現在時間は PlayableDirector.time で取得することができます。
が、この値が普通に Timeline を再生して動いたのか、それとも手動で Timeline のヘッドを動かしたから動いたのかを区別することはできません。
(あるかもしれないけど見つけられなかった

そこで、Timeline を少しいじって手動で動かしたときのイベントを仕込んで、
イベントハンドラを設定できるようにしてみました。

普通に再生された分にはこのイベントは発火しませんが、ヘッドを Timeline ウィンドウから動かしたときに発火します。

前提

使用したバージョンはそれぞれ以下です。
Unity: 2020.3.19f1
Timeline: 1.5.5

前準備

まずは、PackageManagerから落としてきているであろう Timeline をいじれるようにします。
これは本題ではないので割愛しますが、こちら参考にしていただければと思います。
tsubakit1.hateblo.jp

Timeline側にイベントを仕込む

肝心になるのは TimelineWindow_TimeCursor.cs というファイルです。
差分をはっつけておきます

実際の差分はこちらから見れます。
Timelineのヘッドが動いたイベントを仕込む · atori708/Sandbox2020@889761e · GitHub

大事なのは OnChangedPlayHead ですが、クリックしたときとドラッグしたときで2箇所に仕込む必要があります。

また、イベントハンドラを削除する処理も一応仕込んでおきます。
Sandbox2020/TimelineWindow.cs at master · atori708/Sandbox2020 · GitHub

仕込んだイベントをフックする

リフレクション使って、追加したイベントにイベントハンドラを登録します。
ChangePlayHeadHandlerというイベントハンドラを使います。
これはただ単にログ出力をするだけのメソッドです。

・今回の場合、 OnChangedPlayHead を持ってる TimelineWindowのインスタンスを持ってきます。
TimelineWindow はシングルトンになっているので、CreateInstance() はせず、instance という名前を取得してきます。

Assembly assem = typeof(TimelineEditor).Assembly;

// TimelineWindowクラスのインスタンスを生成
Type timeilneWindowType = assem.GetType("UnityEditor.Timeline.TimelineWindow");
timelineWindowObj = timeilneWindowType.GetProperty("instance", BindingFlags.Public | BindingFlags.Static).GetValue(null);

・TimelineWindow のインスタンスから、 OnChangedPlayHead のイベントを取得します。

// OnChangedPlayHeadのイベントを取得
onChangedPlayHead = timeilneWindowType.GetEvent("OnChangedPlayHead");
Type tDelegate = onChangedPlayHead.EventHandlerType;

・OnChangedPlayHead にイベントハンドラを登録するには、イベントハンドラのデリゲートを作る必要があります。

// ハンドラを取得
MethodInfo miHandler = typeof(TimelinePlayHeadTest).GetMethod("ChangePlayHeadHandler", BindingFlags.NonPublic | BindingFlags.Instance);

// ハンドラーのデリゲートを作成
changedPlayHeadHandlerDelegate = Delegate.CreateDelegate(tDelegate, this, miHandler);

・作成後、イベント登録用のメソッドを取得し、イベントハンドラを引数として実行します。

// OnChangedPlayHeadのAddメソッドを取得
MethodInfo addHandler = onChangedPlayHead.GetAddMethod();

// ハンドラーを引数にAddメソッドを実行
System.Object[] addHandlerArgs = { changedPlayHeadHandlerDelegate };
addHandler.Invoke(timelineWindowObj, addHandlerArgs);

イベントハンドラを削除するための処理も、イベントハンドラ登録と同じような流れになります。

// OnChangedPlayHeadのRemoveメソッドを取得
MethodInfo removeHandler = onChangedPlayHead.GetRemoveMethod();

// ハンドラーを引数にRemoveメソッドを実行
System.Object[] removeHandlerArgs = { changedPlayHeadHandlerDelegate };
removeHandler .Invoke(timelineWindowObj, removeHandlerArgs );