T3D/Tutorials/Fake Aircraft Real Airstrikes

From TDN

Faking Overhead Aircraft and Real Airstrikes In T3D


Faking aircraft zooming overhead like all the cool FPS's have ... but let's expand that and have fake aircraft deliver REAL airstrikes! In T3D using scripted objects, baffling 3D maths and raycasts.


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.cs. 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.

So we're going to have clusterbombs spawn after the aircraft passes, and rain down from a right angle, 100 units along the aircraft's flight path, and "saturate" the target area, where our smoke has popped up to mark. For this tutorial, we'll be using the stock particleFX/exposion data of the rocket launcher.

//bombs
function dropBomblets(%start, %vector)
{
	//You shouldn't need aircraft but just case ...
	
	%bombMask = 
  $TypeMasks::VehicleObjectType |      
  $TypeMasks::TerrainObjectType |     
  $TypeMasks::StaticTSObjectType |   
  $TypeMasks::StaticShapeObjectType |
  $TypeMasks::WaterObjectType |
   $TypeMasks::ForestObjectType;//forest replaces obsolete interior


	echo("dropBomblets");
	echo("start = " @ %start);
	%bombarray = new arrayobject();
	MissionCleanup.add(%bombarray);

	//bomblet accuracy
	%spread = 8/800;
	%spread = %spread $= "" ? 0.001 : %spread;
	%spread = %spread <= 0 ? 0.001 : %spread;

	//how many bomblets?
	%Bombloop = 10;
	%Bombcount = 0;	

	// Create each projectile and send it on its way 
	for(%bomb=0; %bomb<%bombloop; %bomb++)
	{
		%velocity = VectorScale(%vector, 500);//500 is the length to check for impacts - alter to taste

		%x = (getRandom() - 0.5) * 4 * 3.1415926 * %spread * 2.00;
		%y = (getRandom() - 0.5) * 4 * 3.1415926 * %spread * 2.00;
		%z = (getRandom() - 0.5) * 4 * 3.1415926 * %spread * 2.00;
		%mat = MatrixCreateFromEuler(%x @ " " @ %y @ " " @ %z);

		%velocity = MatrixMulVector(%mat, %velocity);
		%aimpoint = VectorAdd(%start, %velocity);
	
		%targetSearch = containerRayCast(%start, %aimpoint, %bombMask);

		%hit = firstword(%targetSearch);

		if(%hit != 0)
		{
			%impactpoint = getWords(%targetSearch, 1, 3);
			
			echo("hit classname = " @ %hit.getClassname());
			if(%hit.getType() == $TypeMasks::waterObjectType)
			{
				echo("WET!");
				%impactPoint = %impactPoint @ " 1";
			}
		
			%dist = vectorDist(%start, %impactpoint);
		
			%bombarray.add(%impactPoint, %dist);
		
		//	debugdraw.togglefreeze();
		//	debugdraw.drawline(%start, %velocity, "0 0 1");

			%Bombcount++;
		}

	}
	
	%bombarray.sortd();
	%bombarray.moveFirst();
	%bombarray.echo();
	
	//bombletStrike(%bombarray, 10);
	
	//%array = %bombarray.getID();
	//echo("array ID = " @ %array);
	schedule(4000, 0, "BombletStrike", %bombarray, %bombcount);
}

function BombletStrike(%array, %bombs)
{
	echo("Bomblet strikes!");
	%total = %array.count();
	echo("array count = " @ %total);
	
	%i = %array.getcurrent();
	%origin = %array.getKey(%i);
	
	%wet = getWordCount(%origin);
	if(%wet == 4)
	{
		echo("wet wordcount = " @ %wet);
		echo("wet1 = " @ %origin);
	
		%origin = getWords(%origin, 0, 2);
		
		echo("wet2 = " @ %origin);
		
		%blast = new explosion()
		{
			dataBlock = RocketLauncherWaterExplosion;
			position = %origin;
		};
		MissionCleanup.add(%blast);
	
	}
	else
	{
		%blast = new explosion()
		{
			dataBlock = RocketLauncherExplosion;
			position = %origin;
		};
		MissionCleanup.add(%blast);
	}
	
	%bombs--;
	
//let's not use standard radiusDamage - let's make a custom!	
	//radiusDamage(%sourceObject, %position, %radius, %damage, %damageType, %impulse)
			airStrikeDamage(0, %origin, 15, 100, "radius", 100 * 10);
		
	if(%bombs < 1)
	{
		%array.empty();
		%array.delete();
	}
	else
	{
		%array.moveNext();
		schedule(200, 0, "bombletStrike", %array, %bombs);
	}
}

Note that bombs will explode on the surface of water, giving the waterExplosion. When the airstrike hits, it will explode the clusterbombs one at a time, starting with the nearest strike to the point of launch. Rather than use a standard "radiusDamage" - to show that bombs have much more penetrative power than normal projectiles and can thus punch through walls and obstacles, we're going to use a custom damage function that will ignore all cover.

Finally add:

function AirStrikeDamage(%sourceObject, %position, %radius, %damage, %damageType, %impulse)
{
   InitContainerRadiusSearch(%position, %radius, $TypeMasks::ShapeBaseObjectType);

   	%outerRadius = %radius /1.3;//yorks
     %halfRadius = %radius / 2;
   %quarterRadius = %radius /4;//yorks

   while ((%targetObject = containerSearchNext()) != 0)
   {
	  %range=vectorDist(%targetObject.getposition(), %position);
	  
	if(%range <= %quarterradius)
	{
		%mydamage = %damage * 2;//* 0
		%rangedam ="high - " @ %mydamage/2;
	}
	else
	{
		if(%range <= %halfradius && %range > %quarterradius)
		{
			%mydamage = %damage;// /2
			%rangedam ="medium - " @ %mydamage/2;
		}
		else
		{
			if(%range <= %outerradius && %range > %halfradius)
			{
				%mydamage = %damage /2;// /4
				%rangedam ="low - " @ %mydamage/2;
			}
			else
			{
					%mydamage = %damage /3;// /6
					%rangedam ="lowest - " @ %mydamage/2;
			}
		}
	}
      // Apply the damage
 //     %targetObject.damage(%sourceObject, %position, %damage * %coverage * %distScale, %damageType);
	%targetObject.damage(%sourceObject, %position, %mydamage, %damageType);//damage has to be x2 now ... why
		
		// Apply the impulse
		if (%impulse)
		{
			%impulseVec = VectorSub(%targetObject.getWorldBoxCenter(), %position);//note getPosition can cause a random headshot wtf?
			%impulseVec = VectorNormalize(%impulseVec);

			if(%targetObject.getClassName() $= "AIPlayer" || %targetObject.getClassName() $= "Player")
			{
				%impulseVec = VectorScale(%impulseVec, %impulse);
				%targetObject.applyImpulse(%position, %impulseVec);	
			}
			else
			{
					%impulseVec = VectorScale(%impulseVec, %impulse);
							%targetObject.applyImpulse(%position, %impulseVec);	
			}
		}
	}
}

And that should be that!

Now when you aim the player at a point and enter the command into the console:

airstrike();

A cloud of smoke should mark the target area, an aircraft should spawn 2k units out to the left of the player's view, race across infront of the player in 8 seconds, and 10 bomblets impact on the target zone.

And it should look something like this (only without my custom art assets obviously):


HD VIDEO Example (YouTube)


Apologies for the annoying "pings" as I swap camera/player control :P


Enjoy!

" I love the smell of Dual Purpose Improved Conventional Munitions in the morning!"


Thanks to TRON and Silent Mike for their help in finding the old math functions with the wayback machine, and the various people who contributed to those functions back in the day when trolling meant something ...