Хабрахабр

[Перевод] Основа геймплея игры на C++ для Unreal Engine

image

Основа геймплея для Unreal Engine 4 предоставляет разработчику мощный набор классов для создания игры. Ваш проект может быть шутером, симулятором фермы, глубокой RPG — это неважно, основа очень универсальна, делает за вас часть тяжёлой работы и задаёт некоторые стандарты. Она довольно сильно интегрирована в движок, поэтому рекомендую вам придерживаться этих классов, а не пытаться изобретать собственную основу игры, как это часто бывает в движках наподобие Unity3D. Понимание этой основы очень важно для успешной и эффективной работы над проектами.
Для всех, кого интересует создание игр в UE4, а конкретно на C++, и кто хочет больше узнать о основе геймплея Unreal. В этом посте рассматриваются базовые классы, которые вы будете использовать в основе геймплея, и объясняется их применение, процесс создания их экземпляров движком и способ получения доступа к этим классам из других частей кода игры. Бо́льшая часть информации справедлива также и для блюпринтов.
Если вы хотите познакомиться с основами Unreal Engine 4, то изучите моё предыдущее руководство. Также у меня есть отдельное руководство, посвящённое виртуальной реальности для начинающих. Оно пригодится тем, кто изучает специфику VR в Unreal Engine 4.

Существует несколько классов, которые вы часто будете использовать при создании игр на C++ или в блюпринтах. При создании игр в Unreal Engine 4 вы встретите много уже готовых boilerplate-заготовок. Бо́льшая часть информации этого руководства применима и к блюпринтам, однако я использую фрагменты кода на C++ и поэтому некоторые функции будут недоступны в блюпринтах и полезны только для пользователей C++. Мы рассмотрим каждый из этих классов, их приятные особенности и узнаем, как ссылаться на них из других частей кода.

Actor

Наверно, самый часто используемый класс в играх. Actor — это основа для любого объекта на уровне, в том числе для игроков, управляемых ИИ врагов, дверей, стен и геймплейных объектов. Акторы создаются с помощью таких ActorComponents (см. следующий раздел), как StaticMeshComponent, CharacterMovementComponent, ParticleComponent и многих других. Даже такие классы, как GameMode (см. ниже) являются акторами (хотя у GameMode нет «реального» положения в мире). Давайте обсудим пару аспектов, которые вам нужно знать об акторах.

Это легко делается с помощью вызова в конструкторе SetReplicates(true). Actor — это класс, который можно реплицировать по сети (для многопользовательского режима). Для создания эффективного сетевого программирования акторов необходимо учитывать множество аспектов, которые я не смогу рассмотреть в этой статье.

Урон может наноситься непосредственно актору с помощью MyActor->TakeDamage(...) или через UGameplayStatics::ApplyDamage(...). Акторы поддерживают концепцию получения урона. На официальном сайте Unreal Engine есть замечательная вводная статья Damage in UE4. Стоит учесть, что существуют вариации: PointDamage (например, для оружия, попадание из которого вычисляется трассировкой луча (hitscan)) и RadialDamage (например, для взрывов).

Создать новый экземпляр актора в коде можно просто с помощью GetWorld()->SpawnActor<T>(…); где T — это возвращаемый класс, например, AActor для одного из ваших собственных классов — AGadgetActor, AGameplayProp и т.д.

Вот пример кода, в котором актор создаётся в процессе выполнения приложения:

FTransform SpawnTM;
FActorSpawnParameters SpawnParams;
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
SpawnParams.Owner = GetOwner();
/* Attempt to assign an instigator (used for damage application) */
SpawnParams.Instigator = Cast<APawn>(GetOwner());
ASEquippableActor* NewItem = GetWorld()->SpawnActor<ASEquippableActor>(NewItemClass, SpawnTM, SpawnParams);

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

Она позволяет нам получит массив из всех акторов передаваемого класса (в том числе и порождённых классов; если передать в качестве класса Actor, то мы получим ВСЕ объекты уровня). Очень полезной функцией, которую можно использовать при прототипировании или освоении движка, является UGameplayStatics::GetAllActorsOfClass(…). Этой функции часто боятся и избегают как не очень эффективного способа взаимодействия с окружением, но иногда это единственный доступный инструмент.

Всё это задаётся и получается с помощью RootComponent, т.е. Акторы не имеют собственных переноса, поворота или масштаба. Наиболее часто используемые функции наподобие MyActor->GetActorLocation() на самом деле переходят к RootComponent и возвращают его расположение в мире. компонента верхнего уровня в иерархии SceneComponents (подробнее о SceneComponents рассказывается ниже).

Вот ещё несколько полезных функций, которые используются в контексте актора:

  • BeginPlay // «Первая» функция, вызываемая после создания и полной инициализации актора. Это удобное место для задания базовой логики, таймера и внесения изменений в свойства, потому что актор уже полностью инициализирован и может выполнять запросы к своему окружению.
  • Tick // Вызывается в каждом кадре. Для большинства акторов можно отключить её из соображений производительности, но по умолчанию она включена. Замечательно подходит для быстрой настройки динамической логики и проверки условий в каждом кадре. Постепенно вы начнёте перемещать всё больше кода связанной с событиями логики из таймеров в логику, работающую на меньших частотах.
  • EndPlay // Вызывается, когда актор удаляется из мира. Содержит «EEndPlayReason», в котором указывается причина вызова.
  • GetComponentByClass // Находит один экземпляр компонента определённого класса. Очень полезно, когда вы не знаете точного типа актора, но знаете, что он должен содержать определённый тип компонента. Также существует GetComponentsByClass, возвращающая все экземпляры класса, а не только первый найденный.
  • GetActorLocation // И все его вариации — *Rotation, *Scale, в том числе и SetActorLocation и т.д.
  • NotifyActorBeginOverlap // Удобна для проверки наложений, вызванных любым из его компонентов. Таким способом можно быстро настраивать геймплейные триггеры.
  • GetOverlappingActors // Находит, какие другие акторы пересекаются с выбранным. Существует также вариант для компонентов: GetOverlappingComponents

Actor содержит огромный функционал и множество переменных — он является фундаментом основы геймплея в Unreal Engine, поэтому это не удивительно. Для дальнейшего исследования этого класса неплохо будет открыть файл заголовка Actor.h в Visual Studio и посмотреть, какой функционал в нём есть. В статье же нам предстоит ещё многое рассмотреть, поэтому давайте перейдём к следующему классу в списке.

ActorComponent

Компоненты располагаются внутри акторов, стандартными компонентами считаются StaticMeshComponent, CharacterMovementComponent, CameraComponent и SphereComponent. Каждый из этих компонентов обрабатывает свою частную задачу, например, движение, физическое взаимодействие (например, объём коллизии для чёткой проверки взаимодействующих акторов) или визуально отображает что-то в мире, например, меш игрока.

Например, мы можем прикрепить CameraComponent к SpringArmComponent для настройки камеры от третьего лица. Подклассом этого компонента является SceneComponent — это базовый класс для всего, связанного с Transform (Position, Rotation, Scale), поддерживающий прикрепление. Для правильной настройки относительного расположения требуются и transform, и прикрепление.

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

ASEquippableActor::ASEquippableActor()
{
PrimaryActorTick.bCanEverTick = true;
MeshComp = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("MeshComp"));
MeshComp->SetCollisionObjectType(ECC_WorldDynamic); // Just setting a property on our component
RootComponent = MeshComp; // Set our root component for other SceneComponents to attach to
ItemComp = CreateDefaultSubobject<USItemComponent>(TEXT("ItemComp")); // not attached to anything (not a SceneComponent with a transform)
}

USkeletalMeshComponent создаётся с помощью CreateDefaultSubobject<T> (функции актора) и требует указания имени (это имя можно увидеть в списке компонентов блюпринта). Если вы пишете код игры на C++, то часто будете использовать эту функцию, но ТОЛЬКО внутри контекста конструктора.

Теперь все Scene Component должны прикрепляться к этому мешу, что можно легко сделать с помощью следующей строки: Можно также заметить, что мы задаём MeshComp в качестве нового RootComponent.

WidgetComp = CreateDefaultSubobject<UWidgetComponent>(TEXT("InteractWidgetComp"));
WidgetComp->SetupAttachment(MeshComp);

SetupAttachment займётся обработкой исходного прикрепления; ожидается, что он будет вызываться в конструкторе для ВСЕХ компонентов сцены, кроме самого RootComponent. Можно задаться вопросом, почему мой ItemComponent не вызывает эту функцию SetupAttachment. Так получилось просто потому, что этот компонент является ActorComponent, но НЕ SceneComponent и не имеет Transform (позиции, поворота, масштаба), а потому не должен добавляться в иерархию. Тем не менее, компонент всё равно будет регистироваться с Actor. То, что он отделён от иерархии, означает, что функции наподобие MyActor->GetComponentByClass будут возвращать все ActorComponents и SceneComponents.

Именно они являются строительными кирпичиками игры. Наряду с Actor, эти компоненты критически важны для создания игры как на C++, так и на блюпринтах. Вы запросто можете создать собственные компоненты, чтобы они обрабатывали какие-то специфические аспекты игры, например HealthComponent, хранящий очки здоровья и реагирующий на урон, получаемый его родительским актором.

Это отличается от поведения CreateDefaultSubobject, используемого только для конструкторов. С помощью представленного ниже кода можно создавать в процессе выполнения собственные компоненты.

UActorComponent* SpawnedComponent = NewObject<UActorComponent>(this, UStaticMeshComponent::StaticClass(), TEXT("DynamicSpawnedMeshCompoent"));
if (SpawnedComponent)
{
SpawnedComponent->RegisterComponent();
}

Вот часть полезного функционала ActorComponents:

  • TickComponent() // Как и Tick() актора, выполняется каждый кадр для обработки высокочастотной логики.
  • bool bIsActive и связанные с ней функции наподобие Activate, Deactivate,… Используются для полного включения/отключения компонента (в том числе и TickComponent) без уничтожения компонента и удаления его из актора.

Чтобы обеспечить репликацию ActorComponent, необходимо вызвать функцию SetIsReplicated(true), название которой слегка отличается от функции актора. Это необходимо только тогда, когда вам нужно реплицировать конкретную часть логики компонента, например переменную при вызовах функции, то есть реплицировать нужно не все компоненты реплицируемого актора.

PlayerController

Это базовый класс для игрока, получающего ввод от пользователя. Сам по себе PlayerController не отображается визуально в окружении, вместо этого он управляет экземпляром Pawn, определяющим визуальное и физическое представление этого игрока в мире. Во время игрового процесса игрок может обладать несколькими разными Pawn (например, транспортным средством или свежей копией Pawn при респауне), а экземпляр PlayerController остаётся одинаковым на протяжении всего уровня. Это важно, потому что в некоторые моменты PlayerController может не обладать вообще никакими Pawn. Это значит, что такие вещи, как открытие меню должны добавляться к PlayerController, а не к классу Pawn.

Это значит, что в игре на 4 игрока у сервера есть 4 контроллера игроков, а у каждого клиента — только по одному. В многопользовательских играх PlayerController существует только на владеющем им клиенте и на сервере. Это очень важно понимать, когда необходимо использовать переменные; если для всех игроков требуется репликация переменной игрока, то она должна существовать не в PlayerController, а в Pawn или даже в PlayerState (рассмотренном ниже).

Получение доступа к PlayerControllers

  • GetWorld()->GetPlayerControllerIterator() // GetWorld доступен в любом экземпляре Actor
  • PlayerState->GetOwner() // владелец playerstate имеет тип PlayerController, и вы должны передавать его PlayerController самостоятельно.
  • Pawn->GetController() // Задаётся только тогда, когда пауном уже владеет (т.е. управляет) PlayerController.

Этот класс содержит PlayerCameraManager, который обрабатывает view targets и transforms камеры, в том числе и тряску. Ещё одним важным классом, которым управляет PlayerController, является HUD (рассмотрен ниже). Он применяется для рендеринга на Canvas (теперь используется не так часто, потому что есть UMG) и его можно использовать для управления данными, которые необходимо передать в интерфейс UMG.

Когда к GameMode подключается новый игрок, для этого игрока в классе GameModeBase с помощью Login() создаётся PlayerController.

Pawn

Является физическим и визуальным представлением того, чем управляет игрок (или ИИ). Это может быть машина, воин, башня или что угодно, обозначающее персонажа игры. Стандартным подклассом Pawn является Character, в котором реализуется SkeletalMesh и, что более важно, CharacterMovementComponent со множеством опций для точной настройки движения игрока по окружению с помощью обычного шутерного движения.

Это значит, что в игре на 4 игрока и на сервере, и на каждом клиенте есть 4 экземпляра pawn. В многопользовательских играх каждый экземпляр Pawn реплицируется другим клиентам. Имейте это в виду при хранении данных, которые должны сохраняться после завершения жизни игрока (или полностью откажитесь от этого паттерна и постоянно оставляйте экземпляр pawn живым)
Довольно часто экземпляр Pawn «убивают» при смерти игрока, а при респауне создаётся новый экземпляр.

Получение доступа к Pawn

  • PlayerController->GetPawn() // Только когда PlayerController владеет Pawn
  • GetWorld()->GetPawnIterator() // GetWorld доступен для любого экземпляра Actor и возвращает ВСЕ Pawn, в том числе и для ИИ.

Создание

GameModeBase создаёт Pawn с помощью SpawnDefaultPawnAtTransform. Класс GameModeBase также определяет, какой класс Pawn нужно создавать.

GameModeBase

Базовый класс, определяющий используемые классы (PlayerController, Pawn, HUD, GameState, PlayerState). Часто используется для задания правил игры в таких режимах, как «Capture the Flag»; он может обрабатывать флаги или волны врагов. Обрабатывает и другие важные функции, такие как создание игрока.

Он содержит ещё несколько функций, которые изначально использовались в Unreal Tournament, такие как MatchState и другие шутерные функции. GameMode — это подкласс GameModeBase.

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

Получение доступа к GameMode

  • GetWorld()->GetAuthGameMode() // GetWorld доступен для любого экземпляра Actor.
  • GetGameState() // возвращает gamestate для репликации функций и/или переменных
  • InitGame(…) // инициализирует некоторые из правил игры, в том числе, указанные в URL (например, «MyMap?MaxPlayersPerTeam=2»), которые можно передавать при загрузке уровней в игре.

HUD

Это класс интерфейса пользователя. В нём содержится много кода Canvas, который является кодом отрисовки интерфейса пользователя, написанного до появления UMG. Сегодня основной работой по отрисовке интерфейса пользователя занимается UMG.

Репликация невозможна. Класс существует только в клиенте. Им владеет PlayerController.

Получение доступа к HUD

PlayerController->GetHUD() // Доступен в локальном PlayerController.

Создание

Создаётся с помощью SpawnDefaultHUD (создаёт обычный AHUD) внутри PlayerController, который владеет HUD, а затем переопределяется GameModeBase с помощью InitializeHUDForPlayer классом HUD, указанным в GameModeBase.

Мои личные примечания

Я всё реже и реже стал пользоваться этим классом, и использую UMG, которым можно управлять через PlayerController. Не забывайте — перед созданием виджетов в многопользовательских играх нужно убедиться, что контроллер игрока является IsLocalController().

World

UWorld — это объект верхнего уровня, представляющий карту, на которой будут существовать и рендериться акторы и компоненты. Содержит постоянный уровень и многие другие объекты, такие как gamestate, gamemode, а также списки находящихся на карте Pawns и Controllers.

Трассировка линий и все её вариации выполняются через World с помощью таких функций, как World->LineTraceSingleByChannel и многих других подобных вариаций.

Получение доступа к World

Для получения доступа достаточно вызвать GetWorld() внутри акторов.

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

static APlayerController* GetFirstLocalPlayerController(UObject* WorldContextObject);

GameInstance

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

Получение доступа к GameInstance

  • GetWorld()->GetGameInstance<T>(); // где T — тип класса, например GetGameInstance<UGameInstance>() или вас собственный порождённый тип.
  • Actor->GetGameInstance()

Мои личные примечания

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

PlayerState

Контейнер для переменных, реплицируемых между клиентом/сервером для отдельного игрока. В многопользовательских играх он не предназначен для выполнения логики и является просто контейнером данных, поскольку PlayerController недоступен для всех клиентов, а Pawn часто уничтожается при смерти игрoка, поэтому неприменим для данных, которые должны храниться после смерти.

Получение доступа к PlayerState

Pawn содержит его как переменную Pawn->PlayerState, также доступную в Controller->PlayerState. PlayerState в Pawn назначен только тогда, когда Pawn владеет Controller, в противном случае имеет значение nullptr.

Список всех имеющихся экземпляров PlayerState (например, всех игроков, находящихся в матче) можно получить через GameState->PlayerArray.

Создание

Создающий класс назначается в GameMode (PlayerStateClass) и создаётся в AController::InitPlayerState()

Мои личные примечания

Полезен только при работе над многопользовательскими играми.

GameStateBase

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

GameState обрабатывает дополнительные переменные, требуемые GameMode (в отличие от GameModeBase) Имеет две вариации — GameState и GameStateBase.

Получение доступа к GameStateBase

  • World->GetGameState<T>() // где T — вызываемый класс, например GetGameState<AGameState>()
  • MyGameMode->GetGameState() // хранится и доступен в экземпляре gamemode (необходим только на сервере, который владеет единственным экземпляром GameMode); клиенты должны использовать указанный выше вызов.

Мои личные примечания

Используйте GameStateBase вместо GameState, только если gamemode не наследуется от GameMode вместо GameModeBase.

UObject

Базовый объект практически для всего в движке. Из UObject наследуются акторы, а также другие базовые классы, такие как GameInstance. Он никогда не должен использоваться для рендеринга, но очень полезен для хранения данных и функции, когда под ваши требования не подходят struct.

Создание UObjects

UObjects не спаунятся подобно акторам, а создаются с помощью NewObject(). Например:

TSubclassOf<UObject> ClassToCreate;
UObject* NewDesc = NewObject<UObject>(this, ClassToCreate);

Личные примечания

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

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

GameplayStatics

Статические классы используются для обработки различного стандартного функционала игр, например, воспроизведения звуков и создания эффектов частиц, создания акторов, применения урона к акторам, получения Pawn игрока, PlayerController и т.д. Этот класс очень полезен для всевозможного доступа к геймплейным функциям. Все функции являются статическими, то есть вам не требуется указатель на экземпляр этого класса и вы можете вызывать функции напрямую из любого места, как это показано в примере ниже.

Получение доступа к GameplayStatics

Так как GameplayStatics является UBlueprintFunctionLibrary, вы можете получить к нему доступ из любого места кода (или блюпринта)

UGameplayStatics::WhateverFunction(); // static functions are easily accessed anywhere, just include #include "Kismet/GameplayStatics.h"

Мои личные примечания

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

Теги
Показать больше

Похожие статьи

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

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть