How we implemented a camera system for a mobile TPS game

Content

  • The problem to be solved

    • Normal camera view

    • Aiming

    • Camera behavior in combat

    • Camera on the run

    • Indoor camera

  • Goals and objectives of the system

  • The main elements of the system

    • First element. Camera mod and its parameters

    • Second element. Support systems

    • Third element. Transitions between modes

  • Implementation

    • Camera Modes

    • Interpolation when switching modes

  • Result

The problem to be solved

In console AAA games, we see a dynamic third-person camera that constantly moves and changes plans during the game. Sometimes this is necessary for artistic purposes, such as bringing the camera closer to create a claustrophobic feeling or create tension. Sometimes, to show the general plan, and focus on the scale of the enemy and the player. But more often the main task of the camera is to show the player what he should and wants to see at a given moment in time. Show the objects with which he is currently interacting, and do not force him to rotate the camera manually again.

Let’s look at a few examples from a design point of view, which will be discussed in the article.

Normal camera view

The player is a little to the side. We free the center area of ​​the screen so that the player can see where he is going.

The Last of us Part II
The Last of us Part II

Aiming

When shooting from a third person, you need to bring the camera closer, reduce the FoV (Field of View) to better see the target. And change the position of the camera to give the impression that we are aiming together with the game character.

Division 2
Division 2

Camera behavior in combat

In battle, we move the camera away so that the player can see more space around the game character. This is necessary in order to see opponents, whom to attack, from whom to dodge.

Assassin's creed odyssey
Assassin’s creed odyssey

Camera on the run

In sprint, it is beneficial to move the camera away and increase the FoV angle of view. This is necessary so that more objects enter the screen, and the player has time to control the movement of the character at high speed. FoV also provides an additional visual effect, which is commonly used in games to denote acceleration.

Batman arkham knight
Batman arkham knight

Indoor camera

Indoors, we are faced with a different organization of space. In games, the dimensions of rooms and buildings are usually larger and more spacious than real ones, but they still hamper the movement of the character and the camera. In order for the camera not to “stumble” over the surroundings once again, we keep the camera closer to the player indoors. It also creates a certain sense of the enclosure of the surrounding space, allows the player to feel cramped.

Red dead redemption 2
Red dead redemption 2

Of course, there may be more cases requiring a unique setting. For example, we have more than 40 of them in our project. The described system makes it easy to create ready-made sets of settings for such cases.

Goals and objectives of the system

  1. Implement a console file. Camera behavior is similar to AAA console games.

  2. Ease of customization… All customization and creation of new content should happen without writing any code.

  3. Scalability… The ability to expand and supplement the system.

The main elements of the system

The system has three main elements.

The first element is camera mode

The camera mode is implemented as a DataAsset, which contains all the necessary settings to expose the camera to the desired position.

Inside the camera mode, the simplest parameters are configured, such as:

Pitch… Up down.

Yaw… Left / Right.

Roll… Rotation.

Distance… Or Arm Length in UE4. Distance to the player.

Offset… Camera offset.

FoV. Line of sight.

Below is shown how changing individual parameters of the camera gradually brings it to the state that we need in a particular camera mode.

Second element – auxiliary systems

Subsystems are separate mechanics that influence camera behavior, depending on certain conditions and settings.

These systems perform different tasks. For example, Subsystem pitch position automatically set the camera to certain Pitch and Yaw parameters when the player moves.

Subsystem auto rotation rotates the camera in the direction of the player’s movement.

Subsystem focus target include focus on the target in battle.

Each such subsystem is a separate module. The modular approach is convenient for a variety of reasons:

  1. Not every camera mode needs all assistive systems.

  2. It is much easier to work with code and blueprints when the asset is not overloaded with a lot of mechanics heaped together.

  3. There is a possibility of parallel development of several subsystems at once.

  4. A clear and explicit order of execution of subsystems affecting the camera.

Subsystems – this is a separate large topic, we will not delve into it in this article. Let’s just denote that they are. The result, on some of the materials presented from the game, would not have been achieved without these systems.

The third element – transitions between modes

The system selects one mode or another based on the conditions in which the game character is.

Examples of some camera modes with transition conditions:

  1. Base mode. Normal camera view.

    • Regular weapon in hand.

    • The player is on the street.

    • There are no opponents nearby.

  2. Battle mode. Camera behavior in combat.

    • There are opponents nearby.

    • The player strikes at opponents.

  3. Aim mode. Aiming.

    • The player is holding a small arms.

    • The player has activated aiming.

  4. Sprint mode. Camera on the run.

    • The player started to move.

    • The player has activated acceleration.

  5. Indoor mode. Indoor camera.

    • The player is indoors.

From a design point of view, we always definitely know which camera mode the player should be in, depending on the conditions. But it may so happen that two modes want to be activated at the same time.

For example:

The player is in battle with opponents. Switched on Battle mode cameras. But at the same time, he took out a small arms and activated aiming. In this case, we want to activate Aim mode

To resolve such situations, the modes have prioritization. The order in which they are activated. In the picture below Aim mode higher priority than Battle mode

We needed a flexible, easy-to-configure and transparent system for changing the camera mode, so that it could be configured by game designers without the participation of programmers. As a result, it was decided to make a system of transition from one mod to another, based on gameplay tags

Implementation

Camera Modes

First, you need to create a C ++ project from the Third Person template.

We will store the set of character states in the form of TMap, where the key is FGameplayTagand the value is the number of tags. This is necessary when tags are overlaid and removed from different sources.

You also need functions for adding / removing tags, a getter, as well as a delegate that will be called when they change. Don’t forget to add the GameplayTags module to your build.cs file.

The code
//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;
The code
//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;
}

After that, we will add a component that will be responsible for the system of camera modes and their operation.

The code
//CameraModeComponent.h

UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class CAMERASYSTEMHABR_API UCameraModeComponent : public UActorComponent
{
	GENERATED_BODY()

};

You also need to create a class for camera mode. Each camera mode is represented as a DataAsset with a set of settings for the desired behavior.

The code
//CameraMode.h

UCLASS(Abstract, Blueprintable, editinlinenew)
class UCameraMode: public UDataAsset
{
  ...
};

In the CameraModeComponent, you need to declare a structure that should contain the Camera mode and FGameplayTagQuery, which determines the conditions for switching to this mode, based on the state of the character at the moment.

The code
//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;
};

Let’s add an array of configs for all possible camera modes to the component and the variable of the current camera mode, as well as the callback function, which will be called when the tags on the character are changed.

The code
//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;

  ...

};
The code
//CameraModeComponent.cpp

void UCameraModeComponent::BeginPlay()
{
	Super::BeginPlay();

	Character = CastChecked<ACameraSystemCharacter>(GetOwner());

	if (Character->IsLocallyControlled())
	{
		Character->OnTagContainerChanged.AddUObject(this, &UCameraModeComponent::OnAbilityTagChanged));
	}
}

Further, after changing the tag, it is necessary to determine whether one of the camera mode from the config is suitable for the current conditions and, if so, change the camera mode to a new one.

The code
//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;
}

For example, let’s make two camera modes and transitions between them. Basic and while running the character. By analogy, you can add any number of states to switch to different camera modes, for example, for aiming, combat, etc.

First of all, in ProjectSettings, you need to create gameplay tags for the states of the character that affect the camera. In this case, this is the state when the character is running.

CharacterState.Sprint

Next, we will introduce several more methods in the character to bind keys and enable / disable the running state:

The code
//CameraSystemCharacter.h

virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

void EnableSprint();
void DisableSprint();
The code
//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")));
}

After that, you need to create two UCameraMode assets in the editor and select them in the Camera mode component config. The basic Camera mode must be placed at the very end, since it has no conditions for transition and is activated only if none of the camera modes match the current conditions.

Next, we set up the transition conditions.

The transition to Sprint mode is carried out when the CharacterState.Sprint tag is present.

Interpolation when switching

It is necessary that one mode smoothly transitions into another by means of parameter interpolation. Shown below is switching Base mode to Sprint mode and back with and without interpolation.

In UCameraMode, add settings for the position of the camera and the speed of transition between camera modes.

The code
//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;
};

Next, add methods to the Camera component tick that will be responsible for interpolating values:

The code
//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;
The code
//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);
}

Thus, after changing the camera mode to a new one, there will be a smooth transition between them.

Result

The video shows how the transition between camera modes works in our project. Of course, the video is a little more complex than the one described in the article. Since it is impossible to describe all the elements of the camera system within the framework of one article.

We have prepared a small project on GitHub in which the camera modes described in the article are implemented. Download link

Worked on the article:

Dmitry Gorbachev, Technical Game Designer
Dmitry Vergasov, C ++ programmer
Kirill Mintsev, C ++ programmer

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *