Геймдев — занятие по настоящему увлекательное, особенно когда в команде есть как опытные программисты, так и новички. В отличии от таких движков как Unreal и CryEngine, в Unity достаточно низкий порог вхождения, и зачастую ветераны .NET и новички начинают выяснять отношения, которые заканчиваются дракой с применением офисной мебели.

В этой статье я попытался собрать советы которые помогут вам и вашей команде договориться наконец о том, как надо писать код, и, надеюсь, хорошо посмеяться. И так, поехали!

1. Пиши на UnityScript

Один язык в проекте — это скучно. Разнообразь жизнь своих коллег, пусть учаться быть полиглотами. C# — это майкрософт, и вообще строгая типизация это не твое.

2. Не используй var

Сами Unity запрещают* это делать, да и вообще непонятно, какого типа переменная. Когда тебе скажут про подсказки IntelliSense — закатывай глаза, майкрософт головного мозга! Твой Notepad++ этого не умеет.

3. Смешивай стили

Ну а что? Сами Unity нарушают C# Style Guide и фигачат camelCase в пропертях, чем ты хуже? Если используешь MonoDevelop, не забывай периодически менять табы на пробелы и обратно. При этом говори всем, что в MonoDevelop «настройки слетели» — про этот баг все знают!

4. Не пиши XML Doc комментарии

Они засоряют файл, а твой код должен быть понятен без дополнительных комментариев. И вообще, твой Notepad++ не поддреживает это всё. Если меняешь сигнатуру чужого метода, не обновляй XML Doc — так чтение автодокументации будет веселее.

5. Не используй namespace

Писать using — слишком муторно, и вообще зачем это делать, если можно обойтись без этого. Все скрипты должны лежать в папке Script — так их проще находить по алфавиту.

6. Связывай компоненты

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

7. Создавая референсы, прячь их через [SerializeField] private GameObject myGameObject;

Так твой референс можно будет отредактировать только через редактор, а в коде они мешаться не будут. Когда окажется, что что-то надо все-таки поменять через код, просто переделай private на public.

8. Не используй паттерны проектирования, кроме singleton

Когда тебе говорят про Dependency Injection или другие паттерны, закатывай глаза. Какие нафиг паттерны, тут тебе не энтерпрайз!

9. GameObject.SendMessage — потрясающе удобный инструмент, используй его как можно чаще

По возможности собирай название метода из нескольких строк — так твоим коллегам будет веселее искать, откуда вызвался метод!

10. Используй UnityEvent везде, где это возможно

Стандартные event'ы — это для лохов, тем более они не отображаются в инспекторе. Unity не зря придумали свои ивенты.

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

11. Подписываясь на событие через AddListener, забывай прописать RemoveListener

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

12. Кэшируй все компоненты на GameObject'ах

Даже если вы их не используете или используете один раз при нажатии на кнопку. Оптимизировать надо начинать как можно раньше! Не забывайте закэшировать transform; те, кто говорят, что transform уже закэширован — лжецы.

13. Не используй свойства

Свойства — это медленно, как вызов метода, а доступ к полю гораздо быстрее. Все преимущества свойств — выдуманы. Когда тебе кто-то начнет говорить про private set, обратную совместимость и необходимость рекомпиляции assembly в случае изменения поля на свойство, смотри на них как на дураков — какая еще компиляция, у нас геймдев а не энтерпрайз, мы тут dll не используем! И вообще, когда ты последний раз менял поле на свойство?

14. Не используй foreach

Ну, про это уже не раз говорили. Сами Unity запрещают использование foreach. Найдя foreach в коде коллеги, замени его на for и прочитай 10 минутную лекцию о том, что foreach создает мусор, для убедительности покажи свой бенчмарк, где ты обходишь список каждый кадр.

15. Не используй Linq

Linq — это медленно, сложно, и вообще это майкрософт. For на 10 строк — это гораздо красивее (foreach мы же не используем — забыл?). Если кто-то посмеет написать Query Expressions, то смотри на него как на идиота — майэскюЭль с Юнити перепутал!

16. Не используй строки

Строка — это выделение памяти, которую потом будет собирать GC. Используй char[] — не зря же ты в универе сишку проходил. Никакой юникод тебе не нужен — нечего лишние байты гонять, все равно у тебя bitmap шрифты.

17. Не используй Generics

Дженерики — это медленно и сложно, а Generic Constraints — еще сложнее. Какая нафиг ковариантность и контравариантность? У нас Юнити, тут этого нет! Когда нужны классы с разным типом, храните имя класса в переменной и находите его с помощью Type.GetType().

18. Не используй Coroutines без надобности

Они не работают на неактивных объектах, и вообще, создают дополнительную нагрузку. Храни состояние в булевых переменных, и проверяй их в Update — так гораздо удобнее, ведь все собрано в одном месте.

19. Как только выходит новая версия движка, ставь ее и закоммить папку ProjectSettings

Пусть твои коллеги учаться держать обновленный инструментарий. Ну и что, что проект сдавать через 2 дня, а новая версия с багами? Не твоя это проблема, что Unity не могут зарелизить нормально! Зато там есть новая система частиц, с которой тебе хочется поиграться.

20. Не используй текстовую сериализацию ассетов

Она тормозит редактор. Бинарная сериализация — это гораздо быстрее. Когда кто-то скажет про merge, закати глаза — он что, дурак, сцену мержить? Когда смержить нужно будет тебе, просто откати чужие изменения. Надо было говорить, над каким ассетом работаешь, что бы другие не трогали!

Про var и foreach
1. В своем Bitbucket репозитории Unity действительно указывают на то, что в pull-request'ах нельзя использовать var или foreach. Интересно, что там же они пишут не использовать префиксы m_ k_ итд, хотя сами делают это в огромном количестве мест, в нарушение стандарта C# Style Guide.

Статья была частично навеяна вот этой статьей по оптимизации и периодическими перлами, которые выдают знакомые программисты.

Что бы не устраивать холивар, я предлагаю условиться, что foreach действительно создает мусор, который потом приходится собирать GC. Но во-первых, это правило работает не всегда (иногда foreach разворачивается компилятором в обычный for), а во-вторых, если не обходить большие списки в каждом кадре, в 99% случаев проблемой это не является.

P.S. Всем хороших выходных, и с Наступающим!
Поделиться с друзьями
-->

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


  1. alt3d
    30.12.2016 08:45
    +1

    А где почитать инфу о том, что transform кэшируется самим unity?


    1. splatt
      30.12.2016 08:45

      Ну например, здесь (самая последняя сноска в статье).


      1. alt3d
        30.12.2016 10:49

        Спасибо.
        Им бы стоило об этом написать большими буквами, а не мимоходом в посте на совершенно другую тему.
        Я думаю многие, как и я, до сих пор не в курсе и городят свое кеширование.


        1. Leopotam
          30.12.2016 12:46
          +2

          А нужно не доверять, а проверять. Да, стало немножно лучше. Нет, это до сих пор в 2 раза медленнее чем через ручное кеширование и переопределение свойства transform. Нет это в 4 раза медленнее чем прямой доступ через кешированное приватное поле.


          1. Tutanhomon
            30.12.2016 14:42

            Здесь же знатный момент про перегруженный оператор "==", который проверяет объекты на плюсовой стороне, отчего может иметь смысл (и довольно часто имеет) конструкция this==null.
            Собственно, из-за этой проверки и тормозит .transform — перед тем, как вернуть кешированную переменную, он проверяет, варен ли кеш нулю, для чего лезет внутрь плюсовой части.

            пруф


            1. Leopotam
              30.12.2016 14:45

              this==null

              В таком виде как раз не имеет — нужно так:
              (object)this==null
              

              При кастинге принудительно отрывается перегруженный в unity == и все бежит быстрее.


          1. splatt
            30.12.2016 21:32

            Откуда информация про «в 2 раза медленнее» или «в 4 раза медленнее»?
            Судя по вот этим бенчмаркам, собственное кэширование дает прирост производительности в 15%-20%.

            Если для вас действительно так критичны эти 15%-20%, то я считаю что проблему можно решить различными способами, например переопределить .transform, написать свой CachedMonoBehavior или вообще пройтись по всем transform'ам пост-процессинговым скриптом.

            Но уж точно не прописывать вручную в каждом классе myTransform (а я такое видел, и в статье говорю именно об этом).


            1. Leopotam
              30.12.2016 21:36

              https://github.com/Leopotam/LeopotamGroupLibraryUnity.Examples/blob/master/Assets/LeopotamGroup.Examples/Common/MonoBehaviourBase/MonoBehaviourBaseTest.cs

              190130 — patched transform, access from local component
              64640 — cached to internal field transform, access from local component
              258130 — standard transform, access from external component

              Еще раз — лучше самому попробовать, чем кидать ссылки на «а вот там такое написано, я буду им верить».


              1. splatt
                30.12.2016 23:05

                Пожалуйста, прочтите и осмыслите то, что я вам напишу.

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

                Я провел несколько бенчмарков с .transform и с различными операциями над transform, 10 миллионов итерация каждая:

                .transform:                0.2374664 sec
                .cachedTransform:          0.0418570 sec
                Transform.position:        0.5588227 sec
                Transform.position (set)   0.5367007 sec
                Transform.localPosition:   0.5538622 sec
                Transform.rotation:        0.5620576 sec
                Transform.parent:          0.5024181 sec
                Transform.SetParent:       0.5636547 sec
                


                Да, вы правы, и кэширование .transform даст вам прирост производительности в несколько раз. В абсолютной величине — это 2E-8 сек, т.е. 20 наносекунд. Но вы ведь к transform обращаетесь не просто так, что бы положить его в переменную, верно?
                Практически любая операция над .transform, будь то взятие position, rotation, и уж тем более вызов методов типа SetParent, займет как минимум 50-60 наносекунд, и это в несколько раз превысит любой выигрыш, который вы получите с помощью своей оптимизации.

                Т.е. даже если предположить, что у вас проект из одного Update цикла, и в этом update цикле вам надо взять transform и провести над ним всего-лишь одну операцию (например, поменять позицию), то ваша оптимизация позволит вам выиграть около 30%. Но 30% — это прирост производительности «в вакууме» — без учета работы самого цикла, без учета draw calls на ваш gameobject, без учета физики, без учета всего.
                В реальной жизни, ваша оптимизация даст вам настолько мизерный прирост производительности, что в масштабах игры он будет абсолютно не значителен.

                Поэтому предложение кэшировать все .transform — это оптимизация, она работает, но это оптимизация «на спичках». Если вы со мной не согласны, я предлагаю вам взять любой серьезный проект, и провести замеры производительности с кэшированием и без кэширования transform.


                1. Leopotam
                  30.12.2016 23:13

                  Это все хорошо только когда пара GameObject-ов. А что, если таких объектов много (пара сотен, например) и нужно щупать их трансформы в апдейтах постоянно, причем возможно с вызовом десятка методов зависимых компонентов, в которых transform тоже может быть задействован? Случаи бывают разные, я предпочел не думать об этом в принципе и нарисовал базовый класс, где оно все подтыкается. Конкретный пример — 20 отрядов, в каждом из которых по 32 юнита с одной стороны и 20 отрядов по 32 юнита — в другом. И тут уже разговор не идет об ООП и раздельных компонентах логики на каждом юните (это очень медленно), а о data driving programming — когда поля юнитов публичны и обрабатываются циклом в менеджере, чтобы уменьшить накладные расходы на вызовы методов. Разница по скорости — в 2-3 раза. На мобильных платформах это критично.
                  По поводу перерассчета самого трансформа — это другая тема, юнитеки хвастались, что подхачили их и получили ускорение в 1.5-2 раза и собирались выпустить в unity5.5. Но как обычно — слушай маркетологов из юнити и продолжай делать то, что делаешь. На данный момент планы по релизу перенесены на unity5.6.


                  1. splatt
                    30.12.2016 23:30

                    Безусловно, всё зависит от конкретного use-case. Если вы делаете RTS с сотнями юнитов для мобильных устройств, понятно, что управление этими юнитами это нестандартная задача, и решать ее надо нестандартными средствами. Честно говоря, я игр в таком жанре на мобильных платформах не видел — да и на PC RTS типа SC2 всегда есть ограничение на лимит юнитов, и как правило он не превышает 200 юнитов на игрока.

                    Мне всё равно трудно представить, как у вас может быть прирост в производительности в 2-3 раза от использования cached transform, моя математика выше это опровергает.
                    Но даже если представить, что это реально дает вам хоть какой-то прирост производительности, даже в 10%-20% — пожалуйста. Никто же не говорит, что это никогда нельзя делать. Все зависит от конкретного случая.

                    Но в общем случае, в 99% проектах, я убежден, что это не даст никакого результата, по сравнению с тем, сколько процессорного времени занимают другие расчеты, вызовы функций, draw calls, тот же GC, итд.


                    1. Leopotam
                      30.12.2016 23:32

                      Никто же не говорит, что это никогда нельзя делать.

                      Тезис в статье говорит об обратном :)


                      1. splatt
                        30.12.2016 23:37
                        -1

                        Слушайте, ну статья — это юмор, и на самом деле я вообще выисмиваю людей, которые пишут cachedTransform = transform; вручную в каждом классе. Я чуть выше писал — я таких людей видел :)

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


                        1. Leopotam
                          30.12.2016 23:44

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


  1. Suvitruf
    30.12.2016 11:13
    +1

    transform сейчас кешируется, да. Но всё равно криво. Лучше всё равно кешировать его ещё самим, будет быстрее. Правда, последний раз я это смотрел на 5.4. На 5.5. уже может это поменяли.

    13, 14, 15 — посмотри, к примеру, видео от разработчиков Inside на Unite 2016. Свойства, foreach, Linq тормозят игру и они советую ничего из этого не использовать.

    А Linq действительно много мусора создаёт.

    Что бы не устраивать холивар, я предлагаю условиться, что foreach действительно создает мусор, который потом приходится собирать GC. Но во-первых, это правило работает не всегда (иногда foreach разворачивается компилятором в обычный for), а во-вторых, если не обходить большие списки в каждом кадре, в 99% случаев проблемой это не является.
    Так зачем тогда его использовать, если он всегда либо равен for, либо хуже?

    P.S. в этом же видео и много другого интересного есть. Особенно занятно посмотреть на их кастомный профайлер.


    1. marcor
      30.12.2016 11:51
      +4

      В ряде случаев он читабельнее, но это на мой взгляд.

      Я думаю, что проблема в преждевременной оптимизации. Если команде нравится foreach/for/while/… — пожалуйста, пока это не пробивает производительность. Начинаются проблемы — только тогда надо микрооптимизацией заниматься. Иначе легко весь проект загубить.

      Надо понимать плюсы и минусы каждого — и всё будет хорошо.


      1. Suvitruf
        30.12.2016 11:55

        У нас просто сейчас как раз неделя оптимизаций (мы периодически их проводим). Поэтому все эти места не понаслышке знаем. Я б ещё добавил вредный совет:
        21) Везде используйте enum и toString() на них.


        1. Leopotam
          30.12.2016 12:48

          Enum-ы возможно применять, если аккуратно их кастовать к byte / int в момент использования как ключа.


          1. Suvitruf
            30.12.2016 13:06

            Я об и говорю, если в строку не переводит и боксинг/анбоксинг избегать (не использовать enum как ключ у Dictionary и т.п.), то можно использовать.


  1. JeriX
    30.12.2016 11:20

    спасибо!
    поясните, пожалуйста, пункт 7, а то вообще не понятно что вы имеете ввиду?
    и пункт 12 тоже поясните: чем плохо кэшировать ссылки на компоненты?


    1. Leopotam
      30.12.2016 13:06
      +1

      7 — идиотский пункт. Кейс использования — предоставить дизайнеру возможность крутить свойства в инспекторе, но показать программеру, что напрямую менять ссылку на это поле нельзя, нужно использовать какое-то апи.


      1. splatt
        30.12.2016 22:08

        Можете привести пример кейса, когда программистам нужно использовать API, а дизайнер может менять поле напрямую? В чем логика — почему одни должны работать через api, а другие нет?
        Мне кажется в случае с API имеет смысл написать расширение редактора для работы с этим полем, которое в свою очередь так же будет работать через API.


        1. Leopotam
          30.12.2016 22:10

          Например, свойства / параметры юнита — дизайнер может их крутить визуально (попутно глядя на вспомогательную информацию, рисуемую инспекторами / гизмами), ограниченные всякими атрибутами / инспекторами, а программер не должен мочь их менять напрямую, а только забирать через геттер / метод / использовать как составное поле в рассчетах.


    1. afrokick
      30.12.2016 18:16

      Согласен с Leopotam
      7 пункт использую для VIEW слоя — все вьюшные компоненты засовываю в такие поля. Видел, что делают через всякие GetChild/GO.Find(), но по мне это привносит неудобства:
      1) нужно сохранять именование/порядок, чтобы ничего не отвалилось
      2) не понятно когда ссылка установлена

      Для себя выбрал подход все что относится к ui по возможности настраивать в коде(колбеки к примеру), а на редактор оставить форматирование(стили)


  1. AxisPod
    30.12.2016 11:40
    +3

    Начал ковырять Unity совсем недавно. Сразу по туториалам в догонку можно добавить:
    — Никогда не используйте коллекции, это MS и ничего хорошего быть не может, используйте массив, там же есть возможность ресайза
    — Если всё же вы решились использовать коллекции, не используйте Stack, он от лукавого, эмулируйте всё это на List, а лучше всё же на Array.
    — Не изучайте математику, это вам не институт, делайте всё по наитию, другим программистам будет проще вас понять

    В плане Code Style, там не только проблема с именованием, что больше всего напрягло, так это:
    — Встроенные методы приватные, а никак не protected, при этом и не виртуальные, что даже здравую логику нарушает, как-то вообще непонятно как они должны вызываться
    — Private не пишется
    — Фигурные скобки открываются на строке с кодом


    1. Nagg
      30.12.2016 11:57

      Какой-то набор вредных советов, хотя бы за коллекции можете пояснить? или "МС и ничего хорошего быть не может" на ваш взгляд хорошее объяснение?
      А хотя… почитал статью и понял, что ваш коммент с ней примерно в одном стиле, нет вопросов.


      1. Suvitruf
        30.12.2016 11:59
        +2

        Так это ж в копилку вредных советов. Так что, всё наоборот (:


        1. Nagg
          30.12.2016 12:02

          Действительно :-) Но всё же такой формат как по мне не слишком понятен без объяснений почему тот или иной совет вреден.


          1. Leopotam
            30.12.2016 12:52

            Если всё же вы решились использовать коллекции, не используйте Stack, он от лукавого, эмулируйте всё это на List, а лучше всё же на Array.

            Внутри оно все-равно сделано через List, а List — через Array. В чем прикол делать прослойки, если можно реализовать быстрее? Для прототипирования — сойдет и штатный BCL, если нужна реальная скорость на куче итераций по данным — приходится писать свое, например, симулировать List с прямым доступом к внутренним данным. Да, не безопасно, да скорость просто несоизмеримо выше при итерациях по массиву вместо индексатора List-а. Решение должно быть под задачу.


            1. poxu
              30.12.2016 15:45

              Внутри оно все-равно сделано через List, а List — через Array. В чем прикол делать прослойки, если можно реализовать быстрее? Для прототипирования — сойдет и штатный BCL, если нужна реальная скорость на куче итераций по данным — приходится писать свое

              А как вообще измеряется производительность Array по сравнению с List? Есть фреймворки какие-нибудь, которые с этим помогают?


              1. Leopotam
                30.12.2016 15:58

                Можно использовать Stopwatch и покрутить цикл по одной операции, чтобы оценить время доступа. Тут можно, конечно, говорить о том, что оно будет заоптимизировано после прогрева JIT и тп, но общее представление о скоростях исполнения в сравнении можно получить.
                Обращение к свойству List — это get / set методы.


                1. Leopotam
                  30.12.2016 16:03

                  Не успел дописать. Обращение к элементу List — это get / set методы «индексатора», т.е. по сути всегда накладные расходы на вызов метода. В случае с Array — сама среда исполнения предоставляет прямой доступ к элементу без прослойки методов.


                  1. aikixd
                    02.01.2017 00:30

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


                    1. Leopotam
                      02.01.2017 13:45

                      Можно обернуть в unchecked, но на самом деле проверка на границы жрет удивительно мало (разница в тестах незначительная).


              1. Leopotam
                30.12.2016 16:15

                Можете сделать такой тест:

                const int MaxIteration = 100000;
                const int ItemsAmount = 10;
                
                IEnumerator Start() {
                // чтобы unity успела стартануть и меньше влияла на тест.
                yield return new WaitForSeconds(1f);
                
                var list1 = new List<int>();
                var list2 = new FastList<int>();
                var sw = new System.Diagnostics.Stopwatch ();
                
                for (var i = 0; i < ItemsAmount; i++) {
                    list1.Add(i);
                    list2.Add(i);
                }
                
                int t;
                
                sw.Reset();
                sw.Start();
                for (var i = 0; i < MaxIteration; i++) {
                    t = list1[i % ItemsAmount];
                }
                sw.Stop();
                Debug.Log(sw.ElapsedTicks);
                
                sw.Reset();
                sw.Start();
                for (var i = 0; i < MaxIteration; i++) {
                    t = list2[i % ItemsAmount];
                }
                sw.Stop();
                Debug.Log(sw.ElapsedTicks);
                
                sw.Reset();
                sw.Start();
                var data = list2.GetData();
                for (var i = 0; i < MaxIteration; i++) {
                    t = data[i % ItemsAmount];
                }
                sw.Stop();
                Debug.Log(sw.ElapsedTicks);
                }
                

                На выходе 3 циферки, надеюсь, что в порядке убывания :) Писал в редакторе хабра, так-что не уверен, то скомпилируется, но суть, думаю, ясна. FastList лежит тут, энумератор не реализован из принципа — для любителей foreach :)


                1. Leopotam
                  30.12.2016 16:22

                  Довольно интересно, но мой индексатор медленнее штатного из-за доп-проверки, массив все-равно почти в 2 раза быстрее. Мои результаты: 16650, 22440, 8650.


      1. AxisPod
        30.12.2016 12:01
        -1

        Ну так я дополнение к статье, именно как вредные советы. Просто видел кучу примеров, где постоянно используют Array.Resize вместо использования коллекций. А так же видел когда нужен именно стэк (для пула объектов к примеру) используют List в виде стека, эмулируя pop и push операции.

        Ну и в плане математики, к примеру вместо того, чтобы взять разницу 2х векторов, такого понагородят, что хоть стой хоть падай. Когда дело идёт дальше, до генерации многогранников и т.д., так вообще какая-то содомия.

        И эти люди учат других из всех сил и часто являются представителями игровых компаний и т.д…


        1. Nagg
          30.12.2016 12:05

          ну Array.Resize к примеру имеет смысл — меньше аллокаций\копирования, прямой контроль над разрастанием массива.


          1. Leopotam
            30.12.2016 13:01
            +1

            Аллокация делается все-равно на каждый ресайз, но сам List — просто обертка над Array и таким же ресайзом.


          1. Deosis
            30.12.2016 13:04

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


            1. Leopotam
              30.12.2016 13:11

              Ну ресайз в листе делается не поэлементно, а сразу в 2х раза по отношению к текущему количеству элементов. Но логику писать придется, да.


            1. afrokick
              30.12.2016 18:18

              или достаточно заранее позаботиться о размере листа new List(сколько будет элементов) :) И тогда не будет ресайзов


        1. Leopotam
          30.12.2016 12:57
          +1

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

          var a = new Vector2(1, 2);
          var b = new Vector2(3, 4);
          var c = a + b;
          

          var a = new Vector2(1, 2);
          var b = new Vector2(3, 4);
          Vector c;
          var c.x = a.x + b.x;
          var c.y = a.y + b.y;
          

          Можете проверить — второй вариант выигрывает всегда, чем больше итераций — тем существеннее. К сожалению в моно нет принудительного инлайна, есть только рекомендация, да и то с с 4.6. в юнити используется 2.6.3, собираются компилятор в unity5.6 (не рантайм) перевести на 4.4, что тоже ничего для инлайна не даст.


  1. ChapayHabr
    30.12.2016 11:46

    еще можно добавить:
    «Использовать для отладки только вывод в консоли, никаких дебагеров с брейкпоинтами»

    PS
    сталкивался как-то: ребята отлаживали игры только через консоль. Рассказывал про дебагер, брекпоинты, стек вызовов, но так и не получилось перевести эти термины на их язык.


    1. Suvitruf
      30.12.2016 11:58

      Честно признать, я за много лет работы с юнити не так часто использовал дебагер (хотя в VS он очень удобный). Обычно логов в консоли хватает. Дебагером пользуюсь, только если что-то совсем страшное происходит и нужно по шагам пройтись.


      1. ChapayHabr
        07.01.2017 11:53

        я абсолютная противоположность )))
        и со слезами на глазах смотрел, как чуваки крэши фиксили расставляя выводы в консоли вместо того, чтобы пройтись дебагером
        Я тогда был новичек в юнити, еще начал уточнять «а где точка входа в приложении? ну где можно поставить первую точку останова?» И мне не ответили, точнее отвечали всякую ахинею вместо не знаю «ну оно по разному», или «юнити сам решает, где ему начинать выполнение» ))) и тд

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

        PS
        У юнити есть очевидный "+" и он гениален — на нем с минимальными знаниями программирования можно писать крутые прилаги!!! Визуальный редактор, куча ассетов, интуитивно понятно кидаешь скрипт на объект и происходит магия!!! это все дико как круто!
        Но «минимальными знаниями программирования» — все же часто становится его "-"


  1. marcor
    30.12.2016 12:38

    Наболело:
    21) Используйте мутабельные структуры. Все это любят.
    22) Изменяйте значения входных аргументов функции. Все это любят ещё больше.
    23) Передавайте в функцию побольше всего. Функция разберётся, что из этого ей надо. Передавайте разными аргументами. Половину — булевскими. Комбинируя сочитания для удобства.

    Комбинируя?
    Своими глазами видел десяток конструкторов в классе. Отличались они порядком параметров.


  1. HOMPAIN
    30.12.2016 12:53
    +2

    По моему про var это не вредный совет. Люблю шарп и пишу в студии с подсказками. Всё равно var не одобряю, поскольку с ним код становится менее понятным. Сам var использую очень редко, только когда объявление переменной очень длинное с точками. И уж точно код хуже не будет, если ты его использовать не станешь, так что где здесь вред совсем не понятно.

    Про foreach тоже не согласен. Практика показала мне, что лучше сразу не мусорить делая игру, чем потом всё это исправлять.


  1. Leopotam
    30.12.2016 13:19
    +3

    18 пункт — тоже ущербен. Каждый старт coroutine создает мусор (который в текущем рантайме моно собирается отвратительно и не надо рассказывать про многопоточную сборку в несколько поколений в актуальном .net clr — в юнити этого нет). В одной из 5.1 версий (могу ошибаться, сейчас уже пофикшено) каждая итерация coroutine вызывала создание мусора. Т.е. просто на ровном месте они что-то внутри поломали и мусор начал течь рекой. В новом uGUI все транзишны цвета на кнопках сделаны через старт coroutine. Т.е. каждый раз, когда наводишь курсор или тычешь кнопку — льется мусор. Мы ради чего пишем код, ради своего удобства или у нас таки геймдев и самое важное — это производительность на конечном устройстве?


    1. WeslomPo
      31.12.2016 11:06

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


  1. FDsagizi
    30.12.2016 15:13

    21 всегда используй RayCastAll юнити запрещяет использовать RayCastAll_NonAlloc


  1. maniacscientist
    30.12.2016 18:12

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


    1. alex_zzzz
      08.01.2017 01:20
      +1

      Мне в своё время хватило родного руководства: https://docs.unity3d.com/Manual/index.html


      Искал, выбирал себе какой-нибудь 3D движок, среди прочих наткнулся на Unity 3.0. Потыкался пару часов и понял ? оно. Следующие два вечера потратил на чтение официального руководства, благо, в отличие от предыдущих движков, оно было и подробное. Прочитал его один раз от начала до конца: что легко шло, прочитал полностью: какие-то очень специфические вещи, типа шейдеров, просмотрел по-диагонали. Стало понятно, что вообще в Unity есть, зачем надо, как оно между собой сообщается и в какую сторону смотреть, когда понадобиться сделать ту или иную вещь.


      Имхо, кроме родного руководства больше ничего не надо. Можно ещё посмотреть обучающие ролики на интересующую тему: https://unity3d.com/learn/tutorials Ко всяким левым «урокам» на Ютубе отношусь скептически.


    1. alex_zzzz
      08.01.2017 01:21

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


  1. Sovent
    30.12.2016 20:48

    Я мимо проходил, Юнити не знаю, но разве здесь преждевременные оптимизации в цене? Высокоуровневые языки разработки неспроста придуманы, предполагается, что с их помощью вы можете больше времени уделять предметной области и меньше техническим деталям. Разве игры не про предметную область в первую очередь? Я сам сильно раздражаюсь, когда игра не оптимизирована и тупит на предназначенном для неё девайсе. Однако мне думается, что игры, при разработке которых все силы были брошены на преждевременные оптимизации, до меня, как до конечного потребителя, не добрались вовсе.
    Реализуйте игровые концепты, делайте стройную гибкую архитектуру, используйте паттерны по необходимости. Сделайте проект легко читаемым и поддерживаемым, а потом берите профайлеры, снимайте дампы и определяйте узкие места. А то можно подумать, что ваши велосипеды будут производительнее и продуманнее, чем вдоль и поперёк исследованные проверенные годами инструменты Microsoft.


    1. Klotos
      31.12.2016 01:36
      +1

      Потому что это не работает: Запоздалая оптимизация
      Если ты не думаешь о производительности с момента появления идеи игры, не пишешь каждую строчку с «учётом производительности» в уме, то в конце, когда ты возьмёшься за профайлер, окажется, что для достижения приемлемой производительности надо переписать что-то в районе 100% всей кодовой базы. Это только в идеальном мире все тормоза сосредоточены в одном методе на 20 строк, который оптимизируется в последний день перед сдачей проекта за 20 минут. В реальности тормоза равномерно размазаны по всей кодовой базе, даже там, где ты их не ожидаешь.

      P.S. Тоже не умею в Unity, но всё, о чём написал выше, опробовал на практике.


      1. Sovent
        31.12.2016 02:05
        -1

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


      1. splatt
        31.12.2016 02:10

        Слушайте, ну зачем делить все на черное и белое?

        Безусловно, если не знать, что такое сложность алгоритма и не понимать, чем отличается dict[«key»] от list.Where(e => e.name == «key»), то можно наворочать такого, что прийдется переписывать 100% кодовой базы.

        Но и оптимизировать на спичках с самого начала — тоже глупо. Строить проект на костылях, пытаясь ускорить обращение к transform в полтора раза (когда в рамках всего проекта этот вызов занимает 0.00001% процессорного времени по сравнению с другими вызовами) — это и есть оптимизация на спичках.

        Если я не уверен, станет ли определенное место бутылочным горлышком или нет, я пытаюсь написать его так, что бы потом его было легко отрефакторить, без переписывания 100% кода.


  1. SadOcean
    31.12.2016 01:55
    +2

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


    1. WeslomPo
      31.12.2016 10:56
      +1

      7-й пункт как раз полезный. Нужно прятать все свойства от изменения их программистом. Потому что чаще всего это и не требуется. Вообще максимально закрытый класс это хорошо. Изменение переменных либо через геттеры, либо через метод с понятным названием. Десять раз стоит подумать прежде что-то объявить публичным, либо виртуальным.

      Этот пункт должен звучать как: объявляй все поля и функции публичными вдруг тебе нужно будет поменять значение или вызвать функцию.


      1. SadOcean
        01.01.2017 02:29

        Я кстати знал людей, не разделявших такой простой мысли и делавших все на всякий случай публичным.


        1. WeslomPo
          04.01.2017 23:29

          Я думаю, все такими были когда-то… :)


  1. Charoplet
    31.12.2016 20:22

    Добавлю:

    • каждый класс должен быть наследником MonoBehaviour, даже если это класс никогда не будет компонентом игрового объекта
    • если тебе нужно, чтобы некоторые классы при инициализации выполняли одну и ту же логику — не делай базовый класс. Просто копипасть эту логику в каждый класс, потому что наследование, полиморфизм — это все ужасно медленно и сложно
    • для доступа к объекту активно используй transform.GetChild(i). Можно даже несколько раз: transform.GetChild(2).GetChild(4).GetChild(0).GetChild(0). Если кто-то поменял иерархию объектов и все сломал — сам виноват.
    • если ты вставил хак или костыль из-за производительности/бага в ОСи/бага в Юнити — не оставляй никакой комментарий на этот счет. Все всегда будут понимать, что этот костыль нужен, и никто его не уберет.


  1. Garynya
    31.12.2016 22:25
    -1

    Вы про эту статью писали? Просто магия какая-то. Сейчас обе статьи открыты — читаю не могу понять кому верить.


    1. Leopotam
      01.01.2017 17:52

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


      1. Garynya
        01.01.2017 23:16

        Таким образом делаем вывод, что шутки в этой статье не смешные и советы не такие уж вредные?


        1. Leopotam
          02.01.2017 00:17

          Они так скажем — неоднозначные. А лучше почитать все комментарии к этой статье и попытаться понять, почему.


          1. Garynya
            02.01.2017 02:05

            И к той статье тоже. И понять, что нет ничего однозначного в этом мире.


  1. isuo
    02.01.2017 00:25

    Хотел удостовериться, что в комментариях есть поправка насчет CamelCase. Ее не было. Извините


  1. alex_zzzz
    02.01.2017 00:25
    +1

    Мой вредный совет:


    Всегда верьте статьями с «полезными» советами. Не тратьте время на проверку, делайте в точности так, как советуют. Авторы статей ? умные люди, они знают всё о вашем проекте: целевую платформу, жанр, все потенциально узкие места. Они даже точно знаю версию Unity, которую вы используете.


    Начиная с версии 5.5 в Unity используется компилятор C# из Mono 4.6. В нём нет бага с аллокациами в foreach из-за боксинга итераторов. Да и до версии 5.5, если этот баг действительно на что-то принципиально влиял, его, имхо, было проще обходить использованием для финальных билдов другого компилятора, чем переписыванием исходного код на for. Сейчас же тем, кто переписывал foreach на for, можно начинать переписывать обратно. :)


    1. Leopotam
      02.01.2017 13:37

      Начиная с версии 5.5 в Unity используется компилятор C# из Mono 4.6.

      sealed class NewBehaviourScript : MonoBehaviour {
          readonly List<int> _list = new List<int> ();
          void Start () {
              for (int i = 0; i < 10; i++) {
                  _list.Add (i);
              }
          }
          void Update () {
              var sum = 0;
              IList<int> data = _list;
              foreach (var item in data) {
                  sum += item;
              }
          }
      }
      

      Пробуем, удивляемся, что не все энумераторы подхачены, возвращаемся обратно на for.


      1. alex_zzzz
        02.01.2017 19:33

        Не удивляемся. Интерфейсы ? отличный архитектурный инструмент, но и щедрый источник боксинга, причём не только внутри foreach. Ничего специфичного для Unity.


        IList, ICollection, IEnumerable, IDictionary аллоцируют энумератор; List, HashSet, Queue, Stack, Dictionary ? нет.


        1. Leopotam
          02.01.2017 19:35

          Я про то, что зачем лишний геморой и воспоминания о том, генерит тот или иной энумератор мусор или нет? Проще писать в едином стиле и с потенциально мутабельным / крути-как-хочешь итератором, чем мучаться с foreach.


          1. alex_zzzz
            02.01.2017 23:29

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


            Stack, Queue, HashSet, Dictionary, LinkedList, ICollection, IDictionary, IEnumerable на for не переводятся, у них нет индексаторов. Можно перевести на while с использованием итератора, но получим тот же foreach. LinkedList разве что можно перевести на while без итератора, но у него и так нет проблем с foreach.


            На цикл for можно перевести массивы (но у них нет проблем с foreach и никогда не было), List (но у него нет проблем с foreach) и IList ? вот он, единственный случай, где от for есть польза.


            Т.е. выходит, что правило «не использовать foreach» существует ради одного единственного случая с IList, а в остальных случаях либо бессмысленно, либо малоприменимо.


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


        1. Leopotam
          02.01.2017 19:40

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


          1. FadeToBlack
            03.01.2017 20:25
            -1

            Прекрасно, спички сэкономлены, но кэш скажет вам огромное спасибо за то, что вы по памяти против шерсти выборку ведете.


            1. Leopotam
              03.01.2017 21:15

              С чего бы это? Кеш выбирает память линиями, т.е. не важно в каком направлении идешь, главное, чтобы последовательно.


  1. Leopotam
    02.01.2017 13:36

    Промахнулся, ответил выше.


  1. FadeToBlack
    02.01.2017 23:18

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


    1. Suvitruf
      04.01.2017 15:13
      +1

      Не надо смотреть на оптимизации только как на «кеширование трансофрма» — это комплексная задача. Если устранить одно медленное место, то это может и не будет заметно, но если таких мест (которые по отдельности в расчёт не брались) накопится достаточное количество, то прирост производительности будет немалый.