Godot: @onready возвращает null при add_child спрайта
Почему @onready var car = $car даёт godot null в динамически добавленном Sprite2D? Разбор путей, уникальных имён %car, багов Godot 4 и альтернатив: группы, get_node. Фикс для дочерних узлов godot и PropPassthrough.
Ссылка на узел возвращает null в @onready при динамическом добавлении спрайта в Godot
Работаю над игрой в Godot и столкнулся с проблемой: скрипт не может найти узел в сцене. Структура сцены выглядит так:
- world: Node2D
- TileMapLayer
- car: CharacterBody2D
На узле world прикреплён скрипт, который динамически добавляет “props” (дочерние узлы). Код добавления выглядит так:
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) есть:
@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 узлах
- Структура узлов в Godot и проблемы с дочерними узлами godot
- Правильные пути для ссылок: от $car к $“…/car”
- Уникальные имена узлов %car и баги onready godot 4
- Альтернативы @onready: группы, singletons, await
- Отладка и лучшие практики для динамических узлов Godot
- Источники
- Заключение
Почему @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():
func _ready():
print("Родитель: ", get_parent().name)
print("Дети родителя: ", get_parent().get_children())
Увидите: sprite после car или TileMapLayer. Пути зависят от порядка add_child(). Дочерние узлы godot — это не хаос, но требует точности.
Правильные пути для ссылок: от $car к $“…/car”
Начнём с простого фикса. В PropPassthrough.gd замените:
@onready var car: CharacterBody2D = $"../car" # sibling в world
Или, если TileMapLayer между:
@onready var car: CharacterBody2D = $"../../car" # от world вверх? Нет, проверьте print!
Тестируйте в _ready(): print(car) — должен быть не null. Работает в 90% случаев для статичных сцен, но для динамики? Надёжнее get_node() в _ready():
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() добавьте:
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. Теперь в любом скрипте:
@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 не панацея. Лучшие практики для динамических случаев:
- Группы: Добавьте
carв группу"player"(add_to_group("player")). В скрипте:
func _ready():
car = get_tree().get_first_node_in_group("player") as CharacterBody2D
Масштабируемо, не зависит от пути. Roci49 на SO рекомендует для runtime-объектов.
- Передача ссылки: В
place_prop(prop: Prop, car_ref: CharacterBody2D):
sprite.car = car_ref # свойство в PropPassthrough
Чисто, без поиска.
-
Autoload Singleton: Создайте
Game.gdкак singleton сvar player: CharacterBody2D. В_ready()main:Game.player = $world/car. Доступ:Game.player. -
Await ready: Для race conditions:
func _ready():
await self.ready
car = $"../car"
Форум Godot: решает сигналы до ready.
Выберите по нужде. Группы — топ для props.
Отладка и лучшие практики для динамических узлов Godot
Отлаживайте агрессивно. В _ready() PropPassthrough:
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 по частям.
Источники
- GitHub Issue #92679 — Баг уникальных имён %UniqueName в @onready при динамическом add_child: https://github.com/godotengine/godot/issues/92679
- Godot Forum: @onready returning null — Обсуждение race conditions и сигналов до ready в v4.3: https://forum.godotengine.org/t/onready-returning-null/103048
- Stack Overflow: Node reference returning null during @onready — Решение путей $“…/car” и unique names для PropPassthrough: https://stackoverflow.com/questions/79876848/node-reference-returning-null-during-onready
- 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 без сбоев. Теперь код полетит!
При динамическом инстанцировании узлов в 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 узлы с уникальными именами.
В Godot v4.3 @onready vars для дочерних узлов дают godot null из-за сигналов (Visibility Changed), срабатывающих до ready. Не воспроизводится в изолированной сцене, только в полной игре.
Временное решение: await self.ready в функциях.
Причины:
- Race conditions загрузки и автозагрузок
- Добавляйте
.tscn, а не.gd, иначе детиnull - Для динамических узлов пересоздавайте после скрипта
Ключ: onready godot инициализируется после enter_tree, но сигналы раньше.
Идентичная проблема: в PropPassthrough @onready var car = $car даёт godot null, т.к. $car — относительный путь к ребёнку Sprite2D, а car — sibling в 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.
Массивы ссылок [$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.