Unity: знакомство со Scriptable Objects

Автор оригинала: Jeff Fisher
  • Перевод
image

В этом туториале вы узнаете, как создавать и использовать Scriptable Objects в Unity. Scriptable Objects помогут усовершенствовать ваш рабочий процесс, снизить объём занимаемой памяти и даже позволят разделить архитектуру кода.

Согласно документации Unity, ScriptableObject — это код класса, позволяющий создавать в игре Scriptable Objects для хранения больших объёмов общих данных, не зависящих от экземпляров скриптов.

Существует множество причин для использования Scriptable Objects в Unity. Они могут снизить объём используемой под каждый дополнительный префаб памяти, потому что по своей сути Scriptable Object следуют паттерну разработки Flyweight.

Ещё одно преимущество Scriptable Objects, которое будет основной темой этого туториала, заключается в их использовании для удобной пересылки данных. Мы рассмотрим это свойство на примере создания лавки торговца мечами, в которой будут отображаться параметры, цены и описания различных мечей.

Примечание: в этом туториале подразумевается, что вы знакомы с редактором Unity. Вы должны разбираться в том, как править код в редакторе кода и иметь базовые знания C#. Если вам нужно повысить свои навыки Unity, изучите другие туториалы по Unity.

Приступаем к работе


Начнём с загрузки материалов, которые нам потребуются.

Распакуйте скачанный файл в удобное для вас место и откройте в Unity проект Scriptable Object Tutorial-Starter.

Вы должны увидеть следующую папку, созданную как часть заготовки проекта:


  • _Setup: для туториала эта папка не понадобится.
  • Scenes: содержит сцену Sword Merchant, которой мы будем заниматься в течение всего туториала. Откройте эту сцену.
  • Scripts: пока здесь находится только один скрипт, но в течение туториала мы создадим новые.
  • Sword Icons: содержит неподвижные изображения отдельных мечей.
  • Sword Prefabs: содержит префабы всех мечей в сцене Sword Merchant.

Создание Scriptable Object


Для начала перейдите в сцену Sword Merchant. Она должна выглядеть следующим образом:


Подготовка Scriptable Object


Настало время для создания первого Scriptable Object!

В папке Scripts создайте новый скрипт под названием SwordData. Этот класс будет использоваться как контейнер для всех данных мечей, отображаемых в лавке торговца мечами.

Внутри этого класса начнём с наследования из ScriptableObject вместо MonoBehaviour:

public class SwordData : ScriptableObject
{

}

Это действие сообщает Unity, что мы по-прежнему хотим использовать возможности и методы Unity, как обычный MonoBehaviour, но нам больше не нужно прикреплять этот скрипт к GameObject. Вместо этого он будет обрабатываться как любой обычный ассет, который можно создавать аналогично созданию префаба, сцены или материала.

Заполним скрипт сериализированными полями, в которых будут содержаться все данные, соответствующие информации, отображаемой в UI Sword Merchant.

public class SwordData : ScriptableObject
{
    [SerializeField]
    private string swordName;
    [SerializeField]
    private string description;
    [SerializeField]
    private Sprite icon;
    [SerializeField]
    private int goldCost;
    [SerializeField]
    private int attackDamage;
}

  • swordName: string, в котором будет храниться название меча.
  • description: string, в котором будет храниться описание меча.
  • icon: спрайт, в котором будет содержаться значок меча.
  • goldCost: int для хранения стоимости меча в золоте.
  • attackDamage: int для хранения урона при атаке мечом.

Примечание: SerializeField

В Unity атрибут SerializeField позволяет иметь частные переменные скрипта, доступные в Инспекторе. Он позволит задавать значения в редакторе, не предоставляя доступ к переменной из других скриптов.

Каждому мечу потребуется собственная уникальная реализация Scriptable Object SwordData. Но прежде чем мы сможем создать эти реализации, нам нужно добавить Scriptable Object в Asset Menu.

Добавим наш Scriptable Object в Asset Menu, добавив к классу SwordData следующий атрибут:

[CreateAssetMenu(fileName = "New SwordData", menuName = "Sword Data", order = 51)]
public class SwordData : ScriptableObject

  • fileName: имя по умолчанию при создании ассета.
  • menuName: имя ассета, отображаемое в Asset Menu.
  • order: место размещения ассета в Asset Menu. Unity разделяет ассеты на подгруппы с множителем 50. То есть значение 51 поместит новый ассет во вторую группу Asset Menu.

Если всё сделано правильно, то вы сможете зайти в Assets >> Create и увидеть в меню новый ассет Sword Data. Он должен быть расположен во второй группе под ассетом Folder:


Также можно нажать правой клавишей мыши в окне Project и тоже увидеть новый ассет Sword Data:


Добавление данных


Упорядочим проект, создав в папке Scripts папку с названием Scriptable Objects, а внутри этой папки — ещё одну папку с названием Sword Data.

Внутри только что созданной папки Sword Data folder создадим наш первый ассет Sword Data.

У нового ассета Sword Data по-прежнему должно быть указанное ранее имя по умолчанию fileName. Выберите ассет и дублируйте его шесть раз (Ctrl/Cmd + D), чтобы создать семь ассетов Sword Data, по одному для каждого из мечей. Теперь переименуйте каждый ассет в соответствии с префабами:


Нажмите на первый ассет Sword Data в папке Sword Data и посмотрите на окно Inspector:


Здесь мы видим ассет, в котором будет храниться информация о конкретном мече. Заполните информацию для каждого меча. Постарайтесь дать им уникальное описание, стоимость в золоте и урон при атаке. В поле Icon Sprite используйте соответствующие спрайты, расположенные в папке Sword Icons:


Поздравляю! Вы создали Scriptable Object и настроили с помощью этого Scriptable Object несколько ассетов.

Использование Scriptable Object


Теперь мы приступим к получению данных из этих Scriptable Objects.

Во-первых, нам нужно добавить несколько публичных методов-получателей (getter methods), чтобы другие скрипты могли получать доступ к частным полям внутри Scriptable Object. Откроем SwordData.cs и допишем под добавленными ранее полями следующее:

    public string SwordName
    {
        get
        {
            return swordName;
        }
    }

    public string Description
    {
        get
        {
            return description;
        }
    }

    public Sprite Icon
    {
        get
        {
            return icon;
        }
    }

    public int GoldCost
    {
        get
        {
            return goldCost;
        }
    }

    public int AttackDamage
    {
        get
        {
            return attackDamage;
        }
    }

Откроем Sword.cs и добавим следующий код:

    [SerializeField]
    private SwordData swordData; // 1

    private void OnMouseDown() // 2
    {
        Debug.Log(swordData.name); // 3
        Debug.Log(swordData.Description); // 3
        Debug.Log(swordData.Icon.name); // 3
        Debug.Log(swordData.GoldCost); // 3
        Debug.Log(swordData.AttackDamage); // 3
    }

Вот, что мы добавили этим кодом:

  1. Контейнер данных для данных этого меча.
  2. OnMouseDown — это встроенная функция MonoBehaviour, вызываемая при нажатии пользователем левой клавиши мыши.
  3. Примеры того, как получать данные из нашего ассета Scriptable Object asset

Вернёмся в Unity и перейдём к окну Hierarchy. Выберите игровой объект 1_Longsword в префабе меча. Добавьте соответствующий ассет 1_Longsword Data переменной Sword Data скрипта Sword.cs в окне Inspector:


Нажмите на Play (Ctrl/Cmd + P) в редакторе Unity, а затем щёлкните самый левый меч:


В консоль должна выводиться информация, напоминающие данные, переданные из ассета Sword Data.

Scriptable Objects позволяют с лёгкостью заменять эти данные. Попробуйте вставлять разные Sword Data Scriptable Object в поле Sword Data меча.

Scriptable Objects на основе событий


Итак, мы создали Scriptable Object, и вы увидели, как можно получать доступ к его данным внутри игры. Но нам всё равно нужно интегрировать Sword Data с UI!

Для этого можно использовать быстрый и грязный паттерн Singleton. Однако мы теперь обладаем другими возможностями…

…а именно Scriptable Objects! Воспользуемся ими для создания чистого и аккуратно разделённого на части кода.

В этом разделе вы узнаете, как создавать Game Events с помощью класса UnityEvent.

Game Events и Listeners


В папке Scripts создайте два скрипта: GameEvent.cs и GameEventListener.cs. Они зависят друг от друга, поэтому чтобы избавиться от ошибок, нужно создать оба.

using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(fileName = "New Game Event", menuName = "Game Event", order = 52)] // 1
public class GameEvent : ScriptableObject // 2
{
    private List<GameEventListener> listeners = new List<GameEventListener>(); // 3

    public void Raise() // 4
    {
        for (int i = listeners.Count - 1; i >= 0; i--) // 5
        {
            listeners[i].OnEventRaised(); // 6
        }
    }

    public void RegisterListener(GameEventListener listener) // 7
    {
        listeners.Add(listener);
    }

    public void UnregisterListener(GameEventListener listener) // 8
    {
        listeners.Remove(listener);
    }
}

Вот что делает приведённый выше код:

  1. Добавляет GameEvent в качестве ассета в Asset Menu.
  2. GameEvent является Scriptable Object, поэтому должен наследоваться от ScriptableObject.
  3. Список GameEventListeners, которые будут подписаны на GameEvent.
  4. Метод для вызова всех подписчиков GameEvent.
  5. Последний подписанный GameEventListener будет первым вызываемым (последним пришёл, первым вышел).
  6. Вызов каждого UnityEvent GameEventListeners.
  7. Метод, позволяющий GameEventListeners подписаться на этот GameEvent.
  8. Метод, позволяющий GameEventListeners отписываться от этого GameEvent.


using UnityEngine;
using UnityEngine.Events; // 1

public class GameEventListener : MonoBehaviour
{
    [SerializeField]
    private GameEvent gameEvent; // 2
    [SerializeField]
    private UnityEvent response; // 3

    private void OnEnable() // 4
    {
        gameEvent.RegisterListener(this);
    }

    private void OnDisable() // 5
    {
        gameEvent.UnregisterListener(this);
    }

    public void OnEventRaised() // 6
    {
        response.Invoke();
    }
}

В показанном выше коде происходит дальнейшее развитие проекта:

  1. Требование использования класса UnityEvent.
  2. GameEvent, на который будет подписан этот GameEventListener.
  3. Отклик UnityEvent, который будет вызываться, при генерации событием GameEvent этого GameEventListener.
  4. Привязка GameEvent к GameEventListener, когда этот GameObject включен.
  5. Отвязка GameEvent от GameEventListener, когда этот GameObject отключен.
  6. Вызывается при генерации GameEvent, приводящей к вызову слушателем GameEventListener события UnityEvent.

Сложно? Ничего, со временем разберётесь!

Подготовка редактора


Вернитесь в редактор Unity и создайте новую папку Game Events в Scripts >> ScriptableObjects. Затем создайте семь Game Events из Asset Menu, как мы это делали для каждого ассета Sword Data. Разместите их в новой папке Game Events.


Замените код внутри скрипта Sword.cs следующими строками:

    [SerializeField]
    private GameEvent OnSwordSelected; // 1

    private void OnMouseDown()
    {
        OnSwordSelected.Raise(); // 2
    }

Этот код добавляет в лавку торговца мечами две возможности:

  1. Генерацию Game Event при выборе меча.
  2. Генерацию события при нажатии на меч.

Сохраните скрипт. Теперь в каждом GameObject меча в Hierarchy подключите соответствующее событие OnSwordSelected.


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

Интеграция с UI


Теперь нужно заставить работать UI. Наша цель заключается в отображении соответствующих данных Sword Data при нажатии на каждый меч.

Ссылки UI


Перед обновлением UI необходимо получить ссылку на каждый элемент UI. Начнём с создания нового скрипта под названием SwordMerchant.cs и добавления в этот новый скрипт следующего кода:

using UnityEngine;
using UnityEngine.UI;

public class SwordMerchant : MonoBehaviour
{
    [SerializeField]
    private Text swordName; // 1
    [SerializeField]
    private Text description; // 2
    [SerializeField]
    private Image icon; // 3
    [SerializeField]
    private Text goldCost; // 4
    [SerializeField]
    private Text attackDamage; // 5
}

С помощью этого кода мы добавили следующее:

  1. Ссылку на компонент Text игрового объекта NameText.
  2. Ссылку на компонент Text игрового объекта DescriptionText.
  3. Ссылку на компонент Image игрового объекта Sword_Icon.
  4. Ссылку на компонент Text игрового объекта GoldText.
  5. Ссылку на компонент Text игрового объекта AttackText.

Указанные выше игровые объекты расположены в SwordMerchantCanvas >> SwordMerchantPanel окна Hierarchy. Добавьте скрипт к GameObject SwordMerchantCanvas, а затем настройте все ссылки:


Слушатели и отклики UI


У всех мечей есть событие, на которое может подписаться UI с помощью скрипта GameEventListener. Добавьте GameEventListener для каждого события OnSwordSelected к GameObject SwordMerchantCanvas:


Как вы могли заметить, у нашего Game Event Listener есть два поля: событие Game Event, которое он слушает, и отклик, который вызывается при генерации Game Event.

В нашем случае отклик будет обновлять UI. Добавьте в скрипт SwordMerchant.cs следующий метод:

    public void UpdateDisplayUI(SwordData swordData)
    {
        swordName.text = swordData.SwordName;
        description.text = swordData.Description;
        icon.sprite = swordData.Icon;
        goldCost.text = swordData.GoldCost.ToString();
        attackDamage.text = swordData.AttackDamage.ToString();
    }

Этот метод получает ассет Sword Data, а затем обновляет каждое поле UI значением соответствующего поля Sword Data. Заметьте, что GoldCost и AttackDamage возвращают int, поэтому для текста нужно преобразовать его в string.

С помощью нашего нового метода мы можем добавить отклик к каждому GameEventListener.

Для каждого добавляемого отклика необходимо в качестве значения поля None (Object) ссылку на наш игровой объект SwordMerchantCanvas. После этого выберем SwordMerchant.UpdateDisplayUI из раскрывающегося меню справа от раскрывающегося списка Runtime Only.

Будьте внимательны и используйте правильный ассет Sword Data для каждого события OnSwordSelected.


Теперь мы можем запустить игру, нажать на меч и увидеть, что UI обновился соответствующим образом!


Поскольку мы используем Game Events, то можно просто SwordMerchantCanvas, и всё по-прежнему будет работать, только без UI. Это значит, что префабы мечей отделены от SwordMerchantCanvas.

Куда двигаться дальше?


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

Если вы хотите двигаться дальше, то попытайтесь сделать так, чтобы каждый меч воспроизводил собственный звук. Попробуйте расширить Scriptable Object Sword Data и слушание событий OnSwordSelected.

Хотите больше узнать о Unity? Посмотрите нашу серию видео по Unity или прочитайте туториалы по Unity.
Поддержать автора
Поделиться публикацией

Похожие публикации

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

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

    +2

    Так и не понял, зачем нужны ScriptableObject в части про события. А картинка с одним и тем же листенером, навешенным семь раз с разным аргументом, вообще пугает до колик.

      0
      Присоединяюсь про листенеры, ужаснуло просто
        +1
        У меня аналогичные ощущения.
        Если не видели, очень рекомендую выступление на эту же тему от Ryan Hipple: Unite Austin 2017 — Game Architecture with Scriptable Objects
          0

          Спасибо, посмотрю.

        +2
        а если у меня в ммо 5000 итемов, я что каждую буду так создавать ручками? бред же.
        Дефайн 5000 итемов через тхт файл удобнее для дальнейших массовых правок либо добавлений новых параметров
          0
          А если каждому предмету понадобится привязать отдельный звук, например, удара меча? Или просто какой-нибудь префаб связать.
            0
            Не согласен. Отличным примером грамотного использования подобной архитектуры являются движок Warcraft 3 и Starcraft 1/2. Да никто и не мешает задавать значения в автоматическом режиме через массив данных.
              0
              Для массовых правок можно написать кастомный редактор, и изменять ScriptableObject прямо в Unity Editor. Можно сделать, чтобы редактору можно было скормить txt-файл, по которому внесутся нужные изменения. Но, как правильно написали выше, с событиями в статье – полный ужас. Проще было бы реализовать следующее: Sword Merchant каждому своему Sword передаёт ссылку на себя в виде ссылки на ISwordClickListener; Sword по клику (при наличии слушателя) вызывает метод ISwordClickListener.OnSwordClicked, передавая ссылку на свой SwordData. В дальнейшем, если нужны будут разные виды интерфейсов (разные торговцы), можно будет сделать у Sword Merchant свойство со ссылкой на объект, реализующий ISwordClickListener для конкретного случая и так далее.
              +1

              Хранение параметров айтемов в SO это нормально. Даже если их будет гораздо больше 5000, нужно просто сделать удобный кастомный Editor для этого.
              Но с эвентами реально просто дичь. С таким подходом до чистого кода как до луны. И что самое печальное, многие бегинеры могут последовать этому примеру и получить кашу в голове.
              В данном случае нужен всего один статический евент с параметром и тогда была бы реально полезная статья.

                0
                Будьте внимательны и используйте правильный ассет Sword Data для каждого события OnSwordSelected.

                Гораздо проще создать одно событие и передавать Sword Data через аргумент.
                Иначе, как уже писали выше для добавления нового элемента придется писать много шаблонного кода.

                  0
                  Планируете переписать последний кусок? С удовольствием посмотрел бы реализацию с нормальной архитектурой

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

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