Ошибки новичков

Ошибки новичков

Сообщение UnitySexyBoys 08 окт 2017, 16:32

Все новички совершают абсолютно одни и те же ошибки, которые я наблюдаю каждый божий день. Не то, чтобы я нетерпим к ним, нет, я довольно толерантен к изъянам в коде (т.к. сам когда-то через это проходил), но вот три ошибки, которые описаны ниже, раздражают меня больше всего.

Сокрытие данных.

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

Синтаксис:
Используется csharp
public class SomeClass : MonoBehaviour
{
        public int SomeVariable;
}
 


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

Синтаксис:
Используется csharp
namespace SomeClasses
{
        public class SomeClass : MonoBehaviour
        {
                public int SomeVariable
                {
                        get;
                        private set;
                }
        }
}
 


Чтобы сделать поле видимым в инспекторе, примените к нему модификатор доступа private и аттрибут SerializeField.

Синтаксис:
Используется csharp
namespace SomeClasses
{
        public class SomeClass : MonoBehaviour
        {
                [SerializeField]
                private int someVariable;

                public int SomeVariable
                {
                        get
                        {
                                return someVariable;
                        }
                }
        }
}
 


Таким образом, мы получаем сразу два преимущества: пользователь (например, геймдизайнер) сможет настраивать значение из инспектора + никакой другой класс не сможет изменить свойство SomeVariable напрямую из кода, что в противном случае могло бы нарушить состояние объекта. Представьте сценарий, в котором вы описываете класс с нарушенной инкапсуляцией. В ходе разработки приходит программист Вася и видит, что у вашего класса наружу торчит некое вкусное поле. Вася предполагает, что значение этого поля можно невозбранно менять и, конечно же, меняет его, ломая при этом вашу прекрасную логику, завязанной на значении этого поля. Проще говоря, пользователь (вы или другой программист) класса "Ручка" не должен знать о типе механизма ее открытия. Ручка должна просто писать.

Кэширование компонентов.

Как говорят бородатые разработчики Unity: «Кэшируйте, кэшируйте и кэшируйте!». Рассмотри вымученный искусственный пример:

Синтаксис:
Используется csharp
namespace Pens
{
        public class BasePen : MonoBehaviour
        {
                public void Close ()
                {
                        /* Каждый раз, когда вы вызываете GetComponent, где-то
                        в мире умирает котенок */

                        if (GetComponent<BaseCap> () != null)
                        {
                                GetComponent<BaseCap> ().Install ();
                        }
                }

                public void Draw ()
                {
                        bool canWrite = true;

                        if (GetComponent<BaseCap> () != null)
                                canWrite = !GetComponent<BaseCap> ()).Installed;
                       
                        if (canWrite)
                        {
                                //Много сложного кода
                        }
                        else
                        {
                                //Бросаем исключение/взрываем компуктер/ничего не делаем
                        }
                }
        }
}
 


Каждый раз, чтобы обратиться к компоненту колпачка ручки, мы вызываем GetComponent<BaseCap>(), что в итоге захламляет код, делает его плохо читаемым и медленным. Тут на форуме можно увидеть реальные километровые портянки из GetComponent<T>(). Выход — закэшировать BaseCap.

Синтаксис:
Используется csharp
namespace Pens
{
        public class BasePen : MonoBehaviour
        {
                private BaseCap cap;

                public void Close ()
                {
                        if (cap != null)
                                cap.Install ();
                }

                public void Draw ()
                {
                        bool canWrite = true;

                        if (cap != null)
                                canWrite = !cap.Installed;
                       
                        if (canWrite)
                        {
                                //Много сложного кода
                        }
                        else
                        {
                                //Бросаем исключение/взрываем компуктер/ничего не делаем
                        }
                }

                void Start ()
                {
                        cap = GetComponent<BaseCap> ();
                }
        }
}
 


Согласитесь, что код стал чище и компактнее. К тому же мы освободили ресурсы ЦПУ от постоянного судорожного поиска необходимого нам компонента. Круто? Ну, не очень. Скорее всего в реальной жизни вашему компоненту будет жизненно необходим другой, без которого он попросту не сможет работать. Поэтому можно воспользоваться аттрибутом RequireComponent и избавиться от проверки на null.

Синтаксис:
Используется csharp
[RequireComponent (typeof (BaseCap))]
namespace Pens
{
        public class BasePen : MonoBehaviour
        {
                private BaseCap cap;

                public void Close ()
                {
                        cap.Install ();
                }

                public void Draw ()
                {
                        if (!cap.Installed)
                        {
                                //Много сложного кода
                        }
                        else
                        {
                                //Бросаем исключение/взрываем компуктер/ничего не делаем
                        }
                }

                void Start ()
                {
                        cap = GetComponent<BaseCap> ();
                }
        }
}
 


Лирическое отступление.

Теперь немного порассуждаем. Предположим, моделлер выгрузил ручку с колпачком, да так, что сама меша колпачка находится где-то в жопе по иерархии. К тому же как-то странно вешать ̶м̶о̶д̶е̶л̶л̶е̶р̶а̶ компонент колпачка на тот же объект, где висит компонент самой ручки. Логичнее повесить компонент колпачка на сам колпачок, а в инспекторе в компоненте ручки сделать поле для ссылки на колпачок. Тогда код упрощается еще больше.

Синтаксис:
Используется csharp
namespace Pens
{
        public class BasePen : MonoBehaviour
        {
                [SerializeField]
                private BaseCap cap;

                public void Close ()
                {
                        cap.Install ();
                }

                public void Draw ()
                {
                        if (!cap.Installed)
                        {
                                //Много сложного кода
                        }
                        else
                        {
                                //Бросаем исключение/взрываем компуктер/ничего не делаем
                        }
                }
        }
}
 


Может возникнуть вопрос: а почему не воспользоваться GetComponentInChildren<BaseCap>()? Пожалуйста, если вы уверены, что колпачки будут всегда находиться в иерархии ручки.
Однако, как подсказывает практика, завтра обязательно решат, что все колпачки будут идти отдельно, и вообще мы будем их продавать через встроенный магазин за биткоины.

Работа в Update.

Люди очень часто выносят тяжелую логику в Update, не понимая, что все тоже самое можно сделать гораздо «легковеснее», если использовать событийно-ориентированный подход.

Синтаксис:
Используется csharp
namespace MySuperGame.UI
{
        public class Health : MonoBehaviour
        {
                [SerializeField]
                private Text text;

                private Player player;

                void Start ()
                {
                        player = FindObjectOfType<Player> ();
                }

                void Update ()
                {
                        text.text = string.Format ("Жыза: {0}%", player.Health.ToString ());
                }
        }
}
 


Во-первых, компоненты UI ничего не должны знать о персонаже! За такой код вас нужно гнать ссаными тряпками. Во-вторых, зачем КАЖДЫЙ РАЗ в Update переприсваивать значение здоровья персонажа, вызывая при этом тяжелейший string.Format() и ToString(), когда это можно сделать ОДИН РАЗ при ИЗМЕНЕНИИ здоровья?

Синтаксис:
Используется csharp
namespace MySuperGame.UI
{
    /// <summary>
    /// Класс, отвечающий за отображение здоровья персонажа
    /// </summary>
    public class Health : MonoBehaviour
    {
        private const string HEALTH_FORMAT = "Жыза: {0}%";

        [SerializeField]
        private Text text;

        /// <summary>
        /// Обновляет значение здоровья персонажа
        /// </summary>
        /// <param name="value">Значение здоровья</param>
        public void Set (float value)
        {
            text.text = string.Format (HEALTH_FORMAT, value.ToString ());
        }
    }
}

namespace MySuperGame
{
    public class Player : MonoBehaviour
    {
        /// <summary>
        /// Максимальное значение здоровья игрока
        /// </summary>
        private const float MAX_HEALTH = 100f;

        /// <summary>
        /// Событие, возникающее при изменении здоровья
        /// </summary>
        public event System.Action<Player> OnHealthChanged;

        private void DoHealthChanged ()
        {
            if (OnHealthChanged != null)
            {
                OnHealthChanged (this);
            }
        }

        private float health;

        /// <summary>
        /// Значение здоровья персонажа
        /// </summary>
        public float Health
        {
            get
            {
                return health;
            }

            set
            {
                float _health = Mathf.Clamp (value, 0f, MAX_HEALTH);

                /* если предыдущее значение здоровья не равняется текущему */
                if (health != _health)
                {
                    /* то присваимваем новое значение здоровья */
                    health = _health;

                    /* вызываем событие изменения здоровья */
                    DoHealthChanged ();
                }
            }
        }
    }

    /// <summary>
    /// Класс, реализующий логику игры
    /// </summary>
    public class MainController : MonoBehaviour
    {
        [SerializeField]
        private Player player;

        [SerializeField]
        private Health health;

        private void Player_OnHealthChanged (Player sender)
        {
            /* обновляем значение здоровья в UI */
            health.Set (sender.Health);
        }

        void Start ()
        {
            /* подписываемся на событие изменения здорвоья персонажа */
            player.OnHealthChanged += Player_OnHealthChanged;
        }
    }
}
 


Если игрока ранили или он подобрал аптечку, вызывается событие изменения здоровья, на которое подписан MainController. MainController знает об UI и вызывает у него необходимый метод для обновления. Все! И никакого обновления 120 раз в секунду.

Приводите свои примеры для улучшения качества кода!
UnitySexyBoys
UNец
 
Сообщения: 30
Зарегистрирован: 06 окт 2017, 18:11

Re: Ошибки новичков

Сообщение lawsonilka 08 окт 2017, 17:41

я наблюдаю каждый божий день

Зарегистрирован: 06 окт 2017, 19:11

что то вы быстро сдались!
За такой код вас нужно гнать ссаными тряпками

хспади

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

Высказались, стало лучше?
lawsonilka
UNIверсал
 
Сообщения: 390
Зарегистрирован: 21 окт 2014, 14:48

Re: Ошибки новичков

Сообщение UnitySexyBoys 08 окт 2017, 17:51

lawsonilka писал(а):
я наблюдаю каждый божий день

Зарегистрирован: 06 окт 2017, 19:11

что то вы быстро сдались!
За такой код вас нужно гнать ссаными тряпками

хспади

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

Высказались, стало лучше?


Высказался? Вы вообще уловили суть моих слов? И с чего вы взяли, что от даты регистрации на форуме что-то зависит? Я давно читаю форум и у себя на работе часто имею дело со студентами практикантами.
UnitySexyBoys
UNец
 
Сообщения: 30
Зарегистрирован: 06 окт 2017, 18:11

Re: Ошибки новичков

Сообщение lawsonilka 08 окт 2017, 17:54

Высказался? Вы вообще уловили суть моих слов?

да уж.

Так значит вы зарегистрировались только чтобы написать этот пост!?

Страшно вас представить в виде какого-то преподавателя.
lawsonilka
UNIверсал
 
Сообщения: 390
Зарегистрирован: 21 окт 2014, 14:48

Re: Ошибки новичков

Сообщение seaman 08 окт 2017, 18:56

Если Вы собираетесь писать тут цикл статей, советую обратить внимание на блоги http://blogs.unity3d.ru/
Скорее всего чтобы туда иметь доступ на запись, нужно обратиться к Нео.

Блоги к сожалению заброшены, а было бы хорошо такие статьи собирать там.

PS^ Кстати у lawsonilka тоже есть цикл статей, который было бы хорошо туда перенести.
seaman
Адепт
 
Сообщения: 8352
Зарегистрирован: 24 янв 2011, 12:32
Откуда: Самара

Re: Ошибки новичков

Сообщение Cr0c 09 окт 2017, 09:14

UnitySexyBoys писал(а):Вы вообще уловили суть моих слов?

Таких статей тысяча и одна штука. Ну, уже и ещё одна )) проблема только в том, что новички их не читают, а более-менее опытные на практике об этом узнали. Вот если бы ещё упомянули, что и Camera.main и просто transform надо кешировать - было бы полезно )) и что структуры не надо кешировать, и что не всё то поля, что кажется, а геттеры, которые тоже могут быть медленными медленные...
Аватара пользователя
Cr0c
Адепт
 
Сообщения: 3035
Зарегистрирован: 19 июн 2015, 13:50
Skype: cr0c81

Re: Ошибки новичков

Сообщение Anonymyx 09 окт 2017, 11:30

а это точно про transform и main?
я ожидал что-то типа

get
{
if(_main == null)
_main = FindWithTag(...);
return _main;
}

и для трансформа аналгично.
Аватара пользователя
Anonymyx
Адепт
 
Сообщения: 1973
Зарегистрирован: 05 апр 2015, 15:55

Re: Ошибки новичков

Сообщение Cr0c 09 окт 2017, 11:44

this.transform медленнее кешированного, а Camera.main ещё медленнее этого. По профайлеру, по крайней мере ))
Аватара пользователя
Cr0c
Адепт
 
Сообщения: 3035
Зарегистрирован: 19 июн 2015, 13:50
Skype: cr0c81

Re: Ошибки новичков

Сообщение UnitySexyBoys 09 окт 2017, 17:19

У них (у юнити) была статья в блоге, что из-за перегрузки сравнения на их «особенный» null, они не увидели прироста в перфомансе при кэшировании трансформа (this.transform). То бишь кэш и lazy получение компонента друг друга компенсировали по производительности. Если же говорить про Camera.main, то они каждый раз просто перебирают все камеры, что очень медленно.
UnitySexyBoys
UNец
 
Сообщения: 30
Зарегистрирован: 06 окт 2017, 18:11

Re: Ошибки новичков

Сообщение snezhok_13 10 окт 2017, 14:06

Годнейшая статья. Серьезно. Можно ли взять ее для блога?
Оформление можно будет более читабельное сделать.
Блог тут - coremission.net
Разработка игр, немножко игровая журналистика, сейчас делаем Календарь: даты выхода игр
Аватара пользователя
snezhok_13
UNIверсал
 
Сообщения: 450
Зарегистрирован: 09 сен 2013, 11:12
Skype: s.coremission
  • Сайт

Re: Ошибки новичков

Сообщение UnitySexyBoys 10 окт 2017, 17:27

snezhok_13 писал(а):Годнейшая статья. Серьезно. Можно ли взять ее для блога?
Оформление можно будет более читабельное сделать.
Блог тут - coremission.net


Берите конечно.
UnitySexyBoys
UNец
 
Сообщения: 30
Зарегистрирован: 06 окт 2017, 18:11


Вернуться в Tips & Tricks

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и гости: 2