Pathed Camera
by Tim Heldna · 02/28/2006 (6:58 pm) · 22 comments
I wanted to create a mission preview of my level before the player spawns in. A pathed camera immediately came to mind (like the ones in the torque demo). I had a look around but couldn't find much useful information on the subject.
There was one old resource which i dismissed as it seemed dated and required head code changes (I avoid any head code changes if it can be done in script), so after working out how to do this I thought I'd share...
1). Create a new *.cs script file called pathCamera.cs (or whatever you like) and place it in the server/scripts directory.
2). Execute this script from within server/scripts/game.cs in the usual manner i.e. exec("./pathCamera.cs");
3). Here's what to put in this new script...
5). Now all you have to do is create a path within your mission (mine's called MissionPreviewPath as you can see) and you're set. Inside pathCamera.cs if you look inside the function PathCamera::followPath(%this,%path) section you'll also notice you can change the speed of your camera.
6). So to give you an example; To have it so you spawn into a mission as a camera that loops around your mission on a path, replace your whole function GameConnection::onClientEnterGame(%this) function inside server/scripts/game.cs with this...
That's it. If you don't know how to create a path check out the stronghold mission and look at the path kork uses.
If you don't know how to get it to spawn as a player check out this resource on team selection. It covers a lot of useful ground.
Teams Implementation
Enjoy, let me know if you have any problems.
There was one old resource which i dismissed as it seemed dated and required head code changes (I avoid any head code changes if it can be done in script), so after working out how to do this I thought I'd share...
1). Create a new *.cs script file called pathCamera.cs (or whatever you like) and place it in the server/scripts directory.
2). Execute this script from within server/scripts/game.cs in the usual manner i.e. exec("./pathCamera.cs");
3). Here's what to put in this new script...
//-----------------------------------------------------------------------------
// Path Camera
//-----------------------------------------------------------------------------
datablock PathCameraData(LoopingCam)
{
mode = "";
};
function LoopingCam::onNode(%this,%camera,%node)
{
if (%node == %camera.loopNode) {
%camera.pushPath(%camera.path);
%camera.loopNode += %camera.path.getCount();
}
}
function PathCamera::followPath(%this,%path)
{
%this.path = %path;
if (!(%this.speed = %path.speed))
%this.speed = 10;
if (%path.isLooping)
%this.loopNode = %path.getCount() - 2;
else
%this.loopNode = -1;
%this.pushPath(%path);
%this.popFront();
}
function PathCamera::pushPath(%this,%path)
{
for (%i = 0; %i < %path.getCount(); %i++)
%this.pushNode(%path.getObject(%i));
}
function PathCamera::pushNode(%this,%node)
{
if (!(%speed = %node.speed))
%speed = %this.speed;
if ((%type = %node.type) $= "")
%type = "Normal";
if ((%smoothing = %node.smoothing) $= "")
%smoothing = "Linear";
%this.pushBack(%node.getTransform(),%speed,%type,%smoothing);
}4). There are several different ways of adding this camera into your game. This is the script you'll need... (don't worry an example will follow if you're new to torque)// Create path camera
%this.PathCamera = new PathCamera() {
dataBlock = LoopingCam;
position = "0 0 300 1 0 0 0";
};
%this.PathCamera.followPath("MissionGroup/Paths/MissionPreviewPath");
MissionCleanup.add( %this.PathCamera);
%this.PathCamera.scopeToClient(%this);5). Now all you have to do is create a path within your mission (mine's called MissionPreviewPath as you can see) and you're set. Inside pathCamera.cs if you look inside the function PathCamera::followPath(%this,%path) section you'll also notice you can change the speed of your camera.
6). So to give you an example; To have it so you spawn into a mission as a camera that loops around your mission on a path, replace your whole function GameConnection::onClientEnterGame(%this) function inside server/scripts/game.cs with this...
function GameConnection::onClientEnterGame(%this)
{
commandToClient(%this, 'SyncClock', $Sim::Time - $Game::StartTime);
// Create a new camera object.
%this.camera = new Camera() {
dataBlock = Observer;
};
MissionCleanup.add( %this.camera );
%this.camera.scopeToClient(%this);
// Create path camera
%this.PathCamera = new PathCamera() {
dataBlock = LoopingCam;
position = "0 0 300 1 0 0 0";
};
%this.PathCamera.followPath("MissionGroup/Paths/MissionPreviewPath");
MissionCleanup.add( %this.PathCamera);
%this.PathCamera.scopeToClient(%this);
$Server::Client = %this;
%this.setControlObject(%this.PathCamera);
}7). You'll obviously want to create another similar function that spawns you in as a player when you're ready.That's it. If you don't know how to create a path check out the stronghold mission and look at the path kork uses.
If you don't know how to get it to spawn as a player check out this resource on team selection. It covers a lot of useful ground.
Teams Implementation
Enjoy, let me know if you have any problems.
About the author
Recent Blogs
• BCS Street props• Character Pack - Vince
• Recent Artwork
• Progress of our Weapon Pack
• Digital Speedometer
#2
EDIT:
Upon closer inspection that line of code isn't a bug, It's there for a reason. Being that if you want to change the smothingType of all the nodes in your path you simply add a dynamic field called 'smoothing' to your path and whatever smoothing type you specify (spline or linear) within this new dynamic field (smoothing) will be applied to every node within that path. If you don't add a dynamic field called 'smoothing' it will then default to the linear smoothing type. This method saves you specifying the smoothingType for each individual node within your path.
Hope that makes sense, i've returned the resource to it's original state.
02/12/2006 (4:35 am)
Thanks for the bug fix Sebastian Potter, resource has been updated accordingly.EDIT:
Upon closer inspection that line of code isn't a bug, It's there for a reason. Being that if you want to change the smothingType of all the nodes in your path you simply add a dynamic field called 'smoothing' to your path and whatever smoothing type you specify (spline or linear) within this new dynamic field (smoothing) will be applied to every node within that path. If you don't add a dynamic field called 'smoothing' it will then default to the linear smoothing type. This method saves you specifying the smoothingType for each individual node within your path.
Hope that makes sense, i've returned the resource to it's original state.
#3
02/22/2006 (9:53 am)
Just what I needed. Thanks!
#4
Good job!
One note though... I think you might be a little confused about what is happening in the 'pushNode' function. The 'smoothing' value is being read from the node inside the path and not the actual path itself. To read a value from the path itself you'd probably want something like this:
Cheers!
Stephane
02/28/2006 (11:26 pm)
Hey Tim,Good job!
One note though... I think you might be a little confused about what is happening in the 'pushNode' function. The 'smoothing' value is being read from the node inside the path and not the actual path itself. To read a value from the path itself you'd probably want something like this:
function PathCamera::followPath(%this,%path)
{
%this.path = %path;
if (!(%this.speed = %path.speed))
%this.speed = 10;
if (%path.isLooping)
%this.loopNode = %path.getCount() - 2;
else
%this.loopNode = -1;
%this.pushPath(%path);
%this.popFront();
}
function PathCamera::pushPath(%this,%path)
{
for (%i = 0; %i < %path.getCount(); %i++)
%this.pushNode(%path.getObject(%i));
}
function PathCamera::pushNode(%this,%node)
{
if (!(%speed = %node.speed))
%speed = %this.speed;
if ((%type = %node.type) $= "")
%type = "Normal";
if ((%smoothing = %node.smoothing) $= "")
{
if ((%smoothing = %this.path.smoothing) $= "")
%smoothing = "Linear";
}
%this.pushBack(%node.getTransform(),%speed,%type,%smoothing);
}Cheers!
Stephane
#5
If what you want to do is provide an override then adding a dynamic field to your path called smoothing you have to be aware that nodes are assigned the smoothingType value of "Linear" upon creation, and that by overriding you lose the per-node feature of TGE's pathing code. If you really want to default all nodes to ignore the per-node value then adding a flag to enable this behaviour would be one approach, another would be a simple text search/replace on your mssion file.
Still a great resource.
03/01/2006 (9:56 pm)
Indeed, the standard path functions allow you to set the field smoothingType (not smoothing) on a per-node basis in your path. This allows you to use either linear or bezier curve functions for node transitions.If what you want to do is provide an override then adding a dynamic field to your path called smoothing you have to be aware that nodes are assigned the smoothingType value of "Linear" upon creation, and that by overriding you lose the per-node feature of TGE's pathing code. If you really want to default all nodes to ignore the per-node value then adding a flag to enable this behaviour would be one approach, another would be a simple text search/replace on your mssion file.
Still a great resource.
#6
- Jason
05/27/2006 (11:55 am)
Okay, about the pathed camera... what if I don't want to start a camera on the path at the beginning of the level? Let's say I wanted to script a camera to run on a path to add a dramatic element when the player reaches a certain point in the level? I need to know how to access GameConnection object from else where in the code perhaps... any help would be welcome!- Jason
#7
Regards,
05/27/2006 (11:27 pm)
Brilliant resource, I have been strugling with this for months, thank you very much!Regards,
#8
06/28/2006 (6:02 pm)
I was wondering if anyone knew how to utilize the msToNext variable. No matter what I set the variable to nothing seems to happen. BTW
#9
Here is the function call
Here is the part where the server recieves a command from the client.
Now I will totally elimate 2d Gui's from my game! Thanks much!
This allows you to change paths, and follow along nodes in no particular order.
edit : typo
edit 2 : additions
07/17/2006 (12:26 pm)
Currently I am writing more functions for this resource, here is one I have now. Next I plan on tossing in a commandToServer so the client can send transition requests through a GUI to move the camera somewhere else.Here is the function call
%this.PathCamera.moveToNode("path name",#node-1);And here is the functionfunction PathCamera::moveToNode(%this,%path,%index)
{
%this.path=%path;
%this.currentNode = %index;
%node = %this.path.getObject(%index);
%this.pushBack(%node.getTransform(),10,"Normal","Spline"); // (node,speed,type,shape)
}Here is the part where the server recieves a command from the client.
function serverCmdmoveToNode(%this,%path,%index)
{
%this.path=%path;
%this.currentNode = %index;
%node = %this.path.getObject(%index);
%this.PathCamera.pushBack(%node.getTransform(),20,"Normal","Spline");
}Here is what the client command looks like.commandToServer('moveToNode',"path name",#node-1);Now I will totally elimate 2d Gui's from my game! Thanks much!
This allows you to change paths, and follow along nodes in no particular order.
edit : typo
edit 2 : additions
#10
Here is what I have so far, but it doesn't fully function.
07/17/2006 (3:09 pm)
If someone can make a command to server to follow a path again, please post it for me =-)Here is what I have so far, but it doesn't fully function.
function serverCmdfollowPath(%this,%path)
{
%this.PathCamera.path = %path;
if (%path.isLooping)
%this.PathCamera.loopNode = %path.getCount() - 2;
else
%this.PathCamera.loopNode = -1;
for (%i = 0; %i < %path.getCount(); %i++)
{
%node = %path.getObject(%i);
%this.PathCamera.pushBack(%node.getTransform(),20,"Normal","Spline");
}
%this.PathCamera.popFront();
}
#11
10/13/2006 (2:38 pm)
I am trying to have it so the camera continuously loops through its path while you control the player. Is this resource what I am looking for?
#12
Ever when the cam starts the TrackCamDialog is closed.
Could someone help me?
12/29/2006 (7:04 am)
I have this:function GameConnection::onClientEnterGame(%this)
{
if (%this.isAIControlled()) {
%this.player = CreateAIWheeledVehicle2("Herbie","CarPath",pickSpawnPoint());
} else {
commandToClient(%this, 'SyncClock', $Sim::Time - $Game::StartTime);
commandToClient(%this, 'SetMaxLaps', $Game::Laps);
// Create a new camera object.
%this.camera = new Camera() {
dataBlock = Observer;
};
MissionCleanup.add( %this.camera );
%this.camera.scopeToClient(%this);
// Client controls the camera by default.
%this.setControlObject(%this.camera);
if(!$Game::Running)
{
// Create a car object.
%this.spawnCar();
// Orbit the camera around the car
%this.camera.setOrbitMode(%this.car, %this.car.getTransform(), 0.5, 4.5, 4.5);
}
}
%this.PathCamera = new PathCamera() {
dataBlock = LoopingCam;
position = "0 0 300 1 0 0 0";
};
%this.PathCamera.followPath("MissionGroup/CarPath");
MissionCleanup.add( %this.PathCamera);
%this.PathCamera.scopeToClient(%this);
$Server::Client = %this;
%this.setControlObject(%this.PathCamera);
canvas.pushDialog(TrackCam);
}Only the last line won't work fine.Ever when the cam starts the TrackCamDialog is closed.
Could someone help me?
#13
Dang you garage games why could you not include a short run through of path cameras in your OFFICAL guide
02/12/2007 (9:47 am)
I have a question what is it that acctualy makes the camera move along the line I have looked in the TDN section and read through your code several times and cannot seem to figure it out. Maybe I am misunderstanding something here.Dang you garage games why could you not include a short run through of path cameras in your OFFICAL guide
#14
I altered PathCamera::advancePosition like so:
void PathCamera::advancePosition(S32 ms)
{
delta.timeVec = mPosition;
// Advance according to current speed
if (mState == Forward) {
...no change
}
else
if (mState == Backward) {
...no change
}
else
if (mState == Stop) {
// Con::printf("Pitch = %f Yaw = %f\n", mLookPitch, mLookYaw);
// I verified these values were being upadated
mRot.x += mLookPitch; // these are exposed to script and are actionmapped, etc.
mRot.z += mLookYaw;
MatrixF objTx = getTransform();
VectorF vPos;
objTx.getColumn(3, &vPos); // Get the current camera position
setPosition(vPos, mRot); // Use my overridden setPosition function.
}
// Script callbacks
...no change
}
Then, I overloaded the setPosition function as follows which ignores the mPosition variable (which is a percentage of progress on the path, not an actual position):
void PathCamera::setPosition(Point3F pos, Point3F &rot)
{
// This is pretty much directly from camera.cc
// I've debeugged and verified that all this gets called.
MatrixF xRot, zRot;
xRot.set(EulerF(rot.x, 0, 0));
zRot.set(EulerF(0, 0, rot.z));
MatrixF temp;
temp.mul(zRot, xRot);
temp.setColumn(3, pos);
Parent::setTransform(temp);
setMaskBits(PositionMask);
// is the mask the problem since this isn't what pathCamera defines as a postion?
}
The end result, however, is when I setState to stop, the camera does not rotate at all. I've verified that I'm getting pitch and yaw values by printing them out to console. I thought that setTransform or setMaskBits would mark everything as dirty and update the pathCamera object, but I'm willing to bet I'm missing something or doing something wrong. Anybody see where I went wrong or have experience setting up something like this?
03/07/2007 (9:28 am)
Great resource!! I was able to get a PathCamera setup and flying through my level thanks to this great post. Since then, I've been trying to integrate a mouse-look mode so that when the path camera is stopped, I can look around with mouse. I was looking at the mouse code in camera.cc and tried to port it over, but have not met with success. I altered PathCamera::advancePosition like so:
void PathCamera::advancePosition(S32 ms)
{
delta.timeVec = mPosition;
// Advance according to current speed
if (mState == Forward) {
...no change
}
else
if (mState == Backward) {
...no change
}
else
if (mState == Stop) {
// Con::printf("Pitch = %f Yaw = %f\n", mLookPitch, mLookYaw);
// I verified these values were being upadated
mRot.x += mLookPitch; // these are exposed to script and are actionmapped, etc.
mRot.z += mLookYaw;
MatrixF objTx = getTransform();
VectorF vPos;
objTx.getColumn(3, &vPos); // Get the current camera position
setPosition(vPos, mRot); // Use my overridden setPosition function.
}
// Script callbacks
...no change
}
Then, I overloaded the setPosition function as follows which ignores the mPosition variable (which is a percentage of progress on the path, not an actual position):
void PathCamera::setPosition(Point3F pos, Point3F &rot)
{
// This is pretty much directly from camera.cc
// I've debeugged and verified that all this gets called.
MatrixF xRot, zRot;
xRot.set(EulerF(rot.x, 0, 0));
zRot.set(EulerF(0, 0, rot.z));
MatrixF temp;
temp.mul(zRot, xRot);
temp.setColumn(3, pos);
Parent::setTransform(temp);
setMaskBits(PositionMask);
// is the mask the problem since this isn't what pathCamera defines as a postion?
}
The end result, however, is when I setState to stop, the camera does not rotate at all. I've verified that I'm getting pitch and yaw values by printing them out to console. I thought that setTransform or setMaskBits would mark everything as dirty and update the pathCamera object, but I'm willing to bet I'm missing something or doing something wrong. Anybody see where I went wrong or have experience setting up something like this?
#15
08/12/2007 (1:33 am)
Is there a way to get a camera that follows the path one time and then calls the startGame function?
#16
02/23/2008 (8:57 pm)
works great!
#17
02/23/2008 (8:57 pm)
works great!
#18
03/24/2008 (11:52 am)
Has anyone tired to make the camera be restricted to the path as it follows the player movement? like a 2d game.
#19
PathCameras support floats for the speed and in the statement:
if (!(%speed = %node.speed))
%speed = %this.speed;
%speed gets truncated so if %node.speed = 1.9, %speed = 1 or if %node.speed = 0.5 it doesn't move here is the simple fix:
function PathCamera::pushNode(%this,%node)
{
%speed = %node.speed;
if (%speed $= "")
%speed = %this.speed;
if ((%type = %node.type) $= "")
%type = "Normal";
if ((%smoothing = %node.smoothingType) $= "")
%smoothing = "Linear";
%this.pushBack(%node.getTransform(),%speed,%type,%smoothing);
}
Not sure why torque casts it to an int, but it is nice to know now.
04/08/2008 (1:35 pm)
Found a big bug in torque script so you may want to change the pushNode function.PathCameras support floats for the speed and in the statement:
if (!(%speed = %node.speed))
%speed = %this.speed;
%speed gets truncated so if %node.speed = 1.9, %speed = 1 or if %node.speed = 0.5 it doesn't move here is the simple fix:
function PathCamera::pushNode(%this,%node)
{
%speed = %node.speed;
if (%speed $= "")
%speed = %this.speed;
if ((%type = %node.type) $= "")
%type = "Normal";
if ((%smoothing = %node.smoothingType) $= "")
%smoothing = "Linear";
%this.pushBack(%node.getTransform(),%speed,%type,%smoothing);
}
Not sure why torque casts it to an int, but it is nice to know now.
#20
This isn't a bug--you are trying to accomplish something that isn't really correct:
The if statement above does not accomplish what you would think--you do not get back the value of %type from within the parentheses, you get the return value of the = operation when the statement is executed.
Since an assignment always succeeds (barring catastrophic problems), the return result is always "1", which is never $= 0.
The same thing applies in your code:
The expression !(%speed = %node.speed) always evaluates to false (and in fact you should have a console warning that states this in your console.log), and therefore the body of your if statement should never execute.
04/08/2008 (1:58 pm)
Ah, you're more descriptive in this example, which helps point out a lot.This isn't a bug--you are trying to accomplish something that isn't really correct:
if ((%type = %node.type) $= "") %type = "Normal";
The if statement above does not accomplish what you would think--you do not get back the value of %type from within the parentheses, you get the return value of the = operation when the statement is executed.
Since an assignment always succeeds (barring catastrophic problems), the return result is always "1", which is never $= 0.
The same thing applies in your code:
if (!(%speed = %node.speed))
The expression !(%speed = %node.speed) always evaluates to false (and in fact you should have a console warning that states this in your console.log), and therefore the body of your if statement should never execute.

Torque Owner Sebastian Potter
Just one small bug:
function PathCamera::pushNode(%this,%node) { if (!(%speed = %node.speed)) %speed = %this.speed; if ((%type = %node.type) $= "") %type = "Normal"; if ((%smoothing = %node.smoothing) $= "") %smoothing = "Linear"; %this.pushBack(%node.getTransform(),%speed,%type,%smoothing); }This won't pick up the smoothing type for nodes from the mission file, as the field is called smoothingType.
Replace with:
function PathCamera::pushNode(%this,%node) { if (!(%speed = %node.speed)) %speed = %this.speed; if ((%type = %node.type) $= "") %type = "Normal"; if ((%smoothing = %node.smoothingType) $= "") // changed to reflect correct data field %smoothing = "Linear"; %this.pushBack(%node.getTransform(),%speed,%type,%smoothing); }and you'll get proper spline movement between nodes.
One other bug that I hit with TGE's Path Manager code is that the number of markers in a path has a hard-coded limit of 20. Rather than throwing a warning, TGE will silently and omit markers from the start of your path if the number of nodes is 20 or more.
To change this you can edit pathCamera.cc and look for:
enum Constants { NodeWindow = 20 // Maximum number of active nodes };Apparently there's a practical limit to path length dependent upon packet transmission size. TGE is supposed to chunk node transmission but this is not working. Keeping the path sizes limited and working with multiple paths might be a better approach for much longer path lengths.