🐨
Unreal Engine Tips
  • 🐦О сайте
  • 🐸Об авторе
  • Unreal Engine
    • 🦉C++
    • 💎Материалы
    • 🎆Niagara
    • 🦴Анимации
    • 🌞Управление уровнями
    • 🌴Game Design
    • 🐾Git
    • 🪱Workflow разработки
    • 🐙Coding Standard
    • 🦞Руководство по стилю
    • 🦋Рефакторинг
    • 🐞JetBrains Rider
    • 🐧Разное
    • 🐫Полезные ссылки
Powered by GitBook
On this page
  • Полезные функции и классы, которые не популярны
  • Макрос UE_LOGFMT
  • UMG Viewmodel
  • Использование шаблонных указателей на функции
  • Макрос TEXT() нужен только для FString и FText
  • HealthComponent должен сам обрабатывать урон
  • Архитектура не должна быть сложной для простых проектов
  • Пояснение по Single Responsibility Principle из SOLID
  • Переопределять конкретные функции - плохо
  • Создавать зависимости от неиспользуемого - плохо
  • Принцип устойчивых зависимостей и метрики управления зависимостями
  • Политики стоит группировать в компоненты по способам изменения
  • Передача данных между компонентами
  • Роль API-тестирования - скрыть структуру приложения от тестов
  • Уровень должен зависеть только от соседнего, нижележащего уровня
  • Закон Деметры
  • Вместо сырых UObject лучше использовать TObjectPtr
  • Оператор / для FString
  • SStarshipGallery.cpp
  • Композиция не всегда означает "содержит"
  • Любая "пометка или флаг", что произошло изменение, должны ставиться только после изменения
  • Стоит избегать возврата дескрипторов внутренних данных объекта (ссылки, указатели, итераторы)
  • Стоит избегать приведения типов
  • Объявляйте переменные, как можно позже
  • Все данные-члены стоит делать private
  • Хорошие интерфейсы легко использовать правильно и трудно - неправильно
  • Никогда не стоит вызывать виртуальные функции в конструкторе или деструкторе
  • Правила инициализации
  • Использование mutable
  • Использование EditCondition в UPROPERTY
  • Использование static_cast, const_cast и dynamic_cast
  • Желательно воздерживаться от определяемых пользователем функций преобразования типа
  • Если компилятор или IDE сообщает, что переменная не используется, то можно удалить её имя
  • Префиксные формы инкремента и декремента - предпочтительны
  • Никогда нельзя перегружать операторы &&, || и ,
  • Операторы присваивания (например, +=) обычно более эффективны, чем отдельные операторы (например, +)
  • Не стоит объявлять inline для виртуальных функций
  • Всегда стоит делать нетерминальные классы абстрактными
  • Желательно использовать модули в UE
  • PublicDependencyModuleNames или PrivateDependencyModuleNames?
  • Желательно использовать префиксы в названии классов
  • Добавлять надо только те компоненты в AActor, которые используются
  • Композиция или наследование?
  • Локализация
  • USTRUCT не собираются сборщиком мусора
  • Поиск ENUM по имени
  • Использование UPROPERTY(Instanced)
  • Получение случайной точки
  • Использование auto (или decltype(auto)) в качестве типа возвращаемого значения
  • Использование auto – предпочтительно
  • Использование static_cast и auto
  • Использование фигурной инициализации – предпочтительно
  • Всегда лучше использовать nullptr вместо 0 или NULL
  • Использование перечисления с областью видимости – предпочтительно
  • Удалённые функции стоит объявлять, как public
  • Использование std::unique_ptr – предпочтительно
  • Избегать создания указателей std::shared_ptr из переменных, тип которых – обычный встроенный указатель
  • Применение std::weak_ptr
  • Использование make-функций - предпочтительно
  • Когда параметр является универсальной ссылкой
  • Не всегда возвращаемое значение без ссылок и указателей является неэффективным
  • Применение std::move и std::forward
  • Избегать режима захвата по умолчанию в lambda-выражениях
  • Применение auto в lambda-выражениях приветствуется
  • Захваты в lambda-выражениях применяются только к нестатическим локальным переменным
  • Lambda-выражения могут принимать любое количество параметров
  1. Unreal Engine

C++

PreviousОб автореNextМатериалы

Last updated 4 months ago

Полезные функции и классы, которые не популярны

- функция, возвращающая нормализованный вектор направления от одной точки к другой.

- класс, позволяющий присоединять физические объекты к компонентам и физически корректно их двигать.

- функция, которая телепортирует Actor, и если он не помещается точно в указанное место, пытается немного выдвинуть его за пределы препятствия.

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

- возвращает InstigatorController для этого Actor.

- возвращает APlayerStart, который будет использоваться для следующего появления игрока.

- класс, который обеспечит вращение необходимого компонента.

- функция, позволяющая понять, кто управляет этим Actor: сервер или клиент.

- это Actor, который не имеет визуального представления и физики, но при этом имеет доступ к миру игры.

- возвращает true, если SpringArmComponent с чем-то столкнулся.

- класс с полезными функциями для читов.

- прячет кость (и всю её иерархию). Очень удобная функция для создания вида от первого лица, когда нужно спрятать голову, но оставить всё остальное.

- рисует текст в заданной точке.

- получает имя объекта без необходимости вызывать функцию через указатель.

- рассчитывает LaunchSpeed (например, для ), позволяющий достичь конкретной точки. Очень полезная функция для реализации прыжков AI.

Макрос UE_LOGFMT

Это более удобный вариант логирования:

UE_LOGFMT(LogPRJ, Display, "Name: {0}; Health: {1}", Name, Health);

UMG Viewmodel

Использование шаблонных указателей на функции

template<typename T>
using TStaticFuncPtr = typename TBaseStaticDelegateInstance<T, FDefaultDelegateUserPolicy>::FFuncPtr;

Макрос TEXT() нужен только для FString и FText

Для FName макрос TEXT() использовать не нужно.

HealthComponent должен сам обрабатывать урон

void UHealthComponent::BeginPlay()
{
    Super::BeginPlay();
    if (!GetOwner()) return;
    GetOwner()->OnTakeAnyDamage.AddDynamic(this, &ThisClass::DamageTaken);
}

Архитектура не должна быть сложной для простых проектов

Разработка архитектуры для уровня предприятия, когда в действительности нужен маленький и удобный инструмент для настольного компьютера, - это верный рецепт провала.

Пояснение по Single Responsibility Principle из SOLID

Модуль (класс, функция) должен иметь одну и только одну причину для изменения. Или модуль должен отвечать за одного и только за одного актора.

Переопределять конкретные функции - плохо

Лучше сделать функцию абстрактной и создать несколько её реализаций.

Создавать зависимости от неиспользуемого - плохо

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

Принцип устойчивых зависимостей и метрики управления зависимостями

Неустойчивость компонента: I = FanOut / (FanIn + FanOut), где FanOut - количество исходящих зависимостей, а FanIn - количество входящих зависимостей. I = 0 соответствует максимальной устойчивости компонента, а I = 1 - максимальной неустойчивости.

Зависимости должны быть направлены в сторону устойчивости или минимизации I. То есть метрики I должны уменьшаться в направлении зависимости. Другими словами, метрика I компонента должна быть больше метрик I компонентов, от которых он зависит.

Абстрактность: A = Na / Nc, где Na - число абстрактных классов и интерфейсов в компоненте, а Nc - общее число классов. A = 0 - означает полное отсутствие абстрактных классов, а A = 1 означает, что компонент не содержит ничего, кроме абстрактных классов.

Расстояние до главной последовательности: D = Abs(A + I - 1). D = 0 означает, что компонент находится на главной последовательности, что очень хорошо. D = 1 означает, что компонент находится на максимальном удалении от главной последовательности, что требует его пересмотра и реструктуризации.

Метрики управления зависимостями помогают получить количественную оценку соответствия структуры зависимостей и абстракции дизайна тому, что называется "хорошим" дизайном.

Политики стоит группировать в компоненты по способам изменения

Чем дальше политика от ввода и вывода, тем выше её уровень и тем реже она изменяется и по более важным причинам.

Передача данных между компонентами

Обычно через границы стоит передавать данные в виде простых структур. Данные можно передавать в вызовы функций через аргументы. Или упаковывать их в ассоциативные массивы или простые объекты.

Это важно делать, поскольку снижает степень зависимости между компонентами.

Однако, не всегда требуется делать архитектурную границу полностью. Какие-то можно сделать частично, а какие-то вообще игнорировать.

Роль API-тестирования - скрыть структуру приложения от тестов

Структурная зависимость - одна из самых сильных и наиболее коварных форм зависимости тестов. Набор тестов, в котором имеются тестовые классы для всех прикладных классов и тестовые методы для всех прикладных методов - плохая идея, поскольку затрудняет внесение изменений в приложение.

Уровень должен зависеть только от соседнего, нижележащего уровня

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

Закон Деметры

Метод f класса C должен ограничиваться вызовом методов следующих объектов:

  • C;

  • объекты, созданные f;

  • объекты, переданные f в качестве аргумента;

  • объекты, хранящиеся в переменной экземпляра C.

ctxt.GetAbsolutePathOfScratchDirectoryOption(); или ctxt.getScratchDirectoryOption().GetAbsolutePath(); - оба варианта плохие.

Первый приведёт к разрастанию набора методов объекта ctxt.

Второй вариант предполагает, что getScratchDirectoryOption() возвращает структуру, а не объект.

Если ctxt является объектом, то мы должны приказать ему выполнить некую операцию, а не запрашивать у него информацию о его внутреннем устройстве. Зачем нам понадобился абсолютный путь к временному каталогу? Что мы собираемся с ним делать? Почему бы не приказать объекту ctxt выполнить нужную операцию:

BufferedOutputStream bos = ctxt.createScrtachFileStream(classFileName);

Вместо сырых UObject лучше использовать TObjectPtr

Например, вместо:

USpringArmComponent* SpringArmComponent;

Лучше написать так:

TObjectPtr<USpringArmComponent> SpringArmComponent;

Оператор / для FString

Этот оператор позволяет быстро добавлять каталоги и файлы к переменной с директорией.

const TSharedPtr<IPlugin> Plugin = IPluginManager::Get().FindPlugin(TEXT("PluginName"));
if (Plugin.IsValid())
{
    const FString ResourcesDirectory = Plugin->GetBaseDir() / "Resources" / "Icon.png";
}

SStarshipGallery.cpp

Этот файл содержит большое количество примеров использования Slate-виджетов.

Композиция не всегда означает "содержит"

В области реализации (в отличие от предметной области) композиция означает "реализовано посредством".

Любая "пометка или флаг", что произошло изменение, должны ставиться только после изменения

Это и логичнее, и безопаснее.

Стоит избегать возврата дескрипторов внутренних данных объекта (ссылки, указатели, итераторы)

Это повышает степень инкапсуляции, помогает константным функциям-членам быть константными и минимизирует вероятность появления "висячих дескрипторов".

Стоит избегать приведения типов

Особенно dynamic_cast. Когда приведение всё же необходимо, то стоит сделать такую функцию, где это приведение будет в ней инкапсулировано.

Объявляйте переменные, как можно позже

Это делает программы яснее и повышает их эффективность.

Все данные-члены стоит делать private

Protected-члены не более инкапсулированы, чем открытые. Private-члены дают клиентам синтаксически однородный доступ к данным, обеспечивают возможность тонкого управления доступом, позволяют гарантировать инвариантность и предоставляют авторам реализации классов гибкость.

Хорошие интерфейсы легко использовать правильно и трудно - неправильно

Нужно стремиться обеспечивать эти характеристики своим интерфейсам.

Никогда не стоит вызывать виртуальные функции в конструкторе или деструкторе

Во-первых, это некорректно, во-вторых, это работает не так, как обычно предполагает программист.

Правила инициализации

  1. Всегда необходимо вручную инициализировать переменные встроенных типов, поскольку C++ делает это далеко не всегда.

  2. В конструкторе стоит отдавать предпочтение применению списка инициализации членов перед прямым присваиванием значений в теле конструктора. Перечислять данные-члены стоит в том порядке, в каком они объявлены в классе.

Использование mutable

Модификатор mutable освобождает нестатические данные-члены от ограничений побитовой константности. Отличный вариант для параметров, которые "лениво" инициализируются через const-функции.

Использование EditCondition в UPROPERTY

UPROPERTY(EditAnywhere)
bool bCanFly;

// EditConditionHides скрывает переменную MaxFlightSpeed, если bCanFly = false
UPROPERTY(EditAnywhere, meta=(EditCondition="bCanFly", EditConditionHides))
float MaxFlightSpeed;

UENUM()
enum class EAnimalType
{
    Bird,
    Fish
};
UPROPERTY(EditAnywhere)
EAnimalType AnimalType;

UPROPERTY(EditAnywhere, meta=(EditCondition="AnimalType==EAnimalType::Bird"))
float MaxFlightSpeed;

UPROPERTY(EditAnywhere, meta=(EditCondition="AnimalType==EAnimalType::Fish"))
float MaxSwimSpeed;

Использование static_cast, const_cast и dynamic_cast

Оператор static_cast встречается чаще всего:

int x = 5;
double y = static_cast<double>(x); // А не double y = (double) x;

Оператор dynamic_cast служит для безопасного приведения типа между уровнями иерархии наследования:

Widget* pw;
SpecialWidget* psw = dynamic_cast<SpecialWidget*>(pw);

Оператор const_cast чаще всего используется для изменения атрибута const:

void update(SpecialWidget* psw);

SpecialWidget sw;
const SpeicalWidget& csw = sw;
update(&csw); // Ошибка компиляции
update(const_cast<SpecialWidget*>(&csw));

Желательно воздерживаться от определяемых пользователем функций преобразования типа

Добавление таких функций может привести к их нежелательному (и даже неожиданному) вызову, из-за чего будет труднее понять работу программы, а также увеличится сложность её отладки.

Если компилятор или IDE сообщает, что переменная не используется, то можно удалить её имя

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

virtual int calculate(int x, double, float)
{
  return x * 2;
}

Префиксные формы инкремента и декремента - предпочтительны

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

for (int i = 0; i < 10; ++i);

Никогда нельзя перегружать операторы &&, || и ,

Это сильно усложняет чтение логических выражений, например:

if (expression1 && expression2) {}

Здесь, если expression1 - false, то expression2 даже не вычисляется. Однако, если для этих двух выражений будет переопределён оператор &&, то легко может вычисляться сначала expression2, потом обязательно expression1, даже если expressoin2 - false, и потом ещё вызывается сама функция.

Оператор запятой же просто внесёт огромную путаницу в код хотя бы по той причине, что его стандартное поведение невозможно повторить при его переопределении (поскольку нет гарантии, что выражение до запятой будет вычислено до выражения после запятой).

Операторы присваивания (например, +=) обычно более эффективны, чем отдельные операторы (например, +)

Например:

Widget w1;
Widget w2;
w1 += w2; // Обычно работает быстрее, чем w1 = w1 + w2;

Не стоит объявлять inline для виртуальных функций

Это очевидно, поскольку inline означает "в процессе компиляции подставить вместо вызова функции её тело", но "виртуальная" предполагает "определить вызываемую функцию во время выполнения программы". Таким образом, компиляторы обычно игнорируют директиву inline для виртуальных функций, однако, для корректности кода всё равно не стоит её указывать.

Всегда стоит делать нетерминальные классы абстрактными

Другими словами, не стоит наследоваться от конкретных классов. Если такая потребность возникла, значит, надо создать ещё один абстрактный класс, перенеся туда всю общую логику, и уже от него унаследовать оба конкретных класса (старый и новый).

Желательно использовать модули в UE

Это даёт следующие преимущества:

  1. На уровне компилятора "продвигает" разделение проекта на уровни абстракции

  2. Более удобная организация работы в команде

  3. Более быстрая компиляция

Например, могут быть такие модули (снизу-вверх): PRJUtility, PRJWeapons, PRJEnemies (на том же уровне, что и PRJWeapons), PRJCore (содержащий, например, GameMode, PlayerController, Pawn), PRJUI, плюс, primary-модуль, который будет совпадать с названием проекта.

При этом зависимости возможны только в направлении сверху - вниз. Blueprint можно считать отдельным модулем, который находится на самом верху, а потому может зависеть от любых модулей на C++.

Кстати, обычно в primary-модуле не нужны папки Public/Private, поскольку от primary-модуля ни один другой зависеть не будет.

PublicDependencyModuleNames или PrivateDependencyModuleNames?

Если зависимость используется только внутри cpp-файлов модуля, то добавлять её надо в PrivateDependencyModuleNames, а если используется в h-файле, то в PublicDependencyModuleNames. Это будет важно для тех модулей, которые будут зависеть от текущего.

Обычно можно сильно сократить количество добавлений в PublicDependencyModuleNames, если в h-файле использовать Forward Declaration.

Для primary-модуля можно всё добавлять в PublicDependencyModuleNames.

Желательно использовать префиксы в названии классов

Например, класс AWeapon лучше назвать APRJWeapon (PRJ - префикс, специфичный для проекта), что позволит устранить конфликты имён, а также сразу будет видно, где пользовательский класс, а где нет.

Данное правило так же актуально для перечислений, структур и модулей.

Добавлять надо только те компоненты в AActor, которые используются

Например, если StaticMeshComponent нужен только для внешнего вида и у нет никакой логики внутри C++, то лучше его добавить в дочернем Blueprint-классе.

Композиция или наследование?

Если имеет место отношение «Is a», то применяется наследование. Например, собака – это животное. А если имеется отношение «Has a», то применяется композиция. Например, собака имеет характеристику здоровья (Health Stat).

Локализация

FText MyText = NSLOCTEXT("Game UI", "Health Warning Message", "Low Health");

Или:

#define LOCTEXT_NAMESPACE "Game UI"
FText MyText = LOCTEXT("Health Warning Message", "Low Health");
#undef LOCTEXT_NAMESPACE

USTRUCT не собираются сборщиком мусора

Если используется динамическая структура, то лучше использовать интеллектуальные указатели.

Поиск ENUM по имени

const UEnum* EnumPtr = FindObject<UEnum>(ANY_PACKAGE, TEXT("EComboSections"), true);

Использование UPROPERTY(Instanced)

Это значение позволяет динамически создать объект класса через указание его в свойстве (например, какой-нибудь тип атаки или хранилище параметров оружия).

Получение случайной точки

FVector center = GetActorLocation();
FVector respawnPos(0, 0, center.Z);
float radius = FMath::RandRange(300.0, 1350.0);
float angle = FMath::RandRange(0.0, 360.0);
respawnPos.X = center.X + radius * FMath::Cos(angle);
respawnPos.Y = center.Y + radius * FMath::Sin(angle);

Использование auto (или decltype(auto)) в качестве типа возвращаемого значения

template<typename Container, typename Index>
decltype(auto) authAndAccess(Container&& c, Index i);

Использование auto – предпочтительно

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

Использование static_cast и auto

double calcEpsilon();
auto ep = static_cast<float>(calcEpsilon()); // Лучше, чем float ep = calcEpsilon()

Использование фигурной инициализации – предпочтительно

Фигурная инициализация предотвращает сужающие преобразования и нечувствительна к особенностям синтаксического анализа C++. Однако, с фигурной инициализацией надо быть осторожным, если есть перегрузка конструктора с параметром std::initializer_list.

Всегда лучше использовать nullptr вместо 0 или NULL

Также следует избегать перегрузок с указателями и целочисленными параметрами.

Использование перечисления с областью видимости – предпочтительно

То есть лучше писать так:

enum class Color {black, white, red};

чем так:

enum Color {black, white, red};

Удалённые функции стоит объявлять, как public

Например:

public:
  basic_ios(const basic_ios&) = delete;
  basic_ios& operator=(const basic_ios&) = delete;

Использование std::unique_ptr – предпочтительно

Он меньше весит, быстрее работает, а также легко преобразуется в std::shared_ptr. Это позволяет использовать его в качестве возвращаемого значения фабричных функций.

template<typename… Ts>
std::unique_ptr<Investment, void (*) (Investment*)> makeInvestment(Ts&&… params);
std::shared_ptr<Investment> sp = makeInvestment(arguments);

Избегать создания указателей std::shared_ptr из переменных, тип которых – обычный встроенный указатель

Например, не надо делать так:

auto pw = new Widget;
std::shared_ptr<Widget> spw(pw);

Лучше писать так:

std::shared_ptr<Widget> spw(new Widget);

А лучше всего так:

auto spw = std::make_shared<Widget>();

Применение std::weak_ptr

Их следует применять для кеширования и списков наблюдателей.

Использование make-функций - предпочтительно

По сравнению с непосредственным использованием new, make-функции устраняют дублирование кода, повышают безопасность кода по отношению к исключениям и в случае функций std::make_shared и std::allocate_shared генерируют меньший по размеру и более быстрый код.

Когда параметр является универсальной ссылкой

Если параметр шаблона функции имеет тип T&& для выводимого типа T или если объект объявлен с использованием auto&&, то параметр или объект является универсальной ссылкой.

Если вид объявления типа не является в точности type&& или если вывод типа не имеет места, то type&& означает rvalue-ссылку.

Универсальные ссылки соответствуют rvalue-ссылкам, если они инициализируются значениями rvalue. Они соответствуют lvalue-ссылкам, если они инициализируются значениями lvalue.

Не всегда возвращаемое значение без ссылок и указателей является неэффективным

Например:

Widget makeWidget()
{
  Widget w;
  …
  return w;
}

Здесь компилятор использует RVO (Return Value Optimization) для того, чтобы избежать копирования w. Это означает, что «копирующая» версия makeWidget на самом деле копирования не выполняет. Это работает всегда, если выполняются оба условия:

  1. Тип локального объекта совпадает с возвращаемым функцией

  2. Локальный объект представляет собой возвращаемое значение

Применение std::move и std::forward

  1. Применять std::move к rvalue-ссылкам, а std::forward – к универсальным ссылкам, когда вы используете их в последний раз.

  2. Делать то же для rvalue и универсальных ссылок, возвращаемых из функций по значению.

  3. Никогда не применять std::move и std::forward к локальным объектам, которые могут быть объектом RVO.

Избегать режима захвата по умолчанию в lambda-выражениях

То есть, всегда нужно захватывать интересующие переменные, например, так:

int x = 5;
auto lambda = [&x] {x = 10;};

Применение auto в lambda-выражениях приветствуется

Например:

auto lambda = [] (const auto& value) {};

Захваты в lambda-выражениях применяются только к нестатическим локальным переменным

Это ещё один повод избегать режима захвата по умолчанию.

Lambda-выражения могут принимать любое количество параметров

Например, так:

auto f = [] (auto&&… xs) {return normalize(std::forward<decltype(xs)>(xs)…); };

- эта функция в AGameModeBase, по сути, вызывает BeginPlay у объектов на уровне.

- установка точки, на которую должен смотреть контроллер.

- получение Actor, на которого был установлен фокус с помощью .

- установка фокуса (Use Controller Desired Rotation должен быть в true). Удобно использовать вместо связки и .

- получение прошедшего времени с момента, указанного в параметре.

- возвращает местоположение "глаз" у Pawn.

- направление взгляда Pawn (обычно совпадает с ).

- возвращает пустую строку типа FText.

- помимо приведения типа, данная функция добавляет fatal error в лог, если оно было неудачным. После CastChecked() дополнительный check для указателя уже не нужен, тогда как после обычного Cast необходим либо check, либо if.

– возвращает true, если две точки находятся рядом.

Удобный плагин для создания виджетов:

Документация:

🦉
TVector::GetSafeNormal()
UPhysicsHandleComponent
AActor::K2_TeleportTo()
UUserWidget::OnAnimationFinished_Implementation()
AActor::GetInstigatorController()
AGameModeBase::FindPlayerStart()
URotationMovementComponent
AActor::GetLocalRole()
AInfo
USpringArmComponent::IsCollisionFixApplied()
CheatManager
USkinnedMeshComponent::HideBoneByName()
DrawDebugString()
GetNameSafe()
UGameplayStatics::SuggestProjectileVelocity_CustomArc
ACharacter::LaunchCharacter
AGameMode::StartPlay()
AAIController::SetFocalPoint()
AAIController::GetFocusActor()
AAIController::SetFocus()
AAIController::SetFocus()
UKismetMathLibrary::FindLookAtRotation()
AActor::SetActorRotaion()
UWorld::TimeSince()
APawn::GetPawnViewLocation()
APawn::GetViewRotation()
Controller->GetControlRotation()
FText::GetEmpty()
CastChecked()
TVector::PointsAreNear()
https://dev.epicgames.com/documentation/en-us/unreal-engine/umg-viewmodel
https://docs.unrealengine.com/en-US/unreal-engine-modules/