Archive for November, 2012

From the last tutorial, we have a camera to the right of the positive X axis but the player can still rotate to face any direction and move in whatever direction the player is facing. What we need is to prevent the player from turning and also have the player only move back and forth along the X axis. Player Movement On every frame, the PlayerController’s PlayerMove function is called. This function often overridden and implemented in the different movement states to provide state specific movement behavior. We’ll be overriding the function in the PlayerWalking state. This is the state the player is in when moving on solid ground. The function makes use of the current input to calculate the players new acceleration.

GetAxes(Pawn.Rotation,X,Y,Z);

// Update acceleration.
NewAccel = PlayerInput.aForward*X + PlayerInput.aStrafe*Y;
NewAccel.Z	= 0;
NewAccel = Pawn.AccelRate * Normal(NewAccel);

GetAxes uses the pawns rotation to get 3 normalized vectors each representing the rotation in the 3 axes. i.e. the X vector points to the direction the player is facing. The forward input is then used to scale the X vector and calculate the forward acceleration.

Moving the player back and forth using the strafe inputs

In a sides-scroller, the player moves back and forth using the strafe inputs. To implement this, we need to scale our X/forward vector by the value of aStrafe instead of the aForward input and zero out the movement along the Y axis (the Z axis is already zeroes out by default).

NewAccel = PlayerInput.aStrafe*X;
NewAccel.Y	= 0;
NewAccel.Z	= 0;

The player now only moves along the X axis.

Disabling the player’s turn

You’ll notice that the player can still turn, we only want the player to turn to either face the positive X axis or the negative X axis i.e. always a 180 degree turn.

The player’s rotation is calculated within the PlayerController.UpdateRotation function, which goes something like this:

1) Calculate Delta to be applied on ViewRotation based on the input.

DeltaRot.Yaw	= PlayerInput.aTurn;
DeltaRot.Pitch	= PlayerInput.aLookUp;

2) Let the ProcessViewRotation function calculate our rotation using the Delta.

ProcessViewRotation( DeltaTime, ViewRotation, DeltaRot );
SetRotation(ViewRotation);

3) Finally set the paw’s rotation.

NewRotation = ViewRotation;
NewRotation.Roll = Rotation.Roll;

if ( Pawn != None )
	Pawn.FaceRotation(NewRotation, deltatime);

What we need to do is calculate a new Delta depending on which direction the player is trying to move. While facing the positive X direction i.e. to the right, if the forward input is used (D on the keyboard) the Delta needs to be 0 and if the back input (A) is used the Delta needs to be 180 degrees.

Start by overriding the PlayerController.UpdateRotation function.

NOTE: I initially wanted to override the function within the PlayerWalking state and leave the logic within the other states intact but then I realised that this being a sidescroller game, the other states should default to sidescroller behavior and explicitly override when they need to.

The logic is that, with the player facing a certain direction, if the input is such that it requests the player to move in the opposite direction, we need to turn the player 180 degrees.

We’ll make use of vector dot products. An interesting property of the dot product of 2 vectors is that it it is less than zero, the two vectors are facing opposite directions i.e. the angle between them is between 90 and 180 degrees.

The two vectors we are referring to are

1) the player’s forward direction

To get the player’s forward direction, we’ll use the GetAxis function that we have seen previously.

GetAxes(Pawn.Rotation,X,Y,Z);

The player’s forward direction is off-course the X vector.

2) The forward input vector.The forward input comes from the PlayerInput.aStrafe (only because in a sidescroller, forward is the the strafe input ) which is positive when D is pressed (forward) and negative when A is pressed(backward). We start by creating a vector pointing in the positive x axis.

...
local Vector ForwardInput;
ForwardInput = vect(1.f, 0.f, 0.f);
...

Then multiply this vector by the forward input, which will make it either positive or negative. We then normalize the vector before we use it to calculate the dot product.

...
if(PlayerInput.aStrafe!= 0.f)
{
    ForwardInput = vect(1.f, 0.f, 0.f);
    // normalize the vector
    ForwardInput = Normal(ForwardInput);
}

NOTE: This is done within an if to make sure that there was a forward input, if we multiplied by a zero input, we’d end up with a zero vector which would not give us what we expect from the dot product.

Next we get the dot product of the player’s X vector and the forward input, if the two vectors are facing away from each other, we need to rotate the player by 180 degrees.

    ...
    ForwardInput = Normal(ForwardInput);
    if(X dot ForwardInput <= 0.f)
    {
        DeltaRot.Yaw	= 180 * DegToUnrRot;
    }
    ...

Run the game now to see what we have and try out the A and D keys.

You’ll notice a problem, the A key turns the player but, the player runs backward instead. The problem is that the A is still the back key. The A key should theoretically switch to the forward key when we are looking to the left. There are a number of ways to fix this.

Just to understand what going on we’ll first go the longer route then implement the easy fix.

Method 1 – Rotating the players X vector if we are facing left

Within the PlayerMove function, before we calculate the pawn’s new acceleration, we need to figure out if the player if facing the positive X direction (right) or the negative X direction (left). If the pawn/player is facing left, we rotate the X vector by 180 degrees so that it faces the opposite direction.

    GetAxes(Pawn.Rotation,X,Y,Z);

    if(X.X < 0)     
    {         
        X = X >> MakeRotator(0, DegToUnrRot * 180, 0);
    }

    // Update acceleration.
    NewAccel = PlayerInput.aStrafe*X;

NOTE: the vector’s >> operator rotates the vector by the given rotator.

Method 2 – Using the camera’s rotation

Our camera is always looking at the player from the player’s side, the camera’s Y vector is therefore parallel to the player’s X vector and always pointing in the positive X direction, regardless of which way the player is looking. We’ll use the GetAxes function to get the camera’s Y vector and use it to calculate our new acceleration.

    ...
    PlayerCamera.GetCameraViewPoint(CameraLocation, CameraRotation);
    GetAxes(CameraRotation,X,Y,Z);

    // Update acceleration.
    NewAccel = PlayerInput.aStrafe*Y;

And the full code listing of our player controller class.

class SKPlayerController extends UDKPlayerController;

function UpdateRotation( float DeltaTime )
{
	local Rotator	DeltaRot, newRotation, ViewRotation;
	local Vector ForwardInput, X, Y, Z;

	ViewRotation = Rotation;
	if (Pawn!=none)
	{
		Pawn.SetDesiredRotation(ViewRotation);
	}

	GetAxes(Pawn.Rotation,X,Y,Z);
	ForwardInput = vect(1.f, 0.f, 0.f);

	if(PlayerInput.aStrafe != 0.f)
	{
		ForwardInput *= PlayerInput.aStrafe;
		ForwardInput = Normal(ForwardInput);
		if(X dot ForwardInput <= 0.f)
		{
			DeltaRot.Yaw	= 180 * DegToUnrRot;
		}
	}

	// Calculate Delta to be applied on ViewRotation
	DeltaRot.Pitch	= PlayerInput.aLookUp;

	ProcessViewRotation( DeltaTime, ViewRotation, DeltaRot );
	SetRotation(ViewRotation);

	ViewShake( deltaTime );

	NewRotation = ViewRotation;
	NewRotation.Roll = Rotation.Roll;

	if ( Pawn != None )
		Pawn.FaceRotation(NewRotation, deltatime);
}

// Player movement.
// Player Standing, walking, running, falling.
state PlayerWalking
{
	function PlayerMove( float DeltaTime )
	{
		local vector			X,Y,Z, NewAccel, CameraLocation;
		local eDoubleClickDir	DoubleClickMove;
		local rotator			OldRotation, CameraRotation;
		local bool				bSaveJump;

		if( Pawn == None )
		{
			GotoState('Dead');
		}
		else
		{
			PlayerCamera.GetCameraViewPoint(CameraLocation, CameraRotation);
			GetAxes(CameraRotation,X,Y,Z);

			// Update acceleration.
			NewAccel = PlayerInput.aStrafe*Y;
			NewAccel.Y	= 0;
			NewAccel.Z	= 0;
			NewAccel = Pawn.AccelRate * Normal(NewAccel);

			if (IsLocalPlayerController())
			{
				AdjustPlayerWalkingMoveAccel(NewAccel);
			}

			DoubleClickMove = PlayerInput.CheckForDoubleClickMove( DeltaTime/WorldInfo.TimeDilation );

			// Update rotation.
			OldRotation = Rotation;
			UpdateRotation( DeltaTime );
			bDoubleJump = false;

			if( bPressedJump && Pawn.CannotJumpNow() )
			{
				bSaveJump = true;
				bPressedJump = false;
			}
			else
			{
				bSaveJump = false;
			}

			if( Role < ROLE_Authority ) // then save this move and replicate it
			{
				ReplicateMove(DeltaTime, NewAccel, DoubleClickMove, OldRotation - Rotation);
			}
			else
			{
				ProcessMove(DeltaTime, NewAccel, DoubleClickMove, OldRotation - Rotation);
			}
			bPressedJump = bSaveJump;
		}
	}	
}

defaultproperties
{
	CameraClass=class'SKPlayerCamera'
}

In the last tutorial, I mentioned that we could implement our camera update logic in our SKThirdPersonCamera.UpdateCamera function, this would technically work but would also result in a lot of unnecessary code – inherited from GameThirdPersonCamera.

Lets instead create a new camera type – SKSideScrollerCamera that inherits from GameCameraBase and override its UpdateCamera function where we will add our view calculations.

class SKSideScrollerCamera extends GameCameraBase;

/**
 * Called by PlayerCamera.UpdateViewTarget
 */
function UpdateCamera(Pawn P, GamePlayerCamera CameraActor, float DeltaTime, out TViewTarget OutVT)
{
}

defaultproperties
{
}

Take a moment to to figure out how we can get the game to use our new camera type.

And the right answer goes to assigning it to the SKPlayerCamera’s ThirdPersonCameraClass variable.

We now need to implement the UpdateCamera function to calculate our desired side-scroller view.

The player will only be moving along the X axis, either back or forward. The camera will be positioned to the right of the positive X axis with an offset of 400 units and will always be looking at the player – like in the diagram below.

On every frame/UpdateCamera call, we start off the calculation at our offset. The initial position is therefore at X=0.0, Y=400.0, Z=0.0. This position is relative to the world’s center. Since the player is moving, we need to make this position relative to the player instead. We do this by simple vector addition, the player is at X=10.0, Y=-5.0, Z=0.0, by adding our initial position, we get X=10.0, Y=395.0, Z=0.0.

class SKSideScrollerCamera extends GameCameraBase;

var Vector CamOffset;

/**
 * Called by PlayerCamera.UpdateViewTarget
 */
function UpdateCamera(Pawn P, GamePlayerCamera CameraActor, float DeltaTime, out TViewTarget OutVT)
{
    local Vector CamLocation;

    CamLocation = P.Location + CamOffset;	
    OutVT.POV.Location = CamLocation;
}

defaultproperties
{
	CamOffset=(X=0.f,Y=400.f,Z=0.f)
}

The camera is now always 400 units away from the player on the positive Y axis but is also facing the positive X axis and not the player.

We need to rotate the camera to look at the player. There a number of ways to do this, the simplest to to rotate the camera -90 degrees around the Z axis i.e. the Yaw.

NOTE: Unreal rotation units are expressed by values ranging from 0 to 65536 with 0 being 0 degrees and 65536 being 360 degrees. We will use the DegToUnrRot constant to convert by simple multiplication.

...
OutVT.POV.Rotation = DegToUnrRot * -90;
...

Lets also modify our offset a bit to have a bit of elevation. We do this by modifying the Z offset value.

Our final camera code looks like this.

class SKSideScrollerCamera extends GameCameraBase;

var Vector CamOffset;

/**
 * Called by PlayerCamera.UpdateViewTarget
 */
function UpdateCamera(Pawn P, GamePlayerCamera CameraActor, float DeltaTime, out TViewTarget OutVT)
{
	local Vector CamLocation;
	local Rotator CamRotation;

	// calculate the new location
	CamLocation = P.Location + CamOffset;

    // calculate the new rotation
    CamRotation.Yaw = DegToUnrRot * -90;
    CamRotation.Pitch = 0;
    CamRotation.Roll = 0;

    OutVT.POV.Location = CamLocation;
    OutVT.POV.Rotation = CamRotation;
}

defaultproperties
{
	CamOffset=(X=0.f,Y=400.f,Z=50.f)
}

The camera rotation can also be calculated using the direction between the player’s and the camera’s locations. This would be a more flexible approach if  for example the player could move along an arbitrary axis. To get this direction, we do some vector subtraction. Since we want the direction from the camera to the player, we subtract the camera’s location from the player’s.

...
local Vector CamDir;

CamDir = P.Location - CamLocation;
...

From our diagram above, this will give us the vector X=0.0, Y=-400.0, Z=0.0. Since its a direction vector, we need to normalize it.

As a direction, there is no difference between our subtraction result vector and its normalized representation, X=0.0, Y=-1.0,  Z=0.0, they both go directly up the negative Y axis.

CamDir = Normal(P.Location - CamLocation);

We then need to convert the direction vector into a rotation. We do this by constructing a Rotator with the vector, we end up with a Rotator oriented in the vectors direction.

CamRotation = Rotator(CamDir);

And the final code listing using the subtraction method

class SKSideScrollerCamera extends GameCameraBase;

var Vector CamOffset;

/**
 * Called by PlayerCamera.UpdateViewTarget
 */
function UpdateCamera(Pawn P, GamePlayerCamera CameraActor, float DeltaTime, out TViewTarget OutVT)
{
	local Vector CamLocation;
	local Rotator CamRotation;
	local Vector CamDir;

	/// get the new location
	CamLocation = P.Location + CamOffset;

	/// get the new direction
    CamDir = Normal(OutVT.Target.Location - CamLocation);
    CamRotation = Rotator(CamDir);

    OutVT.POV.Location = CamLocation;
    OutVT.POV.Rotation = CamRotation;
}

defaultproperties
{
	CamOffset=(X=0.f,Y=400.f,Z=50.f)
}