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

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

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

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

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

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

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

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

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

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

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

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

    # Parent
    $Child.connect("signal_name", object_with_method, "method_on_the_object")
    
    # Child
    emit_signal("signal_name") # Triggers parent-defined behavior.
    
    // Parent
    GetNode("Child").Connect("SignalName", ObjectWithMethod, "MethodOnTheObject");
    
    // Child
    EmitSignal("SignalName"); // 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).
    
    // Parent
    GetNode("Child").Set("MethodName", "Do");
    
    // Child
    Call(MethodName); // Call parent-defined method (which child must own).
    
  3. Ініціалізуйте властивість FuncRef. Безпечніша, ніж метод, оскільки право власності на метод непотрібне. Використовується для початку поведінки.

    # Parent
    $Child.func_property = funcref(object_with_method, "method_on_the_object")
    
    # Child
    func_property.call_func() # Call parent-defined method (can come from anywhere).
    
    // Parent
    GetNode("Child").Set("FuncProperty", GD.FuncRef(ObjectWithMethod, "MethodOnTheObject"));
    
    // Child
    FuncProperty.CallFunc(); // Call parent-defined method (can come from anywhere).
    
  4. Ініціалізуйте посилання на Вузол або інший Об’єкт.

    # Parent
    $Child.target = self
    
    # Child
    print(target) # Use parent-defined node.
    
    // Parent
    GetNode("Child").Set("Target", this);
    
    // Child
    GD.Print(Target); // Use parent-defined node.
    
  5. Ініціалізуйте NodePath (Шлях вузла).

    # Parent
    $Child.target_path = ".."
    
    # Child
    get_node(target_path) # Use parent-defined NodePath.
    
    // Parent
    GetNode("Child").Set("TargetPath", NodePath(".."));
    
    // Child
    GetNode(TargetPath); // 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)
// Parent
GetNode<Left>("Left").Target = GetNode("Right/Receiver");

public class Left : Node
{
    public Node Target = null;

    public void Execute()
    {
        // Do something with 'Target'.
    }
}

public class Right : Node
{
    public Node Receiver = null;

    public Right()
    {
        Receiver = ResourceLoader.Load<Script>("Receiver.cs").New();
        AddChild(Receiver);
    }
}

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

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

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

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

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

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

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

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

So, a developer starts work on a game only to stop at the vast possibilities before them. They might know what they want to do, what systems they want to have, but where to put them all? Well, how one goes about making their game is always up to them. One can construct node trees in countless ways. But, for those who are unsure, this helpful guide can give them a sample of a decent structure to start with.

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

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

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

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

  • Вузол "Main" (main.gd)
    • Node2D/Spatial "World" (game_world.gd)

    • Control "GUI" (gui.gd)

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

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

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

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

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

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

Примітка

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

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

If one has systems that modify other systems' data, one should define those as their own scripts or scenes rather than autoloads. For more information on the reasons, please see the Autoloads versus regular nodes documentation.

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

Примітка

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Примітка

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

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

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