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

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

1. Архитектура UE5 и модель объектов: UCLASS, USTRUCT, UFUNCTION, UPROPERTY

Архитектура UE5 и модель объектов: UCLASS, USTRUCT, UFUNCTION, UPROPERTY

Зачем Unreal Engine “просит” макросы

Unreal Engine на C++ выглядит как обычный C++, но ключевые игровые системы движка работают поверх модели объектов UE и рефлексии. Именно поэтому в коде появляются макросы UCLASS, USTRUCT, UFUNCTION, UPROPERTY.

Эти макросы нужны, чтобы движок мог:

  • Создавать объекты UE в рантайме и в редакторе
  • Видеть поля и функции в Blueprints и Details Panel
  • Сериализовать данные (уровни, ассеты, сохранения)
  • Реплицировать свойства и вызывать RPC по сети
  • Управлять временем жизни объектов через сборщик мусора
  • В этой статье мы разберём архитектурный контекст и практическое применение основных макросов.

    Картина мира UE5: модули, объектная система и Gameplay Framework

    На высоком уровне проект UE5 состоит из модулей C++ (ваш игровой модуль и плагины), которые собираются через Unreal Build Tool и интегрируются с движком.

    Ключевой момент: игровая логика UE строится вокруг UObject-иерархии.

    !Базовая иерархия типов и где они существуют

    Базовые типы

  • UObject
  • - Базовый тип для большинства “умных” объектов UE. - Умеет участвовать в рефлексии, сериализации и сборке мусора. - Обычно не существует “в мире”, если это не Actor или Component.
  • AActor
  • - Объект, который существует в UWorld и может быть размещён на уровне. - Имеет трансформ и жизненный цикл уровня.
  • UActorComponent
  • - Компонент, который добавляет актёру поведение и данные. - Типичный способ строить композицию вместо глубокого наследования.

    Почему это важно для макросов

    UCLASS/USTRUCT/UFUNCTION/UPROPERTY подключают ваш код к системам:

  • Reflection: движок знает, что за типы/поля/функции у вас есть
  • Serialization: движок умеет сохранять и загружать значения
  • Networking: движок умеет реплицировать свойства и вызывать RPC
  • Editor: движок показывает свойства в Details Panel и работает с Blueprints
  • Рефлексия UE5 и Unreal Header Tool

    UE использует собственную систему рефлексии. В отличие от некоторых языков, C++ не предоставляет рефлексию “из коробки”, поэтому UE генерирует вспомогательный код.

    Что делает Unreal Header Tool

    Unreal Header Tool (UHT) анализирует заголовки, где встречаются UE-макросы, и генерирует код в *.generated.h.

    !Как UHT генерирует код для рефлексии

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

  • #include "YourType.generated.h" должен быть последним include в вашем .h.
  • Макросы работают только там, где UHT может их “увидеть”.
  • Не любой C++ тип/шаблон корректно “рефлектится” и будет виден движку.
  • Официальная документация:

  • Unreal Header Tool
  • Unreal Property System (Reflection)
  • UCLASS: классы, которые понимает Unreal

    UCLASS применяется к классам, которые наследуются от UObject (или его потомков, например AActor, UActorComponent).

    Минимальный шаблон:

    Что даёт UCLASS

  • Регистрацию типа в системе рефлексии
  • Возможность создавать объекты через UE-механизмы (NewObject, SpawnActor)
  • Доступность для редактора и Blueprints (при соответствующих спецификаторах)
  • Интеграцию с сериализацией и репликацией
  • Часто используемые спецификаторы UCLASS

    | Спецификатор | Для чего | Типичный сценарий | |---|---|---| | Blueprintable | Разрешает наследование в Blueprint | Базовый C++ класс, от которого дизайнеры делают BP-потомков | | BlueprintType | Разрешает использовать тип как переменную в Blueprint | Data-объекты, компоненты, структуры данных | | Abstract | Запрещает создание экземпляров напрямую | Базовые классы-фреймворки | | Config=Game | Подключает систему ini-конфигов | Настройки, которые читаются из DefaultGame.ini | | NotBlueprintable | Запрещает наследование в Blueprint | Внутренние технические классы |

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

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

  • UCLASS
  • USTRUCT: структурные типы для данных, сериализации и Blueprints

    USTRUCT нужен, когда вы хотите:

  • Хранить значимые данные (value type), а не объект с жизненным циклом UObject
  • Сериализовать структуру или показывать её в редакторе
  • Использовать структуру как тип переменной в Blueprint
  • Пример структуры с видимостью в Blueprint:

    Когда USTRUCT лучше, чем UObject

  • Данных много, и они часто копируются или лежат в массивах
  • Не нужен сборщик мусора и виртуальные вызовы
  • Хотите простую сериализацию и отображение в редакторе
  • Документация:

  • Structs
  • UPROPERTY: поля, которые видит движок

    UPROPERTY помечает член класса или структуры как свойство UE. Это включает его в рефлексию и подключает к подсистемам движка.

    Почему UPROPERTY критично для UObject-ссылок

    Unreal использует сборщик мусора для UObject. Он отслеживает ссылки между объектами, но только те, которые он знает.

    Если вы храните ссылку на UObject в обычном “сыром” указателе без UPROPERTY, сборщик мусора может считать, что объект больше никому не нужен, и удалить его. Итог: висячий указатель и краш.

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

  • Ссылки на UObject-типы, которые должны удерживать объект живым, обычно должны быть UPROPERTY()
  • В UE5 часто встречается TObjectPtr<T> как рекомендуемая форма UObject-ссылок в свойствах, но базовый принцип остаётся тем же: свойство должно быть известно системе.

    Категории спецификаторов UPROPERTY

    #### Видимость и редактирование в редакторе

    | Спецификатор | Что делает | Пример | |---|---|---| | EditAnywhere | Можно редактировать и в defaults, и у instance | Настройки актёра на уровне | | EditDefaultsOnly | Редактирование только в Class Defaults | Параметры, одинаковые для всех инстансов | | VisibleAnywhere | Видно, но нельзя менять | Отладочные/вычисляемые поля |

    #### Доступность в Blueprints

    | Спецификатор | Что делает | Пример | |---|---|---| | BlueprintReadOnly | Только чтение в BP | Текущие значения состояния | | BlueprintReadWrite | Чтение/запись в BP | Настраиваемые параметры |

    #### Сериализация, сохранения, временные данные

    | Спецификатор | Что делает | Пример | |---|---|---| | Transient | Не сериализуется | Кэш, временные вычисления | | SaveGame | Участвует в системе сохранений | Прогресс игрока |

    #### Репликация

    | Спецификатор | Что делает | Пример | |---|---|---| | Replicated | Реплицируется по сети | Состояния, важные для клиентов | | ReplicatedUsing=OnRep_X | Репликация + коллбек при обновлении | Запуск VFX/SFX при изменении |

    Для репликации одного UPROPERTY(Replicated) недостаточно: нужно ещё включить репликацию актёра и описать реплицируемые поля в GetLifetimeReplicatedProps.

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

  • Properties
  • UFUNCTION: функции для Blueprint, делегатов и сети

    UFUNCTION делает метод видимым системе рефлексии. Это нужно для:

  • Вызова из Blueprint
  • Подписки на динамические делегаты (которые требуют рефлексии)
  • Сетевых RPC (Server/Client/NetMulticast)
  • Вызовов из консоли (Exec)
  • Примеры:

    Сетевые RPC через UFUNCTION

    RPC задаются спецификаторами:

  • Server вызывается на сервере
  • Client вызывается на клиенте
  • NetMulticast вызывается на всех (при вызове с сервера)
  • Reliable/Unreliable задаёт гарантии доставки
  • Пример объявления:

    Важно: RPC работают только в контексте сетевой модели UE и при корректных настройках репликации для актёра/компонента.

    Blueprint-события из C++

    Два частых варианта:

  • BlueprintImplementableEvent означает, что реализация будет в Blueprint
  • BlueprintNativeEvent означает, что есть C++ реализация по умолчанию, но Blueprint может её переопределить
  • Документация:

  • UFunctions
  • Сборка примера: актёр, структура данных, свойства и методы

    Ниже пример, который связывает всё вместе: актёр хранит структуру, имеет ссылку на компонент, экспонирует настройки в редакторе и предоставляет функции для Blueprint.

    Ключевые моменты:

  • Mesh помечен как UPROPERTY, чтобы редактор видел компонент, и чтобы ссылка корректно учитывалась системами UE
  • BaseDamage настроечный параметр, удобно править в defaults
  • LastHit это USTRUCT, отображается и редактируется
  • DealDamageAtLocation доступна из Blueprint
  • Типичные ошибки и как их избегать

  • Неправильный include порядка generated.h
  • 1. *.generated.h должен подключаться последним include в .h файле.
  • UObject-ссылка без UPROPERTY
  • 1. Если храните UObject* как обычный указатель, GC может удалить объект. 2. Используйте UPROPERTY() и подходящий тип ссылки.
  • Ожидание, что любой C++ тип появится в редакторе
  • 1. Редактор “видит” только то, что поддержано системой свойств UE.
  • RPC без UFUNCTION
  • 1. Сетевые функции должны быть UFUNCTION(Server/Client/NetMulticast, ...).
  • Попытка использовать USTRUCT как UObject
  • 1. USTRUCT не имеет сборки мусора и не живёт как отдельный объект UE.

    Итоги

  • UCLASS подключает класс к объектной системе UE и рефлексии
  • USTRUCT подходит для данных-значений и удобен для сериализации и Blueprint-переменных
  • UPROPERTY делает поле видимым для UE, критично для GC, сериализации, редактора и репликации
  • UFUNCTION делает метод вызываемым из Blueprint, пригодным для делегатов и сетевых RPC
  • В следующих материалах курса эта база станет фундаментом для более сложных тем: жизненный цикл объектов, подсистемы, модульность, делегаты, репликация, сериализация и архитектура игрового кода.

    Ссылки

  • Unreal Property System (Reflection)
  • Unreal Header Tool
  • UCLASS
  • UFunctions
  • Properties
  • Structs
  • 2. Игровой фреймворк и жизненный цикл: Actor, Component, Pawn, Controller, GameMode

    Игровой фреймворк и жизненный цикл: Actor, Component, Pawn, Controller, GameMode

    Как эта тема связана с предыдущей статьёй

    В прошлой статье мы разобрали модель объектов UE и рефлексию: UCLASS, USTRUCT, UFUNCTION, UPROPERTY. Игровой фреймворк UE5 построен поверх этой модели.

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

  • Вы наследуетесь от фреймворк-классов (AActor, APawn, APlayerController, AGameModeBase).
  • Вы используете UPROPERTY для ссылок на компоненты и другие UObject, чтобы корректно работали редактор, сериализация и сборщик мусора.
  • Вы используете UFUNCTION для событий, делегатов, RPC и Blueprint-интеграции.
  • В этой статье мы разберём роли ключевых классов фреймворка и то, когда именно движок вызывает ваши методы.

    Официальная база:

  • Gameplay Framework
  • Actors
  • Components
  • Pawn
  • Controller
  • Game Mode and Game State
  • Карта фреймворка: кто за что отвечает

    !Схема ролей основных классов и их связей

    Базовые роли

  • AActor
  • - Любой объект, существующий в мире (UWorld) и имеющий трансформ. - Контейнер для компонентов.
  • UActorComponent
  • - Единица поведения и/или данных, которая “живёт” внутри актёра. - Главный инструмент композиции.
  • APawn
  • - Актёр, которым можно управлять (человек или AI). - Обычно имеет ввод, перемещение, камеру (через компоненты).
  • AController
  • - “Мозг”, который управляет Pawn. - Бывает APlayerController (игрок) и AAIController (ИИ).
  • AGameModeBase
  • - Правила игры: как создавать игроков, что считается победой, какие классы использовать по умолчанию. - Существует только на сервере.

    Два важных “соседа”, без которых GameMode часто непонятен

  • AGameStateBase
  • - Состояние матча/мира, которое нужно клиентам. - Реплицируется на клиенты.
  • APlayerState
  • - Данные игрока, которые должны переживать смену Pawn (смерть/респаун).

    AActor: ядро мира и контейнер компонентов

    Когда использовать Actor

  • Нужен объект в мире с трансформом.
  • Нужны компоненты (визуал, коллизия, аудио, логика).
  • Нужны репликация и сетевые RPC.
  • Когда не нужен Actor

  • Вам нужны только данные без присутствия в мире: часто лучше UObject или USTRUCT.
  • Вам нужна логика “на уровне системы”: часто лучше подсистемы (Subsystems), но это отдельная тема курса.
  • UActorComponent: композиция вместо наследования

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

  • Компоненты могут тикать отдельно.
  • Их можно переиспользовать на разных акторах.
  • Их можно включать/выключать, создавать/удалять динамически.
  • Типичные компоненты:

  • UStaticMeshComponent, USkeletalMeshComponent
  • UCapsuleComponent, UBoxComponent
  • UCharacterMovementComponent
  • Ваши компоненты логики: UHealthComponent, UInteractionComponent
  • Жизненный цикл Actor: что вызывается и зачем

    Важно понимать: один и тот же C++ класс существует в нескольких “режимах” (шаблон класса, экземпляр на уровне, временные редакторские экземпляры). Ошибки часто появляются из-за неправильного выбора места для логики.

    !Временная шкала ключевых коллбеков жизненного цикла актёра

    Конструктор: AActor::AActor()

    Что делать:

  • Создавать дефолтные компоненты через CreateDefaultSubobject.
  • Настраивать значения по умолчанию.
  • Включать/выключать тик через PrimaryActorTick.
  • Чего избегать:

  • Любых операций, зависящих от мира (GetWorld() часто невалиден).
  • Обращения к PlayerController, GameMode.
  • Спауна других актёров.
  • Причина: конструктор вызывается, в том числе, при создании Class Default Object (CDO). CDO — это “эталон” дефолтных значений класса, он не является “живым актёром в мире”, но конструктор у него тоже выполняется.

    OnConstruction(const FTransform&)

    Вызывается:

  • При спауне актёра.
  • В редакторе при изменении свойств (часто многократно).
  • Типичное применение:

  • Пересобрать процедурные данные “под редактор”.
  • Обновить визуализацию на основании UPROPERTY(EditAnywhere).
  • Важно:

  • Этот код должен быть идемпотентным: повторный вызов не должен ломать состояние.
  • BeginPlay()

    Главный вход в рантайм-логику.

    Что здесь обычно делают:

  • Подписки на события.
  • Инициализация, зависящая от мира.
  • Запуск таймеров.
  • Tick(float DeltaSeconds)

    Выполняется каждый кадр, если тик включён.

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

  • По умолчанию тик выключайте и включайте только при необходимости:
  • - PrimaryActorTick.bCanEverTick = false;
  • Если тик нужен не каждый кадр, используйте:
  • - PrimaryActorTick.TickInterval = 0.1f;
  • Для периодических действий часто лучше FTimerManager, чем тик.
  • EndPlay(const EEndPlayReason::Type) и уничтожение

    EndPlay вызывается при:

  • Смене уровня.
  • Удалении актёра.
  • Завершении PIE.
  • В EndPlay обычно:

  • Отписываются от делегатов.
  • Останавливают таймеры.
  • Чистят внешние ресурсы.
  • Жизненный цикл Component: когда компонент “готов”

    У компонента есть свои ключевые моменты:

  • Создание (в конструкторе владельца через CreateDefaultSubobject или динамически через NewObject).
  • Регистрация в мире: OnRegister.
  • Инициализация: InitializeComponent.
  • Начало игры: BeginPlay.
  • Критичное различие:

  • Компонент может существовать, но быть не зарегистрированным: тогда он не участвует в сцене/физике/тиках.
  • Для динамически созданного компонента обычно нужно:
  • - MyComp->RegisterComponent(); - и при необходимости MyComp->AttachToComponent(...).

    Практический шаблон: актёр с компонентами и корректными точками инициализации

    Обратите внимание на два момента:

  • Компоненты — это UPROPERTY, чтобы редактор и GC корректно учитывали ссылки.
  • Тик выключен, потому что он не нужен “на всякий случай”.
  • Pawn: управляемый актёр и точка входа для управления

    APawn — это AActor, который может быть “занят” контроллером.

    Ключевые идеи:

  • Pawn — тело, Controller — мозг.
  • Один контроллер обычно управляет одним Pawn, но Pawn может меняться (респаун, транспорт).
  • Где обычно находится ввод

    Есть два распространённых подхода:

  • Ввод обрабатывает APlayerController, а затем вызывает команды на Pawn.
  • Ввод обрабатывает Pawn (или ACharacter), а контроллер отвечает за высокий уровень (камера, UI, переключение Pawn).
  • В классической схеме UE ввод часто биндят в Pawn через SetupPlayerInputComponent.

    Controller: PlayerController и AIController

    AController существует, чтобы отделить принятие решений от физического представления.

    Что обычно делает APlayerController:

  • Управляет вводом.
  • Создаёт/управляет UI.
  • Управляет камерой (через PlayerCameraManager).
  • Может переживать смерть Pawn (важно для мультиплеера).
  • Что обычно делает AAIController:

  • Дерево поведения, восприятие, выбор целей.
  • Команды перемещения.
  • Possession: когда контроллер “занимает” Pawn

    !Последовательность событий при назначении Pawn контроллеру

    Ключевые коллбеки (самые используемые в C++):

  • AController::Possess(APawn) и AController::OnPossess(APawn)
  • APawn::PossessedBy(AController*) (важно: это серверная точка владения)
  • APawn::UnPossessed()
  • APawn::OnRep_Controller() (клиентская реакция на репликацию контроллера)
  • Практический смысл:

  • Сервер назначает владение: Possess.
  • Клиенты узнают об этом через репликацию и реагируют в OnRep_Controller.
  • GameMode: правила игры и создание игроков

    AGameModeBase:

  • Определяет классы по умолчанию: Pawn, PlayerController, HUD, GameState, PlayerState.
  • Обрабатывает вход игроков.
  • Спавнит игроков и выбирает стартовые точки.
  • Критично:

  • GameMode существует только на сервере.
  • Клиенты получают нужную информацию через GameState и PlayerState.
  • Минимальный пример GameMode на C++

    Если вам нужно тонко контролировать спаун, чаще переопределяют:

  • ChoosePlayerStart
  • RestartPlayer
  • SpawnDefaultPawnAtTransform
  • Частые ошибки жизненного цикла и как их избегать

  • Логика “игры” в конструкторе
  • - Конструктор выполняется и для CDO, и в редакторских сценариях. Мир может быть не готов.
  • Неидемпотентный OnConstruction
  • - В редакторе этот метод вызывается много раз, и он должен устойчиво пересобирать состояние.
  • Динамический компонент создан, но не зарегистрирован
  • - Без RegisterComponent компонент не станет частью мира.
  • Попытка обращаться к GameMode на клиенте
  • - На клиенте используйте GameState/PlayerState и репликацию.

    Итоги

  • AActor — базовый объект мира и контейнер компонентов.
  • UActorComponent — основной способ строить игровую логику композиционно.
  • APawn — управляемое “тело”, которое может быть занято контроллером.
  • AController — “мозг” (игрок или AI), который может переживать смену Pawn.
  • AGameModeBase — серверные правила игры и процесс спауна игроков; клиентам его заменяет GameState.
  • Следующий логичный шаг в углублении C++-разработки под UE5 — сетевой жизненный цикл (репликация, RPC, владение, роли) и то, как он накладывается на Pawn/Controller/GameMode.

    3. Gameplay-системы на C++: подсистемы, плагины, модули и зависимости

    Gameplay-системы на C++: подсистемы, плагины, модули и зависимости

    Как эта тема связана с предыдущими статьями

    В первых материалах курса мы разобрали:

  • модель объектов UE и рефлексию (UCLASS, UPROPERTY, UFUNCTION) и то, как движок “видит” ваш код
  • Gameplay Framework и жизненный цикл (Actor, Component, Pawn, Controller, GameMode) и то, где логика должна жить
  • Следующий шаг в углублённой C++-разработке под UE5: понять, как организовать код в крупном проекте.

    Когда проект растёт, появляются типовые проблемы:

  • “Сервисная” логика не принадлежит конкретному AActor, но должна жить в правильном жизненном цикле
  • код начинает зависеть “от всего сразу” и сборка становится хрупкой
  • нужно отделять runtime-логику от editor-логики
  • хочется переиспользовать функциональность между проектами
  • Для этого в UE есть четыре ключевых инструмента:

  • модули: единицы компоновки C++ и зависимостей на уровне сборки
  • плагины: упаковка модулей (и при желании контента), которую можно подключать/выключать
  • подсистемы: стандартный способ делать “сервисы” с привязкой к жизненному циклу (Engine, GameInstance, World, LocalPlayer)
  • зависимости: правила, по которым один кусок кода может использовать другой
  • Официальная база:

  • Programming Subsystems
  • Unreal Engine Modules
  • Plugins
  • Unreal Build Tool
  • Модули: как UE компилирует и связывает C++ код

    Модуль в UE — это набор исходников, который собирается в отдельную единицу и имеет явный список зависимостей.

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

  • ускорение итераций сборки (меньше перекомпилируется)
  • контроль видимости заголовков и символов
  • разделение runtime и editor функциональности
  • возможность вынести часть кода в плагин без переписывания
  • !Схема зависимостей модулей и разделение runtime/editor

    Структура модуля в проекте

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

  • Source/MyGame/
  • Source/MyGame/Public/
  • Source/MyGame/Private/
  • Source/MyGame/MyGame.Build.cs
  • Правило по умолчанию:

  • всё, что должны включать другие модули, кладут в Public
  • внутреннюю реализацию кладут в Private
  • Build.cs: где объявляются зависимости

    Файл *.Build.cs — это место, где вы говорите Unreal Build Tool, какие модули вам нужны.

    Ключевое различие:

  • PublicDependencyModuleNames — ваши публичные заголовки могут включать заголовки этих модулей
  • PrivateDependencyModuleNames — только ваша реализация в Private использует эти модули
  • Пример:

    Практическое правило для архитектуры:

  • чем меньше PublicDependencyModuleNames, тем менее “заразным” становится ваш модуль для остальных
  • Экспорт символов и *_API

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

    Пример:

    Где это важно:

  • классы/структуры/функции, которые используются за пределами модуля
  • интерфейсы, которые реализуют внешние модули
  • Типовые ошибки зависимостей

  • циклические зависимости модулей
  • “протекание” editor модулей в runtime
  • включение тяжёлых заголовков в Public без необходимости
  • Практика, которая почти всегда помогает:

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

    Плагин — это контейнер, который может включать:

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

  • переиспользовать код между проектами
  • изолировать подсистему игры (например, инвентарь, диалоги, аналитика)
  • поддерживать включаемую функциональность (feature toggles)
  • Что лежит в .uplugin

    Файл .uplugin описывает плагин: имя, версию, список модулей и их тип.

    Важные поля на практике:

  • список Modules
  • Type модуля
  • LoadingPhase
  • Идея разделения по типам:

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

  • если вы добавляете UnrealEd, LevelEditor, AssetTools и подобные зависимости, это почти всегда Editor модуль
  • Когда плагин лучше, чем “папка в Source”

    Плагин обычно лучше, если:

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

  • код специфичен для текущей игры
  • вам не нужен механизм включения/выключения
  • Подсистемы: сервисы с правильным жизненным циклом

    Подсистемы — это стандартные классы UE для “глобальных” или “полуглобальных” сервисов, которые должны:

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

  • подсистема живёт ровно столько, сколько живёт её контекст (Engine, GameInstance, World, LocalPlayer)
  • вы получаете стандартные точки Initialize и Deinitialize
  • вы избегаете неопределённого порядка инициализации
  • !Карта видов подсистем и их “контекстов жизни”

    Виды подсистем и где их использовать

  • UEngineSubsystem
  • - живёт вместе с движком - подходит для “движковых” сервисов уровня приложения (например, глобальная телеметрия)
  • UGameInstanceSubsystem
  • - живёт вместе с UGameInstance и переживает смену уровней - подходит для мета-прогрессии, менеджеров матчмейкинга, глобальных игровых сервисов
  • UWorldSubsystem
  • - создаётся на каждый UWorld (в том числе для PIE) - подходит для систем, привязанных к миру: спавн-менеджер, менеджер зон, менеджер погоды
  • ULocalPlayerSubsystem
  • - создаётся на игрока на локальной машине - подходит для локальных настроек, инпута, UI-сервисов, которые должны быть на уровне игрока
  • UEditorSubsystem
  • - существует только в редакторе - подходит для инструментов, автоматизаций и editor workflow

    Минимальный пример UGameInstanceSubsystem

    Задача: сделать сервис инвентаря, который живёт между уровнями и доступен из Actor, Controller или UI.

    InventorySubsystem.h:

    InventorySubsystem.cpp:

    Получение подсистемы из актёра:

    Здесь важно, что подсистема:

  • не привязана к конкретному AActor
  • переживает смену уровней, потому что живёт в UGameInstance
  • остаётся частью модели объектов UE, поэтому можно использовать UPROPERTY и UFUNCTION
  • Управление созданием: ShouldCreateSubsystem

    Иногда подсистема нужна не всегда.

    Типичные сценарии:

  • включать только в определённом режиме игры
  • отключать на dedicated server
  • отключать в editor preview
  • Для этого переопределяют ShouldCreateSubsystem.

    Практический смысл: вы делаете подсистему “условной”, но по-прежнему встроенной в стандартный жизненный цикл.

    Как выбрать место для логики: Actor, Component, Subsystem, Module

    Ниже — краткая таблица выбора, которая помогает избежать хаоса в архитектуре.

    | Инструмент | Жизненный цикл | Для чего подходит лучше всего | Типичная ошибка | |---|---|---|---| | AActor | объект в мире | сущности с трансформом, репликацией, коллизией | делать “менеджер мира” актёром без причины | | UActorComponent | часть актёра | переиспользуемое поведение для разных актёров | превращать компонент в “глобальный сервис” | | *Subsystem | зависит от контекста | сервисы уровня Engine/GameInstance/World/LocalPlayer | хранить всё в GameInstance без разделения | | Module | единица сборки | границы кода и зависимостей | складывать весь проект в один модуль | | Plugin | упаковка модулей | переиспользование, изоляция, включаемость | смешивать runtime и editor в одном runtime модуле |

    Практический рецепт: как спроектировать игровую систему “правильно”

    Ниже — типовой процесс для новой gameplay-системы (например, инвентарь, квесты, диалоги, сезоны).

  • Выберите контекст жизни.
  • Реализуйте сервис как подсистему в этом контексте.
  • Определите публичный C++ API (минимальный набор типов и функций).
  • Разделите код по модулям.
  • Если систему нужно переиспользовать, упакуйте модули в плагин.
  • Мини-шпаргалка по выбору контекста:

  • состояние должно переживать уровни: UGameInstanceSubsystem
  • состояние строго “на уровень/мир”: UWorldSubsystem
  • состояние строго “на локального игрока”: ULocalPlayerSubsystem
  • Чеклист зависимостей для крупных проектов

  • держите PublicDependencyModuleNames минимальным
  • не добавляйте editor зависимости в runtime модули
  • не включайте тяжёлые заголовки в публичные .h, если можно обойтись forward declaration
  • для переиспользуемых систем делайте отдельный модуль или плагин, а не “общую папку утилит”
  • Итоги

  • Модуль — основная единица сборки и управления зависимостями в UE
  • Плагин — контейнер для модулей (и иногда контента), удобен для переиспользования и изоляции
  • Подсистемы — лучший стандартный способ создавать “сервисы” с корректным жизненным циклом
  • Архитектурно сильные проекты отделяют:
  • - runtime от editor - world-логику от gameinstance-логики - публичные API от приватной реализации

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

    Ссылки

  • Programming Subsystems
  • Unreal Engine Modules
  • Plugins
  • Unreal Build Tool
  • 4. Взаимодействие с Blueprint: API-дизайн, события, интерфейсы и экспонирование данных

    Взаимодействие с Blueprint: API-дизайн, события, интерфейсы и экспонирование данных

    Зачем C++ коду “правильно” общаться с Blueprint

    Blueprint в UE5 почти никогда не заменяет C++ в серьёзном проекте: чаще он служит слоем конфигурации и интеграции.

    Из предыдущих статей у нас уже есть база:

  • объектная модель и рефлексия (UCLASS, UPROPERTY, UFUNCTION) определяют, что вообще может “увидеть” редактор и Blueprint
  • Gameplay Framework объясняет, где жить логике (Actor, Component, Subsystem)
  • модули и плагины задают границы кода и зависимостей
  • Теперь задача — спроектировать контракт между C++ и Blueprint так, чтобы:

  • C++ оставался источником правил и инвариантов
  • Blueprint получал удобные точки расширения
  • API было стабильным для команды и контента
  • !Слои ответственности между C++ и Blueprint и направление зависимостей

    Принципы API-дизайна для Blueprint

    Дизайн через “узкий контракт”

    Blueprint-публичное API лучше делать узким и намеренно ограниченным.

  • Экспортируйте только то, что нужно дизайнерам и UI
  • Скрывайте внутреннее состояние и промежуточные шаги
  • Делайте “команды” атомарными, а не выдавайте наружу всю механику
  • Практический эффект: меньше графов, меньше случайных зависимостей, проще рефакторить C++.

    Разделяйте “данные” и “поведение”

  • Данные удобно экспонировать как UPROPERTY и USTRUCT
  • Поведение удобнее отдавать через UFUNCTION и события
  • Когда дизайнеру нужно “подкрутить значения”, это почти всегда EditDefaultsOnly/EditAnywhere. Когда нужно “вклиниться в процесс” — это BlueprintNativeEvent, делегат или интерфейс.

    Будьте аккуратны с типами

    Blueprint-дружественные типы:

  • bool, int32, float, FName, FString, FText
  • FVector, FRotator, FTransform
  • UObject/AActor ссылки (через UPROPERTY)
  • TArray и TMap (умеренно)
  • UENUM(BlueprintType) и USTRUCT(BlueprintType)
  • Нежелательные для BP-контракта:

  • “внутренние” движковые типы, требующие контекста владения
  • сырые указатели без UPROPERTY
  • сложные шаблоны и тонкие C++ абстракции, которые в BP не выражаются
  • Официальная база:

  • Exposing C++ to Blueprints
  • Blueprints Visual Scripting
  • Экспонирование данных: UPROPERTY как “контентный контракт”

    Основные правила выбора спецификаторов

  • EditDefaultsOnly для параметров класса (баланс, настройки архетипа)
  • EditAnywhere для параметров экземпляра на уровне
  • VisibleAnywhere для диагностических/вычисляемых значений
  • BlueprintReadOnly если BP не должен менять значение напрямую
  • BlueprintReadWrite только если это действительно нужно
  • Пример: параметры взаимодействия в компоненте.

    Метаданные для удобства редактора

    Метаданные не меняют логику, но резко повышают “качество жизни” при работе с контентом.

  • ClampMin и ClampMax ограничивают ввод
  • UIMin и UIMax задают диапазон слайдера
  • ToolTip документирует поле
  • AdvancedDisplay прячет редкие настройки
  • Частая ошибка: давать BP писать во всё подряд

    Если Blueprint меняет состояние напрямую через BlueprintReadWrite, вы теряете точку контроля. Часто лучше:

  • сделать поле BlueprintReadOnly
  • дать UFUNCTION(BlueprintCallable) которая валидирует и применяет изменения
  • Экспонирование функций: UFUNCTION как “операции” системы

    BlueprintCallable vs BlueprintPure

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

    Стабильность узлов Blueprint

    Переименование UFUNCTION и UPROPERTY влияет на узлы и графы. Чтобы уменьшить боль рефакторинга:

  • используйте DisplayName для “красивого” имени в BP, сохраняя стабильное C++ имя
  • не меняйте сигнатуры без необходимости
  • События: как C++ сообщает Blueprint, что “что-то произошло”

    В UE есть три основных паттерна событий для BP-интеграции:

  • Blueprint-события через BlueprintImplementableEvent/BlueprintNativeEvent
  • делегаты (особенно динамические multicast) и BlueprintAssignable
  • интерфейсы, когда событие должно прийти в разные типы объектов
  • BlueprintImplementableEvent

    Подходит, когда:

  • реализация должна быть только в BP
  • C++ просто “сигналит” о факте
  • Минус: нет C++ реализации по умолчанию.

    BlueprintNativeEvent

    Подходит, когда:

  • по умолчанию есть C++ поведение
  • Blueprint может переопределить, если нужно
  • В заголовке:

    В .cpp реализуется вариант с суффиксом _Implementation:

    Практика для командной разработки: критические правила держите в C++, а в BP отдавайте визуальные/звуковые эффекты и вариативные сценарии.

    Делегаты и BlueprintAssignable

    Делегаты — это способ подписки на события. Для Blueprint обычно используют динамические multicast делегаты.

    Где это полезно:

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

    Интерфейсы: единый контракт для разных актёров и компонентов

    Интерфейс нужен, когда вы хотите вызвать действие на объекте, не зная его конкретный класс.

    C++ интерфейсы UE: UINTERFACE и IInterface

    Пример интерфейса взаимодействия.

    Вызов интерфейса из кода:

    Здесь важны два момента:

  • проверка ImplementsInterface защищает от вызова “в никуда”
  • Execute_... корректно вызывает реализацию, даже если она определена в Blueprint
  • Официальная база:

  • Interfaces
  • Когда интерфейс лучше, чем каст к классу

    Интерфейс предпочтительнее, если:

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

  • вы точно знаете конкретный тип
  • вам нужен доступ к большому набору специфичных данных
  • Практический шаблон API: C++ правило, BP расширение

    Рассмотрим мини-систему взаимодействия, где:

  • C++ гарантирует проверки дистанции и доступности
  • Blueprint расширяет реакцию (анимации, VFX, UI)
  • Компонент взаимодействия

    В реализации TryInteract вы делаете строгую C++-валидацию (трейс, дистанция, доступность), а затем либо вызываете интерфейс Interact, либо сигналите в BP об ошибке через OnInteractFailedBP.

    Объекты-цели через интерфейс

    Дверь и NPC могут быть разными классами, но оба реализуют IInteractable.

  • дверь в C++ может открываться логикой состояния
  • NPC в BP может запускать диалог
  • Так вы избегаете “зоопарка кастов” и не превращаете взаимодействие в зависимость от конкретных классов.

    !Последовательность вызовов при взаимодействии через интерфейс

    Blueprint Function Library и статические утилиты

    Иногда нужно отдать в Blueprint чистые утилиты, не привязанные к объекту.

    Для этого есть UBlueprintFunctionLibrary.

    Рекомендации:

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

  • Blueprint Function Libraries
  • Как это связывается с модулями, подсистемами и жизненным циклом

    Где размещать BP-ориентированный API

  • Если это логика конкретной сущности в мире: Actor или Component
  • Если это глобальный сервис: UGameInstanceSubsystem/UWorldSubsystem
  • Если это чистые функции: UBlueprintFunctionLibrary
  • Граница runtime и editor

    BP-API почти всегда относится к runtime, но редакторские инструменты для удобного создания контента должны уходить в отдельный Editor модуль или плагин.

    Это продолжает правила из статьи про модули:

  • не добавляйте editor зависимости в runtime модуль
  • не заставляйте игру тянуть инструменты
  • Типовые ошибки и как их избегать

  • Слишком много BlueprintReadWrite на внутренних полях
  • Логика правил игры “размазана” по BP и её нельзя валидировать
  • Отсутствие стабильного контракта и постоянные изменения сигнатур
  • Касты на конкретные классы вместо интерфейсов
  • Делегаты без отписок в корректной точке жизненного цикла (актуально для UI и подписчиков)
  • Итоги

  • Удачный BP-контракт — это узкий, стабильный API, где C++ держит правила, а BP расширяет поведение
  • UPROPERTY формирует удобный слой данных для контента и редактора
  • UFUNCTION задаёт операции, а события реализуются через BlueprintImplementableEvent, BlueprintNativeEvent и делегаты
  • Интерфейсы позволяют взаимодействовать с разными типами объектов без каскада кастов
  • Архитектурно чисто располагать BP-API там, где живёт ответственность: Actor/Component/Subsystem/FunctionLibrary
  • Ссылки

  • Exposing C++ to Blueprints
  • Blueprints Visual Scripting
  • Interfaces
  • Blueprint Function Libraries
  • 5. Асинхронность и задачи: Timers, Async Tasks, Latent Actions, UE::Tasks

    Асинхронность и задачи: Timers, Async Tasks, Latent Actions, UE::Tasks

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

    В прошлых материалах курса мы зафиксировали три опоры:

  • Модель объектов и рефлексия UE (UCLASS, UPROPERTY, UFUNCTION) определяют, что движок видит и как управляет временем жизни объектов.
  • Gameplay Framework и жизненный цикл (BeginPlay, Tick, EndPlay, владение Pawn/Controller) определяют, когда ваш код имеет право взаимодействовать с миром.
  • Модули/плагины/подсистемы определяют, где жить сервисной логике и как не устроить архитектурный хаос.
  • Асинхронность добавляет четвертую опору: как выполнять работу без блокировки игрового потока и как безопасно вернуть результат в игру.

    В UE5 есть несколько разных механизмов, которые часто смешивают:

  • FTimerManager и таймеры
  • асинхронные задачи на фоновых потоках (Async Tasks в широком смысле)
  • Latent Actions для “ожидающих” операций (часто ориентированы на Blueprint)
  • UE::Tasks как современная система мелких задач с зависимостями
  • Цель статьи: научиться выбирать инструмент по задаче и не ломать потокобезопасность, время жизни UObject и жизненный цикл мира.

    !Схема того, где выполняется код и как безопасно возвращать результат в игру

    Ментальная модель: что можно делать не на Game Thread

    Для геймплей-кода важно простое правило:

  • Мир (UWorld), актёры (AActor), компоненты (UActorComponent), большинство API физики/сцены/анимации предполагают доступ только с Game Thread.
  • Фоновые потоки хорошо подходят для:

  • чистых вычислений (математика, поиск пути в вашей кастомной структуре, генерация данных)
  • парсинга/декодирования
  • подготовки данных для последующего применения на Game Thread
  • Если вам нужно изменить актёра, компонент, UPROPERTY, спавнить/удалять актёры, трогать компоненты сцены, вы почти всегда должны вернуться на Game Thread.

    Timers: FTimerManager как “планировщик” на Game Thread

    Когда таймеры лучше тика

    Таймеры в UE работают через FTimerManager и исполняют коллбеки на Game Thread.

    Таймеры выбирают, когда:

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

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

  • Gameplay Timers
  • Базовый пример: одноразовая задержка

    Повторяющийся таймер и корректная остановка

    Практическая причина чистки: таймер может попытаться вызвать метод после того, как владелец начал уничтожаться. В большинстве случаев UE “защитит” вызов, если таймер привязан к UObject, но в проектах с лямбдами и сложными графами жизненного цикла лучше держать привычку к явной остановке.

    Таймеры и лямбды: осторожно со временем жизни

    Можно ставить таймер на лямбду, но нельзя бездумно захватывать this.

    Рекомендуемый паттерн: захватывать TWeakObjectPtr и проверять валидность.

    Где брать FTimerManager в подсистемах

  • В UWorldSubsystem удобно использовать GetWorld()->GetTimerManager().
  • В UGameInstanceSubsystem таймеры обычно ставят через текущий UWorld (например, через UGameInstance::GetWorld()), но нужно понимать, что мир может меняться при загрузке уровней.
  • Если задача должна переживать смену уровней, таймеры часто не лучший инструмент: лучше хранить состояние в GameInstanceSubsystem, а “планирование” делать на уровне мира заново после загрузки.

    Async Tasks: фоновые вычисления и возврат результата на Game Thread

    Под Async Tasks в UE-практике часто подразумевают несколько разных подходов. Важно не путать их:

  • “Запустить что-то в фоне”
  • “Запустить много мелких задач параллельно”
  • “Сделать долгоживущий поток/воркер”
  • Минимально безопасный шаблон: вычислили в фоне, применили в игре

    Идея:

  • фоновой код работает только с вашими структурами/копиями данных
  • результат возвращается на Game Thread, где вы уже трогаете UObject и мир
  • Ключевые мысли:

  • фоновые вычисления должны быть потокобезопасными
  • любые изменения актёров и компонентов делаются только на Game Thread
  • TWeakObjectPtr защищает от обращения к уничтоженному объекту
  • Когда Async(...) недостаточно

    Async(...) хорош для коротких задач, но в более “инженерных” сценариях выбирают другие инструменты:

  • FAsyncTask и FNonAbandonableTask для задач с более явной моделью владения и возможностью ждать завершения
  • FRunnable и собственный поток для долгоживущих воркеров (редко нужно именно для геймплея)
  • В углублённой геймплей-архитектуре чаще всего хватает: Async(...) или UE::Tasks.

    Типичная ошибка: “я просто прочитаю UPROPERTY в фоне”

    Даже чтение полей UObject из фонового потока может быть проблемой:

  • поля могут меняться на Game Thread
  • объект может уничтожаться
  • часть движковых структур не потокобезопасна
  • Правильная техника: на Game Thread собрать снимок данных в обычные структуры (USTRUCT или plain C++), затем работать с копией.

    Latent Actions: “ожидание” как часть выполнения, особенно для Blueprint

    Что такое Latent Action

    Latent Action в UE это механизм, который позволяет “начать операцию сейчас, а закончить позже”, при этом вызывающий код (часто Blueprint) выглядит как последовательный.

    Классические примеры в Blueprint:

  • Delay
  • MoveComponentTo
  • асинхронные узлы, которые продолжают выполнение после завершения работы
  • Официальная документация:

  • Latent Actions
  • Когда Latent Action лучше таймера

    Latent Action выбирают, когда:

  • вы делаете Blueprint-узел с “продолжением” выполнения
  • вам нужно встроиться в модель исполнения Blueprint графа
  • вы хотите управлять отменой/завершением через FLatentActionManager
  • Если у вас чисто C++ геймплей и нужна задержка или интервал, обычно проще FTimerManager.

    Скелет собственного latent action

    Ниже упрощённый пример: делаем функцию WaitAndCall, которую можно вызвать из Blueprint как latent.

    Ключевые элементы, которые нужно понимать:

  • FPendingLatentAction живёт в менеджере latent actions и обновляется движком
  • UpdateOperation вызывается на Game Thread
  • FLatentActionInfo содержит, куда и как “вернуть управление” в Blueprint
  • UE::Tasks: современная система задач для параллелизма

    Зачем UE::Tasks, если есть Async(...)

    UE::Tasks удобен, когда у вас много небольших единиц работы и вам нужны:

  • зависимость задач друг от друга
  • продолжения (continuations)
  • контроль группировки и ожидания
  • Концептуально это “конструктор графа задач”, который помогает распараллеливать CPU-работу без ручного управления потоками.

    Базовая идея использования

    Паттерн такой же, как и в Async(...):

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

    Практический вывод для архитектуры:

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

    Независимо от механизма фоновой работы, вам нужен единый стиль:

  • отдельный метод Apply... или Commit..., который вызывается только на Game Thread
  • передача результата как готовых plain-данных
  • Это помогает держать инварианты C++-ядра, о которых мы говорили в статье про API-дизайн для Blueprint.

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

    | Инструмент | Где выполняется | Лучший сценарий | Что часто путают | |---|---|---|---| | FTimerManager | Game Thread | задержки, интервалы, планирование без Tick | пытаются использовать для тяжёлых вычислений | | Async(...) | Worker Threads | одна/несколько коротких фоновых работ | трогают UObject в фоне | | Latent Actions | Game Thread | latent Blueprint-узлы, продолжение графа | используют как замену потокам | | UE::Tasks | Worker Threads | много мелких задач, зависимости, параллелизм CPU | ожидают, что это “магически потокобезопасно” для UObject |

    Типовые ошибки и защитные паттерны

    Ошибка: фоновой код пишет в актёра напрямую

    Правильно:

  • в фоне считать и подготовить данные
  • на Game Thread применить данные к компонентам/актёрам
  • Ошибка: захват this в лямбду без контроля времени жизни

    Правильно:

  • захватывать TWeakObjectPtr и проверять IsValid()
  • Ошибка: “задача пережила мир”

    Это часто случается при:

  • смене уровня
  • остановке PIE
  • уничтожении актёра
  • Правильно:

  • хранить состояние в правильном контексте (например, UGameInstanceSubsystem, если нужно пережить смену уровня)
  • отменять/игнорировать результаты, если контекст умер
  • Ошибка: таймеры/latent actions не очищаются

    Правильно:

  • очищать таймеры в EndPlay/Deinitialize
  • делать идемпотентные операции отмены (вызов ClearTimer безопасно повторять)
  • Итоги

  • Таймеры (FTimerManager) это планирование на Game Thread и замена лишнему Tick.
  • Асинхронные вычисления нужно делать на фоновых потоках, но изменения мира выполнять только на Game Thread.
  • Latent Actions полезны, когда вы проектируете “ожидающие” Blueprint-узлы и хотите продолжение выполнения графа.
  • UE::Tasks удобен для распараллеливания большого количества небольших вычислительных задач с зависимостями.
  • В следующих материалах курса эта тема обычно связывается с практиками масштабирования: асинхронная загрузка данных, стриминг, фоновые пайплайны подготовки, а также с сетевым кодом (где особенно важно не блокировать Game Thread и не нарушать владение/роль объектов).

    6. Оптимизация и память: GC, владение, профилирование, Gameplay-трейсы и логирование

    Оптимизация и память: GC, владение, профилирование, Gameplay-трейсы и логирование

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

    В прошлых материалах курса мы построили базу:

  • В статье про модель объектов UE мы разобрали, что UCLASS/UPROPERTY/UFUNCTION подключают код к рефлексии, сериализации и сборщику мусора.
  • В статье про жизненный цикл зафиксировали, где безопасно делать инициализацию (BeginPlay) и где нужно освобождать ресурсы (EndPlay).
  • В статье про подсистемы и модули определили, где жить “сервисному коду” и как отделять ответственность.
  • В статье про асинхронность добавили правило: фоновые потоки не трогают UObject, а результаты возвращаются на Game Thread.
  • Оптимизация и память — это “сквозная дисциплина”, которая пронизывает все темы выше:

  • вы не сможете оптимизировать игру, если не понимаете владение и время жизни UObject
  • вы не сможете ускорить код, если не умеете измерять (профилирование)
  • вы не сможете быстро локализовать проблему, если не умеете ставить трейсы и писать лог с правильной гранулярностью
  • !Граф владения и то, какие ссылки видит GC

    Сборщик мусора UE: что он делает и чего он не делает

    UE использует GC для UObject-иерархии. Это не общий GC “для всего C++”.

  • GC управляет временем жизни UObject (включая AActor, UActorComponent, подсистемы и многие ассеты).
  • GC не управляет памятью обычных C++ объектов, выделенных через new/delete, и не освобождает ресурсы ОС автоматически.
  • Официальная документация:

  • Garbage Collection
  • Object Pointers
  • Ментальная модель GC: граф достижимости

    GC в UE работает по принципу достижимости:

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

    Почему UPROPERTY — это не “для редактора”, а для корректной жизни объектов

    Если вы храните ссылку на UObject в поле без UPROPERTY, GC может не увидеть эту связь и освободить объект.

    Плохой пример:

    Хороший пример:

    Когда UPROPERTY недостаточно

    Есть частый сценарий в “системном” C++:

  • вы храните ссылки на UObject не внутри UObject, а внутри plain C++ структуры/менеджера
  • Тогда вы должны либо:

  • Перенести владение в UObject (компонент/подсистема), где можно использовать UPROPERTY.
  • Или реализовать FGCObject, чтобы вручную “сообщить” GC о ссылках.
  • Минимальный скелет FGCObject:

    Владение и типы ссылок: что выбирать в UE5

    UE5 предлагает несколько видов “ссылочности”. Их важно различать, потому что они влияют и на GC, и на загрузку ассетов, и на безопасность.

    Краткая таблица выбора

    | Тип ссылки | Удерживает объект в памяти | Видна GC | Типичный сценарий | |---|---:|---:|---| | TObjectPtr<T>UPROPERTY) | Да | Да | Поля компонентов/актёров/подсистем | | TWeakObjectPtr<T> | Нет | Нет (но безопасно проверяется) | Кэш “на всякий случай”, подписки, асинхронные коллбеки | | TSoftObjectPtr<T> / TSoftClassPtr<T> | Нет (хранит путь) | Не удерживает | Ссылки на ассеты, которые можно подгружать по требованию | | TStrongObjectPtr<T> | Да | Да (через внутренний механизм) | Временное удержание UObject вне UPROPERTY (например, в системном коде) |

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

  • Если поле живёт внутри UObject и это “нормальная” связь владения или композиции, используйте UPROPERTY() + TObjectPtr.
  • Если вы хотите ссылаться на объект, но не влиять на время жизни, используйте TWeakObjectPtr.
  • Если вы хотите ссылаться на ассет без его загрузки, используйте soft-ссылки (TSoftObjectPtr).
  • Избегайте “лечения” проблем AddToRoot, пока вы не уверены, что это действительно нужно.
  • AddToRoot: мощно, но опасно

    AddToRoot помещает объект в Root Set, и GC перестаёт его собирать.

    Это полезно в редких случаях (например, для глобальных объектов, которые должны жить весь runtime), но типовые риски:

  • утечки: забыли RemoveFromRoot
  • некорректный жизненный цикл: объект “переживает” мир/PIE
  • В геймплей-коде чаще правильнее:

  • хранить владение через UPROPERTY в подсистеме нужного контекста (GameInstanceSubsystem или WorldSubsystem)
  • освобождать связи в Deinitialize/EndPlay
  • Память и аллокации: как не сделать GC и аллокатор “врагом”

    GC и аллокации часто встречаются в одном профиле: сначала вы делаете много мелких выделений, затем увеличиваете давление на сборку мусора и кеши CPU.

    Основные источники проблем в gameplay-коде

  • аллокации в Tick
  • создание временных TArray/TMap каждый кадр
  • частое спавнение/удаление AActor вместо пуллинга
  • создание/удаление UObject в больших количествах без контроля
  • Практические паттерны снижения аллокаций

  • Reserve для TArray, если вы знаете примерный объём
  • переиспользование контейнеров вместо создания новых
  • аккуратное использование строк (FString) в hot-path: избегайте конкатенаций в циклах
  • вынесение расчётов из Tick в таймеры или события
  • Пример: подготовка массива без лишних реаллокаций:

    Профилирование: дисциплина “сначала измерь, потом оптимизируй”

    Оптимизация в UE без профилирования обычно превращается в серию случайных правок.

    Базовый процесс профилирования

  • Сформулируйте симптом (просадка FPS, лаг при открытии инвентаря, фриз при спавне).
  • Определите тип проблемы:
  • - CPU, GPU, IO, память, сеть
  • Снимите профиль и найдите горячую точку.
  • Добавьте точечные метки (статы, трейсы), чтобы сузить причину.
  • Оптимизируйте.
  • Повторно снимите профиль и подтвердите эффект.
  • Unreal Insights: основной инструмент для “почему тормозит”

    Unreal Insights — ключевой профайлер UE5 для анализа:

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

  • Unreal Insights
  • !Пример того, как читать таймлайн и находить “спайки”

    Stats System: быстрые счётчики и тайминги прямо в движке

    Stats System — способ включить счётчики/тайминги, которые видны через stat команды и частично попадают в профайлы.

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

  • Stats System
  • Минимальный пример цикла-статы:

    CSV Profiler: когда нужны метрики “по времени” и сравнение билдов

    CSV Profiler удобен, когда вы хотите:

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

  • CSV Profiler
  • Gameplay-трейсы: как оставлять “следы” в профиле и в рантайме

    Под gameplay-трейсами в контексте геймплей-кода обычно понимают: “события и маркеры, которые помогают понять поведение системы во времени”.

    На практике это несколько уровней инструментов:

  • временные зоны CPU (попадают в профайлер)
  • события/закладки (чтобы отмечать моменты: “инвентарь открылся”, “матч начался”)
  • счётчики (сколько предметов, сколько актёров, сколько запросов)
  • CPU scope метки для профайлера

    Часто достаточно добавить scope-метку вокруг подозрительного участка.

    Если вы уже используете Stats System, иногда удобнее SCOPE_CYCLE_COUNTER или QUICK_SCOPE_CYCLE_COUNTER.

    “Закладки” важны для корреляции

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

    Паттерн: при старте сценария пишем маркер, затем по таймлайну ищем, что происходило вокруг.

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

    Логирование в UE — это не только UE_LOG, но и дисциплина:

  • категории
  • уровни подробности
  • объём
  • влияние на производительность
  • Документация:

  • Logging in Unreal Engine
  • Log категории: делайте лог управляемым

    Определите категорию в .cpp:

    Пишите сообщения с уровнем:

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

  • Verbose и VeryVerbose используйте для “болтливой” диагностики, которую можно включать точечно.
  • Warning для ситуаций, которые не ломают игру, но требуют внимания.
  • Error для реально нештатных ситуаций.
  • check, ensure и “контракт кода”

  • check(...) — жёсткая проверка: упадёт, если условие ложно (обычно в non-shipping конфигурациях).
  • ensure(...) — мягкая проверка: сообщает об ошибке, но старается продолжить выполнение.
  • Паттерн для геймплей-инвариантов:

    Типовые ошибки логирования

  • лог в Tick без ограничения частоты
  • конвертация строк и тяжёлая подготовка текста “на каждый кадр”
  • использование Error там, где это ожидаемая ветка геймплея
  • Паттерны защиты:

  • логируйте события, а не “состояние каждую миллисекунду”
  • добавляйте “rate limit” через таймер/счётчик
  • используйте UE_CLOG для условного логирования
  • Практический чеклист: что делать, когда вы видите фриз или утечку

    Если фриз

  • Снимите Unreal Insights и найдите спайк кадра.
  • Определите, где время:
  • - GameThread - RenderThread - задачи
  • Добавьте CPU scope метки вокруг подозрительных C++ участков.
  • Уберите:
  • - лишние аллокации - работу из Tick (переведите на таймер/событие) - синхронные загрузки ассетов

    Если “кажется, течёт память”

  • Убедитесь, что это не кеш и не рост ассетов по сценарию.
  • Проверьте удержание UObject:
  • - нет ли лишних сильных ссылок UPROPERTY - нет ли AddToRoot - нет ли подписок на делегаты, которые удерживают объекты (и вы не отписываетесь)
  • Проверьте жизненный цикл:
  • - чистите таймеры/подписки в EndPlay (акторы/компоненты) - чистите в Deinitialize (подсистемы)

    Итоги

  • GC UE управляет временем жизни UObject через граф достижимости, и видит только корректно оформленные ссылки.
  • UPROPERTY + TObjectPtr — базовый инструмент корректного владения в gameplay-коде.
  • TWeakObjectPtr и soft-ссылки помогают избежать лишнего удержания объектов и ассетов.
  • Оптимизация начинается с профилирования: Unreal Insights для таймлайна, Stats System и CSV Profiler для метрик.
  • Gameplay-трейсы и логирование должны быть управляемыми: категории, уровни, scope-метки, условный лог.
  • Следующая практическая ступень после этой статьи обычно — закрепление на реальном кейсе: оптимизация конкретной gameplay-системы (инвентарь, взаимодействие, спавн-менеджер) с замерами “до/после” и добавлением трейсов, чтобы проблема была видна не только вам, но и всей команде.

    7. Сетевая логика: репликация, RPC, предсказание, Authority и Dedicated Server

    Сетевая логика: репликация, RPC, предсказание, Authority и Dedicated Server

    Как эта тема связана с предыдущими статьями курса

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

  • Модель объектов UE и рефлексия: реплицируются только поля и вызовы, которые движок “видит” (через UPROPERTY и UFUNCTION).
  • Gameplay Framework и жизненный цикл: владение Pawn/Controller, спавн через GameMode, инициализация в BeginPlay и очистка в EndPlay влияют на то, что и когда успевает реплицироваться.
  • Подсистемы/модули: сетевые системы часто живут в подсистемах (например, матч, сессии, инвентарь) и должны иметь правильные границы зависимостей.
  • Асинхронность: сетевые коллбеки приходят в рамках сетевого тика, и вы не должны блокировать Game Thread тяжёлыми вычислениями.
  • Оптимизация и память: репликация может стать главным источником CPU и bandwidth нагрузки, поэтому нужны профилирование, условная репликация, dormancy и аккуратный лог.
  • Официальная документация для опоры:

  • Networking and Multiplayer
  • Actor Replication
  • Remote Procedure Calls
  • Setting Up Dedicated Servers
  • Базовая модель мультиплеера UE: сервер авторитетен

    В классической сетевой модели Unreal:

  • Сервер хранит “истину” игрового мира (позиции, здоровье, инвентарь, состояние матча).
  • Клиенты получают состояние от сервера (репликация) и отправляют намерения (input/команды) на сервер (обычно через Server RPC).
  • Термины, которые будут использоваться дальше:

  • Authority: “кто имеет право окончательно решать”. В типичном multiplayer — это сервер.
  • Ownership (владение): какой клиент “владеет” конкретным актёром (обычно своим Pawn/Character). Владение важно для того, какие RPC вообще разрешены и кому отправлять Client RPC.
  • Репликация: механизм, когда сервер автоматически отправляет клиентам значения помеченных свойств и некоторые события.
  • RPC: удалённый вызов функции (Server/Client/NetMulticast).
  • !Схема направления данных: команды вверх на сервер, состояние вниз на клиенты

    NetMode: Listen Server, Dedicated Server и Standalone

    UE запускает игру в разных сетевых режимах. В коде это влияет на ветвления и доступность некоторых объектов.

    Режимы

  • Standalone: нет сети, всё локально.
  • Listen Server: один из игроков является сервером и одновременно играет.
  • Dedicated Server: отдельный серверный процесс без локального игрока и без рендера.
  • Почему Dedicated Server требует дисциплины

    На dedicated server:

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

    Authority и роли: как понимать, где вы исполняетесь

    Главная проверка: HasAuthority()

    Для актёров на сервере HasAuthority() обычно возвращает true. На клиентах — false.

    Роль и владение

    Ключевая идея: не каждый клиент имеет право отправлять команды для любого актёра.

  • Обычно клиент отправляет Server RPC только для актёров, которыми он владеет.
  • Владение обычно выстраивается через PlayerController и его Pawn.
  • Практический вывод для архитектуры:

  • Команды игрока (стрельба, использование, переключение оружия) обычно отправляются на сервер из Pawn/Character или PlayerController.
  • “Мировые” актёры (двери, лут, цели) чаще обрабатывают взаимодействие на сервере, но инициируется это командой владельца.
  • Репликация: свойства, условия, OnRep и жизненный цикл

    Репликация — это когда сервер считает актёр “значимым” для клиента и отправляет обновления свойств.

    Минимальные условия, чтобы актёр реплицировался

  • Актёр должен включить репликацию: bReplicates = true.
  • Поля должны быть помечены как UPROPERTY(Replicated) или UPROPERTY(ReplicatedUsing=...).
  • Нужно перечислить поля в GetLifetimeReplicatedProps.
  • Пример: репликация Health с клиентским коллбеком

    Replicated vs ReplicatedUsing

  • Replicated просто синхронизирует значение.
  • ReplicatedUsing дополнительно вызывает OnRep_... на клиенте при изменении, чтобы вы могли запустить реакцию.
  • Практическое правило:

  • Если изменение требует клиентской реакции (UI/VFX/SFX/анимация) — часто выгоднее ReplicatedUsing, чем ручные проверки в Tick.
  • Условия репликации (когда отправлять)

    UE позволяет ограничивать, кому и когда отправлять свойство.

  • Например, не отправлять чувствительные данные всем, а только владельцу.
  • Или отправлять какие-то поля только в определённых ситуациях.
  • На уровне UE это делается через механизмы условий репликации (например, варианты DOREPLIFETIME_CONDITION). Полезный принцип здесь архитектурный:

  • Разделяйте “публичное состояние мира” и “приватное состояние игрока”.
  • Пример приватного состояния:

  • “сколько патронов в запасе”, если вы не хотите, чтобы другие игроки это знали
  • Пример публичного состояния:

  • текущий HP персонажа, если оно должно быть видно другим
  • Репликация компонентов

    Если у вас логика вынесена в компонент, это нормально: компоненты тоже могут реплицироваться.

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

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

  • Актёр реплицируется всегда.
  • Компонент включает репликацию и реплицирует свои UPROPERTY.
  • RPC: Server, Client, NetMulticast и правила безопасности

    RPC — это “вызвать функцию на другой стороне сети”. В UE есть три основных направления.

    Виды RPC

    | Тип RPC | Откуда вызывается | Где исполняется | Типичный смысл | |---|---|---|---| | Server | клиент или сервер | сервер | клиент просит сервер выполнить действие | | Client | сервер | конкретный клиент-владелец | сервер сообщает владельцу что-то приватное | | NetMulticast | сервер | сервер и все клиенты | сервер транслирует событие всем |

    Ключевое правило: не используйте NetMulticast для логики “истины”. Он хорош для эффектов и сигналов, но состояние должно жить в реплицируемых свойствах.

    !Схема направлений RPC и репликации

    Пример: клиент просит выстрелить (Server RPC), сервер применяет урон и реплицирует результат

    Reliable vs Unreliable

  • Reliable: доставить гарантированно, но при перегрузке может увеличивать задержки из-за очереди.
  • Unreliable: может потеряться, зато не раздувает очередь гарантированной доставки.
  • Практический выбор:

  • События, которые нельзя терять (покупка предмета, смена режима, подтверждение действия) — чаще Reliable.
  • Частые события, которые устаревают (косметика, некоторые VFX) — часто Unreliable.
  • Типичная ошибка: “клиент говорит серверу итог”

    Неправильно:

  • клиент отправляет Server RPC: “я попал, сними 30 HP вот этому игроку”
  • Правильно:

  • клиент отправляет намерение: “я нажал Fire”
  • сервер сам делает проверку: трассировку/попадание/урон
  • Это базовая защита от читов и рассинхронизаций.

    Relevancy и Dormancy: как не утонуть в сетевом трафике

    Когда в мире много актёров, нельзя бесконечно реплицировать всё всем.

    Relevancy: “кому вообще нужен этот актёр”

    Сервер решает, отправлять ли актёр конкретному клиенту (обычно по дистанции, видимости, ownership и правилам).

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

  • “мировые” актёры должны иметь разумные настройки репликации.
  • “приватные” актёры (например, UI-only менеджеры) часто не должны реплицироваться вообще.
  • Dormancy: “актёр долго не меняется”

    Если актёр редко меняется (например, дверь, которая стоит закрытой), можно переводить его в dormancy, чтобы уменьшить сетевую нагрузку.

    Идея:

  • активная фаза: актёр меняется, репликация работает
  • пассивная фаза: актёр “спит”, обновления не шлются, пока вы его не “разбудите”
  • Это особенно важно для dedicated server, где на одном процессе может быть много игроков.

    Предсказание (prediction) и согласование (reconciliation)

    Зачем нужно предсказание

    Если клиент будет ждать подтверждения сервера для каждого действия, управление станет “ватным” из-за задержки сети.

    Предсказание — это когда клиент:

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

    Самый известный пример — перемещение персонажа через CharacterMovementComponent:

  • клиент предсказывает движение локально
  • сервер подтверждает и при необходимости корректирует
  • Практический вывод:

  • если вы делаете персонажа на базе ACharacter, вы получаете сильную часть предсказания бесплатно
  • если вы пишете кастомное движение на APawn без CharacterMovementComponent, вам придётся проектировать предсказание аккуратно
  • Предсказание для “действий” (abilities, стрельба, использование)

    Типичный паттерн для действий:

  • Клиент мгновенно проигрывает локальную анимацию/звук (косметика).
  • Клиент отправляет Server RPC с намерением.
  • Сервер валидирует и меняет “истину” (урон, расход патронов, применение эффекта).
  • Результат приходит обратно через репликацию.
  • Клиент при необходимости откатывает косметику или корректирует состояние.
  • Важно разделять:

  • косметика: можно показывать сразу (и даже если потом отменится)
  • игровая истина: только сервер
  • Частая ошибка предсказания: менять реплицируемые поля на клиенте

    Если клиент меняет UPROPERTY(Replicated) локально, то потом сервер пришлёт своё значение, и вы получите “прыжки” состояния.

    Правильный подход:

  • клиент хранит локальные “предсказанные” флаги отдельно (не реплицируемые)
  • серверные поля меняются только на сервере
  • Dedicated Server: архитектурные правила, которые экономят недели

    Не смешивайте правила игры и визуальные эффекты

    Рекомендуемая структура:

  • серверная часть: правила, состояние, валидация
  • клиентская часть: UI/VFX/SFX/камера
  • Практический паттерн:

  • Реплицируемое состояние запускает OnRep, а в OnRep вы делаете только клиентскую реакцию.
  • Не опирайтесь на GameMode на клиентах

    GameMode существует только на сервере. Для клиентов используйте:

  • GameState для состояния матча
  • PlayerState для данных игроков
  • Это продолжает правила из статьи про Gameplay Framework.

    Проверяйте код на dedicated server ранним этапом

    Минимальная практика:

  • регулярно запускать PIE с dedicated server
  • или гонять отдельный server target
  • Иначе вы рискуете накопить зависимости от UI/рендера, которые всплывут слишком поздно.

    Диагностика сетевого кода и производительность

    Сетевые баги почти всегда “временные”: что-то пришло позже, порядок событий другой, объект ещё не создан.

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

  • UE_LOG с отдельной категорией для сети вашей системы
  • Unreal Insights для поиска лагов/спайков
  • аккуратные трейсы (TRACE_CPUPROFILER_EVENT_SCOPE) вокруг тяжёлых мест, которые срабатывают при сетевых апдейтах
  • Практическое правило логирования:

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

    Ошибка: логика спауна на клиенте

    Решение:

  • спавн “истины” делайте на сервере
  • клиент получает актёр через репликацию
  • Ошибка: RPC вызывается, но “не приходит”

    Проверьте по порядку:

  • Владеет ли клиент актёром, с которого вызывается Server RPC.
  • Включена ли репликация у актёра: bReplicates = true.
  • Есть ли соединение и актёр существует в сети (актуально при relevancy).
  • Ошибка: состояние скачет (jitter) или “откатывается”

    Частые причины:

  • клиент меняет реплицируемые поля локально
  • нет разделения “предсказание vs серверная истина”
  • визуальная часть привязана к нереплицируемому/не тем OnRep
  • Итоги

  • Authority почти всегда у сервера: сервер решает “истину”, клиент отправляет намерения.
  • Репликация синхронизирует состояние от сервера к клиентам через UPROPERTY и GetLifetimeReplicatedProps.
  • OnRep даёт безопасную точку клиентской реакции на обновление состояния.
  • RPC применяются для команд и событий: Server, Client, NetMulticast, с осознанным выбором Reliable/Unreliable.
  • Предсказание нужно для отзывчивости: косметика может быть мгновенной, но итог всегда подтверждает сервер.
  • Dedicated Server требует строгого разделения: никакой зависимости от UI/рендера, правила только на сервере.
  • Следующий практический шаг после этой статьи обычно — взять одну конкретную механику (стрельба, взаимодействие, инвентарь), реализовать её “сеть-правильно” и измерить стоимость репликации и RPC в профайлере.