Entry Level Guide to UE4 C++ RUS

From Epic Wiki


Обзор

 Автор  ()
 Переводчик  ()
 Редактура (),  ()
 

Уважаемое сообщество!

Эта страница Вики посвящена снижению порога вхождения для новых программистов на C++ .

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

Я и, надеюсь, другие смогут ответить на эти вопросы.

С чего лучше начать изучение UE4 C++  ?

Лично я рекомендую начать с расширения класса PlayerController, а затем сделать Blueprint из него.

Потом Вы сможете указать свой пользовательский PlayerController в вашем Game Mode Class.

У меня есть урок на эту тему:


ClientMessage

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


Вы можете использовать логирование для этого, но также можно использовать ClientMessage!

Мне нравится ClientMessage, потому что 'всё, что Вы должны сделать, это нажать ~ или другую клавишу, чтобы открыть консоль' и Ваша обратная связь появится тут.

Когда только Вы начинаете, важно иметь обратную связь, чтобы знать, что сделали всё правильно и где части кода работают неправильно, или просто не работают вовсе!

ClientMessage("Да эта функция работает!");
ClientMessage("Значения переменой: ");
ClientMessage(FString::SanitizeFloat(TheFloat));
ClientMessage("Текущее значения переменной: ");
ClientMessage(FString::SanitizeFloat(TheFloat));

PlayerController встречается повсюду

Фактически в любой игре будет PlayerController, так что это хорошая точка входа в мир UE4 C++ :)

Что такое (->), ( ), (::) ?

Я видел, что в C++ мы используем дефис со стрелкой (->), а не точку, как в других языках (.). Но я также видел использование двойных двоеточий (::) которые я не понимаю. Вы можете, пожалуйста, объяснить- что делают с помощью этих символов в C++? Какая разница между (->) и (::) ?

Например, UnrealEdEngine.cpp (\UnrealEngine\Engine\Source\Editor\UnrealEd\Private), Вы можете увидеть функции UUnrealEdEngine ::Init в строке 26. Почему эта функция использует двойное двоеточие? И внутри этой функции я вижу Super::Init(InEngineLoop).

Я ожидал увидеть Super->Init(InEngineLoop), но вместо него двойное двоеточие. И внутри этой функции большинство вызовов с помощью двойного двоеточия. Поэтому пожалуйста: объясните разницу между (::) и (->) и (.) .

(->) и (.)

Простым языком:

(->) означает, что это переменная, указывающая на данные. То есть переменная является ссылкой на контейнер, хранящий данные.

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

То есть (->), может указывать на хранилище (контейнер) данных, которого на самом деле не существует.

Таким образом, Вы всегда должны проверять такие переменные!

Пример

FVector* LocationPtr;
FVector Location;

Location.X = 5;
LocationPtr = &Location;
ClientMessage(FString::SanitizeFloat(LocationPtr->X));

После создания LocationPtr не указывает на хранилище данных, её необходимо связать с фактическими данными.

LocationPtr = &Location;

Переменная Location, напротив, сама является данными и может быть доступна сразу, используя точку (.)

Location.X = 5;

Теперь, когда мы связали LocationPtr, мы можем получить доступ к переменной X с помощью косвенной ссылки (->)

if(!LocationPtr) return;
ClientMessage(FString::SanitizeFloat(LocationPtr->X));
 Значение выведенное на экран "5"

Итог

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

Когда Вы используете указатель, который является косвенной ссылкой, Вы не можете быть уверены, что указываете на существующие данные, поэтому Вы должны это проверить:

if(!LocationPtr) return; //Переменная не указывала на что-либо


Если указатель ссылается на фактическую переменную, Вы можете получить к ней доступ, воспользовавшись стрелкой (->).

LocationPtr->X;


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

(::) говорит о пространстве имён (namespace) / или области видимости функции или переменой.

UUnrealEdEngine::Init

Это означает, что функция объявлена в классе UUnrealEdEngine.

Super::Init(InEngineLoop).

Так как функция инициализации является виртуальной, Вы можете вызвать версию функции из родительского класса (и Вы должны это сделать!)

Super означает родительский класс UUnrealEdEngine.

Для чего это?

У Вас есть огромная власть и свобода в C++!

Это, возможно, то, что делает его немного более сложным, для изучения :)

Вы можете объявить функции и переменные в любом месте в C++, поэтому используются (::) чтобы разграничить области: различные классы могут иметь переменные и функции с тем же именем, но при этом компилятор их будет различать.

Пример

Например, у Вас есть классы AMyTree и AMyFlower.

AMyTree хочется иметь функцию GetLocation (), так же как и в AMyFlower.

FVector GetLocation() const;

Однако в C ++, Вы работаете на очень низком уровне, что означает, что Вы можете объявить функции и переменные в глобальном пространстве имён.

Если Вы просто объявите переменную или функцию вне класса(class) или структуры(struct) или пространства имен(namespace),

static const FVector MyGlobalVector = FVector(2,4,16);
FORCEINLINE void MyVeryGlobalFunction()
{
  //что то делает
}

то тогда эти функции и переменные будут объявлены в глобальном пространстве имён и будут вызываться везде, где бы они не использовались.

Это на самом деле очень полезно для ! Однако, это вызывает проблемы для AMyTree и AMyFlower, где в обоих классах требуется функция GetLocation ()! Если Вы не будете использовать (::), то компилятор не сможет различить GetLocation () для AMyTree и для AMyFlower при компиляции! Используя (::) Вы говорите компилятору: какая версия функции для какого класса.

FVector AMyFlower::GetLocation() const
{
	//один код
}
FVector AMyTree::GetLocation() const
{
	//другой код
}

Если бы не (::)

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

Итог

 Воспринимайте (::) как просто ярлык, говорящий Вам- какая функция или переменная принадлежит какому классу.

T

Что значит, Т? В таких случаях как TSharedRef, TWeakPtr, TArray и т.д. "

Не допускайте, чтобы T запутала Вас! Воспринимать код с ней немного сложнее, так что постарайтесь игнорировать её для себя в таких ситуациях как этот:

TArray

..и просто читать следующим образом:

Array

Буква Т нужна, но это скорее заявление о типе класса- Array. Представьте себе: имена классов могут быть разных цветов, Т просто один цвет, или вкус класса в UE4. Но, если Вы пытаетесь прочитать код и если Вы не игнорировали ее, то буква Т может помешать пониманию- что делает сам класс.

Общая Ссылка

Еще один пример:

TSharedRef

Читая без T, мы видим "Общая Ссылка" (SharedRef). Игнорируя T, Вы можете читать основной код значительно легче, чтобы получить четкое представление о значениях каждого класса. Просто убедитесь, что использовали T, когда Вы попытаетесь работать с классом :)

Префиксы

Зачем нужны дополнительные префиксы. Например

  • зачем F в FVector и FSourceControlStatePtr
  • зачем T в TSharedRef, TWeakPtr, TArray
  • зачем U в (я думаю это Unreal) UObject, UUnrealEdEngine
  • зачем G в GIsEditor, GSlowTaskOccurred
  • и так далее...

За исключением G, это как цвета или вкусы классов в UE4. Каждая буква указывает на то, от какого класса унаследована та или иная сущность.

EPIC, Официальная документация по классам

Epic подробно рассказывает что означают данные префиксы:

Epic's Class Documentation

G

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

Хорошо, так что в действительности означает T ?

В исходных кодах UE4, все типы данных имеют префикс указывающий на его категорию, например: Т используется для обозначения класса шаблона: TSharedRef, TWeakPtr, TArray и т.д.,- это всё шаблонные классы. Префиксы позволяют легко отличать имена классов от имён переменных, поскольку имена переменных обычно не объявляются с префиксами в исходных кодах UE4 (одно исключение к этому правилу- имена логических переменных (bool), они начинаются с 'B').

В общем T даёт Вам понять две вещи:

  1. Вы смотрите на название типа (не имя переменной).
    1. Этот тип данных является шаблоном класса.

Так же есть и другие префиксы, которые можно найти в исходниках, например, U, A, S, I и F. Вы можете прочитать об их значениях здесь:

Epic's Naming Conventions

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

В качестве объяснения, шаблонные классы это такие классы, которым можно определить "подтип" к "основному типу". Для TArray, подтип может быть типом данных, которые буду хранится в нём, например:

TArray<FString> MyBadVariableName;

Это получился контейнер для ряда значений типа FString. Без шаблонных классов, Вам бы пришлось,"просто знать", что на самом деле массив "MyBadVariableName" содержит переменные типа "FStrings", но благодаря шаблонным классам это знает и компилятор, и это хорошо, так что Вам не придется держать столько вещей в свое голове во время чтения магически-запутанного вуду кода вашего друга.

Вы можете больше прочитать о шаблонных классах здесь:

Template (Wikipedia Article)

Указатели

Указатели (*) и ссылки (&). Я с трудом понимаю как их использовать. Что они такое? Вы можете показать несколько примеров того, как использовать и где использовать и какие преимущества их использования ?

Это, пожалуй, самое главное что надо понять новичкам, чтобы действительно достичь прогресса в C++. Если у Вас есть четкое представление об указателях, то Вы должны быть счастливы от программирования в UE4 C++! Указатели чрезвычайно-мощные и немного опасные, потому что дают Вам силу, которая у них есть.

 Указатели требуют, чтобы Вы аккуратно кодили. В свою очередь они дают Вам скорость и силу.

Указатель ссылается на адрес памяти, где хранятся фактические данные. Чтобы получить адрес памяти для указателя, нужно использовать (&).

FVector Location = FVector(1, 2, 9000);
FVector* LocationPtr; //LocationPtr в настоящее время указывает на ПУСТОЕ МЕСТО

LocationPtr = &Location; //LocationPtr теперь указывает на адрес памяти по которому хранится переменная "Location"

Всегда проверяйте ваши указатели

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

check(LocationPtr);

или:

if(!LocationPtr) return;

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

Разыменования указателей (Получение доступа)

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

Разыменования с помощью (*)

FVector NewVector = FVector::ZeroVector;
if(LocationPtr)
{
  NewVector = *LocationPtr; //Разыменования указателей
}

Разыменования с помощью (->)

if(!LocationPtr) return;
const float XValue = LocationPtr->X;

Зачем использовать указатели?

  • Указатели дают Вам прямой доступ к области памяти, которая может быть далеко-далеко от локального контекста в котором работает ваш код.
  • Указатели также дают Вам возможность работать с огромными объёмами данных без создания копии этих данных.
  • Указатели дают Вам живую связь, с динамическим обновлением данных, которые всегда будут актуальными, потому что указатель это косвенная ссылка, и её не надо менять, чтобы обновились данные.

Доступ к данным издалека

Скажем, у Вас есть персонаж, который является частью подуровня, который является частью мира, который является частью далекой Галактики. Чтобы фактически получить доступ к переменной "Броня" этого персонажа, Вам надо пройти через целый ряд гетеров (Gets), как то так:

GetGalaxy()->GetSolarSystem()->GetPlanet()->GetMainCharacter()->GetCurrentArmor();

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

FArmorStruct* TheArmor = & GetGalaxy()->GetSolarSystem()->GetPlanet()->GetMainCharacter()->GetCurrentArmor();

Теперь, чтобы получить данные о броне, Вам не придётся писать

GetGalaxy()->GetSolarSystem()->GetPlanet()->GetMainCharacter()->GetCurrentArmor().Durability;
GetGalaxy()->GetSolarSystem()->GetPlanet()->GetMainCharacter()->GetCurrentArmor().Color;
GetGalaxy()->GetSolarSystem()->GetPlanet()->GetMainCharacter()->GetCurrentArmor().Size;

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

Вместо этого Вы можете написать:

//Всегда проверяйте ваши указатели 
if(!TheArmor) return;

TheArmor->Durability;
TheArmor->Color;
TheArmor->Size;

Видите насколько это легче прочитать? Но подождите, это еще не все!

Доступ к огромным объемам данных в любой момент

Указатели могут быть связаны со сравнительно-небольшим объемом данных, или с большим или даже с гигантским объемом данных!

 Указатель, связывается с местом в памяти,
 А фактический объем этой памяти может быть огромным!

Продолжая приведенный выше пример, можно спросить, "Почему я не могу просто создать копию переменной "Броня" ?"

FArmorStruct ArmorVar = GetGalaxy()->GetSolarSystem()->GetPlanet()->GetMainCharacter()->GetCurrentArmor();

Но что, если FArmorStruct содержит гигантское количество данных, и Вы хотите сделать это для 300 персонажей по всей галактике? Вы бы стали копировать данные много раз. Но данные уже есть в одном месте памяти. Зачем Вам копировать их несколько раз, тем самым дублируя данные? Указатели позволяют избежать этого! Вы можете просто указать на одну ячейку в памяти и получать доступ к ней в любое время.

Будьте в курсе изменений в реальном времени

Также как и в приведенном выше случае, когда Вы копировали данные брони, это уже не броня персонажа из другого мира. Вы потеряли живую связь с бронёй. Так что, если персонаж из другого мира решит изменить цвет брони, Вы не будете знать этого! Указатели дают знать Вам актуальные данные, потенциально большие объемы данных, и потребность в получении доступа только один раз.

Итак, еще раз, правильный путь:

FArmorStruct* TheArmor = & GetGalaxy()->GetSolarSystem()->GetPlanet()->GetMainCharacter()->GetCurrentArmor();

Теперь у Вас есть легкий доступ и обновляемая связь с данными, что находятся в далекой-далекой галактике, без необходимости дублировать данные.

Передача данных по ссылке

Еще одно применение ссылок для передачи данных в функции. Это особенно важно для очень больших объемов данных, которые Вы бы не хотели, копировать в контексте функции!

int32 AMyClass::GetArraySize(TArray<uint8>& MyHugeBinaryArray) const
{
	return MyHugeBinaryArray.Num(); //ты используешь (.) вместо (->), потому что проходишь по ссылке
}

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

 Проходите по ссылке, где это возможно, вместо передачи указателя. Это гораздо безопаснее :)

Связанный урок от Epic

Programming Quick Start

Итог

Не стесняйтесь отправлять новые вопросы на вкладку Обсуждение! Я надеюсь, что это поможет Вам преодолеть входной барьер C++ и наслаждаться силой и красотой UE4 C++!

Перевод: ( ) Коррекция перевода и грамматики: ( )