GameManager в Unity: как управлять состоянием игры и не собрать весь код в одном классе

Название manager в Unity встречается в разных местах: Package Manager устанавливает пакеты, Unity Hub управляет редакторами и проектами, а GameManager обычно называют собственный компонент игрового состояния. Единого обязательного класса «UnityManager», который решает всё, нет.

Разберём последний вариант: небольшой менеджер раунда. Его задача — хранить состояние и счёт. Движение, звук, интерфейс и создание врагов остаются отдельными системами.

ТОП-5 курсов обучения Unity:

Сначала определите ответственность

Менеджер раунда отвечает на вопросы: раунд начат? можно ли начислять очки? какое текущее значение счёта? Он сообщает об изменениях, чтобы другие системы могли реагировать.

Если тот же класс читает клавиатуру, вычисляет физику игрока, меняет текст, воспроизводит музыку и хранит настройки каждого врага, изменение одной механики затронет множество несвязанных частей. Разделяйте системы по тому, какие решения они принимают.

Для маленькой сцены достаточно сериализованной ссылки на менеджер. Singleton нужен только если вы осознанно выбираете один общий экземпляр с доступом из разных мест. Это удобство создаёт глобальную зависимость, поэтому его не стоит добавлять автоматически ко всем компонентам.

Пример менеджера между сценами

Создайте корневой пустой объект RunManager и файл RunManager.cs. Прикрепите компонент к этому объекту. Пример сохраняет экземпляр при обычной смене сцен и удаляет дубликат, если следующая сцена содержит ещё один такой объект.

using System; using UnityEngine; public class RunManager : MonoBehaviour { public static RunManager Instance { get; private set; } public int Score { get; private set; } public bool IsRunning { get; private set; } public event Action Changed; [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] private static void ResetStatics() { Instance = null; } private void Awake() { if (Instance != null && Instance != this) { Destroy(gameObject); return; } Instance = this; DontDestroyOnLoad(gameObject); } public void BeginRun() { Score = 0; IsRunning = true; Changed?.Invoke(); } public void AddPoints(int points) { if (!IsRunning || points <= 0) return; Score += points; Changed?.Invoke(); } public void FinishRun() { IsRunning = false; Changed?.Invoke(); } [ContextMenu("Test run in Play")] private void TestRun() { if (!Application.isPlaying || Instance != this) return; BeginRun(); AddPoints(5); FinishRun(); Debug.Log(quot;Счёт: {Score}; раунд активен: {IsRunning}", this); } private void OnDestroy() { if (Instance == this) Instance = null; } }

В Play вызовите Test run in Play через меню компонента. Получится счёт 5 и завершённый раунд. Поставьте второй корневой объект с этим компонентом: после запуска должен остаться один менеджер. Для проверки сохранения между сценами добавьте две сцены в список сборки и загрузите вторую обычным способом.

Используйте стандартное поведение Play с перезагрузкой сцены. При специальных настройках отключённого Scene Reload нужен отдельно продуманный процесс инициализации. Сброс static-поля помогает при отключённом Domain Reload, но не заменяет все варианты жизненного цикла.

Что сохраняет DontDestroyOnLoad

Метод сохраняет объект при смене сцены; для GameObject сохраняется также его дочерняя иерархия. Он рассчитан на корневые объекты. Поэтому не помещайте под менеджер весь уровень или Canvas текущего экрана без необходимости.

Сохранение в памяти между сценами не является сохранением на диск. После закрытия приложения счёт исчезнет. Для постоянного прогресса нужен отдельный формат данных и загрузка в новой сессии.

Поведение описано в API DontDestroyOnLoad. Загрузка самих сцен разобрана в отдельной статье.

Как подключить интерфейс

Интерфейс подписывается на Changed, читает Score и IsRunning и обновляет отображение. При отключении или уничтожении UI нужно убрать подписку с того же экземпляра менеджера, к которому её добавили.

Не храните в постоянном менеджере ссылки на все кнопки каждой сцены. При смене сцены они уничтожаются, а менеджер остаётся. Лучше пусть экран сам подключается к доступному игровому состоянию и отсоединяется при завершении своего жизненного цикла.

Если компонент может включиться до создания менеджера, договоритесь о порядке старта: отдельная bootstrap-сцена, явная передача ссылки или событие готовности. Случайный порядок Awake не является надёжной инициализацией.

Когда менеджер пора разделить

Смотрите на причины изменений. Если добавление музыки требует правки логики начисления очков, ответственности смешаны. Выделите AudioService. Если смена экрана изменяет правила победы — отделите отображение. Если растут режимы игры, опишите переходы состояний явно.

Не создавайте десятки менеджеров только ради структуры папок. Один небольшой класс с ясной задачей лучше сложной сети объектов, где невозможно понять владельца данных. Основа — явное состояние, понятные зависимости и проверяемые переходы.

Документация