Unity:UI Elements でいくつかサンプル書いたよ!

最終更新日

はじめに

たっつー(管理人)
たっつー(管理人)

こんにちは、のんびりエンジニアのたっつーです。

今回は Unity 2019 から使える新機能の、UI Elements のサンプル集をいくつか書いてみたいと思います。
ぜひ、お付き合いいただければと思います(*’ω’*)

この記事はUnityゆるふわサマーアドベントカレンダー2019』の20日の記事です。

また、関連記事は以下となります。

UI Elementsとは?

従来のエディタ拡張システムの ImGui を置き換える新しいGUIシステムになります。また、将来的にはランタイムのUI(uGUI)もこの UI Elements に置き換わるとされています。

開発者がUIレイアウトおよびスタイル設定をすばやく作成および編集できるようにする新しい保持モードのGUIシステムです。
新しいGUIシステムは、UnityのUIの作成と最適化を容易にするために、WebのCSS、jQuery、HTML DOM、およびEventsシステムから概念を借りています。

また、パフォーマンスが向上し、スタイルシートや動的/コンテキストイベント処理などの多くの新機能も提供されます。


パフォーマンスとスケーラビリティを念頭に置いて新しいシステムを構築したので、開発者がUIを構築、変更、および対話することを可能にする従来の包括的なC#APIがあります。
使い慣れたC#API、イベントシステム、CSS、およびXMLのインポート形式により、ユーザーインターフェイスを簡単に構築できます。
UI Elementsは、エディタUIを拡張および作成するためのIMGUIに代わるものであり、将来のリリースではランタイムUIを作成するためにuGUIに代わるものとなるでしょう。

https://blogs.unity3d.com/jp/2019/04/16/introducing-unity-2019-1/#uielements

つまり何が言いたいかと、、、

こうゆう事風にエディタを拡張できます。(今)

こうゆう事がuGUIが将来的にUIElementsでできるようになります。(未来)

UI Elements はどんな事に使える?

まずは Youtube の動画を見ていただけば雰囲気はつかめると思いますのでぜひ一回再生してみてください! 1分20秒と短い!!

それではできる事をそれぞれ書き出してみたいと思います。

  • インスペクタの拡張
  • XML/CSS/C# で記述できる
  • UI Elements デバッガーが付いてくる
  • リッチなUI

詳しくは、こちらの記事をご参照ください。



UI Elements の使い方

詳しくは、 こちらの記事をご参照ください。

色々試して遊んでみよう!

共通コントロールを実装してみる

UIフレームワークでは、よく共通コントロールを作成する機会があるかと思います。今回は共通コントロールを作成する場合にUXML の Template 機能を紹介する場合がありますが、もっとスマートに実際に使えるコントロールを作成してみようと思います。

ソースコード(呼び出し側)

共通コントロールとして、以下の赤い文字のボタン(RedButton)を用意しました、これを使いまわせるパーツとして、ボタン1~3の計3つを作成しています。

ポイントとしては、「xmlns:my=”MyControls”」 で my:RedButton が使えるようになります。

<?xml version="1.0" encoding="utf-8"?>
<engine:UXML
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:engine="UnityEngine.UIElements"
    xmlns:editor="UnityEditor.UIElements"
    xmlns:my="MyControls"
    xsi:noNamespaceSchemaLocation="../../UIElementsSchema/UIElements.xsd"
    xsi:schemaLocation="UnityEngine.UIElements ../../UIElementsSchema/UnityEngine.UIElements.xsd">

  <engine:Label text="Sample1 UXML" />

  <my:RedButton text="button1" name="btn1" />
  <my:RedButton text="button2" name="btn2" />
  <my:RedButton text="button3" name="btn3" />

  <engine:Label text="Sample1 UXML end" />
</engine:UXML>
Label {
    font-size: 20px;
    -unity-font-style: bold;
    color: rgb(68, 138, 255);
}

ポイントとしては、各ボタンイベント(Clicked)にイベントを紐づけるために「root.Q(“btn1”).clicked += ButtonClicked;」で実行しています。

using MyControls;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;

public class Sample1 : EditorWindow
{
    [MenuItem("UIElementsSamples/Sample1")]
    public static void ShowExample()
    {
        Sample1 wnd = GetWindow<Sample1>();
        wnd.titleContent = new GUIContent("Sample1");
    }

    public void OnEnable()
    {
        VisualElement root = rootVisualElement;

        var visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/Editor/Sample1.uxml");
        VisualElement labelFromUXML = visualTree.CloneTree();
        root.Add(labelFromUXML);

        root.Q<RedButton>("btn1").clicked += ButtonClicked;
        root.Q<RedButton>("btn2").clicked += ButtonClicked;
        root.Q<RedButton>("btn3").clicked += ButtonClicked;
    }

    void ButtonClicked(EventBase eventBase)
    {
        var redButton = (RedButton)eventBase.target;
        Debug.Log($"Clicked {redButton.text} RedButton!! ");
    }
}

ソースコード(共通コントロール:RedButton)

さて、本題の RedButton の実装になります。
特に意味はないですが ~.uxml は使っていない実装になります、もちろん「AssetDatabase.LoadAssetAtPath(“~.uxml”);」を使っても実装もできます。

Button {
    font-size: 15px;
    -unity-font-style: bold;
    color: rgb(255, 0, 0);
}

以下のソースコードのポイントとしては、text プロパティの引数を受け取れる実装になります。
また、独自イベントとして clicked を定義しており、ボタンを押下イベントに合わせて clicked が呼ばれるようにしております。
また、MyControls のネームスペースに書く事をお忘れずに!

using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine.UIElements;


namespace MyControls
{
    internal class RedButton : VisualElement
    {
        public new class UxmlFactory : UxmlFactory<RedButton, UxmlTraits> { }

        public new class UxmlTraits : VisualElement.UxmlTraits
        {
            UxmlStringAttributeDescription m_Text = 
                new UxmlStringAttributeDescription { name = "text", defaultValue = "none label" };

            public override IEnumerable<UxmlChildElementDescription> uxmlChildElementsDescription
            {
                get { yield break; }
            }

            public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
            {
                base.Init(ve, bag, cc);
                var text = m_Text.GetValueFromBag(bag, cc);
                ((RedButton)ve).Init(text);
            }
        }

        private Button m_button;

        public string text
        {
            get { return m_button.text; }
            set { m_button.text = value; }
        }

        public event Action<EventBase> clicked;

        public RedButton()
        {
            var styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>("Assets/Editor/Components/RedButton.uss");
            styleSheets.Add(styleSheet);

            m_button = new Button();
            m_button.text = "noen label";
            m_button.clickable.clickedWithEventInfo += Button_Clicked;
            hierarchy.Add(m_button);
        }

        public RedButton(string text) : this()
        {
            Init(text);
        }

        public void Init(string text)
        {
            this.text = text;
        }

        private void Button_Clicked(EventBase eb)
        {
            eb.target = this;
            clicked?.Invoke(eb);
        }
    }
}

ツールバーを実装してみる

ソースコードは簡単です、「Toolbar()」を実体化して、Toolbarのコントロールを追加していくだけです。

<?xml version="1.0" encoding="utf-8"?>
<engine:UXML
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:engine="UnityEngine.UIElements"
    xmlns:editor="UnityEditor.UIElements"
    xsi:noNamespaceSchemaLocation="../../UIElementsSchema/UIElements.xsd"
    xsi:schemaLocation="UnityEngine.UIElements ../../UIElementsSchema/UnityEngine.UIElements.xsd">
  
  <engine:Label text="Sample2 UXML" />
</engine:UXML>
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
using UnityEditor.UIElements;


public class Sample2 : EditorWindow
{
    [MenuItem("UIElementsSamples/Sample2")]
    public static void ShowExample()
    {
        Sample2 wnd = GetWindow<Sample2>();
        wnd.titleContent = new GUIContent("Sample2");
    }

    public void OnEnable()
    {
        VisualElement root = rootVisualElement;

        // Create Toolbar
        {
            var toolbar = new Toolbar();
            root.Add(toolbar);

            var btn1 = new ToolbarButton { text = "Button" };
            toolbar.Add(btn1);

            var spc = new ToolbarSpacer();
            toolbar.Add(spc);

            var tgl = new ToolbarToggle { text = "Toggle" };
            toolbar.Add(tgl);

            var spc2 = new ToolbarSpacer() { name = "flexSpacer1", flex = true };
            toolbar.Add(spc2);

            var menu = new ToolbarMenu { text = "Menu" };
            menu.menu.AppendAction("Default is never shown", a => { }, a => DropdownMenuAction.Status.None);
            menu.menu.AppendAction("Normal menu", a => { }, a => DropdownMenuAction.Status.Normal);
            menu.menu.AppendAction("Hidden is never shown", a => { }, a => DropdownMenuAction.Status.Hidden);
            menu.menu.AppendAction("Checked menu", a => { }, a => DropdownMenuAction.Status.Checked);
            menu.menu.AppendAction("Disabled menu", a => { }, a => DropdownMenuAction.Status.Disabled);
            menu.menu.AppendAction("Disabled and checked menu", a => { }, a => DropdownMenuAction.Status.Disabled | DropdownMenuAction.Status.Checked);
            toolbar.Add(menu);

            var spc3 = new ToolbarSpacer() { name = "flexSpacer2", flex = true };
            toolbar.Add(spc3);

            var popup = new ToolbarMenu { text = "Popup", variant = ToolbarMenu.Variant.Popup };
            popup.menu.AppendAction("Popup", a => { }, a => DropdownMenuAction.Status.Normal);
            toolbar.Add(popup);
        }

        // Import UXML
        {
            var visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/Editor/Sample2.uxml");
            // var styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>("Assets/Editor/Sample2.uss");
            VisualElement labelFromUXML = visualTree.CloneTree();
            // labelFromUXML.styleSheets.Add(styleSheet);
            root.Add(labelFromUXML);
        }
    }
}

マウスイベントを取得してみる

マウスイベントの取得も結構簡単です。
「Manipulator」を継承して実装し、VisualElement.AddManipulator メソッドで対象のコントロールにひ紐づければ対応可能です。

using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
using UnityEditor.UIElements;


public class Sample3 : EditorWindow
{
    [MenuItem("UIElementsSamples/Sample3")]
    public static void ShowExample()
    {
        Sample3 wnd = GetWindow<Sample3>();
        wnd.titleContent = new GUIContent("Sample3");
    }

    public void OnEnable()
    {
        VisualElement root = rootVisualElement;

        root.AddManipulator(new MouseEventLogger());
        root.Add(new Label() { style = { backgroundColor = Color.yellow }, text = "output console log!" });
    }

    class MouseEventLogger : Manipulator
    {
        protected override void RegisterCallbacksOnTarget()
        {
            target.RegisterCallback<MouseUpEvent>(OnMouseUpEvent);
            target.RegisterCallback<MouseDownEvent>(OnMouseDownEvent);
        }

        protected override void UnregisterCallbacksFromTarget()
        {
            target.UnregisterCallback<MouseUpEvent>(OnMouseUpEvent);
            target.UnregisterCallback<MouseDownEvent>(OnMouseDownEvent);
        }

        void OnMouseUpEvent(MouseEventBase<MouseUpEvent> evt)
        {
            Debug.Log("Mouse Up " + evt + " in " + evt.propagationPhase + " for target " + evt.target);
        }

        void OnMouseDownEvent(MouseEventBase<MouseDownEvent> evt)
        {
            Debug.Log("Mouse Down " + evt + " in " + evt.propagationPhase + " for target " + evt.target);
        }
    }
}

画像をパラパラ漫画風に動かす

スライダーの数値変更イベントと、画像の切り替えを同期させてパラパラ漫画風にアニメーションをできるようにしてみました。

必要なリソース

プロジェクトのリソースフォルダー以下に大量の画像を用意します。

ソースコード

<?xml version="1.0" encoding="utf-8"?>
<engine:UXML
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:engine="UnityEngine.UIElements"
    xmlns:editor="UnityEditor.UIElements"
    xsi:noNamespaceSchemaLocation="../../UIElementsSchema/UIElements.xsd"
    xsi:schemaLocation="UnityEngine.UIElements ../../UIElementsSchema/UnityEngine.UIElements.xsd
                        UnityEditor.UIElements ../../UIElementsSchema/UnityEditor.UIElements.xsd
                        MyControls ../../UIElementsSchema/MyControls.xsd
                        UnityEditor.PackageManager.UI ../../UIElementsSchema/UnityEditor.PackageManager.UI.xsd">
  
  <engine:Label text="start" name="label" />
  <engine:Image name="image" />
  <engine:SliderInt low-value="0" high-value="92" value="1" name="slider" />
</engine:UXML>
Label {
    font-size: 20px;
    -unity-font-style: bold;
	-unity-text-align: middle-center;
    color: rgb(68, 138, 255);
}

Image {
    padding-left: 20;
    padding-top: 20;
    height: 150;
	background-image: url("/Assets/Resources/img_0.png");
}
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
using UnityEditor.UIElements;


public class Sample4 : EditorWindow
{
    [MenuItem("UIElementsSamples/Sample4")]
    public static void ShowExample()
    {
        Sample4 wnd = GetWindow<Sample4>();
        wnd.titleContent = new GUIContent("Sample4");
    }

    private Label label;
    private Image image;
    private SliderInt slider;

    public void OnEnable()
    {
        VisualElement root = rootVisualElement;

        // Import UXML
        var visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/Editor/Sample4.uxml");
        var styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>("Assets/Editor/Sample4.uss");
        VisualElement labelFromUXML = visualTree.CloneTree();
        labelFromUXML.styleSheets.Add(styleSheet);
        root.Add(labelFromUXML);

        this.label = root.Q<Label>("label");
        this.image = root.Q<Image>("image");
        this.slider = root.Q<SliderInt>("slider");

        slider.RegisterValueChangedCallback<int>(Slider_ValueChanged);
    }

    private void Slider_ValueChanged(ChangeEvent<int> evt)
    {
        this.label.text = $"page {evt.newValue}/{slider.highValue}";
        var backgroundTexture = Resources.Load<Texture2D>($"img_{evt.newValue}");
        this.image.style.backgroundImage = backgroundTexture;
    }
}

後書き・Github

3ファイルの関係

UI Elements を使ってみた感じですが、uxml、uss、cs ファイルの3つがデフォルトで作成されますがこの3つは特に紐づいていなくて、3ファイルの紐づけは手動で記載する必要があります。

UXの定義が別ファイルになるメリット

皆さんご存知の通りシーン内に画面の情報が含まれていると、バージョン管理システムでの差分変更点などが直感的にわからない事が多いです。
そのため、このように別ファイルで管理されると変更点が一目でわかるようになるのでとても便利かなと思います。

Github

また、ソースコードはこちらに保存してあるので気になる方は試してみてください。

おすすめの記事

ちなみに、以下の記事にも Unity 2019 の新機能の記事を書いていますので興味があればぜひご参照ください。







よければ、SNSにシェアをお願いします!

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

コメントする