Sunday, September 16, 2012

Side-scroller camera in UDK

I’m currently working on a side-scroller game and just want to share how I implemented my side-scroller camera.

 NOTE: This all assumes you already have a PlayerController and a Pawn with a skeletal mesh already set-up.

 According to the UDK documentation here - http://udn.epicgames.com/Three/CameraTechnicalGuide.html, you can implement a camera in one of 2 ways.

 The first and also quickest is to override your Pawn’s CalcCamera function and calculate the views rotation and location.

 The second is by overriding the camera class which is said to be more flexible and allows you to have fancy things like camera animations and effects. This is what I did by sub-classing the Camera class.

Lets start by creating this sub-class. We need to figure out the camera's location.

1. Create a vector that points to the right of the characters position, this is basically a vector pointing down Y in world coordinates.

2. Offset this unit vector by multiplying it by the number of units we want. This moves the camera's position to the right of the character by said units.

3. Add any other offsets we want in the X and Z directions, in my case I want my camera slightly moved up.

This is achieved by defining a vector with these offsets - FollowCamDist which is initialised as (X=0.f,Y=400.f,Z=100.f)

Once we have this position, we need to rotate the camera to look at the character. This is done by getting the direction from the camera's position to the character's position like so:

 CamDir = (OutVT.Target.Location - CamLocation);

Then converting this to a Rotator and assigning it as the camera's rotation.

And here is the complete camera class. The stuff discussed above is what is within the highlighted block. The rest of the function is picked from the base Camera class.


class DecadePlayerCamera extends Camera;

var Vector FollowCamDist;

/**
 * Query ViewTarget and outputs Point Of View.
 *
 * @param   OutVT      ViewTarget to use.
 * @param   DeltaTime   Delta Time since last camera update (in seconds).
 */
function UpdateViewTarget(out TViewTarget OutVT, float DeltaTime)
{
 local vector  HitLocation, HitNormal, CamDir, CamLocation;
 local Actor   HitActor;
 local CameraActor CamActor;
 local TPOV   OrigPOV;
 local Pawn          TPawn;

 // Don't update outgoing viewtarget during an interpolation
 if( PendingViewTarget.Target != None &&
        OutVT == ViewTarget && BlendParams.bLockOutgoing )
 {
  return;
 }

 // store previous POV, in case we need it later
 OrigPOV = OutVT.POV;

 // Default FOV on viewtarget
 OutVT.POV.FOV = DefaultFOV;

 // Viewing through a camera actor.
 CamActor = CameraActor(OutVT.Target);
 if( CamActor != None )
 {
  CamActor.GetCameraView(DeltaTime, OutVT.POV);

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

  // See if the CameraActor wants to override the PostProcess settings used.
  CamOverridePostProcessAlpha = CamActor.CamOverridePostProcessAlpha;
  CamPostProcessSettings = CamActor.CamOverridePostProcess;
 }
 else
 {
  TPawn = Pawn(OutVT.Target);
  // Give Pawn Viewtarget a chance to dictate the camera position.
  // If Pawn doesn't override the camera view, then we proceed with our own defaults
  if( TPawn == None || !TPawn.CalcCamera(DeltaTime, OutVT.POV.Location,
            OutVT.POV.Rotation, OutVT.POV.FOV) )
  {

   /// make a unit vector that points down the Y/left axis seems that for the camrea, X is forward - perpediculr to pawn rotation
            CamDir = vect(0.f, 1.f, 0.f);

            /// displace vector by Z offset
            CamDir *= FollowCamDist.Y;

            /// offset direction by follow distance
            CamLocation = CamDir + OutVT.Target.Location;

            CamLocation.X += FollowCamDist.X;
            CamLocation.Z += FollowCamDist.Z;

            /// handle collisions @todo: fix player mesh intersection
            HitActor = Trace(HitLocation, HitNormal, CamLocation,
                OutVT.Target.Location, false, vect(12,12,12));

            //if (HitActor != none)
            //    CamLocation = HitLocation;

            /// set view position and location
            OutVT.POV.Location = CamLocation;

            /// get the new direction
            CamDir = (OutVT.Target.Location - CamLocation);
            OutVT.POV.Rotation = Rotator(CamDir);
  }
 }

    ApplyCameraModifiers(DeltaTime, OutVT.POV);
 //`log( WorldInfo.TimeSeconds  @ GetFuncName() @ OutVT.Target @ OutVT.POV.Location @ OutVT.POV.Rotation @ OutVT.POV.FOV );
}

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

Now lets look at how to make the character only walk along the X axis. Input is translated into movement in the PlayerWalking state's PlayerMove function.



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

What we want is to use the strafe input to move the player. We'll take the player's X direction and multiply it by the strafe input. Then we'll zero out the Y movement (Z is already zeroed out)


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

We also need to prevent the player from turning around the Z axis, this we do in the PlayerController's UpdateRotation function by removing the line that uses the aTurn input to update the Yaw rotation.


...
DeltaRot.Yaw = PlayerInput.aTurn; // turning is not allowed
...

And here is the full code listing for the PlaeyrController class

NOTE: the character runs backwards but likely what we want is to have the player change direction when the A key or equivalent is used. I'm still working on this and will post an update when I can.

As always questions, comments and suggestions are welcome.




class DecadePlayerController extends AcventurePlayerController;

function UpdateRotation( float DeltaTime )
{
 local Rotator DeltaRot, newRotation, ViewRotation;

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

 // Calculate Delta to be applied on ViewRotation
 DeltaRot.Yaw = 0; // turning is not allowed
 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
{
    ignores SeePlayer, HearNoise, Bump;

 function PlayerMove( float DeltaTime )
 {
  local vector   X,Y,Z, NewAccel;
  local eDoubleClickDir DoubleClickMove;
  local rotator   OldRotation;
  local bool    bSaveJump;

  if( Pawn == None )
  {
   GotoState('Dead');
  }
  else
  {
   GetAxes(Pawn.Rotation,X,Y,Z);

   // Update acceleration.
            //@TODO: use aStrafe * X and figure out how to change pawns direction
   NewAccel = PlayerInput.aStrafe * X;
   NewAccel.Z = 0;
            NewAccel.Y = 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;
  }
 }
Begin:
}

defaultproperties
{
    CameraClass=class'SideScrollerCamera'
}