This tutorial goes over the process for creating a 3rd person follow camera (think Mario 64) within Unreal. Very useful for any platformer-type game especially if you plan to target a game pad as the input system. Keyboard and mouse controls still function reasonably.

First Steps

First of all, you need to have custom player and controller classes hooked up through your custom GameInfo class. If you’re unsure of how to accomplish that then this tutorial is probably too advanced for you! :[

Alright, now that we’ve gotten rid of the stragglers. You’ll be creating a custom camera class (I recommend inheriting from GamePlayerCamera) and a custom input class (GamePlayerInput within Your_Custom_Controller_Class). I also highly recommend adding a “player” variable to your GameInfo class that maintains a reference to the player instance. If you are creating a single player game this will be highly valuable throughout development. We won’t need it for the camera, though.

Let’s go ahead and create those input and camera classes now. We’ll call the camera class MG_Camera and the input class MG_Input (MG for My Game). Now we’ll add the following lines to our custom player class and custom controller class respectively:

// Player Class:
simulated function name GetDefaultCameraMode( PlayerController RequestedBy )
{
	// You can optionally query the PlayerController or gamestate to
	// decide between different camera modes here.
	return 'FollowCam';
}
// Controller Class:
DefaultProperties
{
	InputClass=class'MG_Input'
	CameraClass=class'MG_Camera'
}

Starting the Camera Class

Now we’re ready to start work on the camera. First we need to establish where the camera should be at any given moment. I decided on a distance of 416 unreal units but YMMV. Create a variable to store that number called FollowCamDist.

We’ll need a few more variables to keep track of the camera state. We’ll keep the current position/rotation/distance of the camera and a height offset to place the camera above the player. Next, we’ll override the UpdateViewTarget function in order to control how the camera behaves. We’ll steal some code from GamePlayerCamera and mix in our own. All together, the camera class now looks like this:

class MG_Camera extends GamePlayerCamera;

var Vector  CurrentCamLocation;
var Rotator CurrentCamRotation;

var(FollowCam) float  FollowCamDist;
var(FollowCam) Vector FollowZOffset;

function UpdateViewTarget(out TViewTarget OutVT, float dt)
{
	local CameraActor	CamActor;

	// Default FOV on viewtarget
	OutVT.POV.FOV = DefaultFOV;
	
	// Viewing through a camera actor
	CamActor = CameraActor(OutVT.Target);
	if( CamActor != None )
	{
		// In LD cinematic!
		CamActor.GetCameraView(dt, OutVT.POV);

		// Grab aspect ratio from the CameraActor
		bConstrainAspectRatio	= bConstrainAspectRatio || CamActor.bConstrainAspectRatio;
		OutVT.AspectRatio		= CamActor.AspectRatio;

		// CameraActor wants to override the PostProcess settings
		bCamOverridePostProcess = CamActor.bCamOverridePostProcess;
		CamPostProcessSettings = CamActor.CamOverridePostProcess;

		// Set my variables to account for current targeting
		CurrentCamLocation = OutVT.POV.Location;
		CurrentCamRotation = OutVT.POV.Rotation;
	}
	// Give Pawn Viewtarget a chance to dictate the camera position
	else if( Pawn(OutVT.Target) != None && Pawn(OutVT.Target).CalcCamera(dt, OutVT.POV.Location, OutVT.POV.Rotation, OutVT.POV.FOV) )
	{
		// If this ever happens send me an email!
		`log("NOTICE ME: No idea when this happens!");
		CurrentCamRotation = OutVT.POV.Rotation;
		CurrentCamLocation = OutVT.POV.Location;
	}
	// Finally, we can make our own camera!
	else
	{
		switch( CameraStyle )
		{
			case 'FollowCam':
				FollowCamTarget(OutVT, dt);
				break;
		}

		OutVT.POV.Location = CurrentCamLocation;
		OutVT.POV.Rotation = CurrentCamRotation;
	}

	// Lets LDs do shake and whatnot
	ApplyCameraModifiers(dt, OutVT.POV);
}

DefaultProperties
{
	FollowCamDist=416.f
	FollowZOffset=(X=0.f,Y=0.f,Z=160.f)
}

It’s true, there’s a lot of handwavium up there. Here’s a brief explanation. If the ViewTarget is a CameraActor then the game is in cinematic mode and gets full control of the camera. A ViewTarget can also control the camera although I haven’t been able to figure out when this happens. Finally, if neither of those is the case, we’re in default mode and we get to control the camera. Remember when we returned ‘FollowCam’ in the player class above? That’s what we’re checking in the switch statement. It’s a good idea to also set up a default case.

Most importantly, on line 49 we call FollowCamTarget which we’ll write as our custom camera.

The FollowCam

For simplicity, we’ll calculate the camera in two dimensions. This is why we need the FollowZOffset above. The downside is that the player won’t be able to change the height of the camera while playing. If people want, I’ll go into how to make that work at a later date. In most cases, the player isn’t looking up and down anyway.

Here’s basically how this will work. We figure out the current direction from the camera to the player on the x-y plane. Multiply that direction by our FollowCamDist to get a vector of the desired length. Add that to the target’s location, add in the ZOffset and done!

function FollowCamTarget(TViewTarget OutVT, float dt)
{
	local Vector ToTarget;
	local Vector CamNoZ;
	local Vector TargetNoZ;

	CamNoZ      = CurrentCamLocation;
	CamNoZ.Z    = 0.f;
	TargetNoZ   = OutVT.Target.Location;
	TargetNoZ.Z = 0.f;

	toTarget = TargetNoZ - CamNoZ;
	toTarget = Normal(ToTarget);

	CurrentCamLocation = ToTarget * -FollowCamDist + OutVT.Target.Location + FollowZOffset;

	ToTarget = OutVT.Target.Location - CurrentCamLocation;
	CurrentCamRotation = Rotator(ToTarget);
}

Input

If you run this in the game you’ll notice a distinct problem: the controls remain relative to the character and not the camera. Unless you want the player to control like a tank we’ll have to fix this. That’s why we need a custom input class.

The goal is to translate the input so that the player deals with controls relative to the camera. To do that, we’ll translate the input from camera space through world space and into character space. That way when unreal applies motion it will have values pre-converted relative to the character. While we’re at it, it is a good idea to have the character rotate to face the movement direction. You can see the code for those two adjustments below:

class MG_Input extends GamePlayerInput within MG_Controller;

var(MG_Input) float turnRate;

event PlayerInput( float DeltaTime )
{
	local Vector  move, facing;
	local Rotator rot, camRot, playerRot;

	super.PlayerInput(DeltaTime);

	move.X = aStrafe;
	move.Y = aForward;
	move.Z = 0.f;

	facing.x = aForward;
	facing.Y = aStrafe;
	facing.Z = 0.f;

	playerRot.Yaw = Rotation.Yaw;
	camRot.Yaw = MG_Camera(PlayerCamera).CurrentCamRotation.Yaw;

	move = TransformVectorByRotation(camRot, move, true);
	move = TransformVectorByRotation(playerRot, move, false);
	aForward = move.Y;
	aStrafe = move.X;

	facing = TransformVectorByRotation(camRot,facing,false);
	if( move.X != 0.f || move.Y != 0.f )
	{
		rot = RInterpTo( Rotation, Rotator( Normal(facing) ), DeltaTime, turnRate );
		SetRotation(rot);
	}

	aTurn = 0.f;
}

DefaultProperties
{
	turnRate = 6.f

	// This keeps Unreal from messing with rotations
	bLockTurnUntilRelease = true
}

Awesome! Now movement is relative to the camera and the character will turn to face the movement direction. Depending on how you animate the character, this may look ugly. The character will move and rotate at the same time. It’s best from a responsiveness standpoint but worst from an artistic standpoint. Some games like to limit the player speed during this adjustment phase. Others simply lock the player in place until the rotation is finished. What works best for your game may be somewhere between these three options.

Input Hotspot

There is one piece we’d like to change, however. You’ll notice when you try out these controls that if you press left on the keyboard/joystick that the character makes a circle around the camera. Ideally, the character would keep moving in the same direction as long as the player doesn’t “change” input. But if we lock the translation until input releases, players will notice that the direction they’re pressing (changing to) isn’t the direction of movement. If we do nothing, camera changes leave the player confused as the character switches directions.

To deal with this we’ll create a “hotspot” within which the player can leave the joystick that prevents direction from being updated with the camera. First, store the current rotation and joystick position into class variables. Next, check if the change in joystick position puts it outside of the hotspot. If the joystick moved too much, update the rotation and joystick positions. If not, lock the rotation and joystick position through the next input cycle. Here’s how to do it with a simple circular hotspot:

class MG_Input extends GamePlayerInput within MG_Controller;

var(MG_Input) float turnRate;

var bool    bMoveLocked;
var float   HotSpotRadius;
var Rotator currCamRot;
var Vector  oldJoy;

event PlayerInput( float DeltaTime )
{
	local Vector  move, facing, joy;
	local float   moveDeltaAbs;
	local Rotator rot, camRot, playerRot;

	super.PlayerInput(DeltaTime);

	move.X = aStrafe;
	move.Y = aForward;
	move.Z = 0.f;

	facing.x = aForward;
	facing.Y = aStrafe;
	facing.Z = 0.f;

	// Hotspot needs to be independent of DeltaTime
	joy.X = RawJoyRight;
	joy.Y = RawJoyUp;

	moveDeltaAbs = Abs(joy.Y - oldJoy.Y) + Abs(joy.X - oldJoy.X);

	if(!bMoveLocked)
	{
		if(  ( move.X != 0.f || move.Y != 0.f ) && moveDeltaAbs < HotSpotRadius )
			bMoveLocked = true;

		currCamRot = MG_Camera(PlayerCamera).CurrentCamRotation;
		oldJoy = joy;
	}
	else if(aTurn != 0.f || moveDeltaAbs > HotSpotRadius)
	{
		bMoveLocked = false;
		oldJoy = joy;
	}

	playerRot.Yaw = Rotation.Yaw;
	camRot.Yaw = currCamRot.Yaw;

	move = TransformVectorByRotation(camRot, move, true);
	move = TransformVectorByRotation(playerRot, move, false);
	aForward = move.Y;
	aStrafe = move.X;

	facing = TransformVectorByRotation(camRot,facing,false);
	if( move.X != 0.f || move.Y != 0.f )
	{
		rot = RInterpTo( Rotation, Rotator( Normal(facing) ), DeltaTime, turnRate );
		SetRotation(rot);
	}
	else bMoveLocked = false;

	MG_Camera(PlayerCamera).desiredTurn = aTurn;
	aTurn = 0.f;
}


DefaultProperties
{
	bMoveLocked = false
	turnRate = 6.f
	HotSpotRadius = 0.25f

	// This keeps Unreal from messing with rotations
	bLockTurnUntilRelease = true
}

You’ll notice I also started using aTurn. That variable holds the desired turn amount from the game pad or mouse. It’s the x-axis of either the R-Stick or the mouse. If the player manually turns the camera, we want the input to update. This allows you to steer with the right stick while moving forward. Plus, it takes us right into turning the camera!

Rotating the Camera

Of course, the player should have control over which direction the camera is facing. To do this, we have to change the camera’s position based on player input. The camera needs to rotate around the player when the mouse or right stick moves. From the input class above, we’ve put the turn amount into the DesiredTurn variable within the camera class. Make sure to create that variable within the camera. We’ll create a new function RotationUpdate to handle the rotating. Again, we’ll stick to two dimensions for simplicity but ideally the player would also be able to look up and down.

function RotationUpdate(TViewTarget OutVT, float dt)
{
	local Vector fromTarget;
	local Vector desiredDir;
	local float  currAngle;
	local float  desiredAngle;
	local Vector CamNoZ;
	local Vector TargetNoZ;

	CamNoZ      = CurrentCamLocation;
	CamNoZ.Z    = 0.f;
	TargetNoZ   = OutVT.Target.Location;
	TargetNoZ.Z = 0.f;

	fromTarget = CamNoZ - TargetNoZ;
	
	if(fromTarget.X != 0) currAngle = Atan( fromTarget.Y / fromTarget.X );
	else                  currAngle = Pi/2;
	if(fromTarget.X < 0 ) currAngle -= Pi;

	desiredAngle = currAngle + DesiredTurn / TurnRateRatio;

	desiredDir.Y = Sin(desiredAngle);
	desiredDir.X = Cos(desiredAngle);

	CurrentCamLocation = OutVT.Target.Location + (desiredDir*VSize(fromTarget));

	fromTarget = OutVT.Target.Location - CurrentCamLocation;
	CurrentCamRotation = rotator(fromTarget + FreeCamOffset);
}
&#91;/uc&#93;

Great, now we just call RotationUpdate before FollowCamTarget and we're golden! Handshakes all around! Oh wait, there's still one more issue...
<h2 class="arTutorial">Clipping with Walls</h2>
The last problem we have to fix is when the player gets too close to the environment. Some games use an amazing trick where surfaces become transparent and you can just see through into the world. There's a lot of game design that goes into that as well and for most of us it's simpler to just make the camera clip with the environment.

Unreal provides us with a wonderful <em>Trace</em> function we can use to check collision. We'll just check if that collides with anything when we're done and if it does, move the camera inside of that collision. It's a quick bit of code to be inserted within UpdateViewTarget after the switch statement but prior to the assignment to OutVT.


// *****************************************************
HitActor = Trace(HitLocation, HitNormal, CurrentCamLocation, 
				OutVT.Target.Location, false, vect(12,12,12));
	
if (HitActor != none)
	CurrentCamLocation = HitLocation;

Wrap-Up

Now you’ve got a gorgeous 3rd person follow camera for your game. It’s a good idea to go back and tweak those distances we set up at the beginning now that it’s all together. Plus, a nice addition is to have the camera centered above the character instead of directly on it. This puts the focus in front of the character and doesn’t waste as much screen space on the floor.

Here are the completed classes ready for copy-paste magic!

class MG_Camera extends GamePlayerCamera;

var Vector  CurrentCamLocation;
var Rotator CurrentCamRotation;
var float   DesiredTurn;

var(FollowCam) float  FollowCamDist;
var(FollowCam) Vector FollowZOffset;
var(FollowCam) Vector CamLookOffset;
var(FollowCam) float  TurnRateRatio;

function UpdateViewTarget(out TViewTarget OutVT, float dt)
{
	local CameraActor	CamActor;
	local Vector        HitLocation, HitNormal;
	local Actor         HitActor;

	// Default FOV on viewtarget
	OutVT.POV.FOV = DefaultFOV;
	
	// Viewing through a camera actor
	CamActor = CameraActor(OutVT.Target);
	if( CamActor != None )
	{
		// In LD cinematic!
		CamActor.GetCameraView(dt, OutVT.POV);

		// Grab aspect ratio from the CameraActor
		bConstrainAspectRatio	= bConstrainAspectRatio || CamActor.bConstrainAspectRatio;
		OutVT.AspectRatio		= CamActor.AspectRatio;

		// CameraActor wants to override the PostProcess settings
		bCamOverridePostProcess = CamActor.bCamOverridePostProcess;
		CamPostProcessSettings = CamActor.CamOverridePostProcess;

		// Set my variables to account for current targeting
		CurrentCamLocation = OutVT.POV.Location;
		CurrentCamRotation = OutVT.POV.Rotation;
	}
	// Give Pawn Viewtarget a chance to dictate the camera position
	else if( Pawn(OutVT.Target) != None && Pawn(OutVT.Target).CalcCamera(dt, OutVT.POV.Location, OutVT.POV.Rotation, OutVT.POV.FOV) )
	{
		// If this ever happens send me an email!
		`log("NOTICE ME: No idea when this happens!");
		CurrentCamRotation = OutVT.POV.Rotation;
		CurrentCamLocation = OutVT.POV.Location;
	}
	// Finally, we can make our own camera!
	else
	{
		switch( CameraStyle )
		{
			case 'FollowCam':
				RotationUpdate(OutVT, dt);
				FollowCamTarget(OutVT, dt);
				break;
		}
		// *********************************************
		HitActor = Trace(HitLocation, HitNormal, CurrentCamLocation,
				OutVT.Target.Location, false, vect(12,12,12));
		
		if (HitActor != none)
			CurrentCamLocation = HitLocation;

		OutVT.POV.Location = CurrentCamLocation;
		OutVT.POV.Rotation = CurrentCamRotation;
	}

	// Lets LDs do shake and whatnot
	ApplyCameraModifiers(dt, OutVT.POV);
}


function FollowCamTarget(TViewTarget OutVT, float dt)
{
	local Vector toTarget;
	local Vector CamNoZ;
	local Vector TargetNoZ;

	CamNoZ      = CurrentCamLocation;
	CamNoZ.Z    = 0.f;
	TargetNoZ   = OutVT.Target.Location;
	TargetNoZ.Z = 0.f;

	toTarget = TargetNoZ - CamNoZ;
	toTarget = Normal(toTarget);

	CurrentCamLocation = toTarget * -FollowCamDist + OutVT.Target.Location + FollowZOffset;

	toTarget = OutVT.Target.Location - CurrentCamLocation;
	CurrentCamRotation = rotator(toTarget + CamLookOffset );
}


function RotationUpdate(TViewTarget OutVT, float dt)
{
	local Vector fromTarget;
	local Vector desiredDir;
	local float  currAngle;
	local float  desiredAngle;
	local Vector CamNoZ;
	local Vector TargetNoZ;

	CamNoZ      = CurrentCamLocation;
	CamNoZ.Z    = 0.f;
	TargetNoZ   = OutVT.Target.Location;
	TargetNoZ.Z = 0.f;

	fromTarget = CamNoZ - TargetNoZ;
	
	if(fromTarget.X != 0) currAngle    = Atan(fromTarget.Y / fromTarget.X);
	else                  currAngle    = Pi/2;
	if(fromTarget.X < 0 ) currAngle -= Pi;

	desiredAngle = currAngle + DesiredTurn / TurnRateRatio;

	desiredDir.Y = Sin(desiredAngle);
	desiredDir.X = Cos(desiredAngle);

	CurrentCamLocation = OutVT.Target.Location + (desiredDir*VSize(fromTarget));
}

DefaultProperties
{
	FollowCamDist=416.f
	DesiredTurn=0.f
	TurnRateRatio=10240.f
	FollowZOffset=(X=0.f,Y=0.f,Z=160.f)
	CamLookOffset=(X=0,Y=0,Z=100)

	DefaultFOV=70.f
}
&#91;/uc&#93;


&#91;uc collapse="true"&#93;
class MG_Input extends GamePlayerInput within MG_Controller;

var(MG_Input) float turnRate;

var bool    bMoveLocked;
var float   HotSpotRadius;
var Rotator currCamRot;
var Vector  oldJoy;

event PlayerInput( float DeltaTime )
{
	local Vector  move, facing, joy;
	local float   moveDeltaAbs;
	local Rotator rot, camRot, playerRot;

	super.PlayerInput(DeltaTime);

	move.X = aStrafe;
	move.Y = aForward;
	move.Z = 0.f;

	facing.x = aForward;
	facing.Y = aStrafe;
	facing.Z = 0.f;
	
	joy.X = RawJoyRight;
	joy.Y = RawJoyUp;

	moveDeltaAbs = Abs(joy.Y - oldJoy.Y) + Abs(joy.X - oldJoy.X);

	if(!bMoveLocked)
	{
		if(  ( move.X != 0.f || move.Y != 0.f ) && moveDeltaAbs < HotSpotRadius )
			bMoveLocked = true;

		currCamRot = MG_Camera(PlayerCamera).CurrentCamRotation;
		oldJoy = joy;
	}
	else if(aTurn != 0.f || moveDeltaAbs > HotSpotRadius)
	{
		bMoveLocked = false;
		oldJoy = joy;
	}

	playerRot.Yaw = Rotation.Yaw;
	camRot.Yaw = currCamRot.Yaw;

	move = TransformVectorByRotation(camRot, move, true);
	move = TransformVectorByRotation(playerRot, move, false);
	aForward = move.Y;
	aStrafe = move.X;

	facing = TransformVectorByRotation(camRot,facing,false);
	if( move.X != 0.f || move.Y != 0.f )
	{
		rot = RInterpTo( Rotation, Rotator( Normal(facing) ), DeltaTime, turnRate );
		SetRotation(rot);
	}
	else bMoveLocked = false;

	MG_Camera(PlayerCamera).desiredTurn = aTurn;
	aTurn = 0.f;
}


DefaultProperties
{
	bMoveLocked = false
	turnRate = 6.f
	HotSpotRadius = 0.25f

	// This keeps Unreal from messing with rotations
	bLockTurnUntilRelease = true
}