March 7, 2022

С++ & Unreal Engine

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

Сперва, среда, в которой будем работать: не Visual Studio. Он медленный и спряжение с движком такое себе. Отличная альтернатива: JetBrains Rider.
На момент написания есть бета-версия, которая бесплатна и позволяет пользовать весь функционал без ограничений. Преимущества описывать смысла не имеет: он лучше буквально во всём.

Заметки.

.h

Header. В таких файлах формируется структура:
Constructor (Construction Script), переменные

Для классов Pawn там же лежат PlayerInput-ы, принимающие ввод от игрока
В header файле так же регистрируются функции, но сам функционал их прописывается в .cpp

.cpp

Там лежит вся логика, к примеру то, что исполняется во время Tick (каждый кадр), BeginPlay

Build/Run

После написания логики необходимо скомпилировать код. IDE проверит на ошибки и если всё отлично: скажет что компиляция успешно завершена. За это отвечают пара кнопок в верхней правой части экрана:

Быстрые обновления без перезагрузки движка

- Из Rider запускаем движок с помощью Run
- Удостоверяемся, что включен LiveCoding на стороне Unreal
Теперь, когда движок запущен и LiveCoding включен, можно нажимать лишь кнопку быстрой рекомпиляции из движка после внесения изменений в Rider. Так же, как если бы жмакали в блюпринтах Compile перед тестом.

Запустили движок из Rider, теперь видим, что в Rider горит зеленая плашка - движок запущен
В движке кнопка снизу быстро рекомпилирует внесенные изменения.

Полезные ссылки для освоения:

Логгирование в консоль:

https://unrealcommunity.wiki/logging-lgpidy6i

https://www.tomlooman.com/unreal-engine-ufunction-specifiers/

https://www.unrealengine.com/en-US/blog/c-for-blueprinters

https://docs.unrealengine.com/4.27/en-US/ProgrammingAndScripting/ProgrammingWithCPP/IntroductionToCPP/

https://unrealcpp.com/

https://unreal.blog/

https://unreal.gg-labs.com/

https://www.stevestreeting.com/2020/11/02/ue4-c---interfaces---hints-n-tips/

https://www.develop4fun.it/glossario/unreal-engine-fmathfinterpconstantto/

https://cpp.hotexamples.com/examples/-/UWorld/GetDeltaSeconds/cpp-uworld-getdeltaseconds-method-examples.html

https://nerivec.github.io/old-ue4-wiki/index.html#cat-Code

https://www.unrealengine.com/en-US/blog/ue4-libraries-you-should-know-about

Начинка классов.

.h файл класса Pawn, который я делал по уроку с небольшими изменениями

Декларация (регистрация/объявление о существовании ) функции, у которой на выходе нет никаких переменных, но внутри которой происходит какая-то логика, происходит посредством использования конструкции:
void название функции(тип переменной Значение);

Для быстрого создания функционального блока внутри .cpp файла, чтобы прописать начинку и функционал объявленной фукции в Rider используется функционал Alt+Space на выделенном наименовании. Либо через правую кнопку.

Generate Definition создаст необходимый блок внутри .cpp, куда можно будет записывать весь необходимый фукнкционал, а на входе будет ожидать переменную(значение) вида Float, которая здесь обозначена как AmountOfFantasticness.

Символ точки с запятой в конце завершает логический блок, очень внимательно к этому!

Декларация переменных, их типа, их свойств происходит так же в .h классе. Компоненты начинаются с заглавной U (UStaticMeshComponent например) в виде

UPROPERTY(ОткудаВидим, РедактируетсялиВБлюпринтах, Категория) ТипКомпонента* Название; UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Player") UStaticMeshComponent* MeshComp;

По-умолчанию те компоненты объекта, что будут задекларированы/объявлены в .h (хедер) файле, будут в памяти, будут иметь ссылки, но не будут видимы. Поэтому, чтобы отобразить компонент, помимо регистрации в хедер файле необходимо инициализировать компонент в .cpp файле, чтобы он стал видим и операбелен.

.cpp файл, в котором инициализирован компонент SpringArm и добавлен правильный #include

Назначение параметров внутри компонента.

-> служит для назначения одного из параметров внутри компонента.
Достаточно вписать тот же SpringArmComp и поставить стрелку, как Rider тут же предложит варианты:

Как добавить новую функцию?

Сначала нужно задекларировать наличие функции в .h файле
Допустим, это будет функция для включения фонарика:

Затем, добавить тело функции в .cpp файл, в которое занесём весь нужный нам функционал. Для теста я добавил здесь обычный PrintString, который выведет тестовую строчку текста на экран. Добавляю тело для фукнции, которая вызывается "единоразово" в секцию BeginPlay():

Важная заметка:
Есть разница между void ASuperPawn::ToggleFlashLight()
и между void ToggleFlashLight()
Заключается в том, что без указания класса, в котором функция создана, она будет абстрактной и в моем случае отображала ошибку. В дополнение к этому, используя горячие клавиши (бинды) я без указания ASuperPawn:: не смог вызывать эту фукцию, она была "невидима" для кода.

После, добавлю нужные бинды в настройках проекта, в секции Input:

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

Вуаля!

Как добавить интерфейс

Необходимо первым делом создать класс интерфейса в С++ из представленных шаблонов (делается из движка, как и другие классы), затем, в .h файл интерфейса добавить нужные связки. Все связки делаются типом
UFunction(BlueprintCallable, BlueprintImplementableEvent, Category="YourAmazingInterface")

После чего, этот интерфейс будет виден из движка:

Вызов интерфейса (к примеру, через LineTrace, происходит так:

Как сделать LineTraceByChannel

Как вывести сообщение на экран (Print String)

GEngine->AddOnScreenDebugMessage(-1, 2.5f, FColor::White, "Amazing Message!");

Как конвертировать значения в FString

int32 -> FString

FString TestString = FString::FromInt(MyInt);

float -> FString

FString TestString = FString::SanitizeFloat(MyFloat);

FString -> FName

TestHUDName = FName(*TestHUDString);

FString -> FText

TestHUDText = FText::FromString(TestHUDString);

FText -> FString

TestHUDString = TestHUDText.ToString();

https://docs.unrealengine.com/4.27/en-US/ProgrammingAndScripting/ProgrammingWithCPP/UnrealArchitecture/StringHandling/

Как создать и добавить виджет на экран

в .h файл добавляем ссылки на класс виджета и на сам объект виджета:
(в моем случае название виджета - PauseMenu)

После, в .cpp файле выполняем следующий код создания и добавления:

Позже, мне захотелось передавать значение в функцию, когда я нажимаю кнопку, но просто так этого нельзя сделать, используя примерно вот такой шаблон:
PlayerInputComponent->BindAction("InteractMouse", IE_Pressed, this, &ABuilderPawn::BeginInteract);

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

в .cpp я изменил эту функцию, скорректировав ввод в нее параметра boolean:

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

Draw Debug Box

DrawDebugBox(GetWorld(), CollisionBoxOrigin, CollisionHalfSize, FColor::Yellow, true, 2, 0, 0.5);

Как подцепить элемент из Widget Blueprint


Используем мета-тег! Имя объекта и класс должны совпадать в блюпринте, чтобы связка заработала, иначе возможны вылеты.

UPROPERTY (meta = (BindWidget))
class UTextBlock* InGameMessage;

Как сделать Cast в класс

Допустим. Есть два объекта - родительский и дочерний. Мы лучом попали в какой-то объект. Пусть это будет AActor класс (почти-любой объект в игровом мире). Но мы хотим сделать каст по дочернему классу от него. Ссылка на объект имеется, допустим вида

AActor* ObjectOfInterest = HitResult.GetActor();

Из конструкции видно, что ObjectOfInterest имеет класс AActor (пока что).
Теперь, допустим нам надо сделать каст в другой класс, который является дочерним от AActor*, к примеру APowerfulHeadset*. Решаем так:

class APowerfulHeadset* Headset = Cast<APowerfulHeadset>(ObjectOfInterest);

Такая конструкция сразу сделает каст и в случае успеха -
назначит переменную Headset

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

Как доставать переменные из класса

Продолжая предыдущий абзац, представим, что мы добавили несколько переменных в наш класс Headset - (записываем и объявляем в .h):

// Номер играющего трека
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MainInfo")
int32 SongNum;
// Уровень громкости
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MainInfo")
float Volume;

Достаём эти переменные вот таким образом:

float CurrentVolume = Headset->Volume;
int32 CurrentTrack = Headset->SongNum;


Полезные классы и функции

UGameplayStatics::
UKismetSystemLibrary::