Latest  | Search | Go
Edit this page   |   Attach file 

  Home | Tutorials | Technical Reference | Runtime | API Documentation | ThirdPersonCamera  

Giving DemoPlayer a Third Person Camera

Author:Jeremy Stieglitz


thirdpersoncamera.jpg

Introduction

This tutorial assumes you've gone through the DemoPlayer Tutorial and are familiar with its methods. We'll now be adding a toggle-able Third Person option to DemoPlayer, complete with and animated character Model and collision-tested camera positioning.

Loading the Player Model & Animation

The first step in creating this third person view for the DemoPlayer will be loading a character Model so that we'll be able to see it from the external view. Since we'll be using the EvalKit media for this tutorial, copy the "Alien" folder from your "Bin\EvalKit\Models" directory into your "Bin\Models" directory.

In the previous tutorial, we were already Loading an arrow Model to represent the DemoPlayer in the Editor, but we were not loading any Model for use when you are controlling the DemoPlayer in-game. Let's change that Arrow model to the Alien model, the same one we'll be using when we actually are controlling the character in the Game. That will allow us to position the character more accurately in the Editor. Let's also add an idle animation handle for this character. To Load this animation handle as a static resource, let's modify our Precache() function to do it once! Our final Precaching media section will now be as follows (you can replace the analogous portion of DemoPlayer.cs with this):

   /// -----------------------------------------------
   /// PRECACHING & STATIC DATA VALUES
   /// -----------------------------------------------
   public static string ClassName = "DemoPlayer";
   private static bool HasCached = false;
   public static void Precache() 
   {
      bool Cached = MPrecacher.Precache(ClassName, HasCached);
      if (!HasCached)
      {
         StaticAnimHandleIdle = StaticModel.LoadAnimation("Alien_idle.X",true);
         HasCached = Cached;
      }
   }
   ///
   // load our player model for use
   static private MModel StaticModel = MPrecacher.PrecacheModel(ClassName, "Alien.xml");
   // it's good practice to set all anim handles should to -1 until we load them
   static private int StaticAnimHandleIdle = -1;
   /// -----------------------------------------------
   /// -----------------------------------------------
   /// 

We also need to alter our DemoPlayer constructor to use this character Model in-Game. We'll also create a new boolean variable to store whether we're in Third Person View. So our new DemoPlayer constructor will be:

/// <summary>
   /// Whether we're in Third Person View or not. Default true
   /// </summary>
   bool IsInThirdPerson = true;

   /// <summary>
   /// ctor
   /// </summary>
   public DemoPlayer(MWorld world) : base(world)
   {
      Precache();

      // let's create a new Model instance of our character Model
      MyModel = new MModel();
      StaticModel.CreateNewInstance(MyModel);
      // and play the Idle animation on it
      MyModel.TransitionToAnimation(StaticAnimHandleIdle, 0);

      // In-game, we want these basic Engine Physics states, 
      // they're ideal for a wall-sliding, stair-climbing, pushable character in the world
      CollisionFlags = COLLISION_FLAGS.CF_BBOX;
      PhysicsFlags = PHYSICS_FLAGS.PHYS_PUSHABLE | PHYSICS_FLAGS.PHYS_ONBLOCK_CLIMBORSLIDE;         

      // set the demoplayer's collision box size
      SetCollisionBox(new MVector(-DemoPlayerBoxWidth, -DemoPlayerBoxHeight / 2, -DemoPlayerBoxWidth),
            new MVector(DemoPlayerBoxWidth, DemoPlayerBoxHeight / 2, DemoPlayerBoxWidth));

      // we'll create the DemoPlayer in slightly different ways
      // depending on whether we're in Reality Builder or in the actual Game
      if (MHelpers.EditorMode)
      {         
         
         CollisionFlags |= COLLISION_FLAGS.CF_ALLOW_STUCK;
            PhysicsFlags |= PHYSICS_FLAGS.PHYS_NOT_AFFECTED_BY_GRAVITY;
      }
      else
      {
         // In-game, let's set this DemoPlayer as the GameCore's avatar, 
         // so that the GameCore will call its input and camera updating functions during the World Tick!
         GameCore.Game.LocalAvatar = this;

         // hide the main menu, because we're ready to control this DemoPlayer now
         if (!MHelpers.IsDedicated())
            GameCore.Gui_MainMenu.Hide();
      }
   // let's give this character a dropshadow
   ShadowType = Shadows.Drop;
   }

Updating the Camera for a 'Third Person View'

Now we'll modify the UpdateCamera function to put the camera a transformation behind the DemoPlayer. We'll create a new function called GetCameraTransform() to keep all our Camera math in its own function. Note that we'll also set the IsHidden state of the Actor based on whether you're in third person view or first person view.

   /// <summary>
   /// This function is called by the GameCore.cs in Tick when you declare this Actor to be the GameCore's avatar
   /// </summary>
   public override void UpdateCamera()
   {
      IsHidden = !IsInThirdPerson;
      MMatrix cameraTransform = GetCameraTransform(IsInThirdPerson);
      MCameraHandler.setToView(cameraTransform.m3, cameraTransform.GetRotationMatrix());
   }

Here in this new function we'll calculate the Camera transformation based on whether we're in Third Person View or First Person View. If we're in First Person View, just set the returned Matrix (camTransform) to what we'll call the Eye position of the DemoPlayer. If we're in Third Person View, however, we'll offset the position of the camTransform to be at a location behind the Player, and we'll do a raytest to ensure that the camTransform's desired position doesn't collide with any geometry. If it does collide, we'll move the position a little bit to compensate. Then we'll make the camTransform look to where this Actor is looking, and return the camTransform. That's it for updating the Camera!

   /// <summary>
   /// Gets Third Person or First Person camera transformation for this Player's viewpoint
   /// </summary>
   protected MMatrix GetCameraTransform(bool thirdPerson)
   {
      MMatrix camTransform;
         // when the player is in first person view
         if (!thirdPerson)
         {
            // just set the camera to the DemoPlayer's Location plus half the height of the bbox up, 
            // so that his eyes are located at the top of the bbox
            // and use his Rotation too.
            camTransform = new MMatrix(Rotation, Location + new MVector(0, m_DemoPlayerBoxHeight / 2, 0));
         }
         else
         {
            const float CameraCollisionTestSize = .3f;
            const float CameraDirOffset = 1.75f;
            const float CameraYOffset = 0;
            const float CameraMaxYRadians = 1.3f;

            MMatrix firstPersonTransform = GetCameraTransform(false);

            // third person camera
            MVector startPos = firstPersonTransform.m3;

            MVector rotDir = firstPersonTransform.GetDir();

            // limit the pitch of the translation of the camera so we aren't looking up the character's, er, legs
            float pitchAngle = rotDir.RadAngle(MHelpers.VectorDown) - (float)Math.PI / 2;
            if (pitchAngle > CameraMaxYRadians)
               pitchAngle = CameraMaxYRadians;

            // get the yaw angle of the character's Rotation so we can reassemble the Rotation Matrix with our new pitch..
            float yawAngle = MHelpers.Rad2Deg((float)Math.Atan2((double)rotDir.z, (double)-rotDir.x)) - 90;

            //.. like so
            MMatrix cameraRot = MMatrix.LookTowards(MVector.MakeDirection(yawAngle, MHelpers.Rad2Deg(-pitchAngle), 0));

            MVector testCamPos = startPos + CameraYOffset * MHelpers.VectorUp + -CameraDirOffset * cameraRot.GetDir();

            // ray test the desired camera position so it doesn't go through walls
            // if a collision is found, put it a little ways in front of the wall
            MCollisionInfo info = new MCollisionInfo();
            if (MyWorld.CollisionCheckRay(this, startPos, testCamPos + CameraCollisionTestSize * (testCamPos - startPos).Normalized(), 
                MCheckType.CHECK_EVERYTHING, info))
            {
               testCamPos = info.point + CameraCollisionTestSize * (startPos - testCamPos).Normalized();
            }

            // camera's rotation should be towards the player's infinite look point, no matter where the Camera itself is
            camTransform = MMatrix.LookTowards(((startPos + rotDir * 500) - testCamPos).Normalized());

            // put the camera's location at our determined point
            camTransform.m3 = testCamPos;
         }
         return camTransform;
   }

Because the Alien.xml Model is rotated backwards, and its pivot point is about a meter too low when compared to the centered DemoPlayer Location, we'll compensate in OnRender() by rotating the yaw of the Model transformation -180 degrees, and offsetting vertically by a meter. Bear in mind, this may not be necessary with your own character Models, if you have constructed them facing the correct direction.

   public override void OnRender(MCamera camera)
   {
      // since we don't want our entire Player model to actually rotate vertically
      // where we're look, we'll clear the Model's Y direction here
      MVector direction = Rotation.GetDir();
      direction.y = 0;
      MMatrix newRotation = MMatrix.LookTowards(direction.Normalized());

      // also, let's rotate the Player Model -180 degrees on the Y axis, 
      // to fix the Alien.xml's backwards rotation
      const float RotationYawFix = -180;
      // and a -1 Y offset to fix the Alien.xml's Locational offset 
      // from the Location-centered DemoPlayer
      const float TranslationYFix = -1.1f;
      newRotation.Rotate(0, RotationYawFix, 0);
      MVector locationOffset = TranslationYFix*newRotation.GetUp();
      MyModel.SetTransform(newRotation, Location + locationOffset);
   }

Playing some basic movement animations

We'll also play some simple Animations for this Actor. In its Tick, if it's moving we'll Transition to a running state. Since the run and idle animations were loaded as a looping animation, we can continually TransitionToAnimation() on them and they will not restart the looping playback. If it was a non-looping Animation, playback would begin anew each time we called TransitionToAnimation().

   public override void Tick()
   {
      // set our animations depending on whether this DemoPlayer is running
      if (Velocity.Length() > 1.8f && CurrentState == STATE_ONGROUND)
            MyModel.TransitionToAnimation(StaticAnimHandleRun, .3f);
      else
            MyModel.TransitionToAnimation(StaticAnimHandleIdle, .3f);

      base.Tick();
   }


Complete DemoPlayer.cs with Third Person View

Here's the entire class you can use. Remember, because of the media used in this code, it will be necessary to copy the "EvalKit\Models\Alien\" folder into the SimpleModule's "Models" folder or the shared "Bin\Models" folder.


//=========== (C) Copyright 2004, Artificial Studios. All rights reserved. ================
/// DemoPlayer:  A simple example avatar that processes input and updates the camera
/// 
/// A tutorial that describes the creation of this class is located at: 
/// http://reality.artificialstudios.com/twiki/bin/view/Main/DemoPlayer
/// 
/// Author: Jeremy Stieglitz
//=========================================================================================

#region Using directives
using System;
using ScriptingSystem;
using System.ComponentModel;
#endregion

[Browsable(true)]
public class DemoPlayer : MActor
{
/// -----------------------------------------------
/// PRECACHING & STATIC DATA VALUES
/// -----------------------------------------------
public static string ClassName = "DemoPlayer";
private static bool HasCached = false;
public static void Precache() 
{
bool Cached = MPrecacher.Precache(ClassName, HasCached);
if (!HasCached)
{
   StaticAnimHandleIdle = StaticModel.LoadAnimation("Alien_idle.X",true);
   StaticAnimHandleRun = StaticModel.LoadAnimation("Alien_runforward.X", true);
   HasCached = Cached;
}
}
///
// load our player model for use
static private MModel StaticModel = MPrecacher.PrecacheModel(ClassName, "Alien.xml");
// it's good practice to set all anim handles should to -1 until we load them
static private int StaticAnimHandleIdle = -1;
static private int StaticAnimHandleRun = -1;
/// -----------------------------------------------
/// -----------------------------------------------
/// 

private float m_WalkSpeed = 6.0f;
[Description("The speed at which the DemoPlayer walks, in meters per second.")]
public float WalkSpeed
{
get { return m_WalkSpeed; }
set { m_WalkSpeed = value; }
}

private float m_JumpVelocity = 8.0f;
[Description("The Y velocity with which the DemoPlayer jumps off the ground.")]
public float JumpVelocity
{
get { return m_JumpVelocity; }
set { m_JumpVelocity = value; }
}

private float m_DemoPlayerBoxWidth = 0.4f;
[Description("The width of the DemoPlayer's bounding box, in meters.")]
public float DemoPlayerBoxWidth
{
get { return m_DemoPlayerBoxWidth; }
set { m_DemoPlayerBoxWidth = value; }
}

private float m_DemoPlayerBoxHeight = 2;
[Description("The height of the DemoPlayer's bounding box, in meters. \nThe DemoPlayer's eyes will be located at the top of this height.")]
public float DemoPlayerBoxHeight
{
get { return m_DemoPlayerBoxHeight; }
set { m_DemoPlayerBoxHeight = value; }
}

/// <summary>
/// Whether we're in Third Person View or not. Default true
/// </summary>
bool IsInThirdPerson = true;

/// <summary>
/// ctor
/// </summary>
public DemoPlayer(MWorld world) : base(world)
{
Precache();

// let's create a new Model instance of our character Model
MyModel = new MModel();
StaticModel.CreateNewInstance(MyModel);
// and play the Idle animation on it
MyModel.TransitionToAnimation(StaticAnimHandleIdle, 0);

// In-game, we want these basic Engine Physics states, 
// they're ideal for a wall-sliding, stair-climbing, pushable character in the world
CollisionFlags = COLLISION_FLAGS.CF_BBOX;
PhysicsFlags = PHYSICS_FLAGS.PHYS_PUSHABLE | PHYSICS_FLAGS.PHYS_ONBLOCK_CLIMBORSLIDE;         

// set the demoplayer's collision box size
SetCollisionBox(new MVector(-DemoPlayerBoxWidth, -DemoPlayerBoxHeight / 2, -DemoPlayerBoxWidth),
      new MVector(DemoPlayerBoxWidth, DemoPlayerBoxHeight / 2, DemoPlayerBoxWidth));

// we'll create the DemoPlayer in slightly different ways
// depending on whether we're in Reality Builder or in the actual Game
if (MHelpers.EditorMode)
{         
   
   CollisionFlags |= COLLISION_FLAGS.CF_ALLOW_STUCK;
    PhysicsFlags |= PHYSICS_FLAGS.PHYS_NOT_AFFECTED_BY_GRAVITY;
}
else
{
   // In-game, let's set this DemoPlayer as the GameCore's avatar, 
   // so that the GameCore will call its input and camera updating functions during the World Tick!
   GameCore.Game.LocalAvatar = this;

   // hide the main menu, because we're ready to control this DemoPlayer now
   if (!MHelpers.IsDedicated())
      GameCore.Gui_MainMenu.Hide();
}

// let's give this character a dropshadow
ShadowType = Shadows.Drop;
}

/// <summary>
/// This function is called by the GameCore.cs in Tick when you declare this Actor to be the GameCore's avatar
/// </summary>
public override void ProcessInput()
{
// calculate Player Rotation according to mouse yaw and pitch
Rotation = MMatrix.LookTowards(MVector.MakeDirection(MInput.mouseYaw, MInput.mousePitch, 0));

MVector newDirection = new MVector(); // User's new attempted direction

//basic movement inputs

// strafing left and right
if (GameInput.ControlDown(GameInput.KEY_STRAFE_LEFT))
   newDirection += 0.5f * (-Rotation.GetRight());
else if (GameInput.ControlDown(GameInput.KEY_STRAFE_RIGHT))
   newDirection += 0.5f * (Rotation.GetRight());

// moving forward and backwards
if (GameInput.ControlDown(GameInput.KEY_WALK_FORWARD))
   newDirection += Rotation.GetDir();
else if (GameInput.ControlDown(GameInput.KEY_WALK_BACKWARD))
   newDirection -= Rotation.GetDir();

newDirection.y = 0; // demoplayer movement has no Y component

if (CurrentState == STATE_ONGROUND)
   Accelerate(newDirection.Normalized(), WalkSpeed, 46); // ground acceleration
else
   Accelerate(newDirection.Normalized(), WalkSpeed, 7); // air acceleration

// check if the jump key is pressed, and if the user's feet are on the ground, jump him
if (GameInput.ControlJustPressed(GameInput.KEY_JUMP) && CurrentState == STATE_ONGROUND)
   Velocity.y = JumpVelocity;
}


/// <summary>
/// This function is called by the GameCore.cs in Tick when you declare this Actor to be the GameCore's avatar
/// </summary>
public override void UpdateCamera()
{
IsHidden = !IsInThirdPerson;
MMatrix cameraTransform = GetCameraTransform(IsInThirdPerson);
MCameraHandler.setToView(cameraTransform.m3, cameraTransform.GetRotationMatrix());
}

/// <summary>
/// Gets Third Person or First Person camera transformation for this Player's viewpoint
/// </summary>
protected MMatrix GetCameraTransform(bool thirdPerson)
{
MMatrix camTransform;
   // when the player is in first person view
   if (!thirdPerson)
   {
      // just set the camera to the DemoPlayer's Location plus half the height of the bbox up, 
      // so that his eyes are located at the top of the bbox
      // and use his Rotation too.
      camTransform = new MMatrix(Rotation, Location + new MVector(0, m_DemoPlayerBoxHeight / 2, 0));
   }
   else
   {
      const float CameraCollisionTestSize = .3f;
      const float CameraDirOffset = 1.75f;
      const float CameraYOffset = 0;
      const float CameraMaxYRadians = 1.3f;

      MMatrix firstPersonTransform = GetCameraTransform(false);

      // third person camera
      MVector startPos = firstPersonTransform.m3;

      MVector rotDir = firstPersonTransform.GetDir();

      // limit the pitch of the translation of the camera so we aren't looking up the character's, er, legs
      float pitchAngle = rotDir.RadAngle(MHelpers.VectorDown) - (float)Math.PI / 2;
      if (pitchAngle > CameraMaxYRadians)
         pitchAngle = CameraMaxYRadians;

      // get the yaw angle of the character's Rotation so we can reassemble the Rotation Matrix with our new pitch..
      float yawAngle = MHelpers.Rad2Deg((float)Math.Atan2((double)rotDir.z, (double)-rotDir.x)) - 90;

      //.. like so
      MMatrix cameraRot = MMatrix.LookTowards(MVector.MakeDirection(yawAngle, MHelpers.Rad2Deg(-pitchAngle), 0));

      MVector testCamPos = startPos + CameraYOffset * MHelpers.VectorUp + -CameraDirOffset * cameraRot.GetDir();

      // ray test the desired camera position so it doesn't go through walls
      // if a collision is found, put it a little ways in front of the wall
      MCollisionInfo info = new MCollisionInfo();
      if (MyWorld.CollisionCheckRay(this, startPos, testCamPos + 
                 CameraCollisionTestSize * (testCamPos - startPos).Normalized(), MCheckType.CHECK_EVERYTHING, info))
                {
         testCamPos = info.point + CameraCollisionTestSize * (startPos - testCamPos).Normalized();
                }

      // camera's rotation should be towards the player's infinite look point, no matter where the Camera itself is
      camTransform = MMatrix.LookTowards(((startPos + rotDir * 500) - testCamPos).Normalized());

      // put the camera's location at our determined point
      camTransform.m3 = testCamPos;
   }
   return camTransform;
}

public override void Tick()
{
// set our animations depending on whether this DemoPlayer is running
if (Velocity.Length() > 1.8f && CurrentState == STATE_ONGROUND)
      MyModel.TransitionToAnimation(StaticAnimHandleRun, .3f);
else
      MyModel.TransitionToAnimation(StaticAnimHandleIdle, .3f);

base.Tick();
}

public override void OnRender(MCamera camera)
{
// since we don't want our entire Player model to actually rotate vertically
// where we're look, we'll clear the Model's Y direction here
MVector direction = Rotation.GetDir();
direction.y = 0;
MMatrix newRotation = MMatrix.LookTowards(direction.Normalized());

// also, let's rotate the Player Model -180 degrees on the Y axis, 
// to fix the Alien.xml's backwards rotation
const float RotationYawFix = -180;
// and a -1 Y offset to fix the Alien.xml's Locational offset 
// from the Location-centered DemoPlayer
const float TranslationYFix = -1.1f;
newRotation.Rotate(0, RotationYawFix, 0);
MVector locationOffset = TranslationYFix*newRotation.GetUp();
MyModel.SetTransform(newRotation, Location + locationOffset);
}

/// <summary>
/// Our Quake-style walk acceleration function
/// </summary>
protected void Accelerate(MVector wishdir, float wishspeed, float accel)
{
// apply acceleration towards desired direction, with removal of over-acceleration.
float addspeed, currentspeed;

//calculate the acceleration vecetor to add to achieve the desired velocity direction
currentspeed = Velocity.Dot(wishdir);

if (wishspeed == currentspeed)
   return;

// add time-scaled acceleration
if (wishspeed > currentspeed)
   addspeed = accel * MHelpers.DeltaTime;
else
   addspeed = -accel * MHelpers.DeltaTime;

// limit acceleration to exact needed value to achieve desired speed
if ((currentspeed + addspeed > wishspeed && wishspeed > currentspeed) || (currentspeed + addspeed < wishspeed && wishspeed < currentspeed))
   addspeed = wishspeed - currentspeed;

// add the calculated acceleration amount in the desired direction to our current Velocity
Velocity += addspeed * wishdir;
}

/// <summary>
/// Called right after the World renders, to allow custom canvas drawing with regards to this Actor
/// In the DemoPlayer's case, we'll draw a very simple HUD
/// </summary>
public override void PostRender(MCamera camera)
{
// Some way cool HUD text
if(GamePrefs.Instance.displayHUD)
   MCanvas.TextCenteredf(MCanvas.MediumFont, MHelpers.ColorFromRGBA(200, 255, 255, 190), 512, 740, "I am a DemoPlayer.");
}

/// <summary>
/// Disposal of the Actor's resources, called when the Actor is destroyed,
/// namely by having its LifeTime set to 0, having Destroy() called on it, or upon World unloading/app shutdown
/// </summary>
protected override void DisposeResources()
{
// if this actor is currently the GameCore's local avatar,
// it's good C# programming practice to nullify the local avatar reference
// when this Actor is disposed of.
if (GameCore.Game.LocalAvatar == this)
   GameCore.Game.LocalAvatar = null;

base.DisposeResources();
}
}


Attachment sort Action Size Date Who Comment
thirdpersoncamera.jpg manage 110.9 K 11 May 2005 - 22:16 Main.guest  

ThirdPersonCamera   Edit | Attach | Ref-By | Printable | Diffs | r1.2 | > | r1.1 | More