Як правильно структурувати проєкт в Unity: що треба і не треба робити

Всім привіт! Мене звати Максим, я Unity Developer в Pingle Game Studio. У цій статті я хочу поговорити про структуру проєкту в Unity.

Добре структурований проєкт має ряд переваг. У першу чергу — зручність. Зручність для розробника, який його почав, для команди, яка буде підтримувати продукт через 3 роки, команди техартів, яким треба оптимізувати текстури під нову платформу, геймдизайнерам, що коригують баланс гри, та багатьом іншим людям, причетним до розробки. Тобто, якщо ви читаєте про структуру проєкту «як для програміста», зазвичай все зводиться до ООП, інтерфейсів, патернів та подібного. При розробці на Unity важливо усе, тому що саунд-дизайнер чи художник навряд оцінить красу коду — а ось те, що картинки та звуки розміщуються в логічному місті, а ієрархія на сцені дозволяє легко та без головного болю додавати чи заміняти контент, приймуть з радістю. Іншими словами, зручна структура веде до економії часу, грошей, та нервових клітин.

Що ми розуміємо під структурою проєкту? Я б виділив 3 розділи:

  • Структура коду;
  • Структура папок проєкту;
  • Структура в ієрархії сцени.

І далі йдемо докладніше

Структура коду

Є базові поради та найголовніша з них...не тупити :) База тому і називається базою, що важко порекомендувати щось НЕ очевидне. Далі мої особисті поради:

  • Завжди залишати коментарі. Це зараз зрозуміло, що зробити цим кодом. А через рік? А чи зрозуміють інші розробники?
  • Не забувати про ООП та SOLID. Звісно бувають ситуації коли важко писати «чистий код». Але дотримання цих принципів надалі полегшить роботу над проєктом і зменшить вірогідність появи багів.
  • GIT або інший репозиторій — це найкращий друг) За можливості треба робити коміти якнайчастіше. Якщо необхідно, використовувати різні гілки. Таким чином, можна побачити, як самі мислили та куди це привело код, і також як розмірковували інші. Наприклад, якщо щось зламалось у системі аутлайнів, хоча раніше вони працювали.

Але все це загальні поради, які підходять до майже будь-якої мови програмування. Тому перейдемо до більш специфічних для Unity. Що ж, почнемо з самого початку... з папок проєкту.

Папки проєкту

На початку проєкту зазвичай є лише ідея, у кращому випадку — ще хоч якийсь дизайн-документ. Тому, на перших етапах розробки, можна бачити творчий бедлам у проєкті. Папки створюються, змінюються, переносяться, перейменовуються і так далі. Звісно, цей хаос несе проблеми, які з’їдають час та нерви. Але з досвідом, кількома великими проєктами, та з десяток малих геймджем/інді-ігор позаду, формуються звички, корисні для старту нового проєкту. Серед них, наприклад, створення папок Images, Scripts, Scenes, Sound, Prefabs по дефолту. Більш того, картинка, яка показує, скільки залишилося здоров’я, не піде відразу до папки Images, її шлях має бути трошки складнішим, по типу Images/Character/UI/HPBar. Звісно, так само треба робити з іншими префабами проєкту.

Це все цікаво, але на біса воно нам треба? Ви звісно скажете «Добре, я не буду пхати усі файли у кореневу папку, та дійсно поділю скрипти окремо, а картинки окремо. Але нащо мені створювати 5-6 папок, одна в одній?» І ось тут ми й приходимо до основного питання необхідності архітектури. А точніше, що буде, якщо її ігнорувати. Це призведе до втрати інтуїтивності. Коли нова людина знайомиться з проєктом та хоче подивитись, як у ньому реалізована клієнт-серверна архітектура, звісно, вона полізе її шукати в окремій папці. Зазвичай, це не один файл, скоріш — низка файлів, де ми можемо бачити реалізації для клієнта, хоста, особливості стіма та окремі — для консолей.

Кажучи про коректне ведення папок, неможливо не згадати про неймінг. Звісно, тут працюють правила з неймінгом для назв у програмуванні. Назва повинна відображати те, чим являється файл і таке інше...Але це все в ідеальному світі. Існує проблема у великих проєктах: наприклад, є аватарки юнітів (припустимо, це 50 картинок розміром 34 на 34 пікселі). Вони знаходяться у папці Assets/Images/Units/MiniAvatar та пронумеровані 1, 2, 3...49, 50.

Порада художникам: не робіть так!

Це не складе великої проблеми для розробника, якщо цей номер прив’язаний до id юніта. Деякі навіть пишуть мініплагіни, де такий неймінг чудово підходить. Наприклад, автоматом заповнити скриптабл обджект, який буде своєрідною базою даних від 1 до 50. Та навіть вручну поставити не проблема. Але ось приклад з практики — приходить додаткова задача для техартів: юніту Gandalf повністю переробити три види зображень, анімацію, звук тощо. А юніта Saruman взагалі випиляти з гри, включаючи з баз даних.

На цьому моменті я бажаю розробнику везіння. І це пів біди, що витрачається час на пошук. Коли стане ясно, що Gandalf на мініаватарці йде під номером 18, то, звісно, з легкістю можна змінити звук юніта під номером 18. Звучить непогано, крім одного моменту: звуки, які зробила інша команда півтора року тому, були пронумеровані від 0 до 49. І те, що щось пішло не так, може виявитись через кілька днів, а то й на релізі. А це може вплинути на стартові оцінки, негативні коментарі, втрачені гроші. І лише через те, що ніхто не наполіг на НОРМАЛЬНИХ назвах. Наприклад, вбити у пошук Unity Gandalf, чи навіть використати додаткові фільтри, щоб мінімізувати людську помилку.

Неймінг допомагає оптимізувати пошук

Що ж, перейдемо до ще одного моменту, який хотів би освітити з цією темою. Зазвичай не має значення, яку назву має папка. Але існує ряд папок, назви яких зарезервовані рушієм. Наприклад, папка Resources. Її назва дуже логічна. Бачите, у чому справа: якщо потрібно, щоб при натисканні миші з’являвся об’єкт/префаб, можна просто помістити посилання на нього у скрипті, протягнувши, наприклад, з папки префабів. Тобто, після того, як зібрано білд, рушій бачить, що є посилання на цей об’єкт, а значить, його потрібно засунути у білд. Але все це можна зробити іншим шляхом. Цей об’єкт можна викликати з папки ресурсів командою Resources.Load<Sprite>("Sprites/sprite01«). Ефект буде той самий, він піде у білд, але не через те, що рушій бачить посилання на нього, а через те, що він буде у папці ресурсів. З одного боку, папка ресурсів дає гнучкість, динамічно змінює посилання через скрипт. З іншого, дуже легко зробити з неї «Сміттярку», забувши видалити зайві файли.

Повернемось до минулого прикладу. Ми видалили посилання на Saruman та в грі його ніколи не побачимо. Але забули знищити файли у папці ресурсів, тому усі арти з ним та аудіо файли, які там були, пішли у білд та займають місце. Як уникнути такого «сміття»? Насправді одночасно дуже легко, та дуже важко. Просто треба підтримувати архітектуру, що була запланована. Це знизить вірогідність проблем. Ось тільки якщо одночасно працює багато людей: програмісти, художники, звуковики, друга команда на аутсорсі — контролювати цей момент дуже важко.

Власне, перейдемо до сцени та її ієрархії.

Наприклад, неймінг об’єктів. Рушій сам дає дефолтні імена, та зазвичай прийнято їх перейменовувати. Інколи розробники не бачать у цьому сенсу. Для прикладу, бачимо здоров’я юніта. Складається з бекграунду, лінії та тексту. Так, є головний об’єкт, що має конкретну назву, а інші — дефолтні назви. Але, як з кодом, важко не створити об’єкт, важко його підтримувати. При задачі замінити зображення його ще потрібно знайти. Чи ось коли об’єкт складається з багатьох частин?

Воно то працює, але краще перейменовувати автоназви, які дає рушій

Чому я стільки часу присвятив темі з папками? Як ми бачимо по тому ж неймінгу, ігрові об’єкти також мають вкладений об’єкт, який, своєю чергою, має свій об’єкт і так далі. Тим паче через код можна отримати своєрідний шлях до вкладеного об’єкта. Можна, але не треба так робити. Чому? Бо проблемно, коли є один шлях до файлу, а у процесі розробки папки змінюють назву та неймінги. Добре, коли цей момент контролюється. А якщо ні? Звісно, у деяких випадках є залежність, якої неможливо позбутись.

Наприклад, «Ось так зробили анімацію аніматори, тому нічого не чіпайте», чи це взагалі плагін на рекламу, чи керування специфічним, консольним функціоналом. Можна з цим рішенням не погоджуватись, але «дешевше» змиритись, чим витрачати до біса часу, щоб все перекроїти. Перекроїти задля того, щоб воно працювало так, як і раніше, але виглядало красивіше. Звісно, не враховуємо моменти, коли плагін чи анімацію робили колеги, тоді вже можна «наїхати», але, будемо відверті, якщо працювати з професіоналами, то технічні моменти обговорюються заздалегідь.

Хотілося б додати ще кілька порад:

— Коли є багато однотипного контенту (кулі, які створюються при стрілянині), краще засунути його у якийсь контейнер. Виконуючи таски на «Щось там кудись не туди летить», це «Щось» краще шукати у добре класифікований, гарній архітектурі, чим у «Ми бачимо усе підряд».

Краще зробити їх дочірніми до якогось об’єкта з назвою Bullets. Згорнувши, однотипні об’єкти не заважатимуть працювати.

— Якщо ви не робите якийсь прототип, завжди перейменовуйте стандартні назви у щось логічне.

Головне. Якщо доведеться працювати з кодом інших людей, чи буде їх архітектура ідеальною? Звісно, ні!) Але це не означає, що не треба прагнути до порядку у проєкті. Як вже казав, добра архітектура веде до економії часу та розуміння. Більш того, вірогідно, спеціаліст не займається архітектурою цілого проєкту, а лише малого префабу чи плагіну. Та навіть при цій тасці, скіл в архітектурі допоможе без зайвих нервів інтегрувати функціонал, змінювати у процесі розробки об’єкти так, щоб QA не прийшли з фразою «Максим/Сергій/Влад ти все зламав, учора все працювало!».

Підписуйтеся на Telegram-канал @gamedev_dou, щоб не пропустити найважливіші статті і новини.

👍ПодобаєтьсяСподобалось5
До обраногоВ обраному2
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Завжди залишати коментарі. Це зараз зрозуміло, що зробити цим кодом. А через рік? А чи зрозуміють інші розробники?

Во первых — комментарий должен содержать только объяснение, почему было принято такое решение. Если комментарий расписывает то, что происходит в коде — это плохой и не нужный комментарий. Во-вторых, если ваш код нуждается в комментариях чтобы его понять — это говнокод.
Также, люди склонны менять код и при этом НЕ МЕНЯТЬ КОММЕНТАРИЙ к коду — что влечет кучу бед в будущей поддержке проекта. Если вы рекомендуете писать комментарии ВЕЗДЕ, то эта проблема должна быть как минимум упомянута в статье
Я также не увидел обязательного пункта о том, что код должен быть сложен в папки, которые в данном случае есть визуальное отображение неймспейсов. Именно это выступает критерием упорядоченности кода в вашем проекте.

Теж юзаю контейнери для обжектів на сцені. Всі річки, моря і озера — в один контейнер Sea&Lakes. Всякі невидіми колайдери для обмеження пересування гравця за межу мапи: WallBlockers. Та і всю мапу поділив на 4 зони по компасу. Щоб менше було скролити і легше шукати де який обжект знаходиться.

Дякую за поради!

Мені код з коментами важче читати, ніж код з норм неймінгом і структурою. Мені ті коментарі тільки заважають концентруватись.

Повністю підтримую. Це не лише мені чи тобі вони заважають, між іншим. Будь-якому програмісту.

Самий сік коли код поміняли, а каменти забули. Дивишся на код — він робить одне, а каменти говорять не «вір очам своїм»)

Коментарі у 2023 xD
Така собі порада. Гарному коду коментарі не потрібні. Просто треба розбивати код на методи у 3-4 рядки та вміти правильно іменувати. Якщо метод названо правильно, то і без коментарів зрозуміло що в ньому відбувається — навіть через рік, два, три.
Коментарі доречні тільки у випадках, коли ви застосовуєте неочевидні формули з фізики/математики, або ще якісь НЕОЧЕВИДНІ речі.
Ну й, звісно, якщо ви пишете якусь тулзу чи бібліотеку (у такому випадку лише summary).

Рекомендую подивитися відео Knowledge Syndicate з цього приводу.

Не завжди по 3-4 строчки методи в саме ІГРОВОМУ коді — гарна ідея.
Перфоманс ніхто не відміняв.

Цікаво, з яких пір кількість методів у класі впливає на перформанс? 😐
Це не так працює...

Більше функцій -> більше викликів -> повільніший код?

Це так не працює.
Компілятор доволі розумний, щоб зібрати все разом. Він за можливості інлайнить методи і в результаті для процесора нема ніякої різниці у кількості викликів. Що один великий метод, що розбитий на маленькі методи — виклик буде один. Бо вони просто заінлайняться. Це ж база. Якщо ви розробник, то маєте знати матчастину.

Або вирішить що оверхед несуттєвий і незаінлайнить) Компілятори хіба по дефолту інлайнять? Мені здається навпаки. Тобто ви хочете сказати що якщо метод розбити на 2 то 100% компілятор заінлайнить(це є в доці мови чи компілятора як певна гарантія)?

Як мені відомо, JIT-компілятори, (а Mono є саме Just-in-Time), самі вирішують, коли інлайнити, а коли ні. І там є певний ряд умов, коли це можна робити.
Цитую доку Юніті:

The primary issue here is that Unity performs very little method inlining, if any. Even under IL2CPP, many methods do not currently inline properly. This is especially true of properties. Further, virtual and interface methods cannot be inlined at all.

Тож дарма я настільки самовпевнено це стверджував, що воно ледь не 100% заінлайнить.

Тим не менш, там є свої нюанси. Бувають випадки, коли виклик метода був більш швидким, ніж інлайн-код.

docs.unity3d.com/...​gPerformanceInUnity8.html

І все ж я продовжу розбивати код на маленькі методи, оскільки реальний вплив на перформанс знаходиться у межах похибки і їм можна знехтувати.

Коли-небудь виключно з цікавості заінлайню методи у якомусь проєкті і подивлюся, наскільки багато наносекунд я зможу виграти у перформансі.

Я чув що найваща робота для програміста це оптимізація та дописування чужого коду. По цій причині начебто тони коду на таких давніх мовах як С , С++. І їх ще кілька десятиліть не замінять всякі там новомодні Пітон та інші.

Ну бачите, новомодний пітон дуже програє у c++ в продуктивності. І рахунок не йде на наносекунди в межах похибки. Тому, наприклад, той же v8 двіжок від Гугл написаний на c++, і на пайтон ніхто переписувати його і не думає. Ну і вважати с++ давнім також є недоречно, він активно розвивається.

Підписатись на коментарі