Pull to refresh

Расширение возможностей Tween-анимаций в NGUI

Reading time 6 min
Views 9.9K
Привет, меня зовут Михаил Куликов, я разработчик игр и в своем нелегком ремесле я использую Unity. Не буду вдаваться в описание того, что представляет из себя Unity или как я дожился до того, что начал использовать его в качестве движка. Скажу лишь, что это отличный инструмент с множеством как плюсов, так и минусов, и основным минусом, на мой взгляд, является отсутствие инструментария для комфортной работы над UI. Тот инструментарий, что разработчики Unity предоставили в версии 4.6 в качестве открытой беты, не в счет. Особо я в него не вдавался, да и особого желания нет, так как я уже давно пользуюсь плагином NGUI. Сегодня я хочу поделиться с вами проблемами, с которыми мне пришлось столкнуться, делая tween-анимации для интерфейса, а также решением этих проблем.

Проблема #1. Анимации и анкоры


Или я чего-то не понимаю, или компонент TweenPosition никак не стыкуется с anchor’ами. Я имею ввиду следующую ситуацию. У меня есть widget, который крепится anchor’ами к верхнему левому углу экрана. Меняя разрешение экрана и соотношения его сторон, мой widget хорошенько сохраняет свою позицию и все смотрится замечательно.
Соотношение сторон 4:3

Соотношение сторон 16:9

Когда я попытался использовать TweenPosition для анимации «выезжания» widget’а из-за края экрана, я понял, что ничего не выйдет. TweenPosition использует координаты Vector3 для обозначения начальной и конечной позиций анимации. Например, мы устанавливаем следующие значения для анимации:



Когда я меняю соотношение сторон, widget продолжает двигаться по координатам, сохраненным в TweenPosition, которые никак не соответствуют его новым координатам
Соотношение сторон 4:3

Соотношение сторон 16:9

Я пробовал написать скрипт, который высчитывал бы смещение для векторов From и To, но он не хотел дружить с anchor’ами, и анимация превращалась в ад. Это никуда не годится. Начинаем думать дальше.

В NGUI есть замечательный компонент TweenTransform, который позволяет перемещать объект из точки А в точку Б, где А и Б – transform’ы каких-либо объектов. Вот пример:



Убираем anchor’ы у widget’а и ставим anchor’ы у наших game object’ов A и B



Смотрим что получилось:
Соотношение сторон 4:3

Соотношение сторон 16:9

Теперь при любых разрешениях и соотношениях сторон анимация сохраняет свой вид.
Ну что же, работает отлично, по крайней мере ожидаемый результат достигнут. Но хочется автоматизировать процесс. Элементов UI, требующих анимации, у меня в проекте много и на каждом из них проводить описанные выше операции ужасно скучно. Пишем нехитрый скрипт, который нам поможет. Назовем его TweenTransformHelper.
//TweenTransformHelper.cs
[RequireComponent(typeof(TweenTransform))]
public class TweenTransformHelper : MonoBehaviour {
    public GameObject FromAnchor;
    public GameObject ToAnchor;
}

Я знаю, вышеописанный скрипт наверняка поразил вас своей сложность, так что следующий вас уже не напугает:
TweenTransformHelperEditor.cs
//TweenTransformHelperEditor.cs
[CustomEditor(typeof (TweenTransformHelper))]
public class TweenTransformHelperEditor : Editor {

    private TweenTransformHelper _tweener;

    private void Awake() {
        _tweener = (TweenTransformHelper) target;
    }

    public override void OnInspectorGUI() {
        EditorGUILayout.BeginHorizontal();
        if (GUILayout.Button("Create from anchor")) {
            CreateAnchorFrom();
        }
        if (GUILayout.Button("Destroy")) {
            DestroyAnchor(_tweener.FromAnchor);
        }
        EditorGUILayout.EndHorizontal();
        _tweener.FromAnchor = (GameObject) EditorGUILayout.ObjectField(_tweener.FromAnchor, typeof (GameObject));

        EditorGUILayout.BeginHorizontal();
        if (GUILayout.Button("Create to anchor")) {
            CreateAnchorTo();
        }
        if (GUILayout.Button("Destroy")) {
            DestroyAnchor(_tweener.ToAnchor);
        }
        EditorGUILayout.EndHorizontal();
        _tweener.ToAnchor = (GameObject) EditorGUILayout.ObjectField(_tweener.ToAnchor, typeof (GameObject));
        
        CreateAndApplyTweener();
        UpdateUI();
    }

    private void CreateAndApplyTweener() {
        bool toAnchorNotEqualsNull = _tweener.ToAnchor;
        bool fromAnchorNotEqualsNull = _tweener.FromAnchor;

        if (!fromAnchorNotEqualsNull) {
            EditorGUILayout.HelpBox("From anchor not created!", MessageType.Warning);
        }
        else if (!toAnchorNotEqualsNull) {
            EditorGUILayout.HelpBox("To anchor not created!", MessageType.Warning);
        }
        else {
            if (GUILayout.Button("Apply to tween")) {
                var tweenComponent = _tweener.GetComponent<TweenTransform>() ?? _tweener.gameObject.AddComponent<TweenTransform>();
                tweenComponent.from = _tweener.FromAnchor.transform;
                tweenComponent.to = _tweener.ToAnchor.transform;
                tweenComponent.enabled = false;
            }
        }
    }

    private void UpdateUI() {
        if (GUI.changed) {
            EditorUtility.SetDirty(_tweener);
        }
    }

    private void DestroyAnchor(GameObject gameObj) {
        if (gameObj == null) {
            return;
        }
        DestroyImmediate(gameObj);
    }

    private void CreateAnchorTo() {
        var anchor = CreateAnchor("$anchorTo");
        _tweener.ToAnchor = anchor;
    }

    private void CreateAnchorFrom() {
        var anchor = CreateAnchor("$anchorFrom");
        _tweener.FromAnchor = anchor;
    }

    private GameObject CreateAnchor(string anchorName) {
        var anchorGameObj = new GameObject(anchorName);
        anchorGameObj.transform.parent = _tweener.transform;
        anchorGameObj.transform.localPosition = Vector3.zero;
        anchorGameObj.transform.localScale = Vector3.one;
        var widgetScript = anchorGameObj.AddComponent<UIWidget>();
        widgetScript.width = widgetScript.height = 100;
        return anchorGameObj;
    }
}


Подробно описывать работу скрипта я не вижу смысла, в нем нет ничего сложного. Теперь, если добавить скрипт к widget’у, мы увидим вот такую панель:



Создаем $anchorTo и $anchorFrom и жмем «Apply to tween» (это автоматически заполнит соответствующие поля в TweenTransform). Теперь дело за малым, настроить привязку к краям экрана для $anchorTo и $anchorFrom, предварительно выставив их на нужные позиции.



На этом проблема #1 решена. Идем дальше.

Проблема #2 Последовательные анимации


А что, если мы захотим сделать цепочку из выезжающих объектов? При помощи NGUI сделать это элементарно. У каждого компонента TweenTransformer есть поле On Finished, которое может содержать любые методы любого компонента, при условии, что этот метод является публичным. Вызываются добавленные методы сразу после того, как анимация доиграет. Например, сделать последовательность из выезжалочек мы можем вот так:



Теперь при запуске анимации мы увидим следующие чудеса:



При активации экрана элементы выезжают и это выглядит неплохо. Но что, если мы захотим проигрывать анимацию в обратном порядке при деактивации экрана?

Исходя из того, что я нашел в обсуждениях на форуме плагина, не я один столкнулся с такой проблемой. Участники обсуждений предлагали решить эту проблему следующим путем: каким-то образом получать список вызовов событий, переворачивать его и выстраивать вызовы в обратном порядке. Мне это решение показалось чересчур сложным, ведь как говорится, нормальные герои всегда идут в обход. Имеющегося функционала события OnFinished класса UITweener (наследником которого является TweenTransform) недостаточно, т.к. оно вызывается, когда анимация играет из начала в конец и наоборот. Невозможно определить в какую сторону играла анимация, перед тем как завершиться. Если бы это было возможно, моя проблема была бы решена. В итоге я решил расширить возможности NGUI. Да простит меня ArenMook, но мне пришлось похозяйничать в его коде. На самом деле изменения, которые необходимо внести в класс UITweener, минимальны.

В UITweener.cs добавляем следующие поля:
List<EventDelegate> mTempForward = null;
List<EventDelegate> mTempReverse = null; 
[HideInInspector]
public List<EventDelegate> onFinishedForward = new List<EventDelegate>();
[HideInInspector]
public List<EventDelegate> onFinishedReverse = new List<EventDelegate>();

А в методе Update после
if (onFinished != null)
{
mTemp = onFinished;
…
}

добавляем
if (onFinishedForward != null && direction == Direction.Forward) {
	mTempForward = onFinishedForward;
	onFinishedForward = new List<EventDelegate>();
    EventDelegate.Execute(mTempForward);

	for (int i = 0; i < mTempForward.Count; ++i) {
		EventDelegate ed = mTempForward[i];
        if(ed != null && !ed.oneShot) EventDelegate.Add(onFinishedForward, ed, ed.oneShot);
	}
	mTempForward = null;
}

if (onFinishedReverse != null && direction == Direction.Reverse) {
	mTempReverse = onFinishedReverse;
    onFinishedReverse = new List<EventDelegate>();
    EventDelegate.Execute(mTempReverse);

	for (int i = 0; i < mTempReverse.Count; ++i) {
        EventDelegate ed = mTempReverse[i];
		if (ed != null && !ed.oneShot) EventDelegate.Add(onFinishedReverse, ed, ed.oneShot);
	}
	mTempReverse = null;
}	           	                

Заходим в UITweenerEditor.cs и вносим еще несколько строк кода, чтобы расширенные возможности класса UITweener отображались в редакторе.

После
NGUIEditorTools.DrawEvents("On Finished", tw, tw.onFinished);

добавляем
NGUIEditorTools.DrawEvents("On Finished forward", tw, tw.onFinishedForward);
NGUIEditorTools.DrawEvents("On Finished reverse", tw, tw.onFinishedReverse);

В результате этих манипуляций окно TweenTransform теперь выглядит вот так



Имея возможность узнать в каком направлении доиграла анимация, я могу выстроить последовательность, которая сможет корректно воспроизвестись как вперед, так и назад. Делается это элементарно:



Чтобы проиграть анимацию вперед, нужно вызвать метод PlayForward у первого элемента цепочки, а для того, чтобы проиграть ее в обратном порядке, нужно вызвать PlayReverse у последнего элемента. Теперь мы получаем ожидаемый результат:



Вывод


NGUI – великолепный плагин для Unity, с огромными возможностями, но у него, как и у любого сложного инструмента есть свои недостатки или недоработки. Но, имея в запасе немножко времени и желания, их легко исправить и добиться нужного результата.
Tags:
Hubs:
+3
Comments 3
Comments Comments 3

Articles