Как я потратил два дня в поисках потерянного компонента в Unity

Потихоньку работаю над вторым лонгом про разработку модов для Beat Saber. Два дня потратил на отладку одной херни, и сейчас я просто обязан про это выговориться. Я не хочу держать это в себе.

Задача: получить ссылку на компонент GamePause. Это компонент из кода самого Beat Saber, который нужен мне, чтобы отслеживать, когда игру поставили на паузу. Я выяснил, что GamePause определен в MainAssembly.dll в папке с игрой, поэтому добавил эту библиотеку в проект как зависимость. После этого мне просто нужно было в моем коде получить ссылку на этот компонент.

В Unity это делается как-то так:

Resources.FindObjectsOfTypeAll<GamePause>();

Этому методу все равно, на какой сцене компонент, что за объект и активен ли он. Если компонент создан, он будет найден.

Но он не был найден. Метод всегда возвращал пустой массив. Я вообще не сам это придумал, я это взял из мода BS_Utils. Там это работает. Если я использую в своем коде BS_Utils, то он работает, а значит, он нашел GamePause, он существует. С помощью декомпилятора в Rider я видел, что BS_Utils делает то же самое, что и мой код.

Я нашел исходники BS_Utils на GitHub, склонировал репозиторий, добавил туда отладочных логов, пересобрал его с нужной версией и засунул в игру. Логи подтведили, что BS_Utils успешно находит GamePause. Через те же логи я узнал, что нужный компонент лежит на сцене под названием StandardGameplay, и получил полный путь к объекту в иерархии на этой сцене.

С этой информацией я вернулся в код своего мода. Я добавил обработчик события SceneManager.sceneLoaded, чтобы отловить, когда загружается сцена StandardGameplay. Я думал, что проблема может быть в методе Resources.FindObjectsOfTypeAll(), поэтому взял загруженную сцену, через GetRootGameObjects() взял у нее все корневые объекты и через GetComponentInChildren() попробовал получить ссылку на нужный компонент. Но все, что я получил, это хренов null. Компонента не было на сцене. Но он должен был быть.

Еще раз проверил, что класс правильный. Я вернулся в код BS_Utils, перешел на определение класса GamePause (через декомпилятор), потом сделал то же самое для своего проекта — там был тот же класс с двумя методами Pause и Resume, реализующий интерфейс IGamePause. Вроде все нормально.

Затем я решил вывести в лог вообще все компоненты, которые есть на сцене. Я написал рекурсивный метод, который обходит вглубь все объекты в иерархии сцены и выводит в лог названия объектов и типы компонентов, которые у них есть.

... [DEBUG @ 16:57:17 | BeatSaberTimeTracker] OnSceneLoaded: StandardGameplay (Additive) [DEBUG @ 16:57:17 | BeatSaberTimeTracker] - Wrapper (not active) ... [DEBUG @ 16:57:17 | BeatSaberTimeTracker] -- Pause (not active) [DEBUG @ 16:57:17 | BeatSaberTimeTracker] --+ Comp: GamePause [DEBUG @ 16:57:17 | BeatSaberTimeTracker] --+ Comp: PauseController ...

Вот же он, GamePause, существует! Я решил попробовать получить его напрямую. Взял ссылку на корневой объект Wrapper, затем среди его дочерних объектов нашел Pause, просто сравнивая имена, и вызвал для него GetComponent(). И получил все тот же null.

Я обратил внимание, что объекты не активны. Может, в Unity что-то поменялось, я не знаю. Сделал их активными, попробовал опять получить компонент, но не вышло.

Тут я уже окончательно решил, что двинулся кукухой и перестал понимать, как работает Unity. Единственный вариант, почему это все могло быть — это то, что я использовал не тот GamePause, который нужен. У него не было какого-то явного пространства имен, чтобы точно сказать, что это другой класс. Я решил еще раз вывести компоненты в лог, но добавить больше информации. Вместо GetType().Name я стал выводить GetType().AssemblyQualifiedName. Получилось такое:

... [DEBUG @ 18:10:26 | BeatSaberTimeTracker] OnSceneLoaded: StandardGameplay (Additive) [DEBUG @ 18:10:26 | BeatSaberTimeTracker] - Wrapper (not active) ... [DEBUG @ 18:10:26 | BeatSaberTimeTracker] -- Pause (not active) [DEBUG @ 18:10:26 | BeatSaberTimeTracker] --+ Comp: GamePause, Main, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null [DEBUG @ 18:10:26 | BeatSaberTimeTracker] --+ Comp: PauseController, Main, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null ...

Так, стоп, что за Main? В этот момент я стал что-то подозревать. Пошел смотреть зависимости в BS_Utils.

Как я потратил два дня в поисках потерянного компонента в Unity

Вот оно, отличие! Я использовал MainAssembly.dll, а в BS_Utils используется Main.dll. По какой-то неизвестной мне причине обе сборки содержат класс GamePause. Не совсем идентичные, правда. У GamePause в MainAssembly.dll все поля с модификатором private, а у GamePause в Main.dll — protected. Я этого не заметил, когда сравнивал классы. Я видел, что сами поля одинаковые и методы Pause и Resume тоже одинаковые, поэтому решил, что это один и тот же класс.

Я понятия не имею, зачем в коде Beat Saber два класса GamePause. Может, кто-то из Unity-программистов знает (Андрей?), но я джавист, я не. Понятно, что ни один из перечисленных методов не нашел ссылку на компонент, потому что искал

GamePause, MainAssembly, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null

а надо было

GamePause, Main, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null

Для него это, очевидно, разные классы. А для меня — расстройство психики и два увлекательных дня (нет).

Как я потратил два дня в поисках потерянного компонента в Unity
1717
6 комментариев