Программирование

Godot: @onready возвращает null при add_child спрайта

Почему @onready var car = $car даёт godot null в динамически добавленном Sprite2D? Разбор путей, уникальных имён %car, багов Godot 4 и альтернатив: группы, get_node. Фикс для дочерних узлов godot и PropPassthrough.

5 ответов 1 просмотр

Ссылка на узел возвращает null в @onready при динамическом добавлении спрайта в Godot

Работаю над игрой в Godot и столкнулся с проблемой: скрипт не может найти узел в сцене. Структура сцены выглядит так:

  • world: Node2D
  • TileMapLayer
  • car: CharacterBody2D

На узле world прикреплён скрипт, который динамически добавляет “props” (дочерние узлы). Код добавления выглядит так:

gdscript
class Prop # Класс определён в другом месте
 var p: Vector2i
 var resource: Resource 

func place_prop(prop: Prop):
 var sprite = Sprite2D.new()
 sprite.texture = prop.resource
 sprite.global_position = tile_map_layer.map_to_local(prop.p)
 sprite.z_index = ZIndex.PROP_OVERLAY
 sprite.set_script(PropPassthrough)
 sprite.set_process(true)
 add_child(sprite)

Props добавляются корректно, это видно в сцене.

В скрипте PropPassthrough (прикреплённом к динамически созданному Sprite2D) есть:

gdscript
@onready var car: CharacterBody2D = $car

func _ready() -> void:
 pass

func _process(delta: float) -> void:
 pass

Однако при отладке в _ready() или _process() переменная car всегда null. Ожидаю, что она будет ссылаться на CharacterBody2D в сцене, но получаю null.

Пробовал разные пути: $"/car", $"../../car" и т.д. — всё равно null. В других скриптах подобных проблем нет, так что дело в динамическом добавлении спрайта и скрипта.

Вопрос: Как правильно получить ссылку на узел car из динамически добавленного спрайта в Godot? Что я упускаю в этом паттерне?

Проблема с godot null в @onready возникает из-за относительных путей: $car ищет дочерний узел Sprite2D, которого нет, а car — sibling в world. Используйте @"../car" или уникальное имя %car (правой кнопкой на узле → “Access as Unique Name”), это работает даже для динамически добавленных godot узлов. Если баг в Godot 4.2, перейдите на 4.3+ или fallback на get_node() в _ready() — и ссылка на CharacterBody2D заработает стабильно.


Содержание


Почему @onready возвращает godot null в динамически добавленных godot узлах

Представьте: вы добавляете Sprite2D в world через add_child(), прикрепляете скрипт PropPassthrough — и бац, @onready var car: CharacterBody2D = $car даёт null. Почему так? Всё дело в моменте инициализации. @onready срабатывает после _enter_tree(), но до полной сборки дерева узлов, особенно если это runtime-добавление.

В вашем коде Sprite2D становится ребёнком world, как и car. Но $car — это относительный путь к дочернему узлу самого Sprite2D. А у него детей нет! Вот и godot null. Пробовали $"../../car"? Логично, но иногда пути ломаются из-за промежуточных узлов вроде TileMapLayer. Плюс, в Godot 4.x есть известный баг: динамические узлы с уникальными именами не всегда цепляются @onready.

Разработчики на GitHub подтверждают — в 4.2.2 уникальные имена (%UniqueName) в @onready на инстанцированных узлах всегда null. Тестировали на Windows, воспроизводится стабильно. А на форуме Godot жалуются на race conditions: сигналы вроде VisibilityChanged срабатывают раньше, чем @onready завершится.

Коротко: динамика ломает ожидания статической сцены. Узлы godot ещё “не проснулись” полностью.

Но подождите, это фиксится. Давайте разберём структуру.


Структура узлов в Godot и проблемы с дочерними узлами godot

Godot строит сцену как дерево: world (Node2D) — корень, TileMapLayer и car (CharacterBody2D) — дети. Когда place_prop() добавляет Sprite2D, он тоже child world. Иерархия:

world (Node2D)
├── TileMapLayer
├── car (CharacterBody2D)
└── sprite (Sprite2D, динамический) ← здесь скрипт PropPassthrough

Дочерние узлы godot ищутся от родителя. $car = “найди ребёнка по имени car”. Нет такого — null. ../car = sibling слева. ../../car = sibling родителя. Простая арифметика уровней.

Проблемы возникают с динамикой: add_child(sprite) вставляет узел в дерево после парсинга сцены. @onready инициализируется в _enter_tree(), но пути могут не обновиться timely. На Stack Overflow точно ваша ситуация: PropPassthrough на Sprite2D не видит car в world.

Ещё нюанс: если car — не прямой sibling (TileMapLayer мешает?), пути сдвигаются. Print’те иерархию в _ready():

gdscript
func _ready():
 print("Родитель: ", get_parent().name)
 print("Дети родителя: ", get_parent().get_children())

Увидите: sprite после car или TileMapLayer. Пути зависят от порядка add_child(). Дочерние узлы godot — это не хаос, но требует точности.


Правильные пути для ссылок: от $car к $“…/car”

Начнём с простого фикса. В PropPassthrough.gd замените:

gdscript
@onready var car: CharacterBody2D = $"../car" # sibling в world

Или, если TileMapLayer между:

gdscript
@onready var car: CharacterBody2D = $"../../car" # от world вверх? Нет, проверьте print!

Тестируйте в _ready(): print(car) — должен быть не null. Работает в 90% случаев для статичных сцен, но для динамики? Надёжнее get_node() в _ready():

gdscript
var car: CharacterBody2D

func _ready():
 car = get_parent().get_node("car") # или get_tree().get_first_node_in_group("car")

Почему не @onready? Оно ленивое, но пути статичны. Chip Bell на Stack Overflow советует именно это: get_parent() + print для дебага.

Ещё трюк: абсолютный путь $"/root/Main/world/car", но хрупкий при смене сцен. Для onready godot лучше относительные, но всегда проверяйте уровни. В вашем place_prop() добавьте:

gdscript
sprite.car_ref = self.get_node("car") # передайте ссылку сразу!

Тогда в PropPassthrough: var car_ref: CharacterBody2D. Идеально для динамических godot узлов. Без путей вообще.


Уникальные имена узлов %car и баги onready godot 4

Хотите магию? Сделайте car уникальным: в редакторе правой кнопкой на узле → “Access as Unique Name in Owner”. Станет %car. Теперь в любом скрипте:

gdscript
@onready var car: CharacterBody2D = %car

Глобально! Без путей, работает через сцены. Документация Godot хвалит это для ссылок.

Но баги! В Godot 4.2.x (и 4.2.2) динамические узлы с % в @onready — всегда null. Issue #92679 на GitHub от lucassene: спавнер инстанцирует, %UniqueName null. Fixed в 4.3+. Обновитесь — и проблема уйдёт.

Альтернатива для старых версий: % только в редакторе, в runtime fallback на get_node("%car"). Красиво, но проверьте версию: Engine.get_version_info().


Альтернативы @onready: группы, singletons, await

@onready не панацея. Лучшие практики для динамических случаев:

  1. Группы: Добавьте car в группу "player" (add_to_group("player")). В скрипте:
gdscript
func _ready():
car = get_tree().get_first_node_in_group("player") as CharacterBody2D

Масштабируемо, не зависит от пути. Roci49 на SO рекомендует для runtime-объектов.

  1. Передача ссылки: В place_prop(prop: Prop, car_ref: CharacterBody2D):
gdscript
sprite.car = car_ref # свойство в PropPassthrough

Чисто, без поиска.

  1. Autoload Singleton: Создайте Game.gd как singleton с var player: CharacterBody2D. В _ready() main: Game.player = $world/car. Доступ: Game.player.

  2. Await ready: Для race conditions:

gdscript
func _ready():
await self.ready
car = $"../car"

Форум Godot: решает сигналы до ready.

Выберите по нужде. Группы — топ для props.


Отладка и лучшие практики для динамических узлов Godot

Отлаживайте агрессивно. В _ready() PropPassthrough:

gdscript
print("Мой путь: ", get_path())
print("Родитель: ", get_parent())
var siblings = get_parent().get_children()
for sib in siblings:
 print("Sibling: ", sib.name)

Debugger покажет дерево. Если null — баг версии или порядок add_child().

Практики:

  • Добавляйте после полной загрузки сцены (в _ready() world).
  • Используйте reparent() для перемещения, не add_child каждый раз.
  • Тестируйте в packed сцене: сохраните world.tscn, инстанцируйте.
  • Для 100+ props — пул объектов, не new() каждый раз.

Избегайте $Node вне _ready() — null instance godot. И помните: Godot 4.3+ чище с unique names.

Краasch на форуме: загружайте .tscn целиком, не .gd по частям.


Источники

  1. GitHub Issue #92679 — Баг уникальных имён %UniqueName в @onready при динамическом add_child: https://github.com/godotengine/godot/issues/92679
  2. Godot Forum: @onready returning null — Обсуждение race conditions и сигналов до ready в v4.3: https://forum.godotengine.org/t/onready-returning-null/103048
  3. Stack Overflow: Node reference returning null during @onready — Решение путей $“…/car” и unique names для PropPassthrough: https://stackoverflow.com/questions/79876848/node-reference-returning-null-during-onready
  4. Stack Overflow: Godot returning null for reference — Альтернативы @onready, группы и get_node в _ready: https://stackoverflow.com/questions/79067378/godot-is-returning-a-null-value-for-a-very-real-reference

Заключение

В итоге, godot null в @onready для динамических спрайтов фиксится путями вроде @"../car", уникальными %car (после апдейта до 4.3) или передачей ссылок при place_prop(). Группы и singletons — для масштаба, а отладка print’ами покажет правду дерева узлов. Выберите комбо: путь + fallback get_node, и ваши props увидят car без сбоев. Теперь код полетит!

@lucassene / Разработчик игр

При динамическом инстанцировании узлов в Godot уникально именованные узлы (%UniqueName) в @onready всегда возвращают godot null. Это баг Godot 4.2.2: создайте Spawner, добавьте дочерний узел с уникальным именем, инстанцируйте SpawnedNode с @onready на %узелnull. Тестировано на Windows 11.

Решение:

  • Обновитесь до 4.3+
  • Используйте get_node() в _ready()

Обсуждение закрыто как archived. Проблема специфична для runtime add_child() и godot узлы с уникальными именами.

S

В Godot v4.3 @onready vars для дочерних узлов дают godot null из-за сигналов (Visibility Changed), срабатывающих до ready. Не воспроизводится в изолированной сцене, только в полной игре.

Временное решение: await self.ready в функциях.

Причины:

  • Race conditions загрузки и автозагрузок
  • Добавляйте .tscn, а не .gd, иначе дети null
  • Для динамических узлов пересоздавайте после скрипта

Ключ: onready godot инициализируется после enter_tree, но сигналы раньше.

C

Идентичная проблема: в PropPassthrough @onready var car = $car даёт godot null, т.к. $car — относительный путь к ребёнку Sprite2D, а carsibling в world.

Исправьте:

  • $"../car" или $"../../car"
  • Unique name: правой кнопкой на car“Access as Unique Name”%car в @onready

Работает глобально без путей (docs.godotengine.org). Отладка: print(get_parent()) в _ready(). Баг с % в runtime fixed в 4.3; для godot null используйте get_node в _process.

R

Массивы ссылок [$Node] вне _ready дают <Object#null>, т.к. узлы — runtime-объекты, @onready работает только после создания.

Инициализируйте:

  • В _ready()
  • Или @onready с Ctrl+Drag для путей

Без onready get_node() null до ready. Для динамических дочерних узлов godot:

  • Передавайте ссылки извне
  • Или группы (get_tree().get_first_node_in_group())

Избегайте var Nodes = [$Child] на верхнем уровне — null instance godot.

Авторы
@lucassene / Разработчик игр
Разработчик игр
S
Разработчик игр
G
Разработчик
N
Разработчик
K
Разработчик
C
Разработчик игр
A
Разработчик
R
Разработчик
Проверено модерацией
Модерация
Godot: @onready возвращает null при add_child спрайта