В этой заметке я кратко конспектирую содержание курса Unreal Engine C++ Developer: Learn C++ and Make Video Games, который я прошел на Udemy.
Подготовка к работе
Создать проект C++
Импорт ассетов: in Content Browser > ПКМ > Import to...
Выбрать среду разработки Edit > Editor Preferences > Source Code

Настройка Visual Studio Code
Установка расширения C/C++ for Visual Studio Code для поддержки IntelliSense.
Установка расширения VSCode UE Intellisense Fixes Extension для исправления ошибок Intelli Sense в VSCode.
При переносе проекта UE4 с одной машины на другую либо в другую папку на той же машине проект C++ может перестать работать, поэтому его необходимо обновить из Unreal Editor: File > Refresh Visual Studio Code Project

Дизайн уровня
Создание материалов (Creating materials)
Material = Shader + Textures
Примеры текстур в материале: Base Color, Normal, Specular, Ambient Occlusion.
Материал программируется в виде блюпринта (blueprint). Материал похож на класс в ООП в том смысле, что можно создать один материал и множество экземпляров этого материала (material instances), с одинаковыми шейдерами, но отличающихся т. н. входными параметрами (input parameters). На рис. 3 — пример минимального более-менее универсального материала, позволяющего накладывать текстуру (base color) на объекты static mesh. Отдельно следует отметить узлы, имена которых заканчиваются словом Parameter (например, ScalarParameter, TextureObjectParameter) — те самые входные параметры, которые можно изменять в material instance — это показано на рис. 4.


Binary Space Partitioning (BSP)
Статья BSP на Valve Developer Community. Всё пространство делится на две части, затем каждая из частей может делиться еще на две части и т. д. Таким образом получается дерево BSP tree. BSP позволяет определить, принадлежит ли точка пространства тому или иному геометрическому объекту, что в свою очередь позволяет выполнять collision detection.
Brushes (or Constructive Solid Geometry pieces) — выпуклые геометрические фигуры, которые используются для создания более крупных элементов геометрии при помощи операций объединения (merging) и вычитания (subtracting). Разместить brush можно через окно Place Actors > Geometry (рис. 5).

Для brush’ей, в отличие от static mesh’ей, можно редактировать отдельные грани. Для этого нужно перейти в Brush Editing Mode (рис. 6).

BSP brushes rendering is less effective than that of static meshes, поэтому обычно после размещения множества BSP brush’ей их превращают в static mesh’и — это называется запекание геометрии (рис. 7).

В UE4 есть проблема: после запекания и перестраивания света (освещение в UE4 запекается в текстурах — это можно сделать, выбрав меню Build Light) возникают странные неправильные тени (иногда поверхности объектов становятся абсолютно черными). Это связано со слишком низким разрешением текстур, в которых запекается свет. Чтобы исправить эту ошибку, нужно в Content Browser’е открыть static mesh (двойным кликом) и в окне Details найти и отредактировать свойства: Light Map Resolution: 4 => 64, Light Map Coordinate Index: 0 => 1 (рис. 8.1).

Другая проблема при запекании геометрии связана с коллизиями. Так например, когда я впервые запек Static Mesh для пола, на котором должен стоять персонаж игрока, я обнаружил, что персонаж проваливается сквозь пол. UE4 использует в ассетах типа Static Mesh два вида collision shapes, которые называются simple и complex. Их можно увидеть, дважды щелкнув на ассете Static Mesh, зайдя таким образом в соответствующий редактор (в панели инструментов есть кнопка Collision). Оказалось, что в моем ассете под названием SM_Floor присутствовал complex collision, но отсутствовал simple collision. Решил проблему выбор опции Details > Collision > Customized Collision = Use Complex Collision As Simple
(рис. 8.2).

При размещении объектов на сцене важны настройки Snapping Modes (рис. 9), которые позволяют привязывать объекты к сетке и к поверхностям других объектов, что удобно для размещения объектов на поверхности ландшафта.

C++ API
Поведение объектов (actor’ов) в игре программируется путем добавления кусочков кода на C++ — компонентов (actor’s components). Мне, поскольку я программирую на C#, сразу вспомнилась библиотека Microsoft.Xaml.Behaviors, где для добавления поведения к объектам используется такой же принцип. Для добавления компонента выбираем actor’а и кликаем на кнопке Add actor component C++. Рыба компонента включает в себя две основных функции: BeginPlay() и TickComponent() — например, ниже показан код компонента (только заголовочный файл), который применяется к объекту «дверь» для ее открывания и закрывания:
#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "OpenDoorComponent.generated.h"
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class MYFLAT_API UOpenDoorComponent : public UActorComponent
{
GENERATED_BODY()
public:
// Sets default values for this component's properties
UOpenDoorComponent();
protected:
// Called when the game starts
virtual void BeginPlay() override;
public:
// Called every frame
virtual void TickComponent(
float DeltaTime,
ELevelTick TickType,
FActorComponentTickFunction* ThisTickFunction) override;
}
Сразу заметим, что очень многие классы в UE содержат виртуальные функции BeginPlay и Tick. BeginPlay вызывается движком после того, как все объекты в игровом уровне были созданы (т. е. отработали их конструкторы). Если вы выполняете инициализацию объекта, то подумайте хорошенько, куда поместить эту инициализацию, в конструктор класса или в функцию BeginPlay. Ведь на момент когда отрабатывает конструктор класса, еще не все объекты игрового уровня могут созданы. С функцией Tick всё просто — она вызывается на каждой итерации цикла отрисовки (т. е. каждый кадр).
Есть функции API Unreal Engine, с которыми полезно ознакомиться в первую очередь. Макрос UE_LOG позволяет писать сообщения в окне Message Log программы Unreal Editor. Принцип работы макроса — такой же, как у функции printf. Вот например, как распечатать имя Actor’а, к которому применен компонент (Actor Component):
Обратите внимание на макрос TEXT, который нужен для того, чтобы строковые литералы были в Юникоде. И на то, что для распечатывания строки к возвращаемому значению функции FString GetName()
должен быть применен оператор разыменования (*).
Важная функция AActor* UActorComponent::GetOwner();
— она позволяет получить указатель на того Actor’а, к которому применен компонент.
Еще один макрос UPROPERTY, который применяется к полям класса и позволяет Unreal Editor’у отображать свойства объектов, а пользователю, соответственно, изменять их значения без перекомпиляции исходного кода. Например, угол открывания двери можно задать в классе UOpenDoorComponent вот так:
UPROPERTY(EditAnywhere)
float OpenAngle = 90.0f;
Важные типы данных
FString | Mutable string |
---|---|
TArray | Контейнер, аналогичный std::vector , но реализующий множество алгоритмов в виде функций-членов (например, Sort, Find, Heapify и пр.) |
FRotator | Инкапсулирует информацию о повороте объекта в форме Yaw-Pitch-Roll в градусах |
AActor | Базовый класс для всех объектов, которые могут быть размещены в игровом уровне. Поведение Actor’а может создаваться и модифицироваться через коллекцию компонентов ActorComponents. |
UActorComponent | Базовый класс для компонентов, которые могут быть добавлены к любому Actor’у. |
UPrimitiveComponent | Содержит геометрические данные, которые либо отрисовываются на экране (StaticMeshComponent и SkeletalMeshComponent), либо используются для коллизий (ShapeComponent). |
UWorld | Является контейнером для всех Actor’ов и компонентов, которые могут быть отрисованы. Может содержать один или несколько уровней. |
UInputComponent | Позволяет Actor’у связывать функции с событиями ввода. |
APlayerController | Actor, который используется игроком для контроля над персонажем (Pawn). Воспринимает события персонажа и модифицирует его поведение. |
APawn | Базовый класс для всех Actor’ов, которые могут управляются игроком или искусственным интеллектом |
ADefaultPawn | Класс, производный от APawn, обладающий сферической коллизией и умеющий летать |
FVector | Вектор в 3-хмерном пространстве (X, Y, Z) |
UPhysicsHandleComponent | Позволяет перемещать физические объекты |
ATriggerVolume | Объем, который размещается в игровом уровне и может быть использован для обработки событий попадания Actor’ов в указанный объем |
Разные полезные функции
FString UObjectBaseUtility::GetName() |
UObject
|
Возвращает имя объекта |
FRotator AActor::GetActorRotation() |
GameFramework/Actor.h |
Возвращает информацию об ориентации объекта (Yaw, Pitch, Roll) |
bool AActor::SetActorRotation(
|
GameFramework/Actor.h |
Задает ориентацию объекта (Yaw, Pitch, Roll) |
UActorComponent*
|
GameFramework/Actor.h |
Находит первый компонент заданного типа в массиве компонентов |
UWorld* AActor::GetWorld() |
GameFramework/Actor.h |
Возвращает указатель на объект World |
float UWorld::GetTimeSeconds() |
Engine/World.h |
Возвращает время в секундах с момента начала игры |
T FMath::Lerp(
|
Math/UnrealMathUtility.h |
Выполняет линейную интерполяцию между двумя значениями |
float UPrimitiveComponent::GetMass() |
Components
|
Возвращает массу компонента в килограммах |
FInputActionBinding& UInputComponent::BindAction(
|
Components
|
Привязывает функцию к операции, определенной в настройках проекта |
TArray<AActor*>&
|
GameFramework/Actor.h |
Возвращает список actor’ов, которые перекрываются с данным actor’ом |
void UAudioComponent::Play() |
Components
|
Воспроизводит звук компонента Audio Component |
bool UWorld::LineTraceSingleByObjectType(
|
Engine/World.h |
Выполняет hit test (какие объекты задевает линия, проведенная из Start в End) |
UPrimitiveComponent*
|
Engine/EngineTypes.h |
Returns the Component that was hit |
AActor* FHitResult::GetActor() |
Engine/EngineTypes.h |
Returns the Actor that owns the Component that was hit |
T* UWorld::GetFirstPlayerController() |
Engine/World.h |
Возвращает контроллер игрока |
void APlayerController::GetPlayerViewPoint( |
GameFramework
|
Возвращает позицию и ориентацию игрока |
void UPhysicsHandleComponent
|
PhysicsEngine
|
Задает целевую позицию схваченного (grabbed) объекта |
void UPhysicsHandleComponent
|
PhysicsEngine
|
Схватить (grab) заданный компонент за указанную точку |
void
|
PhysicsEngine
|
Отпустить схваченный компонент |
void DrawDebugLine(
|
DrawDebugHelpers.h |
Нарисовать линию в 3-хмерном пространстве (для отладочных целей). Пример вызова функции:DrawDebugLine(
|
Изменение GameMode и DefaultPawn
В ходе создания игры так или иначе придется модифицировать персонажа (pawn), которым управляет игрок. Изменить его можно через диалог Project Settings, но сделать это можно только предварительно изменив объект GameMode. Сначала придется создать новый класс, унаследовав его от GameModeBase. Чтобы это сделать, запускаем игру (кнопка Play), и тогда в окне World Outliner появятся объекты GameModeBase и DefaultPawn (рис. 10-11). Выбираем в этом окне GameModeBase (рис. 10) и в меню Blueprints выбираем Convert Selection to Blueprint Class… (рис. 12). В открывшемся диалоге в поле Blueprint Name задаем имя класса и кликаем на кнопке New Subclass. После создания нового класса производного от GameModeBase нужно в диалоге Project Settings изменить выбор в списке Default Game Mode на только что созданный класс (рис. 13).




Теперь можно изменить Default Pawn — персонажа, управляемого игроком. Для начала создадим класс, производный от DefaultPawn. Для этого запускаем игру в редакторе (кнопка Play) и в окне World Outliner выбираем объект DefaultPawn (рис. 11), после чего выбираем меню Blueprints > Convert Selection to Blueprint Class...
. Затем, как и в случае с GameModeBase, придумываем имя для нового класса и нажимаем New Subclass (рис. 14).

После создания нового класса, производного от DefaultPawn, его можно редактировать — для этого нужно дважды щелкнуть на значке класса в окне Content Browser (рис. 15). После этого можно, например, добавлять к нему компоненты (рис. 16).


Теперь, когда у нас есть свой класс, унаследованный от DefaultPawn, мы можем сделать его активным в окне Project Settings (рис. 17). Кстати, в процессе игры (после нажатия на кнопку Play) можно открепить камеру от персонажа DefaultPawn и посмотреть со стороны на то, что он собой представляет — для этого нужно нажать кнопку Eject (рис. 18) или нажать клавишу F8 (чтобы привязать камеру обратно к DefaultPawn, нужно нажать кнопку Posess или клавишу F8).


Input & Action Mapping
Чтобы привязать события ввода к конкретным функциям в коде на C++, нужно прежде всего дать этим событиям имена, на которые можно будет потом ссылаться из кода на C++. Для этого заходим Project Settings > Input > Action Mappings [+]
и добавляем события (рис. 19).

После того, как мы дали имена интересующим нас событиям ввода, можно привязывать к ним функции-обработчики при помощи вызова функции UInputComponent::BindAction
примерно так:
/* ActionName */ "Grab",
/* KeyEvent */ IE_Pressed,
/* Object */ this,
/* Func */ &UGrabber::Grab);
InputComponent->BindAction(
/* ActionName */ "Grab",
/* KeyEvent */ IE_Released,
/* Object */ this,
/* Func */ &UGrabber::Release);
Гравитация
Если мы хотим, чтобы объекты в игре вели себя в соответствии с законами механики (т. е. падали под действием гравитации, отскакивали при столкновении и пр.), нужно сделать некоторые настройки соответствующих объектов в окне Details (рис. 20): нужно поставить галки Simulate Physics и Enable Gravity а также назначить объекту массу в килограммах. Массу объекта можно затем получить программно, например, при помощи вызова AActor::FindComponentByClass<UPrimitiveComponent>()->GetMass()
.

Коллизии
Объекты в играх часто взаимодействуют путем непосредственного соприкосновения. Также различные события в играх происходят в момент попадания того или иного объекта в то или иное место. Механизм определения того, перекрываются ли два 3-хмерных объекта, называется collision detection. Для того, чтобы этот механизм можно было использовать для конкретного объекта, нужно опять же настроить объект (рис. 21) — поставить галку Generate Overlap Events.

Если необходимо определять, попадает ли объект в определенное место (определенный объем), можно разместить в игровом уровне объект TriggerVolume (рис. 22). Определить объекты, которые попадают в указанный объем можно например путем вызова функции AActor::GetOverlappingActors(out TArray<AActor*>&)
.

Импорт ассетов в проект
Можно импортировать ассеты в проект вручную из окна Content Browser (меню Import to…). Можно импортировать целые пакеты ассетов через Epic Games Launcher, как показано на рис. 23.

Также можно импортировать пакеты ассетов, выбрав Content Browser > Add/Import > Add Feature or Content Pack...
Спасибо тебе огромное за консолидацию всей информации! Очень полезно для начинающих программистов.