среда, 3 февраля 2016 г.

Unity и его новый интерфейс

Сегодня почувствовал, что голова трещит от объектной модели и решил отвлечься на мелкое украшательство. А конкретно - на создание некоторых элементов интерфейса, которые заменять стандартные элементы, предоставляемые Unity. А поскольку от новой системы интерфейса Unity меня откровенно прет - решил попутно набросать небольшой гайд. Для зубров он будет абсолютно неинтересен, но начинающим разработчикам вполне может пригодится, а тем, кто с Unity незнаком - проиллюстрировать удобство работы. Сразу хочу предупредить, что я профессиональный зануда, поэтому рассказывать буду подробно :)
Итак я хочу создать кнопку, которая будет использоваться для отображения персонажа в списках. На кнопке должны слева направо выводится
- иконка пола (если он есть)
- имя персонажа
- иконка с куском цепи для рабов
В итоге должно получиться так:
Фон кнопки пока оставим стандартный, поскольку общий спрайт для элементов интерфейса - дело ответственное и я за него пока не брался :) Приступим.

Создание заготовки элемента

Создавать и отлаживать элементы интерфейса удобнее всего в отдельной тестовой сцене. Поэтому создадим новую сцену и разместим в ней Canvas для вывода интерфейса.

Добавим стандартную кнопку Юнайти как дочернюю к canvas.

Теперь удалим элемент Text, который создается автоматически и создадим вместо него собственный пустой объект. Так как этот объект является дочерним к canvas, компонент transform у него автоматически имеет тип rect, характерный для всех 2D объектов. Настроим его так, чтобы он всегда растягивался по размерам родительского элемента (кнопки) и как-то обзовем. Например CharacterSign.

К этому объекту мы будем цеплять элементы подписи - иконки и текст. У тех, кто уже работал с Юнайти может возникнуть вопрос - а почему не цеплять элементы сразу к кнопке. Отвечаю. Если вам захочется использовать готовые наработки в другом элементе интерфейса (например прицепить такую же подпись к большой иконке персонажа), то гораздо проще скопировать один элемент вместе с дочками, чем копировать дочек по одной (кнопка-то вам не нужна).
Теперь прицепим, наконец, наши элементы. К объекту CharacterSign добавляем дочерними объектами Image, Text и еще один Image. Сразу их обзываем так, чтобы было понятно где что.

Теперь нужно настроить размещение элементов и их поведение в плане изменения размеров. Советую поменять цвет основной кнопки на какой-нибудь яркий (я взял красный), чтобы хорошо видеть как дочерние объекты размещаются на ее фона.
Для иконки пола указываем ее размеры (16х16 у меня), поведения якоря (по вертикали центрируем, по горизонтали прижимаем влево) и отступ от левого края для красоты (я пока взял 5). Хочу обратить особое внимание на поля pivot. Они определяют где находится якорь нашего элемента. Значения (0, 0.5) означают, что якорь находится на левом краю, в центре по вертикали. Теперь когда Юнайти прижимает элемент влево, то он прижимает именно его левый край. Если оставить по умолчанию (0.5, 0.5), то по левому краю родителя будет выставлен не левый край дочки, а ее центр и иконка уедет сильно влево.

Теперь настроим текст. Он также прижимается влево, но отступ у него будет больше. 5 отступ иконки + 16 ширина иконки + 5 отступ от иконки = 26. Также добавим ему отступ от правого края, там будет место для иконки статуса. Вместо того чтобы центрировать элемент по вертикали мы скажем ему растягиваться до размеров родителя, а центрирование укажем уже для свойства текста. Заодно подсунем какой-нибудь приятный шрифт.
Для иконки статуса сделаем прижимание вправо по горизонтали (не забываем про pivot) и сделаем отступ от правого края в 5.
В итоге мы имеет такую вот заготовку для нашего элемента:

Можно переходить к коду.

Написание кода компонента

Для начала определимся с данными персонажа. У меня персонаж представлен классом amCharacter, который имеет примерно такой вид (я вырезал все, что не относится к нашему примеру):

public class amCharacter:ScriptableObject
{
    public enum Genders {any = -1, male, female}
    public enum SocialStatuses {any = -1, slave, freeman, noble}
 
    public string characterName;
    public Genders gender;
    public SocialStatuses socialStatus;
}

Сразу поясню, что значения -1 для пола и статуса используются в генераторе случайных персонажей и обозначают, что данный параметр должен быть сгенерирован случайно. Наследование от ScriptableObject сделано для сериализации при сохранении игры и для возможности создавать предопределенных персонажей в виде ассетов Юнайти.
Для нашего компонента создаем в ассетах стандартный C# скрипт, который назовем, например, amCharacterSign, такое же имя будет у класса нашего компонента. Сразу объявляем необходимые переменные.

using UnityEngine;
using UnityEngine.UI;
using System.Collections;

public class amCharacterSign : MonoBehaviour
{

    //Данные нашего персонажа
    [SerializeField]
    private amCharacter _currentChar;
    //Признак необходимости апдейта
    private bool _needUpdate;

    //Ссылки на элементы надписи
    public Image genderImage;
    public Text nameText;
    public Image statusImage;

    //Ссылки на сорцы иконок
    public Sprite maleIcon;
    public Sprite femaleIcon;
    public Sprite slaveIcon;

    public void Start()
    {
    }

    public void Update()
    {
    }
}

Упреждаю вопрос "почему ссылка на персонаж спрятана в приват, что за SerializeField и _needUpdate". Нам необходимо апдейтить надпись и иконки так, чтобы они были актуальны для текущего персонажа. Апдейтить их каждый кадр - плохая идея, лишняя растрата ресурсов. Поэтому необходимо отслеживать момент когда нам передали другого персонажа и только тогда апдейтить. Поэтому публично назначение персонажа мы реализуем через property, а [SerializeField] позволит нам видеть значение переменной в редакторе во время отладки:

    public amCharacter currentChar
    {
        get
        {
            return _currentChar;
        }
        set
        {
            _currentChar = value;
            _needUpdate = true;
        }
    }

Осталось написать код апдейта нашего элемента

    public void Update()
    {
        //Если апдейт не нужен - возврат
        if(!_needUpdate) return;
        //Назначаем иконку пола
        if(_currentChar.gender == amCharacter.Genders.male)
        {
            genderImage.sprite = maleIcon;
        }
        else if(_currentChar.gender == amCharacter.Genders.female)
        {
            genderImage.sprite = femaleIcon;
        }
        else
        {
            genderImage.sprite = null;
        }
        //Прописываем имя
        nameText.text = _currentChar.characterName;
        //Назначаем иконку статуса
        if(_currentChar.socialStatus == amCharacter.SocialStatuses.slave)
        {
            statusImage.sprite = slaveIcon;
        }
        else
        {
            statusImage.sprite = null;
        }
        //Отмечаем, что апдейт сделан
        _needUpdate = false;
    }
}

Код готов. Возвращаемся в Юнайти и перетаскиваем скрипт на нашу кнопку. Юнайти послушно отображает нам переменные класса на панели и мы заполняем их, просто перетаскивая нужные объекты. Элементы надписи перетаскиваем из сцены, заготовки иконок - из ассетов. Сразу извиняюсь за внешний вид иконок, это так, заготовки по быстрому сделанные в InkScape (да, я копираст и не тырю ПО :) )

Наш объект готов, осталось проверить его в бою. Возвращаем цвет кнопки, переименовываем ее в CharacterSignButton и перетаскиваем кнопку целиком в окно ассетов, это создаст нам префаб (шаблон) кнопки для использования в дальнейшем.

Проверка боем

Напишем небольшой скрипт для сцены, который будет на старте генерить случайных персонажей и выводить на сцену кнопки для них. Для этого я использую свой генератор случайных персонажей, реализацию которого оставим пока за рамками. Важно, что он возвращает нам готовый экземпляр класса amCharacter. Код скрипта:

using UnityEngine;
using UnityEngine.UI;
using System.Collections;

public class CharSignTester : MonoBehaviour
{
    //Ссылка на наш префаб кнопки
    public GameObject buttonPrefab;

    public amCharacterGenerator generator;

    void Start()
    {
        for (int i = 0; i < 5; i++)
        {

            //Создаем случайного персонажа
            amCharacter newchar = generator.CreateRandomCharacter(amCharacter.Cultures.any,
                amCharacter.Genders.any, amCharacter.SocialStatuses.any, amCharacter.AgeGroups.any);
            //Создаем экземпляр кнопки
            GameObject newbut = Instantiate<GameObject>(buttonPrefab);
            //Делаем его дочерним к canvas
            newbut.transform.SetParent(this.transform);
            //Получаем наш компонент
            amCharacterSign sign = newbut.GetComponent<amCharacterSign>();
            //Передаем персонажа
            sign.currentChar = newchar;
        }
    }

    void Update()
    {
    }
}

Скрипт прицепим к canvas на сцене и запустим. Вуаля!


Статья получилась довольно длинной, но реально это делается за пять минут. Надеюсь я не зря превратил их в сорок и вам это было полезно или хотя бы интересно :)

Комментариев нет:

Отправить комментарий