Добрый день, Хабр. В эфире снова я, Илья Кудинов, QA-инженер компании Badoo. В свободное от основной работы время я занимаюсь разработкой игрушек на Unity 3D и решил в качестве эксперимента написать статью об одной из проблем, с которой столкнулась наша команда. Я являюсь основным разработчиком, и наш гейм-дизайнер в «гробу видал» копание в моем коде с какой бы то ни было целью (разделение труда — одно из величайших достижений цивилизации), значит, моя обязанность — предоставить ему все необходимые рычаги управления и настройки геймплея в виде удобных визуальных интерфейсов. Благо Unity сам по себе имеет достаточно удобные (кхе-кхе) готовые интерфейсы и ряд методов их расширения. И сегодня я расскажу вам о некоторых приемах, которые делают жизнь нашего гейм-дизайнера проще и удобнее, а мне позволяют не биться головой о клавиатуру после каждого его запроса. Надеюсь, они смогут помочь каким-нибудь начинающим командам или тем, кто просто упустил эти моменты при изучении Unity.

    Сразу скажу, что наша команда все еще активно учится и развивается, хоть мы уже и выпустили дебютную игру. И если «дедлайны не горят», то я предпочитаю разбираться в каких-то вещах сам, а не обращаться к экспертам и различным best practices. Поэтому что-то из рассказанного мною может оказаться не оптимальным или банальным. Буду очень рад, если в таких случаях вы подскажете мне более удобные решения в комментариях и личных сообщениях. Ну и в целом информация здесь скорее базового уровня.

    Код для Unity я пишу исключительно на C#, поэтому все выкладки в статье будут именно на этом языке.

Singleton-объекты


    В архитектуре любой игры зачастую предусмотрены различные классы менеджеров, фабрик и хелперов, которым не нужны физические представления в игровом мире. В идеальном случае можно было бы реализовать их классами со статическими методами, не создавать в сцене никаких GameObject для их работы и спокойно пользоваться кодом вида GameController.MakeEverybodyHappy(). Однако у этого подхода есть два существенных минуса в нашем случае:
  • для изменения каких-либо параметров гейм-дизайнерам придется лазить напрямую в код, а они это очень не любят;
  • будет сложнее использовать ссылки на любые ассеты в Unity (префабы, текстуры и т.д.), так как придется загружать их через Resources.load(), а такой код поддерживать существенно труднее, чем те ссылки, которые можно создавать через интерфейс Unity.

    Решение проблемы? Наследовать ваши классы от MonoBehaviour и создавать для каждого из них объект в сцене. Минусы этого подхода? При обращении к этим объектам придется пользоваться извращенными вызовами типа FindObjectOfType<GameController>() или даже GameObject.Find(”GameController”).GetComponent<GameController>(). Ни один уважающий себя разработчик делать так на каждом шагу не захочет. Дополнительные проблемы и возможные ошибки начинают возникать при необходимости переносить такие объекты между сценами или при возникновении нескольких объектов с таким классом (либо их полном отсутствии).
    Значит, нам нужен какой-то механизм, который позволит получать интересующий нас объект без какой-либо магии и контролировать, что на момент каждого обращения в нашей сцене будет один и ровно один объект этого класса.
    Наше решение выглядит следующим образом («костяк» класса я когда-то давно нашел на просторах интернета и слегка доработал для собственного удобства):

using UnityEngine;
public class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
    private static T _instance;

    public void Awake()
    {
        // Если в сцене уже есть объект с таким компонентом, то
        // он пропишет себя в _instance при инициализации
        if (!_instance) {
            _instance = gameObject.GetComponent<T>();    
        } else {
            Debug.LogError("[Singleton] Second instance of '" + typeof (T) + "' created!");
        }
    }

    public static T Instance
    {
        get 
        {
            if (_instance == null) {
                _instance = (T) FindObjectOfType(typeof(T));

                if (FindObjectsOfType(typeof(T)).Length > 1) {
                    Debug.LogError("[Singleton] multiple instances of '" + typeof (T) + "' found!");
                }

                if (_instance == null) {
                    // Если в сцене объектов с этим классом нет - создаём
                    // новый GameObject и лепим ему наш компонент
                    GameObject singleton = new GameObject();
                    _instance = singleton.AddComponent<T>();
                    singleton.name = "(singleton) " + typeof(T).ToString();
                    DontDestroyOnLoad(singleton);
                    Debug.Log("[Singleton] An instance of '" + typeof(T) + "' was created: " + singleton);
                } else {
                    Debug.Log("[Singleton] Using instance of '" + typeof(T) + "': " + _instance.gameObject.name);
                }
            }        
            return _instance;
        }
    }
}


    Как этим пользоваться? Наследуем свои классы от Singleton, указав самого себя в шаблоне:
 public class GameController : Singleton<GameController>
    В дальнейшем мы сможем обращаться к полям и методам нашего класса как GameController.Instance.MakeEverybodyHappy() в любом месте кода. Чтобы создавать ссылки на ассеты, достаточно добавить этот компонент на любой объект в сцене и настраивать его привычным образом (и сохранять в префаб для верности). Мы используем объект Library в корне сцены для хранения всех Singleton-классов, настройка которых может понадобиться нашему гейм-дизайнеру, чтоб ему не приходилось их искать по всей сцене.

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


Кастомные поля инспектора класса


    Итак, гейм-дизайнер получил свои визуальные интерфейсы и начал творить геймплей. И достаточно быстро начал ненавидеть меня, невинного разработчика, за все неудобства, которые на него наваливаются. Массив сериализованных объектов? Многомерные массивы? Почему это все настолько неудобно настраивать? Как бы вы ни старались сделать универсальную и расширяемую систему на стороне кода, ваш гейм-дизайнер предпочел бы видеть минимальное количество выпадающих списков и уж тем более массивов элементов с названиями вроде Element 73. Но разве мы можем что-то с этим поделать?
    На самом деле можем. Предположим, в нашей игре появляются настройки сложности, и в данный момент вариантов три. Но, возможно, станет больше. Поэтому мы смотрим в будущее и для упрощения дальнейшего ВОЗМОЖНОГО увеличения количества сложностей создаем вот такой замечательный класс «зависящих-от-сложности-интов» и заменяем в нужных местах обычные инты на него:

[System.Serializable]
public class DifDepInt 
{
    public int[] values = {0, 0, 0};

    static public implicit operator int (DifDepInt val)
    {
        return val.Get();
    }

    public int Get()
    {
        return values[GameConfig.Instance.difficulty];
    }
}


    DifDep означает Difficulty Dependant. Конечно, с точки зрения архитектуры лучше было бы сделать вместо этого специфического класса шаблон DifDep<T>, принимающий любые типы данных, но, к сожалению, я не нашел способа создания кастомных полей редактора для шаблонов.

    Итак, мы довольны собой, мы получили возможность без особых трудов ввести в игру варьирующиеся параметры. Но наш гейм-дизайнер, которому надо это все настраивать, почему-то недоволен. Надо бы спросить его, что происходит… Ах вот оно что!


    Да, однозначно, это не очень интуитивно и удобно. Давайте сделаем так, чтобы все выглядело иначе! Для этого мы воспользуемся вышеназванной «плюшкой» Unity — возможностью создавать кастомных инспекторов для отображения различных классов. Это достаточно хитрая система, позволяющая вам делать практически все что угодно, но в ней не так просто разобраться с первого взгляда (с самого начала она меня отпугнула, и поэтому какое-то время мы-таки страдали со стандартным инспектором, но в конце концов момент истины настал).
Итак, мы пишем следующий код:

#if UNITY_EDITOR
using UnityEditor;

[CustomPropertyDrawer(typeof(DifDepInt))]
public class DifDepIntDrawer : PropertyDrawer 
{
    int difCount = 3;

    public override void OnGUI (Rect position, SerializedProperty property, GUIContent label) 
    {
        EditorGUI.BeginProperty(position, label, property);

        Rect contentPosition = EditorGUI.PrefixLabel(position, label);
        contentPosition.width *= 1 / difCount;
        float width = contentPosition.width;

        SerializedProperty values = property.FindPropertyRelative ("values");

        for (int i = 0; i < difCount; i++) {
            EditorGUI.PropertyField (contentPosition, values.GetArrayElementAtIndex(i), GUIContent.none);
            contentPosition.x += width;
       }

        EditorGUI.EndProperty();
    }
}
#endif


    Давайте разберемся, что тут происходит. Директива компилятора #if UNITY_EDITOR сообщает Unity, что она должна компилировать этот класс только во время разработки в редакторе. В противном случае она будет пытаться собрать этот код при сборке билда игры, а модуль UnityEditor там недоступен целиком, и это может вызвать сбивающие с толку ошибки.
    [CustomPropertyDrawer(typeof(DifDepInt))] говорит Unity, что для отрисовки полей классов типа DifDepInt ей нужно использовать предоставленный ниже код вместо стандартного. Таких директив можно указать сколько угодно подряд для всех DifDep-классов, которые вам понадобятся — сам код кастомного редактора написан так, что примет любые классы, имеющие в себе массив элементов под названием values, поэтому этот класс у меня обслуживает и int, и float, и даже Sprite и GameObject.
    Мы перегружаем метод OnGUI(), который и занимается отрисовкой области редактирования поля в инспекторе. Unity вызывает его иногда несколько раз за кадр — это нужно иметь в виду. Не забываем оставлять методы EditorGUI.BeginProperty() и EditorGUI.EndProperty(), без них корректно работать ваш код не будет.
    Остальной код достаточно интуитивно понятен, если заглянуть в документацию Unity. Вместо магии с contentPosition можно использовать методы отрисовки из класса EditorGUILayout, а не EditorGUI, однако они не всегда ведут себя очевидным образом и в некоторых плохих случаях разбираться с ними себе дороже.
    Ради чего же мы этим занимались? Смотрите, какая красота!


    Это однозначно удобнее того, что было. Возможности, которые дает подобный функционал, практически безграничны — вы можете отображать самые сложные структуры максимально удобным для редактирования способом. Но не думайте, что гейм-дизайнер будет вам благодарен. Он примет это как должное, я вам гарантирую (:

Кастомные редакторы целого класса


    Окей, красиво рисовать отдельные поля мы научились, а можем ли мы рисовать что-то, что охватывает весь класс? Конечно же да! Например, параметры всех грейдов отдельно взятого вида оружия мы задаем вот так:


    Помимо редактирования полей здесь присутствует еще и калькулятор, значения в котором изменяются автоматически при изменении параметров оружия (на самом деле они read-only, вид инпутов они имеют только для консистентности и удобства выравнивания).
    Как же сделать что-то подобное? Очень просто и схоже с тем, что мы делали до этого! Продемонстрирую на простом примере— добавлении простенького калькулятора DPS перед всеми остальными полями в классе поведения монстра:

#if UNITY_EDITOR
using UnityEditor;

[CustomEditor(typeof(EnemyBehaviour), true)]
public class EnemyCalculatorDrawer : Editor 
{
    public override void OnInspectorGUI() {
        EnemyBehaviour enemy = (EnemyBehaviour)target;
        float dps1, dps20;

        dps1 = enemy.damageLeveling.Get(1) / enemy.getAttackDelay(1);
        dps20 = enemy.damageLeveling.Get(20) / enemy.getAttackDelay(20);

        GUIStyle myStyle = new GUIStyle ();
        myStyle.richText = true;
        myStyle.padding.left = 50;
        EditorGUILayout.LabelField("<b>Calculator</b>", myStyle);
        EditorGUILayout.LabelField("DPS on level 1: " + dps1.ToString("0.00"), myStyle);
        EditorGUILayout.LabelField("DPS on level 20: " + dps20.ToString("0.00"), myStyle);
        EditorGUILayout.Separator();

        base.OnInspectorGUI();
    }
}
#endif


    Ситуация очень похожая: сначала мы сообщаем Unity о желании заменить отрисовщик для этого класса с помощью директивы [CustomEditor(typeof(EnemyBehaviour), true)]. Затем переопределяем метод OnInspectorGUI() (да, в этот раз не OnGUI(), потому что разработчик должен страдать), пишем в нем свою кастомную логику (унаследованное от класса Editor поле под названием target содержит в себе ссылку на отображаемый объект как на Object) и затем вызываем base.OnInspectorGUI(), чтобы Unity отрисовал все остальные поля так же, как и обычно. GUIStyle позволяет нам изменять внешний вид отображаемых данных. В этом случае я использовал методы из EditorGUILayout просто потому, что здесь совершенно не надо было беспокоиться о выравненном позиционировании.
    Итог же выглядит так:

    Соответственно, таким образом можно отрисовывать в инспекторе все что угодно, хоть графики зависимости урона и выживаемости от уровня и этапа игры.

Всякие мелочи


    Конечно, можно делать огромное количество других вещей, чтобы спасти глаза и мозг ваших коллег. Unity предлагает целый набор директив для того, чтобы превратить простыню public-полей в структурированное целое. Самая важная из них, это, конечно, [HideInInspector], которая позволяет скрыть public-поле из инспектора. И больше не нужно вопить: «Пожалуйста, не трогайте эти галочки, они служебные!», и затем все равно часами разбираться, почему все монстры внезапно начали ходить задом наперед. Помимо этого есть еще приятные вещи вроде [Header(«Stats»)], которые позволяют отображать аккуратный заголовок перед блоком полей, и [Space], который просто делает небольшой отступ между полями, помогая разбивать их на смысловые группы. Все три эти директивы нужно писать непосредственно перед объявлением public-поля (если вы поставите [Header()] перед приватным полем, то ругаться Unity не станет, но никакого заголовка не отобразит).
    Небольшая подсказка: если ваш сериализуемый объект имеет в себе string-поле под названием name, то когда вы засовываете несколько таких объектов в публичный массив, его “имя” будет отображаться вместо неинтуитивного Element X в инспекторе.
    И ещё один полезный совет: даже если какой-то настраиваемый объект лежит у вас в сцене и является единственным представителем своего рода, все равно имеет смысл сделать из него префаб. Тогда, в случае совместной работы над проектом, не произойдут конфликты из-за одновременного редактирования сцены: правки, внесённые в инстанс префаба и применённые с помощью кнопки Apply, никак не аффектят файл сцены.

    Любой проект, над которым трудится более одного человека, заставляет своих участников чем-то жертвовать ради других, особенно когда область деятельности каждого из них очень сильно отличается. Разработка компьютерных игр — дело для специалистов из множества разных областей. В небольшой сплоченной команде долг каждого стараться все делать для того, чтобы минимизировать суммарные жертвы. А значит, потраченные пара часов для изучения кастомных редакторов или еще каких-либо приемов, которые кажутся не очень важными с точки зрения разработки кода — замечательное вливание в ваш проект, которое способно спасти не один час работы и миллионы нервных клеток ваших коллег. Товарищи разработчики-программисты, давайте жить дружно с теми, для кого ваш код все равно что поэмы на традиционном китайском языке для вас.

    Товарищи разработчики-программисты, в совершенстве владеющие традиционным китайским языком, — глубокий поклон вам и извинения за такие предрассудки.
Поделиться с друзьями
-->

Комментарии (65)


  1. inborn_killer
    05.08.2016 15:47
    +1

    А не думали для этих нужд заюзать ScriptableObject вместо MonoBehavior? Я не гуру Юнити и только недавно начал постигать полезность SO, но по-моему, если нужно сделать что-то, для чего не нужно визуальное отображение (т.е. как раз таки настройки чего-либо), SO подойдут в самый раз.


    1. Relz
      05.08.2016 15:55

      Тут как раз возникает проблема невозможности редактирования public-полей и констант ScriptableObject без открывания кода. То есть такие классы смогут обрабатывать свою логику и хранить какие-то данные, что удобно — но гейм-дизайнер не сможет их настраивать из редактора Unity.
      Можно было бы использовать ScriptableObject для тех объектов, настройкой которых гейм-дизайнер занимать не будет — но тогда у нас будут две практически одинаковые сущности (SO и синглтоны), которые нужно будет поддерживать отдельно — я принял решение упростить эту систему. Может быть, есть решение оптимальнее, но я пока что его не придумал (:


      1. inborn_killer
        05.08.2016 16:28

        Почему же нельзя редактировать поля без открывания кода? Наследуемся от SO, заводим нужные поля, создаём ассет с этим SO, жмём по ассету, инспектор показывает поля. И не нужно создавать лишние GameObject'ы, которые, например, меня очень сильно раздражают и травмируют мою неокрепшую психику.

        Вот, например...


        1. Relz
          05.08.2016 16:31

          Любопытно. Спасибо, надо покопаться!


        1. Ichimitsu
          05.08.2016 18:37
          +1

          А вы лучше напишите статью, как вы рендерите подобные деревья? через UnityEditor, или через GL. Вопрос занимательный весьма)


          1. inborn_killer
            05.08.2016 19:19
            +1

            Напишу, когда доделаю, чтобы работали, собственно, сами деревья =)) А то я начал, и так увлёкся Editor'ом, что всё это работает пока что только в теории.
            Но вкратце, ноды рисуются при помощи GUI.Window, а кривые — Handles.DrawBezier. Всё вот так вот просто =)


            1. Ichimitsu
              06.08.2016 20:06
              +1

              Ясно, тогда вам будет полезна инфа, что как-то только у вас размеры вылезут за 2-3 экрана подобного дерева, начнутся просто дичайшие лаги. Я потом и спросил, потому что мы сейчас ищем способ кастом рендеринга виджетов, через тот же GL.


              1. aGosh
                07.08.2016 10:46
                +1

                Возможно тогда вам следует для своего редактора (если он сложный и лагает) использовать встроенный в юнити хромиум. И создавать свой редактор с помощью веб.


              1. inborn_killer
                08.08.2016 13:22

                Спасибо за информацию. Значит, меня ожидают те же проблемы. Хотя, может быть мне хватит деревьев такого размера, т.к. никакого сложного AI я делать не собираюсь, и деревья тут только для того, чтобы были )


            1. Leopotam
              07.08.2016 21:26

              Визуализация графа хороша только на количестве нод < 10-15, дальше начинается ужас. В результате было решено дропнуть визуализацию и собирать все в коде, как-то так: https://github.com/Leopotam/LeopotamGroupLibraryUnity/blob/master/Assets/LeopotamGroup.Examples/Events/BehaviourTreeTest.cs


  1. softaria
    05.08.2016 16:05
    +1

    Singleton-ы не всегда являются лучшим решением. Как вы смотрите на использование в Unity Dependency Injection?
    Например, вот это https://github.com/Disturbing/UnitySuice работает очень неплохо и решает проблему более удобно.


    1. Relz
      05.08.2016 16:15

      Спасибо, в ближайшее время посмотрю и отпишусь о результатах исследования (:


    1. HexGrimm
      07.08.2016 13:56
      +1

      Начинать использовать DI нужно сразу с чего нибудь классического и проверенного временем, даже в юнити. http://www.ninject.org Отлично работает при компиляции с MONO. По вашей ссылке неизвестный проект с 20 коммитами в год.


      1. softaria
        07.08.2016 20:31

        Основной проект там вот этот https://github.com/Disturbing/suice
        Коммитов мало, но это — урезанный клон google guice
        Работает тоже отлично. Я его использовал.


        1. Leopotam
          07.08.2016 21:31

          1. Как оно по производительности по сравнению с классическим подходом?
          2. Как оно работает с il2cpp?


          1. softaria
            08.08.2016 09:15

            Специально производительность не замерял. Но заметного эффекта не оказывает.
            Про il2cpp могу сказать что под WebGL проект, основанный на Suice собирался и работал без проблем. Значит, наверное, работает.


            1. Leopotam
              08.08.2016 09:22

              Если идет интенсивное обращение к методам / свойствам синглтона — без локального кеширования инстанса оно будет работать соизмеримо с вот этим?


              1. softaria
                08.08.2016 09:30

                Почему нет? Если там и есть какой-то overhead то только в момент создания bean-ов и внедрения зависимостей (то есть — один раз на каждый bean). Потом они точно также существуют и могут использоваться. Или я не понял ваш вопрос?


                1. Leopotam
                  08.08.2016 09:33

                  Я про время инициализации, саму ленивую инициализацию, возможность проверки, что инстанс существует, чтобы не вызывать повторно ленивую загрузку в OnDestroy, например (это вызывает пробивание песочницы и объект может остаться в редакторе после выхода из playmode). Как оно работает с GC, есть ли дополнительные выделения памяти при инициализации? Просто раз пользуетесь, то должны знать об основных особенностях.


                  1. softaria
                    08.08.2016 09:41

                    Про ленивую инициализацию ничего сказать не могу, поскольку мы пользовались первой версией suice, где все было Eager.
                    Но даже и в этом случае у нас не возникло желания измерять это время, поскольку заметным оно не было.
                    Что касается GC, то по крайней мере в первой версии bean-ы жили все время жизни приложения.


              1. streeter12
                08.08.2016 11:59

                Посмотрел реализацию по ссылке возникло несколько вопросов.

                1. Что произойдет если кто — то уничтожит объект одиночки и _instanceCreated created будет true?
                2. При смене сцен точно произойдет уничтожение объекта, но так как у нас класс статический будет то же, что в 1.
                3. Как быть с модульными тестами (с 5.3 они не работают в плей моде)?
                4. В методе Awake будут уничтожены все повторные копии, это не приводило к сложно диогностируемым ошибкам?
                5. Как боретесь с проблемой утечки памяти, когда кто — то повесил ссылку на одиночку в кеше?
                6. Вы используете одиночку не только в контексте наследования (зачем _instance = this as T если этот awake только у прямых потомков и у нас произойдет стандартное неявное приведение ссылочного типа потомка к родителю)?
                7. Какой прирост производительности даёт этот способ над сравнением при переопределенной операции ==?
                8. Была ли ситуации когда == был BottleNeck?

                Заранее спасибо за ответ и уделенное время.)


                1. Leopotam
                  08.08.2016 12:26

                  1. Почему кто-то должен хотеть уничтожать инстанс подсистемы, которая предоставляет shared доступ к какому-то апи / данным?
                  2. Необязательно, в OnConstruct (который вызывается только у одного валидного инстанса и один раз) можно сделать DontDestroyOnLoad(gameObject), например, как тут: ScreenManager
                  3. Для тестов придется прогонять их в рантайме в отдельной сцене — ведь тестировать нужно практически всегда рантайм-апи / данные, а не редактор.
                  4. Awake нельзя использовать, вместо него нужно всю инициализацию перенести в OnConstruct. Так же запрещено использовать доступ к синглтону из Awake внешних компонентов (делать все в OnEnable / Start и тп). Если следовать этим правилам — волосы будут мягкими и шелковистыми.
                  5. Что значит кто-то повесил в кеше? Кеш нужен очень редко, доступ достаточно оптимизирован. Используется только как кеш внутри одного метода при массовых использованиях апи синглтона, но тут тоже нет проблемы — оно все одноптоточно и в принципе не сможет умереть до конца метода.
                  6. Не совсем понял вопрос. Синглтон можно повесить как обычный компонент в редакторе и настроить его свойства (дизы танцуют и поют) — после запуска playmode выживет только один (если их будет много) и только у него отработает OnConstruct.
                  7. 2х, подобный подход используется для патча трансформа вот тут, тут можно погонять глупый, но какой-никакой тест
                  8. Оно зависит от использования — иногда требуется максимально снизить нагрузку на множественное использование апи синглтона без локального кеширования. Ну и написать один раз и потом лениво копировать код по проектам :)


                  1. streeter12
                    08.08.2016 13:32

                    Кстати по поводу ссылки в 7 Transform это класс, думаю можно убрать дополнительную переменную флаг.
                    Время доступа к Transform значительно превысит операции вида ((System.Object)transform == null).
                    Точнее сказать можно по результатам теста.


                    1. Leopotam
                      08.08.2016 13:34

                      Не превысит — transform — это Getter со всеми вытекающими (поиск по компонентам внутри судя по времени отработки).
                      З.Ы. Понял про что речь, сравню, отпишусь


                      1. streeter12
                        08.08.2016 13:38

                        Да я это и говорю, что операция == для System.Object (я сделал явное приведение) значительно быстрее чем метод Get и вызов кода движка. Поэтому можно убрать дополнительную переменную и так код будет читабельные.

                        Ну это мое скромное мнение)


                        1. Leopotam
                          08.08.2016 14:04

                          Сравнил — результаты соизмеримы с флаговой реализацией в пределах погрешности, откатил флаг на кастинг.


              1. streeter12
                08.08.2016 12:12

                Еще забыл.

                9. Что будет если кто — то попросит ссылку на одиночку в своем Awake до того как там пропишется наш объект?
                10. После присваивание в Awake мы не ставим флаг создания ссылки в true, у нас будет повторный поиск объекта на сцене?

                Еще раз спасибо за уделенное время.


                1. Leopotam
                  08.08.2016 12:26

                  Ответ был в 4 выше.


                  1. streeter12
                    08.08.2016 12:58

                    Большинство вопросов сводится, а если 10 — й программист с недосыпа нахалтурит)
                    Кстати бывает часто, а ловить такие ошибки бывает сложно.

                    1. Специально уничтожать его ни кто не будет. Просто это может привести к сложно диагностируемой ошибке.
                    2. Да я знаю DontDestroyOnLoad(gameObject). Но в этой реализации его нет и я бы предпочел возлагать такую ответственность не на программиста реализующего наследника, а на себя.
                    3. По возможности я стараюсь обходится минимум тестов раинтайма, это медленнее и черновато ошибками.
                    Модульное тестирование на сцене в основном требуется для объектов с физикой и анимацией и редко когда встречал другие случаи. Но за это я плачу увеличением кода и тд.
                    4. Я имел ввиду если у нас несколько одинаковых объектов добавлены на сцену по ошибке)
                    5. Если реализация это позволяет, а проект большой как показывает практика это происходит.
                    6. Просто если мы используем подход class Test = Signaleton <>, то у нас нет необходимости _instance = this as T,
                    а можно _instance = this.
                    7. Спасибо огромное ознакомлюсь. Если сильно важна производительность можно делать и без Одиночки.
                    Статик классы и ScriptableObjects могут гарантировать одну ссылку.
                    Конечно все зависит от задач все сделать и есть вещи которые они не покрывают.
                    8. Кстати под подобные вещи сделал отдельную библиотеку, чтоб при копировании не ошибаться)
                    9. Я имел ввиду ситуацию когда объект не наследник одиночки, а просто Mono в своём Awake затребует ссылку.
                    Конечно можно запретить это делать всем, но кто — то обязательно не удержится).
                    Например у нас менеджер звука и один объект на сцене хочет взять его параметры в Awake вместо Start.
                    10. Я имел в виду у нас добавлен на сцену один объект наследник Одиночки.
                    Он запускает Awake определенный вами и прописывает ссылку в _instance.
                    Объект на сцене обращается к методу get instance.
                    Так как флаг _instanceCreated не поставлен в true происходит тяжелый поиск на сцене по типу.

                    Огромное спасибо за ответ.


                    1. Leopotam
                      08.08.2016 13:35

                      1. Если оно умрет, то при следующем обращении будет создано снова. Вообще, можно потратить какое-то время, найти виноватого и жестоко покарать его, например, спиливанием пары зубов напильником. Синглтон подразумевает что его никто не создает и никто не убивает, он просто есть. Для принудительного создания есть пустой метод Validate, который служит исключительно для прогрева инстанса.
                      2. Так это же абстрактный класс, он сам из себя ничего не представляет, чтобы что-то дергать в синглтоне — это нужно реализовать в наследнике и запечатать его если потребуется. Ну и тут получается свобода — можно делать локальные для сцены синглтоны, а можно глобальные для всех сцен. Каждая реализация должна решить как она себя должна вести. Флаг глобального инстанса кстати был в одной из промежуточных версий, но потом был выпилен за ненадобностью и проблемами с поведением в дальнейшем.
                      4. Ну они все помрут и останется только один, как горец. У него будет вызван OnConstruct. Есть одна тонкость — синглтоны должны висеть на отдельных ГО — если один из них придумает помирать, то утянет весь ГО с собой, а при множественных инстансах повешении нескольких компонентов на один ГО это чревато. Но это уже из области стиля сборки кубиков логики в сцене — должна быть договоренность и правила в команде.
                      5. Кеширования быть не должно — написать на обоях кровью джуниора, чтобы остальные устрашились.
                      6. Так это UnitySingleton, исключительно для компонентов MonoBehaviour, для pure-C# классов оно пишется вообще как «static readonly T Instance = new T();» свойство без всего прочего мусора и работает как надо.
                      7. Если нужна реакция на события MonoBehaviour — так просто не обойтись, а тут получается вполне просто и без дураков.
                      9. см п.1.
                      10. Да, фейл, поправлено.


                      1. streeter12
                        08.08.2016 13:50

                        2. Кстати для локальных и глобальных одиночек у меня два разных базовых класса.
                        В моей абстракции тут нет связи является.
                        5. В этом пункте возможен фаил при использовании стороннего кода)
                        6. Кончено реализация Pure c# мне известна, я просто говорю, что нет случая когда неявное приведение вызовет исключение и в 51 строчке класса одиночке не требуется использовать оператор as.
                        9. Да помрет. Но может привести к ошибочным данным по умолчанию в классе вызвавшем ссылку при инициализации в Awake. А поиск ошибки виновного после 20к кода дело не простое.


                        1. Leopotam
                          08.08.2016 14:09

                          5. Так сторонний код должен же как-то узнать про синглтон, те быть интегрирован в текущий код, а значит это можно сделать правильно.
                          6. Все-равно не совсем понял, про что речь. Приведение типа требует компилятор, причем мягкого.
                          9. Так чтобы оно произошло — должны произойти 2 вещи: убийство должно быть жестоким (через DestroyImmediate, потому что Destroy вызывается после текущего фрейма чтобы все отработало как есть) и должен быть локальный кеш во внешнем коде, чего быть не должно. Если разраб настолько глуп, что может совместить эти 2 условия, то ССЗБ, идеального антивандального решения со всеми фишками событий MonoBehaviour просто не существует.


                          1. streeter12
                            08.08.2016 14:21

                            6. Согласен мой косяк вычислял в голове забыл один пункт приведения обобщенных типов все верно прошу прощения.)
                            9. Я имею ввиду другое допустим у нас есть класс ScoreCollector на сцене он одиночка и подсчитывает очки.
                            В него сериализуемы настройки конкретного уровня по стоимости и тд.
                            Есть класс которому для начала работы нужны данные из одиночки в Awake.
                            Когда создается объект вызывается его метод Awake и порядок их вызова не определен (если не указать, что есть зло).
                            Он запросит данные раньше чем объект присутствующий на сцене пропишет себя (порядок вызова Awake не определен) и будет создан объект одиночки с данными по умолчанию.
                            Объект получает неправильные данные по умолчанию и работает некорректно.
                            Далее исходный объект одиночка чистит ссылку.
                            А программисты ищут несколько часов в чем фаил и только потом задумаются об одиночке.

                            Если что пример надуманный важна его суть.)


                            1. Leopotam
                              08.08.2016 14:27

                              Ну если есть работа из Awake, то это уже должно наводить на мысль где фейл. Если это реально требуется (а это практически всегда не так, можно задействовать OnEnable, например), то можно в OnConstruct синглтона делать подгрузку данных из какого-то стора (csv, префаба, SO, любого ассета) и убирать его из сцены вообще — он лениво загрузится из кода.
                              З.Ы. Что еще не так с этим кодом? :)


                              1. streeter12
                                08.08.2016 14:31

                                Остальное чисто эстетические соображения, больше связанные с конкретными решениями задач и архитектурой))
                                В любом случае эта реализация нравится гораздо больше чем у автора статьи.


                            1. streeter12
                              08.08.2016 14:28

                              9. По этой причине я почти полностью отказался от варианта создания ссылки если исходный объект не найден.
                              Либо объект есть на сцене либо это ошибка. Это решает почти все проблемы которые рассмотрены нами.
                              Кончено если нам не требуется отложенная инициализация, что как я думаю бывает редко.


                              1. Leopotam
                                08.08.2016 14:31

                                Как раз ленивая загрузка — одна из самых востребованных, тк дизам проще дать крутить циферки в гуглодоках и потом их забирать в проект + крутить циферки в префабах + редко когда напрямую в сцене, а все остальное делать лениво из кода — сцены получаются легкими как по весу, так и по diff-у в репе.


                                1. streeter12
                                  08.08.2016 14:33

                                  Да но для таких случаев я не использую mono signaleton а предпочитаю другие реализации.


                                  1. Leopotam
                                    08.08.2016 14:35

                                    Это только пока не нужны эвенты MonoBehaviour — без них всегда рациональнее использовать pure C#. Этот костыль был выстроган исключительно для обертки над MonoBehaviour.


                                    1. streeter12
                                      08.08.2016 14:37

                                      Да да да. Поэтому всегда стараюсь разделять по возможности хранение данных и сигналетоны исполнители.
                                      Просто когда вижу недоработанные сигналетоны где попало это вгоняет в уныние. А многие копируют код из статей.

                                      Спасибо за продуктивную дискуссию.


                1. Leopotam
                  08.08.2016 12:31

                  Есть еще одна неуказанная неоднозначность — что будет если в OnDisable / OnDestroy пощупать апи синглтона допустим для отписки? Из-за неопределенности порядка вызовов это становится большой проблемой — синглтон к этому времени может самоубиться и при щупании его инстанса произойдет ленивая загрузка — инстанс создастся по-новой, чего юнити категорически не любит и что вызывает пробивает песочницы редактора — инстанс может пережить остановку и остаться в сцене как полноценный объект (багу минимум 3 года, никто не торопится править). Поэтому существует статик метод проверки существования инстанса, в вызов которого нужно оборачивать все обращения к синглтону в OnDisable / OnDestroy:

                  if (NetworkManager.IsInstanceCreated ()) {
                      NetworkManager.Instance.OnConnectionClosed -= OnNetworkDisconnected;
                  }
                  


  1. kreo_OL
    05.08.2016 16:14
    +1

    То что нужно!


  1. streeter12
    05.08.2016 16:16
    +1

    Хочу добавить некоторые замечания.

    1.Проверка ссылки на null в юнити дял UnityEngine.Object перегружена и вернет будет true даже если объект существует.
    Если не брать это во внимание возможна утечка памяти (удалили одиночку, создали, а ссылки повисли в других объектах).

    2. Если использовать Unity Test Tools с версии 5.3, то тесты проводятся на той же сцене,
    что открыта в текущий момент и возможны серьезные проблемы при тестировании.

    3. Для статических объектов геим дизайнеру совсем не обязательно лезть в код. Все зависит от того, как будут храниться данные.
    Можно сделать отдельный сериализуемый класс данных и EditorWindow с любыми изысками в GUI. И по моему мнению для работы с подсистемами Editor Windows более удобны (не нужен лишний хлам на сценах).

    4. В данной реализации Одиночки дважды запускается тяжелые функции FindObjectsOfType(typeof(T)) и FindObjectOfType(typeof(T)),
    хотя достаточно первой из них.

    5. Использовать в Одиночке Awake таким образом очень опасно.
    Если порядок вызова Awake не определен, то кто — то может запросить Одиночку,
    до того как там пропишется объект со сцены => гарантированный еррор.
    Дочерний класс может захотеть иметь свой Awake.
    Код получения ссылки дублирует код Awake.
    Достаточно вместо тяжелого GetComponent написать this.

    6. В качестве базы для одиночки всегда следует рассмотреть ScriptableObject. Получить две копии одного и того же объекта по моей памяти невозможно + он не тянет хлам в виде трансформа и тд.

    Спасибо за внимание)


    1. Relz
      05.08.2016 16:20

      Огромное спасибо, обязательно всё это учту!
      Не думаю, что буду серьёзно менять имеющуюся архитектуру в текущем проекте, но переписать что-то для последующих очень вероятно (:


    1. Ichimitsu
      05.08.2016 18:39

      .Проверка ссылки на null в юнити дял UnityEngine.Object перегружена и вернет будет true

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


      1. streeter12
        05.08.2016 18:53
        +1

        Ну статья про то, как сделать работу геим дизанера проще, я думаю он работает в редакторе.
        А не очевидная логика работы в редакторе прилагается 100 процентов.


      1. streeter12
        05.08.2016 19:22
        +1

        Если быть точным я говорил только про случай когда на наш сигналетон есть ссылки из других объектов.

        Еще раз проверил этот факт в юнити 5.4 и создал два скрипта Mono.
        Первый с полем public MonoBehaviour testObject и Destroy(testObject) по нажатию кнопки.
        После чего в билде смотрел вывод до и после уничтожения выражений

        testObject == null
        (System.Object)testObject == null и
        !testObject.

        До уничтожения объекта вывод false false false.
        После удаления вывод true false true.
        Так что все сказанное о сторонних ссылках и проверке верно и для билда. Юнити уничтожает внутреннее представление объекта из пула,
        а так как у нас есть ссылка на объект GC не помечает объект подлежащий сбору мусора. В результате имеем мемори лик.
        И это справедливо не только для редактора.

        Если вы это имели ввиду.


        1. Ichimitsu
          05.08.2016 19:41
          +1

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


          1. streeter12
            05.08.2016 20:19
            +1

            К сожалению данная реализация ни как не защищает от удаления.
            А значит это произойдет по чей — то ошибке.
            При закрытии приложения объекты будут уничтожаться в произвольном порядке.
            И тут при данной реализации можно получить опять не предсказуемую работу приложения.


  1. streeter12
    05.08.2016 16:55
    +1

    Не за что.
    Если будет возможность поправите код.
    И хорошо будет отметить, что код if (!_instance) равноправен (_instance == null) с теми же оговорками о перегрузке.

    Удачи. Еще раз спасибо за внимание)


  1. inborn_killer
    05.08.2016 17:35
    +1

    если вы поставите [Header()] перед приватным полем, то ругаться Unity не станет, но никакого заголовка не отобразит

    Если сделать [SerializeField], то отобразит =) Вместе с полем, кстати. Тоже очень удобный аттрибут, когда нужно предоставить возможность редактировать поле из редактора, а в коде, скажем, сделать публичное свойство с необходимыми геттерами-сеттерами.


  1. CheeseMaster
    05.08.2016 19:24
    +1

    Директива #if UNITY_EDITOR разрешает скрипты касмного редактора размещать не в папке «Editor» или про это (что всю кастомизацию редактора нужно описывать в специальной папке) ограничение просто не написали? Сам на днях начал менять редактор и некоторое время был в смущении, почему же ничего не работает.


    1. Relz
      05.08.2016 19:25

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


  1. semenyakinVS
    05.08.2016 22:56
    +1

    Спасибо! Полезно! Как раз начинаем работать над первой игрой и уже успели столкнуться с проблемой, которую вы решили с помощью Singletone. До этого решали проблему прямым указанием скрипта-менеджера через поле инспектора объекта, для которого нужен менеджер. Скрипты-менеджеры лежал в кучу в специальном даммике на сцене. Не очень удобно — часто забывали указывать менеджеры.

    Ваш вариант решения выглядит намного более удобным… Да и прочие штуки выглядят вкусно. Будем внедрять по мере надобности. Ещё раз спасибо!


  1. OlegGelezcov
    05.08.2016 23:49
    +1

    На практике вместо HideInInspector на открытых переменных, лучше использовать [SerializeField] на приватных. Когда много классов потом не приходится смотреть можно ли менять ее напрямую, все делать через аксессоры доступа.

    Ну а в целом по архитектуре — выводить параметры геймплея в инспектор нормально для не очень больших игр. Но когда количество настроек начинает сильно увеличиваться — это смотрится ужасно. Мы предпочитаем хранить настройки в ресурсных файлах и писать интерфейсы к ним на основе EditorWindow, а в силу того что выделенного тулсет-программиста нет, то часто и не пишем ничего, а работаем напрямую с файлами.


  1. CrazyFizik
    06.08.2016 12:55
    +1

    Хм. В тексте есть картинка скрипта WeaponScriptController с параметрами анимации, в том числе и скорости. Вопрос, как контролируйте скоростные параметры анимации коли не секрет?


    1. Relz
      06.08.2016 13:02
      +1

      Вроде бы можно изменять скорость воспроизведения анимации через параметры компонента Animator (:
      Но у нас часть «анимации» производится из кода, как раз чтобы можно было гибко менять параметры анимации — скорость замаха, время отдыха после удара…


      1. CrazyFizik
        06.08.2016 19:51
        +1

        Так-то все можно, возникает вопрос как все это правильно контролировать из кода? Как-то не хочется городить огород костылей.
        Вариант animation[«Animation name»].speed = 0.5f; остался в прошлом (работает только для легаси анимации). Можно выставить что-нибудь в стиле animator.speed = 0.5f; однако это скорость работы самого аниматора, а не какого-то клипа или состояния (у AnimatorStateInfo для свойства speed доступен только геттер, хотя в редакторе аниматора ручками править его таки можно).
        А у AnimationClip такого параметра как скорость вообще нет, есть правда метод AnimationClip.SampleAnimation(GameObject go, time t); Однако наличие параметра GameObject наводит на неприятные размышления, да в доках указано, что работает эта штука медленно, юзайте как лучше Animation… Окей, лезем дальше и после путешествий по гиперссылкам обнаруживаем:
        image
        Хм… видимо доки обновляют какие-то индусы, причем одни индусы вкурсе, а другие индусы не в курсе что Animation где-то там с 4.3 версии has been deprecated… Не, ну можно переключиться через Debug mode на Легаси-анимацию…

        Вот теперь меня и интересует, как дальше жить? Не с мешаниной же легаси кода. Вот поэтому и поинтересовался, как вы там анимацией управляйте. Unity Scripting API Reference мягко говоря противоречивый


        1. Papasol
          08.08.2016 12:59
          +1

          Скорость анимации можно менять через float-параметр, так же как и переходы между состояниями анимации (anim.SetFloat(«shootspeed», shotspeed)). А в инспекторе для конкретного клипа говорите, что используете multiplier для скорости и в качестве параметра выбираете заданную переменную.


          1. CrazyFizik
            08.08.2016 19:51

            Ну да, такая возможность в 5-ой версии как раз появилась, правда все равно это похоже на какие-то танцы с бубном (что бы что-то поюзать из кода, приходиться что-то дополнительно прикручивать ручками в редакторе, при этом когда-то для этого был реализован понятный и доступный интерфейс). А еще как можно по извращаться? Вообще вопрос анимации в Юнити таки очень интересен…

            Меня вот еще что интересует, как в Юнити реализована работа таких вот функций в стиле SetFloat(string name), GetComponent() [когда-то можно было получить свойство gameObject.rigidbody, теперь вот надо использовать GetComponent()] и прочие Find(string name) и т.п. Уж случаем не рефлексию ли они используют или еще какое темное колдунство?


  1. Tar
    06.08.2016 13:43
    +1

    Как уже заметили, проще для дата-классов использовать ScriptableObject. Урок от Unity как им пользоваться как раз для таких задач.
    Вообще, в больших проектах дизайнерам проще работать с Excel'ем. Забивать туда сотни и тысячи item'ов, высчитывать по странным формулам цену, графики рисовать и прочее. Потом всё это экспортится в xml или json. Хотя можно прямо из xls читать. Некоторые вообще по сети из google spreadsheet конфиги тянут.


    1. Ichimitsu
      06.08.2016 20:04
      +1

      да это весьма удобно, использовали такой подход (я про спридшит из гуглдокс).


    1. Leopotam
      08.08.2016 09:17

      Некоторые вообще по сети из google spreadsheet конфиги тянут.


      Как и всю остальную инфу + конвертить ее после чтения в нужные типы данных один раз + оборачивать в синглтон.


  1. LifeKILLED
    14.08.2016 02:11
    +1

    Спасибо за статью, даже не задумывался обо всём этом.


    1. Relz
      15.08.2016 14:16

      Самое ужасное, что и мы почти год жили в уже коммерческом проекте без таких прекрасных вещей (: