Классы, их методы и свойства

## Общие сведения

Технически все классы движка и определяемые разработчиком игровые классы являются потомками класса UObject (и через него его предков UObjectBaseUtility и UObjectBase). К общеупотребительному функционалу, определённому в этих базовых классах, относится, например, сборка мусора, сериализация и репликация по сети в многопользовательской игре.

Каждый класс должен определяться в своей собственной паре h- и cpp-файлов; имена этих файлов рекомендуется делать совпадающими с именем класса без буквы-префикса (**U** для классов, унаследованных от других U-классов, в частности, от самого UObject, и **A** для классов-потомков AActor). Например, для класса UMyClass (или AMyClass) следует использовать файлы MyClass.h и MyClass.cpp.

Обычное определение класса-наследника UObject на Си++ должно предваряться макросом UCLASS(), а первым элементом внутри определения должен быть макрос GENERATED\_UCLASS\_BODY(). Кроме того, заголовочный файл класса должен первым делом включать автоматически генерируемый заголовочный файл с именем *имя-класса*.generated.h. Это необходимо для того, чтобы включить в класс автоматически генерируемый код, делающий его совместимым с движком. Заголовочный файл файла для класса UMyClass будет выглядеть примерно так:

```
#include <MyClass.generated.h>

UCLASS(*спецификатор*,...,*спецификатор*,meta(*ключ*=*значение*,....,*ключ*=*значение*))
class UMyClass : UObject
{
    GENERATED_UCLASS_BODY()

    *Объявления полей и методов*
}

```

Здесь *спецификаторы* — различные атрибуты, задающие характеристики класса, например, возможность использовать его экземпляры в Blueprint, а пары «*ключ*=*значение*» — различные метаданные, также описывающие этот класс.

В cpp-файле включается заголовочный файл соответствующего класса, однако это не обязательно производится прямо. Например, включение многих заголовков, используемых кодом движка, обеспечивается файлом EnginePrivate.h, поэтому в движке явно включаться может именно этот файл, а не, например Actor.h.

С каждым классом-потомком UObject связан экземпляр класса UClass; он общий для всех экземпляров данного UObject'а. UClass содержит различные метаданные, описывающие соответствующий UObject. Эти описания касаются лишь полей и методов класса, тегированных с помощью соответствующих макросов. Для обычных полей и методов, не имеющих тегов, метаданные не формируются.

Для каждого UObject'а существует шаблонный экземпляр данного класса, называемый CDO (Class Default Object). Он проинициализирован значениями по умолчанию и используется как источник этих значений при создании других экземпляров классов. Как правило, после создания значения полей CDO не изменяются, хотя это и возможно (см. ниже подраздел «Поля»).

## Конструкторы и инициализация объектов

Как правило, конструкторы находятся в cpp-файле класса, а в заголовочном файле присутствует лишь их объявления. Возможно, однако, разместить весь конструктор в заголовочном файле, но при этом для класса необходимо указать спецификатор CustomConstructor. Конструкторы некоторых классов движка находятся не в соответствующих cpp-файлах классов, а в, например, файле EngineConstructors.cpp. Это объясняется историческими причинами; постепенно конструкторы должны «переехать» в свои родные файлы.

Типичный конструктор имеет следующий вид:

```
AMyClass::AMyClass(const FObjectInitializer  &ObjectInitializer)
        : Super(ObjectInitializer)
{
    *инициализация полей класса и т.п.*
}

```

Сами по себе конструкторы выполняют совершенно традиционную роль — инициализируют необходимые поля создаваемого экземпляра соответствующего класса. Однако прямой вызов конструктора практически никогда не производится, вместо этого следует использовать шаблонную функцию NewObject. Причиной этому является необычный порядок действий при выполнении инициализации создаваемых объектов. Механизм инициализации объектов работает приблизительно следующим образом.

Сначала под экземпляр игрового класса выделяется память, причём создание экземпляров в стеке потока невозможно — они должны создаваться в динамической памяти (при нарушении этого требования возникнет ошибка). После выделения памяти под новый объект происходит обычный вызов конструктора (как правило, имеющего приведённый выше вид, но это не обязательно). Этот конструктор явно или неявно вызывает конструктор класса-предка и так далее в полном соответствии с правилами языка (если явного вызова нет — например, конструкторы класса AActor его не применяют, компилятор сам вызовет нижележащий конструктор по умолчанию). Когда будет выполнен самый нижний из конструкторов, управление по цепочке обычным образом вернётся обратно к конструктору создаваемого объекта, при этом каждый из нижележащих конструкторов выполнит необходимую инициализацию «своих» полей (а при нужде может переопределить значения полей, установленные нижележащими конструкторами). После того, как выполнение конструкторов завершится, настанет черёд завершения инициализации нового объекта с помощью структуры типа FObjectInitializer и информации, содержащейся в экземпляре-архетипе и в CDO.

Структура FObjectInitializer, вообще говоря, создаётся перед вызовом конструктора. В ней хранится различная информация, относящаяся к инициализации – она заносится туда конструктором этой структуры. Когда вся цепочка конструкторов создаваемого объекта будет выполнена, будет вызван деструктор этой структуры. Перед тем, как её уничтожить, он проверит, нет ли нужды выполнить дополнительные действия по инициализации объекта. Они не нужны для нативных объектов (определённых полностью кодом на Си++), но в них нуждаются объекты блупринтов. Для таких объектов после того, как штатные конструкторы Си++ выполнят всю заложенную в них инициализацию, деструктор FObjectInitializer выполнит оставшуюся часть работы и инициализирует поля, относящиеся к блупринтовой части объекта.

Нередко определённым полям разных экземпляров одного и того же класса при создании надо присваивать одинаковые значения. Если эти значения имеют простой тип, проблем не возникает: осуществляется обычное присваивание в конструкторе. Однако, если тип сложный (например, сам представляет из себя класс), создавать его каждый раз индивидуально для каждого экземпляра неэффективно, да и недопустимо, если все экземпляры класса должны иметь подлинно одно и то же значение поля (например, указатель на один и тот же экземпляр некоторого вспомогательного класса). В подобных случаях в конструкторе объявляется структура, традиционно носящая имя FConstructorStatics, и локальная статическая переменная этого типа. Конструктор этой структуры создаёт сложные типы, которые нужны для инициализации создаваемых экземпляров класса. Поскольку переменная является статической, вызов конструктора этой структуры будет произведён лишь один раз, при первом вызове конструктора класса; во всех последующих случаях будет использоваться уже инициализированная переменная. Пример такой инициализации будет дан ниже.

Специально для нужд конструкторов предусмотрен набор шаблонов, например, для поиска ссылок на ассеты или классы. Эти шаблоны определены в файле ObjectBase.h и расположены в пространстве имён ConstructorHelpers.

В приведённом ниже примере иллюстрируется использование структуры FConstructorStatics для идентичной инициализации полей разных экземпляров одного и того же класса ссылкой на ассет, жёстко закодированный на Си++. Заметим, что подобное жёсткое кодирование обычно является нежелательным; как правило, ссылки на ассеты лучше определять в редакторе или устанавливать с помощью брупринтов.

```
AMyClass::AMyClass(const FObjectInitializer  &ObjectInitializer)
        : Super(ObjectInitializer)
{
    struct FConstructorStatics
    {
        ConstructorHelpers::FObjectFinder<UStaticMesh>  Obj;
        FConstructorStatics()
            : Obj(TEXT("StaticMesh'/Game/...*путь*.../MyMesh.MyMesh'"))
        {
        }
    }
    static FConstructorStatics  ConstructorStatics;
    StaticMesh = ConstructorStatics.Obj.Object;
}

```

Для поиска UClass'ов можно использовать ConstructorHelpers::FClassFinder. Однако обычно предпочтительней использовать метод StaticClass, имеющийся у любого UObject'а. Поиск с помощью FClassFinder используется главным образом лишь для межмодульных ссылок.

Для создания и поиска компонентов и подобъектов используются ConstructorHelpers::CreateComponent и ConstructorHelpers::FindComponent соответственно. Последний ищет компоненты/подобъекты, созданные классом-родителем; его применение обычно нежелательно.

## Поля и свойства

Если какое-либо поле класса должно быть известно редактору, непосредственно перед объявлением поля должен быть помещён макрос UPROPERTY, содержащий необходимую информацию о поле. Такие поля называются свойствами.

Если свойство является указателем на какой-либо игровой объект, это свойство будет учитываться в сборке мусора: пока хотя бы одно свойство, находящееся в объекте, относящемся к так называемому корневому набору (root set – похоже, это группа объектов, которая никогда в процессе работы приложения не уничтожается), содержит ссылку на некоторый объект, этот объект не будет автоматически уничтожен. Тем не менее, если для этого объекта вызван метод Destroy, он будет отмечен на уничтожение и в ближайшем будущем уничтожен сборщиком мусора, при этом все свойства, содержащие указатель на этот объект, будут принудительно обнулены.

Если необходимо сохранить адрес некоторого объекта, но так, чтобы этот адрес не уничтожал объект от уничтожения сборщиком мусора, можно создать поле, являющееся обычным указателем Си++. Однако нередко лучше использовать для этого шаблонный класс TWeakObjectPtr. Указатель, созданный этим классом, является слабым в том смысле, что не может удержать объект от уничтожения, однако, если объект уничтожается, сборщик мусора обнулит этот указатель — в отличие от обычного «низкоуровневого» указателя Си++.

Свойства, в отличие от обычных полей, автоматически загружаются/сохраняются при (де)сериализации объекта. Если некоторое свойство сериализовать не нужно, оно должно быть явным образом помечено как временное (transient). Сериализацию обычных полей движок сам выполнить не в состоянии.

(Де)сериализация, выполняемая движком, поддерживает изменение версий форматов объектов «на лету»: если происходит загрузка из сэйва старого формата, то значения свойств, отсутствующих в сэйве, будут получены из CDO для данного класса объектов, а если происходит загрузка из сэйва более нового формата, чем объект, то лишние свойства просто игнорируются.

Если значение какого-либо свойства в CDO изменяется (обычно этого не бывает), движок автоматически поменяет значение этого же свойства у всех объектов этого класса, у которых его значение было равно старому значению свойства в CDO. Это выполняется в том числе при загрузке из сэйва.

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

## Методы (функции)

Чтобы метод некоторого объекта был доступен для блупринтов, необходимо перед его объявлением поместить макрос UFUNCTION с соответствующими атрибутами. Этот макрос позволяет также определять методы, которые могут вызываться по сети (при многопользовательской игре).

## Приведение и проверка типов

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

Для проверки типов используется метод IsA. Он вернёт true, если проверяемый объект имеет заданный тип или тип, являющийся потомком заданного.