Up to date

This page is up to date for Godot 4.2. If you still find outdated information, please open an issue.

Організація сцени

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

Як ефективно будувати залежності

Коли користувачі Godot починають створювати свої сцени, вони часто стикаються з такою проблемою:

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

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

Одна з найбільших речей, яку слід враховувати в ООП, - це підтримка цілеспрямованих класів особливого призначення з вільним зв'язком з іншими частинами кодової бази. Це зменшує розмір об’єктів (для ремонтопридатності) та покращує їх повторне використання.

Ці найкращі практики ООП мають деяку причетність до найкращих практик у структурі сцени та використанні скриптів.

Якщо це можливо, слід розробляти незалежні сцени. Тобто слід створювати сцени, які зберігають усе необхідне в собі.

Якщо сцена повинна взаємодіяти із зовнішнім контекстом, досвідчені розробники рекомендують використовувати `Dependency Injection (Ін'єкцію Залежності)<https://en.wikipedia.org/wiki/Dependency_injection>`_. Ця техніка передбачає наявність API високого рівня, що забезпечує залежності API низького рівня. Навіщо це робити? Просто класи, які покладаються на своє зовнішнє середовище, можуть ненавмисно викликати помилки та несподівану поведінку.

Для цього потрібно виставити дані, а потім покластися на батьківський контекст для їх ініціалізації:

  1. Підключення до сигналу. Надзвичайно безпечний, але повинен використовуватися лише для "реагування" на поведінку, а не для її запуску. Зазвичай назви сигналів - це дієслова в минулому часі, як-от "увійшов", "уміння активовано" або "предмет зібрано".

    # Parent
    $Child.signal_name.connect(method_on_the_object)
    
    # Child
    signal_name.emit() # Triggers parent-defined behavior.
    
  2. Викличте метод. Використовується для початку поведінки.

    # Parent
    $Child.method_name = "do"
    
    # Child, assuming it has String property 'method_name' and method 'do'.
    call(method_name) # Call parent-defined method (which child must own).
    
  3. Ініціалізувати властивість Callable. Безпечніше, ніж метод, оскільки володіння методом не є обов'язковим. Використовується для запуску поведінки.

    # Parent
    $Child.func_property = object_with_method.method_on_the_object
    
    # Child
    func_property.call() # Call parent-defined method (can come from anywhere).
    
  4. Ініціалізуйте посилання на Вузол або інший Об’єкт.

    # Parent
    $Child.target = self
    
    # Child
    print(target) # Use parent-defined node.
    
  5. Ініціалізуйте NodePath (Шлях вузла).

    # Parent
    $Child.target_path = ".."
    
    # Child
    get_node(target_path) # Use parent-defined NodePath.
    

Ці опції приховують точки доступу з дочірнього вузла. Це, в свою чергу, зберігає дочірню вершину слабко пов'язаною з її оточенням. Ви можете повторно використовувати його в іншому контексті без будь-яких додаткових змін у його API.

Примітка

Хоча наведені вище приклади ілюструють зв'язок предка-нащадка, однакові принципи застосовуються до всіх об'єктних відносин. Вузли, які є сусідніми, повинні знати лише про їх ієрархію, тоді як предок опосередковує їх зв'язки та посилання.

# Parent
$Left.target = $Right.get_node("Receiver")

# Left
var target: Node
func execute():
    # Do something with 'target'.

# Right
func _init():
    var receiver = Receiver.new()
    add_child(receiver)

Ті самі принципи також застосовуються до невузлових об'єктів, які підтримують залежності від інших об'єктів. Який би об'єкт насправді не був власником об'єктів, він повинен керувати відносинами між ними.

Попередження

Слід віддавати перевагу збереженню даних у приміщенні (всередині сцени), хоча розміщення залежності від зовнішнього контексту, навіть слабо пов'язаної, все одно означає, що вузол буде очікувати, що щось у його середовищі буде точно. Філософія дизайну проєкта повинна запобігти цьому. Якщо ні, властиві коду зобов’язання змусять розробників використовувати документацію для відстеження об’єктних відносин у мікроскопічному масштабі; це ще називають пеклом розвитку. Написання коду, який покладається на зовнішню документацію для безпечного використання, за замовчуванням схильне до помилок.

Щоб уникнути створення та ведення такої документації, можна перетворити залежний вузол ("дочірній" вище) на інструментальний скрипт, який реалізує _get_configuration_warnings(). Повернення непорожнього масиву PackedStringArray з нього змусить док Сцени згенерувати піктограму попередження з рядком(ами) у вигляді підказки біля вузла. Це та сама піктограма, яка з'являється для таких вузлів, як вузол Area2D, якщо для нього не визначено дочірніх вузлів CollisionShape2D. Після цього редактор самостійно документує сцену за допомогою коду скрипту. Дублювання вмісту за допомогою документування не потрібне.

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

Тож, чому вся ця складна система працює? Тому що сцени працюють найкраще, коли вони працюють поодинці. Якщо вони не можуть працювати наодинці, то наступним найкращим варіантом буде анонімна робота з іншими (з мінімальними жорсткими залежностями, тобто слабким зв'язком). Неминуче може знадобитися внести зміни до класу, і якщо ці зміни змусять його взаємодіяти з іншими сценами у непередбачуваний спосіб, то все почне руйнуватися. Весь сенс опосередкованого зв'язку полягає в тому, щоб уникнути ситуації, коли зміна одного класу призводить до негативного впливу на інші класи, залежні від нього.

Скрипти та сцени, як продовження класів движка, повинні дотримуватися всіх принципів ООП. Приклади включають ...

Вибір дерева структури вузла

Отже, розробник починає роботу над грою лише для того, щоб зупинитися на величезних можливостях, що стоять перед ним. Він може знати, чим хоче займатися, які системи хоче мати, але куди їх усіх розмістити? Що ж, спосіб створення гри завжди залежить від розробника. Дерева вузлів можна побудувати безліччю способів. Але для тих, хто не має певності, цей корисний посібник може дати початковий зразок гідної структури.

Гра завжди повинна мати своєрідну "точку входу"; яку розробник може спокійно знайти, і почати відстежувати логічне продовження в інших місцях. Це місце також виконує функції поверхневого огляду всіх інших даних та логіки програми. Для традиційних додатків це буде функція "main" (головна). У цьому випадку це буде вузол Main (Головний).

  • Вузол "Main" (main.gd)

Скрипт main.gd тоді буде служити головним контролером гри.

Потім, власне, світ гри "World" (Світ) (2D або 3D). Він може бути нащадком Main. Крім того, для гри потрібен основний графічний інтерфейс, який керує різними меню та віджетами, необхідними проєкту.

  • Вузол "Main" (main.gd)
    • Node2D/Node3D "Світ" (game_world.gd)

    • Control "GUI" (gui.gd)

Змінюючи рівні, можна міняти місцями нащадків вузла "World". Зміна сцен вручну дає користувачам повний контроль над змінами ігрового світу.

Наступним кроком є розгляд того, які ігрові системи вимагає проєкт. Якщо хтось має систему, яка ...

  1. відстежує всі свої дані зсередини

  2. має бути доступною глобально

  3. може існувати ізольовано

... то слід створити вузол автозавантаження 'singleton'.

Примітка

Для невеликих ігор простішою альтернативою з меншим контролем буде синглтон "Game", який просто викликає метод SceneTree.change_scene_to_file(), щоб замінити вміст головної сцени. Ця структура більш-менш зберігає "Світ" як основний вузол гри.

Будь-який графічний інтерфейс також повинен бути одностороннім; бути тимчасовою частиною "World"; або бути доданим вручну як безпосередній нащадок кореня. В іншому випадку вузли графічного інтерфейсу також видаляються під час переходу між сценами.

Якщо у когось є системи, що модифікують дані інших систем, слід визначати їх як власні скрипти, чи сцени, а не як автозавантаження. Для отримання додаткової інформації про причини, будь ласка, дивіться документацію про автозавантаження і внутрішні вузли'.

Кожна підсистема в межах гри повинна мати свій розділ у Дереві Сцени. Використовувати зв'язки предків та нащадків слід лише у тих випадках, коли вузли фактично є елементами предків. Чи виправдано, що вилучення предка прибирає його нащадків? Якщо ні, то нащадки повинні мати своє власне місце в ієрархії.

Примітка

У деяких випадках потрібно, щоб ці відокремлені вузли також позиціонували себе відносно один одного. Для цього можна використати вузли RemoteTransform / RemoteTransform2D. Вони дозволять цільовому вузлу умовно успадковувати вибрані елементи трансформації з вузла Remote*. Щоб призначити target NodePath, скористайтеся одним з наведених нижче способів:

  1. Надійну третю сторону, ймовірно, батьківський вузол, який буде посередником у призначенні.

  2. Групу, яка легко отримує посилання на потрібний вузол (припускаючи, що колись буде лише одна з цілей).

Коли потрібно це робити? Ну, це суб’єктивно. Виникає дилема, коли потрібно мікрокерувати, коли вузол повинен рухатись по дереву сцени, щоб зберегти себе. Наприклад...

  • Додайте вузол "player" до "room".

  • Потрібно поміняти кімнати, тому потрібно видалити поточну кімнату (room).

  • Перш ніж кімнату можна буде видалити, потрібно зберегти та/або перемістити гравця (player).

    Є проблеми з пам'яттю?

    • Якщо ні, то можна просто створити дві кімнати, перемістити гравця і видалити стару. Нема проблем.

    Якщо да, то потрібно...

    • Перемістити гравця ще кудись на дереві.

    • Видалити кімнату.

    • Створити і додати нову кімнату.

    • Повторно додати гравця.

Справа в тому, що гравець тут - це "особливий випадок"; такий, де розробники повинні знати, що їм потрібно поводитися з гравцем таким чином для проєкту. Таким чином, єдиний спосіб надійно ділитися цією інформацією в команді - це документувати її. Однак зберігати деталі реалізації в документації небезпечно. Це тягар технічного обслуговування, ускладнює читабельність коду та надмірно роздуває інтелектуальний зміст проєкту.

У складнішій грі з великими активами може бути кращою ідеєю просто повністю утримати гравця де-небудь ще в Дереві Сцени. Це призводить до:

  1. Більшої послідовності.

  2. Ніяких "особливих випадків", які потрібно десь задокументувати та підтримувати.

  3. Уникнення помилок, оскільки ці деталі не враховуються.

На відміну від цього, якщо коли-небудь потрібно мати дочірній вузол, який не успадковує перетворення свого предка, у нього є такі варіанти:

  1. Декларативне рішення: помістити вузол між ними. Як вузли без перетворення, Вузли не передаватимуть таку інформацію своїм нащадкам.

  2. Імперативне рішення: Використовуйте властивість top_level для вузла CanvasItem або Node3D. Це змусить вузол ігнорувати успадковане перетворення.

Примітка

Створюючи мережеву гру, майте на увазі, які вузли та ігрові системи мають відношення до всіх гравців проти тих, що стосуються лише авторитетного сервера. Наприклад, не всім користувачам потрібно мати копію логіки кожного гравця "PlayerController". Натомість їм потрібно лише своя. Таким чином, утримання їх у відокремленій від "світу" гілці може допомогти спростити управління ігровими зв’язками.

Ключ до організації сцени - розглядати Дерево Сцени в реляційних термінах, а не просторових. Чи залежать вузли від існування предків? Якщо ні, то вони можуть процвітати самі десь в іншому місці. Якщо вони залежать, то цілком зрозуміло, що вони повинні бути нащадками цього предка (і, можливо, частиною сцени цього предка, якщо вони ще не є такими).

Чи означає це, що самі вузли є компонентами? Зовсім ні. Дерево вузлів Godot формує зв'язок агрегації, а не склад. Але незважаючи на те, що людина все ще має гнучкість переміщення вузлів, все-таки краще, коли такі переміщення за замовчуванням непотрібні.