UE4 Isometric Camera: 2D Sprites in a 3D world

I always preface these type of posts by saying this is not meant to be a tutorial, but just a place for me to jot things down, and maybe someone might find my crazy useful.

Introduction

Next up on the mimic-list, Octopath Traveller / Project Triangle Story Strategy. Their blend of 2D and 3D, really caught my eye when they first came out (I guess Project Triangle isn’t out yet), it simply pulled at my nostalgia heart strings, and I knew if I ever made my own game, it would be in a similar style. I would love to experiment more with the colors, as the washed out and muted look didn’t do it for me, but more on that someday in the future.

Octopath Traveller
Project Triangle Strategy
Befuddled’s Project Octopath Strategy Story

Oh also, I’m trying out Unreal Engine 4, as Godot’s C++ GDNative feature had a couple bugs that made me go, “Maybe when it’s in a more polished state” which I have no doubt they’ll achieve. But for now, Godot, our paths will diverge (in a yellow wood?).

First thing I have to setup, an Isometric Camera, and a system so that all sprites will rotate towards the camera, else they disappear into the fabric of space and time.

Requirements

Isometric Camera

Perspective

The camera must follow the isometric perspective. This entails rotating the Y and Z Axis as follows:

  • X: No rotation.
  • Y: Rotated to arcsin( 1 / sqrt(3) ) or roughly around -35.264381 °
  • Z: Limited to the values of 45°, 135°, 225° or 315°. Or similarly Z must take the form of 45° + 90m°

Zoom, Panning, Rotating

The player must be able to zoom in game, using either the keyboard( two keys one for zooming in and one for zooming out) or the mouse wheel, with tunable max and min zoom values.

The player must also be able to pan the camera, with the keyboard in the left, right, up, down direction. Take special note that the panning speed will have to change when the player zooms in and out. Slower pan speed when zoomed out, and faster pan speed when zoomed in.

Rotation will be limited, so that the player will only see the game in Z = 45°, 135°, 225° or 315°. They player should be able to rotate the camera using the keyboard. The player can only rotate in +/- 90°increments, to preserve the isometric perspective.
When the camera rotates, there should be a tunable Tween that controls its movements from the starting location to the target location.
The player should not be able to activate another rotate action while currently rotating.

Sprites

The sprite will only be in 2D. But, in the future, we want the sprite to be able to take lighting effects in the surrounding 3D spaces, so just keep that in mind.

Tech Specs

Isometric Perspective

After a couple experiments, I found the easiest way to create the isometric perspective is to create pawn with a Camera attached to a SpringArm. The pawn will be place at the {0, 0, 0} offset. And it’s World rotation can be change to match the rotation expected of an isometric view.
Note: We set the FOV to 1.0f, and this will require the distance of the camera from the origin to be fairly fair away.

	// Create a camera boom...
	m_cameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom"));
	m_cameraBoom->SetupAttachment(RootComponent);
	m_cameraBoom->SetUsingAbsoluteRotation(true); // Don't want arm to rotate when character does
	m_cameraBoom->TargetArmLength = m_distance;
	m_cameraBoom->SetWorldRotation( FRotator( -s_pitch, s_yaw, 0 ) );
	m_cameraBoom->bDoCollisionTest = false; // Don't want to pull camera in when it collides with level

	//Create Camera Component
	m_camera = CreateDefaultSubobject<UCameraComponent>(TEXT("IsometricCamera"));
	m_camera->SetFieldOfView( m_FOV );
	//Attach to CameraBoom
	m_camera->SetupAttachment(m_cameraBoom, USpringArmComponent::SocketName);
	m_camera->bUsePawnControlRotation = false; // Camera does not rotate relative to arm

Camera Rotation

Rotation is fairly easy, as we only have to rotate the SpringArm Component. Tweening in Unreal Engine takes shape as the combine efforts of Timelines and LARPing Lerping. And by exposing the Timeline Curve in the editor, tuning the rotate animation will be easy for the designer.
Special consideration have to made because I want limit the rotation values to 0° to 360°, so if I’m at 45°, and I need to rotate -90°, the final value should be 315° and not -45°. This is because I want to be able to use Z = 45°, 135°, 225° or 315° to identify the current world orientation in other places in the code.

// Called when the game starts or when spawned
void AIsometricCameraPawn::BeginPlay()
{
	...
	FOnTimelineFloat tickFunction;
	tickFunction.BindUFunction( this, FName( "Rotate" ) );

	//Setting up the loop status and the function that is going to fire when the timeline ticks
	m_rotationTimeline.AddInterpFloat( m_rotationCurveFloat, tickFunction );
	m_rotationTimeline.SetLooping( false );
	...
}

void AIsometricCameraPawn::Rotate( float value )
{
	float tickRoration = FMath::Lerp( 0.0f, s_singleRotationDegrees, value );
	auto worldRotation = 0.0f;
	
	auto deltaRotatation = m_rotationDirection * tickRoration;
	if ( m_currentWorldRotation + deltaRotatation < 0 ) //0 to 360 boundary
		worldRotation = 360.0f - ( tickRoration - m_currentWorldRotation );
	else if ( m_currentWorldRotation + deltaRotatation > 360 )//360 to 0 boundary
		worldRotation = deltaRotatation - ( 360.0f - m_currentWorldRotation);
	else
		worldRotation = m_currentWorldRotation + deltaRotatation;

	m_cameraBoom->SetWorldRotation( FRotator( -s_pitch, worldRotation, 0 ) );

	if ( tickRoration == 90.0f )
		m_currentWorldRotation = worldRotation;
}

Camera Zoom

Currently, I’m finding that changing the FOV on the camera is enough to give the illusion of zooming (Low values is zoomed in, high values are zoomed out) It’s simple, so I’ll keep it 🙂

Camera Panning

To change the position of the camera, we can simply change the TargetOffset property of the camera boom whenever the panning buttons (WASD) are pressed. For W(Up) and S(Down), this is changing the Z offset.
For panning left and right, this involves changing the X and Y offsets by the same magnitude with varying signs depending on the current rotation of the camera. Something like:

		if( m_currentWorldRotation == 45.0f )
			m_cameraBoom->TargetOffset += FVector( -axisValue * m_cameraMoveSpeed, axisValue * m_cameraMoveSpeed, 0.0f );
		else if( m_currentWorldRotation == 135.0f )
			m_cameraBoom->TargetOffset += FVector( -axisValue * m_cameraMoveSpeed, -axisValue * m_cameraMoveSpeed, 0.0f );
		else if ( m_currentWorldRotation == 225.0f )
			m_cameraBoom->TargetOffset += FVector( axisValue * m_cameraMoveSpeed, -axisValue * m_cameraMoveSpeed, 0.0f );
		else if ( m_currentWorldRotation == 315.0f )
			m_cameraBoom->TargetOffset += FVector( axisValue * m_cameraMoveSpeed, axisValue * m_cameraMoveSpeed, 0.0f );

Character Rotation

For the purpose of this demo, I just wanted the Sprite to always face the camera (I guess that means parallels to the camera), in a game, I’d probably also have animations for the back of the character, so that if you rotate far enough it will change the animation.
In Unreal, we are able to query the rotation of the camera currently being used:

void AIsometricPaperCharacter::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	auto cameraRotator = GEngine->GetFirstLocalPlayerController( GetWorld() )->PlayerCameraManager->GetCameraRotation();
	SetActorRotation( FRotator( 0, 90.0f + cameraRotator.Yaw, 0 ) );
}

So using that value and adding 90°, keeps the sprite in the orientation we want.

[GitHub Repo to follow]

Leave a Comment