![](https://habrastorage.org/getpro/habr/upload_files/8b2/aed/7dd/8b2aed7dd0f53758557484fa33942e83.jpg)
Содержание
Обычный вид камеры
Прицеливание
Поведение камеры в бою
Камера на бегу
Камера внутри помещений
Первый элемент. Камера мод и его параметры
Второй элемент. Вспомогательные системы
Третий элемент. Переходы между режимами
Режимы камеры
Интерполяция при переключении режимов
Решаемая проблема
В консольных ААА играх мы видим динамическую камеру от третьего лица, которая в процессе игры постоянно двигается, меняет планы. Иногда это нужно для художественных задач, например, приблизить камеру, чтобы вызвать ощущение клаустрофобии или создать напряжение. Иногда, чтобы показать общий план, и акцентировать внимание на масштабе противника и игрока. Но чаще основная задача камеры — показать игроку то, что он должен и хочет видеть в данный момент времени. Показать те объекты, с которыми он сейчас взаимодействует, и не вынуждать его лишний раз вращать камеру вручную.
Рассмотрим несколько примеров с точки зрения дизайна, о которых пойдет речь в статье.
Обычный вид камеры
Игрок немного сбоку. Мы освобождаем центральную область экрана, чтобы игрок видел куда он идёт.
![The Last of us Part II The Last of us Part II](https://habrastorage.org/getpro/habr/upload_files/3a1/192/b4e/3a1192b4e3011ab217bbdd78ee3c97a4.jpg)
Прицеливание
При стрельбе от третьего лица нужно приблизить камеру, уменьшить FoV (Field of View), чтобы лучше видеть цель. И изменить позицию камеры, чтобы создавалось впечатление, что мы целимся вместе с игровым персонажем.
![Division 2 Division 2](https://habrastorage.org/getpro/habr/upload_files/ce2/5bc/119/ce25bc1198ac42c1ed99cb2b56ab283f.jpg)
Поведение камеры в бою
В бою мы отдаляем камеру, чтобы игрок видел больше пространства вокруг игрового персонажа. Это нужно, чтобы видеть противников, кого атаковать, от кого уворачиваться.
![Assassin's Creed Odyssey Assassin's Creed Odyssey](https://habrastorage.org/getpro/habr/upload_files/d7d/a63/9be/d7da639be4693760d43c08e18b85e9c5.jpg)
Камера на бегу
В спринте выгодно отдалить камеру и увеличить угол обзора FoV. Это нужно, чтобы в экран попадало больше объектов, и игрок успевал управлять движением персонажа на большой скорости. Также FoV даёт дополнительный визуальный эффект, которым в играх принято обозначать ускорение.
![Batman Arkham Knight Batman Arkham Knight](https://habrastorage.org/getpro/habr/upload_files/c4d/04d/924/c4d04d9241036cde48ba933ac30fb75e.jpg)
Камера внутри помещений
Внутри помещений мы сталкиваемся с другой организацией пространства. В играх размерности комнат и зданий обычно больше и просторней реальных, но они всё равно стесняют движения персонажа и камеры. Для того чтобы камера лишний раз не «спотыкалась» об окружение, в помещениях мы держим камеру ближе к игроку. Также это создаёт некоторое ощущение замкнутости окружающего пространства, позволяет игроку ощутить себя в тесноте.
![Red Dead Redemption 2 Red Dead Redemption 2](https://habrastorage.org/getpro/habr/upload_files/a65/bfc/346/a65bfc346eb01f056a49a26ae5909149.jpg)
Конечно случаев, требующих уникальной настройки, может быть больше. Например, у нас в проекте их более 40. Описываемая система позволяет легко создавать для таких случаев готовые наборы настроек.
Цели и задачи системы
Реализовать консольный фил. Поведение камеры похожее на поведение в ААА-консольных играх.
Удобство настройки. Все настройки и создание нового контента должны происходить без написания кода.
Масштабируемость. Возможность расширять и дополнять систему.
Основные элементы системы
В системе есть три основных элемента.
Первый элемент — режим камеры
Режим камеры (camera mode) реализован как DataAsset, который содержит все необходимые настройки, выставляющие камеру в нужное положение.
Внутри camera mode настраиваются простейшие параметры, такие как:
Pitch. Вверх/вниз.
![](https://habrastorage.org/getpro/habr/upload_files/142/826/472/142826472d0080c2527d049e84699d20.gif)
Yaw. Влево/вправо.
![](https://habrastorage.org/getpro/habr/upload_files/3c5/3df/021/3c53df021ea3c6bdf2d814bdf8eab503.gif)
Roll. Вращение.
![](https://habrastorage.org/getpro/habr/upload_files/52d/e88/d01/52de88d01dff83215ba0afc0f9a85fbf.gif)
Distance. Или Arm Length в UE4. Дистанция до игрока.
![](https://habrastorage.org/getpro/habr/upload_files/4f3/714/8bf/4f37148bf0dc266c01b235f992a17408.gif)
Offset. Смещение камеры.
![](https://habrastorage.org/getpro/habr/upload_files/655/8fe/edc/6558feedce94818cdceaf4f0435dc7d9.gif)
FoV. Поле зрения.
![](https://habrastorage.org/getpro/habr/upload_files/9ff/51e/85c/9ff51e85ce1f65ac0fb232dba5ebd32b.gif)
Ниже показано, как изменение отдельных параметров камеры постепенно приводит её в то состояние, которое нам требуется в конкретном camera mode.
![](https://habrastorage.org/getpro/habr/upload_files/276/cb6/66a/276cb666af843945d0af05bb410bc220.gif)
Второй элемент — вспомогательные системы
Вспомогательные системы (subsystems) являются отдельными механиками, влияющими на поведение камеры, в зависимости от определенных условий и настроек.
Эти системы выполняют различные задачи. Например, Subsystem pitch position автоматически выставляют камеру в определённые Pitch и Yaw параметры, при движении игрока.
![](https://habrastorage.org/getpro/habr/upload_files/dc5/255/fee/dc5255fee7de75ad03ab91273c7339b3.gif)
Subsystem auto rotation поворачивает камеру по направлению движения игрока.
![](https://habrastorage.org/getpro/habr/upload_files/014/5c0/749/0145c07491f54c0fc4f06225bee1f7b2.gif)
Subsystem focus target включают фокус на цель в бою.
![](https://habrastorage.org/getpro/habr/upload_files/e3b/a9b/99d/e3ba9b99d19f3ec344568cf9149e6a7e.gif)
Каждая такая subsystem — это отдельный модуль. Модульный подход удобен по множеству причин:
Не каждому режиму камеры нужны все вспомогательные системы.
Куда легче работать с кодом и блупринтами, когда ассет не переполнен множеством механик собранных в кучу.
Есть возможность параллельной разработки нескольких subsystems сразу.
Четкий и явный порядок выполнения subsystems, воздействующих на камеру.
Subsystems — это отдельная большая тема, мы не будем углубляться в неё в этой статье. Просто обозначим, что они есть. Результат, на некоторых представленных материалах из игры, не был бы достигнут без этих систем.
Третий элемент — переходы между режимами
Система выбирает тот или иной режим, исходя из условий, в которых находится игровой персонаж.
Примеры некоторых режимов камеры с условиями перехода:
Base mode. Обычный вид камеры.
Обычное оружие в руках.
Игрок стоит на улице.
Противников рядом нет.
Battle mode. Поведение камеры в бою.
Рядом находятся противники.
Игрок наносит удары по противникам.
Aim mode. Прицеливание.
У игрока в руках стрелковое оружие.
Игрок активировал прицеливание.
Sprint mode. Камера на бегу.
Игрок начал двигаться.
Игрок активировал ускорение.
Indoor mode. Камера внутри помещений.
Игрок находится внутри помещения.
С точки зрения дизайна, мы всегда однозначно знаем в каком режиме камеры должен находиться игрок в зависимости от условий. Но может так получиться, что два режима хотят активироваться одновременно.
Например:
Игрок находится в бою с противниками. Включен Battle mode камеры. Но при этом он достал стрелковое оружие и активировал прицеливание. В таком случае мы хотим активировать Aim mode.
Чтобы разрешать такие ситуации у режимов есть приоритизация. Порядок, согласно которому они активируются. На картинке ниже Aim mode более приоритетный, чем Battle mode.
![](https://habrastorage.org/getpro/habr/upload_files/1bd/92d/c85/1bd92dc8575388550cc9fc3c8e505652.jpg)
Нужна была гибкая, простая в плане настроек и прозрачная система для смены camera mode, чтобы она могла бы настраиваться гейм-дизайнерами без участия программистов. В итоге было решено сделать систему перехода от одного мода к другому, базирующуюся на gameplay tags.
Реализация
Режимы камеры
Для начала необходимо создать C++ проект из шаблона Third Person.
Набор состояний персонажа мы будем хранить в виде TMap, где ключ — FGameplayTag, а значение — количество тегов. Это необходимо, когда теги накладываются и убираются из разных источников.
Так же необходимы функции для добавления/удаления тегов, геттер, а также делегат, который будет вызываться при их изменении. Не забудьте добавить модуль GameplayTags в build.cs файле.
Код
//CameraSystemCharacter.h
public:
DECLARE_EVENT_TwoParams(ACameraSystemCharacter, FOnTagContainerChanged, const FGameplayTag& /*ChangedTag*/, bool /*bExist*/);
FOnTagContainerChanged OnTagContainerChanged;
void AddTag(const FGameplayTag& Tag);
void RemoveTag(const FGameplayTag& Tag);
FGameplayTagContainer GetGameplayTags() const;
protected:
TMap<FGameplayTag, int32> TagMap;
Код
//CameraSystemCharacter.срр
void ACameraSystemCharacter::AddTag(const FGameplayTag& Tag)
{
auto& val = ++TagMap.FindOrAdd(Tag);
OnTagContainerChanged.Broadcast(Tag, val > 0);
}
void ACameraSystemCharacter::RemoveTag(const FGameplayTag& Tag)
{
auto& val = --TagMap.FindOrAdd(Tag);
OnTagContainerChanged.Broadcast(Tag, val > 0);
}
FGameplayTagContainer ACameraSystemCharacter::GetGameplayTags() const
{
FGameplayTagContainer tags;
Algo::ForEach(TagMap, [&](const auto& it) { if (it.Value > 0) tags.AddTag(it.Key); });
return tags;
}
После этого добавим компонент, который будет отвечать за систему режимов камеры и их работу.
Код
//CameraModeComponent.h
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class CAMERASYSTEMHABR_API UCameraModeComponent : public UActorComponent
{
GENERATED_BODY()
};
Также необходимо создать класс для camera mode. Каждый camera mode представлен в виде DataAsset с набором настроек для требуемого поведения.
Код
//CameraMode.h
UCLASS(Abstract, Blueprintable, editinlinenew)
class UCameraMode: public UDataAsset
{
...
};
В CameraModeComponent необходимо объявить структуру, которая должна содержать Camera mode и FGameplayTagQuery, который определяет условия для перехода в этот режим, исходя из состояния персонажа на данный момент.
![](https://habrastorage.org/getpro/habr/upload_files/8be/031/9ae/8be0319ae3cb9275c8082ddaa3387679.png)
Код
//CameraMode.h
USTRUCT(BlueprintType)
struct FCameraModeData
{
GENERATED_BODY()
public:
bool CanActivateMode(const FGameplayTagContainer& TagsToCheck) const
{
return ModeActivationConditions.IsEmpty() || ModeActivationConditions.Matches(TagsToCheck);
}
UCameraMode* GetCameraMode() const
{
return CameraMode;
}
protected:
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Instanced)
UCameraMode* CameraMode;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
FGameplayTagQuery ModeActivationConditions;
};
Добавим массив конфигов для всех возможных режимов камеры в компонент и переменную текущего camera mode, а также функцию callback, которая будет вызываться при изменении тегов на персонаже.
Код
//CameraModeComponent.h
UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class CAMERASYSTEMHABR_API UCameraModeComponent : public UActorComponent
{
GENERATED_BODY()
...
protected:
virtual void BeginPlay() override();
void OnAbilityTagChanged(const FGameplayTag& Tag, bool TagExists);
protected:
UPROPERTY(EditDefaultsOnly)
TArray<FCameraModeData> CameraModes;
UPROPERTY()
UCameraMode* CurrentCameraMode;
TWeakObjectPtr<ACameraSystemCharacter> Character;
...
};
Код
//CameraModeComponent.cpp
void UCameraModeComponent::BeginPlay()
{
Super::BeginPlay();
Character = CastChecked<ACameraSystemCharacter>(GetOwner());
if (Character->IsLocallyControlled())
{
Character->OnTagContainerChanged.AddUObject(this, &UCameraModeComponent::OnAbilityTagChanged));
}
}
Дальше, после смены тега, необходимо определить, подходит ли один из camera mode из конфига под текущие условия и, если да, сменить camera mode на новый.
Код
//CameraModeComponent.cpp
void UCameraModeComponent::OnAbilityTagChanged(const FGameplayTag& Tag, bool TagExists)
{
SetCameraMode(DetermineCameraMode(Character->GetGameplayTags()));
}
UCameraMode* UCameraModeComponent::DetermineCameraMode(const FGameplayTagContainer& Tags) const
{
if (auto foundMode = Algo::FindByPredicate(CameraModes, [&](const auto modeData) {return modeData.CanActivateMode(Tags);}))
{
return foundMode->GetCameraMode();
}
return nullptr;
}
void UCameraModeComponent::SetCameraMode(UCameraMode* NewMode)
{
CurrentCameraMode = NewMode;
}
Для примера сделаем два режима камеры и переходы между ними. Базовый и во время бега персонажа. По аналогии можно добавлять любое количество состояний для перехода в различные режимы камеры, например для прицеливания, боя и тд.
Прежде всего в ProjectSettings надо завести gameplay tags для состояний персонажа, влияющих на камеру. В данном случае это состояние, когда персонаж бежит.
CharacterState.Sprint
![](https://habrastorage.org/getpro/habr/upload_files/c17/301/0a2/c173010a20fabde6200ce3b559a81fe8.png)
Далее в персонаже заведём ещё несколько методов, для привязки клавиш и включения/выключения состояния бега:
Код
//CameraSystemCharacter.h
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
void EnableSprint();
void DisableSprint();
Код
//CameraSystemCharacter.cpp
void ACameraSystemCharacter::SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent)
{
PlayerInputComponent->BindAction("Sprint", IE_Pressed, this, &ACameraSystemCharacter::EnableSprint);
PlayerInputComponent->BindAction("Sprint", IE_Released, this, &ACameraSystemCharacter::DisableSprint);
}
void ACameraSystemCharacter::EnableSprint()
{ AddTag(FGameplayTag::RequestGameplayTag(TEXT("CharacterState.Sprint")));
}
void ACameraSystemCharacter::DisableSprint()
{
RemoveTag(FGameplayTag::RequestGameplayTag(TEXT("CharacterState.Sprint")));
}
После этого необходимо в редакторе создать два ассета UCameraMode и выбрать их в конфиге Camera mode component. Базовый Camera mode необходимо разместить в самом конце, т.к. он не имеет условий для перехода и активируется только в случае, если ни один режим камеры не подошёл под текущие условия.
![](https://habrastorage.org/getpro/habr/upload_files/b81/569/905/b815699055a9e75eb896eccf0baee0ab.png)
Дальше настраиваем условия перехода.
Переход в Sprint mode осуществляется при наличии тега CharacterState.Sprint.
![](https://habrastorage.org/getpro/habr/upload_files/374/d09/833/374d09833cfc24bcaf22010f7854f3ff.png)
Интерполяция при переключении
Необходимо, чтобы один мод плавно переходил в другой при помощи интерполяции параметров. Ниже показано переключение Base mode в Sprint mode и обратно с интерполяцией и без.
![](https://habrastorage.org/getpro/habr/upload_files/52a/0ae/41f/52a0ae41fd733abde04d3971813022b8.gif)
В UCameraMode добавляем настройки положения камеры и скорости перехода между режимами камеры.
Код
//CameraMode.h
UCLASS(Abstract, Blueprintable, editinlinenew)
class UCameraMode : public UDataAsset
{
GENERATED_BODY()
public:
//Cкорость интерполяции для текущего мода
UPROPERTY(EditDefaultsOnly, meta = (ClampMin = 0.f, UIMin = 0.f))
float InterpolationSpeed = 5.f;
//Продолжительность смены скорости интерполяции при переходе из одного мода в другой, требуется, чтобы достичь максимальной плавности при переходе между модами.
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, meta = (ClampMin = 0.f, UIMin = 0.f))
float InterpolationLerpDuration = 0.5f;
//Длина SprintArm в камера моде
UPROPERTY(EditDefaultsOnly, meta = (ClampMin = 0.f, UIMin = 0.f))
float ArmLength = 250.f;
//Значение FOV для камеры
UPROPERTY(EditDefaultsOnly, meta = (ClampMin = 0.f, UIMin = 0.f))
float Fov= 60.f;
//оффсет камеры относительно SprigArm
UPROPERTY(EditDefaultsOnly)
FVector CameraOffset = FVector::ZeroVector;
//Параметр, который будет включать вращение персонажа по направлению контроллера, например, для режима прицеливания
UPROPERTY(EditDefaultsOnly)
bool bUseControllerDesiredRotation = false;
};
Далее в Camera component tick добавляем методы, которые будут отвечать за интерполяцию значений:
Код
//CameraModeComponent.h
protected:
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
virtual void BeginPlay() override;
void SetCameraMode(UCameraMode* NewMode);
float GetInterpSpeed() const;
void UpdateCameraMode (float DeltaTime);
void UpdateSpringArmLength(float DeltaTime);
void UpdateCameraLocation(float DeltaTime);
void UpdateFOV(float DeltaTime);
//Мировое время, во время смены одного камера мода на другой, потребуется, чтобы реализовать плавный переход скорости интерполяции между модами
float TimeSecondsAfterSetNewMode = 0.f;
//Скорость интерполяции с прошлого мода, требуется, чтобы плавно перейти в новую скорость интерполяции
float PreviousInterpSpeed = 0.f;
//Камера менеджер, пригодится в дальнейшем для смены значений FOV
TWeakObjectPtr<APlayerCameraManager> PlayerCameraManager;
Код
//CameraModeComponent.cpp
void UCameraModeComponent::SetCameraMode(UCameraMode* NewMode)
{
if (CurrentCameraMode != NewMode)
{
PreviousInterpSpeed = CurrentCameraMode == nullptr ? NewMode->InterpolationSpeed : CurrentCameraMode->InterpolationSpeed;
CurrentCameraMode = NewMode;
Character->GetCharacterMovement()->bUseControllerDesiredRotation = CurrentCameraMode->bUseControllerDesiredRotation;
Character->GetCharacterMovement()->bOrientRotationToMovement = !CurrentCameraMode->bUseControllerDesiredRotation;
TimeSecondsAfterSetNewMode = GetWorld()->GetTimeSeconds();
}
}
void UCameraModeComponent::BeginPlay()
{
...
PlayerCameraManager = CastChecked<APlayerController> (Character->GetController())->PlayerCameraManager;
...
}
float UCameraModeComponent::GetInterpSpeed() const
{
auto timeAfterModeWasChanged = GetWorld()->GetTimeSeconds() - TimeSecondsAfterSetNewMode;
auto lerpDuration = CurrentCameraMode->InterpolationLerpDuration;
auto lerpAlpha = FMath::IsNearlyZero(lerpDuration) ? 1.f : FMath::Min(1.f, timeAfterModeWasChanged / lerpDuration);
return FMath::Lerp(PreviousInterpSpeed, CurrentCameraMode->InterpolationSpeed, lerpAlpha);
}
void UCameraModeComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
UpdateCameraMode(DeltaTime);
}
void UCameraModeComponent::UpdateCameraMode(float DeltaTime)
{
if (CurrentCameraMode != nullptr)
{
UpdateSpringArmLength(DeltaTime);
UpdateSpringArmPivotLocation(DeltaTime);
UpdateCameraLocation(DeltaTime);
}
}
//Интерполяция параметра TargetArmLength
void UCameraModeComponent::UpdateSpringArmLength(float DeltaTime)
{
const auto currentLength = Character->GetCameraBoom()->TargetArmLength;
const auto targetLength = CurrentCameraMode->ArmLength;
const auto newArmLength = FMath::FInterpTo(currentLength, targetLength, DeltaTime, GetInterpSpeed());
Character->GetCameraBoom()->TargetArmLength = newArmLength;
}
void UCameraModeComponent::UpdateCameraLocation(float DeltaTime)
{
const auto currentLocation = Character->GetCameraBoom()->SocketOffset;
const auto targetLocation = CurrentCameraMode->CameraOffset;
FVector newLocation = FMath::VInterpTo(currentLocation, targetLocation, DeltaTime, GetInterpSpeed());
Character->GetCameraBoom()->SocketOffset = newLocation;
}
//Смена значений FOV
void UCameraModeComponent::UpdateFOV(float DeltaTime)
{
const auto currentFov = PlayerCameraManager->GetFOVAngle();
const auto targetFov = CurrentCameraMode->Fov;
auto newFov = FMath::FInterpTo(currentFov, targetFov, DeltaTime, GetInterpSpeed());
PlayerCameraManager->SetFOV(newFov);
}
Таким образом, после смены режима камеры на новый, будет происходить плавный переход между ними.
Результат
На видео показано, как работает переход между режимами камеры у нас в проекте. Конечно, на видео немного более сложная система, чем та, которая описана в статье. Так как в рамках одной статьи невозможно описать все элементы системы камер.
Мы подготовили небольшой проект на GitHub, в котором реализованы описанные в статье camera modes. Ссылка для скачивания.
![](https://habrastorage.org/getpro/habr/upload_files/2ac/e3e/a0d/2ace3ea0d7d8adc03a4430d8864da9af.gif)
Над статьёй работали:
Дмитрий Горбачев, Technical Game Designer
Дмитрий Вергасов, C++ programmer
Кирилл Минцев, C++ programmer