|
10.Apr.07 Lecture 10
Torque Script, Datablocks, Agile Programming
Motivate
Make sure you read this entire page. http://www.motivate.com.au/self-motivation.htm
The keys to achieving are ACTION and POSITIVITY.
Start small, big things will follow.
We have now entered the orange zone. 35
days left. Your screenshots and 'marketing materials' are
due on three weeks! You should really start to feel that Discount
Expectancy Theory kicking in.
Names and IDs
I found this piece of code in starter.fps/server/scripts/game.cs
and wanted to place it here for my own notes and lookup. This is very
useful and shows how to get a list of items you've placed into a scene
and access them randomly or inorder.
function pickSpawnPoint()
{
%groupName = "MissionGroup/PlayerDropPoints";
%group = nameToID(%groupName);
if (%group != -1) {
%count = %group.getCount();
if (%count != 0) {
%index = getRandom(%count-1);
%spawn = %group.getObject(%index);
return %spawn.getTransform();
}
else
error("No spawn points found in " @ %groupName);
}
else
error("Missing spawn points group " @ %groupName);
// Could be no spawn points, in which case we'll stick the
// player at the center of the world.
return "0 0 300 1 0 0 0";
}
Notice you can get a list of objects you have placed
in a folder by using the folder name:
%grouplist = nameToID("MissionGroup/PlayerDropPoints");
Then you can access individual elements in that list
using.
%item = %grouplist.getObject(%index);
Creating your own objects
There are many different reasons for creating your
own objects in script. A good sample of this is the AIManger in starter.fps.
This is simply how they declare the code for the
object in Torque script. It is a simple two methods. The first is think
which is called once, sets up a timer and is called again every 500ms.
The second is spawn which is called on startup.
function AIManager::think(%this)
{
// We could hook into the player's onDestroyed state instead of
// having to "think", but thinking allows us to consider other
// things...
if (!isObject(%this.player))
%this.player = %this.spawn();
%this.schedule(500,think);
}
function AIManager::spawn(%this)
{
%player = AIPlayer::spawnOnPath("Kork","MissionGroup/Paths/Path1");
if (isObject(%player))
{
%player.followPath("MissionGroup/Paths/Path1",-1);
%player.mountImage(CrossbowImage,0);
%player.setInventory(CrossbowAmmo,1000);
return %player;
}
else
return 0;
}
Then this is how they create a new instance of the
object in server/scripts/game.cs
// Start the AIManager
new ScriptObject(AIManager) {};
MissionCleanup.add(AIManager);
AIManager.think();
The last thing to consider is when the mission ends,
the AIManger object you created needs to be destroyed.
// Stop the AIManager
AIManager.delete();
These objects have nothing to do with items that
are visible in the scene. Using the generic torque engine, the only
way to get items in to the scene is through the existing classes StaticShape,
Item and TSStatic. At least that is what I have found.
Datablocks
Datablocks still sometimes confuse me. When I get
a little confused, this is the
best datablock description page I have found.
General reading
I went back an re-read this Torque
Documentation page. I don't think it's perfect, but each time
I read it I come away with something.
Engine Code Documentation
The engine part of the code has a way to extract
documentation from the source. This is much like JavaDoc which we discussed
last year. The tool they use is Doxygen.
To use this tool, simply direct it to scan the engine directory and
give it an output directory. This will place html formatted documentation
in the directory you specify. Simply open the index.html file once
doxygen is done.
This process is documented further on the How
to Regenerate the Torque Engine Reference page on the Garage
Games website.
ShapeData, StaticShapeData, TSStatic and the difference
between Datablocks and object instances
I was working on the popup target and did some more
digging on these unclear types. Mostly I was trying to get my own class
to be created when I dragged in a popup from the mission editor. The
datablock I was using was StaticShapeData and the class being created
was StaticShape.
In editor.cs is the StaticShapeData::create which
does the following:
function StaticShapeData::create(%data)
{
%obj = new StaticShape() {
dataBlock = %data;
};
return %obj;
}
Just as an experiement I change my type to ShapeBase
and I was able to add the object, but the dts file was not shown, only
the outline. Remember (See p#158 and 183 in the yellow book), ShapeBase
is a virtual class and cannot be used to crate objects in the world.
Instead, use the children classes: ItemData, StaticShapeData and TSStatic.
ItemData is for items the player interacts with.
StaticShapeData is for object that need to interact
with moving objects. This is a bad name and it should probably be called
DynamicShapeData...
TSStatic is for objects that are completely stationary
and do not require interaction. Remember, these are created automatically
when dragging from StaticShape in the mission editor.
I puzzled about this for many hours. The shotgun
(or otherweapons) is calling the damage method on the datablock. My
problem with this is that datablocks are singular instances and shared
across all the instances of a specific type of object. In my case PopupData
was the datablock which was shared across all the popup doors I would
add to my level.
This is what shotgun does.
function shotgunProjectile::onCollision(%this,%obj,%col,%fade,%pos,%normal)
{
// Apply damage to the object all shape base objects
if (%col.getType() & $TypeMasks::ShapeBaseObjectType)
{
%col.damage(%obj,%pos,%this.directDamage,"shotgunBullet");
}
}
It calls damage on %col which is a datablock. Since
I'm using the tutorial.base the damage method was never found. This
was the error message in the log.
GameOne/data/shapes/shotgun.cs (164): Unknown
command Damage.
Object (1492) StaticShape -> ShapeBase -> GameBase -> SceneObject
-> NetObject -> SimObject
As you can see the StaticShape does not have the
damage method even though it was part of the datablock. This is where
I was getting confused. I kept trying to create my own class that had
a damage method that would be the parent for the popup datablock.
That just lead me back to the fact that the only
thing that was going to work was a StaticShape and StaticShapeData
for the root class of the popup door. Again, they never should have
called it StaticShape, it should be called DynamicShape if anything,
but that is something we just have to recognize and live with.
I finally went back to the starter.fps and searched
for "damage(". Sure enough I found the following in the file
starter.fps/server/scripts/shapebase.cs
function ShapeBase::damage(%this, %sourceObject, %position, %damage, %damageType)
{
// All damage applied by one object to another should go through this
// method. This function is provided to allow objects some chance of
// overriding or processing damage values and types. As opposed to
// having weapons call ShapeBase::applyDamage directly.
// Damage is redirected to the datablock, this is standard proceedure
// for many built in callbacks.
%this.getDataBlock().damage(%this, %sourceObject, %position, %damage, %damageType);
}
I never had to create my own object, they just modify
the root level object to have the new behavior. This took me so long
because I was being stubborn and not wanting to modify the root level
behavior, but to extend that behavior. Torque just expects you to take
that root behaviour and change it at your will. This is fine, it just
goes against some modern programming practices and you have to retrain
your brain.
Notice that it IS calling the datablock's
damage method which is what I wanted to avoid! But also notice that
it is passing the ShapeBase object instance (%this) in as the first
parameter.
The one class that has a damage method is player.cs
function Armor::damage(%this, %obj, %sourceObject, %position, %damage, %damageType)
{
if (%obj.getState() $= "Dead")
return;
%obj.applyDamage(%damage);
%location = "Body";
Notice that it ends up calling applyDamage on the
%obj, which is ShapeBase, not the datablock. ShapeBase has damage handling
code in it already and we could have just called this from the start
(in the shotgun.cs), but this modified damage system they are using
allows them to only apply damage on certain conditions. In this case
it is only applied with the player is not already dead.
As another example. This is part of my popup door
definition.
datablock StaticShapeData(PopupData)
{
// Mission editor category, this datablock will show up in the
// specified category under the "shapes" root category.
category = "PopTargets";
// Basic Item properties
shapeFile = "./popup.dts";
mass = 10;
friction = 1;
elasticity = 0.3;
RAD2DEG = 57.2957795;
};
function PopupData::onCollision(%this, %obj, %col)
{
/*
* MANTRA - REPEAT OVER AND OVER UNTIL DATABLOCK UNDERSTANDING SETS IN
* 1. %this is the datablock
* 2. %obj is the object instance
*
* of much less importance
* 3. %col is the object colliding with this instance
*/
echo("PopupData collision!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
if (%obj.ready == 1)
{
%this.StartPop(%obj);
}
}
Notice the MANTRA I
placed in the comment at the beginning of the datablock's onCollision
method. The datablock is what get's called. %this is passed to every
object instance and the datablock onCollision callback is no different.
The second parameter is the actual object instance. You could call
%obj.getDatablock() and you would have the same value as %this. Remember,
the datablock is shared across all the instance of that specific
type of object. It is a read only description of the object. All
data you want to change should be stored and modified in the actual
object instance. In this case %obj.ready will be unique to the actual
object instance.
So, most callbacks are going to be calling the
datablock methods. You simply use the %obj (second parameter) for
storing and retrieving information about the specific object that
caused the callback.
Schedule timers on objects or datablocks?
Take a look a the following code from my popup door.
This schedules the rotations ever 100 milliseconds then a downward
rotation after it is vertical for 2 seconds. Is this right, calling
the schedule on the datablock instead of on the object itself? Will
this cause problems with multiple instances?
function PopupData::doRotate(%this,%pu)
{
echo("doRotate ang=" @ %pu.currentRotation @ " count=" @ %pu.rotCount);
%pu.rotCount++;
if (%pu.rotCount<3)
{
%pu.currentRotation = %pu.currentRotation + %pu.rotIncrement;
echo("new Angle = " @ %pu.currentRotation @ " increment = " @ %pu.rotIncrement);
%pu.setTransform(getWords(%pu.origTransform,0,2) SPC %pu.rotAngle SPC %pu.currentRotation);
echo("newTrans = " @ %pu.getTransform());
//%pu.setTransform(getWords(%pu.origTransform,0,5) SPC %pu.currentRotation);
%pu.schedRot = %this.schedule(100,"doRotate",%pu);
}
else
{
%pu.Up = true;
%pu.schedRot = %this.schedule(2000,"startDown",%pu);
}
}
Agile programming and design documents
There are many different ways to design and build
software systems. One of my favorites is Agile. I've already put you
on the road toward using an Agile
development process.
What is Agile?
Software projects that are designed up front tend
to fail. This is because much of the knowledge is unknown at the beginning
and focing the design at the point makes fixed decisions about processes
that are not understood. Using an Agile process you don't design until
you are ready to implement. Design at any other time will include items
which will change when the development time arrives. It would be work
that would be wasted, especially if the feature that was designed never
ends up being built at all which is quite common. This is the problem
with plans, things never seem to work out the way we plan.
"In preparing for battle,
I have found that plans are useless but planning is indispensible" -
General Dwight D Eisenhower
Agile takes this into account in that we keep a lose
plan and fill it in as we go along. Plan to change. Agile is an iterative
process that keeps improving a product. At each step we always have
a usable product.
This would be the cycle.
1. Decide what to put in the next iteration
2. Design the iteration
3. Implement and test the iteration
4. Verify the iteration
In our case, the cycle will be one week. We will
decide what we are going to put into our games and spend the week implementing
those. If you finish early, you can always do a second cycle. At the
end of each cycle we will have a finished and working game ready for
the next cycle.
Most Agile development efforts work in four week
cycles.
Another trait of Agile development is to use unit
testing. Every function written also has a test function that calls
the function with various (good, bad, extremely bad) inputs and checks
the results. Those unit tests are called during every development cycle.
Any problems with unit tests need to be fixed immediately before moving
on.
Agile also requires the close communication of all
developers. In many cases, pair
programming is used to increase efficiency and reduce errors. Pair
programming puts two programmers on the same task. One writing
code and one watching for errors. Periodically they will switch positions.
Blender Skeleton
Someone was asking for a Torque skeleton template
to use with Blender. I found this one.
http://www.garagegames.com/index.php?sec=mg&mod=resource&page=view&qid=9282
Picking
Picking is the use of a ray to find the current object
that the mouse is currently targeting. I will go through the gpgt example
that comes with the yellow book. The World Editor Training mission
has a sample that always lists the object that is currently pointed
to by the center of the screen/camera.
You can see this in the status area.

Do a text search for "Look-at" in the gpgt
directories. It comes up with the file playGui.cs
Searching the playGui.cs file we find the reference
in the function:
function clientCmdUpdateLessonHUDs(
%data )
The value being filled in is %lookID which comes
from the data that was passed from the server. Now we know the client
comand being called is updateLessonHUDs. We do the next search to try
and find the call to this message in the server code by searching for "updateLessonHUDs".
This one returns the files game.cs and playGui.cs. Since we know playGui.cs
receives the message, game.cs must send it. Sure enough it packs up
a bunch of information in to a single string delimited by colons. Trace
through this and the client file and find that the seventh element
is made with a call to.
%clientConn.updateCurrentLookText( );
Sure enough, this returns the item we are currently
looking at. It takes a ray from the center of the camera and casts
it into the scene returning the first item it hits. We could pair this
with a mouse of keyboard click to cause some sort of functionality.
|