Публикую недописанную, но очень длинную заметку про Unreal Engine 4. Буду дописывать по ходу дела, ибо неизвестно, что будет дальше.
- Migrating assets from one Unreal project to another
- Абракадабра в Compilation Log в Unreal Editor
- Глюки Intelli Sense в Visual Studio Code
- Как создать класс в Unreal Engine
- Компоненты
- Forward declaration in C++
- UPROPERTY
- Camera and SpringArm
- Pawn & Character
- Possession of a Pawn
- Input
- Коллизии
- Отладка в UE — DrawDebugHelpers
- Кто такой Controller?
- Line Tracing
- Timers
- Spawn
- Reflection & Runtime Type Information в UE4
- Полезные компоненты
- HitEvents
- Dealing With Damage
- Death & Game Over
- Интерфейс между C++ и блюпринтами
- Widgets
- Переменные в блюпринтах на примере виджетов
- Particle Effects
- Sound Effects
- Camera Shake
- Animations
- Разное
Migrating assets from one Unreal project to another
Если нужно переместить ассеты из проекта Source в проект Destination, то это можно сделать так: откройте проект Source в Unreal Editor. В Content Browser кликните правой кнопкой мыши (ПКМ) по папке, содержащей ассеты. в контекстном меню выберите Migrate, откроется проводник, в котором выберите папку Contents проекта Destination.
Абракадабра в Compilation Log в Unreal Editor
При запуске компиляции из Unreal Editor (кнопка Compile на панели инструментов) или при запуске Packaging’а, если возникали ошибки компиляции, то в моей системе наблюдались сообщения о них отображались в виде абракадабры ромбиков и вопросительных знаков. Связано это, понятно дело, с несоответствием кодировок текста в Unreal Editor и Visual Studio. Решение — удалить в папке установки Visual Studio папку 1049 (1049 — это код русского языка). Я удалил таким образом следующие четыре папки:
C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30037\bin\Hostx64\x86\1049
C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30037\bin\Hostx86\x64\1049
C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30037\bin\Hostx86\x86\1049
Глюки Intelli Sense в Visual Studio Code
Каждый раз при переносе с одной машины на другую проекта UE4 с VSCode в качестве среды разработки я получал ошибки Intelli Sense — ложные синтаксические ошибки, подчеркнутые красной волнистой линией (red squiggles). Причем упомянутое ранее расширение VSCode UE Intellisense Fixes Extension не решало проблему. Однако помогали следующие танцы с бубном:
- Удалить папку Intermediate
- Удалить папку Binaries
- Отредактировать файл c_cpp_properties.json
Что касается последнего пункта, то у меня файл c_cpp_properties.json после редактирования выглядел так:
"configurations": [
{
"name": "ToonTanksEditor Editor Win64 Development (ToonTanks)",
"intelliSenseMode": "msvc-x64",
"compileCommands": "C:\\DVSAV\\Projects\\UnrealEngine\\ToonTanks\\.vscode\\compileCommands_ToonTanks.json",
"cStandard": "c17",
"cppStandard": "c++17",
"compilerPath": "C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\Community\\VC\\Tools\\MSVC\\14.29.30133\\bin\\Hostx64\\x64\\cl.exe"
},
{
"name": "Win32",
"intelliSenseMode": "msvc-x64",
"compileCommands": "C:\\DVSAV\\Projects\\UnrealEngine\\ToonTanks\\.vscode\\compileCommands_Default.json",
"cStandard": "c17",
"cppStandard": "c++17",
"compilerPath": "C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\Community\\VC\\Tools\\MSVC\\14.29.30133\\bin\\Hostx64\\x86\\cl.exe"
}
],
"version": 4
}
Важно, что, если вы работаете в Windows, в качестве разделителя в пути к файлу должен использоваться двойной обратный слэш \\
вместо прямого слэша /
.
Также убедитесь, что в файлах compileCommands_Default.json и compileCommands_YourProjectName.json присутствуют команды компиляции для всех файлов исходного кода (папка Source вашего проекта). У меня почему-то случалось, что команды не генерировались для какого-нибудь вновь добавляемого класса. В таком случае команды легко написать самостоятельно:
...
{
"file": "C:\\\\DVSAV\\\\Projects\\\\UnrealEngine\\\\Sandbox\\\\Source\\\\Sandbox\\\\BasePawn.h",
"command": "cl.exe @"C:\\DVSAV\\Projects\\UnrealEngine\\Sandbox\\.vscode\\compileCommands_Default\\Sandbox.210.rsp"",
"directory": "C:\\Program Files\\Epic Games\\UE_4.27\\Engine\\Source"
},
{
"file": "C:\\\\DVSAV\\\\Projects\\\\UnrealEngine\\\\Sandbox\\\\Source\\\\Sandbox\\\\BasePawn.cpp",
"command": "cl.exe @"C:\\DVSAV\\Projects\\UnrealEngine\\Sandbox\\.vscode\\compileCommands_Default\\Sandbox.210.rsp"",
"directory": "C:\\Program Files\\Epic Games\\UE_4.27\\Engine\\Source"
}
]
Как создать класс в Unreal Engine
Исходный код в UE разрабатывается в виде классов, кои можно создавать как на языке C++, так и на графическом языке Blueptins.
Чтобы создать класс C++ в главном меню выбираем:
Чтобы создать Blueprint class, в панели инструментов выбираем:
Базовый класс для блюпринта можно даже поменять уже после создания последнего. Для этого надо открыть редактор блюпринта и в разделе Class Settings изменить опцию Parent Class.
Часто классы Actor’ов и не только создают так. Сначала создают базовый класс на C++, унаследовав его от другого базового класса в зависимости от того, что мы хотим (например, Actor, Pawn, Controller, GameModeBase и т. д.), затем создают блюпринт-класс и наследуют его от только что созданного класса С++. В классе C++ мы программируем поведение, которое удобно программировать на C++. А вот в редакторе класса блюпринта нам удобно будет задавать значения различных свойств класса по-умолчанию (раздел Class Defaults).
Компоненты
Многие вещи в UE делаются через компоненты — классы производные от UActorComponent. Например, при помощи компонента UStaticMeshComponent Actor’у назначается Static Mesh (или набор Static Mesh’ей), который будет отрисован в сцене; при помощи компонента UCapsuleComponent Actor получает геометрию коллизий; при помощи компонента UProjectileMovementComponent снаряд двигается в пространстве и т. д. и т. п.
Компоненты прикрепляются к Actor’ам и модифицируют именно Actor’ов. Некоторые компоненты непосредственно отрисовываются на экране (производные от UPrimitiveComponent) — к ним относится например UStaticMeshComponent — из таких компонентов вы собираете внешний вид вашего объекта или персонажа как из деталек конструктора. Существуют компоненты, которые не отрисовываются на экране, но непосредственно размещаются в трёхмерном пространстве Actor’а — компоненты, производные от USceneComponent — к ним относится например UCameraComponent. Заметим, что поскольку USceneComponent размещается в трехмерном пространстве, у него существуют функции, которые позволяют получать и изменять его пространственное положение, такие например, как USceneComponent::GetComponentLocation, USceneComponent::SetWorldRotation. Другие же компоненты, которые не отрисовываются на экране и не размещаются в трехмерном пространстве Actor’а, модифицируют поведение Actor’а, например UProjectileMovementComponent.
В UE существует два вида классов: C++ classes и Blueprint classes. Первые разрабатываются в виде текста на языке C++, вторые — на графическом языке блюпринтов. И к тем и к другим можно прикрепить компоненты. Начнем с классов C++. Добавление компонента к классу C++ включает следующие шаги:
1. Создать в классе приватное поле
{
private:
class USomeComponent* MyComponent;
}
2. Создать экземпляр компонента в конструкторе Actor’а при помощи функции UObject::CreateDefaultSubobject:
{
MyComponent = CreateDefaultSubobject<USomeComponent>(TEXT("My Component"));
}
В последствии можно получить указатель на добавленный компонент при помощи функции UObject::GetDefaultSubobjectByName. По-видимому, создание компонента при помощи функции CreateDefaultSubobject необходимо, поскольку именно эта функция делает компонент видимым для Unreal Editor.
3. Далее действия различаются в зависимости от типа компонента.
3.1. Если компонент является классом производным от UPrimitiveComponent, и следовательно определяет некую геометрию, то этот компонент должен быть либо прикреплен к уже имеющемуся UPrimitiveComponent либо являться RootComponent’ом. Компоненты производные от UPrimitiveComponent прикрепляются друг к другу и образуют дерево, корнем которого является RootComponent, который сам ни к какому компоненту не прикреплен. Назначить вновь созданный компонент RootComponent’ом легко и просто — для этого нужно в конструкторе Actor’а выполнить присваивание RootComponent = MyComponent;
. По-умолчанию у Actor’а уже есть компонент, играющий роль RootComponent — он называется DefaultSceneRoot. Можно прикрепить вновь созданный компонент к уже существующему UPrimitiveComponent’у (в том числе — к RootComponent’у) — для этого в конструкторе Actor’а нужно выполнить код: MyComponent->SetupAttachment(ExistingComponent);
.
3.2. Если компонент не является производным от UPrimitiveComponent, то ничего больше делать не нужно.
Добавление компонента к классу Blueprint включает следующие шаги в Blueprint editor:
1. В окне Components нажимаем кнопку [+Add Component] и выбираем компонент. Вновь созданный компонент отобразится в окошке Components.
2. Если компонент является производным от UPrimitiveComponent, то он автоматически прикрепится к RootComponent’у или к другому UPrimitiveComponent’у, который был выбран в окошке Components.
3. Если компонент является производным от UPrimitiveComponent, то его можно назначить RootComponent’ом — для этого нужно в окне Components перетащить его на место RootComponent’а.
4. Можно редактировать дерево компонентов путем перетаскивания их в окне Components.
Смысл идеи «прикрепления» (attachment) компонентов друг к другу таков же, как смысл прикрепления конечностей к торсу. Дочерние компоненты перемещаются в пространстве вместе со своим родительским компонентом.
Важный момент. Только RootComponent задает геометрию, которая проверяется на коллизии, поэтому имеет смысл назначать на роль RootComponent’а компонент CapsuleComponent, который предназначен как раз для проверки коллизий.
Если вы создаете свой класс компонента, то снабдите его макросом UCLASS( meta=(BlueprintSpawnableComponent) )
например так:
class MYFLAT_API UInteractComponent : public UActorComponent
{
...
}
Благодаря этом можно будет добавлять компонент к Actor’у в редакторе блюпринтов.
Forward declaration in C++
Я прежде встречался с использованием понятия forward declaration в C++, когда в коде имелось два класса, которые ссылались друг на друга:
#include "class_B.h"
class A
{
private:
B* PointerToClassB;
}
class A; // FORWARD DECLARATION - No need to #include "class_A.h"
class B
{
private:
A* PointerToClassA;
}
В этом случае, не будь forward declaration, получался бы замкнутый круг взаимных include’ов. Что касается Unreal Engine, то в заголовочных файлах классов, которые пишет программист, может встречаться много полей, которые являются указателями на другие классы. И чтобы облегчить жизнь себе и компилятору, можно применять к ним forward declaration.
class AMyActor : public AActor
{
private:
class A* PointerToClassA; // forward declaration
class B* PointerToClassB; // forward declaration
class C* PointerToClassC; // forward declaration
}
Тогда в заголовочном файле не придется include’ить заголовочные файлы классов A, B и C. Другой вариант того же самого:
class A; // forward declaration
class B; // forward declaration
class C; // forward declaration
class AMyActor : public AActor
{
private:
A* PointerToClassA;
B* PointerToClassB;
C* PointerToClassC;
}
Однако, если методы классов вызываются в методах класса AMyActor, то соответствующие заголовочные файлы придется заinclude’ить в файле исходного кода класса AMyActor:
#include "Class_A.h"
#include "Class_B.h"
void AMyActor::SomeMethod()
{
PointerToClassA->SomeMethodA();
PointerToClassB->SomeMethodB();
}
UPROPERTY
Часто процесс разработки состоит из следующих шагов:
- Создать класс C++.
- Создать класс Blueprint, унаследовав его от ранее созданного класса C++.
Класс C++ может содержать поля (обычно закрытые) — это могут быть, например, какие-то числовые величины и указатели на объекты (например, компоненты). Эти поля, разумеется, наследуются классом блюпринта, и часто эти поля должны быть доступны для редактирования из Blueprint Editor (окошки Class Defaults или Details). Чтобы поля стали доступны для редактирования из Blueprint Editor, нужно добавить к объявлению поля макрос UPROPERTY — это напоминает атрибуты в C#. UPROPERTY — это так называемый variadic macro — макрос с произвольным числом аргументов, а аргументами являются различные «спецификаторы», предназначенные для Unreal Header Tool — утилиты, которая обрабатывает исходный код перед компиляцией.
#define UPROPERTY(...)
Спецификаторы содержат метаданные: например, в каких редакторах доступно поле, название категории (поля группируются в редакторе по категориям).
Спецификаторы, определяющие доступность полей для редактирования в различных редакторах:
EditAnywhere | Поле можно редактировать в окнах Details и Class Defaults редактора. В редакторе можно изменять как значение поля по-умолчанию (т. е. то, которое по-умолчанию получат все экземпляры класса при их создании), так и значения поля для отдельных экземпляров класса. Когда макрос UPROPERTY применяется к указателю на объект, спецификатор EditAnywhere означает, что сам указатель можно изменять из редактора, т. е. выбирать объект, на который он будет указывать. |
VisibleAnywhere | Поле можно видеть в редакторе, но нельзя редактировать его значение. Когда макрос UPROPERTY применяется к указателю на объект, это означает, что в редакторе можно редактировать свойства самого этого объекта (как для объекта, который является значением поля по-умолчанию в окне Class Defaults, так и для объектов, которые являются значениями поля для отдельного экземпляра класса), но нельзя изменять значение указателя, перенаправляя его на другой объект. |
VisibleInstanceOnly | Поле можно видеть в редакторе (если поле — указатель, то также редактировать свойства объекта, на который он указывает) только для отдельных экземпляров класса. |
VisibleDefaultsOnly | Поле можно видеть в редакторе (если поле — указатель, то также редактировать свойства объекта, на который он указывает) только для значения по-умолчанию в окне Class Defaults. |
EditDefaultsOnly | Поле можно изменять в редакторе, но только его значение по-умолчанию в окне Class Defaults. |
Спецификаторы, определяющие доступность полей для редактирования в коде блюпринтов (т. е. в окне Event Graph):
BlueprintReadWrite | Поле может быть считано или записано в коде блюпринта. |
BlueprintReadOnly | Поле может быть только считано в коде блюпринта, но не может быть записано. |
Заметим, что поля, помеченные вышеперечисленными атрибутами, будут доступны как для классов-блюпринтов, производных от C++ класса, которому принадлежат эти поля, так и для сторонних блюпринтов.
Спецификаторы BlueprintReadWrite и BlueprintReadOnly по-умолчанию работают только для публичных и защищенных полей. Чтобы можно было применить эти спецификаторы к приватным полям, нужно добавить метаданые:
Однако, быть может, правильнее делать доступные для блюпринтов свойства защищенными, а не приватными.
Примеры:
UPROPERTY(EditAnywhere)
float Damage = 50.f;
UPROPERTY(VisibleAnywhere, BlueprintReadWrite)
class UStaticMeshComponent* Mesh;
UPROPERTY(VisibleAnywhere, Category = "Components")
class UStaticMeshComponent* Mesh;
UPROPERTY(VisibleAnywhere, Category = "Components", BlueprintReadOnly, meta = (AllowPrivateAccess = "true"))
class UStaticMeshComponent* Mesh;
Camera and SpringArm
Как правило, мы создаем Player Character, которым будет управлять игрок. Тип этого объекта-персонажа обычно либо Pawn, либо Character (который наследует Pawn). В играх от третьего лица, да и от первого тоже — мы хотим расположить камеру относительно персонажа. В играх от третьего лица — где-то за плечом персонажа, в играх от первого — в голове персонажа. Можно добавить компонент Camera к RootComponent — UE автоматически подхватит viewport этой камеры. Однако возникнет проблема — камера будет проходить сквозь стены, а иногда стены буду загораживать персонажа от камеры. Поэтому сначала к RootComponent добавляют компонент под названием Spring Arm и уже к нему прикрепляют компонент Camera (рис. 1).
Spring Arm ведет себя как пружина, к которой прикреплена камера. Длина пружины может изменяться в зависимости от того, насколько близко персонаж прижимается спиной к стенам. Но в любом случае Spring Arm будет работать так, чтобы персонаж всегда оставался в поле зрения. Не стоит изменять свойство Location камеры либо пружины. Взаимное расположение камеры, пружины и персонажа можно редактировать при помощи свойств SpringArm > Details > Target Arm Length
(длина пружины по-умолчанию) и SpringArm > Details > Camera > Socket Offset
а также поворота камеры (Camera > Details > Transform > Rotation
). Таким образом можно наблюдать персонажа с любого ракурса. Заметим, что редактировать свойство Rotation компонента Spring Arm бессмысленно, так как оно не оказывает на него никакого эффекта.
В этой заметке мы упоминаем классы Pawn и Controller. Оба являются производными от класса Actor, т. е. располагаются в трехмерном пространстве сцены и обладают своими собственными положением и ориентацией в пространстве. Также мы рассказываем про то, как осуществляется обработка пользовательского ввода для перемещения и поворота. Интересно, что функции для перемещения (AddMovementInput) перемещают Pawn, причем вместе с ним перемещается и Controller, и камера. Однако функции поворота (AddController<Yaw|Pitch|Roll>Input) поворачивают только Controller, но не Pawn. Так вот, все четыре участника этой истории (Pawn, Controller, SpringArm и Camera) могут синхронизировать повороты друг друга. Например, если мы, редактируя нашу Pawn в blueprint editor, отметим галку Class Defaults > Details > Pawn > [v] Use Controller Rotation Pitch
, то наш персонаж (Pawn) будет вращаться вместе с контроллером (в игре от 3-его лица это выглядит так, что когда вы смотрите вверх, персонаж ложится на спину, а когда вниз — на живот). Мораль: обратите внимание на следующие галки, которые влияют на синхронизацию поворота контроллера-персонажа-камеры-SpringArm (я приведу здесь значения галок, которые типичны для шутера от 3-его лица):
CameraComponent > Details > Camera Options > [ ] Use Pawn Control Rotation
SpringArmComponent > Details > Camera Settings > [v] Use Pawn Control Rotation
Pawn & Character
Pawn (пешка) — это Actor, которым можно управлять. Управлять может либо игрок, либо искусственный интеллект (формально управление осуществляет объект типа Controller). Character — это класс, производный от Pawn, который добавляет к последнему ряд компонентов:
├── CapsuleComponent (Root)
│ ├── ArrowComponent
│ └── MeshComponent
└── CharacterMovementComponent
Указанные компоненты также можно вручную добавить к классу, производному от Pawn.
Создав блюпринт-класс, производный от Character, вы обычно выбираете для него Mesh: MeshComponent > Details > Skeletal Mesh > Choose from assets
. Затем вы выравниваете CapsuleComponent относительно Mesh так, чтобы CapsuleComponent накрывал собою Mesh.
Possession of a Pawn
Каждый объект типа Pawn (пешка) должен кем-то управляться (be possessed) — либо игроком, либо искусственным интеллектом (AI). Можно легко сделать так, чтобы объект Pawn управлялся игроком, для этого надо выбрать этот объект в окне World Outliner, затем Details > Pawn > Auto Possess = Player0
. Игрок может управлять только одной пешкой, поэтому, если вы проделаете приведенные выше манипуляции с другой пешкой, то старая пешка перестанет быть управляема игроком.
Другой способ задать объект Pawn, которым будет управлять игрок: создать блюпринт-класс MyGameMode, производный от GameModeBase и выбрать его в качестве Default GameMode: Project Settings > Maps & Modes > Default Modes > Default GameMode = MyGameMode
. Другой вариант — задать GameMode только для текущего уровня: Settings > World Settings > Game Mode > GameMode Override
.
Далее есть два альтернативных варианта. 1 — В классе MyGameMode нужно задать нужный класс пешки Details > Classes > Default Pawn Class = YourPawnClass
. 2 — Выбрать класс пешки в окне Project Settings: Project Settings > Maps & Modes > Default Modes > Selected GameMode > Default Pawn Class = ...
(Заметим, что только блюпринт-классы, производные от GameModeBase могут иметь свойство PlayerController, которое можно установить таким образом. Т. е. с C++ классом такое не пройдет).
Такой способ позволяет не размещать пешку в сцене вручную, так как она создается автоматически в месте, которое указывает actor PlayerStart (а вот его-то как раз и нужно разместить в сцене).
Input
Ввод данных пользователем осуществляется через клавиатуру, мышь или джойстик. Каждой операции ввода (например, нажатию на клавишу клавиатуры или движению мыши) может быть назначена функция-обработчик. Программист определяет операции ввода, которые должна обрабатывать игра — в окне Project Settings > Input > Action Mappings | Axis Mappings
. Операции разделяются на две категории:
- Action mappings — однократное событие, такое как нажатие или отпускание клавиши на клавиатуре или щелчок мышью.
- Axis mappings — длящееся событие, такое как длительное нажатие на клавишу или такое как движение мыши.
В Project Settings > Input
вы добавляете нужное событие в список и придумываете для него имя, которое затем понадобится для привязки к нему функции-обработчика (рис. 2).
Любой класс, производный от APawn, может обрабатывать события ввода — для этого нужно переопределить функцию APawn::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
и вызвать внутри нее функции UInputComponent::BindAction
и(ли) UInputComponent::BindAxis
. Разумеется, это имеет смысл только для объектов, управляемых игроком.
ВАЖНО: КОГДА ВЫ ПЕРЕОПРЕДЕЛЯЕТЕ ФУНКЦИЮ, ОБЯЗАТЕЛЬНО ВЫЗЫВАЙТЕ ФУНКЦИЮ БАЗОВОГО КЛАССА Super::Foo()
— ИНАЧЕ МОЖЕТЕ ПОЛУЧИТЬ ТРУДНО ОТЛАЖИВАЕМЫЕ ГЛЮКИ.
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
PlayerInputComponent->BindAxis(
TEXT("MoveForward"),
this,
&AMyPawn::MoveForward);
PlayerInputComponent->BindAction(
TEXT("Shoot"),
IE_Pressed,
this,
&AMyPawn::Shoot);
}
void AMyPawn::MoveForward(float AxisValue) { ... }
void AMyPawn::Shoot() { ... }
Событие axis mapping сопровождается значением AxisValue, которое показывает в каком направлении и с какой силой осуществляется ввод, например, насколько резко пользователь переместил мышь, насколько сильно отклонил джойстик и т. п.
Что же должно писать в обработчиках событий ввода (главным образом для axis mappings)? Для обработки движения можно воспользоваться функциями AActor::AddActorLocalOffset или APawn::AddMovementInput (последнее работает по-умолчанию только для классов, производных от ACharacter, поскольку «Base Pawn classes won’t automatically apply movement, it’s up to the user to do so in a Tick event.»). Для поворотов можно воспользоваться функциями AActor::AddActorLocalRotation и(ли) APawn::AddControllerPitchInput, APawn::AddControllerYawInput, APawn::AddControllerRollInput.
Обработчики axis mappings могут выглядеть например так:
{
float DeltaTime = UGameplayStatics::GetWorldDeltaSeconds(this);
AddActorLocalOffset(
FVector
{
Value * DeltaTime * Speed,
0.0f,
0.0f
},
/*bSweep*/ true);
}
void AMyPawn::Turn(float Value)
{
float DeltaTime = UGameplayStatics::GetWorldDeltaSeconds(this);
AddActorLocalRotation(
FRotator
{
/*Pitch*/ 0.0f,
/*Yaw*/ Value * DeltaTime * TurnRate,
/*Roll*/ 0.0f
},
/*bSweep*/ true);
}
Функция AActor::AddActorLocalOffset перемещает Actor’а в его пространстве модели (а не в пространстве уровня) — точнее, перемещает его RootComponent, если он не равен NULL.
Обработчики событий ввода могут выглядеть и по-другому (как правило в программировании одну и ту же задачу можно решить разными способами):
{
AddMovementInput(
GetActorForwardVector() * AxisValue);
}
void AMyCharacter::LookRight(float AxisValue)
{
AddControllerYawInput(AxisValue);
}
void AMyCharacter::LookRightRate(float AxisValue)
{
AddControllerYawInput(
AxisValue * GetWorld()->GetDeltaSeconds());
}
На рисунке 2 показано, что для поворота существует по две версии событий ввода (LookUp и LookUpRate, Turn и TurnRate) — это потому, что события Gamepad’а обрабатываются иначе, чем события клавиатуры и мыши: чем больше у вас частота кадров (FPS), тем чаще генерируются события Gamepad’а, в то время как частота событий клавиатуры и мыши от FPS не зависят — поэтому в обработчиках событий Gamepad’а мы домножаем AxisValue на DeltaSeconds (см. код выше). Функция UWorld::GetDeltaSeconds возвращает количество секунд, прошедших с момента отображения предыдущего кадра. Заметим, что того же самого можно достичь при помощи функции UGameplayStatics::GetWorldDeltaSeconds, которая принимает в качестве параметра указатель на объект WorldContextObject — это любой объект, который находится в уровне (как правило, можно использовать указатель this).
Коллизии
Итак, мы нажимаем кнопку «вперед» на клавиатуре, и наш персонаж двигается вперед. Важно заметить, что если для перемещения персонажа мы пользуемся функцией AddActorLocalOffset, то при столкновении с другими Actor’ами в игре поведение нашего персонажа будет зависеть от параметра bSweep этой функции — только если он равен true, объект при перемещении будет проверяется на коллизии, и персонаж не будет, условно говоря, «проходить сквозь стены». Напомню, что на коллизии проверяется только RootComponent.
Вот хорошая статья о коллизиях в UE: Collision Filtering. Каждый объект, который может проверяться на коллизии (например, CapsuleComponent или StaticMesh) имеет три вещи: 1. Object Type (World Static, World Dynamic, Pawn, Physics Body etc.) 3. Реакция на другие типы объектов (Ignore, Overlap, Block). От этих настроек зависит то, как поведут себя два объекта, которые волею судеб оказались рядом, столкнутся ли они или пройдут друг сквозь друга. Эти настройки можно задать в меню Details > Collision Presets
. Совокупность настроек (галок) в меню Collision Presets называется Collision Profile, и этот profile может быть уникальным (custom) для данного конкретного Actor’а либо он может быть выбран из списка предопределенных профайлов (Block All, Physics Actor, Spectator и прочие).
Чтобы избежать глюков, убедитесь, что RootComponent вашего персонажа не перекрывается с полом игрового уровня. Также проверьте, не перекрываются ли ваш персонаж и дуло его оружия, а то он может убить сам себя.
Чтобы увидеть collision volumes статических мешей, можно во вьюпорте редактора выбрать меню (в левом верхнем углу) Player Collision или Visibility Collision — это может быть полезно при отладке. А чтобы во время игры посмотреть collision volumes персонажей и других Actor’ов, можно нажать на тильду (~) и в открывшейся консоли ввести show Collision
.
Отладка в UE — DrawDebugHelpers
В обычных программах для отладки часто используют вывод сообщений в консоль. В UE это тоже так — для этого используется функция UE_LOG. Но также полезно бывает рисовать отладочную информацию непосредственно в игровом уровне. Функции из заголовочного файла DrawDebugHelpers.h позволяют нарисовать в заданной точке пространства разные объекты: DrawDebugPoint, DrawDebugLine, DrawDebugSphere, DrawDebugCamera и другие. Каждая такая функция имеет параметр bPersistentLines — если он равен true, то нарисованный объект будет оставаться нарисованным неограниченно долго. Если же bPersistentLines=false, то в дело вступает параметр Lifetime, который задает время существования нарисованного объекта в секундах. Вот пример рисования сферы, которая исчезает через 3 секунды после своего появления:
/*InWorld*/ GetWorld(),
/*Center*/ ProjectileSpawnPointLocation,
/*Radius*/ 10.f,
/*Segments*/ 12,
/*Color*/ FColor::Red,
/*bPersistentLines*/ false,
/*Lifetime*/ 3.f);
Кто такой Controller?
Каждая пешка кем-то управляется (см. выше). В API это выражается в том, что у каждого Pawn есть свой Controller, который может быть либо производным от PlayerController, если пешка управляется игроком, либо производным от AIController, если пешка управляется искусственным интеллектом.
Классу, производному от PlayerController, обычно поручают обработку событий ввода от пользователя (у класса PlayerController есть виртуальная функция SetupInputComponent, которую можно переопределить). В примере обработки пользовательского ввода выше мы переопределяли функцию APawn::SetupPlayerInputComponent, но так делают в реже.
Класс, производный от AIController обычно должен реагировать на события в игре, на поведение персонажа игрока и пр. Например, вражеский AI должен охотиться за игроком и нападать на него. Для этого в классе, производном от AIController можно либо переопределить функцию AIController::Tick, в которой AI будет оценивать текущую ситуацию в игре и предпринимать соответствующие действия, либо в переопределенной функции AIController::BeginPlay запустить BehaviorTree, о чем пойдет речь существенно ниже в этой заметке.
Пешка может получить ссылку на контроллер, который ею владеет при помощи функции T* APawn::GetController
Контроллер может получить указатель на управляемую им пешку при помощи функции AController::GetPawn().
Как назначить пешке класс контроллера? Если мы хотим назначить класс контроллера персонажу игрока, то мы действуем через GameMode. Т. е. в блюпринте нашего кастомного GameMode идем в Details > PlayerController = класс контроллера
. Либо идем в Project Settings < Maps & Modes > Default Modes > Selected Game Mode > Player Controller Class = класс контроллера
. Если же мы хотим назначить контроллер для NPC (non-player-character), то идем в блюпринт нашего NPC Details > Pawn > AI Controller Class = класс контроллера
.
Если персонаж NPC был уничтожен, то AIController нужно отключить от пешки, которую он контролирует, в противном случае уже мёртвая пешка будет продолжать выпонять действия, запрограммированные в AIController. Отключить пешку от контроллера можно вызовом функции APawn::DetachFromControllerPendingDestroy().
Line Tracing
В играх часто приходится решать задачу нахождения точки пересечения некоей линии в пространстве и какого-либо объекта в том же пространстве. Или задачу определения набора объектов, которые пересекает заданная линия. Например: куда попадет пуля, выпущенная из оружия персонажа? Или: видит ли один персонаж другого? Или: в какое место игрового уровня указывает курсор мыши?
Еще раз упомянем статью Collision Filtering. Каждый Actor имеет свойство Object Type, которое может принимать ограниченный набор значений (World Static, World Dynamic, Pawn и т.д.). И у каждого Actor’а есть свой индивидуальный набор реакций — object responses (всего вариантов реакций три: Ignore, Overlap, Block) на столкновение с объектами каждого Object Type’а. Далее, когда два объекта действительно столкнутся, то выбор реакции осуществится так, чтобы была выбрана least blocking interaction (см. статью).
Но это еще не всё. В дополнение к Object Type’ам, есть еще и так называемые Trace Channels. У каждого Actor’а есть свой индивидуальный набор реакций — trace responses (вариантов реакций так же три: Ignore, Overlap, Block) на каждый trace channel. Здесь речь идет уже не о столкновении объектов, а о той самой задаче нахождения пересечения объекта и некой линии в пространстве. По-умолчанию в UE уже есть два trace channel’а: Visibility и Camera. Но мы можем добавлять свои trace channel’ы. Зачем это нужно. Ну например, нужно вам определить, находится ли персонаж в поле зрения противника — для этого подойдет канал Visibility — проводим линию между персонажем и противником и смотрим, пересекает ли она какие-либо объекты (если пересекает, значит персонаж вне поля зрения противника, спрятался за препятствием). Или нужно определить, в какой объект попадает пуля: проводим луч от дула оружия в направлении стрельбы и определяем, какие объекты он пересекает — для этого подойдет канал Bullet (который не существует по-умолчанию, поэтому мы должны его создать). Причем заметим, что каналы Visibility и Bullet отличаются — объекты реагируют на них по-разному: пуленепробиваемое стекло Block’ирует Bullet, но Ignore’ирует Visibility. Создать новый trace channel можно через окно Project Settings > Collision > Trace Channels
(рис. 3). В меню Presets можно настроить как каждый collision profile должен реагировать на наш trace channel (чтобы открыть окошко Edit Profile, нужно дважды щелкнуть на строчке нужного collision profile’а).
Первая функция, которую мы упомянем — UWorld::LineTraceSingleByObjectType — ray trace’ит объекты по их типу и дает нам структуру FHitResult, из которой можно получить например Actor, который «проткнула» наша линия trace (FHitResult::GetActor()), компонент этого Actor’а, который был «проткнут» (FHitResult::GetComponent()) и координаты точки «протыка» (FHitResul::Location). Вот пример вызова функции LineTraceSingleByObjectType:
FHitResult Hit;
GetWorld()->LineTraceSingleByObjectType(
/*OutHit*/ Hit,
/*Start*/ PlayerViewPointLocation,
/*End*/ LineTraceEnd,
/*ObjectQueryParams*/ FCollisionObjectQueryParams { ECollisionChannel::ECC_WorldDynamic },
/*Params*/ FCollisionQueryParams TraceParams
{
/*InTraceTag*/ FName(TEXT("")),
/*bInTraceComplex*/ false,
/*InIgnoreActor*/ GetOwner()
});
Вторая интересная функция — APlayerController::GetHitResultUnderCursor — она позволяет определить объект, на который указывает курсор мыши. Вот пример ее вызова:
GetController()->GetHitResultUnderCursor(
/*channel*/ ECollisionChannel::ECC_Visibility,
/*bTraceComplex*/ false,
/*OutHit*/ HitResult);
И третья функция UWorld::LineTraceSingleByChannel — она как раз задействует созданный нами Trace Channel. Вот пример ее вызова:
FCollisionQueryParams Params;
Params.AddIgnoredActor(this);
Params.AddIgnoredActor(GetOwner());
if ( GetWorld()->LineTraceSingleByChannel(
/*OutHit*/ Hit,
/*Start*/ Start,
/*End*/ End,
/*TraceChannel*/ ECollisionChannel::ECC_GameTraceChannel1,
Params) )
{
...
}
В эту функцию, нужно передать параметр enum ECollisionChannel, который соответствует созданному нами Trace Channel’у. Но как узнать какое именно значение перечисления ECollisionChannel соответствует созданному нами каналу? Для этого нужно в папку нашего проекта найти файл Config / DefaultEngine.ini
. В этом файле для созданного нами канала (в примере он называется Bullet) будет иметься такая строчка:
Стало быть нужное нам значение перечисления — это ECollisionChannel::ECC_GameTraceChannel1.
Timers
Таймеры в UE, как и в других приложениях, нужны для выполнения определенных действий через определенные промежутки времени или же для выполнения одного действия, но не сразу, а по прошествии заданного промежутка времени. Таймер можно создать-запустить и остановить, а можно поставить на паузу. Действие, которое будет выполняться по таймеру — это делегат, т. е. указатель на функцию, которая, если я не ошибаюсь, должна являться методом класса. Запуск-пауза-остановка таймера осуществляется вызовами методов класса FTimerManager (заголовочный файл «TimerManager.h»). При этом одновременно таймеров может существовать множество, и конкретный таймер идентифицируется при помощи хэндла FTimerHandle.
Запуск таймера выполняется вызовом функции FTimerManager::SetTimer, которая имеет несколько перегрузок. Отсановка таймера может быть выполнена либо вызовом функции FTimerManager::ClearTimer, либо вызовом функции FTimerManager::SetTimer с передачей ей отрицательного значения пероида таймера. Постановка таймера на паузу выполняется функцией FTimerManager::PauseTimer. Получить ссылку на объект FTimerManager можно вызовом функции AActor::GetWorldTimerManager() или UWorld::GetTimerManager().
Разные перегрузки функции SetTimer, которая запускает таймер, может принимать указатель на метод класса (и одновременно указатель на экземпляр класса), который будет вызываться по таймеру либо принимать объект FTimerDelegate, который содержит и всю вышеперечисленную информацию. Создать объект FTimerDelegate можно при помощи статической функции FTimerDelegate::CreateUObject(UserObject, Callback, Input parameters)
. К сожалению, на момент написания этой заметки почерпнуть какую-то информацию о параметрах этих функций можно скорее из исходного кода нежели чем из документации.
Spawn
Слово spawn означает «порождать». Порождать нам обычно приходится: вражеских персонажей; снаряды, вылетающие из оружия; particle effects, т. е. взрывы, искры, дымы и прочее; звуковые эффекты — удары, щелчки и прочее. Для всего этого есть соответствующие функции:
-
UWorld::SpawnActor
(UClass[, Location, Rotation]) — создать Actor’а. У функции есть ряд перегрузок, но, вообще говоря, она позволяет создать и добавить в игровой уровень объект заданного класса, задав ему нужно пространственное положение. Класс в общем случае задается при помощи объекта UClass (см. ниже). -
UGameplayStatics::SpawnEmitterAtLocation — создает и запускает particle system заданного класса в заданной точке пространства. Эффект проигрывается и исчезает.
UGameplayStatics::SpawnEmitterAttached — делает то же самое, что предыдущая функция, но прикрепляет particle system не к неподвижной точке пространства с заданными координатами, а к заданной точке (сокету — см. ниже) заданного компонента (компонент USeceneComponent принадлежит какому-либо объекту). -
UGameplayStatics::SpawnSoundAtLocation — создает и запускает звуковой эффект. Аналогична SpawnEmitterAtLocation выше.
UGameplayStatics::SpawnSoundAttached — создает и запускает звуковой эффект. Аналогична SpawnEmitterAttached выше.
Когда вы создаете (spawn’ите) Actor’а, крайне полезно вызвать для него функцию
AActor::SetOwner(AActor* NewOwner)
, чтобы созданный Actor мог впоследствии получить указатель на своего владельца, вызвав функцию AActor::GetOwner()
.
Reflection & Runtime Type Information в UE4
UClass — это аналог класса Type в С#/.NET или std::type_info в C++ — он содержит runtime type information или reflection information, если угодно. С помощью UClass можно создать экземпляр класса. Каждый класс в Unreal Engine имеет статическое свойство StaticClass, которое возвращает указатель на UClass, точно так же, как в С#/.NET любой класс имеет метод static Type GetType()
.
Часто в классах создают свойства типа UClass, чтобы дать себе возможность в редакторе выбирать например тип оружия и particle effect’ов. Если вы создадите в своем классе например такое свойство:
UClass SomeClass;
… то в редакторе вы сможете присвоить этому свойству ссылку на любой класс. Но, предположим, что вам нужно ограничить допустимый набор опций теми, которые являются производными от какого-то определенного класса, например AActor. Тогда вместо UClass можно использовать шаблонный класс TSubclassOf:
TSubclassOf<class AActor> SomeActorDerivedClass;
Полезные компоненты
UProjectileMovementComponent. Компонент будучи созданным внутри Actor’а при помощи функции UObject::CreateDefaultSubobject заставляет этого Actor’а двигаться как выпущенный из пушки снаряд (projectile). После создания компонента необходимо присвоить значения его свойствам InitialSpeed и MaxSpeed. Это пример компонента, который не размещается в трехмерном пространстве (т. е. не наследует классу USceneComponent) и не поддерживает функцию SetupAttachment, а лишь изменяет поведение объекта, к которому он применяется.
UParticleSystemComponent. Компонент, производный от USceneComponent, который размещается в трехмерном пространстве и который проигрывает заданный particle effect. Компонент следует создать (функция CreateDefaultSubobject), прикрепить к одному из компонентов объекта (SetupAttachment) и задать для компонента свойство UParticleSystem* UParticleSystemComponent::Template
— указатель на particle effect, который компонент будет воспроизводить. Такой компонент можно использовать например для создания дымового следа за выпущенным из пушки снарядом, так как particle effect будет перемещаться вслед за объектом, к которому он прикреплен.
HitEvents
Класс UPrimitiveComponent представляет собой геометрию, которая может быть использована как для отрисовки на экране, так и для коллизий. Т. е. в том числе для генерации событий столкновения с другими объектами (hit events). Производными от UPrimitiveComponent являются например такие классы как UStaticMeshComponent и UCapsuleComponent.
В UE4 есть понятие событий (аналогичное например понятию события в C#). Класс UPrimitiveComponent имеет событие OnComponentHit, которое генерируется при столкновении компонента с любым твердым телом. Подписка на событие осуществляется вызовом функции AddDynamic, которая принимает два параметра: указатель на объект, метод класса которого добавляется в invokation list события и сам указатель на метод класса (обработчик события). Сигнатура обработчика события OnComponentHit выглядит так:
UPrimitiveComponent* HitComp,
AActor* OtherActor,
UPrimitiveComponent* OtherComponent,
FVector NormalImpulse,
const FHitResult& Hit);
Важно: чтобы отборатчик был вызван, он доллжен быть снабжен макросом UFUNCTION() в своем объявлении в заголовочном файле.
В обработчике события OnComponentHit как правило полезно проверять, не столкнулись ли друг с другом компоненты одного и того же Actor’а — это можно сделать путем сравнения соответствующих указателей с указателем OtherActor.
Чтобы компонент, производный от UPrimitiveComponent, мог генерировать события столкновений, нужно разрешить ему это в редакторе блюпринтов: Выбираем компонент > Details > Collision > Collision Presets > Collision Enabled = Collision Enabled (Query and Physics)
.
Dealing With Damage
Часто геймплей состоит в том, что персонажи наносят друг другу повреждения, поэтому необходимо наладить учет здоровья персонажей а также обрабатывать ситуацию, когда это здоровье заканчивается.
В процессе нанесения повреждений выделяются следующие этапы:
- Sending damage (нанесение повреждения)
- Receving damage (получение повреждения)
С точки зрения API эти этапы могут реализовываться двумя путями.
Вариант 1
Нанесение повреждения Actor’у возможно путем вызова функции UGameplayStatics::ApplyDamage, которую можно вызвать например в упоминавшемся выше обработчике события OnComponentHit:
/*DamagedActor*/ OtherActor, // Actor that will be damaged.
/*DamageAmount*/ Damage, // The base damage to apply.
/*Instigator*/ MyOwner->GetInstigatorController(), // Controller that was responsible for causing this damage (e.g. player who shot the weapon)
/*DamageCauser*/ this, // Actor that actually caused the damage (e.g. the grenade that exploded)
/*DamageType*/ UDamageType::StaticClass() // Class that describes the damage that was done.
);
Чтобы получить указатель AController* Instigator, можно воспользоваться одним из методов APawn::GetController() или AActor::GetInstigatorController().
Получение же повреждения можно осуществить через обработку события AActor::OnTakeAnyDamage. О том, как подписываться на события рассказывалось выше. Обработчик события OnTakeAnyDamage может иметь следующую сигнатуру:
AActor* DamagedActor,
float Damage,
const UDamageType* DamageType,
AController* Instigator, // controller which caused the damage or nullptr (Instigator - подстрекатель, зачинщик)
AActor* DamageCauser)
Сама обработка события сводится к уменьшению здоровья персонажа. Здоровье можно хранить как в классе персонажа, так и в специально созданном компоненте (можно назвать таковой, скажем, HealthComponent). Если здоровье персонажа закончилось, то можно например удалить персонаж со сцены, вызвав AActor::Destroy.
Вариант 2
У класса AActor есть виртуальная функция AActor::TakeDamage, которую можно переопределить (не забудьте вызвать Super::TakeDamage). Нанести повреждение в таком случае означает вызвать эту функцию напрямую (например, в том же обработчике события OnComponentHit). Исходный код для нанесения повреждения может выглядеть как-то так:
Damage,
Hit,
ShotDirection,
nullptr);
AActor* DamagedActor = Hit.GetActor();
DamagedActor->TakeDamage(
/*float DamageAmount*/ Damage,
/*const FDamageEvent&*/ DamageEvent,
/*AController* EventInstigator*/ OwnerController,
/*AActor* DamageCauser*/ this);
Заметим, что функция TakeDamage принимает в качестве параметра ссылку на объект FDamageEvent, который хранит специфическую информацию о характере повреждения и его количественных характеристиках. Есть два класса, производных от FDamageEvent: FPointDamageEvent, FRadialDamageEvent, соответствующие различным видам повреждений.
Если здоровье персонажа закончилось и он отдал концы, то здесь может быть повод известить об этом событий класс GameMode, так как именно он назначен проверять условия проигрыша и выигрыша (если цель игры — уничтожить всех врагов и не помереть самому). Для этого в классе GameMode можно предусмотреть функцию, скажем, ActorDied или PawnKilled и т. п., которую вызывать в обработчике получения повреждений. Получить же указатель на GameMode можно двумя способами:
AMyGameGameModeBase* MyGameMode = GetWorld()->GetAuthGameMode<AMyGameGameModeBase>();
Заметим, что в каждом случае мы преобразовываем полученный указатель к нашему кастомному классу Game Mode.
Death & Game Over
Что делать с пешкой NPC, когда она погибает? Несколько вещей.
1. Прекратить ее активность, т. е. она должна перестать совершать какие-либо действия. Если действия пешки были запрограммированы в методе AActor::Tick, то следует отключить для данной пешки «тиканье» вызовом AActor::SetActorTickEnabled(false). Если действия пешки контролировались контроллером AIController, то нужно отсоединить контроллер от пешки вызовом APawn::DetachFromControllerPendingDestroy.
2. Уничтожить пешку вызовом AActor::Destroy(), в результате чего она пропадет со сцены. Либо не уничтожать пешку, но сделать активной анимацию, которая дает понять, что пешка убита (об анимациях см. ниже) а также отключить коллизии для убитой пешки вызовом GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::Type::NoCollision)
, чтобы остальные персонажи не спотыкались об нее.
3. Известить объект GameMode о том, что пешка убита, чтобы GameMode проверил критерий окончания игры (может быть, это была последняя пешка?).
Что делать с персонажем игрока, если он погибает?
1. Необходимо прервать связь между пользовательским вводом и действиями персонажа. Для этого можно вызвать APawn::DisableInput (см. также APawn::EnableInput) либо выполнит уже упомянутое выше отключение контроллера от пешки вызовом APawn::DetachFromControllerPendingDestroy.
2. Можно спрятать персонажа игрока вызовом AActor::SetActorHiddenInGame(bool). Можно также спрятать курсор мыши при помощи APlayerController::bShowMouseCursor = false;
.
3. 3. Известить объект GameMode о том, что пешка игрока убита, с тем, чтобы GameMode проверил критерий окончания игры (может быть, у игрока есть несколько жизней?).
Как узнать, что игра завершилась? Каков критерий завершения игры? Критерий определяет программист. Например, игра завершается, если персонаж игрока погиб либо если игрок уничтожил всех врагов. Чтобы понять, была убита именно пешка игрока, можно сравнить указатель на убитую пешку с указателем на пешку игрока, который можно получить вызовом UGameplayStatics::GetPlayerPawn(this, 0); либо можно проверить, управляет ли убитой пешкой контроллер, производный от APlayerController:
if (PlayerController)
// Player was killed
Если же убита пешка вражеского NPC, то можно проверить, остались ли еще на поле вражеские пешки, а для этого нужно их пересчитать. Это можно сделать например вызовом UGameplayStatics::GetAllActorsOfClass, если вражеские пешки принадлежат к одному классу:
UGameplayStatics::GetAllActorsOfClass(
/*WorldContextObject*/ this,
/*ActorClass*/ AEnemyPawn::StaticClass(),
/*OutActors*/ EnemyPawns);
auto NumberOfEnemies = EnemyPawns.Num();
Если вражеские пешки, будучи убитыми, не удаляются со сцены, то можно завести для них свойство вида IsAlive (или IsDead) и проверить, остались ли на поле пешки, у которых IsAlive==true (или IsDead==false). Чтобы перебрать всех Actor’ов заданного типа, можно воспользоваться классом TActorRange:
{
if (Enemy->IsAlive())
break; // There are still enemies to kill
}
Что нужно сделать, когда игра завершается? Если мы выяснили, что условие завершения игры выполнено, то мы должны сделать несколько вещей.
1. У класса AController есть виртуальный метод GameHasEnded. GameMode должен вызвать его для всех имеющихся в игре контроллеров, чтобы известить их об окончании игры.
{
Controller->GameHasEnded(
/*EndGameFocus*/ Controller->GetPawn(), // Actor to set as the view target on end game
/*bIsWinner*/ ... // true if this controller is on winning team
);
}
Контроллеры же внутри этого метода (который можно переопределить в производных классах) выполнят какую-то работу, связанную с окончанием игры. Например, контроллер игрока PlayerController может отобразить на экране виджет, показывающий результат игры (о виджетах — ниже).
2. Можно отобразить виджет с результатом игры.
3. Можно перезапустить игру, для чего вызвать APlayerController::RestartLevel, в результате чего в частности все Actor’ы будут respawn’ены. Перезапуск игры можно сделать по таймеру, чтобы игрок успел увидеть результат игры.
Интерфейс между C++ и блюпринтами
Выше упоминались атрибуты в макросе UPROPERTY, которые делали поля класса доступными для чтения или чтения-записи в блюпринтах. Что касается функций-членов класса, то их можно сделать «вызывабельными» из блюпринта (т. е. в блюпринте можно будет вызывать функцию, написанную на C++) а также «переопределяюельными» в классе блюпринта (т. е. функцию можно будет переопределить в классе блюпринта, производном от класса C++; при этом вызывать функцию можно будет из кода на C++; хотя по смыслу своему такая функция виртуальная, но снабжать ее прецификатором virtual не нужно). Делается это всё при помощи макроса UFUNCTION.
«Вызывабельной» функцию можно сделать, снабдив ее через макрос UFUNCTION одним из следующих атрибутов:
BlueprintCallable | Функция имеет то, что называется «execution pin» — это такие белые треугольнички в блюпринтах, которые соединяют белыми же линиями, и которые олицетворяют собой последовательность действий, т. е. поток управления. Такая функция, вообще говоря, может изменять состояние объекта, поэтому и важно какое место ее вызов займет в последовательности других действий. |
BlueprintPure | У функции, помеченной таким атрибутом, отсутствует «execution pin», поскольку она не изменяет объект. По той же причине она обязательно должна быть помечена как const. |
«Переопределябельной» функцию можно сделать, снабдив ее через макрос UFUNCTION одним из следующих атрибутов:
BlueprintImplementableEvent | У такой функции нет определения в классе C++ — есть только объявление. Переопределяется функция в классе блюпринта путем создания узла Event <Function Name> (рис. 4) |
BlueprintNativeEvent | То же, что и BlueprintImplementableEvent, но только на этот раз функция может иметь реализацию по-умолчанию на C++. |
Вот пара примеров применения вышеперечисленных атрибутов:
UFUNCTION(BlueprintImplementableEvent)
void GameOver(bool bWonGame);
UFUNCTION(BlueprintPure)
void IsDead() const;
Widgets
Виджеты — это двумерный графический элемент управления. В игре можно использовать такие элементы в качестве меню, для отображения уровня здоровья игрока, прицела, для индикации начала и окончания игры (надпись Game Over например) и прочего. В UE4 есть соответствующий класс ассетов — WidgetBlueprint.
Чтобы создать виджет, можно воспользоваться меню ContentBrowser > ПКМ > UserInterface > WidgetBlueprint
. Откроется редактор, в котором можно сконструировать виджет из набора стандартных элементов управления (Text, Progress Bar и пр.) — рис. 5.
Элементы управления мы перетаскиваем из окна Palette на поверхность виджета. Элементы управления образуют иерархию, когда одни элементы управления являются контейнерами для других — это напоминает мне библиотеку Windows Presentation Foundation. Дерево элементов управления отображается в окне Hierarchy.
По-умолчанию верхним элементом в иерархии является Canvas Panel. Можно например перетащить элемент управления на Canvas Panel, и он станет его дочерним элементом. Выбрав элемент управления, мы можем изменять различные его свойства в окне Details, некоторые из которых отвечают за внешний вид элемента, а другие — за позицию элемента внутри его родительского элемента управления (контейнера). Позиция элемента внутри Canvas Pane задается относительно Anchor’а («якоря»), который может быть выбран из выпадающего списка в меню Details > Slot (Canvas Panel Slot) > Anchors
. Позиция относительно anchor’а задается свойствами Details > Slot (Canvas Panel Slot) > Position X|Y, Size X|Y, Alignment X|Y
. Например, чтобы разместить элемент управления ровно по центру Canvas Pane, нужно задать Anchors=ПоЦентру, Position X=0, Position Y=0, Alignment X=0.5, Alignment
. Также полезна галочка Size To Content, которая подгоняет размеры рамки элемента управления в соответствии с содержимым элемента.
Y=0.5
У каждого элемента управления есть свои специфические свойства, например, для элемента Text свойство Justification задает выравнивание текста относительно рамок элемента (которые задаются свойствами Position X|Y, Size X|Y
— см. выше).
Как добавить виджет на экран из блюпринта? В коде блюпринтов (например в блюпринте GameMode) мы сначала создаем виджет при помощи узла Create Widget. У узла Create Widget есть вход Class, в котором можно выбрать из выпадающего списка один из имеющихся в проекте классов виджетов. Далее мы добавляем созданный виджет на экран передавая его на вход Target узла Add to Viewport. Чтобы удалить виджет с экрана, можно воспользоваться узлом Remove From Parent, подав ссылку на этот виджет на вход Target этого узла.
Как добавить виджет на экран из C++? WidgetBlueprint наследует классу UUserWidget. В C++ виджет сначала нужно создать при помощи функции CreateWidget(OwnerT*, TSubclassOf
Вызывая вышеуказанные функции, мы можем нарваться на ошибку компоновки вида:
Чтобы ее исправить, нужно найти в папке проекта файл Build.cs и добавить в нем модуль «UMG»:
Чтобы удалить виджет с экрана, можно вызвать его метод UUserWidget::RemoveFromViewport.
Заметим, что если мы создаем объект внутри класса, и внутри класса у нас имеется поле, которое хранит указатель на этот объект, то желательно снабдить это поле макросом UPROPERTY() — для того, чтобы этот объект был собран сборщиком мусора UE4.
Переменные в блюпринтах на примере виджетов
Блюпринты — это аналог классов в C++, программируемые при помощи одноименного графического языка. Поэтому в блюпринтах могут быть и функции-члены (методы), и переменные-слены (поля), которые могут быть приватными либо публичными. Виджеты — это блюпринты. Их внешний вид конструируется в окне Designer, а код (поведение) — в окне Graph. В окне Graph по-умолчанию уже созданы узлы событий Event Pre Construct (аналог конструктора), Event Construct (аналог метода BeginPlay) и Event Tick (аналог метода Tick). В окне Graph можно создавать переменные-члены и функции-члены (в окошке My Blueprint — рис. 6). Но содержимое отдельных элементов управления можно автоматически сделать/связать с переменной. Например, если выбрать в Designer’е элемент управления Text, и в окне Details выбрать галку [v]Is Variable, то автоматически будет создана переменная, которая называется так же, как элемент управления. Если эта переменная будет изменена тем или иным способом, то изменится и содержимое элемента управления Text.
Когда мы создали переменную в блюпринте, выбрав ее, мы сможем отредактировать ее свойства в окне Details, наиболее важные из которых: Variable Name, Variable Type и Default Value. Причем переменная получит свое значение по-умолчанию только после того, как вы нажмете кнопку Compile.
В окошке My Blueprint рядом с каждой переменной есть иконка «глаз». Если этот глаз открыт — значит переменная публичная, и к ней может получить доступ другой блюпринт, если у него есть ссылка на экземпляр блюпринта, которому принадлежит эта переменная. Если глаз закрыт, то, соответственно — не может.
Можно сделать так, чтобы значение содержимого (или иного свойства, цвета например) элемента управления вычислялось в каждом кадре (в каждом Tick’е). Яркий пример — шкала здоровья (Progress Bar). Правильно будет, ели значение, отображаемое в шкале здоровья обновлялось в каждом кадре. Поэтому должна существовать функция-член нашего блюпринта (назовем ее GetHealth), которая будет вызываться в каждом кадре. Этот механизм называется Binding (снова напоминает мне Windows Presentation Foundation). Чтобы задействовать binding, на примере Progress Bar, нужно выбрать Progress Bar, затем Details > Progress > Percent > Bind > Create Binding
(рис. 7) — автоматически будет создана функция-член и вы переместитесь в редактор Graph для написания ее кода.
Particle Effects
Эффекты частиц (взрывы, искры, дымы и прочее) являются ассетами. В любом классе C++ MyClass можно создать свойство:
{
private:
UPROPERTY(EditAnywhere)
class UParticleSystem* SomeParticleEffect;
}
… после чего создать производный класс BP_MyClass и в редакторе блюпринтов установить в окне Class Defaults значение для этого свойства. Затем в нужный момент можно создать (spawn) и воспроизвести эффект при помощи функции UGameplayStatics::SpawnEmitterAtLocation, которая уже упоминалась выше.
Другой вариант использования частиц — когда необходимо, чтобы Actor испускал частицы постоянно. Это можно сделать, прикрепив к Actor’у компонент UParticleSystemComponent, который тоже уже упоминался. Его можно создать (CreateDefaultSubobject) и прикрепить (SetupAttachment) в C++ либо сделать то же самое через редактор блюпринтов.
Sound Effects
Звуковые эффекты — это так же ассеты. Работать с ними можно аналогично вышеупомянутым эффектам частиц. В классе C++ можно создать свойство типа SoundBase:
{
private:
UPROPERTY(EditAnywhere)
class USoundBase* SomeSoundEffect;
}
Воспроизвести звуковой эффект можно, вызвав UGameplayStatics::PlaySoundAtLocation например так:
/*WorldContextObject*/ this,
/*Sound*/ SomeSoundEffect,
/*Location*/ GetActorLocation());
Camera Shake
Сотрясение камеры — это еще один эффект (и ассет), который может пригодиться, когда вам нужно создать например впечатление землетрясения или близкого взрыва. Существует базовый класс UMatineeCameraShake, от которого вы можете унаследовать класс блюпринта и уже в редакторе блюпринтов настроить параметры сотрясения камеры в окне Class Defaults. Основные параметры, которые нужно настроить:
Details > Oscilation > Loc Oscilation
В любом классе C++ можно создать свойство
{
private:
UPROPERTY(EditAnywhere)
TSubclassOf<class UCameraShakeBase> SomeCameraShakeClass;
}
… и в редакторе блюпринтов выбрать значение этого свойства в меню Class Defaults > Details
.
Далее в нужный момент можно воспроизвести эффект тряски камеры, вызвав APlayerController::ClientStartCameraShake например так:
Animations
Анимация — это движение персонажа. В принципе, персонаж двигается всегда, в том числе, когда стоит и ничего не делает (он может в это время переминаться с ноги на ногу). Анимации создаются путем изменения вершин Mesh’а. Эти изменения рассчитываются при помощи скелета (Skeleton), состоящего из связанных между собой костей (bones). SkeletalMesh поддерживает возможность такой анимации, StaticMesh — не поддерживает. Анимации, собственно, задают движение костей скелета во времени. Анимация состоит из ряда кадров (key frames) во времени. Каждый кадр задает определенное положение костей скелета. Положение костей в промежуточные моменты времени между кадрами вычисляется при помощи интерполяции между соседними кадрами. Сами анимации можно комбинировать (для этого они должны использовать один и тот же скелет), т. е. можно создать анимацию, которая будет являться линейной комбинацией других анимаций.
Выбор меша для персонажа осуществляется так: Pawn/Character > Blueprint Editor > Mesh Component > Details > Mesh > Skeletal Mesh = ...
. Выбор анимации персонажа — так: Pawn/Character > Blueprint Editor > Mesh Component > Details > Animation > Animation Mode = Use Animation Asset | Use Animation Blueprint, Anim Class = ...
.
Разное
Setting Editor Startup Map & Game Default Map in Edit > Project Settings… > Maps & Modes.
Button in the form of a globe: Cycles the transform gizmo coordinate system between local and world coordinate systems
Edit > Editor Preferences > Level Editor > Viewports > Look and Feel > [v] Preview Selected Cameras
Play > Simulate — play without possessing any pawns
Drop-down next to [Perspective] > Show FPS
File > New C++ Class > [v] Show All Classes
FRotator FVector::Rotation() — rotation corresponding to the vector
FVector::Dist() calcs distance
static APawn* UGameplayStatics::GetPlayerPawn()
AActor::GetActorLocation()
«Select» node > RMB on Return Value > Change Pin Type
Ctrl+W — duplicate the lastly created node
Ceil node
Switch on Int node > Details > [v] Has Default Pin
Print String node
Set Text node
Branch node
In the Content Browser select Blueprint > RMB > Duplicate
Don’t forget to connect execution wires!
Ceil node
If something is not working in Blueprint try to reload Unreal Editor.
Blueprint Editor > Get Owner Player Pawn node —> Cast To
Content Browser > RMB on a Blueprint class > Create a Child Blueprint Class
Частенько для того, чтобы найти нужный узел в редакторе блюпринтов, нужно в соответствующем контекстном меню снять галку Context Sensitive.
Чтобы камера двигалась плавно, с ускорением, а не сразу дергалась при нажатии на одну из клавиш ASWD: Spring Arm Component > Details > Lag > [v] Enable Camera Lag, [v] Enable Camera Rotation Lag
Создать дистрибутив игры (в том числе исполняемый файл): File > Package Project > Windows > New Folder (Build)