Jump to content
  • entries
    2
  • comments
    14
  • views
    8,855

Third Person Camera Code Walk-through


Chris Vossen

5,267 views

 Share

An in-depth look at a basic Third person camera.

 


Script.target = nil--Entity "Target"

Script.distance = 5--float

Script.debugphysics = false--bool

Script.debugnavigation = false--bool

Script.pitch = 35--float

Script.angle = 180--float

Script.pickradius = 0.5--float

Script.verticaloffset = 1.8--float

Script.smoothness = 30--int

 

function Script:Start()

if self.target==nil then return end

 

--debug functions for viewing physics or navigation in game

self.entity:SetDebugPhysicsMode(self.debugphysics)

self.entity:SetDebugNavigationMode(self.debugnavigation)

 

end

function Script:UpdatePhysics()

 

--Exit the function if the target entity doesn't exist

if self.target==nil then return end

 

local originalcamerapos = self.entity:GetPosition(true)

local targetpos = self.target:GetPosition(true)

local newcamerapos = self.target:GetPosition(true)

targetpos.y = targetpos.y+self.verticaloffset

self.entity:SetPosition(targetpos)

self.entity:SetRotation(self.target:GetRotation(true))

self.entity:Turn(self.pitch,self.angle,0)

self.entity:Move(0,0,-self.distance)

 

local targetpickmode = self.target:GetPickMode()

self.target:SetPickMode(0) --so that the pick doesn't hit the target

newcamerapos = self.entity:GetPosition()

local pickinfo = PickInfo()

if (App.world:Pick(targetpos,self.entity:GetPosition(),pickinfo,self.pickradius,true)) then

newcamerapos = pickinfo.position

end

newcamerapos.x = Math:Curve(originalcamerapos.x,newcamerapos.x,self.smoothness/Time:GetSpeed())

newcamerapos.y = Math:Curve(originalcamerapos.y,newcamerapos.y,self.smoothness/Time:GetSpeed())

newcamerapos.z = Math:Curve(originalcamerapos.z,newcamerapos.z,self.smoothness/Time:GetSpeed())

self.entity:SetPosition(newcamerapos)

self.target:SetPickMode(targetpickmode)--return pick mode to original value

end

 

 

At the top of the code I start off by declaring a set of script variables that will be used throughout the code:

 


Script.target = nil--Entity "Target"

Script.distance = 5--float

Script.debugphysics = false--bool

Script.debugnavigation = false--bool

Script.pitch = 35--float

Script.angle = 180--float

Script.pickradius = 0.5--float

Script.verticaloffset = 1.8--float

Script.smoothness = 30--int

 

Notice that these declarations all have a "--" followed by a type "float, bool, Entity, int" these are extra parameters that will make the variables appear in the engine:

 

blogentry-5181-0-83023900-1362708465_thumb.png

 

target: The target entity that the camera will follow.

distance: The default Z distance the camera will follow behind the target.

debugphysics & debugnavigation: Handy debug settings.

pitch: The pitch angle the camera will be facing.

angle: The angle that the character controller is rotated to.

pickradius: The width of the raycast that will check if there are objects between the camera and target

verticaloffset: The height of the camera offset.

smoothness: How smooth the camera will be when moving between positions.

 


function Script:Start()

if self.target==nil then return end

--debug functions for viewing physics or navigation in game

self.entity:SetDebugPhysicsMode(self.debugphysics)

self.entity:SetDebugNavigationMode(self.debugnavigation)

end

 

 

The start function is called once when the script is loaded and should be used to initialize variables. Here I check to make sure there is a target and then do 2 function calls to camera debug modes.

 


function Script:UpdatePhysics()

--Exit the function if the target entity doesn't exist

if self.target==nil then return end

local originalcamerapos = self.entity:GetPosition(true)

local targetpos = self.target:GetPosition(true)

local newcamerapos = self.target:GetPosition(true)

targetpos.y = targetpos.y+self.verticaloffset

self.entity:SetPosition(targetpos)

self.entity:SetRotation(self.target:GetRotation(true))

self.entity:Turn(self.pitch,self.angle,0)

self.entity:Move(0,0,-self.distance)

local targetpickmode = self.target:GetPickMode()

self.target:SetPickMode(0) --so that the pick doesn't hit the target

newcamerapos = self.entity:GetPosition()

local pickinfo = PickInfo()

if (App.world:Pick(targetpos,self.entity:GetPosition(),pickinfo,self.pickradius,true)) then

newcamerapos = pickinfo.position

end

newcamerapos.x = Math:Curve(originalcamerapos.x,newcamerapos.x,self.smoothness/Time:GetSpeed())

newcamerapos.y = Math:Curve(originalcamerapos.y,newcamerapos.y,self.smoothness/Time:GetSpeed())

newcamerapos.z = Math:Curve(originalcamerapos.z,newcamerapos.z,self.smoothness/Time:GetSpeed())

self.entity:SetPosition(newcamerapos)

self.target:SetPickMode(targetpickmode)--return pick mode to original value

end

 

 

 

The update physics section is where the magic happens, this function is called with every physics update.

 


--Exit the function if the target entity doesn't exist

if self.target==nil then return end

 

First I check again that a target entity actually exists. I do this just in case the target entity gets deleted at some point during the game, so that you wont be referencing a nil value later on.

 


 

local originalcamerapos = self.entity:GetPosition(true)

local targetpos = self.target:GetPosition(true)

local newcamerapos = self.target:GetPosition(true)

 

Three local variables are declared:

originalcamerapos: This will hold the original position of the camera and will be used in the smoothing function later on.

targetpos: This variable holds the position of the target entity.

newcamerapos: This variable will hold position of the next location a camera will be at.

 


targetpos.y = targetpos.y+self.verticaloffset

 

You want the camera to be positioned above the target character so you add the verticaloffset to targetpos.y

 

 


 

self.entity:SetPosition(targetpos)

self.entity:SetRotation(self.target:GetRotation(true))

self.entity:Turn(self.pitch,self.angle,0)

self.entity:Move(0,0,-self.distance)

 

These 4 function calls are awesome and are all that are needed to set the camera to a third person view.

 

1. You put the camera in the same position of the target.

 


self.entity:SetPosition(targetpos)

2. You rotate the camera to the same rotation as the target

 


self.entity:SetRotation(self.target:GetRotation(true))

3.Tilt the camera.

 


self.entity:Turn(self.pitch,self.angle,0)

4.Move the camera backwards along its local z axis.

 


self.entity:Move(0,0,-self.distance)

 

Now the camera is in the correct position, in a perfect world this is all that you would need... but sometimes walls or pillars will come in between the camera and the target blocking the cameras view and that is not cool at all. Luckily we can fix that with picking (a raycast)!

 

 


 

local targetpickmode = self.target:GetPickMode()

self.target:SetPickMode(0) --so that the pick doesn't hit the target

newcamerapos = self.entity:GetPosition()

 

if (App.world:Pick(targetpos,self.entity:GetPosition(),pickinfo,self.pickradius,true)) then

newcamerapos = pickinfo.position

end

 

First off we don't want our raycast to hit the target so we set the target pickmode to 0, but since other functions in the game might want to hit the target entity with a raycast we first need to save the original pickmode and reset it after our ray cast.

 

We also set the newcamerapos to the entities current position, this is for smoothing later.

 

 


local targetpickmode = self.target:GetPickMode()

self.target:SetPickMode(0) --so that the pick doesn't hit the target

newcamerapos = self.entity:GetPosition()

 

Now the fun starts. To understand this code you need to understand how the pick function works.

 

Imagine a scene where there is a wall between the player and the camera:

 

blogentry-5181-0-34949700-1362711329_thumb.png

 

If I was playing this game I'd be pissed because I couldn't see my player all I could see is a wall!

 

So we do a raycast from the target to camera and check if anything is in the way:

 

blogentry-5181-0-87304300-1362711523_thumb.png

 

Then we move the camera in front of the first thing the ray hits:

 

blogentry-5181-0-03721600-1362711558_thumb.png

 

 

Just as in the lovely pictures, you can perform the same thing in code:

 


local pickinfo = PickInfo()

 

if (App.world:Pick(targetpos,self.entity:GetPosition(),pickinfo,self.pickradius,true)) then

newcamerapos = pickinfo.position

end

 


local pickinfo = PickInfo()

A lightweight class that holds the information if a raycast hits an object.

 

 


App.world:Pick(targetpos,self.entity:GetPosition(),pickinfo,self.pickradius,true)

 

Does the raycast from the target to the camera (self.entity:GetPosition) pickradius is the radius of the ray, and true means return the 1st object hit.

 

 


if (App.world:Pick(targetpos,self.entity:GetPosition(),pickinfo,self.pickradius,true)) then

newcamerapos = pickinfo.position

end

 

If an object is hit, then set the camera's new position in front of that object.

 

 

The final thing we need to do is to smooth the camera movements so that it doesn't jump around and make users sick.

 


newcamerapos.x = Math:Curve(originalcamerapos.x,newcamerapos.x,self.smoothness/Time:GetSpeed())

newcamerapos.y = Math:Curve(originalcamerapos.y,newcamerapos.y,self.smoothness/Time:GetSpeed())

newcamerapos.z = Math:Curve(originalcamerapos.z,newcamerapos.z,self.smoothness/Time:GetSpeed())

self.entity:SetPosition(newcamerapos)

self.target:SetPickMode(targetpickmode)--return pick mode to original value

end

 

We call Math::Curve which takes two numbers (the new camera position and the original camera position) and will return a division based on the smoothness. Basically it breaks the camera movement into a bunch of little steps instead of a big jump.

 

For example if your camera had an x value of 0 and the next position was supposed to be at 100, if you just set the position to 100 the user would notice a large jump. but Math:Curve(0,100,10) would break the distance into 10 chunks meaning that instead of a 100 the camera would go to 10, on the first update then to 20, then 30.... basically it would be smoother.

 

Finally we set the pickmode back to it's original value in case a different raycast wants to hit the target.

 

And there you have it, a step by step explanation of the 3rd person camera.

  • Like 1
  • Upvote 10
 Share

4 Comments


Recommended Comments

Awesome, thanks so much Chris smile.png

Yeah what i like the most are the lovely pictures lol

 

LE 3 variable exposition features is indeed one of the most usefull new features.

Link to comment

I have a question :

 

self.entity:Turn(self.pitch,self.angle,0)

4.Move the camera backwards along its local z axis.

self.entity:Move(0,0,-self.distance)

 

***********

 

The last call to entity:Move

it is on Camera local Axis or global axis ? because in the global world axis a move on Z global wouldn't work.

So this move is along Z is a move along the vector : Camera -> character ?

Link to comment
Guest
Add a comment...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...