Между строк: Анимации в UI Toolkit

Здравствуйте, уважаемые господа. Меня зовут Линар Хилажев, я программирую логику, интерфейсы и игры.

Хочу кое-что интересное рассказать… значит начались эти события после того, как была написана статья Между строк: Создание элементов интерфейса.

Значит, потом, ну наитие такое появилось, что нужно написать определенную статью, желательно про анимацию.

Без лишних слов — встречайте: Анимации в Unity UI Toolkit.

Глава 1: Концепция анимации

Давайте начнем с объяснения того, как будет работать анимация для наших элементов интерфейса.

Здесь речь не идет о переходах/задержках и других встроенных возможностях анимации в UI Toolkit.

Из предыдущих статей мы уже научились создавать настраиваемые элементы пользовательского интерфейса. Теперь пришло время добавить немного динамичности.

Стоит отметить, что генерация анимации является довольно ресурсоемким процессом по сравнению с другими операциями, так как она будет нагружать процессор практически на каждом кадре.

Если разложить анимацию на базовые компоненты, то это будет изменение состояния объекта (размер, форма, цвет, положение и т.д.).

Первая задача, перед которой мы стоим, — научиться вызывать внутренние методы класса VisualElement с определенным интервалом времени.

Для этого мы будем использовать внутреннюю реализацию интерфейса IVisualElementScheduler.

Из названия можно догадаться, что оно занимается планированием и выполнением каких-то действий, давайте разбираться, как с ним работать.

public RombElement()
{
    schedule.execute(MethodName);
}

В этом примере кода, при создании элемента мы обращаемся к методу Execute передавая в него Action.

Это значит, что этот метод будет вызван сразу после выполнения конструктора.

Стоит понимать, что выполнен он будет только один раз, а как нам сделать многократные вызовы?

— Я рад, что вы спросили, отвечаю:

public RombElement()
{
    schedule.Execute(MethodName).Every(16);
}

Мы добавляем к нашему вызову .Every, что означает, наш метод будет вызываться каждые X миллисекунд.

С базой разобрались, давайте усложнять.

Глава 2: Анимации цвета

Под анимацией цвета мы будем обозначать изменение цвета/прозрачности и связанное с его составляющими у UI Element’a

Давайте сделаем так, чтобы наш элемент проявлялся из прозрачности.

Как и всегда, приведу весь код, а потом будет разбирать детально.

using UnityEngine;
using UnityEngine.UIElements;

namespace CustomElements
{
    [UxmlElement]
    public partial class RombElement : VisualElement
    {
        private float _timeLeft;
        
        private const float AlphaValue = 255;
        
        [UxmlAttribute] public float AnimationTime = 3f;

        public RombElement()
        {
            generateVisualContent += GenerateVisualContent;

            _timeLeft = AnimationTime;
            schedule.Execute(ChangeColorAnimation).Every(16);
        }

        private void ChangeColorAnimation()
        {
            _timeLeft -= Time.deltaTime;
            
            firstColor.a = (byte)Mathf.Lerp(firstColor.a, AlphaValue, Time.fixedDeltaTime / _timeLeft);
            secondColor.a = (byte)Mathf.Lerp(secondColor.a, AlphaValue, Time.deltaTime / _timeLeft);
            thirdColor.a = (byte)Mathf.Lerp(thirdColor.a, AlphaValue, Time.fixedDeltaTime / _timeLeft);
            fourColor.a = (byte)Mathf.Lerp(fourColor.a, AlphaValue, Time.deltaTime / _timeLeft);

            MarkDirtyRepaint();
        }

        Vertex[] vertices = new Vertex[4];
        ushort[] indices = { 0, 1, 2, 2, 3, 0 };

        private Color32 firstColor  = new (255, 0, 0, 0);
        private Color32 secondColor  = new (0, 255, 0, 0);
        private Color32 thirdColor  = new (0, 0, 255, 0);
        private Color32 fourColor  = new (17, 55, 55, 0);

        void GenerateVisualContent(MeshGenerationContext mgc)
        {
            vertices[0].tint = firstColor;
            vertices[1].tint = secondColor;
            vertices[2].tint = thirdColor;
            vertices[3].tint = fourColor;

            var top = 0;
            var left = 0f;
            var middleX = contentRect.width / 2;
            var middleY = contentRect.height / 2;
            var right = contentRect.width;
            var bottom = contentRect.height;

            vertices[0].position = new Vector3(left, middleY, Vertex.nearZ);
            vertices[1].position = new Vector3(middleX, top, Vertex.nearZ);
            vertices[2].position = new Vector3(right, middleY, Vertex.nearZ);
            vertices[3].position = new Vector3(middleX, bottom, Vertex.nearZ);

            MeshWriteData mwd = mgc.Allocate(vertices.Length, indices.Length);
            mwd.SetAllVertices(vertices);
            mwd.SetAllIndices(indices);
        }
    }
}

Обратим наше внимание на конструктор класса, мы там можем заметить вызов метода scheduler’а.

Мы хотим вызывать метод ChangeColorAnimation каждые 16 ms.

Дальше идет само тело метода анимации, в нем мы можем заметить _timeLeft, это переменная для отслеживания времени анимации, которое изначально равно AnimationTime.

Кстати, его можно настроить напрямую через UI Builder, благодаря новому атрибуту [UxmlAttribute].

Он появился в новой версии пакета, подробнее о нововведениях можно прочитать в моей статье.

Мы немного отвлеклись, вернемся к нашим анимациям.

private void ChangeColorAnimation()
{
    _timeLeft -= Time.deltaTime;
    
    firstColor.a = (byte)Mathf.Lerp(firstColor.a, AlphaValue, Time.fixedDeltaTime / _timeLeft);
    secondColor.a = (byte)Mathf.Lerp(secondColor.a, AlphaValue, Time.deltaTime / _timeLeft);
    thirdColor.a = (byte)Mathf.Lerp(thirdColor.a, AlphaValue, Time.fixedDeltaTime / _timeLeft);
    fourColor.a = (byte)Mathf.Lerp(fourColor.a, AlphaValue, Time.deltaTime / _timeLeft);

    MarkDirtyRepaint();
}

Дальше у нас идет четыре строки, где мы описываем, что оттенки наших вершин должны стремиться от 0 к 255.

После чего, мы вызываем метод MarkDirtyRepaint (), для того чтобы спровоцировать вызов метода GenerateVisualContent (…).

Глава 3: Новые повороты

Давайте признаемся, анимация цвета — это самое простое, что можно придумать.

А что, если у нас есть заявка на непосредственность? К примеру, мы хотим анимировать иконки/спрайты.

Иногда в голову прилетают интересные вещи, такие как мысли или более тяжелые, как анимация иконок без кадровой анимации.

В таком случае рассмотрим следующий пример, предположим, мы хотим чтобы у нас была анимация для колокола.

ll6quhcji_lsuigdyl9oucpgnbm.png

Концептуально, мы не делаем ничего сложного, сперва меняем точку вращения, а потом вращаем VisualElement*. (меняем значение rotate).

Зачем менять точку вращения у элемента? Чтобы логически было проще с этим работать и анимировать.

Для этого нам нужно изменить свойство transform-Origin, которое принимает два значения (X, Y), мы это сделаем прямо в конструкторе класса.

Также можно менять из UI Builder'а

Также можно менять из UI Builder’а

Обычно transform-Origin у элемента это center-center, то есть (X = 50%, Y = 50%), а мы хотим, чтобы он был top-center (X = 0%, Y = 50%).

Более подробно можно посмотреть в документации

Теперь, приступаем к основной части нашего мероприятия, написание самой анимации.

using UnityEngine.UIElements;

namespace CustomElements
{
    [UxmlElement]
    public partial class ImageAnimation : VisualElement
    {
        [UxmlAttribute] private float AngleLimit = 25;
        [UxmlAttribute] private bool AnimationIsStopped = true;
        
        private bool _rotationDirectionIsMinus = true;
        
        public ImageAnimation()
        {
            style.transformOrigin = new TransformOrigin( Length.Percent(50), 0);
            schedule.Execute(PlayAnimation).Every(16);
        }

        private void PlayAnimation()
        {
            if (AnimationIsStopped)
                return;
            
            RotateBell();
            MarkDirtyRepaint();
        }

        void RotateBell()
        {
            var newRotate = style.rotate;
            Angle rotateAngle = newRotate.value.angle;

            if (_rotationDirectionIsMinus)
            {
                rotateAngle.value--;
                if (rotateAngle.value < -AngleLimit)
                {
                    _rotationDirectionIsMinus = false;
                }
            }
            else if (_rotationDirectionIsMinus == false)
            {
                rotateAngle.value++;
                 if (rotateAngle.value > AngleLimit)
                 {
                     _rotationDirectionIsMinus = true;
                 }
            }
            
            newRotate.value = new Rotate(rotateAngle);
            style.rotate = newRotate;
        }
    }
}

Так выглядит весь класс, наш взор падает на метод RotateBell ().

Основной составляющей анимации в данном случае является изменение поворота изображения.

Мы вращаем изображение влево и вправо до тех пор, пока не достигнем предела AngleLimit.

Для этого мы изменяем значение свойства style.rotate и затем вызываем метод MarkDirtyRepaint (), чтобы нам перерисовали VisualElement.

Получаем такой результат.

Глава 4: Новые горизонты?

Сегодня мы разобрались, как можно анимировать элементы интерфейса в UI Toolkit’е и на этом закончим со статьями про кастомные элементы/анимации в Unity.

Если у вас есть вопросы или не поняли какую-то часть, приглашаю вас в комментарии :)

Спасибо за внимание и до скорых встреч!

© Habrahabr.ru