SpringArm won't update from code!

While converting my player class to C++, I was having trouble implementing a zoom feature which prior to conversion, worked perfectly fine in the Blueprint version. After a little too much trouble, I decided to strip all relevant code out of my player class, and copy/paste the code from the C++ Third Person template. For whatever reason, this did not change anything…

Now here’s the weird part:

If I use the mouse wheel to zoom in/out, then Alt+[Tab] into the editor with the game running, select my player class from the World Outliner, and take a peek at the SpringArm component, I can see the value of TargetArmLength being updated, but there is no visible change in the actual game. Even stranger than that is when I manually edit the value in the details panel, then it actually does visibly show the changes… WTF? Why!?

My project’s input settings look like this:

And here’s the stripped-down version of my player class:


Player.h:
#pragma once

#include "GameFramework/SpringArmComponent.h"
#include "Player.generated.h"

class ABasePlayer;

UCLASS(Config = Game)
class APlayer : public ABasePlayer // NOTE: ABasePlayer is currently an empty class deriving from ACharacter
{
	GENERATED_BODY()

	/* Private Components */
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
	class USpringArmComponent* CameraBoom;
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
	class UCameraComponent* FollowCamera;

public:
	/* Class Constructor */
	ATOPAvatarPlayer();

	/* Overrides */
	virtual void SetupPlayerInputComponent(UInputComponent *InInputComponent) override;
	virtual void BeginPlay() override;

	/* Input */
	void Turn(float Value);
	void LookUp(float Value);
	void Zoom(float Value);

	/* Public Class Functions */
	FORCEINLINE class USpringArmComponent* GetCameraBoom() const { return CameraBoom; }
	FORCEINLINE class UCameraComponent* GetFollowCamera() const { return FollowCamera; }

	/* Exposed Variables */
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	float MinimumTurnAngle;
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	float MaximumTurnAngle;
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	float MinimumLookUpAngle;
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	float MaximumLookUpAngle;
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	float MinimumZoom;
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	float MaximumZoom;
};

Player.cpp:
#include "MyGame.h"
#include "BasePlayer.h"
#include "Player.h"

/*
* Class Constructor
* =================
*/
APlayer::APlayer()
{
	this->PrimaryActorTick.Target = this;
	this->PrimaryActorTick.bCanEverTick = true;
	this->PrimaryActorTick.bStartWithTickEnabled = true;

	GetCapsuleComponent()->InitCapsuleSize(42.f, 96.0f);
	bUseControllerRotationPitch = true;
	bUseControllerRotationYaw = true;
	bUseControllerRotationRoll = false;

	GetCharacterMovement()->bOrientRotationToMovement = true; // Character moves in the direction of input...	
	GetCharacterMovement()->RotationRate = FRotator(0.0f, 540.0f, 0.0f); // ...at this rotation rate
	GetCharacterMovement()->JumpZVelocity = 600.f;
	GetCharacterMovement()->AirControl = 0.2f;

	CameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom"));
	CameraBoom->AttachTo(RootComponent);
	CameraBoom->TargetArmLength = 500.0f; // The camera follows at this distance behind the character	
	CameraBoom->bUsePawnControlRotation = true; // Rotate the arm based on the controller

	FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera"));
	FollowCamera->AttachTo(CameraBoom, USpringArmComponent::SocketName); // Attach the camera to the end of the boom and let the boom adjust to match the controller orientation
	FollowCamera->bUsePawnControlRotation = false; // Camera does not rotate relative to arm

	/* Default Class Variables */
	MinimumTurnAngle = -89.5;
	MaximumTurnAngle = 89.5;
	MinimumLookUpAngle = -90.0;
	MaximumLookUpAngle = -0.15;
	MinimumZoom = 500.0;
	MaximumZoom = 1500.0;
}

/*
* Input Functions
* ===============
*/
void APlayer::Turn(float Value)
{
	if (Controller && (Value != 0.0f)) {
		auto Rotation = Controller->GetControlRotation();
		auto DesiredAngle = Value + Rotation.Yaw;
		auto MinAngle = MinimumTurnAngle;
		auto MaxAngle = MaximumTurnAngle;
		auto NewYaw = FMath::ClampAngle(DesiredAngle, MinAngle, MaxAngle);
		auto NewRotation = FRotator::MakeFromEuler(FVector(0.f, Rotation.Pitch, NewYaw));
		Controller->SetControlRotation(NewRotation);
		/* Just as expected, this message always displays when I move the mouse horizontally */
		GEngine->AddOnScreenDebugMessage(-1, 0.1f, FColor::Red, Controller->GetControlRotation().ToCompactString());
	}
}

void APlayer::LookUp(float Value)
{
	if (Controller && (Value != 0.0f)) {
		auto Rotation = Controller->GetControlRotation();
		auto DesiredAngle = Value + Rotation.Pitch;
		auto NewPitch = FMath::ClampAngle(DesiredAngle, MinimumLookUpAngle, MaximumLookUpAngle);
		auto NewRotation = FRotator::MakeFromEuler(FVector(0.f, NewPitch, Rotation.Yaw));
		Controller->SetControlRotation(NewRotation);
		/* Just as expected, this message always displays when I move the mouse vertically */
		GEngine->AddOnScreenDebugMessage(-1, 0.1f, FColor::Red, Controller->GetControlRotation().ToCompactString());
	}
}

void APlayer::Zoom(float Value)
{
	auto NewZoom = FMath::Clamp(CameraBoom->TargetArmLength + Value, MinimumZoom, MaximumZoom);
	if (NewZoom != CameraBoom->TargetArmLength && GEngine) {
		CameraBoom->TargetArmLength = NewZoom;
		/* Just as expected, this message always displays when I use the mouse wheel */
		GEngine->AddOnScreenDebugMessage(-1, 0.1f, FColor::Red, FString::FromInt(CameraBoom->TargetArmLength));
	}
}

/*
* Overrided Functions
* ===================
*/
void APlayer::SetupPlayerInputComponent(UInputComponent *InInputComponent)
{
	this->InputComponent->BindAxis("Turn", this, &APlayer::Turn);
	this->InputComponent->BindAxis("LookUp", this, &APlayer::LookUp);
	this->InputComponent->BindAxis("Zoom", this, &APlayer::Zoom);
}

void APlayer::BeginPlay()
{
	/* Dirty hack to get the Tick function to work in this class */
	auto level = GetWorld()->GetLevel(0);
	this->PrimaryActorTick.RegisterTickFunction(level);
}