Tower Defense своими руками, часть 2: Интерфейс и NGUI

    Доброго времени суток, друзья!

    Да-да, именно друзья, ведь после моего первого поста откликнулось очень много людей, кто был готов помочь всем, чем смогут. И в этом я чрезмерно благодарен Хабру — уже написаны целых две музыкальные композиции и грамотно переведен весь текст в игре на английский язык. Даже не знаю, что бы я без вас делал!

    Но сам пост не об этом. Сегодня я хотел бы поделиться с вами маленькими подсказками по поводу пользовательского интерфейса. О том, чего стоит избежать уже в самом начале разработки, и что потом делать. К сожалению, мой «скилл» недостаточно высок, чтобы писать о чем-то действительно новом и неизведанном в этой области, так что пост будет больше посвящен новичкам в гейм-деве (и в первую очередь тем, кто пока еще боится NGUI), да и пост будет субъективен на все 146%.

    image

    Итак, самая важное замечание при разработке интерфейса, когда вы работаете в Unity3D: ни в коем случае не пользуйтесь стандартным интерфейсом! На Asset Store (это такой внутри-Unity магазин, где люди делятся своими наработками и готовыми решениями) есть огромное множество различных вариантов GUI-систем, и самым популярным на данный момент является NGUI. Не стоит недооценивать его (или другие GUI-системы), ведь выбор подобной технологии позволит сэкономить кучу времени.

    Давным-давно, когда я косо посматривал на NGUI, я считал, что стандартный GUI не так уж и плох — знай себе, да прописывай области для вывода кнопок, и что они делают… На все это дело было потрачена уйма времени и сил, и как оказалось, зря!

    Интерфейс оказался слишком медленным… Очень! По итогам работ оказалось, что даже одно окно интерфейса «просчитывается» целых 25 мс (на ноутбучном i5), что оказалось категорически неприемлемым для конечного пользователя. И вот буквально в тот же день, когда я обратил на это внимание, на Asset Store с большой скидкой стал продаваться NGUI. Вот тогда-то все и изменилось.

    image

    Сам принцип работы NGUI (или любой подобной системы) состоит в том, что все GUI-элементы являются простыми объектами, имеющими свой материал и без проблем отрисовываются даже мобильниками без ущерба производительности. Но тут-то и возникает одна из главных «проблем» (из-за которой я изначально и отказался от NGUI) — приходится выводить отдельные функции для обработки событий, таких как нажатие на кнопку и ей подобные.

    Вообще, это не является проблемой, если у вас есть точное представление о том, что «тут у меня будет кнопка выхода из игры, а здесь откроется окно опций». Проблемы начнутся тогда, когда вы захотите динамически выводить разное число элементов и динамически определять то, что с ними делать. Еще больше проблем обнаруживается, когда ты узнаешь, что при вызове Callback-функции кнопкой в NGUI, NGUI не может передавать аргументы для запуска функции.

    Я нашел довольно изящное (а может и кривое, кому как :) ) решение, и сводится оно к следующему (все написанное ниже касается Unity3D с NGUI):
    Просто напросто в имя объекта-кнопки записывайте все те аргументы, которые вы хотите использовать для вызова Callback-функций. А при вызове Callback-функции воспользуйтесь вот этой замечательной строкой:
    UIButton.current.gameObject.name
    

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

    Для того, чтобы записать различные агрументы в имя объекта (то есть по сути, сформировать одну строку из разных аргументов), можно пользоваться чем-то подобным:
    int itemID = 2;
    int count = 15;
    
    string finalString = itemID.ToString() + ":" + count.ToString();
    // 2:15
    

    А для считывания этого дела, воспользоваться функцией string.Split(char[]);
    Выглядеть это будет примерно так:
    char[] dc = {':'};
    string[] decoded = finalString.Split(dc);
    itemID = int.Parse(decoded[0]);
    count = int.Parse(decoded[1]);
    

    Вообще я очень удивлен, почему так мало разработчиков пользуются возможностью записи различных данных в одну строку, а потом декодировать эту строку. Это же так просто и удобно!

    Кстати, для тех, кто еще не успел приобщиться к NGUI, подскажу еще парочку полезных функций. Мне пришлось изрядно покопаться в справке, чтобы найти все это и проверить на работоспособность:

    1) Добавление элемента интерфейса, дочерним к уже существующему (по сути, это то же самое, что и обычный Instantiate(), но он не работает адекватно в NGUI):
    NGUITools.AddChild(parent, prefab);
    


    2) При работе с сетками элементов, рекомендуется использовать вот такую запись вместо привычного Destroy(object):
    NGUITools.Destroy(object);
    


    3) Для добавления новой функции в список Callback-функций у кнопки, используем следующее:
    GetComponent<UIButton>().onClick.Add(new EventDelegate());
    GetComponent<UIButton>().onClick[GetComponent<UIButton>().onClick.Count - 1].Set( MonoBehaviour, "FunctionName");
    


    4) Если хотите изменить размер виджета:
    NGUIMath.ResizeWidget(widget, widget.pivot, x, y, minWidth, minHeight);
    


    В общем-то, бОльших знаний для работы с NGUI почти и не надо. По крайней мере, со своими задачами я справляюсь довольно легко. Посмотрите обязательно и урок от автора, где он рассказывает, как создавать элементы интерфейса.

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

    На данный момент я практически закончил работу над интерфейсом и ядром игры. После этого я немного поработаю над игровыми объектами и займусь сетевой игрой. У меня уже есть несколько мыслей по этому поводу, и о том, что написать в следующей части, касающейся сетевой игры и защиты от нечестных игроков.

    А быть может, в следующий раз расскажу про Steamworks, если получу зеленый свет на гринлайте.

    image

    Комментарии 17

      0
      Тут все:

      image
        0
        Поддержал в стиме, удачи автору.
          0
          Посмотрим что за обновлённый GUI будет в текущей ветки 4.* юнити, если я не ошибаюсь его разработкой занимается как раз автор NGUI.
            0
            Думается, что новый гуи будет хотя бы частично совместим с NGUI
              0
              Автор писал, что процесс миграции будет существовать и переделывать заново не придётся.
              0
              Занимался. Он уволился еще прошлом году, кажется
              0
              Только вчера просматривал ваш проект на гринлайте, кстати, поговаривают, что гринлайт прикроют, что думаете по этому поводу?
                +1
                Не думаю что прикроют в ближайшие 3-4 месяца, да и надеюсь, что будут поблажки тем, кто на момент закрытия гринлайта уже собрал / уже собирает голоса.
                  0
                  Выглядит очень неплохо, проголосовал за него в steam-е.
                  0
                  Его не прикрывают, а просто меняют форму. Другими словами идет развитие.
                  Ну изменяет ему название, и что? Суть-то останется.
                  +2
                  «Вообще я очень удивлен, почему так мало разработчиков пользуются возможностью записи различных данных в одну строку, а потом декодировать эту строку. Это же так просто и удобно!»

                  Быть может потому, что строки Immutable, и такие вот операции конкатенации очень дороги. Если их будет много (как например при конкатенации в каком-нибудь событии дрэга, ресайза и прочего), то GC убьет всю производительность, а память забьется фрагментами и будет дырявой как шапка почтальона печкина.
                    +4
                    Добавлю, что раз уж так хочется производить подобные операции именно со строками, то лучше юзать класс StringBuilder.
                      0
                      Вот тут с вами не согласятся: habrahabr.ru/post/220921/
                      Если количество соединяемых строк заранее неизвестно (в цикле, например), то StringBuilder будет хорошим выбором.
                      Если известно заранее, но больше 4 — то нужно уже смотреть, что быстрее будет.
                        +1
                        У StringBuilder есть конструктор, принимающий в качестве аргумента изначальный размер внутреннего буфера. Если выделить ровно столько памяти, сколько нужно — едва ли будет какая-то разница между этими вариантами.
                    +1
                    Но тут-то и возникает одна из главных «проблем» (из-за которой я изначально и отказался от NGUI) — приходится выводить отдельные функции для обработки событий, таких как нажатие на кнопку и ей подобные.

                    Ну переход к подпискам начал происходить только с 3.х и еще в процессе. В линейке 2.х (да и сейчас можно, ничего не удалено) есть компонент UIButtonMessage, который форвардит клики в указанный GameObject (ГО) путем вызова метода, указанного строковым параметром. Т.е. в сцене делается центральный ГО с компонентом, в котором есть методы-колбеки для всего локального гуя, например:
                    class GuiCallbacks : MonoBehaviour {
                    void OnButton1Click()
                    {
                    }
                    
                    void OnInventoryItemClick(GameObject itemGO)
                    {
                    }
                    }
                    


                    Сам контрол генерится любыми способами и на него довешивается UIButtonMessage в коде или в инспекторе, в taget вешается ГО с указанным выше компонентом, в method пишется строка «OnButton1Click». Все, метод на главном ГО с UI-логикой будет вызван при клике по кнопке. Поддерживаются 2 варианта — без параметра и с параметром ГО, на котором висел компонент UIButtonMessage. Так же есть готовый компонент по форвардингу любых ссобщений нгуя. Вся работа ведется через стандартный SendMessage. Кто-то скажет, что это медленно и не модно, но все зависит от задачи. Если кнопка не должна кликаться 100 раз в секунду, то отсылка через сообщение будет вполне вариантом. Плюсом данного метода становится независимость от подписки и правильной отписки для корректной сборки объекта через GC. Можно делать прототипы с вызовами определенных методов, а сами методы реализовывать сразу или потом — все свяжется автоматом и без проблем.
                      +1
                      поможем автору в стиме, активнее набегаем!:)
                        0
                        Да, функция для подписке на события не имеет аргументов, но собственно какие аргументы должна передавать кнопка?
                        Ну и принцип динамических кнопок я использую такой. У нас всё равно есть список, допусти итемов в инвентаре из которых генерируешь кнопки. Так же кнопки складываешь в список/словарь, в итоге находишь нажатую кнопку — получаешь доступ к итему.
                        А подписка на события теперь (т.к. UIButtonMessage теперь legacy) делается из кода так:
                        EventDelegate.Set(NGUITools.AddMissingComponent<UIEventTrigger>(prevButton.gameObject).onClick, PrevButtonClick);

                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                        Самое читаемое