【Unity】【UI Toolkit】指定した時間後に処理を実行するIVisualElementSchedulerの使い方

UnityのUI Toolkitで指定した時間後に処理を実行するIVisualElementSchedulerの使い方についてまとめました。

Unity2022.2.19

はじめに

UIアニメーションを実装するときなど、指定した時間の後にプロパティを変更するなどの処理をしたい時があります。

UI Toolkitではこれを実現する仕組みとしてIVisualElementSchedulerがあります。
本記事ではこれについて説明します。

準備として、下図のように中央に円形の要素Circleを配置したUXMLファイルを用意しておきます。

UI Builder

出力されたUXMLファイルは以下の通りです。

<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" xsi="http://www.w3.org/2001/XMLSchema-instance" engine="UnityEngine.UIElements" editor="UnityEditor.UIElements" noNamespaceSchemaLocation="../../../UIElementsSchema/UIElements.xsd" editor-extension-mode="False">
    <ui:VisualElement name="Background" style="flex-grow: 1; background-color: rgb(255, 246, 220); height: 248px; justify-content: center;">
        <ui:VisualElement name="Circle" style="flex-grow: 0; background-color: rgb(255, 0, 0); width: 100px; height: 100px; border-top-left-radius: 50px; border-bottom-left-radius: 50px; border-top-right-radius: 50px; border-bottom-right-radius: 50px; transition-property: scale; transition-duration: 500ms; transition-timing-function: ease-out; position: relative; align-self: center;" />
    </ui:VisualElement>
</ui:UXML>

CircleのスケールプロパティにはTransitionを設定し、スケールが変更されたらアニメーションをするようにしてあります。

Transitionについては下の記事にまとめていますので、必要に応じて参照してください。

light11.hatenadiary.com

指定した時間後に処理を実行する

処理を遅延実行させるには、VisualElement.scheduleによりIVisualElementSchedulerを取得します。
このExecuteに処理を書くと、その処理はこのVisualElementがパネルにアタッチされたタイミングか、アタッチ済みの場合には即座に実行されます。
さらに以下のようにStartingInメソッドを使うと処理の実行を遅延させることができます。

using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;

public sealed class ExampleWindow : EditorWindow
{
    // uxml
    [SerializeField] private VisualTreeAsset _layout;

    private void OnEnable()
    {
        _layout.CloneTree(rootVisualElement);
        var circle = rootVisualElement.Q("Circle");

        circle.schedule
            .Execute(() => circle.transform.scale = Vector3.one * 2) // スケールを2倍にする
            .StartingIn(1000); // 1秒後に実行
    }

    [MenuItem("Window/Example")]
    private static void Open()
    {
        GetWindow<ExampleWindow>();
    }
}

このスクリプトのInspectorからLayoutプロパティに前節のUXMLファイルをアサインし、Window > Exampleからウィンドウを開くと以下の結果が得られます。

遅延実行

1秒遅延してからスケーリングしていることを確認できました。

繰り返しと期間

指定した処理を一定時間間隔で繰り返すには、以下のようにEveryを使います。
またForDurationを使うことで全体の(ここでは繰り返しの)継続期間を指定できます。

using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;

public sealed class ExampleWindow : EditorWindow
{
    // uxml
    [SerializeField] private VisualTreeAsset _layout;

    private void OnEnable()
    {
        _layout.CloneTree(rootVisualElement);
        var circle = rootVisualElement.Q("Circle");

        var pingPong = false;
        circle.schedule
            .Execute(() =>
            {
                // 実行されるごとにサイズを(1,1,1) or (2,2,2)に変更
                circle.transform.scale = pingPong ? Vector3.one : Vector3.one * 2;
                pingPong = !pingPong;
            })
            .StartingIn(500) // 0.5秒後に開始
            .Every(500) // 0.5秒ごとに実行
            .ForDuration(2000); // 2秒間有効
    }

    [MenuItem("Window/Example")]
    private static void Open()
    {
        GetWindow<ExampleWindow>();
    }
}

これを実行すると以下の結果が得られます。

繰り返しと期間

ちなみに上記のコードではForDurationを使っていますが、Untilを使うとフラグを使って任意のタイミングまで処理を継続できます。

任意のタイミングで時間計測を開始する

これまでの方法では、遅延時間の計測が即座に開始されます。
もし、ボタンを押してから1秒など任意のタイミングで時間計測を開始したい場合には、StartingInを使用せずにPauseExecuteLaterを使用します。

using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;

public sealed class ExampleWindow : EditorWindow
{
    // uxml
    [SerializeField] private VisualTreeAsset _layout;

    private void OnEnable()
    {
        _layout.CloneTree(rootVisualElement);
        var circle = rootVisualElement.Q("Circle");

        // 実行ボタンを追加
        var executeLaterButton = new Button { text = "Execute" };
        rootVisualElement.Add(executeLaterButton);

        var scheduledItem = circle.schedule
            .Execute(() => circle.transform.scale = Vector3.one * 2);
        // ポーズしておく
        scheduledItem.Pause();

        executeLaterButton.clickable.clicked += () =>
        {
            // ボタンが押されたタイミングで計測開始して1秒後に実行
            scheduledItem.ExecuteLater(1000);
        };
    }

    [MenuItem("Window/Example")]
    private static void Open()
    {
        GetWindow<ExampleWindow>();
    }
}

実行すると以下の結果が得られます。

任意のタイミングで開始

ボタンを押下してから1秒後にサイズが変更していることを確認できました。

PauseとResume

PauseResumeを使うと時間計測を一時停止/再開できます。

using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;

public sealed class ExampleWindow : EditorWindow
{
    // uxml
    [SerializeField] private VisualTreeAsset _layout;

    private void OnEnable()
    {
        _layout.CloneTree(rootVisualElement);
        var circle = rootVisualElement.Q("Circle");

        var pingPong = false;
        var scheduledItem = circle.schedule
            .Execute(() =>
            {
                circle.transform.scale = pingPong ? Vector3.one : Vector3.one * 2;
                pingPong = !pingPong;
            })
            .Every(500);

        // Pauseボタンを追加
        var paused = false;
        var pauseButton = new Button();
        pauseButton.text = "Pause";
        rootVisualElement.Add(pauseButton);
        pauseButton.clickable.clicked += () =>
        {
            if (paused)
            {
                scheduledItem.Resume();
                pauseButton.text = "Pause";
                paused = false;
            }
            else
            {
                scheduledItem.Pause();
                pauseButton.text = "Resume";
                paused = true;
            }
        };
    }

    [MenuItem("Window/Example")]
    private static void Open()
    {
        GetWindow<ExampleWindow>();
    }
}

参考

docs.unity3d.com