This weekend I’ve implemented a basic camera and input system for The Bug Squad. The camera uses a very simple top down setup for now, implemented inside a custom APlayerCameraManager class. For the input system I decided to try the new Enhanced Input plugin included in UE5.

For reference, here’s the model I’m going to use for the player’s mech. Some design considerations, as we’ll see later, depend directly on how the model is set up.

The model is available on the UE marketplace and even if it’s marked as only being compatible with UE 4.20 it works flawlessly in UE5. It looks great overall, although I was disappointed by the fact that source PSD textures aren’t included in the UE package, while they’re available in the Unity one. I got in touch with the author, but no response so far.

Basic camera

My current camera setup lives in a custom APlayerCameraManager. I decided to go this way instead of the classic USpringArmComponent plus UCameraComponent hierarchy of components on the character for a few reasons:

  • The camera doesn’t need to collide with the world to adjust its position dynamically.
  • I’m planning to have cutscenes in the game, so I may need to implement logic to stack multiple camera viewpoints and interpolate between them. While this is possible with UCameraComponent — in fact, the Lyra example project does things this way — it felt more correct to manage state in APlayerCameraManager.
  • Long term I’d like to calculate the location of the camera depending on the locations of all local players and, potentially, enemies, objectives, etc.

The implementation is minimal and full of TODOs:

void ABSPlayerCameraManager::UpdateViewTargetInternal(FTViewTarget& OutVT, const float DeltaTime)
{
    if (BlueprintUpdateCamera(OutVT.Target, OutVT.POV.Location, OutVT.POV.Rotation, OutVT.POV.FOV))
    {
        return;
    }

    UpdateCameraLocation(DeltaTime, OutVT.POV);
}

void ABSPlayerCameraManager::UpdateCameraLocation(const float DeltaTime, FMinimalViewInfo& OutCameraView)
{
    // TODO: track multiple players, predict movement direction, maybe track enemies, etc.
    if (PCOwner != nullptr)
    {
        if (const APawn* Pawn = PCOwner->GetPawn())
        {
            // CameraRotation and CameraDistance are two variables that can be edited to adjust the camera viewpoint
            OutCameraView.Location = Pawn->GetActorLocation() + CameraRotation.Vector() * CameraDistance;
            OutCameraView.Rotation = CameraRotation;
        }
        else
        {
            // Fall back to default player controller view
            // TODO: potentially incorrect, maybe focusing the player start the player will spawn at while we
            // wait for him to spawn would be more correct
            PCOwner->GetActorEyesViewPoint(OutCameraView.Location, OutCameraView.Rotation);
        }
    }
}

Input setup

In The Bug Squad — as in other twin stick shooters — players move with the gamepad left analog stick and aim with the right stick. From an animation point-of-view this usually means characters will move forward and backward as well as strafe to the sides in order to keep aiming in the direction the right stick points to. While this works well for humanoid characters, I don’t feel like the mech above should strafe and, in fact, the pack doesn’t include strafe animations at all.

What I want is for the legs of the mech to follow the movement direction, with the cockpit and arms free to rotate to match the controller aim. In Unreal I ended up implementing this by rotating the pawn to face the movement direction and using the controller rotation to drive the aim.

A screenshot of the Orient To Movement Direction option in UE5
CharacterMovementComponent has a flag to automatically orient the character to the movement direction.

Enhanced Input plugin

The Enhanced Input plugin is an experimental plugin that ships with UE5 and supports advanced input features like customizable dead-zones, easy runtime remapping, etc. When enabled it completely replaces UE5 long-standing default input system.

The official documentation provides an overview of how the system works and how to set it up, although I found it to be a bit lacking at times. This Youtube tutorial has been helpful to fill in some of the gaps.

A screenshot of input assets inside the UE5 content browser

Input Actions and Mapping Context

The plugin makes heavy use of assets to define the input setup. The first building blocks are Input Actions. These represent, well, actions the player can trigger — such as move, look, jump, etc. — and the type of input they can handle. For move and look this will be a 2D vector (for mouse movement or controller analog sticks), whereas for jump it will be a 1D scalar value representing whether the jump button is pressed.

Actions don’t do anything until they’re mapped to a specific input axis or button, which is achieved via a Input Mapping Context asset.

A screenshot of the input context asset, with actions mapped to specific input axis or button

Raw input can be processed using modifiers. For example, the S key uses a Negate modifier which inverts the input (so 1 becomes -1) in order for the character to move backwards. I’ve used the same modifier on the A key to move leftwards. There are quite a few built-in modifiers, such as Swizzle Axes, useful to switch X and Y axes on analog sticks for example, Dead Zone, also useful for analog sticks, etc.

Mapping contexts can be switched at runtime to change which input actions are available to the player. For example, let’s say that at a certain spot I want players to exit their mechs and proceed on foot. In this case I could switch between mapping contexts that are specific to each mode of travel.

Input binding

Input actions and their mapping context need to be bound to logic in C++ (or Blueprint) in order for input to drive gameplay logic. In my setup I followed what Epic did in the Lyra example project and created a custom data asset that stores a reference to the mapping context and maps individual actions to gameplay tags.

A screenshot of my custom input config asset

This is probably overkill for now, but I like that fact that all input settings live in their own separate asset and the flexibility gameplay tags offer when setting things up in C++:

namespace InputTags
{
    UE_DEFINE_GAMEPLAY_TAG_STATIC(Move, "Input.Move");
    UE_DEFINE_GAMEPLAY_TAG_STATIC(MouseLook, "Input.MouseLook");
    UE_DEFINE_GAMEPLAY_TAG_STATIC(GamepadLook, "Input.GamepadLook");
}

void ABSMech::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
    Super::SetupPlayerInputComponent(PlayerInputComponent);

    if (InputConfig == nullptr)
    {
        UE_LOG(LogBSCharacter, Warning, TEXT("No input config specified in %s"), *GetName());
        return;
    }

    UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent);
    if (!ensure(EnhancedInputComponent != nullptr))
    {
        return;
    }

    // Bind standard input
    if (const TObjectPtr<UInputAction>* MoveAction = InputConfig->StandardInputActions.Find(InputTags::Move))
    {
        EnhancedInputComponent->BindAction(*MoveAction, ETriggerEvent::Triggered,
            this, &ThisClass::OnMoveAction);
    }
    if (const TObjectPtr<UInputAction>* MouseLookAction = InputConfig->StandardInputActions.Find(InputTags::MouseLook))
    {
        EnhancedInputComponent->BindAction(*MouseLookAction, ETriggerEvent::Triggered,
            this, &ThisClass::OnMouseLookAction);
    }
    if (const TObjectPtr<UInputAction>* GamepadLookAction = InputConfig->StandardInputActions.Find(InputTags::GamepadLook))
    {
        EnhancedInputComponent->BindAction(*GamepadLookAction, ETriggerEvent::Triggered,
            this, &ThisClass::OnGamepadLookAction);
    }
}

void ABSMech::PawnClientRestart()
{
    Super::PawnClientRestart();

    if (const APlayerController* PlayerController = GetController<APlayerController>())
    {
        if (UEnhancedInputLocalPlayerSubsystem* Subsystem =
            ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
        {
            Subsystem->ClearAllMappings();

            if (InputConfig != nullptr)
            {
                Subsystem->AddMappingContext(InputConfig->MappingContext, 0);
            }
        }
    }
}

void ABSMech::OnMoveAction(const FInputActionValue& ActionValue)
{
    FVector InputVector = FVector::ForwardVector * ActionValue.Get<FInputActionValue::Axis2D>().X;
    InputVector += FVector::RightVector * ActionValue.Get<FInputActionValue::Axis2D>().Y;
    AddMovementInput(InputVector);
}

// OnMouseLookAction(), OnGamepadLookAction(), etc.

Putting it all together

Here’s how things look right now. I’ve change the pawn’s collision component to be visible in game for testing purposes and added an arrow to show the direction the character is aiming at.

Small beginnings!

I’ve got mixed feeling about whether Enhanced Input is worthwile for a small project such as The Bug Squad. On one hand it has a somewhat steep learning curve and requires additional setup over the default input system. On the other hand I really like the flexibility it provides and I can see a few features potentially helping going forward.

Next up: setting up mech visuals and animation blueprint.