Unreal Engine 5 C++: углублённая разработка игрового кода

Углублённый курс по написанию C++-кода в Unreal Engine 5: от устройства фреймворка и жизненного цикла объектов до производительности, сетевого кода и расширения редактора. Фокус на практических паттернах, архитектуре модулей и создании масштабируемых игровых систем.

1. Архитектура UE5 C++: модули, сборка, UHT и макросы отражения

Архитектура UE5 C++: модули, сборка, UHT и макросы отражения

UE5 — это не просто C++ проект с библиотеками, а целая экосистема со своей системой сборки, генерацией кода и отражением (reflection). Если воспринимать её как обычный CMake/Visual Studio проект, вы быстро упрётесь в загадочные ошибки линковки, невозможность подключить типы из соседнего модуля или «почему UPROPERTY не работает».

В этой статье разберём:

  • Что такое модули UE и почему они важнее, чем «папки с кодом»
  • Как устроены сборка через UBT и генерация кода через UHT
  • Как работает система отражения и почему макросы UCLASS/UPROPERTY — это не «магия ради магии»
  • Типовые правила и ошибки, которые экономят часы отладки
  • Ментальная модель: что именно вы компилируете в UE

    В UE проект обычно состоит из нескольких модулей:

  • Модули игры (Game/Runtime)
  • Модули редактора (Editor)
  • Плагины (каждый плагин может содержать один или несколько модулей)
  • Модули движка (Engine)
  • Модуль в UE — это единица компоновки и зависимостей: он превращается в отдельную библиотеку (или часть общей сборки в зависимости от режима), имеет явные зависимости и правила сборки.

    Официальная точка входа в тему модулей: Unreal Engine Modules.

    !Схема зависимости модулей в типичном UE-проекте

    Unreal Build Tool: кто реально управляет сборкой

    Unreal Build Tool (UBT) — это система сборки UE, которая:

  • Сканирует .Build.cs и .Target.cs
  • Запускает Unreal Header Tool (UHT) для генерации кода отражения
  • Запускает компиляцию C++ (MSVC/Clang) с нужными флагами
  • Линкует модули и формирует конечные артефакты
  • Документация: Unreal Build System.

    Важные файлы сборки

  • ProjectName.uproject — описание проекта и его модулей/плагинов
  • Source/<ModuleName>/<ModuleName>.Build.cs — правила сборки модуля
  • Source/<ProjectName>.Target.cs — правила сборки таргета (например, Game)
  • Source/<ProjectName>Editor.Target.cs — правила сборки таргета редактора
  • Что такое Target и почему их обычно минимум два

    Target — это конфигурация сборки конечного приложения.

    Чаще всего у проекта есть:

  • ProjectName (Game Target) — для запуска игры
  • ProjectNameEditor (Editor Target) — для работы в редакторе
  • У них разные зависимости и разные наборы модулей: редактору нужны Editor-модули, а packaged-игре — нет.

    Unreal Header Tool: зачем нужен отдельный генератор кода

    UE использует собственную систему отражения (reflection), чтобы:

  • Видеть типы и свойства в редакторе
  • Поддерживать сериализацию/загрузку ассетов
  • Делать репликацию по сети
  • Вызывать функции через систему UObject (включая Blueprint)
  • Для этого движку нужно знать метаданные о ваших классах, структурах, свойствах и функциях. C++ сам по себе не даёт UE безопасно и переносимо извлечь эти метаданные на этапе компиляции, поэтому используется UHT — отдельный инструмент, который парсит заголовки и генерирует вспомогательный C++ код.

    Документация: Unreal Header Tool.

    Общий пайплайн: UBT → UHT → компилятор

  • UBT собирает список модулей и исходников по Build.cs/Target.cs.
  • UBT запускает UHT для всех заголовков, где используются макросы отражения.
  • UHT генерирует файлы в Intermediate/Build/... и создаёт *.generated.h.
  • Затем обычный компилятор C++ компилирует уже ваш код + сгенерированный код.
  • !Процесс сборки: как UBT и UHT участвуют до компилятора C++

    Ключевое правило: *.generated.h подключается последним

    Если в заголовке есть UCLASS/USTRUCT/UENUM, то почти всегда должен быть:

  • #include "YourType.generated.h" и он должен идти последним include в файле
  • Это требование связано с тем, как UHT вставляет и ожидает увидеть определённые объявления/макросы.

    Система отражения: что она даёт и что требует

    Отражение в UE — это механизм, позволяющий движку в рантайме знать структуру ваших типов, их свойства и функции.

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

    Главные макросы отражения

  • UCLASS(...) — класс UObject/Actor, видимый системе отражения
  • USTRUCT(...) — структура, видимая отражению (например, для DataTable)
  • UENUM(...) — enum, видимый отражению (например, для Blueprint)
  • UPROPERTY(...) — свойство, видимое отражению (редактор, сериализация, репликация)
  • UFUNCTION(...) — функция, видимая отражению (Blueprint, RPC, события)
  • GENERATED_BODY() — точка вставки кода, сгенерированного UHT
  • Минимальный пример класса, который понимает UHT

    Что здесь важно:

  • UCLASS() делает класс участником системы UObject
  • MYGAME_API управляет экспортом символов модуля (об этом ниже)
  • GENERATED_BODY() обязателен, иначе UHT не сможет подставить нужные объявления
  • UPROPERTY/UFUNCTION добавляют метаданные для редактора/Blueprint/сериализации
  • Что не делает отражение автоматически

  • Не превращает ваш C++ в «скриптовый язык»
  • Не освобождает от понимания времени жизни объектов и владения
  • Не заменяет модульные зависимости: тип из другого модуля всё равно требует корректного dependency в Build.cs
  • Модули UE: границы кода, зависимости и API

    Модуль задаёт:

  • Какие include и зависимости разрешены
  • Что считается публичным API (для других модулей)
  • Какие библиотеки/define/флаги компиляции применяются
  • Папки Public и Private

    Типичная структура модуля:

  • Source/MyModule/Public — заголовки, которые разрешено включать другим модулям
  • Source/MyModule/Private — реализация и приватные заголовки
  • Практический смысл:

  • Если другой модуль включает ваш заголовок, он должен находиться в Public
  • Всё, что не является частью внешнего контракта, лучше держать в Private
  • Module API macro: MYMODULE_API

    MYMODULE_API (например MYGAME_API) — это макрос экспорта/импорта символов при сборке модулей.

  • Когда модуль собирается, классы/функции с MYMODULE_API экспортируются
  • Когда другой модуль использует этот модуль, те же символы импортируются
  • Именно поэтому публичные классы, которыми пользуются другие модули, должны иметь ..._API.

    Типы модулей: Runtime и Editor

    Самое частое разделение:

  • Runtime-модуль — код, который может существовать в packaged-игре
  • Editor-модуль — код, который живёт только в редакторе (детали, кастомизации, тулзы)
  • Правило:

  • Не тяните Editor-зависимости в Runtime-модуль
  • Иначе упрётесь в невозможность собрать/упаковать игру.

    Файл *.Build.cs: как правильно описывать зависимости

    <ModuleName>.Build.cs — C#-класс, который описывает правила сборки конкретного модуля.

    Ключевая идея:

  • Зависимости должны соответствовать тому, что вы используете в заголовках и cpp
  • Минимальный пример:

    PublicDependencyModuleNames vs PrivateDependencyModuleNames

    Смысл не в «мне так удобнее», а в том, куда просачиваются include и типы:

  • PublicDependencyModuleNames — если типы/заголовки зависимого модуля используются в ваших публичных заголовках
  • PrivateDependencyModuleNames — если зависимость нужна только для .cpp или приватных заголовков
  • Практическое правило:

  • Если вы добавили include чужого модуля в файл из Public, почти всегда нужна PublicDependencyModuleNames
  • Частая ошибка: «у меня компилируется локально, но ломается у коллеги/на CI»

    Причины обычно такие:

  • Случайно подтянули заголовок через другой include (порядок include/Unity build)
  • Не добавили зависимость в Build.cs, и сборка прошла лишь из-за косвенных зависимостей
  • Лечение:

  • Явно указывать зависимости
  • Держать публичные заголовки минимальными
  • Использовать forward declarations там, где можно
  • Target.cs: как включать/выключать возможности на уровне приложения

    *.Target.cs управляет настройками таргета: тип (Game/Editor/Server), некоторые флаги сборки, включение модулей и т.д.

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

    Документация по смежной теме (сборочная система в целом): Unreal Build System.

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

    Сценарий: вы хотите вынести часть кода (например, боевую систему) в отдельный Runtime-модуль Combat.

    Шаги на уровне структуры

  • Создайте папку Source/Combat.
  • Добавьте Combat.Build.cs.
  • Добавьте Public/ и Private/.
  • Добавьте файл модуля Private/CombatModule.cpp с реализацией.
  • Пример CombatModule.cpp:

    Подключение из игрового модуля

    Если в MyGame вы хотите использовать публичные заголовки Combat, добавьте зависимость:

    И убедитесь, что классы, которые вы используете снаружи, помечены COMBAT_API.

    Live Coding, Hot Reload и почему архитектура модулей важна для итерации

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

  • Live Coding ориентирован на более предсказуемые инкрементальные изменения
  • Полная пересборка гарантирует, что UHT/генерация/линковка согласованы
  • Когда вы активно работаете с отражением (добавляете UPROPERTY, меняете сигнатуры UFUNCTION), чаще требуется более «чистая» пересборка, потому что меняется сгенерированный код.

    Практика отладки сборки:

  • Если поймали странные ошибки, попробуйте очистить Binaries/ и Intermediate/
  • Смотрите логи UBT/UHT: там почти всегда есть первопричина, даже если ошибка всплыла как «100500 external symbol»
  • Типовые ошибки и как их избегать

  • #include "X.generated.h" не последний include в заголовке
  • Нет GENERATED_BODY() внутри UCLASS/USTRUCT
  • Публичный заголовок включает тяжёлые зависимости вместо forward declaration
  • Тип из другого модуля используется в Public-заголовке, но зависимость указана как PrivateDependencyModuleNames
  • Runtime-модуль зависит от Editor-модулей (ломает упаковку)
  • Публичные классы не помечены ..._API, из-за чего появляются ошибки линковки при использовании в другом модуле
  • Итоги

  • UE строит код вокруг модулей с явными границами и зависимостями
  • UBT управляет сборкой, а UHT генерирует код для отражения до компиляции C++
  • Макросы UCLASS/UPROPERTY/UFUNCTION — контракт с UHT и системой UObject
  • Правильное разделение Public/Private и корректные зависимости в Build.cs — основа масштабируемой кодовой базы
  • Дальше по курсу логично углубляться в то, как UObject-жизненный цикл, GC и подсистема World/Actor/Component опираются на отражение и модульную архитектуру.

    2. UObject и память: жизненный цикл, GC, владение и ссылки

    UObject и память: жизненный цикл, GC, владение и ссылки

    UE5 C++ отличается от “обычного C++” не синтаксисом, а моделью времени жизни объектов. В предыдущей статье мы разобрали, что UHT и макросы отражения генерируют код и метаданные. Здесь важное продолжение: именно отражение делает возможными сериализацию, редакторные свойства и сборку мусора (GC). Если не понимать, кто владеет UObject, какие ссылки видит GC и когда объект реально уничтожается, вы неизбежно получите:

  • “случайные” краши при доступе к уже уничтоженным объектам
  • утечки (объект “вечно живёт”, потому что где-то осталась сильная ссылка)
  • странное поведение при загрузке/выгрузке уровней и ассетов
  • Базовая ментальная модель UObject

    UObject — базовый тип огромной части движка. Он живёт под контролем UE:

  • создаётся через фабрики UE (NewObject, CreateDefaultSubobject, SpawnActor для акторов)
  • хранится в глобальном реестре объектов
  • уничтожается не через delete, а через GC и внутренние стадии разрушения
  • Ключевая идея: UE не может безопасно собирать мусор, если не видит ваши ссылки. Поэтому “правильные” ссылки — это не стиль, а корректность.

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

  • UObject
  • Garbage Collection
  • !Диаграмма того, как GC видит граф ссылок и что реально удерживает объекты в памяти

    Создание объектов: почему нельзя new и что такое Outer

    NewObject и Outer

    Большинство UObject создают так:

    Outer — это владелец в смысле иерархии UObject, часть “пути” объекта и важная составляющая жизненного цикла:

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

  • если объект логически принадлежит актору или компоненту, обычно Outer — это этот актор/компонент
  • если объект должен пережить смену уровня, Outer часто делают UGameInstance (или другой долгоживущий объект), но это нужно делать осознанно
  • CreateDefaultSubobject и под-объекты

    Под-объекты (например, компоненты) в конструкторе обычно создаются через CreateDefaultSubobject:

    Это не просто “удобная фабрика”: движок помечает такие объекты как default subobject, корректно связывает их с CDO и инстансами, и правильно учитывает в сериализации/дублировании.

    SpawnActor для акторов

    Акторы создаются иначе:

    Их жизненный цикл сильно завязан на UWorld и уровень. Важно помнить: акторы тоже UObject, но с дополнительными стадиями жизни (инициализация компонентов, BeginPlay/EndPlay и т.д.).

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

  • Spawning Actors
  • Жизненный цикл UObject: ключевые стадии

    На практике чаще всего важны не все виртуальные методы, а понимание когда объект уже “валиден” и когда он уже “умирает”.

    Типичный жизненный путь

  • Конструирование (C++ конструктор)
  • PostInitProperties (объект уже имеет базовые свойства)
  • Для ассетов/загружаемых объектов: PostLoad
  • Работа в игре
  • Начало уничтожения: BeginDestroy
  • Финализация: FinishDestroy
  • Память освобождается движком
  • Практические ориентиры:

  • конструктор — место для создания default subobject и выставления дефолтов, но не для логики, зависящей от мира
  • PostLoad — место, где объект уже загружен из ассета/пакета и можно чинить совместимость версий
  • BeginDestroy — объект уже в процессе уничтожения, нельзя полагаться на внешние ссылки
  • Документация (сводная по событиям объекта):

  • UObject Lifecycle
  • !Шпаргалка по стадиям жизни UObject и Actor

    Сборка мусора (GC): как UE решает, что объект можно уничтожить

    UE использует mark-and-sweep GC на графе ссылок:

  • сначала выбираются “корни” (Root Set) — объекты, которые считаются живыми по определению
  • далее движок проходит по ссылкам от корней и помечает достижимые объекты
  • всё, что недостижимо, считается мусором и будет уничтожено
  • Важнейший вывод: объект жив, пока достижим из корней по ссылкам, которые GC умеет видеть.

    Что входит в Root Set

    Типичные источники корней:

  • долгоживущие подсистемы и глобальные объекты (например, UGameInstance, часть объектов UWorld)
  • загруженные пакеты/уровни и объекты, “прикреплённые” к ним
  • объекты, принудительно “рутанутые” через AddToRoot
  • Почему GC “не видит” обычные C++ ссылки

    GC сканирует ссылки, которые описаны системе отражения и сериализации. Поэтому:

  • “голый” UObject в классе без UPROPERTY может быть невидим для GC*
  • UObject внутри обычной STL-структуры тоже может быть невидим
  • Из-за этого объект может быть собран, хотя ваш C++ указатель всё ещё на него указывает.

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

  • Unreal Object Pointers
  • Владение и удержание в памяти: что реально продлевает жизнь объекта

    Есть несколько рабочих способов гарантировать, что объект не будет собран.

    Сильная ссылка через UPROPERTY

    Самый частый и правильный способ: хранить ссылку как поле UCLASS/USTRUCT, помеченное UPROPERTY.

    Важно:

  • UPROPERTY() без дополнительных спецификаторов всё равно делает ссылку видимой GC
  • для контейнеров UE используйте UE-контейнеры (например, TArray) и тоже помечайте UPROPERTY
  • AddToRoot: грубая “неубиваемость”

    AddToRoot добавляет объект в Root Set и делает его фактически бессмертным, пока не вызвать RemoveFromRoot.

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

    Если вам нужно хранить ссылки на UObject в структуре, которую UHT не обрабатывает (например, чистый C++ менеджер без наследования от UObject), используйте FGCObject и вручную сообщайте GC о ссылках.

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

  • Garbage Collection
  • Типы ссылок на UObject: hard, weak, soft

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

    TObjectPtr и обычный UObject*

    В UE5 в кодовой базе часто используется TObjectPtr<T> как поле UPROPERTY. Это оптимизация внутреннего представления указателей для движка и инструментов.

    Практика:

  • для полей UCLASS/USTRUCT обычно используйте UPROPERTY() + TObjectPtr<T> (или UObject*, если ваш проект ещё на старом стиле)
  • не делайте “сырые” поля без UPROPERTY, если они влияют на время жизни
  • TWeakObjectPtr: слабая ссылка без удержания

    TWeakObjectPtr<T> полезен, когда вы хотите ссылаться на объект, но не продлевать его жизнь.

  • GC может уничтожить объект
  • weak-ссылка после уничтожения станет невалидной
  • Пример:

    TSoftObjectPtr: ссылка по пути (ленивая загрузка)

    TSoftObjectPtr<T> хранит мягкую ссылку на ассет или объект по пути. Такая ссылка:

  • не удерживает объект в памяти
  • может указывать на не загруженный ассет
  • позволяет загрузить ассет позже (обычно через UAssetManager или FStreamableManager)
  • Это основной инструмент, чтобы не тащить в память тяжёлые ассеты просто потому, что на них есть ссылка.

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

  • Asynchronous Asset Loading
  • Проверка валидности и “умирающие” объекты

    IsValid и Pending Kill

    Для UObject/Actor в UE существует состояние “объект ещё существует, но уже уничтожается”. Поэтому проверки вида Ptr != nullptr часто недостаточно.

    Используйте:

  • IsValid(SomeUObject)
  • для weak-ссылок: TWeakObjectPtr::IsValid()
  • Это помогает избежать доступа к объектам, которые уже помечены на уничтожение.

    Почему “крашится не сразу”

    GC работает не постоянно в каждый кадр. Объект может стать недостижимым, но быть собран позже. Поэтому баги времени жизни часто проявляются “случайно” и зависят от момента сборки мусора.

    UObject и “обычные” умные указатели C++: где граница

    UE имеет два параллельных мира управления памятью:

  • UObject управляются GC
  • не-UObject сущности часто живут на TUniquePtr, TSharedPtr, TSharedRef
  • Документация:

  • Unreal Smart Pointers
  • Правила совместного использования:

  • не храните UObject внутри TSharedPtr как способ владения: GC об этом не узнает
  • хранить TSharedPtr внутри UObject можно, но это не делает UObject “живее”
  • если вам нужна слабая связь на UObject — используйте TWeakObjectPtr, а не TWeakPtr
  • Практические сценарии и типовые ошибки

    Ошибка: сырой указатель на UObject без UPROPERTY

    Симптом:

  • объект “иногда” становится nullptr или вызывает краш при обращении
  • Причина:

  • GC собрал объект, потому что не видел ссылку
  • Исправление:

  • сделать поле UPROPERTY()
  • или перейти на TWeakObjectPtr, если удерживать объект не нужно
  • Ошибка: Runtime-модуль держит Editor-only объекты

    Это связано с предыдущей темой модулей: если вы удерживаете Editor-only UObject (или тянете Editor-модуль), packaged-сборка сломается. В терминах памяти это часто выглядит как “в редакторе работает, в игре не собирается”.

    Исправление:

  • вынести редакторные типы в Editor-модуль
  • не хранить editor-типы в runtime-объектах
  • Ошибка: AddToRoot без RemoveFromRoot

    Симптом:

  • утечки памяти и объектов между уровнями
  • Исправление:

  • использовать UPROPERTY/корректный Outer вместо рута
  • если root необходим, обеспечить симметричное снятие (например, в ShutdownModule, Deinitialize, EndPlay)
  • Отладка GC и ссылок: что делать, когда “что-то живёт” или “что-то умирает”

    Полезные направления диагностики:

  • посмотреть, кто удерживает объект (цепочки референсов)
  • принудительно запустить GC и проверить, что происходит
  • В редакторе доступны консольные команды семейства obj и gc (их удобно использовать для расследований на месте). Также полезно профилировать память через инструменты stat.

    Если у вас повторяемый кейс, хороший подход:

  • Воспроизвести проблему.
  • Принудительно вызвать сборку мусора.
  • Проверить, кто держит объект, который “не должен жить”, или почему объект исчезает.
  • Итоги

  • UObject живёт по правилам UE: создание через фабрики, уничтожение через GC
  • Время жизни определяется достижимостью из Root Set по ссылкам, которые GC видит
  • Главный инструмент удержания: UPROPERTY на полях (включая TArray, TMap на UObject)
  • Для ссылок без удержания используйте TWeakObjectPtr, для ленивых ассет-ссылок — TSoftObjectPtr
  • Outer — важная часть владения и жизненного цикла, а AddToRoot — крайняя мера
  • Следующий логичный шаг после этой темы — разбор того, как UWorld, AActor и UActorComponent организуют время жизни, репликацию и обновление (Tick) поверх этой модели памяти.

    3. Gameplay Framework глубоко: Actor, Component, Pawn, Controller, GameMode, Subsystems

    Gameplay Framework глубоко: Actor, Component, Pawn, Controller, GameMode, Subsystems

    UE5 Gameplay Framework — это не «набор базовых классов», а контракт между вашим кодом и движком: кто и где живёт, кто создаётся автоматически, кто существует только на сервере, где хранить состояние, как подключать функциональность без наследования и как переживать смену уровней.

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

  • Из статьи про сборку, UHT и отражение вы уже знаете, почему UCLASS, UPROPERTY, UFUNCTION — это требования инфраструктуры движка, а не «магия». Это напрямую влияет на то, как корректно объявлять компоненты, ссылки и события во всех классах framework.
  • Из статьи про UObject и память вы уже знаете, что время жизни определяется GC, достижимостью и тем, видит ли GC ваши ссылки. Gameplay Framework построен так, чтобы «правильные» ссылки и владение были типовыми по умолчанию (например, компоненты как default subobject и UPROPERTY).
  • Официальная точка входа: Gameplay Framework.

    !Карта того, кто с кем связан и где живёт

    Ключевая ментальная модель: кто за что отвечает

    В UE полезно мыслить не классами, а ролями:

  • AActor — сущность в мире: трансформ, репликация, участие в уровне, возможность иметь компоненты.
  • UActorComponent — модуль поведения/данных, который «вешается» на актор.
  • APawn — актор, которым можно управлять (обычно перемещать), и которого можно possess.
  • AController — «мозг» управления Pawn: ввод игрока или AI, принятие решений.
  • AGameModeBase / AGameMode — правила матча: кто и как спавнится, какие классы используются, логика победы.
  • Subsystems — долгоживущие сервисы на разных уровнях времени жизни (Engine, GameInstance, World, LocalPlayer) без необходимости создавать акторы.
  • Практическая цель такой декомпозиции: вы уменьшаете количество «божественных» классов, упрощаете тестирование, снижаете связанность, и автоматически вписываетесь в жизненный цикл UE (инициализация, загрузка уровня, сеть, GC).

    Actor: объект мира, его жизненный цикл и точки расширения

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

    Что делает Actor «особенным»

  • Actor принадлежит UWorld и уровню (влияет на загрузку/выгрузку).
  • Actor может реплицироваться и иметь сетевую роль.
  • Actor имеет набор событий жизненного цикла (инициализация компонентов, BeginPlay, EndPlay).
  • Actor естественно агрегирует компоненты и является «точкой сборки» геймплейной сущности.
  • Жизненный цикл Actor: что куда класть

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

    Главные практические правила:

  • Конструктор (AMyActor::AMyActor) работает на CDO и на инстансах, поэтому это место для создания default subobject, установки дефолтов и минимальной конфигурации. Не полагайтесь на GetWorld().
  • OnConstruction вызывается, когда актор сконструирован или изменён в редакторе; удобно для перестройки визуала по параметрам.
  • BeginPlay — безопасная точка для логики, которая зависит от мира, других акторов, подсистем.
  • Tick — только если действительно нужно, и с осознанным PrimaryActorTick.bCanEverTick.
  • EndPlay — место для отписки от делегатов и остановки фоновых процессов.
  • Минимальный шаблон актора:

    Время жизни и GC: актор тоже UObject

    Из предыдущей статьи важно перенести сюда три правила:

  • Ссылки на другие UObject внутри актора должны быть видимы GC: обычно это UPROPERTY().
  • Компоненты как default subobject автоматически правильно связаны с актором и учитываются движком.
  • Для ссылок «не удерживать, но безопасно проверять» используйте TWeakObjectPtr.
  • Component: композиция вместо наследования

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

    Два типа компонентов, которые важно не путать

  • UActorComponent — логика и данные без трансформа.
  • USceneComponent — логика + трансформ + иерархия (attachment).
  • Практически:

  • Если вам не нужно быть частью сцены — начинайте с UActorComponent.
  • Если нужен сокет/точка крепления/положение — USceneComponent.
  • Default subobject: почему это базовый паттерн

    Компоненты, созданные через CreateDefaultSubobject в конструкторе, становятся частью шаблона класса (CDO) и корректно инстансируются для каждого актора:

    Почему здесь важны UPROPERTY и TObjectPtr:

  • Поле становится видимым для GC и сериализации.
  • Редактор и движок понимают компонент как часть объекта.
  • Когда компонент, а когда отдельный Actor

    Используйте компонент, если:

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

  • объект должен существовать как самостоятельная сущность уровня (спавн/уничтожение независимо);
  • у сущности есть собственная репликация и сетевой контракт;
  • объект должен иметь отдельную коллизию/трансформ-иерархию, не привязанную к другому актору.
  • Pawn и Controller: разделение тела и мозга

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

    Что такое Possession

    AController может possess APawn. С этого момента:

  • Controller становится источником управления;
  • у Pawn появляется связь GetController();
  • для PlayerController настраиваются ввод и камера (в зависимости от архитектуры).
  • Это позволяет:

  • отделить ввод игрока и сетевую идентичность от физического тела;
  • «пересаживать» игрока в разные Pawn;
  • иметь одинаковую Pawn-логику, но разные мозги (player vs AI).
  • PlayerController и AIController

  • APlayerController живёт для каждого игрока и связан с вводом и UI.
  • AAIController управляет Pawn от имени AI.
  • Практическое правило состояния:

  • В Pawn храните то, что относится к телу: движение, стамина, оружие как компонент, анимационные состояния.
  • В Controller храните то, что относится к принятию решений: обработка ввода, high-level команды, выбор целей.
  • Минимальная связка Character + PlayerController

    Важно: в реальном проекте BindAxis чаще направляют в Pawn, но тогда вы должны аккуратно получить текущий Pawn (он может меняться), и проверять валидность.

    GameMode: правила матча и серверная истина

    Документация: Game Mode and Game State.

    Что делает GameMode

    AGameModeBaseAGameMode) отвечает за:

  • выбор классов по умолчанию (Pawn, PlayerController, HUD, GameState, PlayerState);
  • правила входа игрока;
  • спавн и рестарт игроков;
  • правила матча (в AGameMode есть матч-состояния).
  • Критически важно для архитектуры: GameMode существует только на сервере. На клиентах он недоступен.

    Следствие:

  • всё, что нужно клиенту (UI, отображение счёта, состояние матча), должно жить в AGameStateBase/APlayerState или реплицироваться другим способом.
  • GameState и PlayerState: состояние, которое видят клиенты

  • AGameStateBase — общие данные матча для всех.
  • APlayerState — данные конкретного игрока, которые должны быть видимы другим (ник, счёт, команда).
  • Практика: если вы однажды поймали себя на желании читать GameMode на клиенте — значит, нужное состояние должно быть перенесено в GameState/PlayerState.

    Subsystems: сервисы по времени жизни, без «менеджер-актора»

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

    Subsystem — это UObject, который создаётся и уничтожается автоматически движком вместе с владельцем (Engine, GameInstance, World, LocalPlayer). Это ответ UE на типичный анти-паттерн: «создам невидимый Actor-менеджер и положу его на уровень».

    !Сравнение подсистем по времени жизни

    Какие подсистемы бывают и когда их выбирать

    Таблица как практический «выбор по месту хранения состояния»:

    | Тип подсистемы | Базовый класс | Время жизни | Типичные задачи | |---|---|---|---| | Engine | UEngineSubsystem | пока запущен движок | глобальные сервисы, аналитика, интеграции | | GameInstance | UGameInstanceSubsystem | между загрузками уровней | мета-прогресс, матчмейкинг, глобальные менеджеры | | World | UWorldSubsystem | на каждый мир/уровень | спавн-системы уровня, менеджер событий мира | | LocalPlayer | ULocalPlayerSubsystem | на каждого локального игрока | UI, настройки ввода, локальные сервисы |

    Ключевое отличие от акторов:

  • подсистема не требует присутствия в уровне;
  • у неё предсказуемый жизненный цикл Initialize/Deinitialize;
  • она удобна как точка доступа через GetSubsystem.
  • Пример: WorldSubsystem как менеджер спавна

    Использование из любого кода, имеющего доступ к UWorld:

    Subsystem и GC: что помнить

    Subsystem — это UObject, значит применяются правила из предыдущей статьи:

  • храните UObject-ссылки как UPROPERTY, если хотите, чтобы GC их видел;
  • в Deinitialize обязательно отписывайтесь от делегатов и прекращайте асинхронные операции, иначе получите «висячие» вызовы;
  • выбирайте правильный уровень времени жизни: если система должна переживать смену уровней, UWorldSubsystem не подходит.
  • Частые архитектурные ошибки и как их избежать

  • Хранить «всё состояние игры» в Pawn: Pawn умирает, пересоздаётся, меняется при respawn. Долгоживущие вещи храните в GameInstanceSubsystem или PlayerState (если это сетевой игрок).
  • Делать «менеджер-актор» на уровне для глобальных задач: при смене уровня он исчезнет. Чаще нужна подсистема.
  • Пытаться использовать GameMode на клиенте: перенесите данные в GameState/PlayerState.
  • Создавать компоненты через NewObject вместо CreateDefaultSubobject в конструкторе, когда вам нужен именно компонент как часть шаблона: получите проблемы с сериализацией, редактором и инициализацией.
  • Хранить ссылки на UObject без UPROPERTY: GC может уничтожить объект, и вы получите нестабильные краши.
  • Итоги

  • Actor — базовая сущность мира и контейнер компонентов, с жизненным циклом, репликацией и привязкой к уровню.
  • Component — основной инструмент композиции поведения; создавайте default subobject в конструкторе и храните через UPROPERTY.
  • Pawn — управляемое «тело», Controller — управляющий «мозг», possession соединяет их.
  • GameMode — серверные правила, клиенту предназначены GameState/PlayerState.
  • Subsystems — правильный способ делать сервисы по времени жизни (Engine/GameInstance/World/LocalPlayer) без привязки к уровню.
  • Следующий логичный шаг в углублении кода — рассмотреть сетевую модель (репликация, RPC, роли) и то, как она «ложится» на Pawn/Controller/GameState/PlayerState, а также как проектировать модули и подсистемы под мультиплеер.

    4. Системы данных: UPROPERTY, Data Assets, Data Tables, конфиги и сериализация

    Системы данных: UPROPERTY, Data Assets, Data Tables, конфиги и сериализация

    В предыдущих статьях курса мы разобрали архитектуру модулей и отражение (UHT, UCLASS/UPROPERTY/UFUNCTION), а также время жизни UObject и GC. Теперь соберём эти темы в практическую систему: как в UE5 правильно хранить, редактировать, загружать и сохранять данные.

    В UE данные редко бывают просто “полями в C++ классе”. Обычно вам нужно одновременно:

  • редактировать значения в Editor
  • сериализовать их в ассеты и уровни
  • поддерживать версии и миграции
  • загружать данные синхронно или асинхронно
  • выбирать между табличными, объектными, конфигурационными и сохранениями данными
  • В этой статье разберём основные инструменты: UPROPERTY, Data Assets, Data Tables, конфиги (Config) и базовую сериализацию.

    !Карта источников данных и путей попадания значений в рантайм

    UPROPERTY как фундамент: что именно даёт движку метаданные

    UPROPERTY не “делает поле красивым в редакторе”. Это часть контракта с системой отражения, сериализации и GC.

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

  • Unreal Engine Reflection System
  • Какие задачи решает UPROPERTY

  • Редактирование в Editor (Details panel)
  • Сериализация (в уровень, в ассет, в дубликаты объектов)
  • Репликация (в связке с сетевыми флагами и GetLifetimeReplicatedProps)
  • Участие в GC (движок “видит” ссылки на UObject)
  • Связь с прошлой статьёй про память проста: если вы храните ссылку на UObject в поле без UPROPERTY, GC может её не учитывать, и объект может быть собран.

    Часто используемые спецификаторы UPROPERTY

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

  • Unreal Engine Properties
  • Практически полезные группы спецификаторов:

  • Видимость и редактирование
  • - EditAnywhere, EditDefaultsOnly, EditInstanceOnly - VisibleAnywhere, VisibleDefaultsOnly, VisibleInstanceOnly
  • Доступ из Blueprint
  • - BlueprintReadOnly, BlueprintReadWrite
  • Категоризация и UX редактора
  • - Category, DisplayName, ToolTip, ClampMin, ClampMax, EditCondition
  • Сериализация в ini
  • - Config, GlobalConfig
  • Сохранения
  • - SaveGame

    Важное различие: Defaults, Instance, Runtime

    Одно и то же поле может иметь разные “слои” значений:

  • C++ дефолты в конструкторе инициализируют CDO (Class Default Object)
  • Blueprint дефолты переопределяют CDO на уровне блюпринта
  • Instance overrides переопределяют значения у конкретного актора/компонента на уровне (или в спавне)
  • Runtime изменения происходят в ходе игры и обычно не сохраняются автоматически
  • Если вы проектируете систему данных, вам нужно заранее решить: значение должно быть настраиваемым в дефолтах, уникальным для инстанса на уровне, или динамическим в рантайме.

    Data Assets: объектные данные как ассеты

    Data Asset удобен, когда данные:

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

  • Data Assets in Unreal Engine
  • Базовый паттерн: UPrimaryDataAsset для игровых “типов”

    UPrimaryDataAsset особенно полезен, когда вы хотите, чтобы ассет имел Primary Asset Id и мог управляться через Asset Manager.

    Почему здесь TSoftObjectPtr:

  • это мягкая ссылка на ассет, она не обязана держать USkeletalMesh загруженным
  • вы избегаете “тащу весь контент в память просто из-за ссылок в данных”
  • Как хранить ссылку на Data Asset в рантайм-объекте

    Если актор/компонент должен ссылаться на данные:

    Если вы хотите не грузить Data Asset сразу (например, для больших каталогов):

    Тогда загрузка может быть отложенной через Asset Manager или FStreamableManager.

    Плюсы и минусы Data Assets

  • Плюсы
  • - структура данных произвольная - удобно группировать “типовые” сущности (оружие, враги, предметы, способности) - хорошо дружит с ленивыми ссылками (TSoftObjectPtr)
  • Минусы
  • - для “табличных” больших наборов данных может быть неудобен (много ассетов) - требует дисциплины ссылок, чтобы не создать циклы и лишние загрузки

    Data Tables: табличные данные на основе USTRUCT

    Data Table — это таблица строк, где каждая строка имеет тип UStruct. Обычно используется для:

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

  • Data Tables in Unreal Engine
  • Как устроена строка Data Table

    Как правило, структура наследуется от FTableRowBase.

    Ключ строки обычно представлен как FName.

    Доступ к строке таблицы

    Типовой код получения данных по ключу:

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

  • проверяйте Row на nullptr
  • не храните “сырой указатель на строку” как долговечную ссылку без понимания времени жизни таблицы
  • Data Table vs Data Asset: грубый критерий выбора

  • Data Table лучше, когда у вас “много строк одного формата” и нужен быстрый доступ по ключу
  • Data Asset лучше, когда сущности сложнее, данных меньше, нужна иерархия/композиция, и/или удобнее работать с ассетами как с объектами
  • Конфиги (ini): параметры окружения и настройки, а не геймдизайн-контент

    Config-система UE позволяет хранить значения в ini-файлах и загружать их при старте/инициализации объектов.

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

  • Configuration Files in Unreal Engine
  • Что хранить в Config

    Хорошие кандидаты:

  • параметры по окружениям (dev/staging/shipping)
  • сетевые адреса сервисов
  • переключатели фич
  • дефолтные настройки качества/управления
  • Плохие кандидаты:

  • контентные данные баланса, которые должен править геймдизайнер в Editor
  • данные, требующие ссылок на ассеты (для этого лучше Data Assets/Data Tables)
  • Пример: класс с Config-полями

    Смысл аннотаций:

  • Config=Game указывает, что значения живут в группе Game ini
  • DefaultConfig позволяет сохранять дефолты в DefaultGame.ini
  • UPROPERTY(Config, ...) говорит системе конфигов, что поле читается/пишется в ini
  • Практический нюанс: конфиги — это не замена SaveGame. Config обычно про настройки и параметры сборки/окружения, а не про прогресс игрока.

    SaveGame: сериализация состояния игрока

    Для сохранения прогресса UE предлагает паттерн USaveGame и метку UPROPERTY(SaveGame).

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

  • Saving and Loading Your Game
  • Минимальная структура сохранения

    Ключевая идея: сериализуются поля, отмеченные SaveGame. Это удобно как явный “белый список” данных.

    Что не стоит сохранять напрямую

  • прямые ссылки на AActor из мира (после загрузки их может не существовать)
  • сырые UObject*, если вы не контролируете, как они будут восстановлены
  • Чаще сохраняют:

  • идентификаторы (FName, строки, GUID)
  • простые типы и структуры
  • ссылки на ассеты через TSoftObjectPtr или пути
  • Базовая сериализация UObject: почему “оно сохраняется само”

    Когда вы помечаете поле UPROPERTY, оно становится участником инфраструктуры:

  • редактор может показывать и изменять значение
  • объект может сохранять значение в ассет/уровень
  • копирование/дублирование объектов учитывает поле
  • (для UObject-ссылок) GC знает, что есть ссылка
  • Это объясняет типичный эффект: “почему мой int32 сохраняется между перезапусками редактора, если я его меняю в Details”. Потому что вы меняете сериализуемые свойства инстанса (или дефолты Blueprint), и UE записывает их как часть данных уровня/ассета.

    Когда вам нужна кастомная сериализация

    Если стандартной сериализации свойств недостаточно, вы используете переопределение Serialize или специализированные хуки загрузки.

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

  • UObject::Serialize
  • Versioning of Assets and Packages
  • Практические случаи:

  • миграция формата данных между версиями игры
  • упаковка данных в более компактный вид
  • условная сериализация (например, разные поля для разных платформ)
  • Важно: кастомная сериализация — продвинутый инструмент. Начинайте с правильного выбора контейнера данных (Data Asset/Data Table/Config/SaveGame), и только затем усложняйте.

    Как выбрать систему данных: шпаргалка

    | Система | Основной сценарий | Редактирование | Ссылки на ассеты | Типичный масштаб | Что хранить | |---|---|---|---|---|---| | UPROPERTY на Actor/Component | значения прямо на объекте в мире | Да | Да | мало/средне | уникальные instance-настройки | | Data Asset | “тип” сущности как ассет | Да | Отлично (включая soft) | средне | оружие, предметы, способности | | Data Table | много строк одного формата | Да | Нормально (обычно soft) | большое | баланс, справочники | | Config (ini) | настройки/окружение | Да (ограниченно) | плохо подходит | небольшое | флаги, URL, дефолтные настройки | | SaveGame | прогресс игрока | нет (обычно) | через идентификаторы/soft | зависит от игры | уровень, инвентарь, открытия |

    Типовые ошибки в проектировании данных

  • Хранить дизайн-данные в Config
  • - ini не заменяет контентные ассеты и плохо подходит для работы дизайнеров в Editor.
  • Делать hard-ссылки на тяжёлые ассеты в “глобальных” данных
  • - это может приводить к неожиданным загрузкам контента и росту времени старта.
  • Хранить UObject-ссылки без UPROPERTY
  • - риск неожиданных сборок GC и нестабильных крашей.
  • Сохранять ссылки на акторы напрямую в SaveGame
  • - после загрузки мира акторы будут другими; сохраняйте идентификаторы и восстанавливайте.

    Итоги

  • UPROPERTY — фундамент для редактирования, сериализации и корректной работы GC.
  • Data Assets — объектный способ хранить типовые игровые данные как ассеты; часто удобны с UPrimaryDataAsset и TSoftObjectPtr.
  • Data Tables — табличный формат для больших наборов однотипных записей через USTRUCT.
  • Config — для настроек окружения и параметров приложения, а не для основного геймдизайн-контента.
  • SaveGame — для прогресса игрока; сохраняйте устойчивые идентификаторы, а не ссылки на объекты мира.
  • Дальше по курсу эти системы данных станут основой для сетевой архитектуры (репликация состояния, предсказуемость источников истины) и для проектирования модулей и подсистем, которые загружают и применяют данные в правильные моменты жизненного цикла.

    5. События и асинхронность: Delegates, Timers, Tasks, Async загрузка ресурсов

    События и асинхронность: Delegates, Timers, Tasks, Async загрузка ресурсов

    В предыдущих статьях курса мы построили фундамент: модули и отражение (что видит UHT), UObject и GC (кто живёт и почему), Gameplay Framework (где хранить состояние), системы данных (откуда приходят значения). Теперь добавим то, без чего невозможно писать масштабируемый игровой код: событийную модель и асинхронность.

    В UE5 подавляющая часть «магии» игрового цикла делается через:

  • Delegates как типобезопасные события и подписки
  • Timers как планировщик вызовов на игровом потоке
  • Tasks как запуск работы вне кадра и вне потока игры
  • Async загрузку как способ не блокировать кадр при подтягивании контента
  • Ключевая идея статьи: в UE важно не просто «вызвать позже» или «в другом потоке», а сделать это так, чтобы не сломать время жизни UObject, не получить гонки потоков и не загрузить лишние ассеты в память.

    !Карта того, как события и асинхронные операции соединяют системы и где проходит граница потоков

    Delegates как основной механизм событий

    Delegate в UE — это типобезопасный объект, который хранит ссылку на функцию (и, опционально, на объект) и позволяет вызвать её позднее, а также управлять подписками.

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

  • Delegates
  • Зачем delegates, если в C++ есть std::function

    UE delegates решают практические задачи движка:

  • интеграция с UObject временем жизни и безопасными биндами
  • мультикаст-подписки (много слушателей)
  • поддержка динамических делегатов для Blueprint и сериализации
  • единый стиль событий для движка и вашего кода
  • Классы делегатов: одиночные, мультикаст и динамические

    В реальном проекте важно сразу выбирать правильный тип.

    | Тип | Макросы объявления | Сколько подписчиков | Blueprint | Типичное применение | |---|---|---:|---:|---| | Одиночный delegate | DECLARE_DELEGATE | 1 | Нет | колбэк завершения операции | | Multicast delegate | DECLARE_MULTICAST_DELEGATE | много | Нет | внутриигровые события в C++ | | Dynamic delegate | DECLARE_DYNAMIC_DELEGATE | 1 | Да | вызов Blueprint функции | | Dynamic multicast | DECLARE_DYNAMIC_MULTICAST_DELEGATE | много | Да | события компонента для BP (OnHealthChanged) |

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

  • если событие должно быть доступно в Blueprint, берите dynamic вариант
  • если Blueprint не нужен, берите обычные C++ delegates, они дешевле и гибче
  • Объявление и использование multicast delegate

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

    Подписка со стороны актора:

    AddUObject, AddLambda, AddRaw и вопрос времени жизни

    Самая частая причина нестабильных крашей в событийных системах UE: подписались на событие, но слушатель умер.

    Практические различия биндов:

  • AddUObject и BindUObject
  • - движок знает, что это UObject - при уничтожении объекта подписка корректно инвалидируется - это базовый выбор для gameplay-кода
  • AddLambda
  • - удобно для локальных колбэков - вы сами отвечаете за то, что захваченные данные живы
  • AddRaw
  • - для сырого указателя на обычный C++ объект - максимально опасно без дисциплины удаления подписки

    Если вы подписываетесь из UObject, используйте AddUObject почти всегда.

    Управление подписками: FDelegateHandle и отписка

    Для multicast delegate обычно нужна отписка, особенно если:

  • источник события живёт дольше подписчика
  • подписка ставится много раз (например, при смене Pawn у PlayerController)
  • вы хотите временно выключать слушателя
  • Связь с предыдущей статьёй про память: EndPlay и Deinitialize — это места, где вы обязаны отписываться, иначе получите «висячие» вызовы на уже уничтоженный объект.

    Dynamic multicast delegate для Blueprint

    Если нужно, чтобы дизайнер мог подписаться в Blueprint:

    Практический нюанс:

  • Dynamic delegates удобны для BP, но тяжелее, поэтому их обычно используют как публичный API, а внутри C++ часто держат отдельный native delegate.
  • Timers: планирование работы на игровом потоке

    Timer Manager — это способ вызвать функцию позже или периодически на Game Thread, не создавая Tick.

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

  • Gameplay Timers
  • Почему таймеры лучше, чем Tick

    Tick вызывается каждый кадр, даже если вам нужно сделать действие раз в секунду. Таймеры:

  • уменьшают стоимость кадра
  • делают намерение кода явным
  • проще управляются (пауза, отмена, переустановка)
  • Базовый паттерн: FTimerHandle и SetTimer

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

  • храните FTimerHandle, если таймер нужно отменять или проверять
  • очищайте таймеры в EndPlay, особенно если таймер вызывает методы этого же объекта
  • Таймеры и время жизни

    Timer Manager привязан к UWorld. Это означает:

  • при смене уровня мир может быть уничтожен, и таймеры исчезнут
  • для долгоживущих систем лучше использовать UGameInstanceSubsystem и запускать таймеры на актуальном мире, когда он есть
  • Связь с Gameplay Framework: выбор места, где стоит таймер, зависит от времени жизни. Таймер «в компоненте на Pawn» умрёт при respawn, таймер «в GameInstanceSubsystem» переживёт смену уровня.

    Таймеры не делают код многопоточным

    Таймеры вызывают ваш код на Game Thread. Это важно:

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

    Tasks и фоновые вычисления

    В UE есть несколько способов запустить работу «не в кадре». На практике в gameplay-коде часто встречаются:

  • Async(...) и AsyncTask(...)
  • Task Graph (как инфраструктура)
  • UE::Tasks (современный API задач в UE5)
  • Документация для ориентира по Async:

  • Async
  • Главный закон потоков UE

    Нельзя трогать UObjects и мир из фонового потока, если вы не понимаете точных гарантий и не используете специальные механизмы синхронизации.

    Практически безопасная модель:

  • В Game Thread собрать входные данные в простые типы (числа, строки, структуры без UObject ссылок, копии).
  • В фоне сделать тяжёлую работу.
  • Вернуться в Game Thread и применить результат к UObject.
  • Пример: вычисление в фоне и возврат в Game Thread

    Почему здесь TWeakObjectPtr:

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

    Когда выбирать tasks, а когда таймер

    | Нужно | Инструмент | Почему | |---|---|---| | Вызвать позже на Game Thread | Timer | простая планировка, без потоков | | Периодическая логика без Tick | Timer | дешевле Tick | | Тяжёлая работа без блокировки кадра | Task / Async | уводим работу в фон | | Вернуть результат в мир | AsyncTask(GameThread) | применение только на Game Thread |

    Типовые ошибки в задачах

  • захват this в лямбду без weak-ссылки
  • чтение/запись UObject полей в фоне
  • хранение ссылки на UWorld в задаче и использование её позже без проверки
  • отсутствие отмены или флага «операция больше не актуальна»
  • Практический паттерн для отмены:

  • хранить std::atomic<bool> или потокобезопасный флаг
  • в фоне периодически проверять флаг и выходить
  • при уничтожении объекта выставлять флаг и не применять результат
  • Асинхронная загрузка ресурсов

    Асинхронная загрузка — это продолжение темы «системы данных» из прошлой статьи. Если вы используете TSoftObjectPtr, вы получаете возможность загрузить ассет тогда, когда он реально нужен, и не блокировать кадр.

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

  • Asynchronous Asset Loading
  • Asset Manager
  • Hard vs Soft ссылки как решение проблемы «неожиданной загрузки»

  • hard-ссылка (TObjectPtr<USkeletalMesh>) заставляет ассет быть загруженным, когда загружается владелец
  • soft-ссылка (TSoftObjectPtr<USkeletalMesh>) хранит путь и не грузит ассет автоматически
  • Практическое правило из систем данных:

  • в Data Assets и таблицах почти всегда лучше использовать soft-ссылки на тяжёлый контент
  • Загрузка через StreamableManager по SoftObjectPath

    Минимальный паттерн: есть soft-ссылка, по запросу грузим и применяем.

    Важные замечания:

  • колбэк RequestAsyncLoad вызывается на Game Thread, поэтому применять результат к компонентам безопасно
  • WeaponMesh.Get() вернёт объект, если он уже загружен
  • если объект, который запрашивал загрузку, может умереть до завершения, используйте weak-паттерн или отмену через handle
  • Asset Manager как масштабируемый уровень

    UAssetManager полезен, когда:

  • ассетов много
  • нужно централизованно управлять каталогами и правилами загрузки
  • вы хотите оперировать Primary Assets (например, UPrimaryDataAsset) и их зависимостями
  • Это связывает тему с прошлой статьёй: Data Assets + soft ссылки + Asset Manager образуют управляемую систему контента, которая не взрывает время старта и память.

    Частые ошибки при async загрузке

  • хранить hard-ссылку на тяжёлый ассет в «глобальных» данных и удивляться долгому старту
  • не проверять, что объект-запросчик ещё жив, когда приходит колбэк
  • не разруливать повторные запросы (например, несколько вызовов Load подряд)
  • Практический паттерн защиты от повторных запросов:

  • хранить состояние bIsLoading
  • хранить TSharedPtr<FStreamableHandle> и проверять/отменять предыдущую загрузку
  • при получении результата проверять, что он всё ещё нужен (например, текущий WeaponId не сменился)
  • Как собрать всё вместе в архитектуру проекта

    Чтобы события и асинхронность не превратились в «паутину колбэков», полезно закрепить роли.

    Один из рабочих вариантов:

  • Компоненты и акторы публикуют события через delegates.
  • Подсистемы координируют долгие процессы: таймеры, очереди задач, async загрузки.
  • Результаты фоновых задач всегда применяются на Game Thread.
  • Все подписки и таймеры корректно чистятся в EndPlay или Deinitialize.
  • Это напрямую продолжает темы времени жизни (GC), Framework (где живут системы) и систем данных (как подгружается контент).

    Итоги

  • Delegates — типобезопасная событийная модель UE; выбирайте между native и dynamic по необходимости Blueprint.
  • Подписки должны учитывать время жизни: используйте AddUObject, FDelegateHandle и отписку в EndPlay.
  • Timers — способ планировать вызовы на Game Thread без Tick; они не дают многопоточности.
  • Tasks/Async — инструмент разгрузить кадр, но требует строгой дисциплины: UObjects трогаем только на Game Thread.
  • Async загрузка ассетов строится вокруг soft-ссылок (TSoftObjectPtr), StreamableManager и при масштабе — Asset Manager.
  • Следующий логичный шаг курса после этой темы — сетевой код и репликация, потому что сетевые события, RPC и реплицируемое состояние почти всегда завязаны на delegates, таймеры и асинхронные операции (загрузка, ожидание, подтверждения).

    6. Сетевой C++: репликация, RPC, Prediction, Authority и оптимизация трафика

    Сетевой C++: репликация, RPC, Prediction, Authority и оптимизация трафика

    Сетевой код в UE5 строится вокруг простой идеи: сервер — источник истины, клиенты — отображают и предсказывают. Всё, что мы обсуждали ранее в курсе, здесь критично:

  • Из темы про UObject и GC: сетевые вызовы приходят асинхронно, поэтому нельзя держать «сырые» ссылки без UPROPERTY() и нельзя захватывать this в долгоживущие колбэки без проверки валидности.
  • Из темы про Gameplay Framework: важно понимать, какие классы существуют на сервере/клиенте и где хранить состояние (например, GameMode только на сервере).
  • Из темы про события и асинхронность: репликация и RPC — это ещё один тип «событий», но с сетевыми гарантиями, стоимостью и задержкой.
  • Официальная точка входа: Networking and Multiplayer.

    !Карта направлений данных и ролей акторов в мультиплеере UE

    Базовая модель мультиплеера в UE

    Термины, которые будем использовать:

  • Сервер: процесс, который симулирует «настоящий» мир и принимает решения.
  • Клиент: процесс, который отправляет ввод и отображает состояние.
  • Listen Server: сервер и клиент в одном процессе (типично для коопа).
  • Dedicated Server: только сервер, без локального игрока.
  • Репликация: сервер отправляет клиентам изменения состояния.
  • RPC: удалённый вызов функции по сети.
  • Authority: право «утверждать» состояние (обычно сервер).
  • Ownership: связь актора с конкретным подключением (кто может посылать Server RPC через этот актор).
  • Документация по репликации акторов: Actor Replication.

    Роли, Authority и Ownership: три вещи, которые нельзя путать

    Authority

    Authority отвечает на вопрос: кто имеет право менять и подтверждать состояние?

  • На сервере большинство акторов имеют HasAuthority() == true.
  • На клиентах HasAuthority() почти всегда false (кроме редких случаев с чисто локальными акторами, не участвующими в сети).
  • Практическое правило:

  • Любое состояние, которое влияет на игру (урон, инвентарь, попадания), должно подтверждаться на сервере.
  • Сетевая роль актора

    Роль отвечает на вопрос: как этот актор симулируется на этой машине?

  • Authority: актор симулируется «по-настоящему» (обычно сервер).
  • Autonomous Proxy: клиент-владелец предсказывает поведение (типично для Pawn игрока).
  • Simulated Proxy: клиент лишь интерполирует состояние, пришедшее с сервера (другие игроки).
  • В коде это читается через GetLocalRole().

    Ownership

    Ownership отвечает на вопрос: какой клиент «владеет» актором и может через него общаться с сервером?

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

  • Команды от игрока удобно слать на сервер через PlayerController, Pawn или компоненты, которые принадлежат этому игроку.
  • Репликация: что реально отправляется по сети

    Важно понять: репликация не пересылает «объект целиком каждый кадр». Сервер периодически сравнивает состояние, вычисляет «грязные» поля и отправляет дельту релевантным клиентам.

    Включаем репликацию для Actor

    Минимальные требования:

  • актор должен иметь bReplicates = true
  • актор должен быть создан на сервере (типично через SpawnActor на сервере)
  • SetReplicateMovement(true) включает стандартную репликацию движения для актора (удобно для простых случаев, но не всегда оптимально для сложной физики и кастомной симуляции).

    Реплицируемые свойства (UPROPERTY Replicated)

    Репликация полей строится на:

  • UPROPERTY(Replicated) или UPROPERTY(ReplicatedUsing=...)
  • GetLifetimeReplicatedProps + DOREPLIFETIME
  • Практические правила:

  • Меняйте реплицируемое состояние на сервере (проверка HasAuthority()).
  • Используйте OnRep_... для клиентских реакций вместо попыток «поймать момент» на клиенте.
  • Не делайте тяжёлую логику в OnRep: это может вызываться часто и пачками.
  • Условия репликации (экономия трафика)

    Не все данные нужны всем. UE позволяет реплицировать свойства условно.

    Пример: отправлять параметр только владельцу.

    Идея:

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

    Документация: Remote Procedure Calls.

    RPC решают задачу «событие сейчас», а репликация свойств — «состояние как факт». Это разные инструменты.

    Типы RPC

  • Server: клиент вызывает, выполняется на сервере.
  • Client: сервер вызывает, выполняется на конкретном клиенте-владельце.
  • NetMulticast: сервер вызывает, выполняется на сервере и на всех клиентах (на которых актор релевантен).
  • Также есть надёжность:

  • Reliable: движок будет пытаться доставить (дороже, опасно спамить).
  • Unreliable: может теряться, но дешевле (часто подходит для частых событий).
  • Пример: команда игрока на сервер (Server RPC)

    Ключевые замечания:

  • Если клиент не владеет актором, Server RPC может не пройти.
  • Сервер обязан валидировать вход (например, нельзя верить координате попадания от клиента без проверки).
  • Пример: визуальный эффект для всех (NetMulticast)

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

  • NetMulticast используйте для эффектов и второстепенных событий.
  • Геймплейное состояние фиксируйте на сервере и доносите через репликацию свойств.
  • Prediction: как сделать управление отзывчивым

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

    Что такое Prediction и Reconciliation

  • Клиент немедленно применяет ввод локально (prediction).
  • Клиент отправляет ввод на сервер.
  • Сервер симулирует «истинную» версию.
  • Если расхождение значимое, клиент получает корректировку и перематывает симуляцию (reconciliation).
  • !Временная диаграмма client-side prediction и server reconciliation

    Готовое решение: CharacterMovementComponent

    Для ACharacter большая часть prediction уже реализована в UCharacterMovementComponent.

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

  • Если вы делаете шутер/экшен с персонажем, начинать лучше с ACharacter, а не с «кастомного Pawn с нуля».
  • Паттерн для кастомного prediction (в общем виде)

    Чтобы построить предсказание для своей механики (например, рывок, каст, способность), обычно делают так:

  • Клиент применяет эффект локально (только визуально и для ощущения).
  • Клиент отправляет на сервер «команду» с минимальными данными (кнопка, направление, время).
  • Сервер проверяет возможность (кд, ресурсы, античит) и утверждает результат.
  • Сервер реплицирует состояние, а клиент при необходимости корректируется.
  • Что важно не делать:

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

    Документация по инструментам анализа: Network Profiler.

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

  • Кому отправлять (relevancy).
  • Как часто отправлять (frequency).
  • Что именно отправлять (размер данных и формат).
  • Relevancy: не все акторы важны всем

    Базовые рычаги:

  • NetCullDistanceSquared: актор станет нерелевантным на больших дистанциях.
  • bAlwaysRelevant: всегда релевантен (используйте редко).
  • bOnlyRelevantToOwner: только владельцу.
  • Практика:

  • Пули, временные эффекты, мелкие интерактивные объекты часто не должны быть релевантны всем.
  • Частота обновлений: NetUpdateFrequency и ForceNetUpdate

    Каждый реплицируемый актор имеет частоту, с которой он рассматривается на отправку:

  • NetUpdateFrequency: «как часто пытаться отправлять».
  • MinNetUpdateFrequency: нижняя граница.
  • ForceNetUpdate(): принудительно отправить как можно скорее (использовать точечно).
  • Практические правила:

  • Снижайте NetUpdateFrequency у объектов, которым не нужна высокая точность.
  • Для редких событий лучше отправить одно RPC или один рывок состояния, чем держать высокую частоту постоянно.
  • Dormancy: засыпание акторов

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

  • У актора есть NetDormancy.
  • Когда состояние меняется, можно «разбудить» и отправить обновление.
  • Это особенно полезно для:

  • лута, дверей, переключателей
  • объектов окружения, которые редко меняются
  • Условия и сжатие данных

    Самые частые приёмы:

  • Репликация только владельцу (COND_OwnerOnly) или только не-владельцу (COND_SkipOwner).
  • Отправка событий как Unreliable (например, частые VFX), если потеря пакета не критична.
  • Использование квантованных типов, если точность не нужна:
  • - FVector_NetQuantize, FVector_NetQuantize10, FVector_NetQuantize100

    Идея квантования простая: вы снижаете точность координаты, чтобы уменьшить размер данных.

    Большие массивы: Fast Array Serializer

    Если вам нужно эффективно реплицировать список элементов, которые часто меняются (инвентарь, эффекты, статы), наивная репликация TArray может стать дорогой.

    Для таких кейсов в UE есть паттерн быстрой репликации массивов через FFastArraySerializer и элементы FFastArraySerializerItem.

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

  • реплицируются изменения (добавление, удаление, изменение элемента), а не весь массив целиком
  • Документация: Fast Array Replication.

    Не реплицируйте то, что можно восстановить

    Типовые примеры экономии:

  • Не реплицировать HP как float с высокой точностью, если достаточно int.
  • Не реплицировать положение VFX, если оно жёстко привязано к реплицируемому актору.
  • Не реплицировать «производные» значения, если их можно вычислить на клиенте из базовых.
  • Типовые ошибки сетевого C++

  • Отправлять геймплей через NetMulticast вместо серверной истины и реплицируемого состояния.
  • Спамить Reliable RPC (очередь надёжных сообщений может стать источником лагов).
  • Держать в RPC «толстые» параметры (структуры с лишними данными, строки, ассет-ссылки).
  • Реплицировать «всё подряд» вместо выбора источника истины и минимального набора данных.
  • Считать, что HasAuthority() означает «это серверный процесс в целом», хотя на самом деле это проверка роли конкретного актора на этой машине.
  • Практическая стратегия проектирования сетевого gameplay

    Короткая, но рабочая схема:

  • Состояние, которое должно быть одинаковым у всех: UPROPERTY(Replicated).
  • Реакции на изменение состояния на клиентах: OnRep_....
  • Команды игрока на сервер: UFUNCTION(Server, ...).
  • Эффекты для всех: NetMulticast (обычно Unreliable).
  • Отзывчивость: prediction на клиенте, подтверждение и коррекция от сервера.
  • Оптимизация: relevancy, частота, dormancy, условия репликации, квантование, FastArray.
  • Итоги

  • Репликация свойств — основной канал доставки состояния из сервера в клиенты.
  • RPC — основной канал доставки событий и команд с понятными направлениями (Server, Client, NetMulticast).
  • Authority, роль и ownership решают разные задачи: «кто утверждает», «как симулируем», «кто владеет каналом команд».
  • Prediction нужен для отзывчивости, но истина остаётся на сервере.
  • Оптимизация трафика строится вокруг relevancy, частоты и уменьшения данных.
  • 7. Производительность и инструменты: профилирование, оптимизация, расширение редактора и автоматизация

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

    Производительность в UE5 C++ — это не «магия оптимизаций», а дисциплина: измерить → понять узкое место → изменить → снова измерить. Почти все темы курса напрямую связаны с производительностью:

  • Архитектура модулей и UHT влияет на время сборки, итерации и разделение Runtime/Editor кода.
  • Понимание UObject, GC и ссылок влияет на память, спайки GC и стабильность.
  • Gameplay Framework определяет, где живёт логика (Pawn/Controller/Subsystem) и как часто она выполняется.
  • Системы данных и async загрузка определяют старт уровня, стриминг и пиковое потребление памяти.
  • Сеть добавляет ограничения по частоте обновлений, размерам данных и количеству реплицируемых акторов.
  • В этой статье разберём практический инструментарий UE5 для профилирования и оптимизации, а также как расширять редактор на C++ и автоматизировать задачи (сборки, тесты, обработку ассетов).

    !Цикл оптимизации: измерение, поиск узкого места, изменение и проверка результата

    Ментальная модель: что именно мы оптимизируем

    В UE5 удобно разделять проблемы по источнику:

  • CPU (Game Thread): ваш геймплейный код, тик акторов/компонентов, обработка ввода, логика AI, работа с контейнерами.
  • CPU (Render Thread / RHI Thread): подготовка команд рендеру и отправка на GPU.
  • GPU: шейдеры, постэффекты, количество отрисовок (draw calls), тяжёлые материалы.
  • Память: объём ассетов и объектов, частые аллокации, спайки GC, фрагментация.
  • IO и загрузка: синхронные загрузки, стриминг, обращение к диску, блокировки кадра.
  • Сеть: частота репликации, размер пакетов, количество релевантных акторов.
  • Практическое правило: сначала определите класс проблемы, иначе легко оптимизировать «не туда» (например, переписывать C++ код, когда упираетесь в GPU).

    Быстрый старт: минимальный набор инструментов

    Stat-команды и встроенные оверлеи

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

  • stat unit показывает время кадра по CPU и GPU.
  • stat game даёт детализацию по подсистемам на игровом потоке.
  • stat slate полезен для UI и редакторных виджетов.
  • stat net помогает понять сетевую нагрузку и частоту обновлений.
  • Они хороши для первой диагностики, но для глубокого анализа нужен трейс.

    Unreal Insights: основной инструмент профилирования UE5

    Unreal Insights — система трейсов и визуализации таймлайнов для CPU, задач, загрузок, иногда памяти и сети.

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

    Что даёт Insights:

  • Таймлайны Game Thread, Render Thread, Task Graph и других потоков.
  • Понимание, кто кого блокирует (ожидания, синхронизации).
  • Анализ «спайков» (например, GC, загрузки, компиляция шейдеров).
  • Профилирование рендера

    Для GPU-анализа в UE часто используют встроенные средства (включая команду ProfileGPU в редакторе). Общая точка входа по теме: Performance and Profiling.

    Практическое правило: если stat unit показывает, что GPU время больше CPU времени, начните с GPU-профиля, а не с переписывания геймплея.

    Как правильно снимать профили: воспроизводимость и сравнение

    Основная ошибка профилирования: снять «случайный» трейс и делать выводы. Рабочий процесс такой:

  • Сделайте воспроизводимый сценарий (одинаковая сцена, одинаковые действия, одинаковое время).
  • Зафиксируйте исходную метрику (например, среднее время кадра или конкретный спайк).
  • Снимите трейс в Unreal Insights.
  • Найдите конкретную причину (функция, подсистема, ожидание, аллокации).
  • Внесите изменение и повторите замер.
  • Если вы оптимизируете не производительность кадра, а время загрузки, принцип тот же: фиксируйте «до/после» и не меняйте сразу 10 вещей.

    Инструментирование кода: как видеть свои функции в профайлере

    Когда вы нашли «подозрительный участок», следующий шаг — пометить его так, чтобы он стал видим в профилировщике.

    CPU-события в трейсе (Unreal Insights)

    В UE можно добавлять области профилирования (scope), которые будут видны в таймлайне.

    Смысл:

  • TRACE_CPUPROFILER_EVENT_SCOPE(...) создаёт именованную область, которая попадёт в трейс.
  • Так вы сможете отличать «вашу работу» от кода движка и быстро оценивать вклад.
  • Быстрые счётчики для stat-системы

    Если вы хотите видеть свой код в stat game или в собственном stat-группе, используйте scoped-метки статистики.

    Практическое правило: помечайте только то, что реально важно, иначе вы «утонете» в шуме.

    Типовые оптимизации в UE5 C++ (которые почти всегда дают эффект)

    Уберите лишний Tick: делайте код событийным

    Tick — один из самых частых источников «смерти от тысячи порезов». Даже пустой Tick у тысяч акторов создаёт накладные расходы.

    Стратегии замены Tick:

  • Таймеры (FTimerManager) для редких и периодических действий.
  • Делегаты и события (например, реагировать на OnRep, на триггеры, на изменения состояния).
  • Подсистемы, которые обновляют только нужные объекты (а не каждый объект сам себя).
  • Связь с прошлой статьёй: таймеры вызываются на Game Thread, а делегаты требуют дисциплины отписок в EndPlay/Deinitialize.

    Сократите аллокации и копирование

    Частые мелкие аллокации в кадре дают спайки и давление на аллокатор.

    Практики:

  • Резервируйте память в контейнерах, если знаете размер (TArray::Reserve).
  • Передавайте тяжёлые структуры по const ссылке.
  • Не создавайте временные FString/FText в тике без необходимости.
  • Следите за временем жизни и GC

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

  • слишком много краткоживущих UObject → больше работы GC;
  • утечки ссылок через UPROPERTY → объекты не выгружаются;
  • удержание тяжёлых ассетов hard-ссылками → рост памяти и время старта.
  • Практика из темы данных: тяжёлые ассеты чаще хранить как TSoftObjectPtr и грузить асинхронно.

    Оптимизируйте сеть системно, а не «подкручивайте частоты» вслепую

    Если у вас сетевой проект:

  • Уменьшайте репликацию через условия (COND_OwnerOnly, COND_SkipOwner).
  • Настраивайте релевантность, частоту обновлений и dormancy.
  • Для списков используйте Fast Array Replication.
  • Это продолжение статьи про сетевой C++: оптимизация трафика почти всегда дешевле, чем попытки «ускорить всё» на CPU.

    Память и загрузки: как ловить проблемы

    Для памяти обычно важно ответить на два вопроса:

  • Что занимает память сейчас?
  • Почему это не выгружается?
  • Инструменты:

  • Unreal Insights (трек памяти зависит от конфигурации проекта и включённых трейс-каналов).
  • Встроенные отчёты и стат-метрики (в том числе stat memory).
  • Если вы подозреваете проблемы удержания объектов, вернитесь к теме GC: в UE критично понимать, какие ссылки видимы сборщику мусора.

    Расширение редактора на C++: правильные границы и архитектура

    Расширение редактора — это почти всегда Editor-модуль или Editor-плагин, который не должен попадать в packaged-сборку.

    Связь с первой статьёй курса: Editor-зависимости нельзя тянуть в Runtime-модуль, иначе сборка игры сломается.

    Документация по модулям и плагинам: Unreal Engine Modules.

    Когда вам нужен Editor-код

    Типовые задачи:

  • Автоматическая генерация ассетов, переименование, проставление метаданных.
  • Кастомные панели, кнопки меню, тулзы для дизайнеров.
  • Кастомизация Details (как отображаются и редактируются свойства).
  • Проверки контента (валидаторы) перед сборкой.
  • Минимальная структура Editor-модуля

    MyToolsEditor.Build.cs обычно включает UnrealEd и UI-модули:

    Дальше вы регистрируете расширение (меню, тулбар, вкладку) в StartupModule и снимаете в ShutdownModule.

    Защита Runtime-кода от Editor-зависимостей

    Если часть логики должна жить в Runtime (например, общие алгоритмы), но вы хотите вызывать её из редакторного инструмента:

  • вынесите алгоритмы в Runtime-модуль без UnrealEd;
  • в Editor-модуле сделайте тонкую оболочку, которая вызывает Runtime-код;
  • используйте #if WITH_EDITOR только точечно, когда это действительно условная часть внутри Runtime-типа.
  • Автоматизация: тесты, командлеты, сборки

    Автоматизация в UE5 C++ обычно строится из трёх уровней:

  • Automation Tests: тестируют код (юнит/интеграционные) и запускаются в редакторе или через командную строку.
  • Commandlets: отдельные команды для пакетной обработки контента и проектов.
  • Unreal Automation Tool (UAT): скриптовый слой, который запускает сборку, cook, package и тесты в CI.
  • Документация:

  • Automation System
  • Commandlets
  • Unreal Automation Tool
  • Automation Tests: зачем они нужны именно геймплей-программисту

    Тесты полезны не только «для библиотек». В UE они помогают:

  • закрепить контракт сетевой логики (например, что сервер валидирует команды);
  • проверять сериализацию данных (Data Assets, таблицы, SaveGame-структуры);
  • ловить регрессии производительности на ключевых сценариях (через стабильные метрики и сравнение).
  • Commandlets: пакетная обработка ассетов без ручной рутины

    Commandlet — это способ сделать повторяемую задачу:

  • прогнать валидаторы контента;
  • переименовать/поправить ассеты;
  • пересоздать промежуточные данные;
  • собрать отчёт по проекту.
  • Смысл для производительности: вы уносите «тяжёлую обработку» из человеческого времени и делаете её воспроизводимой в CI.

    Итоги

  • Производительность в UE5 начинается с правильной диагностики: сначала определите, CPU это, GPU, память, IO или сеть.
  • stat-команды дают быстрый срез, но глубокий анализ делайте в Unreal Insights.
  • Инструментируйте свой код через scoped-события, чтобы видеть вклад конкретных функций.
  • Самые частые выигрыши: убрать лишний Tick, уменьшить аллокации, правильно работать с временем жизни UObject и загрузкой ассетов.
  • Editor-расширения пишите в Editor-модулях/плагинах и не тяните UnrealEd в Runtime.
  • Автоматизация (Tests, Commandlets, UAT) превращает качество и производительность в воспроизводимый процесс, а не в ручную «героику».