Third Person Camera Code Walk-through
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:
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:
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:
Then we move the camera in front of the first thing the ray hits:
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.
- 1
- 10
4 Comments
Recommended Comments