Pull to refresh

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

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

image

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

Unreal C++ очень крут!


Это руководство покажет вам как писать код на С++ в Unreal Engine. Не переживайте, разработка на С++ в Unreal Engine весёлая, и совершенно не сложная, чтобы её начать. Нам нравится думать о Unreal C++ как о «помогающем C++» *, поскольку мы создали множество разных фич чтобы сделать C++ легче для всех!
* буду рад, если кто предложит лучший перевод «assisted C++», но пожалуйста в личку.

Перед тем как мы начнем, важно чтобы вы были уже знакомы с C ++ или другим, схожим языком программирования. Это руководство написано для разработчиков имеющих опыт с C++. Если вы знаете, C#, Java или JS, вы найдете множество знакомых аспектов.

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

Примечания
* Blueprint Visual Scripting guide
** далее, где написано Blueprint, подразумевается как Blueprints Visual Scripting так и «Blueprint-класс». Что конкретно подразумевается вам будет ясно из контекста.


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

C++ и Blueprints


UE предоставляет два метода для создания элементов геймплея — C++ и Blueprint. С++ программисты добавляют основные блоки геймплея, таким образом, чтобы дизайнеры (тут имеется ввиду левел-дизайнер, а не художник) с помощью этих блоков мог создавать свои элементы геймплея для отдельного уровня или всей игры. В таком случае, программисты работают в своем (своей) любимой IDE (например — MS Visual Studio, Xcode), а дизайнер работает в Blueprint редакторе UE.

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

С учетом всего вышесказанного, далее будет рассмотрен типичный рабочий процесс программиста C++, который создает блоки для дизайнера. В этом случает вы должны создать класс, который в дальнейшем будет расширен с помощью Blueprint, созданного дизайнером или другим программистом. В этом классе мы создадим различные свойства (переменные), которые сможет задать дизайнер. На основе этих заданных значений, мы собираемся извлечь новые значения созданных свойств. Данный процесс очень прост благодаря инструментам и макросам, которые мы предоставляем для вас.

Мастер классов


Самое первое что требуется сделать это воспользоваться мастером классов (class wizard) предоставляемый UE, для создания базы будущего С++ класса, который в дальнейшем будет расширен с помощью Blueprint. Ниже показано, каким образом происходит выбор при создании нового класса, дочернего от класса Actor.
image

Далее требуется ввести название вашего класса. Мы воспользуемся именем по умолчанию (MyActor).
image

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

#include "GameFramework/Actor.h"
#include "MyActor.generated.h"

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

public: 
    // Устанавливает значения по умолчанию для свойств этого Actor
    AMyActor();
    // Вызывается во время начала игры или спавне этого Actor
    virtual void BeginPlay() override;

    // Вызывается каждый кадр
    virtual void Tick( float DeltaSeconds ) override;
};


Мастер классов генерирует класс с методами BeginPlay() и Tick(), со спецификатором перегрузки (override). Событие BeginPlay() происходит когда Actor входит в игру, в состоянии разрешённом для игры (playable state). Хорошей практикой является инициирование геймплей-кода вашего класса в этом методе. Метод Tick() вызывается каждый кадр с параметром, который равен времени, прошедшему с последнего своего вызова. В этом методе должна содержаться постоянно повторяющаяся логика. Если у вас она отсутствует, то лучше всего будет убрать данный метод, что немного увеличит производительность. Если вы удалили код данного метода, убедитесь что вы так же удалили строку в конструкторе класса, которая указывает, что Tick() должен вызываться каждый кадр. Ниже приведет код конструктора с указанной строкой:

AMyActor::AMyActor()
{
    // Разрешить данному actor вызывать Tick() каждый кадр.
    // Вы можете отключить это чтобы увеличить производительность,
    // если вам не требуется этот метод.
    PrimaryActorTick.bCanEverTick = true;
}


Создание свойств, отображающихся в редакторе


Теперь у нас есть собственный класс. Давайте создадим несколько свойств, которые могут быть использованы другими разработчиками, непосредственно в UE. Для отображения свойства в редакторе требуется использовать специальный макрос UPROPERTY(). Все что требуется сделать, это написать макрос UPROPERTY(EditAnywhere) перед объявлением переменной, как написано ниже:

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

  UPROPERTY(EditAnywhere)
  int32 TotalDamage;
}


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

UPROPERTY(EditAnywhere, Category="Damage")
int32 TotalDamage;


Теперь, пользователи будут видеть вашу переменную помещенную в категорию с заголовком «Damage». В этой категории так же могут быть другие свойства, у которых указана такая же категория. Это отличный способ размещать наиболее часто используемые переменные вместе.

Теперь сделаем свойство доступным из Bluerpint:
UPROPERTY(EditAnyway, BlueprintReadWrite, Category="Damage")
int32 TotalDamage;


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

Перед тем как перейдем к следующему разделу, давайте добавим несколько переменных нашему классу. У нас уже имеется переменная хранящая полный урон, который может нанести Actor, но давайте считать, что урон может производиться длительное время. Код ниже содержит одну новую переменную доступную для редактирования левел-дизайнером и одну недоступную для редактирования:

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

  UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Damage")
  int32 TotalDamage;

  UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Damage")
  float DamageTimeInSeconds;

  UPROPERTY(BlueprintReadOnly, VisibleAnywhere, Transient,  Category="Damage")
  float DamagePerSecond;
  ...
}


Как видно, DamageTimeInSeconds свойство, которое доступно для редактирования в редакторе. DamagePerSecond будет вычисляться, как вы увидите позднее, на основе значения заданного в DamageTimeInSeconds, например, левел-дизайнером. Флаг VisibleAnywhere указывает что свойство отображается, но не может быть изменено. Флаг Transient означает что это свойство нельзя сохранить или прочитать с диска, то есть полученное значение является непостоянным. На картинке ниже показано как отображаются эти свойства в разделе значений по умолчанию нашего класса.
image

Установки значений по умолчанию в конструкторе



Установка начальных значений переменных происходит как и в обыкновенном C++ классе — в конструкторе. Ниже приведены два примера, каким образом это можно сделать, оба примера эквиваленты по функциональности:
AMyActor::AmyActor()
{
  TotalDamage         = 200.0f;
  DamageTimeInSeconds =   1.0f;
}

AMyActor::AmyActor() :
  TotalDamage       (200.0f);
  DamageTimeInSeconds (1.0f);
{
}


Вот тот же кусок окна, но уже с заданными значениями в конструкторе
image

Так же возможно задавать начальные значения основанные на значениях заданных в редакторе. Эти данные задаются после конструктора, для этого требуется использовать метод PostInitProperties(). В данном примере TotalDamage и DamageTimeInSeconds задаются левел-дизайнером. Независимо от того, заданны ли эти значения из редактора, вы по прежнему можете задать требуемые начальные значения, как мы сделали это ранее.
Заметка: если вы не задаете значения по умолчанию, они автоматически будут установлены в 0 или nullptr для значений указателей.

void AMyActor::PostInitProperties()
{
    Super::PostInitProperties();
    DamagePerSecond = TotalDamage / DamageTimeInSeconds;
}


Тот же кусок окна, что и ранее, но уже после добавления PostInitProperties() в цепь вызовов.
image

Горячая перезагрузка.


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

1. Если редактор запущен, сделайте билд в Visual Studio или Xcode, как вы обычно это делаете. Редактор обнаружит новые скомпилированные DLL и перезагрузит ваши изменения сразу же.
image

Заметка: Если у вас приатачен дебаггер, вы должны открепить его, в начале, иначе VS не позволит вам сделать build.

2. Или просто нажмите на Compele в основном тулбаре в редакторе.
image

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

Расширение С++ класс с помощью Blueprint



До этого мы создали простой геймплей-класс, при помощи С++ мастера классов и добавили в него несколько переменных. Теперь мы изучим на то, как пользователь может создавать уникальные классы из наших скромных набросков.

Первое, что требуется сделать, это создать Blueprint класс из нашего AMyActor класса. Обратите внимание, что на изображении ниже имя базового класса указанно как MyActor, а не AMyActor. Это сделано для того, чтобы спрятать соглашения об именах, которое используется в наших инструментах от пользователя, делая имена более удобными для него.
image

После нажатия на Select, будет создан новый Blueprint с дефолтным именем. Мы зададим ему имя CustomActor1, как указанно на изображении из Content Browser'а ниже.
image

Это наш первый класс, который наш пользователь будет редактировать. Во-первых, поменяем значения наших переменных. В данном случае выставим TotalDamage равным 300 и время, в течении которого наносятся эти повреждения равным двум секундам. Вы можете увидеть это на картинки ниже:
image

Погодите… Наша расчетная величина не соответствует нашему ожиданию. Оно должно быть 150, но мы видим значение равное 200. Это происходит потому, что в данным момент мы вычисляем значения сразу после инициализации значений, которое происходит в момент загрузки. Но у нас происходят изменения времени выполнения в редакторе (runtime changes), которые не учитываются. Эту проблему легко решить, поскольку движок уведомляет целевой объект о событии, которое вызывается при изменении в редакторе. Код ниже показывает, что именно требуется добавить для расчета новых значений, полученных при изменении переменных в редакторе:

void AMyActor::PostInitProperties()
{
  Super::PostInitProperties();

  CalculateValues();
}

void AMyActor::CalculateValues()
{
  DamagePerSecond = TotalDamage / DamageTimeInSeconds;
}

#if WITH_EDITOR
void AMyActor::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
{
  CalculateValues();

  Super::PostEditChangeProperty(PropertyChangedEvent);
}
#endif


Заметьте, что метод PostEditChangeProperty расположен внутри директивы, которая указывает, работаем мы в редакторе или нет. Это сделано для того чтобы при билде, игра содержала только необходимый для неё код, без лишних строк, которые увеличивают размер исполняемого файла, без необходимости. Теперь, после перекомпиляции, значение DamagePerSecond будет соответствующим нашему ожиданию. Это указанно на картинки ниже.
image

Вызов C++ методов в Blueprint


До сих пор мы изучали работу с переменными. Кроме этого требуется изучить еще одну важную базовую вещь, перед тем как более детально изучать движок. В процессе создания геймплея, пользователь должен иметь возможность вызывать в Blueprint функции созданные C++ программистом. Для начала давайте сделаем чтобы метод CalculateValues() можно было вызывать из Bluerpint.

UFUNCTION(BlueprintCallable, Category="Damage")
void CalculateValues();


Макрос UFUNCTION() содержит описание, каким образом наша С++ функция обрабатывается системой рефлексии. Опция BlueprintCallable указывает возможность обработки данного метода в виртуальной машине Blueprint'ов (далее Blueprint VM). Для того, чтобы контекстное меню (вызываемое правой кнопкой мыши) работало должным образом, каждый метод, вызов которого разрешен редактором, должен содержать имя категории. Изображение ниже показывает как именно категории отображаются в контекстном меню:
image

Как видите, метод может быть выбран в категории Damage. Blueprint-код ниже показывает, каким образом происходят изменения в значении TotalDamage, с последующим вызовом метода для пересчета зависимых значений.
image

Тут мы используем метод, описанный ранее, для пересчета зависимых свойств. Большая часть методов движка доступны для вызова из Blueprint, при помощи макроса UFUNCTION(), так чтобы разработчики могли создавать игры без написания С++ кода. Тем не менее, более грамотным подходом будет использования С++ для создания основных блоков геймплея и кода, чья производительность критична, а применение Blueprint для кастомизирования созданного поведения или конструирование нового, в основе которого лежит созданный код.

Теперь, когда наши пользователи могут вызывать ваш C++ код, рассмотрим еще один способ вызова C++ кода в Blueprint. Этот подход позволяет вызывать в С++-коде функции реализованные в Blueprint. Таким образом можно уведомить пользователя о событиях, на которые они могут реагировать тем образом, которым считают нужным. Часто это бывает создание эффектов или другие графические взаимодействия, как показ/сокрытие объектов. Фрагмент кода ниже содержит метод, который реализован в Blueprint:

UFUNCTION(BlueprintImplementableEvent, Category="Damage")
void CalledFromCpp();


Эта функция вызывается как и обычная С++-функция. UE генерирует основу реализации С++ функции для правильного вызова ее в Blueprint VM. Обычно мы называем это Thunk(Преобразователь). Если Blueprint не реализует тело функции, то её поведение представляет С++-функцию с пустым телом, которое ничего не делает. Что делать, если мы хотим обеспечить С++ реализацию по умолчанию и сделать возможным переопределения ее в Bluerpint. Макрос UFUNCTION() имеет опцию для этого случая. Фрагмент кода ниже показывает, какие изменения в заголовочном файле нужно сделать, чтобы добиться этого:

UFUNCTION(BlueprintNativeEvent, Category="Damage")
void CalledFromCpp();


Эта версия метода по-прежнему преобразует метод для его вызова в Blueprint VM. Каким образом мы должны обеспечить реализацию по умолчанию? Инструменты так же генерируют новое определение метода с постфиксом _Implementation(). Мы должны представить вашу версию этого метода или ваш проект не будет слинкован. Вот реализация для указанного выше определения:

void AMyActor::CalledFromCpp_Implementation()
{
  // Do something cool here
}


Теперь этот метод вызывается когда Blueprint не переопределяет его. На заметку: в будущих версиях билд инструментов автосгенерированное определение _Implementation() будет убрано и его нужно будет явно добавлять в заголовок. В версии 4.7 автогенерация этого определения по-прежнему происходит.

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

Небольшие заметки:
1) Текста достаточно много, постарался проверить пару раз, уверен что есть какие-то ошибки. Если найдете, прошу писать ЛС, как только, так сразу поправлю. Это же касается ошибок перевода каких-то идиом или терминов.
2) Лично мне не очень нравится описания, где предложения с одинаковым смыслом повторяются по 3и раза, как будто вам вдалбливают что-то. Но не мне спорить с создателями документации, я думаю им виднее.
3) Если переводы будут востребованы сообществом, то не буду останавливаться на этих 4 частях, а по возможности буду переводить все базовые куски документации.
Only registered users can participate in poll. Log in, please.
Делать ли дальнейшие переводы
97.53% Да 828
0.59% Да, но при условии (пишите в комменты) 5
1.77% Нет, мне оригинальная документация намного понятнее и яснее 15
0.47% Нет, потому что (пишите в комменты) 4
0.47% Нет, просто автор лично не нравится 4
849 users voted. 94 users abstained.
Tags:
Hubs:
+40
Comments 8
Comments Comments 8

Articles