Attention: Here be dragons
This is the latest
(unstable) version of this documentation, which may document features
not available in or compatible with released stable versions of Godot.
Checking the stable version of the documentation...
Організація сцени
Ця стаття охоплює теми, пов’язані з ефективною організацією вмісту сцени. Які вузли слід використовувати? Де їх розмістити? Як вони повинні взаємодіяти?
Як ефективно будувати залежності
Коли користувачі Godot починають створювати свої сцени, вони часто стикаються з такою проблемою:
Вони створюють свою першу сцену і наповнюють її вмістом лише для того, щоб врешті-решт зберегти гілки своєї сцени в окремі сцени, оскільки починає накопичуватися неприємне відчуття, що сцену потрібно розбити на кілька окремих. Однак тоді вони помічають, що надійні посилання, на які вони могли покластися раніше, стають непрацездатними. Повторне використання сцени в кількох місцях створює проблеми, оскільки шляхи вузлів не знаходять своїх цілей і встановлені сигнальні зв’язки розірвані.
Щоб вирішити ці проблеми, ви повинні створити екземпляри підсцени, не вимагаючи для них деталей про своє середовище. Ви повинні бути в змозі повірити в те, що підсцена створиться сама, не будучи вибагливими щодо того, як її використовувати.
One of the biggest things to consider in Object-Oriented Programming (OOP) is maintaining focused, singular-purpose classes with loose coupling to other parts of the codebase. This keeps the size of objects small (for maintainability) and improves their reusability.
Ці найкращі практики ООП мають деяку причетність до найкращих практик у структурі сцени та використанні скриптів.
Якщо це взагалі можливо, ви повинні створювати сцени без залежностей. Тобто ви повинні створювати сцени, які зберігають у собі все необхідне.
Якщо сцена повинна взаємодіяти із зовнішнім контекстом, досвідчені розробники рекомендують використовувати Dependency Injection (Ін'єкцію Залежності). Ця техніка передбачає наявність API високого рівня, що забезпечує залежності API низького рівня. Навіщо це робити? Просто класи, які покладаються на своє зовнішнє середовище, можуть ненавмисно викликати помилки та несподівану поведінку.
Щоб зробити це, ви повинні виставити дані, а потім покластися на батьківський контекст для їх ініціалізації:
Підключення до сигналу. Надзвичайно безпечний, але повинен використовуватися лише для "реагування" на поведінку, а не для її запуску. Зазвичай назви сигналів - це дієслова в минулому часі, як-от "увійшов", "уміння активовано" або "предмет зібрано".
# Parent $Child.signal_name.connect(method_on_the_object) # Child signal_name.emit() # Triggers parent-specified behavior.
// Parent GetNode("Child").Connect("SignalName", Callable.From(ObjectWithMethod.MethodOnTheObject)); // Child EmitSignal("SignalName"); // Triggers parent-specified behavior.
// Parent Node *node = get_node<Node>("Child"); if (node != nullptr) { // Note that get_node may return a nullptr, which would make calling the connect method crash the engine if "Child" does not exist! // So unless you are 1000% sure get_node will never return a nullptr, it's a good idea to always do a nullptr check. node->connect("signal_name", callable_mp(this, &ObjectWithMethod::method_on_the_object)); } // Child emit_signal("signal_name"); // Triggers parent-specified behavior.
Викличте метод. Використовується для початку поведінки.
# Parent $Child.method_name = "do" # Child, assuming it has String property 'method_name' and method 'do'. call(method_name) # Call parent-specified method (which child must own).
// Parent GetNode("Child").Set("MethodName", "Do"); // Child Call(MethodName); // Call parent-specified method (which child must own).
// Parent Node *node = get_node<Node>("Child"); if (node != nullptr) { node->set("method_name", "do"); } // Child call(method_name); // Call parent-specified method (which child must own).
Ініціалізувати властивість Callable. Безпечніше, ніж метод, оскільки володіння методом не є обов'язковим. Використовується для запуску поведінки.
# Parent $Child.func_property = object_with_method.method_on_the_object # Child func_property.call() # Call parent-specified method (can come from anywhere).
// Parent GetNode("Child").Set("FuncProperty", Callable.From(ObjectWithMethod.MethodOnTheObject)); // Child FuncProperty.Call(); // Call parent-specified method (can come from anywhere).
// Parent Node *node = get_node<Node>("Child"); if (node != nullptr) { node->set("func_property", Callable(&ObjectWithMethod::method_on_the_object)); } // Child func_property.call(); // Call parent-specified method (can come from anywhere).
Ініціалізуйте посилання на Вузол або інший Об’єкт.
# Parent $Child.target = self # Child print(target) # Use parent-specified node.
// Parent GetNode("Child").Set("Target", this); // Child GD.Print(Target); // Use parent-specified node.
// Parent Node *node = get_node<Node>("Child"); if (node != nullptr) { node->set("target", this); } // Child UtilityFunctions::print(target);
Ініціалізуйте NodePath.
# Parent $Child.target_path = ".." # Child get_node(target_path) # Use parent-specified NodePath.
// Parent GetNode("Child").Set("TargetPath", NodePath("..")); // Child GetNode(TargetPath); // Use parent-specified NodePath.
// Parent Node *node = get_node<Node>("Child"); if (node != nullptr) { node->set("target_path", NodePath("..")); } // Child get_node<Node>(target_path); // Use parent-specified 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 partial class Left : Node
{
public Node Target = null;
public void Execute()
{
// Do something with 'Target'.
}
}
public partial class Right : Node
{
public Node Receiver = null;
public Right()
{
Receiver = ResourceLoader.Load<Script>("Receiver.cs").New();
AddChild(Receiver);
}
}
// Parent
get_node<Left>("Left")->target = get_node<Node>("Right/Receiver");
class Left : public Node {
GDCLASS(Left, Node)
protected:
static void _bind_methods() {}
public:
Node *target = nullptr;
Left() {}
void execute() {
// Do something with 'target'.
}
};
class Right : public Node {
GDCLASS(Right, Node)
protected:
static void _bind_methods() {}
public:
Node *receiver = nullptr;
Right() {
receiver = memnew(Node);
add_child(receiver);
}
};
Ці ж принципи також застосовуються до об’єктів, що не належать до Node і підтримують залежності від інших об’єктів. Незалежно від того, який об’єкт володіє іншими об’єктами, він повинен керувати зв’язками між ними.
Попередження
Ви повинні віддавати перевагу зберіганню даних усередині (всередині сцени), однак, оскільки розміщення залежності від зовнішнього контексту, навіть слабко пов’язаного, все одно означає, що вузол очікуватиме, що щось у його середовищі буде істинним. Філософія дизайну проекту повинна запобігти цьому. Якщо ні, властиві зобов'язання коду змусять розробників використовувати документацію для відстеження об'єктних зв'язків у мікроскопічному масштабі; це ще називають пеклом розробки. Написання коду, який покладається на зовнішню документацію для його безпечного використання, за замовчуванням є схильним до помилок.
Щоб уникнути створення та підтримки такої документації, ви перетворюєте залежний вузол ("дочірній" вище) у скрипт інструменту, який реалізує _get_configuration_warnings(). Повернення з нього непорожнього PackedStringArray змусить док-станцію Scene створити піктограму попередження з рядком(ами) як спливаючу підказку вузлом. Це та сама піктограма, яка з’являється для таких вузлів, як вузол Area2D, якщо він не має визначених дочірніх вузлів CollisionShape2D. Потім редактор самостійно документує сцену за допомогою коду скрипта. Дублювання вмісту через документацію не потрібне.
A Graphical User Interface (GUI) like this can better inform project users of critical information about a Node. Does it have external dependencies? Have those dependencies been satisfied? Other programmers, and especially designers and writers, will need clear instructions in the messages telling them what to do to configure it.
Отже, чому весь цей складний switcheroo працює? Ну, тому що сцени працюють найкраще, коли вони працюють поодинці. Якщо ви не можете працювати поодинці, тоді найкраще працювати з іншими анонімно (з мінімальними жорсткими залежностями, тобто слабким зв’язком). Неминуче може знадобитися внести зміни в клас, і якщо ці зміни спричинять його непередбачувану взаємодію з іншими сценами, тоді все почне ламатися. Суть усього цього непрямого напряму полягає в тому, щоб уникнути ситуації, коли зміна одного класу призведе до негативного впливу на інші залежні від нього класи.
Скрипти та сцени, як продовження класів движка, повинні дотримуватися всіх принципів ООП. Приклади включають ...
Вибір дерева структури вузла
Ви можете почати працювати над грою, але бути приголомшеними величезними можливостями, які відкриваються перед вами. Можливо, ви знаєте, що хочете робити, які системи хочете мати, але куди їх усі розмістити? Як ви будете створювати свою гру, завжди залежить від вас. Ви можете будувати дерева вузлів незліченною кількістю способів. Якщо ви не впевнені, цей посібник може дати вам зразок пристойної структури для початку.
Гра завжди повинна мати «точку входу»; десь ви можете остаточно відстежити, з чого все починається, щоб ви могли слідкувати за логікою, як це продовжується в інших місцях. Він також служить для перегляду з висоти пташиного польоту всіх інших даних і логіки в програмі. Для традиційних програм це зазвичай «основна» функція. У Godot це головний вузол.
Вузол "Main" (main.gd)
Скрипт main.gd слугуватиме основним контролером вашої гри.
Тоді у вас є внутрішньоігровий «Світ» (2D або 3D). Це може бути дочірній елемент Main. Крім того, вам знадобиться основний графічний інтерфейс для вашої гри, який керує різними меню та віджетами, потрібними проекту.
- Вузол "Main" (main.gd)
Node2D/Node3D "Світ" (game_world.gd)
Керування "GUI" (gui.gd)
Змінюючи рівні, ви можете поміняти дочірні елементи вузла «Світ». Changing scenes manually дає вам повний контроль над тим, як ваш ігровий світ змінюється.
Наступний крок — розглянути, які ігрові системи потрібні вашому проекту. Якщо у вас є система, яка...
відстежує всі свої дані зсередини
має бути доступною глобально
може існувати ізольовано
... то вам слід створити autoload 'singleton' node.
Примітка
Для невеликих ігор простішою альтернативою з меншим контролем буде синглтон "Game", який просто викликає метод SceneTree.change_scene_to_file(), щоб замінити вміст головної сцени. Ця структура більш-менш зберігає "Світ" як основний вузол гри.
Будь-який графічний інтерфейс також повинен бути або єдиним елементом, тимчасовою частиною "Світу", або доданим вручну як прямий дочірній елемент кореня. Інакше вузли графічного інтерфейсу також видалятимуться під час переходів між сценами.
If you have systems that modify other systems' data, you should define those as their own scripts or scenes, rather than autoloads. For more information, see Autoloads versus regular nodes.
Кожна підсистема у вашій грі повинна мати власний розділ у SceneTree. Відносини «батьки-нащадки» слід використовувати лише у випадках, коли вузли фактично є елементами своїх батьків. Чи розумно вилучення батьків означає, що діти також повинні бути вилучені? Якщо ні, то він повинен мати власне місце в ієрархії як рідний брат або інший родич.
Примітка
У деяких випадках вам потрібно, щоб ці розділені вузли також розташовувалися відносно один одного. Для цього можна використовувати вузли RemoteTransform / RemoteTransform2D. Вони дозволять цільовому вузлу умовно успадковувати вибрані елементи перетворення від вузла Remote*. Щоб призначити target NodePath, скористайтеся одним із наступного:
Надійну третю сторону, ймовірно, батьківський вузол, який буде посередником у призначенні.
Група, щоб отримати посилання на потрібний вузол (припускаючи, що завжди буде лише одна ціль).
Коли ви повинні це зробити, суб’єктивно. Дилема виникає, коли вам потрібно мікрокерувати, коли вузол повинен рухатися навколо SceneTree, щоб зберегти себе. наприклад...
Додайте вузол "player" до "room".
Необхідно змінити кімнати, тому ви повинні видалити поточну кімнату.
Перш ніж кімнату можна буде видалити, ви повинні зберегти та/або перемістити гравця.
Якщо пам’ять не турбує, ви можете...
Створіть нову кімнату.
Перемістіть гравця в нову кімнату.
Видалити стару кімнату.
Якщо пам’ять викликає занепокоєння, замість цього вам потрібно буде...
Перемістити гравця ще кудись на дереві.
Видалити кімнату.
Створити і додати нову кімнату.
Повторно додайте гравця до нової кімнати.
Проблема полягає в тому, що програвач тут є «особливим випадком», коли розробники повинні знати, що вони повинні обробляти програвач таким чином для проекту. Єдиний спосіб надійно поділитися цією інформацією в команді — задокументувати її. Зберігати деталі реалізації в документації небезпечно. Це тягар обслуговування, погіршує читабельність коду та надмірно роздуває інтелектуальний вміст проекту.
У складнішій грі з більшими активами краще залишити гравця в іншому місці SceneTree. Це призводить до:
Більшої послідовності.
Ніяких "особливих випадків", які потрібно десь задокументувати та підтримувати.
Уникнення помилок, оскільки ці деталі не враховуються.
Навпаки, якщо вам колись знадобиться дочірній вузол, який не успадковує перетворення свого батьківського, у вас є такі варіанти:
Декларативне рішення: помістіть Node між ними. Оскільки він не має перетворення, вони не передадуть цю інформацію своїм нащадкам.
Імперативне рішення: Використовуйте властивість
top_levelдля вузла CanvasItem або Node3D. Це призведе до того, що вузол ігноруватиме успадковане перетворення.
Примітка
Створюючи мережеву гру, майте на увазі, які вузли та ігрові системи стосуються всіх гравців, а не ті, що стосуються лише авторитетного сервера. Наприклад, не всім користувачам потрібно мати копію логіки «PlayerController» кожного гравця — їм потрібна лише власна. Зберігання їх в окремій гілці від «світу» може допомогти спростити керування підключеннями до гри тощо.
Ключ до організації сцени - розглядати Дерево Сцени в реляційних термінах, а не просторових. Чи залежать вузли від існування предків? Якщо ні, то вони можуть процвітати самі десь в іншому місці. Якщо вони залежать, то цілком зрозуміло, що вони повинні бути нащадками цього предка (і, можливо, частиною сцени цього предка, якщо вони ще не є такими).
Чи означає це, що самі вузли є компонентами? Зовсім ні. Дерева вузлів Godot формують зв’язок агрегації, а не композиції. Але хоча ви все ще маєте можливість переміщати вузли, усе одно найкраще, коли такі переміщення за замовчуванням непотрібні.