Middle Unity Developer: C# и Unity на практическом уровне

Курс закрывает ключевые компетенции, необходимые для уровня middle Unity-разработчика: уверенный C# для игровых задач и профессиональная работа с Unity. Упор на архитектуру, производительность, инструменты разработки, командные практики и выпуск продукта.

1. C# для Unity: ООП, коллекции, LINQ, исключения, SOLID

C# для Unity: ООП, коллекции, LINQ, исключения, SOLID

Эта статья закрывает фундамент C#, который требуется Unity-разработчику уровня middle: уверенное владение ООП, грамотный выбор коллекций, понимание LINQ (и его цены), корректная работа с исключениями и применение SOLID в архитектуре игровых систем.

С точки зрения Unity важно помнить: вы пишете не просто C#-код, а код, который работает в кадре, взаимодействует с движком, сериализацией, инспектором, сборщиком мусора и жизненным циклом объектов.

ООП в Unity: что реально применяется в проектах

Классы, объекты, поля, свойства, методы

В Unity подавляющее большинство логики живёт в классах:

  • Компоненты на сцене обычно наследуются от MonoBehaviour.
  • Данные и конфигурации часто оформляются как ScriptableObject.
  • Чистая бизнес-логика (без Unity API) оформляется обычными C#-классами.
  • Ссылки:

  • Unity Manual: MonoBehaviour
  • Unity Manual: ScriptableObject
  • Практическая рекомендация для middle: старайтесь выносить логику из MonoBehaviour в обычные классы, а MonoBehaviour использовать как слой привязки к сцене и событиям движка.

    Инкапсуляция: управление доступом и защитой состояния

    Инкапсуляция — это контроль над тем, кто и как может менять состояние объекта.

  • Делайте поля private и открывайте доступ через методы или свойства.
  • Для Unity-инспектора используйте [SerializeField] private ..., чтобы сериализация работала, а внешний код не мог менять поле напрямую.
  • Почему это важно в Unity:

  • Инспектор должен видеть поле, но код снаружи не должен ломать инварианты.
  • Вы хотите контролировать точки изменения состояния (например, чтобы триггерить события, обновлять UI, запускать анимации).
  • Наследование и композиция: как принято в Unity

    Unity архитектурно заточен под композицию: объект на сцене собирается из компонентов.

  • Наследование полезно, когда у вас действительно общий контракт и общая реализация.
  • Но часто выгоднее композиция: один компонент отвечает за движение, другой за здоровье, третий за инвентарь.
  • Пример подхода через интерфейс (полиморфизм без жёсткого наследования):

    Полиморфизм: виртуальные методы и интерфейсы

    Полиморфизм позволяет работать с разными реализациями единообразно.

  • virtual/override — когда есть базовый класс и вы хотите переопределять поведение.
  • Интерфейсы — когда важен контракт, но общий код/состояние не обязателен.
  • В Unity интерфейсы особенно полезны для взаимодействий:

  • IInteractable для объектов, с которыми можно взаимодействовать.
  • IDamageable для всего, что получает урон.
  • IMovable для всего, что двигается (неважно, игрок это, NPC или лифт).
  • Ссылка:

  • Microsoft Learn: Polymorphism (C#)
  • Абстракции данных через ScriptableObject

    ScriptableObject удобно использовать как данные, отделённые от поведения:

  • баланс оружия
  • параметры врагов
  • конфигурации уровней
  • Ключевой выигрыш: данные можно править без изменений кода, а логику — тестировать отдельно.

    Коллекции в C# для Unity: выбор структуры под задачу

    Коллекции — это основа производительного кода: неправильная структура данных легко превращает стабильные 60 FPS в просадки.

    Ссылка:

  • Microsoft Learn: Collections (C#)
  • Массивы и List

  • T[] — фиксированный размер, быстро, минимум накладных расходов.
  • List<T> — динамический размер, удобные методы (Add, Remove, Find), но возможны перераспределения памяти при росте.
  • Практика:

  • Если вы заранее знаете максимум элементов, задавайте ёмкость: new List<T>(capacity).
  • В горячем коде (например, внутри Update) избегайте частых Add/Remove, если это вызывает лишние выделения памяти.
  • Dictionary и HashSet

  • Dictionary<TKey, TValue> — быстрый доступ по ключу (например, id -> данные).
  • HashSet<T> — быстро проверяет наличие элемента (уникальные элементы).
  • Важно для Unity:

  • Dictionary по умолчанию плохо сериализуется в инспекторе (встроенная сериализация Unity исторически не поддерживает обычные Dictionary так же прозрачно, как List). Часто используют обёртки, кастомные инспекторы или хранят данные иначе.
  • Queue и Stack

  • Queue<T> — FIFO (первым пришёл, первым вышел). Пример: очередь задач или событий.
  • Stack<T> — LIFO. Пример: история состояний, откат действий, стек UI-экранов.
  • Быстрый справочник: что когда использовать

    | Задача | Рекомендуемая структура | |---|---| | Список объектов, по которым вы проходите циклом | List<T> или массив | | Поиск объекта по уникальному ключу (id, имя, тип) | Dictionary<TKey, TValue> | | Проверка “есть ли элемент в наборе” | HashSet<T> | | Последовательная обработка задач по очереди | Queue<T> | | Откат/история, вложенность, “последний добавлен — первый обработан” | Stack<T> |

    LINQ в Unity: мощно, но нужно знать цену

    LINQ делает код компактным и выразительным, но в Unity важно понимать:

  • Многие LINQ-операции используют перечислители, лямбды и замыкания.
  • Это может приводить к выделениям памяти и нагрузке на GC.
  • Ссылки:

  • Microsoft Learn: LINQ (Language Integrated Query)
  • Unity Manual: Understanding automatic memory management
  • Deferred execution: отложенное выполнение

    Многие LINQ-выражения выполняются не сразу, а при перечислении.

    Это полезно для композиции запросов, но может быть опасно, если:

  • вы многократно перечисляете один и тот же запрос (вы многократно выполняете работу)
  • источник данных меняется между перечислениями
  • Если нужен результат “здесь и сейчас”, фиксируйте:

  • ToArray()
  • ToList()
  • Но помните: это создаёт новые объекты в памяти.

    Где LINQ уместен в Unity

    Хорошие места:

  • код редактора
  • загрузка/инициализация (однократно)
  • подготовка данных (кэширование)
  • нерегулярные операции вне Update
  • Осторожно в:

  • Update, FixedUpdate, LateUpdate
  • часто вызываемых обработчиках (например, каждый кадр на каждом враге)
  • Пример: создание словаря по списку (инициализация один раз):

    Исключения в Unity: как применять безопасно

    Исключения (exceptions) — механизм сигнализации об ошибках выполнения. В Unity необработанное исключение обычно приводит к ошибке в Console и прерыванию текущего выполнения метода.

    Ссылка:

  • Microsoft Learn: Exceptions and Exception Handling (C#)
  • try/catch/finally и throw

  • try — код, который может упасть
  • catch — обработка конкретной ошибки
  • finally — выполнится всегда (даже если была ошибка)
  • throw — выброс исключения
  • Практические правила для Unity-проектов

  • Не используйте исключения как обычный способ управления логикой (например, “попробуем — упадёт — значит не найдено”). Для этого есть Try...-паттерн и проверки.
  • В “горячем” коде исключения особенно дороги: они создают большой overhead и шум в логах.
  • Для проверок входных данных лучше:
  • - валидировать заранее - логировать понятное сообщение - мягко деградировать (например, отключить систему, показать fallback)

    Пример безопасного Try-подхода:

    SOLID в Unity: принципы, которые реально улучшают проект

    SOLID помогает делать код расширяемым, тестируемым и устойчивым к росту проекта. В Unity это особенно критично: игры быстро обрастают системами, а “быстрые решения” в компонентах превращаются в неуправляемую сеть зависимостей.

    Ссылка:

  • Wikipedia: SOLID
  • !Показано, как зависимости направляются к абстракциям (интерфейсам), а реализации подключаются отдельно

    Single Responsibility Principle

    Один модуль — одна ответственность.

    Плохой признак в Unity: один MonoBehaviour одновременно:

  • читает ввод
  • двигает персонажа
  • считает урон
  • обновляет UI
  • пишет в сохранения
  • Правильнее разделять:

  • PlayerInput только читает ввод
  • PlayerMotor только двигает
  • Health только отвечает за здоровье
  • HealthView только отображает здоровье
  • Open/Closed Principle

    Открыт для расширения, закрыт для изменения.

    В Unity это часто достигается так:

  • расширяете поведение новыми компонентами
  • добавляете новые реализации интерфейсов
  • используете ScriptableObject как расширяемые конфигурации
  • Пример идеи: вместо switch по типу оружия — интерфейс IWeapon и разные реализации.

    Liskov Substitution Principle

    Объекты дочернего класса должны заменять объекты базового без поломки логики.

    В Unity частый антипример: базовый класс обещает одно поведение, а наследник “ломает контракт” (например, базовый TakeDamage гарантирует уменьшение HP, а наследник вдруг игнорирует урон без явной причины).

    Практика:

  • формулируйте чёткие контракты
  • предпочитайте интерфейсы для поведения, а не глубокие иерархии
  • Interface Segregation Principle

    Лучше несколько маленьких интерфейсов, чем один большой.

    Плохо:

  • ICharacter с 20 методами (движение, атака, инвентарь, диалоги)
  • Хорошо:

  • IMovable, IAttackable, IInventoryOwner, ITalkable
  • Так компоненты получают только то, что им реально нужно.

    Dependency Inversion Principle

    Зависеть нужно от абстракций, а не от конкретных реализаций.

    В Unity это обычно означает:

  • код в компонентах работает через интерфейсы
  • конкретные реализации подставляются при инициализации (через инспектор, фабрику, сервис-локатор или DI-контейнер)
  • Мини-пример: компоненту нужен ввод, но не важно, клавиатура это или тач.

    Итоги

  • ООП в Unity лучше строить вокруг композиции, интерфейсов и небольших компонентов.
  • Коллекции выбирайте по операции: перебор, поиск по ключу, проверка наличия, очередь задач.
  • LINQ полезен для читаемости, но в кадре используйте осторожно из-за аллокаций и GC.
  • Исключения применяйте для ошибок, а не для обычного ветвления логики.
  • SOLID в Unity лучше реализуется через разделение ответственности, маленькие контракты и зависимости от абстракций.
  • 2. Продвинутый C#: async/await, события, делегаты, generics, память и GC

    Продвинутый C#: async/await, события, делегаты, generics, память и GC

    Эта статья продолжает фундамент из предыдущего материала про ООП, коллекции, LINQ, исключения и SOLID и переводит вас на уровень практического middle в Unity: вы научитесь писать расширяемые системы на делегатах/событиях, уверенно применять generics и понимать, почему аллокации и GC напрямую влияют на FPS.

    Ключевой контекст Unity:

  • Большинство Unity API должно вызываться из главного потока.
  • Любые регулярные аллокации в игровом цикле приводят к работе сборщика мусора и потенциальным фризам.
  • Асинхронность нужна не для того, чтобы “ускорить” CPU-код, а чтобы не блокировать кадр (I/O, ожидания, сеть, сервисы, подготовка данных).
  • Делегаты: типобезопасные функции как значения

    Делегат в C# — это тип, который хранит ссылку на метод (или список методов) с определённой сигнатурой.

    Базовые варианты:

  • Action и Action<T> — метод без возвращаемого значения.
  • Func<TResult> и Func<T, TResult> — метод, возвращающий значение.
  • Пользовательский delegate — когда нужна собственная сигнатура/имя.
  • Ссылки:

  • Delegates (C#)
  • Зачем делегаты Unity-разработчику

    Типовые use-case в проектах:

  • Колбэки для UI: кнопка сообщает, что по ней нажали.
  • Реакции на изменения состояния: здоровье изменилось, нужно обновить HUD.
  • Инверсия зависимостей: вместо жёсткой связи “класс A знает класс B” вы передаёте “что делать” через делегат.
  • Пример: передаём поведение в чистый класс (без Unity API).

    События: безопасная публикация уведомлений

    Событие — это “обёртка” над делегатом, которая ограничивает внешний код: снаружи можно только подписаться (+=) и отписаться (-=), но нельзя присвоить событию новое значение.

    Ссылка:

  • Events (C#)
  • Почему в Unity важно правильно отписываться

    Подписчик удерживается ссылкой в списке делегатов. Если объект должен быть уничтожен, но на него всё ещё подписаны, то:

  • он может не собраться GC, потому что на него есть ссылки
  • или будет происходить вызов методов на “мертвом” объекте (часто это превращается в ошибки и неожиданные состояния)
  • Практический паттерн для MonoBehaviour:

  • подписка в OnEnable
  • отписка в OnDisable
  • EventHandler или Action

    В проектах встречаются оба стиля:

  • Action<T> проще и часто достаточен.
  • EventHandler<TEventArgs> удобен, если вам нужна стандартная форма события и объект-источник.
  • Ссылка:

  • EventHandler Delegate
  • async/await: асинхронность без блокировки кадра

    async/await в C# — это синтаксис для удобной работы с Task (задачами), которые могут завершиться в будущем.

    Ссылки:

  • Asynchronous programming with async and await (C#)
  • Task class
  • Что важно понять в Unity

  • await не создаёт поток сам по себе.
  • После await продолжение обычно возвращается в тот контекст, откуда стартовали (в Unity это часто главный поток), но это зависит от того, как вы устроили код.
  • Unity API обычно нельзя вызывать из фонового потока.
  • !Диаграмма показывает, что тяжёлая операция выполняется отдельно, а Unity API вызывается после возвращения на главный поток

    Практические сценарии

  • Сетевые запросы (не блокировать кадр ожиданием ответа).
  • Загрузка/чтение файлов.
  • Ожидание времени или внешнего события.
  • Параллельная подготовка данных (только если это не Unity API).
  • Почему корутины и async/await — не одно и то же

    Корутина Unity (IEnumerator + StartCoroutine) — это механизм планирования выполнения по кадрам в главном потоке.

    Ссылка:

  • Coroutines
  • Отличия, которые важны на практике:

  • Корутина “живет” в Unity player loop и естественно дружит с yield return null и yield return new WaitForSeconds.
  • Task и await — это инфраструктура .NET, удобная для I/O и композиции асинхронных операций.
  • Частая стратегия в production:

  • корутины для “по кадрам”
  • async/await для I/O и сервисов
  • CancellationToken: как отменять асинхронные операции

    Если объект уничтожился, а ваша асинхронная операция всё ещё выполняется, вы рискуете:

  • обновлять состояние несуществующего объекта
  • ловить исключения
  • расходовать ресурсы
  • Для отмены используют CancellationToken.

    Ссылки:

  • CancellationToken struct
  • CancellationTokenSource class
  • async void: почему почти всегда нельзя

  • async void нельзя await-ить, значит сложнее контролировать завершение и обрабатывать ошибки.
  • Исключения из async void сложнее централизованно обрабатывать.
  • Правило для Unity:

  • используйте async Task почти везде
  • async void допустим только для обработчиков событий, где сигнатура обязана быть void
  • Ссылка:

  • Async return types (C#)
  • Исключения в async-коде

    Исключение, возникшее внутри Task, “всплывает” при await. Поэтому обрабатывайте ошибки там, где вы await-ите задачу, либо внутри самого метода, если он отвечает за стабильность.

    Это напрямую связано с предыдущей статьёй про исключения: в async-коде они не исчезают, но меняется точка, где вы их наблюдаете.

    Generics: обобщения как база для переиспользуемого кода

    Generics позволяют писать код, который работает с разными типами без потери типобезопасности и без постоянных приведений типов.

    Ссылки:

  • Generics in C#
  • Generic collections in .NET
  • Почему generics важны в Unity

  • Повторное использование кода: один Pool<T> вместо десяти пулов под каждый тип.
  • Меньше ошибок: тип проверяется компилятором.
  • Часто меньше аллокаций и приведения типов, чем при использовании object.
  • Пример: простой generic-пул объектов

    Пул — одна из основных техник борьбы с аллокациями и GC в Unity.

    Применение в Unity обычно дополняют правилами:

  • при Release возвращать объект в “нейтральное” состояние
  • отключать GameObject и включать при Get
  • Generic constraints: ограничения на типы

    Ограничения нужны, когда вашему коду важны свойства типа.

    Примеры:

  • where T : class — только ссылочные типы.
  • where T : struct — только значимые типы.
  • where T : new() — у типа есть публичный конструктор без параметров.
  • where T : MonoBehaviour — тип является компонентом Unity.
  • Ссылка:

  • Constraints on type parameters (C#)
  • Память в Unity: управляемая, неуправляемая, аллокации

    Чтобы управлять производительностью, нужно различать типы памяти:

  • Управляемая память: объекты C# в куче, за их освобождение отвечает GC.
  • Неуправляемая память: ресурсы движка и нативные буферы (текстуры, меши, аудио), живут по другим правилам.
  • Ссылки:

  • Unity: Understanding automatic memory management
  • Unity: Memory Profiler package
  • Аллокация и почему она опасна в кадре

    Когда вы создаёте новые управляемые объекты, GC рано или поздно запустится, чтобы освободить память. В Unity это может приводить к заметным паузам.

    Важная практическая мысль:

  • не каждая аллокация — проблема
  • проблема — регулярные аллокации в игровом цикле, особенно если они происходят на множестве объектов
  • Типовые источники GC в Unity-проектах

  • Создание новых объектов в Update (например, new List<T>(), new SomeClass()).
  • Строковые операции (конкатенация, частые ToString() при логировании).
  • LINQ в горячем коде (про это было в предыдущей статье, и это особенно заметно на мобилках).
  • Boxing (упаковка значимых типов в object).
  • Ссылка:

  • Boxing and unboxing (C#)
  • Boxing на практике

    Boxing часто появляется неожиданно:

  • когда вы передаёте int как object
  • когда используете не-generic API
  • когда храните значимый тип в коллекции, рассчитанной на object
  • Как избегать:

  • использовать generic-коллекции (List<int>, а не ArrayList)
  • проектировать интерфейсы типобезопасно
  • Foreach и аллокации

    Полезное правило для Unity:

  • foreach по List<T> обычно не даёт аллокаций, потому что перечислитель — структурный
  • foreach по IEnumerable<T> может привести к аллокациям, если под капотом создаётся объект-перечислитель
  • На уровне middle важно не “запрещать foreach”, а уметь проверять в профайлере.

    Как диагностировать

    Инструменты Unity:

  • Profiler: смотрите GC.Alloc и пики CPU вокруг GC
  • Memory Profiler: снимки памяти и поиск удерживаемых ссылок
  • Ссылки:

  • Unity: Profiler
  • Unity: Profiling
  • GC и архитектура: как связать с SOLID

    Здесь важно соединить тему производительности с темой архитектуры из прошлой статьи.

    Правильная архитектура помогает производительности:

  • SRP упрощает локализацию источника аллокаций (понятно, какой компонент виноват).
  • DIP позволяет подменять реализации (например, реальный сервис на мок) и тестировать без “шумных” аллокаций.
  • Композиция вместо монолитных классов облегчает внедрение пулов и кэширование.
  • Но есть и обратная сторона: абстракции могут добавлять косвенности.

    Практический баланс для middle:

  • абстракции и события используйте там, где это уменьшает связанность
  • в горячих местах (часто вызываемый код) держите код проще, меньше замыканий, меньше выделений памяти
  • Практические правила уровня middle

  • События:
  • - подписка в OnEnable, отписка в OnDisable - не держать в событиях “вечные” ссылки на временные объекты
  • Делегаты и лямбды:
  • - помните про замыкания: захват переменных может создавать лишние аллокации
  • async/await:
  • - не вызывайте Unity API из фонового потока - используйте CancellationToken для жизненного цикла объектов - избегайте async void вне обработчиков событий
  • Generics:
  • - пишите переиспользуемые инструменты: пул, реестр, кэш - используйте ограничения типов, когда это делает контракт точнее
  • Память:
  • - не создавайте мусор в Update - профилируйте, а не гадайте

    Итоги

  • Делегаты и события — основа модульных игровых систем и слабой связанности компонентов.
  • async/await полезен для I/O и ожиданий, но требует дисциплины: главный поток, отмена, обработка ошибок.
  • Generics — ключ к переиспользуемой инфраструктуре (пулы, кэши, сервисы) без потери типобезопасности.
  • Память и GC в Unity напрямую влияют на плавность: цель middle — уметь находить источники GC.Alloc и устранять их архитектурно и точечно.
  • 3. Unity Core: сцены, GameObject, компоненты, Prefab, жизненный цикл

    Unity Core: сцены, GameObject, компоненты, Prefab, жизненный цикл

    Unity как движок строится вокруг связки сцена → GameObject → компоненты, а выполнение кода завязано на жизненный цикл MonoBehaviour. Для middle-уровня важно понимать не только что это такое, но и как это влияет на архитектуру, производительность, связанность кода и работу с данными.

    Эта статья связывает Unity-ядро с тем, что вы уже разобрали в предыдущих материалах:

  • ООП и SOLID помогают держать компоненты маленькими и с одной ответственностью.
  • События и делегаты позволяют связывать компоненты без жёстких зависимостей.
  • Память и GC объясняют, почему частые Instantiate/Destroy и аллокации в кадре приводят к фризам.
  • Сцены

    Сцена в Unity — это контейнер для объектов мира: окружение, персонажи, камеры, свет, точки спавна, UI (если он не вынесен отдельно), а также ссылки между объектами.

    Официальная документация:

  • Scenes
  • SceneManager
  • Что важно знать про сцены в production

  • Сцена хранит состояние уровня и ссылки на объекты.
  • При загрузке сцены Unity создаёт объекты и вызывает их сообщения жизненного цикла.
  • Загрузка бывает одиночная и аддитивная.
  • LoadSceneMode.Single:

  • текущая сцена выгружается
  • загружается новая
  • LoadSceneMode.Additive:

  • новая сцена добавляется поверх текущей
  • часто используют для подгрузки частей мира, UI-сцены, загрузочных сцен
  • Ссылка:

  • LoadSceneMode
  • Как правильно держать “глобальные” объекты

    Если вы делаете сервисы (аудио, аналитика, инвентарь игрока, менеджер сохранений), часто их не хочется пересоздавать между сценами. Базовый способ — DontDestroyOnLoad.

    Ссылка:

  • Object.DontDestroyOnLoad
  • Практические правила:

  • Делайте “глобальные” объекты явными: отдельный GameObject вроде ProjectContext.
  • Не размножайте синглтоны при переходах между сценами: проверяйте, что экземпляр один.
  • Помните про зависимости: “глобальный” объект не должен хранить прямые ссылки на объекты сцены, иначе получите утечки ссылок и ошибки при выгрузке.
  • GameObject и Transform

    GameObject — сущность сцены. Сам по себе GameObject почти ничего не делает: поведение задаётся компонентами.

    Ссылки:

  • GameObject
  • Transform
  • Transform как обязательная часть

    У каждого GameObject есть Transform.

  • позиция, поворот, масштаб
  • иерархия: родитель и дети
  • Важно для понимания поведения:

  • изменение transform.position меняет положение объекта в мире
  • если объект дочерний, его мировые координаты зависят от родителя
  • !Иерархия сцена → объект → компоненты и дерево Transform

    Активность GameObject и компонентов

    gameObject.SetActive(false):

  • отключает объект целиком
  • OnDisable вызовется у компонентов
  • Update и подобные методы перестанут вызываться
  • enabled = false у компонента:

  • отключает только этот компонент
  • сам объект и другие компоненты продолжают работать
  • Ссылка:

  • Behaviour.enabled
  • Компоненты

    Компонент — модуль поведения или данных, прикреплённый к GameObject.

    Ссылки:

  • Components
  • MonoBehaviour
  • Почему Unity архитектурно про композицию

    С точки зрения SOLID, Unity подталкивает к SRP:

  • один компонент отвечает за одну часть поведения
  • несколько компонентов собираются в целого персонажа или систему
  • Типичный пример:

  • CharacterMotor двигает
  • Health хранит здоровье
  • WeaponController стреляет
  • HealthView отображает здоровье
  • Связь между компонентами лучше делать через:

  • интерфейсы
  • события
  • ссылки, назначенные в инспекторе
  • Это напрямую продолжает идеи из тем про ООП, события и DIP.

    Получение ссылок на компоненты

    Основные способы:

  • [SerializeField] private SomeComponent ref; и назначить в инспекторе
  • GetComponent<T>() найти компонент на этом же объекте
  • GetComponentInChildren<T>() найти в детях
  • GetComponentInParent<T>() найти в родителях
  • Ссылки:

  • Component.GetComponent
  • Component.GetComponentInChildren
  • Практические правила уровня middle:

  • Ссылки на зависимости лучше задавать через инспектор или инициализацию, а не искать каждый кадр.
  • Частые GetComponent в Update — типичная ошибка производительности.
  • Если зависимость обязательна, валидируйте её в Awake и логируйте понятную ошибку.
  • Пример проверки зависимости:

    ScriptableObject как данные

    Для конфигураций (баланс, параметры, таблицы) предпочтительны ScriptableObject, чтобы:

  • отделять данные от поведения
  • не плодить хардкод
  • расширять игру без переписывания логики
  • Ссылка:

  • ScriptableObject
  • Prefab

    Prefab — это шаблон объекта: набор GameObject и компонентов, который можно многократно инстанцировать, не копируя вручную.

    Ссылки:

  • Prefabs
  • Object.Instantiate
  • Зачем Prefab нужен на практике

  • единый источник правды для врагов, оружия, VFX, UI-элементов
  • быстрые итерации: правите Prefab, изменения применяются ко всем инстансам
  • архитектурная дисциплина: логика в компонентах, данные в SO или сереализованных полях
  • Инстансы, оверрайды и варианты

    Unity различает:

  • Prefab asset в проекте
  • Prefab instance на сцене
  • У инстанса могут быть overrides:

  • изменённые значения полей
  • добавленные или удалённые компоненты
  • изменения в дочерних объектах
  • Если переопределений слишком много, это:

  • усложняет поддержку
  • приводит к “непонятно, почему тут иначе”
  • Практика:

  • общий шаблон держите в Prefab
  • вариативность делайте через настройки (поля, ScriptableObject, параметры спавнера)
  • используйте Prefab Variants, когда нужна “ветка” шаблона
  • Ссылка:

  • Prefab Variants
  • Instantiate/Destroy и производительность

    Instantiate и Destroy создают и уничтожают объекты, что обычно связано с:

  • аллокациями управляемой памяти
  • ростом нагрузки на GC
  • пиками CPU при массовом создании/удалении
  • Это напрямую связано с предыдущей темой про память и GC. В production для частых объектов (пули, враги, эффекты) обычно используют пулы.

    Ссылки:

  • Object.Destroy
  • Understanding automatic memory management
  • Жизненный цикл MonoBehaviour

    Жизненный цикл определяет, когда Unity вызывает ваши методы, и как правильно инициализировать зависимости, подписываться на события и освобождать ресурсы.

    Ссылки:

  • Execution Order of Event Functions
  • MonoBehaviour
  • !Лента вызовов методов MonoBehaviour

    Основные методы и их смысл

    Awake:

  • вызывается при создании компонента
  • используйте для инициализации внутренних полей и кэширования ссылок
  • OnEnable:

  • вызывается при включении компонента или объекта
  • хорошее место для подписок на события
  • Start:

  • вызывается перед первым кадром, но после Awake
  • удобен для логики, которая требует, чтобы все Awake уже отработали
  • Update:

  • вызывается каждый кадр
  • здесь должна быть лёгкая, предсказуемая работа без регулярных аллокаций
  • FixedUpdate:

  • вызывается с фиксированным шагом физики
  • используйте для работы с Rigidbody и физическими силами
  • LateUpdate:

  • вызывается после Update
  • часто используют для камер, которые должны “догонять” то, что обновилось в Update
  • OnDisable:

  • вызывается при выключении компонента или объекта
  • место для отписок от событий и остановки корутин
  • OnDestroy:

  • вызывается при уничтожении объекта
  • используйте для финальной очистки, если объект реально уничтожается
  • Ссылки:

  • MonoBehaviour.Awake
  • MonoBehaviour.OnEnable
  • MonoBehaviour.Start
  • MonoBehaviour.Update
  • MonoBehaviour.FixedUpdate
  • MonoBehaviour.LateUpdate
  • MonoBehaviour.OnDisable
  • MonoBehaviour.OnDestroy
  • Типовой безопасный паттерн: события и жизненный цикл

    Это соединяет Unity lifecycle с темой событий и делегатов.

    Почему это важно:

  • подписка в OnEnable гарантирует, что объект активен
  • отписка в OnDisable защищает от утечек ссылок и вызовов на уничтоженных объектах
  • Порядок исполнения и зависимости

    Одна из частых причин багов у начинающих и уверенный маркер middle-уровня — понимать порядок вызовов.

    Практические выводы:

  • Не полагайтесь на то, что Start одного объекта выполнится раньше другого.
  • Если порядок критичен, проектируйте явную инициализацию: отдельный “компоновщик” зависимостей, фабрика, или настройка порядка.
  • Unity позволяет настроить приоритет выполнения скриптов.

    Ссылка:

  • Script Execution Order
  • Как это связывается с архитектурой уровня middle

    Чтобы двигаться к middle, полезно держать в голове простую связку:

  • Сцена задаёт композицию уровня.
  • GameObject — контейнер.
  • Компоненты — маленькие модули поведения и данных.
  • Prefab — повторяемый шаблон и точка стандартизации.
  • Жизненный цикл — контракт “когда можно что делать”.
  • Практические правила:

  • Делайте компоненты небольшими и с одной ответственностью (SRP).
  • Склеивайте систему событиями и интерфейсами, а не “жёсткими” ссылками (DIP).
  • Не ищите компоненты и не создавайте мусор в Update, кэшируйте и используйте пулы.
  • Подписывайтесь в OnEnable, отписывайтесь в OnDisable.
  • Итоги

  • Сцены управляют загрузкой, выгрузкой и жизненным циклом объектов.
  • GameObject — контейнер, а Transform формирует иерархию.
  • Компоненты — основной способ построения поведения через композицию.
  • Prefab — инструмент стандартизации и масштабирования контента.
  • Жизненный цикл MonoBehaviour задаёт правила инициализации, обновления и очистки, и напрямую влияет на стабильность и архитектуру.
  • 4. Архитектура и паттерны в Unity: DI, FSM, MVC, ScriptableObject, сервисы

    Архитектура и паттерны в Unity: DI, FSM, MVC, ScriptableObject, сервисы

    На уровне middle вы отличаетесь не тем, что знаете больше методов Unity API, а тем, что умеете собирать систему так, чтобы она:

  • расширялась без переписывания старого кода
  • тестировалась вне сцены
  • не разъезжалась из-за порядка Awake/Start
  • не создавала лишние аллокации и зависимостей
  • Эта статья связывает предыдущие темы курса:

  • из ООП и SOLID берём разделение ответственности и зависимости от абстракций
  • из темы событий берём слабую связанность и жизненный цикл подписок OnEnable/OnDisable
  • из темы памяти и GC берём дисциплину: не создавать мусор в кадре и не плодить Instantiate/Destroy
  • из Unity Core берём понимание, где “живёт” код: MonoBehaviour, сцены, префабы и порядок вызовов
  • Базовая цель архитектуры в Unity

    В Unity легко скатиться в монолитные MonoBehaviour, которые делают всё сразу. Архитектура нужна, чтобы выстраивать систему из небольших частей:

  • чистая логика на C#-классах без Unity API
  • слой интеграции с Unity на компонентах (MonoBehaviour), которые читают ввод, двигают Transform, запускают анимацию, обновляют UI
  • композиция зависимостей в одном месте (bootstrap), а не “каждый ищет каждого” через Find и GetComponent в рантайме
  • Практическое правило: чем больше логики вне MonoBehaviour, тем проще поддержка, тестирование и перенос между проектами.

    !Диаграмма показывает разделение на слои и направление зависимостей

    DI в Unity: внедрение зависимостей без магии

    Dependency Injection (DI) — это способ передать объекту то, от чего он зависит, снаружи, вместо того чтобы создавать зависимости внутри.

    В терминах SOLID это практическая реализация принципа DIP: вы зависите от интерфейса, а конкретная реализация подставляется при сборке.

    Зачем DI в Unity

  • меньше скрытых зависимостей (new внутри логики, FindObjectOfType в каждом классе)
  • проще заменить реализацию (например, реальный звук на заглушку в тестах)
  • проще контролировать порядок инициализации
  • DI без контейнера: то, что реально нужно middle

    В Unity часто достаточно ручного DI:

  • через конструктор для чистых C#-классов
  • через инициализацию методом для классов, которые создаются фабрикой
  • через поля [SerializeField] для MonoBehaviour (но с дисциплиной)
  • #### Конструкторное внедрение для чистой логики

    Плюсы:

  • Cooldown не знает про Unity
  • его можно тестировать обычным юнит-тестом
  • #### Внедрение в MonoBehaviour: композиция в одном месте

    MonoBehaviour нельзя нормально создавать через new, поэтому чаще всего зависимости подаются:

  • через инспектор
  • через “bootstrap” объект (контекст проекта)
  • Ключевой момент: точка композиции — это место, где вы собираете граф объектов. В Unity удобно держать это в одном ProjectContext.

    Ссылки:

  • Unity Manual: Execution Order of Event Functions
  • Unity Scripting API: Object.DontDestroyOnLoad
  • Антипаттерны зависимостей в Unity

  • FindObjectOfType в рантайме как способ “добыть” сервис
  • GetComponent каждый кадр
  • “менеджер всего”, который напрямую знает все системы и дергает их методы
  • Это ломает тестируемость, усложняет порядок инициализации и часто ухудшает производительность.

    Сервисы в Unity: как делать глобальные системы без хаоса

    Сервис — это объект, который предоставляет функцию всему проекту: звук, вибрация, сохранения, аналитика, загрузка ресурсов.

    Признаки хорошего сервиса

  • есть интерфейс контракта, например IAudioService
  • реализация подменяема
  • сервис не держит прямых ссылок на объекты сцены (иначе будут утечки и ошибки при смене сцен)
  • Service Locator vs DI

    Service Locator — это когда вы глобально спрашиваете: Services.Get<IAudioService>().

  • плюс: быстро подключить
  • минус: зависимость становится скрытой (по коду не видно, что объект зависит от аудио)
  • Для middle-практики правило такое:

  • допустимо использовать сервис-локатор как временное решение
  • лучше стремиться к явному DI: зависимости должны быть видны в полях/конструкторе
  • Bootstrap и композиция сервисов

    Это не единственно верный подход, но он делает зависимости явными (через поле) и не требует рефлексии или контейнера.

    FSM: конечный автомат состояний для поведения

    Finite State Machine (FSM) — это модель поведения, где объект находится в одном состоянии из набора, и переключается по событиям.

    Типовые применения в Unity:

  • AI врагов: Patrol, Chase, Attack, Flee
  • игрок: Idle, Run, Jump, Dash
  • UI: Closed, Opening, Opened, Closing
  • FSM полезен тем, что структурирует логику и убирает “лес” из if в Update.

    Минимальная структура FSM

  • IState — контракт состояния
  • StateMachine — владелец текущего состояния
  • конкретные состояния — небольшие классы
  • Пример: FSM для врага (логика отдельно от Unity)

    А в MonoBehaviour вы только связываете это с Unity:

    Практические правила:

  • состояние должно быть маленьким и держать только нужные зависимости
  • переходы делайте либо в одном месте (в “мозге”), либо через явные события
  • избегайте аллокаций при переключениях: состояния создавайте заранее и переиспользуйте
  • MVC в Unity: разделяем данные, отображение и управление

    MVC (Model-View-Controller) — подход, где:

  • Model — данные и правила (чистый C#)
  • View — отображение (часто MonoBehaviour, UI)
  • Controller — принимает ввод/события и изменяет модель
  • В Unity чаще встречаются вариации (MVVM, MVP), но базовая идея одна: UI и логика не должны быть одним классом.

    Пример: здоровье игрока как MVC

    Model:

    View:

    Controller (склеивает, подписывается и отписывается по жизненному циклу):

    Что это даёт:

  • модель не зависит от Unity и легко тестируется
  • view не содержит правил (только рисует)
  • контроллер становится точкой, где применяются правила жизненного цикла Unity
  • Ссылка по событийному контракту и best practice подписок связана с темой жизненного цикла MonoBehaviour:

  • Unity Manual: Execution Order of Event Functions
  • ScriptableObject: данные, конфигурации и “архитектура через ассеты”

    ScriptableObject — это тип ассета для хранения данных в проекте.

    Ссылка:

  • Unity Manual: ScriptableObject
  • Когда ScriptableObject — правильный выбор

  • конфигурации оружия, врагов, уровней
  • таблицы баланса
  • настройки, которые должны редактироваться дизайнерами
  • Пример конфигурации:

    Частая ловушка: хранить рантайм-состояние в ScriptableObject

    ScriptableObject является ассетом. Это значит:

  • он общий для всех объектов, которые на него ссылаются
  • изменения в рантайме могут неожиданно влиять на все инстансы
  • Правило:

  • ScriptableObject для данных и настроек
  • рантайм-состояние держите в моделях или компонентах
  • Если вам нужно “состояние по конфигу”, делайте копию состояния в рантайме:

    Паттерн Event Channel на ScriptableObject

    Иногда удобно хранить не только данные, но и “канал событий” как ассет, чтобы:

  • связывать сцены и объекты без прямых ссылок
  • подписывать UI и геймплей на одно событие
  • Важно:

  • подписки всё равно нужно снимать в OnDisable, иначе получите утечки и вызовы “в никуда”
  • такой подход не заменяет DI полностью, но помогает разрезать зависимости между сценами и системами
  • Как это собрать в проект: практическая схема

    В типичном проекте уровня middle хорошо работает следующая дисциплина:

  • ProjectContext (или аналог) живёт через DontDestroyOnLoad и создаёт сервисы
  • сцены содержат только контент и “локальные” системы
  • чистая логика живёт в обычных C#-классах
  • все связи делаются либо:
  • - через явные ссылки в инспекторе - через явную инициализацию в Awake - через события (с аккуратной подпиской/отпиской)

    Ссылка для загрузки сцен и контекста (если вы делаете аддитивную архитектуру сцен):

  • Unity Scripting API: SceneManager
  • Итоги

  • DI в Unity чаще всего реализуется вручную: конструкторы для чистых классов, инспектор и bootstrap для MonoBehaviour.
  • Сервисы полезны для глобальных систем, но должны иметь интерфейс и не держать ссылки на объекты сцен.
  • FSM — практичный способ структурировать поведение персонажей и UI, заменяя сложные if на состояния.
  • MVC помогает разделить модель, отображение и управление, чтобы UI не превращался в монолит.
  • ScriptableObject — лучший инструмент для конфигураций и данных, но рантайм-состояние в нём нужно хранить осторожно.
  • 5. Игровые системы: Physics, Animation, UI, Input System, Audio

    Игровые системы: Physics, Animation, UI, Input System, Audio

    Middle Unity developer отличается тем, что умеет собирать игровые системы так, чтобы они:

  • работали предсказуемо в рамках жизненного цикла Unity
  • не ломались из-за скрытых зависимостей и порядка инициализации
  • не создавали лишние аллокации и не провоцировали GC-фризы
  • Эта статья связывает знания из предыдущих тем курса:

  • из C# и SOLID берём разделение ответственности, работу через интерфейсы и слабую связанность
  • из делегатов и событий берём безопасную коммуникацию между системами
  • из памяти и GC берём дисциплину производительности
  • из Unity Core берём понимание сцен, компонентов, префабов и порядка вызовов
  • !Карта того, как обычно связаны ввод, геймплей, физика, анимации, UI и аудио

    Physics

    Physics в Unity отвечает за столкновения, триггеры, запросы в пространство (raycast) и симуляцию движения через физическое тело.

    Официальные источники:

  • Unity Manual: Physics
  • Unity Scripting API: Rigidbody
  • Unity Scripting API: Collider
  • Unity Scripting API: Physics.Raycast
  • Компоненты и базовые понятия

  • Collider задаёт форму для столкновений
  • Rigidbody делает объект участником физической симуляции
  • Триггер это коллайдер с isTrigger = true, он не “толкается”, но сообщает о пересечении
  • Слои и матрица коллизий определяют, какие объекты вообще могут сталкиваться
  • Практическое правило: если вы хотите физическое поведение, двигайте объект через Rigidbody, а не через прямую запись в transform.position.

    Update и FixedUpdate

    Физика в Unity шагает с фиксированным интервалом и вызывает FixedUpdate.

    Ссылки:

  • Unity Manual: Execution Order of Event Functions
  • Unity Scripting API: MonoBehaviour.FixedUpdate
  • Практический смысл:

  • ввод и визуальные эффекты часто читают в Update
  • применение сил и физическое движение чаще делают в FixedUpdate
  • Триггеры и столкновения

    События, которые Unity вызывает на MonoBehaviour:

  • OnCollisionEnter/Stay/Exit для столкновений не-триггеров
  • OnTriggerEnter/Stay/Exit для триггеров
  • Ссылки:

  • Unity Scripting API: MonoBehaviour.OnCollisionEnter
  • Unity Scripting API: MonoBehaviour.OnTriggerEnter
  • Пример: урон при входе в триггер, без жёсткой зависимости от конкретного класса.

    Почему это middle-уровень:

  • DamageZone зависит от абстракции IDamageable, а не от Enemy/Player
  • вы легко добавите новые типы целей без переписывания зоны
  • Raycast и физические запросы

    Physics.Raycast позволяет “спросить мир”: есть ли что-то на луче.

    Типовые применения:

  • стрельба хитаcканом
  • взаимодействие (подобрать предмет, открыть дверь)
  • проверка земли под персонажем
  • Ссылка:

  • Unity Scripting API: Physics.Raycast
  • Практика производительности:

  • используйте LayerMask, чтобы не проверять лишние объекты
  • для частых запросов используйте Physics.RaycastNonAlloc, чтобы избегать аллокаций
  • Ссылка:

  • Unity Scripting API: Physics.RaycastNonAlloc
  • Типовые ошибки в physics-коде

  • движение Rigidbody через transform вызывает нестабильность коллизий
  • “поиск всего” через FindObjectOfType вместо явных ссылок и DI
  • обработка столкновений, которая делает тяжёлую работу и аллокации прямо в событии
  • Практика: событие столкновения должно быть “тонким” и быстро сообщать в геймплей, что произошло.

    Animation

    Animation в Unity обычно строится вокруг Animator и графа состояний (Animator Controller). Цель middle-подхода: анимация должна быть выходом из состояния геймплея, а не источником бизнес-логики.

    Официальные источники:

  • Unity Manual: Animation
  • Unity Scripting API: Animator
  • Animator Controller и параметры

    Animator управляет переходами между анимационными состояниями через параметры:

  • bool для флагов
  • float для скорости, прицеливания, blend tree
  • int для выбора варианта
  • trigger для одноразового импульса перехода
  • Практическое правило: не дергать анимации из десяти мест. Один слой кода должен быть ответственен за синхронизацию состояния персонажа и параметров Animator.

    Избавляемся от строк в горячем коде

    Частая проблема: вызывать animator.SetBool("IsRunning", true) со строкой. Это работает, но лучше использовать хеш.

    Ссылка:

  • Unity Scripting API: Animator.StringToHash
  • Почему это важно:

  • меньше ошибок из-за опечаток
  • меньше лишней работы со строками
  • Root motion и что это такое

    Root motion это режим, когда анимация сама двигает объект по данным из клипа, а не ваш код. Он полезен для некоторых типов игр, но усложняет контроль физики.

    Практическая рекомендация:

  • если у вас физический персонаж на Rigidbody, чаще держите движение в коде, а анимацию синхронизируйте параметрами
  • если у вас “анимационно-ориентированное” движение, заранее продумайте, как оно сочетается с коллизиями и контролем скорости
  • События анимации

    Animation Event это вызов метода в заданный момент клипа. Часто используют для:

  • звука шага
  • вспышки выстрела
  • момента нанесения урона в мили-атаке
  • Ссылка:

  • Unity Manual: Animation Events
  • Правило middle-уровня: анимация может сообщать “момент”, но правила урона, попадания и кулдаунов должны жить в геймплейном коде.

    UI

    UI в Unity чаще всего делают на uGUI: Canvas, RectTransform, компоненты вроде Button, Slider, Image.

    Официальные источники:

  • Unity Manual: UI System
  • Unity Manual: Canvas
  • Unity Manual: Event System
  • Роли UI в архитектуре

    UI в продакшене почти всегда должен быть представлением:

  • читает состояние модели через подписки или явные вызовы
  • отображает данные
  • отправляет намерения пользователя наружу (клик, выбор)
  • Это напрямую продолжает подход из темы про MVC/MVP: View не хранит правила игры.

    Стоимость UI и типовые оптимизации

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

    Пояснения терминов:

  • Layout это система расчёта позиций/размеров элементов (например, VerticalLayoutGroup)
  • Rebuild это обновление геометрии UI после изменений
  • Практические правила:

  • не обновляйте текст, слайдеры и layout каждый кадр, если значение не менялось
  • избегайте частых включений/выключений сложных UI-деревьев без необходимости
  • используйте пул UI-элементов для списков, инвентаря, лога
  • Пример связки модель-событие-view

    Ключевой момент: подписка и отписка привязаны к OnEnable/OnDisable, как вы уже делали в теме про события.

    Input System

    Новый Input System в Unity построен вокруг действий.

    Пояснение:

  • Действие это абстракция вроде “Прыжок” или “Движение”, а не конкретная кнопка
  • Привязка связывает действие с клавишей, геймпадом, тачем
  • Официальные источники:

  • Unity Manual: Input System package
  • Unity Scripting API: UnityEngine.InputSystem.InputAction
  • Unity Scripting API: UnityEngine.InputSystem.PlayerInput
  • Polling и события

    Есть два стиля работы:

  • Polling это читать значение в Update, например moveAction.ReadValue<Vector2>()
  • События это подписаться на performed/canceled и получать уведомления
  • Правило middle-уровня: выбирайте стиль под задачу.

  • непрерывное движение обычно удобно читать polling-ом
  • одноразовые действия вроде “прыжок” удобно ловить событием
  • Важное правило жизненного цикла

    InputAction нужно включать и выключать.

  • включение обычно в OnEnable
  • выключение обычно в OnDisable
  • Это согласуется с общим правилом Unity про управление подписками и ресурсами.

    Адаптер ввода под DIP

    Чтобы геймплей не зависел от конкретного ввода, делайте интерфейс.

    Что это даёт:

  • код движения персонажа может зависеть от IPlayerInput
  • вы сможете заменить ввод на AI или запись реплея без переписывания моторики
  • Audio

    Аудио в Unity обычно строится из:

  • AudioClip как звуковых данных
  • AudioSource как проигрывателя на объекте
  • AudioListener как “ушей” сцены
  • AudioMixer как микшера для групповой настройки громкости и эффектов
  • Официальные источники:

  • Unity Manual: Audio
  • Unity Scripting API: AudioSource
  • Unity Manual: Audio Mixer
  • One-shot, лупы и 3D звук

    Пояснения:

  • One-shot это короткий звук, который проигрывается один раз (клик, выстрел)
  • Луп это звук, который играет циклично (двигатель, дождь)
  • 3D звук это режим, когда громкость и панорама зависят от расстояния и позиции
  • Ключевой момент для архитектуры: большинство систем не должно знать про конкретные AudioSource на сцене. Они должны просить “проиграй звук” у аудио-сервиса.

    Аудио-сервис как глобальная система

    Это продолжает тему сервисов и DI.

    Дальше вы развиваете это до production-уровня:

  • несколько источников под разные группы звуков
  • пул AudioSource для частых one-shot, чтобы не создавать объекты в момент выстрела
  • управление громкостью через AudioMixer
  • Настройки громкости через AudioMixer

    AudioMixer полезен, когда вам нужно:

  • один слайдер для всей музыки
  • один слайдер для всех SFX
  • быстро выключить/включить группы
  • Это обычно делается через exposed parameters в микшере и сохранение настроек.

    Как связывать системы без хаоса

    Ниже практическая дисциплина, которая обычно ожидается от middle:

  • Input System сообщает “намерения” игрока, но не двигает физику напрямую
  • геймплейный слой решает правила и вызывает motor, animator, audio, UI
  • Physics сообщает о фактах мира (столкновение, триггер), а правила реакции живут в геймплее
  • UI подписывается на изменения модели и не решает правила
  • Audio живёт как сервис и не держит ссылок на объекты конкретной сцены
  • Практический чеклист

  • Physics: движение Rigidbody в FixedUpdate, запросы через LayerMask, без аллокаций в горячем коде
  • Animation: один владелец параметров Animator, хеши вместо строк, анимация не хранит правила
  • UI: обновлять только при изменениях, избегать тяжёлых перерасчётов каждый кадр
  • Input System: явное Enable/Disable, разделять polling и события, ввод как интерфейс
  • Audio: сервисный слой, пул для частых one-shot, микшер для настроек
  • 6. Оптимизация и профилирование: CPU/GPU, batching, Addressables, память

    Оптимизация и профилирование: CPU/GPU, batching, Addressables, память

    Middle Unity developer отличается тем, что оптимизирует не по ощущениям, а по данным: вы умеете измерить проблему, локализовать источник и выбрать минимально рискованное решение. Эта статья соединяет темы курса:

  • из материалов про память и GC берём дисциплину: не создавать мусор в кадре, использовать пулы, понимать стоимость аллокаций
  • из темы про async/await берём правильную асинхронную загрузку контента и отмену операций по жизненному циклу
  • из тем про архитектуру и DI берём идею изоляции систем, чтобы их можно было профилировать и менять без каскадных правок
  • из Unity Core берём понимание сцен, префабов и жизненного цикла, потому что именно они часто становятся источником проблем в рантайме
  • Правильный порядок оптимизации

    Оптимизация почти всегда идёт в таком порядке:

  • Сначала измеряем: где узкое место, CPU или GPU, и в какой подсистеме.
  • Потом убираем лишнюю работу: меньше вызовов, меньше объектов, меньше пересчётов.
  • Только затем делаем локальные микрооптимизации кода.
  • Важное правило production: не оптимизируйте код, который не находится в top затрат по профайлеру.

    Официальные инструменты:

  • Unity Profiler
  • Профилирование производительности
  • Frame Debugger
  • Memory Profiler package
  • !Диаграмма помогает понять, где именно ограничение кадра: на CPU или на GPU

    CPU-профилирование: где тратится время на стороне процессора

    Что смотреть в Unity Profiler

    Основные модули, которые чаще всего нужны middle-разработчику:

  • CPU Usage: время на скрипты, физику, анимацию, рендер-подготовку, UI
  • Rendering: количество draw calls, batching, setpass calls
  • GC Alloc: где создаётся мусор
  • Timeline view: взаимосвязь потоков и “провалы” кадра
  • Начинайте с ответа на два вопроса:

  • Какая часть кадра самая дорогая: Scripts, Physics, Rendering, UI.
  • Это единичный пик или стабильная нагрузка.
  • Deep Profile: когда включать и почему осторожно

    Deep Profile показывает более подробный разрез по методам, но он сильно замедляет игру.

    Практика:

  • включайте Deep Profile только для локализации конкретного участка
  • не делайте выводы по FPS при включённом Deep Profile
  • Документация:

  • Deep Profiling
  • Свои маркеры профилирования

    Чтобы видеть стоимость именно вашей системы, добавляйте маркеры.

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

  • ProfilerMarker
  • Практический смысл:

  • если система модульная (по SOLID), вы легко ставите маркеры и находите “виновника”
  • если система монолитная, профилирование будет показывать “Scripts”, но не объяснит, что именно дорого
  • Типовые CPU-проблемы в Unity и их причины

  • Слишком много логики в Update на множестве объектов.
  • Частые GetComponent, FindObjectOfType, Find по сцене.
  • Генерация мусора: new в кадре, строки, LINQ, замыкания.
  • Instantiate/Destroy в бою вместо пулов.
  • UI пересчитывается каждый кадр из-за постоянных изменений текста, layout или активностей.
  • Связь с прошлой темой про память: если у вас растёт GC.Alloc, вы почти гарантированно получите микрофризы из-за GC.

    GPU-профилирование: когда упираемся в видеокарту

    CPU может работать быстро, но кадр всё равно будет долгим, если GPU перегружен.

    Признаки GPU-bound

  • Время кадра большое, но CPU-профиль выглядит “лёгким”.
  • Изменение разрешения сильно влияет на FPS.
  • Дорогие шейдеры, много прозрачности, много пост-эффектов.
  • Документация:

  • GPU Profiling
  • Frame Debugger: почему draw calls именно такие

    Frame Debugger позволяет пошагово увидеть, как Unity собирает кадр:

  • какие объекты рисуются
  • почему объект не батчится
  • сколько фактических draw calls
  • Документация:

  • Frame Debugger
  • Практическая польза:

  • вы перестаёте гадать, почему batching “не работает”
  • вы видите конкретные причины: другой материал, другой pass, другая топология, динамические данные
  • Типовые GPU-проблемы

  • Слишком много draw calls из-за материалов и отсутствия batching.
  • Много overdraw: крупные прозрачные UI или эффекты перекрывают экран.
  • Сложные шейдеры, слишком много источников света и теней.
  • Высокие разрешения текстур и дорогие пост-эффекты.
  • Batching: как уменьшать draw calls без магии

    Batching это объединение отрисовки объектов, чтобы сократить накладные расходы на подготовку и отправку команд в GPU.

    В Unity важны четыре подхода:

  • Static Batching
  • Dynamic Batching
  • SRP Batcher
  • GPU Instancing
  • Static Batching

    Работает для объектов, помеченных как static.

    Особенности:

  • хорошо для окружения, которое не двигается
  • увеличивает использование памяти, потому что Unity может создавать объединённые данные
  • Документация:

  • Static Batching
  • Dynamic Batching

    Пытается батчить маленькие движущиеся меши, но имеет ограничения и в современных проектах часто менее полезен, чем SRP Batcher и instancing.

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

  • Dynamic Batching
  • SRP Batcher

    Если вы используете URP или HDRP, SRP Batcher может радикально снизить CPU стоимость рендера, но требует совместимых шейдеров.

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

  • SRP Batcher
  • Практика:

  • проверяйте совместимость материалов и шейдеров
  • используйте Frame Debugger и модуль Rendering в профайлере
  • GPU Instancing

    Подходит, когда у вас много одинаковых мешей с одним материалом.

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

  • GPU Instancing
  • Практические правила:

  • одинаковый меш и материал важнее, чем “похожий”
  • различия в свойствах материала могут ломать инстансинг, если они не переданы как instanced properties
  • Что чаще всего ломает batching

    Batching ломается, если объекты начинают различаться с точки зрения рендера. Типовые причины:

  • разные материалы, даже если текстура одна
  • разные шейдерные варианты или ключевые слова
  • разные параметры материала, если вы создаёте уникальные material instances
  • разные меши
  • дополнительные passes, например тени или эффекты, которые ведут себя иначе
  • Практический вывод middle-уровня: борьба за batching это не “галочка”, а дисциплина работы с материалами и контентом.

    Addressables: управляемая загрузка контента и контроль памяти

    Addressables решает две задачи:

  • загрузка ассетов по адресу, а не через жёсткие ссылки
  • управление жизненным циклом загруженного контента
  • Документация:

  • Addressables
  • Ментальная модель Addressables

    Addressables завязаны на два ключевых понятия:

  • Handle это “дескриптор” операции загрузки
  • Release это уменьшение счётчика ссылок и разрешение выгрузки
  • Если вы загрузили ассет и не вызвали Addressables.Release, память может не освободиться.

    Асинхронная загрузка и жизненный цикл

    Это напрямую связано с прошлой статьёй про async/await и CancellationToken.

    С практической точки зрения:

  • загрузка должна быть асинхронной, чтобы не блокировать кадр
  • если объект ушёл со сцены или отключился, загрузку нужно отменять или игнорировать результат
  • Документация:

  • AsyncOperationHandle
  • Минимальный пример с Task и освобождением ресурсов:

    Практические замечания:

  • async void здесь выбран из-за Start, но в реальном коде лучше выносить логику в async Task и вызывать её из Start
  • освобождение handle в OnDestroy или OnDisable должно быть частью дисциплины жизненного цикла
  • Ошибки, которые приводят к утечкам памяти с Addressables

  • загрузили LoadAssetAsync, но не сделали Release
  • инстанциировали через Addressables, но не освобождаете инстансы правильным способом
  • держите ссылки на загруженные ассеты в “глобальных” сервисах без стратегии выгрузки
  • Документация по освобождению:

  • Addressables.Release
  • Память: managed, native, GC и реальные источники фризов

    Два мира памяти в Unity

  • Managed: объекты C# в куче, которые обслуживает GC
  • Native: ресурсы движка, такие как текстуры, меши, аудио, буферы
  • Документация:

  • Automatic Memory Management
  • Ключевой момент: вы можете убрать GC.Alloc, но всё равно упираться в память из-за текстур или мешей.

    !Картинка объясняет, почему GC и выгрузка ассетов это разные проблемы

    Как искать проблемы памяти

    Инструменты и куда смотреть:

  • Unity Profiler, модуль Memory: общая картина
  • Memory Profiler: снимки памяти, поиск удерживающих ссылок
  • Документация:

  • Memory Profiler package
  • Практика работы со снапшотами:

  • Снимите снапшот в “чистом” месте.
  • Сделайте действие, после которого подозреваете рост памяти.
  • Снимите второй снапшот.
  • Сравните и найдите типы и объекты, которые растут.
  • Основные источники GC.Alloc в кадре

  • создание объектов в Update
  • строки и логирование
  • LINQ в горячем коде
  • замыкания на лямбдах в часто вызываемых местах
  • Instantiate/Destroy без пулов
  • Это напрямую связано с предыдущими статьями: generics помогают сделать Pool<T>, а SRP помогает локализовать систему, которая “мусорит”.

    Пулы и жизненный цикл: оптимизация без потери архитектуры

    Пул это не “хак”, а архитектурный элемент.

    Практические правила:

  • пул должен быть сервисом или инфраструктурным компонентом, а не скрытым статиком в случайном классе
  • объект при возврате в пул должен быть приведён в корректное состояние
  • события и подписки должны быть сняты при деактивации объекта, иначе вы получите удержание ссылок и странное поведение
  • Связь с темой событий: подписывайтесь в OnEnable, отписывайтесь в OnDisable, особенно для объектов, которые переиспользуются пулом.

    Производительность контента: текстуры, меши, аудио

    Не вся оптимизация делается кодом. Часто вы выигрываете больше, настроив импорт и контент.

    Текстуры

    Оптимизация обычно включает:

  • правильный размер, без “4K везде”
  • сжатие под платформу
  • правильные mipmaps, если текстура уходит вдаль
  • Документация:

  • Texture Import Settings
  • Меши

    Типовые подходы:

  • объединять окружение, где это оправдано
  • уменьшать количество submeshes и материалов
  • следить за количеством вершин
  • Документация:

  • Model Import Settings
  • Аудио

    Правильные настройки импорта и формат сжатия часто важнее, чем оптимизация кода воспроизведения.

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

  • Audio Import Settings
  • Практический чеклист middle-уровня

  • Профилирование:
  • - находите top затрат, фиксируйте гипотезу, проверяйте результат - ставьте ProfilerMarker на ключевые системы
  • CPU:
  • - избегайте тяжёлой работы в Update на множестве объектов - избегайте Find и частого GetComponent
  • GPU:
  • - проверяйте draw calls и причины отсутствия batching через Frame Debugger - следите за overdraw и стоимостью прозрачности
  • Batching:
  • - дисциплина материалов важнее “магических настроек” - используйте SRP Batcher и GPU Instancing, если проект на URP/HDRP
  • Addressables:
  • - всегда проектируйте стратегию Release - загружайте асинхронно, учитывайте жизненный цикл
  • Память:
  • - убирайте регулярный GC.Alloc - используйте пулы для частых объектов - снимайте Memory Profiler snapshots и ищите удерживающие ссылки

    Итоги

  • Оптимизация в Unity начинается с профилирования: сначала определяем, CPU-bound или GPU-bound.
  • Batching это работа с контентом и материалами, а не только “галочки”.
  • Addressables дают управляемую загрузку, но требуют дисциплины: загрузили через handle, значит должны освобождать.
  • Память в Unity делится на managed и native, и проблемы могут быть в обоих мирах.
  • Архитектура из предыдущих статей помогает оптимизации: SRP упрощает локализацию проблем, DI и сервисы помогают управлять загрузкой и ресурсами, события требуют правильного жизненного цикла подписок.
  • 7. Продакшн-процессы: отладка, тестирование, Git, CI/CD, сборки и релиз

    Продакшн-процессы: отладка, тестирование, Git, CI/CD, сборки и релиз

    Middle Unity developer отличается тем, что умеет доставлять фичи стабильно: не только писать код, но и встраивать его в процесс команды так, чтобы изменения проверялись, собирались, выкатывались и откатывались предсказуемо.

    Эта статья связывает предыдущие темы курса:

  • из C# и SOLID берём тестируемость, слабую связанность и чистую логику вне MonoBehaviour
  • из событий и жизненного цикла берём управляемость подписок, предсказуемость инициализации и отсутствие утечек
  • из оптимизации и профилирования берём подход сначала измеряем, потом исправляем
  • из Addressables и памяти берём дисциплину жизненного цикла контента и релизных артефактов
  • !Общая карта того, как код проходит путь от коммита до релиза и мониторинга

    Зачем Unity-проекту продакшн-процессы

    Продакшн-процесс закрывает типовые боли Unity-разработки:

  • сцены и префабы меняются часто, конфликты в Git неизбежны
  • баги могут проявляться только в Player, на конкретном устройстве или в релизной конфигурации
  • производительность и память зависят не только от кода, но и от контента и настроек импорта
  • сборка занимает время, поэтому ручные проверки плохо масштабируются
  • Практическая цель: сделать так, чтобы любой разработчик мог получить одинаковый результат при одинаковом состоянии репозитория.

    Отладка в Unity

    Отладка в Unity обычно делится на три уровня:

  • быстрые проверки через Console и логи
  • точечный анализ через Profiler и инструменты профилирования
  • пошаговая отладка в IDE и диагностика Player
  • Логи и Console как первый рубеж

    Unity Console показывает сообщения логов, предупреждения и исключения.

    Полезные правила:

  • лог должен помогать найти причину, а не просто сообщать, что что-то сломалось
  • не логируйте в каждом кадре и в циклах на больших коллекциях, иначе получите шум и просадки
  • исключение в Console это сигнал об ошибке, а не ожидаемая ветка логики
  • Основные инструменты:

  • Debug.Log, Debug.LogWarning, Debug.LogError
  • Debug.LogException для правильного вывода исключений
  • Документация:

  • Unity Scripting API: Debug
  • Unity Manual: Console Window
  • Чтобы логировать безопаснее, используйте условную компиляцию для отладочных сообщений.

    Смысл:

  • в Editor и Development Build лог включён
  • в релизной сборке вызовы будут вырезаны компилятором
  • Assertions как быстрый контракт

    В реальных проектах полезны утверждения, которые фиксируют невозможные состояния на ранней стадии.

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

  • Unity Scripting API: Debug.Assert
  • Профайлер и маркеры для вашей системы

    Профилирование это часть отладки, потому что проблемы производительности и памяти часто проявляются как баги.

    Практические шаги:

  • Найдите, где проблема: CPU, GPU, GC, UI.
  • Локализуйте: какая система создаёт нагрузку.
  • Поставьте маркеры, чтобы видеть стоимость своей логики отдельно.
  • Документация:

  • Unity Manual: Profiler
  • Unity Scripting API: ProfilerMarker
  • Пошаговая отладка: Editor и Player

    Пошаговая отладка нужна, когда логов недостаточно.

    Практическая дисциплина:

  • Для Editor почти всегда достаточно attach от IDE к Unity.
  • Для устройства или отдельного Player используйте Development Build.
  • Включайте Script Debugging только когда нужно, потому что это влияет на скорость и размер сборки.
  • Документация:

  • Unity Manual: Build Settings
  • Unity Manual: Managed code debugging in the Player
  • Логи Player и диагностика вне Editor

    Некоторые баги проявляются только в Player.

    Практика:

  • убедитесь, что вы умеете найти файл лога приложения
  • разделяйте проблемы логики, контента и окружения (устройство, драйвер, ОС)
  • Документация:

  • Unity Manual: Log Files
  • Тестирование в Unity

    Тесты в Unity обычно делятся на два класса:

  • EditMode tests выполняются в редакторе, быстрее, подходят для чистой логики
  • PlayMode tests запускаются в рантайме, подходят для проверки поведения с MonoBehaviour, сценами и корутинами
  • Документация:

  • Unity Manual: Unity Test Framework
  • Почему архитектура из прошлых статей напрямую влияет на тесты

    Если логика живёт в обычных C#-классах и зависимости передаются через интерфейсы, то:

  • юнит-тесты пишутся без сцены
  • ошибки находятся быстрее
  • уменьшается число PlayMode тестов, которые дороже и медленнее
  • Это прямое продолжение подходов из тем про SRP, DIP, DI и MVC.

    Пример простого юнит-теста для чистой модели

    Ниже пример теста на NUnit, который обычно используется в Unity Test Framework.

    Что именно тестировать в Unity-проекте

    Чтобы тесты реально помогали, они должны покрывать самое рискованное:

  • правила игры и экономики: урон, награды, кулдауны, прогресс
  • сериализацию и миграции данных сохранений
  • критичные пайплайны: создание предметов, спавн, загрузка конфигов
  • интеграции: загрузка Addressables, инициализация сервисов, переходы сцен
  • Частая ошибка: тестировать то, что и так стабильно, и не тестировать то, что ломается чаще всего.

    Стабильность тестов: детерминизм

    Тесты должны быть повторяемыми.

    Типовые источники недетерминизма:

  • время (Time.time, Time.deltaTime)
  • случайность (UnityEngine.Random)
  • порядок выполнения инициализации объектов
  • Решение на уровне архитектуры:

  • вводить интерфейсы источника времени и случайности
  • передавать их через DI
  • в тестах подставлять фиксированные реализации
  • Git для Unity-проекта

    Git в Unity требует нескольких обязательных настроек, иначе вы быстро получите конфликтные сцены, пропавшие ссылки и гигабайты бинарных файлов.

    Что обязательно включить в Unity

  • Включите видимые .meta файлы.
  • Включите текстовую сериализацию ассетов.
  • Это делается в настройках редактора.

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

  • Unity Manual: Asset Serialization
  • Базовые правила репозитория

    Практические правила:

  • коммитите папки Assets, Packages, ProjectSettings
  • не коммитите Library, Temp, Obj и прочие локальные артефакты
  • фиксируйте зависимости через Packages/manifest.json и Packages/packages-lock.json
  • Рекомендуемый .gitignore:

  • GitHub: Unity.gitignore
  • Git LFS для больших бинарных файлов

    В Unity много тяжёлых бинарных ассетов: текстуры, модели, аудио.

    Если хранить их в обычном Git:

  • репозиторий быстро разрастётся
  • операции clone и fetch станут дорогими
  • Решение: Git LFS.

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

  • Git LFS
  • Разрешение конфликтов сцен и префабов

    Сцены и префабы в текстовой сериализации это YAML, но конфликт всё равно может быть сложным.

    Unity предоставляет инструмент для мержа YAML ассетов.

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

  • Unity Manual: Smart Merge
  • Практические правила командной работы:

  • Старайтесь уменьшать размер изменений в сценах.
  • Разносите изменения: один PR на одну задачу.
  • Выносите данные в Prefab и ScriptableObject, чтобы не править одну сцену десятью разработчиками.
  • CI/CD для Unity

    CI/CD это автоматизация проверки и сборки проекта на сервере.

    Цели CI:

  • запуск тестов на каждый pull request
  • сборка артефактов предсказуемо, в чистом окружении
  • быстрый фидбек: сломал сборку, узнал сразу
  • Запуск Unity в batchmode

    Unity умеет запускаться без UI для сборки и тестов.

    Ключевые аргументы:

  • -batchmode
  • -quit
  • -projectPath
  • -executeMethod для вызова вашего метода сборки
  • Документация:

  • Unity Manual: Command line arguments
  • Рекомендуемая структура пайплайна

    Ниже минимальная структура CI пайплайна, которая обычно ожидается на уровне middle.

  • Checkout репозитория.
  • Восстановление и кэширование зависимостей.
  • Запуск EditMode тестов.
  • Запуск PlayMode тестов.
  • Сборка Player.
  • Сборка Addressables, если проект использует их.
  • Публикация артефактов: билд, логи, отчёт тестов, символы.
  • Документация по Addressables в контексте билда:

  • Unity Manual: Addressables
  • Скрипт сборки как код

    Сборку лучше описывать кодом, а не ручными кликами в Build Settings.

    Ключевой API:

  • BuildPipeline.BuildPlayer
  • Документация:

  • Unity Scripting API: BuildPipeline.BuildPlayer
  • Практический смысл:

  • сборка становится повторяемой
  • вы можете версионировать изменения сборочного пайплайна
  • CI использует тот же путь, что и локальная сборка
  • Сборки и релиз

    Конфигурации сборки

    Обычно выделяют две конфигурации:

  • Development Build для диагностики и профилирования
  • Release Build для выкладки пользователям
  • Документация:

  • Unity Manual: Development Build
  • Важно:

  • не включайте отладочные флаги в релиз по умолчанию
  • настройте Scripting Define Symbols, чтобы явно разделять код dev и release
  • Документация:

  • Unity Manual: Platform dependent compilation
  • Версионирование и теги

    Хорошая практика: версионировать релизы по семантическому версионированию.

  • MAJOR увеличивается при несовместимых изменениях
  • MINOR увеличивается при добавлении функциональности без ломания совместимости
  • PATCH увеличивается при исправлениях багов
  • Спецификация:

  • Semantic Versioning
  • Практика для Unity:

  • релиз соответствует Git tag
  • к релизу прикладывается changelog
  • для хотфиксов используйте отдельную ветку, чтобы не блокировать разработку новых фич
  • Релизный чеклист

    Ниже чеклист, который помогает делать релизы повторяемыми.

  • Обновлены версии и changelog.
  • Прогнаны тесты локально и в CI.
  • Сделана чистая сборка в CI.
  • Проверены ключевые сценарии вручную.
  • Подготовлены артефакты диагностики: логи, символы, отчёты.
  • Поставлен tag, собран релиз из tag.
  • Настроен сбор обратной связи: краши, ошибки, метрики.
  • Пострелизная дисциплина

    Релиз это не финал, а переход к мониторингу.

    Практические правила:

  • фиксируйте баги как воспроизводимые сценарии и добавляйте тесты на регресс
  • держите возможность отката: понятные теги, сохранённые артефакты и предсказуемая сборка
  • не лечите симптом логами, лечите причину архитектурно, используя принципы из прошлых статей
  • Итоги

  • Отладка в Unity это логи, assertions, профайлер и пошаговая диагностика в Player.
  • Тестирование становится дешевле и эффективнее, если логика вынесена в чистые C#-классы и зависимости внедряются через абстракции.
  • Git для Unity требует текстовой сериализации и корректной работы с .meta, а крупные ассеты обычно уходят в Git LFS.
  • CI/CD делает сборки и тесты повторяемыми, а сборку стоит описывать кодом через BuildPipeline.
  • Релиз это процесс: версии, теги, артефакты, мониторинг и регресс-тесты на найденные проблемы.