|
|
 |
|
|
| Dynamic
Third-Person Camera (Part 1) |
Disclaimer There
are usually many ways to implement a feature into a
complex code-base like V12. I tried a few different
approaches (some of which are detailed in the Background
Research section) and this one seemed to work best.
As I continue to work with the V12 Engine and
surrounding community, a better way to implement this
camera may be revealed - at which time, I will most
likely update our code and this tutorial.
Requirements The
camera manipulation code is all handled in the V12
Engine itself. Therefore, you will need to have the
ability to compile and build the V12 Engine in order to
complete this tutorial. This tutorial is based on
version 1.0 of the V12 SDK.
How To Read This
Document The instructions presented here
assume you have some basic knowledge of C++ programming.
There are code samples associated with each set of
instructions to help explain the specific procedure(s).
Since many people will be modifying their own version of
the V12 Engine, their code may be different than what is
presented here - that is why I tried to write the
textual instructions as informatively as possible.
The code block examples (backed in gray) show
the general section of code where you can make edits to
accomplish the particular instruction(s). These are not
mandates, but rather guidelines to follow. The blue text
represents additions to that code and will correspond to
the instruction being given.
Introduction Our
company, 21-6 Productions, is building a game
called Myrmidon which will employ a rather sophisticated
third-person camera, similar to that found in Asheron's
Call. Using this camera, the gamer can simultaneously
control the movement of the camera and the player it is
pointing at. The camera is centered on the player model
and can be rotated both vertically and horizontally in a
spherical path (aka "orbit"). Of course, the gamer can
also zoom the camera in or out.
Background Research
There is already an "orbit camera" in the V12
engine. It's not hooked up by default, but with a little
scripting, I was able to get it working. It seemed to
work fine except for one important fact - the existing
"orbit camera" is actually a separate physical entity in
the 3D environment. The way that the V12 Engine is coded
- only one physical entity in the world can be
controlled by the keyboard at any time. In other words,
two physical entities in the world (the player and the
camera, in this case) couldn't be controlled from the
same input device at the same time without a great deal
of enhancements to the engine.
So, I went down
another track. The third-person camera support already
built into the V12 Engine (different than the "orbit"
camera mentioned above), positions the camera directly
behind the "eye" of the player model. The position of
the camera behind the player is static and cannot be
changed (until now). It's also worthy to note that the
implementation of this third-person camera does not
require another physical entity in the 3D environment.
It is simply the "view" from the player model in
question. This allows us to have the keyboard control
both the player and camera movements simultaneously.
What's Next There
are many features of the third-person camera that
Myrmidon will require. These include features like:
floating (lag) camera, mouse-look, mouse-orbit, and
auto-orbit (keeping another object in view at all
times). I named this tutorial "Part 1" because I plan to
implement these other features and write follow-up
tutorials as appropriate.
Step 1 - Default to the
"third-person" camera
Myrmidon is
primarily a third-person game and therefore we want the
engine to start up in third-person camera mode. This is
an optional step.
- Open "/engine/game/GameConnection.cc"
in the "V12 Engine"
project.
- Change the initialization of the "GameConnection::mFirstPerson"
variable to "false".
IMPLEMENT_CONOBJECT(GameConnection); bool
GameConnection::mFirstPerson = false; S32
GameConnection::mLagThresholdMS = 0; S32
GameConnection::smVoiceConnections[MaxClients];
|
Step 2 - Add variables to
store position, orientation, and velocity of the
camera
For this step, I have decided to
follow the approach already implemented for keeping
track of camera state in the engine. We will use the
"GameConnection::mFirstPerson" variable as our model.
Therefore, we need to add six static variables to the
GameConnection class.
- Open "/engine/game/GameConnection.h"
in the "V12 Engine"
project.
- Declare the following six static member variables
in the GameConnection class: mCameraPitch, mCameraYaw,
mCameraZoom, mCameraPitchSpeed, mCameraYawSpeed, and
mCameraZoomSpeed.
private: S32
mDataBlockModifiedKey; S32
mMaxDataBlockModifiedKey;
//
Client side first/third
person static bool mFirstPerson;
// Currently first person or not static F32
mCameraPitch; static F32
mCameraYaw; static F32
mCameraZoom; static F32
mCameraPitchSpeed; static F32
mCameraYawSpeed; static F32
mCameraZoomSpeed;
bool
mUpdateCameraFov; // set to notify server of
camera FOV change F32 mCameraFov;
// current camera fov (in
degrees) F32 mCameraPos; //
Current camera pos (0-1) F32
mCameraSpeed; // Camera in/out
speed
|
- Open "/engine/game/GameConnection.cc"
in the "V12 Engine"
project.
- Initialize the six static member variables. These
particular settings will position the camera directly
behind the eye of the player model, far enough back
that you can see the entire model. Adjust these values
for your game as necessary. Note that the camera is
not initially moving, hence the "speed" variables are
all set to zero.
IMPLEMENT_CONOBJECT(GameConnection); bool
GameConnection::mFirstPerson = true; F32 GameConnection::mCameraPitch =
-0.2; F32 GameConnection::mCameraYaw =
M_PI; F32 GameConnection::mCameraZoom =
4; F32 GameConnection::mCameraPitchSpeed =
0; F32 GameConnection::mCameraYawSpeed =
0; F32 GameConnection::mCameraZoomSpeed =
0; S32 GameConnection::mLagThresholdMS
= 0; S32
GameConnection::smVoiceConnections[MaxClients];
|
- Now, we need to persist these new variables so
search for the "writeDemoStartBlock()" method
and add some code to write the variables to the
supplied stream.
stream->writeFlag(false); stream->write(mFirstPerson); stream->write(mCameraPitch); stream->write(mCameraYaw); stream->write(mCameraZoom); stream->write(mCameraPitchSpeed); stream->write(mCameraYawSpeed); stream->write(mCameraZoomSpeed); stream->write(mCameraPos); stream->write(mCameraSpeed);
|
- Of course, we now need to put code in that reads
these variables from a supplied stream so search for
the "readDemoStartBlock()"
method and add the appropriate code.
while(stream->readFlag()) { SimDataBlockEvent
evt; evt.unpack(this,
stream); evt.process(this); } stream->read(&mFirstPerson); stream->read(&mCameraPitch); stream->read(&mCameraYaw); stream->read(&mCameraZoom); stream->read(&mCameraPitchSpeed); stream->read(&mCameraYawSpeed); stream->read(&mCameraZoomSpeed); stream->read(&mCameraPos); stream->read(&mCameraSpeed);
|
Step 3 - Expose new variables
to the console
The six static variables
we created in step 2 need to be exposed to the scripts
so they can be modified with the keyboard. Again, we are
going to follow the implementation of mFirstPerson in
our solution.
- Open "/engine/game/GameConnection.cc"
in the "V12 Engine"
project.
- Search for the "GameConnection::consoleInit"
method and add calls to "Con::addVariable" for each of
the six static camera variables.
void
GameConnection::consoleInit() { Con::addVariable("firstPerson",
TypeBool, &mFirstPerson); Con::addVariable("cameraPitch",
TypeF32,
&mCameraPitch); Con::addVariable("cameraYaw",
TypeF32,
&mCameraYaw); Con::addVariable("cameraZoom",
TypeF32,
&mCameraZoom); Con::addVariable("cameraPitchSpeed",
TypeF32,
&mCameraPitchSpeed); Con::addVariable("cameraYawSpeed",
TypeF32,
&mCameraYawSpeed); Con::addVariable("cameraZoomSpeed",
TypeF32,
&mCameraZoomSpeed); Con::addVariable("pref::Net::lagThreshold",
TypeS32,
&mLagThresholdMS);
|
Step 4 - Animate the position
and orientation of the camera
Every time
the engine is about to render the 3D environment, it
calls the GameConnection::getControlCameraTransform()
method to get the "camera transformation matrix" for the
"control object". The "control object" is the object in
the 3D environment which the player currently is
controlling with their keyboard/mouse - this is usually
the character model. A "camera transformation matrix"
contains the position and orientation information for
the camera, in real world coordinates.
In order
to keep the camera movement smooth, we need to consider
time as we animate the movement of the camera through
space. The "getControlCameraTransform()" method is
called by the engine as fast as possible and can
therefore be affected by the frame rate on the client
machine. This means we have to implement our own time
tracking mechanism for smooth animation of the camera
movements. To do this, we need to add an instance
variable to the GameConnection class that stores the
last clock tick in which the
"getControlCameraTransform()" method was called.
- Open "/engine/game/GameConnection.h"
in the "V12 Engine"
project.
- Declare the mLastCameraUpdate variable in the
GameConnection class:
private: S32
mDataBlockModifiedKey; S32
mMaxDataBlockModifiedKey; F32
mLastCameraUpdate;
//
Client side first/third
person static bool mFirstPerson;
// Currently first person or
not
|
- Open "/engine/game/GameConnection.cc"
in the "V12 Engine"
project.
- Initialize mLastCameraUpdate in the GameConnection
class constuctor.
GameConnection::GameConnection(bool
ghostFrom, bool ghostTo, bool
sendEvents) :
NetConnection(ghostFrom, ghostTo,
sendEvents) { mLastCameraUpdate =
-1; mControlObject =
NULL; mLastMoveAck =
0;
| Now,
lets add in the code which animates the camera position
and orientation. Note that we will be replacing the
existing code to animate the transition between first
and third person camera modes. In a future release, this
will be added back in using a different approach.
- Open "/engine/game/GameConnection.cc"
in the "V12 Engine"
project.
- Search for the "GameConnection::getControlCameraTransform()"
method and replace this code:
if (dt) { if (mFirstPerson
|| obj->onlyFirstPerson())
{ if (mCameraPos >
0) if
((mCameraPos -= mCameraSpeed * dt) <=
0) mCameraPos
= 0; } else
{ if (mCameraPos <
1) if
((mCameraPos += mCameraSpeed * dt) >
1) mCameraPos
=
1; } }
| with
this code:
// animate the camera properties based on
the amount of time that // has passed since
the last call to this method F32 curTime =
(F32)Platform::getRealMilliseconds() /
1000.0f;
F32 elapsedTime = 0;
if(mLastCameraUpdate !=
-1) elapsedTime = curTime -
mLastCameraUpdate; mLastCameraUpdate =
curTime;
mCameraZoom += mCameraZoomSpeed
* elapsedTime; if(mCameraZoom < 1)
mCameraZoom = 1;
mCameraYaw +=
mCameraYawSpeed *
elapsedTime;
mCameraPitch +=
mCameraPitchSpeed * elapsedTime; F32 temp =
M_PI/2 - 0.2; if(mCameraPitch > temp)
mCameraPitch = temp; if(mCameraPitch <
-temp) mCameraPitch =
-temp;
| In
replacing the code mentioned above, we have essentially
removed the need for the mCameraPos variable. It isn't
used much throughout the GameConnection class and could
be easily removed, however it is not essential for the
completion of this tutorial. There is one case, however,
where we need to make a code change in relation to
mCameraPos.
- Open "/engine/game/GameConnection.h"
in the "V12 Engine"
project.
- Search for the "bool
isFirstPerson()" method and change it to return
the value of the mFirstPerson variable.
| bool isFirstPerson() {return mFirstPerson;}
|
Step 5 - Calculate the camera
transformation matrix
The ShapeBase
class is used to represent any 3D object that the player
can control. This is where we will implement the new
code to calculate the transformation matrix used to
position and orient the camera in relation to the player
model. The method on the ShapeBase class that we are
interested in is called "getCameraTransform()".
The way this method works in the base SDK is
that you pass in a "position" variable (mCameraPos was
passed in from the GameConnection class) which defined
what percentage of the maximum camera distance the
camera was from the player model. Since we have extended
the functionality of the camera system, we have a few
more variables to pass into this method. Therefore, we
need to do change the signature of this method to take a
few new parameters and also change any other code that
depends on this method.
- Open "/engine/game/ShapeBase.h" in the
"V12 Engine" project.
- Search for the "void
getCameraTransform" method and modify the
parameter list to look like this:
| virtual void getCameraTransform(bool firstPerson, F32 cameraPitch,
F32 cameraYaw, F32 cameraZoom, MatrixF*
mat); | Because
this is a virtual method, we need to update any classes
that implemented an override method. The only class in
the engine that does this is the Camera class, which we
don't really need anymore but in order to get the
project to compile, we need to make the following
updates.
- Open "/engine/game/Camera.h" in the
"V12 Engine" project.
- Search for the "void
getCameraTransform" method and modify the
parameter list to look like this:
| void getCameraTransform(bool firstPerson, F32 cameraPitch,
F32 cameraYaw, F32 cameraZoom, MatrixF*
mat); |
- Open "/engine/game/Camera.cc" in the
"V12 Engine" project.
- Search for the "Camera::getCameraTransform"
method and modify the parameter list to look like
this:
| void Camera::getCameraTransform(bool firstPerson, F32 cameraPitch,
F32 cameraYaw, F32 cameraZoom, MatrixF*
mat) |
- Search for "obj->getCameraTransform"
method and modify the parameter list to look like
this:
| obj->getCameraTransform(firstPerson, cameraPitch, cameraYaw,
cameraZoom, mat);
| Now that
we have updated the signature of the
getCameraTransform() method, we need to pass in the
appropriate parameters (we recently created) in the
GameConnection class.
- Open "/engine/game/GameConnection.cc"
in the "V12 Engine"
project.
- Search for the "bool
getControlCameraTransform" method and update
the two calls to getCameraTransform() to use the new
static member variables we created in the beginning of
this tutorial.
if (!sChaseQueueSize || mFirstPerson ||
obj->onlyFirstPerson()) obj->getCameraTransform(mFirstPerson, mCameraPitch,
mCameraYaw, mCameraZoom, mat); else
{ MatrixF& hm =
sChaseQueue[sChaseQueueHead]; MatrixF&
tm =
sChaseQueue[sChaseQueueTail]; obj->getCameraTransform(mFirstPerson, mCameraPitch,
mCameraYaw,
mCameraZoom,&hm); *mat
= tm; if (dt)
{ if
((sChaseQueueHead += 1) >=
sChaseQueueSize) sChaseQueueHead
= 0; if
(sChaseQueueHead ==
sChaseQueueTail) if
((sChaseQueueTail += 1) >=
sChaseQueueSize) sChaseQueueTail
=
0; } }
| Finally,
we can add the actual code that calculates the camera
transformation matrix based on these new parameters.
This is done back in the ShapeBase class in the
getCameraTransform() method.
- Open "/engine/game/ShapeBase.cc" in
the "V12 Engine" project.
- Search for the "ShapeBase::getCameraTransform"
method and replace the entire method with the
following code:
void ShapeBase::getCameraTransform(bool
firstPerson, F32 cameraPitch, F32 cameraYaw, F32
cameraZoom, MatrixF* mat) { //
Returns camera to world space
transform // Handles first person
/ third person camera
position
if (isServerObject()
&&
mShapeInstance) mShapeInstance->animateNodeSubtrees(true);
//
if first-person view, use eye vector of
model if
(firstPerson) { getRenderEyeTransform(mat); }
//
else if third person view, figure out where
camera
is else { //
determine the transform matrix for the
eye MatrixF
eye; getRenderEyeTransform(&eye);
//
Use the eye transform to orient the
camera VectorF vp(0,
cameraZoom,
0);
// rotate
around x and z
axis MatrixF xRot,
zRot; xRot.set(EulerF(cameraPitch,
0,
0)); zRot.set(EulerF(0,
0,
cameraYaw)); MatrixF
temp; temp.mul(zRot,
xRot); Point3F
rotPoint; temp.mulV(vp,
&rotPoint); vp =
rotPoint;
//
determine the vector where the camera should go,
in local
coordinates VectorF
cameraVec; eye.mulV(vp,
&cameraVec);
//
center the camera on the eye of the model, in
world
coordinates Point3F
lookAtPos =
eye.getPosition(); Point3F
osp; if(mDataBlock->cameraNode
!= -1)
{ mShapeInstance->mNodeTransforms[mDataBlock->cameraNode].getColumn(3,
&osp); getRenderTransform().mulP(osp,
&lookAtPos); } else getRenderTransform().getColumn(3,
&lookAtPos);
//
Make sure we don't extend the camera into
anything
solid Point3F
cameraPos = lookAtPos +
cameraVec;
//
disable collision system for this test (not sure
why at this
point) disableCollision(); if
(isMounted()) getObjectMount()->disableCollision();
//
cast a ray between the two points and clip if
necessary RayInfo
collision; if
(mContainer->castRay(lookAtPos,
cameraPos, (0xFFFFFFFF
& ~(WaterObjectType
| ForceFieldObjectType
| GameBaseObjectType
| DefaultObjectType)), &collision)
==
true) { F32
veclen =
cameraVec.len(); F32
adj = (-mDot(cameraVec, collision.normal) /
veclen) *
0.1; F32
newPos = getMax(0.0f, collision.t -
adj); if
(newPos ==
0.0f) eye.getColumn(3,
&cameraPos); else cameraPos
= lookAtPos + (cameraVec *
newPos); }
//
re-orient the transform matrix to point at the
lookAtPos from the
cameraPos vp =
lookAtPos -
cameraPos; vp.normalize(); *mat
=
MathUtils::createOrientFromDir(vp); mat->setColumn(3,
cameraPos);
//
enable the collision system
again if
(isMounted()) getObjectMount()->enableCollision(); enableCollision(); } }
|
Step 6 - Build the
engine
We have finished all the engine
changes for this tutorial. At this time, it would be a
good idea to rebuild the V12 Engine project and resolve
any compiler/linker errors. Note that I have had
problems with making header file changes and doing an
incremental compile of the project. Therefore, I suggest
doing a "Rebuild All" on the V12 Engine project.
You can continue with the rest of the tutorial
while the engine is building, if you want.
Step 7 - Add "key binds" to
client scripts
It's time to give the
player access to this new camera system. To do this, we
need to add code to the scripts that transform key
presses into changes in the six console variables we
defined in Step 5.
- Open "/example/client/scripts/default.bind.cs"
- Search for the "Helper Functions" section of the
file and add the following script functions to it.
These functions will act as our keyboard handlers.
// Zoom the camera in by setting the
$cameraZoomSpeed // to a positive
number function
zoomCameraIn(%val) { if($firstPerson)
return; if(%val) $cameraZoomSpeed
+=
$pref::Player::cameraZoomSpeed; else $cameraZoomSpeed
-=
$pref::Player::cameraZoomSpeed; }
//
Zoom the camera in by setting the
$cameraZoomSpeed // to a negative
number function
zoomCameraOut(%val) { if($firstPerson)
return; if(%val) $cameraZoomSpeed
-=
$pref::Player::cameraZoomSpeed; else $cameraZoomSpeed
+=
$pref::Player::cameraZoomSpeed; }
//
Rotate the camera horizontally around the player
// in a clockwise motion by setting the
$cameraYawSpeed // to a positive
number function
rotateCameraLeft(%val) { if($firstPerson)
return; if(%val) $cameraYawSpeed
-=
$pref::Player::cameraRotateSpeed; else $cameraYawSpeed
+=
$pref::Player::cameraRotateSpeed; }
//
Rotate the camera horizontally around the
player // in a counter-clockwise motion by
setting the $cameraYawSpeed // to a negative
number function
rotateCameraRight(%val) { if($firstPerson)
return; if(%val) $cameraYawSpeed
+=
$pref::Player::cameraRotateSpeed; else $cameraYawSpeed
-=
$pref::Player::cameraRotateSpeed; }
//
Rotate the camera vertically around the player
// in an upward motion by setting the
$cameraPitchSpeed // to a positive
number function
rotateCameraUp(%val) { if($firstPerson)
return; if(%val) $cameraPitchSpeed
-=
$pref::Player::cameraRotateSpeed; else $cameraPitchSpeed
+=
$pref::Player::cameraRotateSpeed; }
//
Rotate the camera vertically around the player
// in a downward motion by setting the
$cameraPitchSpeed // to a negative
number function
rotateCameraDown(%val) { if($firstPerson)
return; if(%val) $cameraPitchSpeed
+=
$pref::Player::cameraRotateSpeed; else $cameraPitchSpeed
-=
$pref::Player::cameraRotateSpeed; }
//
Reset the camera position to the "home"
position function
resetCamera(%val) { if($firstPerson)
return; $cameraPitch =
-0.2; $cameraYaw =
3.14159265358979323846; $cameraZoom
= 4; }
|
- Just under this code, add the actual keyboard
bindings that will control the camera. Of course, you
can set these to anything you want. These bindings are
just an example.
moveMap.bind(keyboard, "numpadadd",
zoomCameraOut); moveMap.bind(keyboard,
"numpadminus",
zoomCameraIn); moveMap.bind(keyboard,
"right",
rotateCameraLeft); moveMap.bind(keyboard,
"left",
rotateCameraRight); moveMap.bind(keyboard,
"up", rotateCameraUp); moveMap.bind(keyboard,
"down",
rotateCameraDown); moveMap.bind(keyboard,
"insert",
resetCamera);
|
Step 8 - Add client
preferences
You might have caught the
use of $pref::Player::cameraRotateSpeed and
$pref::Player::cameraZoomSpeed in the last set of script
functions. These are preference variables that the
player can use (possibly through a nice options dialog)
to control the speed at which the camera zooms and
rotates, respectively.
- Open "/example/base/scripts/clientDefaults.cs"
- Search for the "$pref::Player" variables and add
our two new camera preference variables to that
namespace. Again, you can set these defaults to values
that work for your particular game.
$pref::Player::Count =
0; $pref::Player::Current =
0; $pref::Player::defaultFov =
90; $pref::Player::zoomSpeed =
0; $pref::Player::Name = "Test Guy"; $pref::Player::cameraZoomSpeed =
8; $pref::Player::cameraRotateSpeed =
2;
|
Congratulations! You have completed the tutorial. You
should now be able to run your application and use the
NUMPAD keys to rotate the camera around the player
model.
This is the first tutorial I have written
and I am interested in getting some feedback on content
or presentation. Of course, if something in this
tutorial doesn't work, please don't hesitate to write me
either. Feedback can be sent to justin.mette@21-6.com.
| | |
 | |