Достаточно много времени я потратил на рефакторинг одного проекта, который судя по заявлениям автора проекта был основой для обучения студентов в МФТИ. Выполнен этот проект просто ужасно, учить так студентов — пожалуйста так не нужно.

Весь рефакторинг выполнен онлайн в виде 6 частей, каждая по 2-3 часа стримов на ютубе, ниже я дам только ссылку на затравку, а полные стримы вы сможете найти на том же канале.
Разбор чужого кода — так учат делать Tower Defence

В статье же, будет представлена сухая выжимка — резюме, что было сделано в рефакторинге и подискутируем на тему — почему так не правильно учат? Если же у вас на первый взгляд появится субъективное мнение, что я где-то не прав, это нормально. Но лишь задумайтесь, если я на протяжении порядка 15 часов рефакторингов только и делал то, что удалял лишние сущности и связи между ними, то может все таки в моем взгляде есть что-то?





Конечно, если вы скажете, что можно не соблюдать ООП, то статью можно закрыть — и пусть ваш другой подход будет у вас на вашей совести. Эта статья про то как ООП в Юнити облегчает программирование.

Первое с чего начался рефакторинг — это устранение использования ScriptableObject, которые использовались как некоторые структуры, на основании которых создавались объекты данных. Очень рекомендую очень хорошо подумать, использовать ли вообще ScriptableObject. Дело в том, что в 99% они вам будут просто не нужны. Подавляющее число объектов реального мира помимо состояния, отражаемого данными, всегда имеют в себе свое поведение. И ООП нас учит, что нужно так декомпозировать объекты реального мира на классы, чтобы они содержали бы в себе и состояние, и поведение вместе. В этом по сути базис ООП. А значит использование ScriptableObject может понадобится лишь в исключительных случаях, когда объект вырожден. Например, хороший пример это описание какого-то стиля UI — цвет, фонт, материал и т.п., что потом применяется к некому диалогу в UI. Но если это базовые объекты Enemy, Turret самой игры — нельзя их настраивать через ScriptableObject.

Второе, не используйте доступ к объектам/переменным через static поля/свойства. В данном примере существовал класс Game с набором static полей, и все, кому что-то было нужно, просто брал это через него. Возможно, вы не всегда отдаете себе в этом отчет, но это же все равно, что использовать глобальные переменные, которые вот уже несколько десятков лет так не используют.

Третье, в проекте видимо хотели реализовать MVC. И я уже видел, что почему-то некоторые компании даже требуют так реализовывать свои игры. Но первое, ответьте себе на вопрос, что даст вам реализация MVC — в вашем проекте? Если вы посмотрите мой пример рефакторинга на канале, вы заметите, что автору проекта MVC не дал ничего, кроме головной боли и разбиения бизнес-логики на части в разных классах. Напишите в комментариях — для чего вам MVC в проекте юнити? И я уверен почти все ответы будут неправильными :) буду рад, ошибиться.

Четвертое, я периодически сталкиваюсь с мнением, что в Юнити нужно минимизировать использование MonoBehaviour? Но, почему? Как далеко вы готовы зайти, чтобы испортить себе жизнь не используя MonoBehaviour. MonoBehaviour позволяют замечательно использовать редактор Юнити, позволяет обмениваться ссылками между компонентами не написав ни строчки кода, видеть состояние объектов через них во время отладки.

И пятое, когда вы придумываете любую свою архитектуру, не позволяйте своему разуму вводить вас в заблуждение. Если вы вводите лишние сущности, разделяете классы/методы/свойства так, что образуются только лишние связи — ваша архитектура совершенно избыточная и неверная. Если в результате рефакторинга законченного проекта, получается его существенно сократить, не уменьшив функциональности, который этот проект выполняет — это приговор вашей архитектуре. Все другие аргументы померкнут по сравнению с этим. Аналогично принципу бритвы Оккама, архитектура должна проверяться на минимизацию сущностей и связей между ними. И если другой подход позволяет их сократить, он однозначно лучше. И казалось бы такая банальная истина, но очень, очень часто она не выполняется и даже такому учат в университетах. Печально… почему так происходит?

Я наверно всё-таки дам прямые ссылки на стримы и желающие смогут детально просмотреть как выполнялся рефакторинг, и возможно сам рефакторинг будет полезней обучения на таких курсах:
Часть №1
Часть №2
Часть №3
Часть №4
Часть №5
и заключительная №6 (не факт, что уж прям заключительная)

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


  1. tac Автор
    01.06.2022 21:25

    Много людей судя по просмотрам частей рефакторинга составили свое представление на основании только первой части. Да, мой недосмотр, что я не успеваю сделать таймкоды. Я сделал их ко второй части. В первой части делался грубый рефакторинг и было мало объяснений и не всегда людям с другим опытом легко вникнуть. Вторая часть даст больше пояснений и проводя рефакторинг дальше станет более очевидно правильность направления улучшения этого проекта.


  1. cry_san
    01.06.2022 08:50
    +3

    Наверное, лучше бы написали свой мини-курс по созданию TowerDefence. По времени вышло бы намного меньше, чем разбор чужого кода.


  1. DrinkFromTheCup
    01.06.2022 10:08
    +4

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

    Вы знаете, краткий ликбез для совсем "не отстреливающих" типа меня тут бы всё-таки не повредил. В код того Tower Defence я не смотрел, потому не рискну пример на его основе брать. Попробую отсебятину.

    Допустим, я хочу сделать клон Game & Watch. Осовременить старого доброго "волка с яйцами", например.

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

    Одна для текущей позиции волка, по одной для каждого жёлоба для яиц, одна для зайца, счётчик очков, счётчик жизней, переключатель паузы (с положениями "игра идёт" - "пользователь взял паузу" - "пауза на анимацию разбитого яйца" - "пауза по окончании игры").

    По одному контроллеру на анимацию волка (со считыванием последней нажатой управляющей клавиши, может быть, в отдельной переменной), зайца и яиц, контроллер учёта очков (и по совместительству обсчёта разбития яйца / потери жизни / окончания игры).

    Вроде, ничего не забыл.

    Чем плохо использование глобальных переменных в таком случае?


    1. tac Автор
      01.06.2022 10:18

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

      :) я думаю, автора цитаты указывать не нужно


      1. Leopotam
        01.06.2022 12:24
        +3

        Теория без практики мертва. Слепо следовать "чистому коду", solid-у и прочему менеджерскому хайпу с цитированием 80-летних дедов с совершенно другим техническим окружением, провозмогая и просто потому что так надо - не стоит. Если данный подход решает проблему - нет смысла оверинженерить.


        1. tac Автор
          01.06.2022 16:21
          +1

          Серьезно, теперь наступило такое время, что не признаются даже основы? А ну тогда ладно ...


          1. Leopotam
            01.06.2022 17:42

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

            З.Ы. Удивлен, что не было ни слова, что все надо хранить в субд и вообще ее упоминания - необычно. :)


            1. avengerweb
              03.06.2022 23:56

              Да что вы со своими кровавыми интерпрайзами. Подход у нас такой же, каждый пилит так чтобы работало. Отличается только тем что php с логикой около global, он как 20 лет назад жил, так и сейчас живет, иди вон попробуй туда фичу запилить. Пол гига легаси на Java 7? Изи! 50% кода это дубликаты, но потому что живет оно по 20 лет+, вплоть до того что фичу запилили 15 дет назад, 10 лет назад фичу с view убрали про фичу забыли, Новому индусу задачу поставили точно такую же фичу запилить год назад, он человек простой, сказали сделал.


          1. mrguardian
            02.06.2022 20:38

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


      1. DrinkFromTheCup
        01.06.2022 14:01

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

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


        1. tac Автор
          01.06.2022 16:54

          Разница в частоте возникновения данных событий. И скорости исправления таких ошибок.


          1. DrinkFromTheCup
            01.06.2022 22:05

            Это всё объясняет. Но всё-таки не помешало бы уточнение.

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


            1. tac Автор
              01.06.2022 22:13

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


            1. tac Автор
              01.06.2022 22:58

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

              значительным достижением является введение иерархии классов в процессе проектирования. Именно это называется OOD и именно здесь преимущества C++ демонстрируются наилучшим образом ... это означает не только уменьшение объема кода программ, но и удешевление проекта за счет использования предыдущих разработок, что дает выигрыш в стоимости и времени.

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


              1. BellaLugoshi
                02.06.2022 05:54

                когда я писал ДЛЯ СЕБЯ простую утилиту на полтора экрана кода, я сначала решил, что прям-прям вот сейчас как наООПэшу по полной программе и начал её писать. После третьего экрана описания только структуры объекта и без начала написания самой программы я всё же вспомнил свой разговор с преподавателем еще далекого 95-го года об ООП и собственно там речь была об увесистом ПО или о библиотеках , то есть как раз об удобстве разработки больших проектов и о хорошей переносимости кода - если же эти условия не выполняются, то писать ООПэшный код вообще не имеет смысла. Я к чертям удалил всё и написал процедурный код в одном блоке Main().


                1. tac Автор
                  02.06.2022 07:00

                  Конечно, же в вашем негативном опыте оказался виноват ООП, а не ваше понимание задачи? ;)


                  1. BellaLugoshi
                    02.06.2022 08:12

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

                    Во-первых, я написал о положительном опыте.

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

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

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


                    1. ZZZubec
                      02.06.2022 09:12

                      +1


                    1. tac Автор
                      02.06.2022 09:19

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

                      Я уже достаточно давно исповедую принцип ООП, но далеко не безоглядно, и когда нашел соответствующий пример (его и только его), и когда он не настолько банален по принципу "мне лень, не хочу делать через ООП" я о нем честно пишу.

                      Прошу, тогда уж ознакомьтесь:

                      тут попроще - о проблемах ООП

                      +

                      Усовершенствование паттерна Flyweight в биовычислениях


                      1. BellaLugoshi
                        02.06.2022 11:18

                        Прошу, тогда уж ознакомьтесь:

                        тут попроще - о проблемах ООП

                        Как ни странно, но полная статья понятнее чем блок "попроще".

                        Но даже в обсуждении "попроще" вам комментаторы уместно возражают и хардкорном ООП везде и всюду. То что вы это не приемлете - не значит что это константа для всех.

                        Я спорить не вижу смысла, но к примеру ваша же отсылка к 1000 объектов и мой самый первый пример где всего 1 объект и десяток свойств как бы несравнимые вещи, вы указываете на сложности в использовании ООП в конкретно взятом проекте, а я пишу о нецелесообразности его использования с маленьких проектах, то есть примеры говорят о разных вещах. Так же мои слова в ваш адрес, о том что вы ярый сторонник ООП использующий этот подход везде и всюду являются правдой которую вы подтверждаете сами в "попроще".

                        Так что получается что я правильные выводы сделал и о вас и о вашем подходе к вопросу, а вот вы меня так и не поняли.


      1. 0x131315
        03.06.2022 23:24

        Понимание базовых истин и принципов приходит с опытом, через боль и страдания.

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

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

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

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

        В любом случае, по наблюдениям, первые 6-18 месяцев человек упорно пытается придерживаться процедурного подхода, генерируя страшный код. Потом обычно начинается прогресс - начинает приходить понимание, накапливается опыт. Но не у всех, к сожалению.

        Серьезный бустер тут - обмен опытом с более опытными коллегами: ревью и рефакторинг чужого кода, обсуждения. Что-то постигается самостоятельно, что-то подсматривается в коде более опытных коллег, что-то выносится из обсуждений/проблем, свидетелем или участником которых был. Этого к сожалению типичные курсы тоже не дают - даже если есть группы, они одного уровня.


  1. oblakooblako
    01.06.2022 11:00

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

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

    MVC от части дает способ разбиение задач. Договорившись каким интерфейсом обмениваться, множество студентов могут работать над одной задачей. Один над вью, другой над моделью итд.

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

    Про архитектуру я промолчу, с вами категорически не согласен в вашем рефакторинге.


    1. tac Автор
      01.06.2022 11:12

      MVC от части дает способ разбиение задач. Договорившись каким интерфейсом обмениваться, множество студентов могут работать над одной задачей. Один над вью, другой над моделью итд.

      Вы заблуждаетесь. Если для проектов WPF/WindowsForms/ASP.NET MVC хоть что-то дает, а именно позволяет отделить визуализацию от бизнес-логики. То в случае с Unity - в этом нет никакой необходимости. Над "View" работает 3d-моделлер или дизайнер уровней. Как такового кода UI в юнити просто нет. Есть бизнес-логика отображения, и её отделять от другой бизнес-логики совершенно не нужно. И как вы можете видеть на всем протяжении рефакторинга - проект существенно упрощался, бизнес-логика искусственно разделенная на части совмещалась, становилась в разы меньше, убиралось дублирование и бесполезный обмен запутывающими ссылками.

      Если же для разделения работ, требуется написать архитектуру содержащую в 3-5 раз больше сущностей и перекрестных связей, то не кажется ли вам, что эта архитектура уже своим наличием превышает работу 3-х программистов, которую один мог бы выполнить без всякого разделения?

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


      1. oblakooblako
        01.06.2022 11:31

        Про MVC думаю я не прав. Но ваша архитектура ничем не лучше архитектуры, которая была, вам просто так удобно, по своему. Но для обучения проект годный, да тут не ecs , mvvm, mvp-rx или ecs-rx, но основу получить можно, студентам заинтересованным и не очень.


        1. tac Автор
          01.06.2022 11:39

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


      1. oblakooblako
        01.06.2022 11:38
        +1

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


        1. tac Автор
          01.06.2022 11:43

          имхо, для обучения полезнее посмотреть рефакторинг :) вы посмотрите полнее, там много чего интересного по ходу дела .. по первому стриму если только судить, можно получить немного превратное мнение, постепенно там становится все более и более безапелляционно. Но это требует времени, времени которое должен тратить ведущий программист на горе junior`ов, которых обучил молодой преподаватель "по кайфу"


      1. DrinkFromTheCup
        01.06.2022 12:47

        Как такового кода UI в юнити просто нет.

        Я тут влезу немножко и сморожу очередную глупость.

        Относительно кода как сущности понятия UI не существует.
        Понятие UI существует только в разрезе собственно логики, в отрыве от кода.

        Разделение логики и визуализации - это да, нужно-важно-неоспоримо.
        Но что делать, если сам инструмент активно противится чёткому разделению И созданию удачной архитектуры?

        Более того. Что делать, если отсутствие разделения на начальном этапе - неизбежно?
        Между фазой "каждая сущность - объект! боже, сколько же их..." и фазой "здесь объект не нужен, здесь объект не нужен, там тоже не нужен... вуаля! лаконичный поддерживаемый код!" - огромная куча промежуточных шагов. В т. ч. шагов по перестройке образа мышления.

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


  1. fstleo
    01.06.2022 12:20
    +2

    Принцип "Если мне это не надо, значит никому не надо" в действии.

    ScriptableObject - это префаб, только без трансформа. Делать из него игровые сущности так себе затея, но это отличный механизм для хранения настроек. Хранимый на сцене GameObject с параметрами может быть просто уничтожен, его трудно редактировать и переиспользовать.

    MonoBehaviour по сравнению с POCO - оверхед на менеджмент GO, компонентов, времени жизни, плюс всякие подводные камни типа сравнения с null. Ну и иногда просто не хочется быть привязанным к UnityEngine.

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

    MV* позволяет не держать в своей голове сразу всё. Можно собрать всю игру в одном классе, в этом нет ничего плохого. Но в какой-то момент игра разрастается до такого состояния, что менеджмент UI, настроек, сети, файлов хочется все-таки разделить на более контролируемые части.

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

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


  1. Revertis
    01.06.2022 15:53
    +1

    Ещё лет 20 назад я услышал фразу, которая хорошо характеризует ситуацию: "кто не умеет работать - тот учит".


    1. velik97
      01.06.2022 18:13

      Работать умею, спасибо)
      Но хотелось попробовать себя и в преподавании. Спойлер: совмещать работу и преподавание очень сложно.


  1. Reposlav
    01.06.2022 16:17
    +1

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

    А еще в принципах ООП указано Наследование. Однако уже давно определено, что наследование часто бывает вредным.

    Само заявление, что класс должны содержать в себе и состояние (если здесь идет речь именно про динамическое состояние, а не readonly зависимости при DI) и поведение вместе, довольно спорное. Оно актуально для некоторых паттернов, таких, как Builder. Но для слоя бизнес-логики подобный подход стоит использовать с осторожностью.

    Четвертое, я периодически сталкиваюсь с мнением, что в Юнити нужно минимизировать использование MonoBehaviour? Но, почему?

    Для начала, MonoBehaviour - это достаточно дорого для производительности.

    Второе - это проблема с отслеживанием зависимостей и взаимодействия различных систем между собой.


    1. tac Автор
      01.06.2022 16:43

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

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


      1. Reposlav
        01.06.2022 17:35

        ООП сформировалось полвека назад, за это время оно прошло серьезную эволюцию. Более того, те принципы, которые вы проповедуете, были описаны много позже самого появления понятия ООП.

        Если для вас неучи - это те, кто не застрял в семидесятых, то ок.


  1. velik97
    01.06.2022 17:53
    +2

    Я являюсь автором этого курса.

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

    Я вижу своей задачей создать конструктор для геймдизайнера, в котором он сможет создать собственно игру. Без геймдизайнера мой проект выглядит больше как конструктор, в котором пока еще не во что играть. И пока не увидишь работу геймдизайнера, вся эта сложность и гибкость может показаться излишней. Но когда ему придется настроить 20 юнитов, у которых чуть отличаются абилки, параметры и тд и тп, они будут вам очень благодарны за такую гибкость. Говорю это как человек, который два года проработал над очень крупной РПГ. Мне хотелось подготовить студентов к большим проектам, поэтому я пытался привить им такой подход. Не утверждаю, что у меня это получилось хорошо (первый раз пробовал преподавать), но по крайней мере цель была такая.

    1) Мне кажется, что SO гораздо более гибкие для настройки игровых сущностей и не имеют лишних полей (типа Transform), лишних функций префаба, что дает меньше возможности запутаться. Проверено опять же на крупном проекте.

    2) Использование static полей. Использование их налево и направо действительно чревато, но если создать одну точку входа в виде Game, то я не вижу особых проблем в этом.

    3) MVC. На данном этапе игры это кажется излишним, но на самом деле это задел под сериализацию, мультиплеер и сохранение состояния игры в файл (сейвы проще говоря). В этом случае мы сохраняем/передаем по сети только Data. Это кроме того, что так на мой взгляд код гораздо лучше декомпозируется. Плюсы от этого легче ощутить, работая над большим проектом в команде.

    4) Как уже кто-то написал выше, частое использование монобехов ведет к сложностям с отслеживанием зависимостей. Я очень много раз на это натыкался на крупном проекте (особенно в UI). Что-то где-то произошло, а ты не понимаешь почему. А оказывается там в каком-то мелком объекте выставлена криво ссылка. Дебажить это почти нереально. А вот если все из кода, проблема становится сильно проще. Но тут опять же надо ловить грань: что является ответственностью программиста, а что является ответственностью дизайнера. И надо стараться не давать дизайнеру ничего лишнего, потому что они рано или поздно ошибутся, а баг вам искать)

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


    1. tac Автор
      01.06.2022 18:09

      Надеюсь без обид, надеюсь говорить, что я с вами не согласен - излишнее. Именно для больших проектов так делать еще хуже. То, что вы хотели и что в итоге получилось - разные вещи. Нельзя на примере, где нет в этом необходимости, показывать то, что хочется. Я планировал, закончить эту демонстрацию показав, где, какое и главное когда возникает необходимость в MVC. Но количество рефакторинга меня накрыло ... но я думаю я сделаю этот итоговый ролик. Подписывайтесь на канал :)


      1. velik97
        01.06.2022 18:20

        Обид конечно нет. Я основывался на своем опыте работы в крупной студии. Если у вас другой опыт, могу понять.


        1. tac Автор
          01.06.2022 18:26

          Если такого рода архитектуры действительно делают в Owlcat Games - у меня к ним много вопросов. Но кто я такой, чтобы они консультировались бы у простого смертного :)


          1. velik97
            01.06.2022 18:36

            Ага, только там все раз в 10 все сложнее. И на мой взгляд все оправданно. Когда стоит задача создать игру с таким масштабом и с такой гибкостью, работая в команде 50+ человек, другого пути я не вижу. И тут даже вопрос не в теории. Попробуйте перевести все SO на монобехи (боюсь соврать, но по моим ощущениям там 100к+), юнити просто колом встанет и все тут) Она итак еле живая была с таким количеством данных.

            Ну и как адекватно сохранять такое большое дерево данных со всеми абилками со всеми стейтами игровых сущностей без MVC я не представляю.


            1. tac Автор
              01.06.2022 18:47

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

              И прятаться за "объем" - такое себе, что работает плохо на малом объеме, еще хуже работает на большом ...


              1. SaharnyMishka
                01.06.2022 21:25

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

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


                1. tac Автор
                  01.06.2022 21:37

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


                  1. pecheny
                    01.06.2022 22:04

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


                    1. tac Автор
                      01.06.2022 22:29

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


            1. tac Автор
              01.06.2022 18:53

              А и порадую, видимо, моего старого знакомого, правда в гримме я его не узнаю @Leopotam

              Отвечая на вопрос

              как адекватно сохранять такое большое дерево данных

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


              1. pecheny
                01.06.2022 21:38

                Нормализацию придумали не для «максимально все оптимизировать», иначе не пришлось бы придумывать денормализацию.
                И не бывает «правильной» архитектуры, а бывают архитектуры удовлетворяющие (или не удовлетворяющие) определенному набору требований.


                1. tac Автор
                  01.06.2022 21:48

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


                  1. pecheny
                    01.06.2022 21:57

                    Чьей – «её» оптимальности? Как на нее могли пойти, предварительно не придумав? Вы заглядывали на определение денормализации хотя бы там на википедии?


                    1. tac Автор
                      01.06.2022 21:59

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


                      1. pecheny
                        01.06.2022 22:17
                        +1

                        Денормализация – это не когда «нормализацию не осилили», как можно подумать на основе ваших комментариев.


              1. velik97
                02.06.2022 12:28

                БД это ответ на вопрос где хранить сейвы. А MVC отвечает на вопрос как данные должны быть структурированы в игре, чтобы их было максимально удобно далее сериализовать/десериализовать. Например с помощью NewtonJSON можно одним вызовом сериализовать класс со всем его деревом полей. Поэтому если у вас все данные выделены в отдельную область (Data или Model), то их одним вызовом можно перенести в json и обратно. А если этого нет, то придется каждое поле руками сериализовать. И это не номер пройденного уровня, а весь игровой стейт. Все позиции персонажей и мобов, все абилки, весь инвентарь, все статусы, весь лут на карте, все дерево прогрессии персонажа. А если вам нужно просто номер уровня сохранить, то подойдут внутренние механизмы юнити, типа PlayerPrefs.

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

                И приходилось ли вам вообще писать систему сейвов где есть весь игровой стейт?


                1. tac Автор
                  02.06.2022 12:35

                  Конечно, мне приходилось реализовывать сохранения, причем во всех перечисленных вариантах - и в БД на сервере, и в БД у игрока, и в файл у игрока ...

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

                  Вот кстати:

                  DirectConvertor - прямые конвертации альтернатива сериализации [сохранение ваших игр]


  1. velik97
    01.06.2022 18:08

    И маленькая ремарка, это был курс по выбору. Его обязательно пройти, если ты его выбрал, но выбирать его не обязательно.


  1. tac Автор
    01.06.2022 21:25

    Много людей судя по просмотрам частей рефакторинга составили свое представление на основании только первой части. Да, мой недосмотр, что я не успеваю сделать таймкоды. Я сделал их ко второй части. В первой части делался грубый рефакторинг и было мало объяснений и не всегда людям с другим опытом легко вникнуть. Вторая часть даст больше пояснений и проводя рефакторинг дальше станет более очевидно правильность направления улучшения этого проекта.


    1. pecheny
      01.06.2022 21:48

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


  1. Lekret
    02.06.2022 09:43
    +1

    Я не согласен касательно MVC и разделения сущностей.

    Объекты имеет смысл делить, даже если повторное использование не требуется. При разработке стоит проблема сложности логики, высокая сложность усложняет поддержку кода и повышает количество багов. Когда мы делим классы или методы на несколько маленьких (без фанатизма и с умом), то сложность снижается и код становится гибче.

    Логика "меньше сущностей - лучше" выглядит как попытка свести всё к большим объектам по 500-1000 строк кода, хотя я думаю автор статьи и не имел это в виду, но чем больше проект, тем вероятнее такое будет происходить, если не делить код на сущности и не создавать между сущностями связи. Такие вещи как low coupling и high cohesion не просто так были придуманы, как и правила по количеству строк в классе/методе. Всё это напрямую связано со сложностью кода, поддержке, рефакторингу, расширению и т.д.

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

    Ну и касательно создания абстракций над Unity и вынос модели в отвязанный от Unity код. Это тоже имеет смысл. Часто не хочется чтобы цикл жизни каких-то объектов был привязан к визуальным объектам на сцене. Если у меня есть режим игры, либо игровая валюта игрока и т.п., то мне абсолютно не нужен объект на сцене на такие вещи. Я хочу поставить какой-то главный объект на сцену, который внутри себя создаст то, что нужно будет для работы любой игровой сцены, либо меню.


    1. tac Автор
      02.06.2022 09:51

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

      Только почему то одни - восприняли мою статью с тезисами, как разговор об сфере применения ООП. А она таковой не является, хотя такую я писал лет 10 назад здесь ...

      На самом же деле - эта статья и рефакторинг о наглядном, плачевном состоянии умов ... когда нет понимания, когда нужен MVC. И делают абы было, да еще и не правильно. Вот о чем речь ... Это совсем не отменяет того, что MVC может быть полезна и о том, что её можно правильно реализовать. Но лучше без MVC если она не нужна или не выполняет свою роль.

      Именно, тогда и только тогда, я говорю учат неправильно.

      Единственно, у вас не верный критерий:

      Когда мы делим классы или методы на несколько маленьких (без фанатизма и с умом), то сложность снижается и код становится гибче.

      Логика "меньше сущностей - лучше" выглядит как попытка свести всё к большим объектам по 500-1000 строк кода

      Совершенно без разницы сколько строк кода составляет класс или метод. Да это обычно коррелирует с тем, что там может быть что-то не так. Да, у Фаулера - это чуть ли не первый признак "кода с душком". Но вы то хоть видели код над которым я проводил рефакторинг - там пустые методы! И даже если бы они имели тяжелую логику, не это является признаком.

      Когда вы начнете следовать ООП и правильно декомпозировать объекты, вы с удивлением увидите, что "больших" классов/методов у вас не будет. Это произойдет само собой, и в этом и есть преимущество ООП. Почему так произойдет? Потому что вы будете основываться на связности и зацеплености объектов. А не на искусственном разделении "большого" . А это и означает - повторное использование. Поэтому я с вами соглашусь, хотя вы пока еще не поняли что "(без фанатизма и с умом) " это и есть "когда возникает необходимость в повторном использовании".


      1. BellaLugoshi
        02.06.2022 12:35

        как не залезешь на github - проект из 15-30 файлов .cs, каждый на 7-15 строчек кода, программа уровня "hello world!", без поллитра не разобраться, без примера вообще не ясно что и как работает, прыгаешь туда-сюда из файла в файл - сомнительное какое-то удобство.

        нет, я понимаю что я тот еще валенок и не проецирую свою тупость на всех, я в целом о характере кодинга, когда читая asm файл для 6502 я понимаю всё куда лучше и быстрее.

        что "больших" классов/методов у вас не будет

        Я делал обработчик файла, там одна процедура была на 400-500 строк. При желании можно было в паре мест что-то выделить в отдельные процедуры, ну стало бы 350 строк. Не совсем ясно какая связь ООП и количества команд. Если 350 команд раскидать по 10-ти методам то кроме ухудшения чтения кода ничего не получится. Тут же и проблема с оптимизацией кода, когда что-то уходит в отдельный метод, то нужно использовать или глобальные данные или данные копировать или передавать ссылку на данные, то есть выделение кода в отдельный метод становится еще одной задачей, которой не было до этого.

        когда же на самом деле нужен MVC

        Тут важно понимать что MVC не привязан к ООП.


  1. svkozlov
    02.06.2022 11:31

    Каждый понимает ООП и шаблоны проектирования как хочет.


  1. Ma0oo
    02.06.2022 23:44

    1 – Насчет SO я в принципе согласен. Если первая мысль при решении проблемы – использовать СОшки, значит есть другое решение. Но и полностью отрицать СО нельзя. Их можно использовать для создании данных для уровней , использовать пустые СО в виде ID, использовать в качестве конфига игры, который будет пылиться в DI или Service Locator.  

     

    2 – Сатик поля для доступа к объектам. Ну да, не хорошо, я бы так не делал и другим бы не позволил, но в качестве альтернативы ServiceLocator или простого DI для студентов подойдет. Я бы лично для небольшого проекта, да и для любого использовал ServiceLocator такого вот вида. Есть пить не просит, с рефлексий не балуется - ну и пускай существует.

    public class Services<T>
        {
            private T _service;
    
            private static Services<T> _i;
            public static Services<T> S=>_i??=new Services<T>();
    
            public T Get() => _service;
    
            public T Set(T service) => _service = service;
        }

    Почему нужен Di\ServiceLocator? У тебя будет как минимум Controller сцены, который обязан её инициализировать при её загрузке: спавн уровня, спавно игрока, иницилизация всех других сущностей теми, которых он только что заспавнил. А ещё точно будет Mediator между игрой и всем UI. Вот уже как минимум две сущности, к которым захочется достучаться в любое время и им как раз самое место в DI, ServiceLocator или на худой конец в статики.

     

    3 – Про MVC. Вот тут чистое имхо, ногами не бить. MVC в юнити с вероятностью 99 процентов не нужен, но если его все же использовать то модель должна быть точно такой же компонентной.  То есть мы имеем какой-нибудь DataObject с CURD интерфейсом. А это за собой тянет следующие вещи:

    1. Интеграцию с юнити инспектором

    2. Отдельный редактор если надо посмотреть базу вне юнити

    3. Система сохранение этих штук в отдельный файл

    4. Если файл большой, то придется использовать кокой нибудь b-tree, чтобы просто этот объем не хранить в оперативки, а иметь к нему доступ, как мы имеем доступ к ассетам в редакторе через AssetDatabase.

    5. А ещё может понадобиться язык сценариев, например JINT (реализация JS). Это заместо UnityAction или UltEvent, которых не будет в базе.

    В итого это мини DataBase. Я такое искал – не нашел, если у кого-то есть ссылка, то милейше прошу поделиться. Иначе придется писать свой, использую BplusDotNet.

    4 – тут исключительно согласен. Больше компонентов богу компонентов. А ещё если сверху приправить Odin и новым UI Toolkit с котором писать свои редакторы стало в разы проще, то работа с юнити становится просто наслаждением.

    5 – Тоже согласен. Только я бы хотел добавить, что не надо бояться создавать пустые MonoBeh или SO. Такие пустые классы могут служить марками для других классов, которые будут работать с теми game object, на которых навешена пустышка.