Game Development Community

dev|Pro Game Development Curriculum

Tactics-Action Hybrid Game Tutorial Part8: Custom Ai

by Steve Acaster · 05/08/2011 (6:12 pm) · 4 comments

Back to Part Seven: Tactics GameType

Let's create some custom Ai/playerObjects for both Ally team and Enemy team. Enemy team will be somewhat weaker than Ally as the Client is likely to face much more of them in-game. Open up art/datablocks/aiplayer.cs and add the following.

datablock PlayerData(TacticsPlayerData : DefaultPlayerData)
{
	maxEnergy =  40;//stock is 60, seems a bit much
	runSurfaceAngle  = 50;//yorks was 70;
};

datablock PlayerData(TacticsAiData : DefaultPlayerData)
{
	maxDamage = 40;//make enemy Ai weaker against player as there is likely to be more of them
	maxEnergy =  30;//stock is 60, make enemy Ai less fit that players
	runSurfaceAngle  = 50;//yorks was 70;
};

In scripts/server/aiplayer.cs we'll add callbacks for these new PlayerData types.

function TacticsPlayerData::onReachDestination(%this,%obj)
{
      // If there was a decal placed, then it was
   // stored in this %obj variable (see playGui.cs)
   // Erase the decal using the decal manager
   if( %obj.decal > -1 )
      decalManagerRemoveDecal(%obj.decal);

   // Moves to the next node on the path.
   // Override for all player.  Normally we'd override this for only
   // a specific player datablock or class of players.
   if (%obj.path !$= "")
   {
      if (%obj.currentNode == %obj.targetNode)
         %this.onEndOfPath(%obj,%obj.path);
      else
         %obj.moveToNextNode();
   }
}

function TacticsPlayerData::onEndOfPath(%this,%obj,%path)
{
   %obj.nextTask();
}

function TacticsPlayerData::onEndSequence(%this,%obj,%slot)
{
   echo("Sequence Done!");
   %obj.stopThread(%slot);
   %obj.nextTask();
}

function TacticsAiData::onReachDestination(%this,%obj)
{
      // If there was a decal placed, then it was
   // stored in this %obj variable (see playGui.cs)
   // Erase the decal using the decal manager
   if( %obj.decal > -1 )
      decalManagerRemoveDecal(%obj.decal);

   // Moves to the next node on the path.
   // Override for all player.  Normally we'd override this for only
   // a specific player datablock or class of players.
   if (%obj.path !$= "")
   {
      if (%obj.currentNode == %obj.targetNode)
         %this.onEndOfPath(%obj,%obj.path);
      else
         %obj.moveToNextNode();
   }
}

function TacticsAiData::onEndOfPath(%this,%obj,%path)
{
   %obj.nextTask();
}

function TacticsAiData::onEndSequence(%this,%obj,%slot)
{
   echo("Sequence Done!");
   %obj.stopThread(%slot);
   %obj.nextTask();
}

And finally, make make the custom spawn function. Team 1 is Ally and under the player's command, any other is in the enemy Ai Team. The player gets the more powerful "TacticsPlayerData" playerType. After the aiPlayerObject is spawned, it is added to the appropriate team array.

function AIPlayer::tacticsSpawn(%name, %spawnPoint, %team)
{
	if(%team == 1)
		%playerType = TacticsPlayerData;//the human player
	else
		%playerType = TacticsAIData;//the Ai enemy

   // Create the demo player object
   %player = new AiPlayer(%name)
   {
	dataBlock = %playerType;
	moveStuckTolerance = 0;//yorks emergency addition
//there is a bug in 1.1Preview that prevents Ai from moving
//you should be able to remove this line as it's fixed in earlier/later editions
//it should only be an issue with T3D 1.1 Preview.
   };
   MissionCleanup.add(%player);
   
   %player.setShapeName(%name);
   %player.team = %team;
   %player.action = 0;
   %player.hasFired = 0;
   
	if(isObject(%spawnpoint))
		%player.setTransform(%spawnPoint.getTransform());
	else
		echo(%spawnpoint @ " is not an object for " @ %name @ "'s spawnpoint");
		
	%player.setInventory(RocketLauncher, 1);
	%player.setInventory(RocketLauncherAmmo, %player.maxInventory(RocketLauncherAmmo));
	%player.mountImage(RocketLauncherImage, 0);
		
	if(isObject($AllyList) && %team == 1)
		$AllyList.add(%player, %playerType);
		
	if(isObject($EnemyList) && %team != 1)
		$EnemyList.add(%player, %playerType);
		
   return %player;
}

N.B. if you're using 1.1 Preview you'll want this bugfix for aiPlayer movement or you won't go anywhere. Expect later/earlier editions of T3D to have this issue resolved.

Now we've got our custom Ai for our custom Teams and custom gameType preparePlayer function, let's go back to art/gui/playGui.gui and make that "Team" button do something when it receives an action (is pressed).

function tacticsTeam::onAction(%this)
{
	%bot = localClientConnection.player;
	if(isObject(%bot))
	{
		if(%bot.getVelocity() !$="0 0 0")//this does happen sometimes
		{
			%bot.stop();		
			if( %bot.decal > -1 )
				decalManagerRemoveDecal( %bot.decal );
		}
	}

	%num = $AllyList.count();
	echo("number of team members = " @ %num);
	
	if(%num > 1)
	{
		//reset that playerObject's action to zero when we control another
		%bot.action = 0;

		echo($AllyList.getCurrent());
	
		if($AllyList.getCurrent() == %num -1 || $AllyList.getCurrent() == %num)
			%id = $AllyList.getKey($AllyList.moveFirst());
		else
			%id = $AllyList.getKey($AllyList.moveNext());

		//change the camera FIRST!
		commandToServer('tacticsCam');
		localClientConnection.player = %id;
		%id.updateEnergy();
		%id.updateHealth();
		%wpn = %id.getmountedimage(%slot);
		%wpn.UpdateWeaponHud(%id, %slot);
	}
	else
	{
		if(%num == 1)
		{
			%id = $AllyList.getKey($AllyList.moveFirst());	
			
			if(localClientConnection.player != %id)
			{
			//change the camera FIRST!
				commandToServer('tacticsCam');
				localClientConnection.player = %id;
				%id.updateEnergy();
				%id.updateHealth();
				%wpn = %id.getmountedimage(%slot);
				%wpn.UpdateWeaponHud(%id, %slot);
			}
			else
				echo("No more Ally players to swap control to!");
		}
		else
		{
			echo("No Players left!");
		}
	}
	
	tacticsMove.setStateOn(false);
	tacticsShoot.setStateOn(false);
	
	tacticsMove.pressed=0;
	tacticsShoot.pressed=0;
}

Now when "Team" button is pressed, it will reset the others and check for other team members to become the controlled playerObject. As long as there are more than one member of the team, it will cycle through them as controlObjects on each press, and update the on-screen HUD to show the current playerObject's health, energy and ammo.

Now, you should be able to test all of this in-game - but first, let's change the rocketLauncher so it's more like a standard rifle and not so massively overpowered. Open up art/datablocks/weapons/rocketLauncher.cs and find and replace the following datablocks:

//...

datablock ExplosionData(RocketLauncherExplosion)
{
   particleEmitter = RocketExpSmokeEmitter;
   particleDensity = 10;//20;
   particleRadius = 1;//2;
};

//...

datablock ProjectileData(RocketLauncherProjectile)
{
   projectileShapeName = "art/shapes/weapons/SwarmGun/rocket.dts";
   directDamage = 10;
   radiusDamage = 0;
   damageRadius = 0;
   areaImpulse = 0;

   explosion = RocketLauncherExplosion;
   waterExplosion = RocketLauncherWaterExplosion;

 decal = smallImpactDecal;

   muzzleVelocity = 100;
   velInheritFactor = 0.3;

   armingDelay = 0;
   lifetime = 5000; //(500m / 100m/s = 5000ms)
   fadeDelay = 4500;

   bounceElasticity = 0;
   bounceFriction = 0;
   isBallistic = false;
   gravityMod = 0.80;

   damageType = "RocketDamage";
};

//...

Open up art/decals/managedDecalData.cs and add:

datablock DecalData(smallImpactDecal)
{
   Material = "DECAL_RocketEXP";
   size = "0.5";
   lifeSpan = "50000";
   randomize = "1";
   texRows = "2";
   texCols = "2";
   clippingAngle = "60";
};

To replicate Valkyria Chronicles gameStyle, we need to increase the amount of damage headshots do and make the player respond with a yelp on any hit. Open up scripts/server/player.cs and edit:

function Armor::onDamage(%this, %obj, %delta)
{
//...

      // If the pain is excessive, let's hear about it.
      //if (%delta > 10)// <--- yorks commented out!
         %obj.playPain();
   }
}

*NOTE* T3D 1.1 Preview seems to have an issue with bodypart locations ALWAYS returning as headshots - do not uncomment the section below if you are using that version of T3D - bug report filed.
//replace the whole function
function Armor::damage(%this, %obj, %sourceObject, %position, %damage, %damageType)
{
   if (!isObject(%obj) || %obj.getState() $= "Dead")
      return;
	  
   %location = %obj.getDamageLocation(%position);
   %bodyPart = getWord(%location, 0);
   %region = getWord(%location, 1);
      echo("\c4DAMAGELOCATION:  bodyPart = "@ %bodyPart @" || REGION = "@ %region);
   // BODYPARTS:  HEAD | TORSO | LEGS
   // REGION (legs/Torso):  front_left | front_right | back_left | back_right
   // REGION (head):  left_back   | left_middle   | left front
   //                 middle_back | middle_middle | middle_front
   //                 right_back  | right_middle  | right_front
   echo(%bodypart SPC %location);

//remove comments to enable headshots
//		if (%damageType !$= "MissionAreaDamage")
//		{
//			if(%bodyPart $="head")
//				%damage=%damage*4;
//			if(%bodyPart $="torso")
//				%damage=%damage;
//			if(%bodyPart $="legs")
//				%damage=%damage/2;
//		}

   %obj.applyDamage(%damage);
   
   // Update the numerical Health HUD
   if(localClientConnection.player == %obj)//yorks in
		%obj.updateHealth();
	  
	 if(%obj.getState() $="Dead")//yorks
		game.GameSpecificDeath(%obj);//yorks
}

Now let's test.

Upon starting the level, you should see three playerObjects named "Tom", "Dick" and "Harry". Now you should be able to move them, have them shoot, and cycle between them as your selected control object. When you've run out of energy and shots for all of your team members, press "EndTurn" and the turn should cycle - first into the Enemy Team's phase (there isn't one yet), and then returning to Ally Team phase, regiving the Client control and incrementing the number of turns with the Turn Manager.

Next we need some Enemy Team members with some Ai think routines which change between whether they are active (Enemy Move Phase) or whether they are passive (Ally Move Phase).

Part Nine: Ai Combat

#1
05/09/2011 (2:42 am)
Hey Steve,

Gotta say I love your tutorials, what a help!

I'm using T3D and want to pick your brains. Im creating a mini FPS for a uni project and want to add a small hud object that changes colour depending on how close my ai bot is to the player (say from green(safe) to red(caught)) or even a picture

Do you have any ideas on how to get started?

cheers

Martin
#3
05/09/2011 (11:22 am)
I like your tutorials, they are clear to understand and allways very useful!
How i read some threads ago, you really should write a boock for our nice engine :)

edit: im sure i would buy a book!
#4
05/09/2011 (7:54 pm)
I feel tired just looking at all the stuff you wrote...
Thanks for this tutorial. Once I fnally get back to T3D, this will be a great refresher.(and I'm sure I'll learn quite a bit, too)

Also, as everyone has said: Write a book allready! I'd buy it. :)