Pull to refresh

(Перевод) Введение в разработку C++ в UE4 Часть 2

Reading time 10 min
Views 72K
Original author: Epic Games
Часть 1. Введение. Создание класса и добавление свойств. Расширение класса С++ с помощью Blueprint.
Часть 2. Классы геймплея. Структуры. Отражение (reflection) в Unreal. Object/Actor итераторы. Менеджер памяти и сборщик мусора.
Часть 3. Префиксы в именах классов. Целочисленные типы. Типы контейнеров. Итераторы контейнеров. Цикл For-each, хеш-функции.
Часть 4. Unreal Engine 4 для Unity разработчиков.
Часть 5.…

image

От Автора: Начало лето выдалось жарким на проекты, поэтому оформление перевода долго откладывал, дальше будет быстрее.

Эта статья является продолжением перевода части документации по UE4. Оригинальную статью вы можете найти пройдя по это ссылке.

Приятно видеть, что вы еще с нами. В следующих разделах мы будет изучать иерархию геймплей-классов. Мы начнем с базовых блоков и поговорим о том, как они связанны друг с другом. Так же, мы будем изучать как UE использует наследование и композицию, для создания кастомных особенностей геймплея.

Классы геймплея: Object, Actor и Component


Наиболее часто расширению подвергаются четыре основных геймплей-класса. Это UObject, AActor, UActorComponent и UStruct. Далее каждый из них будет подробно описан. Вы можете создавать типы, которые не расширяют ни один из этих классов, но они не позволят вам использовать большинство встроенных возможностей движка. Как правило, классы которые создаются вне UObject-иерархии (не являются наследниками любой глубины от UObject), существуют для следующих целей: интеграция сторонних библиотек, обертки особенностей операционной системы и т.п.

Unreal Objects (UObject)


Базовый блок в UE это класс UObject. Этот класс, совместно с классом UClass, обеспечивает вас наиболее важными базовыми возможностями движка:
  • Рефлексия свойств и методов
  • Сериализация свойств
  • Сборщик мусора
  • Поиск UObject по имени
  • Настройка значений для свойств
  • Поддержка сетевого режима для свойств и методов


Каждый наследуемый от UObject класс содержит UClass-синглтон, созданный для него. Этот класс содержит все метаданные о экземпляре класса. Возможности UObject и UClass лежат (совместно) в основе всего того, что геймплей-объект делает в течении жизненного цикла. Лучше всего думать о разнице между UClass и UObject как о том, что UClass описывает как именно выглядит экземпляр UObject, какие свойства доступны для сериализации, работы с сетью и т.д. В основном, разработка геймплея связанна не с наследованием напрямую от UObject, а с наследованием от классов AActor и UActorComponents. Вам не обязательно знать детали того, как работают UClass/UObject. Но знание о их существовании будет полезно.

AActor


Класс AActor представляет объект, который является частью игрового процесса. Экземпляр этого класса либо размещается дизайнером на уровне, либо создаются во время выполнения (при помощи систем геймплея). Все объекты, размещаемые на уровне, являются наследниками этого класса. Например — AStaticMeshActor, ACameraActor и APointLight. AActor наследуется от UObject, то есть, он использует стандартные функции, которые были перечислены в предыдущем разделе. AActor может быть уничтожен при помощи игрового кода (C++ или Blueprint) или стандартным механизмом сборки мусора (при выгрузки уровня из памяти). Он предоставляет высокоуровневое поведение наших игровых объектов, а так же является базовым типом, который предоставляет возможность репликации для сетевого режима. При репликации (в многопользовательском режиме), AActor так же дает доступ к информации о любом своем UActorComponent, которая требуется для поддержки работы сетевого режима.

AActor имеют свои собственные варианты поведения (специализируется при наследовании), но кроме этого, они являются контейнерами для иерархии UActorComponents (специализируется при композиции). Это можно сделать при помощи члена RootComponent нашего AActor. Он содержит один UActorComponent, который, в свою очередь, может содержать множество других. Перед размещением AActor на уровне, он должен содержать по крайней мере USceneComponent, который хранит положение, поворот и масштаб AActor'а.

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

BeginPlay Вызывается один раз, когда объект входит в игру
Tick Вызывается каждый кадр, для выполнения кода в течении этого кадра
EndPlay Вызывается, когда объект покидает игру


Для более подробного изучения класса AActor, вы можете пройти по ссылке.

Продолжительность жизненного цикла


В предыдущем разделе немного затрагивалась тема жизненного цикла AActor'а. Для Actor'ов размещенных на уровне жизненный цикл можно представить себе следующим образом — загрузка и начало существования Actor'а и его последующие уничтожение (при выгрузки уровня). Давайте разберемся, что представляют из себя процессы создание и уничтожение во время выполнения. Создание AActor немного сложнее, чем создание обычного объекта. Это происходит, потому что AActor должен быть зарегистрирован различными системами (работающих в run-time) — физический движок, менеджер отвечающий за информацию поступающую каждый кадр и т.д. Поэтому, для создания объекта существует метод UWorld::SpawnActor(). После того, как требуемый Actor создан успешно, вызывается метод BeginPlay(), вслед за которым вызывается метод Tick() (в следующем кадре).

Если Actor просуществовал необходимое вам время, вы можете уничтожить его вызвав метод Destroy(). При этом, будет вызван метод EndPlay(), в котором вы можете написать необходимый код, выполняющийся при уничтожении объекта. Другим вариантом контроля жизни Actor'а является использования поля LifeSpan (время жизни). Вы можете установить это значение в конструкторе (или позже в run-time). При истечении указанного времени метод Destroy будет вызван автоматический.

Узнать больше о создание Actor'ов вы можете перейдя по ссылке.

UActorComponent


UActorComponent'ы имеют свое собственное поведение и, как правило, отвечают за функциональность, которая является общей для различных AActor'ов — например, отображение мешей, системы частиц, работа с камерой и физические взаимодействия. В отличие от AActor'ов,, которые предоставляют высокоуровневую логику своего поведения в вашей игре, UActorComponent'ы, как правило, выполняют индивидуальные задания, требуемые для поддержки более высокоуровневых объектов. Компоненты могут быть детьми других компонентам. Их можно прикреплять только к одному родительскому компоненту или Actor'у, но сам компонент может иметь множество дочерних компонентов. Можете редставить себе эти отношения, как дерево компонентов. Следует помнить, что дочерние компоненты имеют положение, вращение и масштаб относительно родительских компонентов или Actor'а.

Существует множество вариантов использовать Actor'ов и компонентов. Одни из способов представить себе отношений Actor'ов и компонентов следующий — Actor отвечает на вопрос «Что это за вещь? (what is this thing?)», а компоненты — на вопрос «Из чего это сделано? (what is this thing made of?)»

  • RootComponent — Объект, который содержит компонент верхнего уровня в дереве компонентов AActor
  • Ticking — Компонент, который обновляются каждый кадр (тикает), являются частью метода Tick() владеющего им AActor.


Препарируем First Person Character


В предыдущих секциях было много слов без демонстраций. Для иллюстрации взаимосвязи AActor и его UActorComponent'ов, давайте изучим Blueprint, который генерируется при открытие проекта основанном на шаблоне First Person. На картинке ниже показанно дерево компонентов для FirstPersonCharacter Actor. Тут RootComponent это CapsuleComponent. Прикрепленные к CapsuleComponent компоненты это ArrowComponent, Mesh и FirstPersonCameraComponent. Наиболее глубоко вложенная вершина — компонент Mesh1P, родителем которого является FirstPersonCameraComponent. Это значит, что положение меша считается относительно данной камеры.
image

Графический это дерево компонентов выглядит так, как показано на картинке ниже, где вы можете видеть все компоненты в 3d пространстве (за исключением Mesh компонента)
image

Данное дерево компонентов прикреплено к одному actor-классу. Как мы видим, мы можем создавать комплексные объекты геймплея используя как наследование, так и композицию. Наследование стоит использовать при изменении существующего AActor или UActorComponent, а композицию, если множество типов AActor должны иметь схожий функционал.

UStruct


Для использования UStruct, вам не требуется наследование от какого-либо конкретного класса, достаточно отметить структуру макросом USTRUCT() и инструменты построения сделают основную работу для вас. В отличии от UObject, сборщик мусора не отслеживает UStruct. Если вы динамический создаете их экземпляры, то должны самостоятельно управлять их жизненным циклом. UStructs используются для того, чтобы POD-типы имели поддержку UObject-рефлексии для возможности редактирования в UE, управление через Blueprint, сериализация, работы с сетью и т.д.

Теперь после обсуждения основ иерархий для создания классов нашего геймплея, снова пришло время для выбора вашего пути. Вы можете подробнее изучить классы геймплея, исследовать наши примеры в поисках дополнительной информации или продолжить погружаться глубже в изучение особенностей С++ для создания игры.

Погружаемся еще глубже


Вы точно уверены что хотите знать больше. Мы будем продолжать изучать работу движка.

Система рефлексии Unreal


Пост в блоге: Система свойств в Unreal (Рефлексия)

Классы геймплея используют специальную разметку, так что прежде чем переходить к ним, давайте рассмотрим некоторые базовые вещи из системы свойств Unreal. UE4 использую свою собственную систему рефлексии, которая предоставляет вам различные динамические возможности — сборщик мусора, сериализация, сетевая реплекация и взаимодействие Blueprint/C++. Эти возможности являются опциональными, то есть вы должны самостоятельно добавлять требуемую разметку для ваших типов, в противном случае Unreal игнорирует их и не генерирует необходимые данные для рефлексии. Ниже приведет короткий обзор основных элементов разметки:

  • UCLASS() — используется для генерации данных рефлексии для класса. Класс должен быть наследником UObject
  • USTRUCT() — используется для генерации данных рефлексии для структуры
  • GENERATED_BODY() — UE4 заменяет это на весь необходимый шаблонный код который создается для типа.
  • UPROPERTY() — разрешает использовать переменную-члена UCLASS или USTRUCT как UPROPERTY. UPROPERTY имеет множество вариантов использования, Этот макрос позволяет сделать переменную возможной для репликации, сериализации и доступной из Blueprint. Так же используется сборщиком мусора для отслеживания количества ссылок на UObject.
  • UFUNCTION() — позволяет методу класса UCLASS или USTRUCT быть использованным как UFUNCTION. UFUNCTION может позволить методу быть вызванным из Blueprint и использоваться как RPCs, и т.д.


Пример определения класса UCLASS:

#include "MyObject.generated.h"

UCLASS(Blueprintable)
class UMyObject : public UObject
{
    GENERATED_BODY()

public:
    MyUObject();

    UPROPERTY(BlueprintReadOnly, EditAnywhere)
    float ExampleProperty;

    UFUNCTION(BlueprintCallable)
    void ExampleFunction();
};

Вы впервые можете заметить включение заголовка «MyClass.generated.h». Unreal будет размещать все cгенерированные данные для рефлексии в этом файле. В декларации вашего типа (в списке включенных файлов) этот файл должен располагаться последним.

Так же вы могли заметить, что существует возможность добавлять дополнительные спецификаторы макросам разметки. В коде выше для демонстрации добавлены некоторые наиболее распространенные из них. Они позволяют указать определенное поведение, которым обладают наши типы:
  • Blueprintable — Класс может быть расширен с помощью Blueprint.
  • BlueprintReadOnly — Свойство доступно только для чтения из Blueprint, и не доступно для записи.
  • Category — Определяет секцию в которой свойство отображаются в Details view в редакторе. Используется для организации.
  • BlueprintCallable — Функция может быть вызвана из Blueprint.


Количество спецификаторов очень велико, поэтому мы не будем перечислять их здесь, а дадим ссылки на соответствующие разделы документации:
  • Список спецификаторов UCLASS
  • Список спецификаторов UPROPERTY
  • Список спецификаторов UFUNCTION
  • Список спецификаторов USTRUCT


Object/Actor итераторы


Итераторы объектов очень полезный инструмент для перебора всех экземпляров конкретного типа UObject и его подклассов.

// Ищем ВСЕ экземпляры конкретного UObject
for (TObjectIterator<UObject> It; It; ++It)
{
  UObject* CurrentObject = *It;
  UE_LOG(LogTemp, Log, TEXT("Found UObject named: %s"), *CurrentObject.GetName());
}

Вы можете ограничить поиск, предоставляя более конкретный тип итератора. Предположим, что у вас есть класс именуемый UMyClass, унаследованный от UObject. Вы можете найти все экземпляры этого класса (и все, которые являются его наследниками) следующим образом:

for (TObjectIterator<UMyClass> It; It; ++It)
{
  // ...
}

ПРЕДУПРЕЖДЕНИЕ: Использование итераторов объектов в PIE (Play in Editor) может приводить к неожиданным результатам. Пока редактор загружен, итератор объектов вернет все UObject'ы созданные для вашего экземпляра игрового мира, в дополнении к тем, что используются в редакторе.

Итераторы Actor'ов работают схожим образом, как и итераторы объектов, но работают только для наследников от AActor. Итераторы Actor'ов не имеют проблему указанную выше и возвращают только те объекты, которые используются в текущем экзамепляре игрового мира.

Когда создается итератор Actor'а, ему требуется передать указатель на экземпляр UWorld. Множество дочерних классов от UObject, например, APlayerController предоставляют этот метод. Если вы не уверены, вы можете проверить возвращаемое значения метода ImplementsGetWorld, чтобы понять, поддерживает ли конкретный класс метод GetWorld.

APlayerController* MyPC = GetMyPlayerControllerFromSomewhere();
UWorld* World = MyPC->GetWorld();

// Like object iterators, you can provide a specific class to get only objects that are
// or derive from that class
for (TActorIterator<AEnemy> It(World); It; ++It)
{
    // ...
}

Поскольку AActor является наследником от UObject, вы можете использовать TObjectIterator так же чтобы найти все экземпляры AActors. Но будьте осторожны в PIE!

Менеджер памяти и сборщик мусора


Actor'ы обычно не собираются сборщиком мусора. Вы должны вручную вызывать метод Destroy, после порождения Actor'а. Удаление произойдет не сразу, а только в течении следующей фазы сборки мусора.

Этот случай наиболее общий, если у вас Actor'ы со свойствами UObject.

UCLASS()
class AMyActor : public AActor
{
  GENERATED_BODY()

public:
  UPROPERTY()
  MyGCType* SafeObject;

  MyGCType* DoomedObject;

  AMyActor(const FObjectInitializer & ObjectInitializer) : Super(ObjectInitializer)
  {
    SafeObject = NewObject<MyGCType>();
    DoomedObject = NewObject<MyGCType>();
  }
};

void SpawnMyActor(UWorld * World, FVector Location, FRotator Rotation)
{
  World->SpawnActor<AMyActor>(Location, Rotation);
}

Когда мы вызываем данную функцию, мы порождаем Actor'а в нашем мире. Его конструктор создает два объекта. Один с меткой UPROPERTY, другой — обычный указатель. Пока Actor'ы являются частью корневого объекта, SafeObject не будет собираться сборщиком мусора, так как к нему могут обращаться из этого корня. DoomedObject, однако, будет иметь другой жизненный цикл. Поскольку он не отмечен как UPROPERTY, сборщик мусора не знает ничего о ссылках на него и в конечном итоге он будет уничтожен.

Когда UObject собирается сборщиком мусора, все UPROPERTY ссылки получат значения nullptr. Это делается для безопасной проверки, является ли объект уничтоженным сборщиком мусора или нет.

if (MyActor->SafeObject != nullptr)
{
  // Use SafeObject
}

Это важное замечание, поскольку, как говорилось ранее — актеры, для которых был вызван метод Destroy() не удаляются до следующей фазы сборки мусора. Вы можете проверить, ожидает ли UObject удаления, с помощью метода IsPendingKill(). Если метод возвращает true, объект считается мертвым и не должен быть использован.

UStructs


Как говорилось ранее UStructs — легковесный вариант UObject'а. UStructs не может быть собран сборщиком мусора. Если вы используете динамический экземпляр UStructs, вы можете использовать умные указатели, о которых мы поговорим позже.

Ссылки не на UObject


Обычно не-UObject'ы так же могут иметь возможность добавлять ссылку на объект для предотвращения их удаления сборщиком мусора. Чтобы сделать это, объект должен быть наследником FGCObject и переопределить метод AddReferencedObjects.

class FMyNormalClass : public FGCObject
{
public:
  UObject * SafeObject;
  FMyNormalClass(Uobject * Object) : SafeObject(Object)
  {
  }

  void AddReferencedObjects(FReferenceCollector & Collector) override
  {
    Collector.AddReferencedObject(SafeObject);
  }
}


Мы используем FReferenceCollector, чтобы вручную добавить жесткую ссылку на требуемый UObject, который не должен быть собран сборщиком мусора. Когда объект удаляется (срабатывает деструктор), все добавленные им ссылки будут удалены.

PS: Прошу все предложения по исправлению ошибок и неточностей присылать в личку.
Tags:
Hubs:
+28
Comments 1
Comments Comments 1

Articles