Faking Aircraft and Real Airstrikes In T3D - Part ONE of two
by Steve Acaster · 04/15/2011 (1:30 pm) · 6 comments
So you're playing the latest 3A installment of the world's biggest FPS franchise, "Ball of Fruity 9001: This Time It's Global War ... Again", and in the middle of all your very shiny, yet absurdly close-range firefights, with some butch AI character constantly shouting "Take Down That X With Your Y" at you, there are jets screeching low overhead to really set the scene of a grand, combined arms battlefield ...
... well those jets aren't real vehicles, they're all scriptObjects, and they're a small redbox with an arrow for forward vector coming off them, and sometimes they've got a small orange box called an "origin" which draws a line to another small orange box to mark out a linear path. In-game the small redbox displays a shapefile and moves down the path between the orangebox at whatever rate is scripted for it, then it deletes.
And you can do the same thing in Torque3D and have your own fake jest screaming close overhead ... but let's make it even fancier and add the ability to for those scriptObject aircraft to deliver airstrikes.
For this you will need:
1 x shapeFile model for your aircraft
1 x smoke emitter to display where the airstrike will be
1 x explosion for bombs
1 x water explosion for bombs striking water
1 x aircraft noise audio file
How you wish to deploy an airstrike is up to you, personally, in my custom stuff I have my player equipped with a laser designator weapon but in this example we'll have a simple function called from console that will bring an airstrike in on whatever the player is looking at ... so don't be looking at your own feet when you test this ...
There are a few commented out sections which refer to spawning markers to help in aiding the visual marking of positions and vectors in editor (not that the stock markers have a visible forward angle). Apart from the aircraft model, there are no objects to spawn, everything is handled with vectors and positions in script (save for the final explosions - which are of course explosion data). For explosions and particles, we'll use stock data in this example.
If you find that you're aircraft model is flying in sideways or backwards, it's because you exported it facing the incorrect angle. Here we are using a StaticShape as our "scriptObject" for the aircraft model.
Create a new cs file, I'd suggest using art directory. Call it whatever you want, but here let's call it bomber_shape. Don't forget to exec it (datablockExec.cs).
You'll also need a few useful utility functions that aid in 3D math - including the function to actually make the static shape move.
Make a note of these math functions - they are useful for a whole number of things ...
-----------------------------------------------
And now the meat of the airstrike itself. Create a new cs file, I'd suggest in "scripts/server" directory and call it airstrike.cs - don't forget to exec it (in scriptExec.cs).
Now, on to the meat of the actual airstrike.
The theory is this:
1. Player calls in strike at position that they are looking at.
2. Aircraft does some checks to make sure that there won't be a collision with anything and attempts to alter it's attack height to fly higher than obstacles. (Feel free to combine viewMask and Bombmask into a global)
3. Aircraft flies in due left of the player's view, heading out due right of the players view - ie: across the screen fom the player's view.
4. Bombs drop on a schedule derived from the aircrafts flight time, not from the aircraft itself.
Now you could have any sort of airstrike that you like, but I have a hankering for clusterbombs.
In Part2!
... well those jets aren't real vehicles, they're all scriptObjects, and they're a small redbox with an arrow for forward vector coming off them, and sometimes they've got a small orange box called an "origin" which draws a line to another small orange box to mark out a linear path. In-game the small redbox displays a shapefile and moves down the path between the orangebox at whatever rate is scripted for it, then it deletes.
And you can do the same thing in Torque3D and have your own fake jest screaming close overhead ... but let's make it even fancier and add the ability to for those scriptObject aircraft to deliver airstrikes.
For this you will need:
1 x shapeFile model for your aircraft
1 x smoke emitter to display where the airstrike will be
1 x explosion for bombs
1 x water explosion for bombs striking water
1 x aircraft noise audio file
How you wish to deploy an airstrike is up to you, personally, in my custom stuff I have my player equipped with a laser designator weapon but in this example we'll have a simple function called from console that will bring an airstrike in on whatever the player is looking at ... so don't be looking at your own feet when you test this ...
There are a few commented out sections which refer to spawning markers to help in aiding the visual marking of positions and vectors in editor (not that the stock markers have a visible forward angle). Apart from the aircraft model, there are no objects to spawn, everything is handled with vectors and positions in script (save for the final explosions - which are of course explosion data). For explosions and particles, we'll use stock data in this example.
If you find that you're aircraft model is flying in sideways or backwards, it's because you exported it facing the incorrect angle. Here we are using a StaticShape as our "scriptObject" for the aircraft model.
Create a new cs file, I'd suggest using art directory. Call it whatever you want, but here let's call it bomber_shape. Don't forget to exec it (datablockExec.cs).
datablock StaticShapeData( bomberShapedata )
{
category = "StaticShape";
shapeFile = "art/shapes/vehicles/bomber.dts";
};You'll also need a few useful utility functions that aid in 3D math - including the function to actually make the static shape move.
//==============================
function shapebase::getVectorTo(%this, %pos)
{ return VectorSub(%pos, %this.getPosition()); }
function shapebase::getAngleTo(%this, %pos)
{ return shapebase::getAngle(%this.getVectorTo(%pos), %this.getEyeVector()); }
// Return angle between two vectors
function shapebase::getAngle(%vec1, %vec2)
{
%vec1n = VectorNormalize(%vec1);
%vec2n = VectorNormalize(%vec2);
%vdot = VectorDot(%vec1n, %vec2n);
%angle = mACos(%vdot);
// convert to degrees and return
%degangle = mRadToDeg(%angle);
return %degangle;
}
// these stay, since having to type %rot = %obj.rotFromTransform(%transform); after already having the transform is
// just way too much.
function posFromTransform(%transform)
{
// the first three words of an object's transform are the object's position
%position = getWord(%transform, 0) SPC getWord(%transform, 1) SPC getWord(%transform, 2);
return %position;
}
function rotFromTransform(%transform)
{
// the last four words of an object's transform are the object's rotation
%rotation = getWord(%transform, 3) SPC getWord(%transform, 4) SPC getWord(%transform, 5) SPC getWord(%transform, 6);
return %rotation;
}
//if we are going to make these inherited functions, might as well let them get the transform as well
function SceneObject::posFromTransform(%obj, %transform)
{
if(%transform $= "")
%transform = %obj.getTransForm();
// the first three words of an object's transform are the object's position
%position = getWord(%transform, 0) SPC getWord(%transform, 1) SPC getWord(%transform, 2);
return %position;
}
function SceneObject::rotFromTransform(%obj, %transform)
{
if(%transform $= "")
%transform = %obj.getTransForm();
// the last four words of an object's transform are the object's rotation
%rotation = getWord(%transform, 3) SPC getWord(%transform, 4) SPC getWord(%transform, 5) SPC getWord(%transform, 6);
return %rotation;
}
function SceneObject::StartMoveObject(%obj, %endpos, %time, %smoothness, %delay)
{
if(isObject(%obj))
{
%startpos = %obj.getTransform();
%diff = VectorSub(%endpos, %startpos);
%numsteps = (%time/1000) * %smoothness;
%interval = 1000 / %smoothness;
%stepvec = VectorScale(%diff, (1/%numsteps));
%numstepsleft = %numsteps;
%currpos = %startpos;
%obj.MoveObject(%startpos, %endpos, %numsteps, %numstepsleft, %stepvec, %currpos, %interval, %delay);
}
}
function SceneObject::MoveObject(%obj, %startpos, %endpos, %numsteps, %numstepsleft, %stepvec, %currpos, %interval, %delay)
{
%rot = rotFromTransform(%obj.getTransform());
%currpos = VectorAdd(%currpos, %stepvec);
%obj.setTransForm(%currpos SPC %rot);
%numstepsleft--;
if(%numstepsleft < 1)
return;
else
%obj.schedule(%interval, "MoveObject", %startpos, %endpos, %numsteps, %numstepsleft, %stepvec, %currpos, %interval, %delay);
}
function pointToXYPosDegree(%posOne, %posTwo)
{
%vec = VectorSub(%posOne, %posTwo);
//get the angle
%rotAngleZ = mATan( firstWord(%vec), getWord(%vec, 1) );
//add pi to the angle
%rotAngleZ += 3.14159;
//make this rotation a proper torque game value, anything more than 240
// degrees is negative
if(%rotAngleZ > 4.18879)//yorks you don't actually need this but if it ain't broke don't fix it
{
//the rotation scale is seldom negative, instead make the axis value negative
%modifier = -1;
//subtract 2pi from the value, then make sure its positive
%rotAngleZ = mAbs(%rotAngleZ - 6.28319);
//sigh, if only this were all true
}
else
%modifier = 1;
//assemble the rotation and send it back
// return "0 0" SPC %modifier SPC %rotAngleZ;//yorks out if you don't want radians - put back in if you do!
%rotAngleZ = mRadToDeg(%rotAngleZ);//yorks in - for returning a degree angle, take out if you want radians
return "0 0" SPC %modifier SPC %rotAngleZ;
}
function pointToPos(%posOne, %posTwo)
{
//sub the two positions so we get a vector pointing from the origin in the
// direction we want our object to face
%vec = VectorSub(%posTwo, %posOne);
// pull the values out of the vector
%x = firstWord(%vec);
%y = getWord(%vec, 1);
%z = getWord(%vec, 2);
//this finds the distance from origin to our point
%len = vectorLen(%vec);
//---------X-----------------
//given the rise and length of our vector this will give us the angle in radians
%rotAngleX = mATan(%z, %len);
//---------Z-----------------
//get the angle for the z axis
%rotAngleZ = mATan(%x, %y);
//create 2 matrices, one for the z rotation, the other for the x rotation
%matrix = MatrixCreateFromEuler("0 0" SPC %rotAngleZ * -1);
%matrix2 = MatrixCreateFromEuler(%rotAngleX SPC "0 0");
//now multiply them together so we end up with the rotation we want
%finalMat = MatrixMultiply(%matrix, %matrix2);
//we're done, send the proper numbers back
return getWords(%finalMat, 3, 6);
}Make a note of these math functions - they are useful for a whole number of things ...
-----------------------------------------------
And now the meat of the airstrike itself. Create a new cs file, I'd suggest in "scripts/server" directory and call it airstrike.cs - don't forget to exec it (in scriptExec.cs).
Now, on to the meat of the actual airstrike.
The theory is this:
1. Player calls in strike at position that they are looking at.
2. Aircraft does some checks to make sure that there won't be a collision with anything and attempts to alter it's attack height to fly higher than obstacles. (Feel free to combine viewMask and Bombmask into a global)
3. Aircraft flies in due left of the player's view, heading out due right of the players view - ie: across the screen fom the player's view.
4. Bombs drop on a schedule derived from the aircrafts flight time, not from the aircraft itself.
function airStrike()
{
%viewMask =
$TypeMasks::VehicleObjectType |
$TypeMasks::TerrainObjectType |
$TypeMasks::StaticTSObjectType |
$TypeMasks::StaticShapeObjectType |
$TypeMasks::WaterObjectType |
$TypeMasks::ForestObjectType;//forest replaces obsolete interior
%count = ClientGroup.getCount();
for( %cl = 0; %cl < %count; %cl++ )
%client = ClientGroup.getObject( %cl );
echo("Targeting for airstrike");
%eyeVec = %client.player.getEyeVector();
%startPos = %client.player.getEyePoint();
%endPos = VectorAdd(%startPos, VectorScale(%eyeVec, 1000));
%target = ContainerRayCast(%startPos, %endPos, %viewMask, %client.player);
%col = firstWord(%target);
if(%col == 0)
{
echo("haven't hit anything to place a target at!");
return;
}
%originArea = getwords(%target, 1, 3);
// echo(%col SPC %col.getClassname());
//return the ID of what we've hit
//3d math madness!
%targetArea = VectorAdd(%originArea, "0 0 100");
// Get the vector from the player to the target
%vec = %client.player.getForwardVector();
echo("player forwardVec = " @ %vec);
%rot = MatrixCreateFromEuler("0 0 " @ mDegToRad(0 - 270));
%mat = MatrixMulVector(%rot, %vec);
%start = VectorAdd(%targetArea, VectorScale(%mat, 2000));
%startVec = pointToXYPosDegree(%start, %targetArea);
echo("startVec1 " @ %startVec);
%end = VectorAdd(%targetArea, VectorScale(%mat, -2000));
echo("end " @ %end);
/*
//just for testing - startNode with rotation - endNode with rotation
%node = new waypoint()
{
team = "0";
dataBlock = "WayPointMarker";
position = %start;
rotation = %startVec;
scale = "1 1 1";
canSave = "1";
canSaveDynamicFields = "1";
};
MissionCleanup.add(%node);
%node2 = new waypoint()
{
team = "0";
dataBlock = "WayPointMarker";
position = %end;
rotation = %startVec;
scale = "1 1 1";
canSave = "1";
canSaveDynamicFields = "1";
};
MissionCleanup.add(%node2);
*/
//now check that the plane has a clear run and isn't going to prang into anything ...
//note: these checks for a clear path on the aircrafts attack run don't check to see if the bomb path is clear
%Maxattempts = 3;
for(%attempt=0; %attempt<%Maxattempts; %attempt++)
{
//oh-oh, hit an obstacle - bomb from higher altitude
%start = VectorAdd(%start, "0 0 100");
%end = VectorAdd(%end, "0 0 100");
%targetArea = VectorAdd(%targetArea, "0 0 100");
%clearPath = ContainerRayCast(%start, %end, $AiPlayer::Obstacles, %obj);
echo("Airstrike Path Blocked - retrying attempt: " @ %attempt);
if(%clearPath == 0)
{
%clearRun = true;
break;
}
}
if(%clearRun != true)
{
echo("Cannot deploy airstrike!");
return;
}
%flare = new ParticleEmitterNode()
{
active = "1";
emitter = "SmokeEmitter";
velocity = "1";
dataBlock = "SmokeEmitterNode";
position = %originArea;
rotation = "1 0 0 0";
scale = "1 1 1";
locked = "1";
canSave = "1";
canSaveDynamicFields = "1";
};
MissionCleanup.add(%flare);
%flare.schedule(4000, "delete");
//and the attack vector for the bombs
%attackPos = VectorAdd(%targetArea, VectorScale(%mat, 100));
//next get the rotation to aim at the target
%BombVec = VectorSub(%originArea, %attackPos);
%bombVecN = VectorNormalize(%bombVec);
%attackVec = VectorAdd(%originArea, VectorScale(%bombVecN, 200));
/*
%attackRot = pointToPos(%attackPos, %originArea);
%attackAngle = getWord(%attackRot, 3);
%attackAngle = mRadToDeg(%attackAngle);
%attackRot = setWord(%attackRot, 3, %attackAngle);
//just for testing - attackNode with attackPostor rotation
%attacknode = new waypoint()
{
team = "0";
dataBlock = "WayPointMarker";
position = %attackPos;//%targetArea;
rotation = %attackRot;
scale = "1 1 1";
canSave = "1";
canSaveDynamicFields = "1";
};
MissionCleanup.add(%attacknode);
*/
// debugdraw.togglefreeze();
// debugdraw.drawline(%attackPos, %attackVec, "1 0 1");
echo("callbomber");
%air = new StaticShape()
{
dataBlock = bomberShapedata;
position = %start;
rotation = %startVec;
};
MissionCleanup.add(%air);
//audio cannot be heard outside of mission max visibility distance
//if you have a very small visDistance in level,
//you might want to have the %targetArea play the sound in 3D space
%air.playAudio(1, "jetNoise");
//start the bomber moving and calculate the airstrike
startBomber(%air, %end);
dropBomblets(%attackPos, %bombVec);
}
function startBomber(%bomber, %end)
{
echo("startBomber");
ShapeBase::StartMoveObject(%bomber, %end, 8000, 800, 1000);
%bomber.schedule(8500, "delete");
}Now you could have any sort of airstrike that you like, but I have a hankering for clusterbombs.
In Part2!
About the author
One Bloke ... In His Bedroom ... Making Indie Games ...
#2
Very cool resource Stevie!
04/15/2011 (4:48 pm)
Quote:... well those jets aren't real vehicles, they're all scriptObjects, and they're a small redbox with an arrow for forward vector coming off them, and sometimes they've got a small orange box called an "origin" which draws a line to another small orange box to mark out a linear path. In-game the small redbox displays a shapefile and moves down the path between the orangebox at whatever rate is scripted for it, then it deletes.You know that for writing a tech article is not mandatory to destroy peoples ilusion, don't you?
Very cool resource Stevie!
#3
Kudos to you. :)
04/16/2011 (11:48 am)
Nice Steve. As usual a brilliant resource and one that teaches a lot of facets about the T3D engine.Kudos to you. :)
#4
==>airStrike();
Targeting for airstrike
scripts/server/airstrike.cs (13): Unable to find object: '' attempting to call function 'getEyeVector'
scripts/server/airstrike.cs (15): Unable to find object: '' attempting to call function 'getEyePoint'
haven't hit anything to place a target at!
This is a great resource and would love to get it working. Thanks for your help!
04/19/2011 (2:43 pm)
Great resource Steve! Lots of great stuff in here. But I can't seem to get it working. The first error is a misspelling of "airStrike" in the function and a missing bracket at the end of the airstrikeDamage resource from part 2. But the one error I cant get passed is this.==>airStrike();
Targeting for airstrike
scripts/server/airstrike.cs (13): Unable to find object: '' attempting to call function 'getEyeVector'
scripts/server/airstrike.cs (15): Unable to find object: '' attempting to call function 'getEyePoint'
haven't hit anything to place a target at!
This is a great resource and would love to get it working. Thanks for your help!
#5
Yes, I'd ported it from a weapon system and had neglected to work out the appropriate ownership of the function.
Hopefully fixed now.
[edit]
I haz manflu ... sniffle
Now by getting the client at the beginning of the function we should then be able to get the player object without having to use:
04/19/2011 (6:33 pm)
How embarrassing!Yes, I'd ported it from a weapon system and had neglected to work out the appropriate ownership of the function.
Hopefully fixed now.
[edit]
I haz manflu ... sniffle
Now by getting the client at the beginning of the function we should then be able to get the player object without having to use:
%obj = getControlObject(%client)And thus return something that we has eyenode to use for targeting!
#6
04/19/2011 (7:37 pm)
Thanks for the quick reply! It all works beautifully now. And I gotta thank you for all these amazing resources. They've really inspired me to start actually trying to make my game! :) 
Associate Michael Hall
Distracted...
Now you've got to give your laser target marker/pointer weapon some visuals ;)