Unreal Engine CookBook — Part 1

В этой заметке я кратко конспектирую содержание курса Unreal Engine C++ Developer: Learn C++ and Make Video Games, который я прошел на Udemy.

Подготовка к работе

Создать проект C++
Импорт ассетов: in Content Browser > ПКМ > Import to...
Выбрать среду разработки Edit > Editor Preferences > Source Code

Рис. 1 — Выбор редактора исходного кода

Настройка 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

Рис. 2 — 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.

Рис. 3 — Material
Рис. 4 — Material Instance

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).

Рис. 5 — Binary Space Partitioning

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

Рис. 6 — Brush Editing Mode

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

Рис. 7 — Convert Geometry Brush to Static Mesh

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

Рис. 8.1 — Static Mesh Lightmap Issue

Другая проблема при запекании геометрии связана с коллизиями. Так например, когда я впервые запек 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).

Рис. 8.2 — Опция Use Complex Collision As Simple в редакторе static mesh

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

Рис. 9 — Snapping modes

C++ API

Поведение объектов (actor’ов) в игре программируется путем добавления кусочков кода на C++ — компонентов (actor’s components). Мне, поскольку я программирую на C#, сразу вспомнилась библиотека Microsoft.Xaml.Behaviors, где для добавления поведения к объектам используется такой же принцип. Для добавления компонента выбираем actor’а и кликаем на кнопке Add actor component C++. Рыба компонента включает в себя две основных функции: BeginPlay() и TickComponent() — например, ниже показан код компонента (только заголовочный файл), который применяется к объекту «дверь» для ее открывания и закрывания:

#pragma once

#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):

UE_LOG(LogTemp, Warning, TEXT("Owner's name is %s"), *GetOwner()->GetName());

Обратите внимание на макрос TEXT, который нужен для того, чтобы строковые литералы были в Юникоде. И на то, что для распечатывания строки к возвращаемому значению функции FString GetName() должен быть применен оператор разыменования (*).

Важная функция AActor* UActorComponent::GetOwner(); — она позволяет получить указатель на того Actor’а, к которому применен компонент.

Еще один макрос UPROPERTY, который применяется к полям класса и позволяет Unreal Editor’у отображать свойства объектов, а пользователю, соответственно, изменять их значения без перекомпиляции исходного кода. Например, угол открывания двери можно задать в классе UOpenDoorComponent вот так:

private:
    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
    /UObjectBaseUtility.h
Возвращает имя объекта
FRotator AActor::GetActorRotation() GameFramework/Actor.h Возвращает информацию об ориентации объекта (Yaw, Pitch, Roll)
bool AActor::SetActorRotation(
    FRotator NewRotation)
GameFramework/Actor.h Задает ориентацию объекта (Yaw, Pitch, Roll)
UActorComponent*
    AActor::FindComponentByClass<T>()
GameFramework/Actor.h Находит первый компонент заданного типа в массиве компонентов
UWorld* AActor::GetWorld() GameFramework/Actor.h Возвращает указатель на объект World
float UWorld::GetTimeSeconds() Engine/World.h Возвращает время в секундах с момента начала игры
T FMath::Lerp(
    const T& A,
    const T&B,
    const U& Alpha)
Math/UnrealMathUtility.h Выполняет линейную интерполяцию между двумя значениями
float UPrimitiveComponent::GetMass() Components
    /PrimitiveComponent.h
Возвращает массу компонента в килограммах
FInputActionBinding& UInputComponent::BindAction(
    const FName ActionName,
    const EInputEvent KeyEvent,
    UserClass* Object,
    FMethodPtr Func)
Components
    /InputComponent.h
Привязывает функцию к операции, определенной в настройках проекта
TArray<AActor*>&
    AActor::GetOverlappingActors()
GameFramework/Actor.h Возвращает список actor’ов, которые перекрываются с данным actor’ом
void UAudioComponent::Play() Components
    /AudioComponent.h
Воспроизводит звук компонента Audio Component
bool UWorld::LineTraceSingleByObjectType(
    FHitResult& OutHit,
    const FVector& Start,
    const FVector& End,
    const FCollisionObjectQueryParams& ObjectQueryParams,
    const FCollisionQueryParams& Params)
Engine/World.h Выполняет hit test (какие объекты задевает линия, проведенная из Start в End)
UPrimitiveComponent*
    FHitResult::GetComponent()
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(
    FVector& Location,
    FRotator& Rotation)
GameFramework
    /PlayerController.h
Возвращает позицию и ориентацию игрока
void UPhysicsHandleComponent
        ::SetTargetLocation(
             FVector NewLocation)
PhysicsEngine
    /PhysicsHandleComponent.h
Задает целевую позицию схваченного (grabbed) объекта
void UPhysicsHandleComponent
        ::GrabComponentAtLocation(
            UPrimitiveComponent* Component,
            FName InBoneName,
            FVector GrabLocation)
PhysicsEngine
    /PhysicsHandleComponent.h
Схватить (grab) заданный компонент за указанную точку
void
    UPhysicsHandleComponent
        ::ReleaseComponent()
PhysicsEngine
    /PhysicsHandleComponent.h
Отпустить схваченный компонент
void DrawDebugLine(
    const UWorld* InWorld,
    const FVector& LineStart,
    const FVector& LineEnd,
    const FColor& Color,
    bool bPersistentLines,
    float LifeTime,
    uint8 DepthPriority,
    float Thickness)
DrawDebugHelpers.h Нарисовать линию в 3-хмерном пространстве (для отладочных целей). Пример вызова функции:
DrawDebugLine(
    GetWorld(),
    PlayerViewPointLocation,
    LineTraceEnd,
    FColor{255, 0, 0},
    false,
    0.0f,
    0,
    5.0f);

Изменение 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).

Рис. 10 — Окно World Outliner объект GameMode
Рис. 11 — Окно WorldOutliner объект DefaultPawn
Рис. 12 — Меню Convert Selected Actor To Blueprint Class
Рис. 13 — Окно Project Settings, изменение выбора GameMode

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

Рис. 14 — New Pawn inherited from DefaultPawn

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

Рис. 15 — Двойной щелчок на значке DefaultPawn, чтобы отредактировать его
Рис. 16 — Добавление компонента PhysicsHandle к классу DefaultPawn

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

Рис. 17 — Окно Project Settings изменение Default Pawn Class
Рис. 18 — Открепить камеру от персонажа DefaultPawn — кнопка Eject

Input & Action Mapping

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

Рис. 19 — Input & Action Mappings

После того, как мы дали имена интересующим нас событиям ввода, можно привязывать к ним функции-обработчики при помощи вызова функции UInputComponent::BindAction примерно так:

InputComponent->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().

Рис. 20 — Gravity for StaticMesh

Коллизии

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

Рис. 21 — StaticMesh > Collision > Generate Overlap Events

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

Рис. 22 — Добавление TriggerVolume

Импорт ассетов в проект

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

Рис. 23 — Epic Games Launcher: Import Assets

Также можно импортировать пакеты ассетов, выбрав Content Browser > Add/Import > Add Feature or Content Pack...

Добавить комментарий

Ваш адрес email не будет опубликован.