Game Development Community

dev|Pro Game Development Curriculum

ChinaTown Ai Deathmatch with Recast Pathfinding Part TWO

by Steve Acaster · 03/23/2012 (5:57 pm) · 4 comments

Back To Part One

-----------------------------

So we have a lifeless Ai player in ... now to make him do something.

We're going to import various stock functions from "Full Template" Project for "DemoPlayer" datablock. And we're also going to edit them and add extra functionality. The navMesh system works in conjuction with stock Torque Ai Paths, so we need to bring some of that int.

In our custom "scripts/server/aiplayer.cs" file add the following stock Ai utility functions:

function DemoPlayer::onReachDestination(%this,%obj)
{
   //echo( %obj @ " onReachDestination" );

   // 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 DemoPlayer::onEndSequence(%this,%obj,%slot)
{
   echo("Sequence Done!");
   %obj.stopThread(%slot);
   %obj.nextTask();
}

function AIPlayer::followPath(%this,%path,%node)
{
   // Start the player following a path
   %this.stopThread(0);
   if (!isObject(%path))
   {
      %this.path = "";
      return;
   }

   if (%node > %path.getCount() - 1)
      %this.targetNode = %path.getCount() - 1;
   else
      %this.targetNode = %node;

   if (%this.path $= %path)
      %this.moveToNode(%this.currentNode);
   else
   {
      %this.path = %path;
      %this.moveToNode(0);
   }
}

function AIPlayer::moveToNextNode(%this)
{
   if (%this.targetNode < 0 || %this.currentNode < %this.targetNode)
   {
      if (%this.currentNode < %this.path.getCount() - 1)
         %this.moveToNode(%this.currentNode + 1);
      else
         %this.moveToNode(0);
   }
   else
      if (%this.currentNode == 0)
         %this.moveToNode(%this.path.getCount() - 1);
      else
         %this.moveToNode(%this.currentNode - 1);
}

function AIPlayer::moveToNode(%this,%index)
{
   // Move to the given path node index
   %this.currentNode = %index;
   %node = %this.path.getObject(%index);
   %this.setMoveDestination(%node.getTransform(), %index == %this.targetNode);
}

On top of this, we need some stock functions customizing, such as what to do when we reach the end of our path, and what to do if we get stuck. Getting stuck is an accumulating number, if after a certain number of attempts to continue and the Ai is still not going where, he'll look for a new path.

function DemoPlayer::onMoveStuck(%this,%obj)
{
   //echo( %obj @ " onMoveStuck" );

	if(%obj.stuck > 20)
	{
		%obj.stop();
		%obj.randomPath();
	}
	else
	{
		%obj.stuck++;
	}
}

function DemoPlayer::onEndOfPath(%this,%obj,%path)
{
   %obj.aiDecide();
}

The last of the stock functionality to employ is finding the nearest human player so that the Ai can later make a Line Of Sight (LOS) test and also for hunting. We customize this function so that it returns the actual player object and not just an index of the clientGroup.

function AIPlayer::getTargetDistance(%this, %target)
{
   echo("c4AIPlayer::getTargetDistance("@ %this @", "@ %target @")");
   $tgt = %target;
   %tgtPos = %target.getPosition();
   %eyePoint = %this.getWorldBoxCenter();
   %distance = VectorDist(%tgtPos, %eyePoint);
   echo("Distance to target = "@ %distance);
   return %distance;
}

function AIPlayer::getNearestPlayerTarget(%this)
{
   echo("c4AIPlayer::getNearestPlayerTarget("@ %this @")");

   %index = -1;
   %botPos = %this.getPosition();
   %count = ClientGroup.getCount();
   for(%i = 0; %i < %count; %i++)
   {
      %client = ClientGroup.getObject(%i);
      if (%client.player $= "" || %client.player == 0)
         return -1;
      %playerPos = %client.player.getPosition();

      %tempDist = VectorDist(%playerPos, %botPos);
      if (%i == 0)
      {
         %dist = %tempDist;
         %index = %i;
      }
      else
      {
         if (%dist > %tempDist)
         {
            %dist = %tempDist;
            %index = %i;
         }
      }
   }
   //return %index;//just the index is not so helpful - yorks
   
	if(%index != -1)//yorks all new below!
	{
		%obj = ClientGroup.getObject(%index);
		%player = %obj.player;
		return %player;//the player is what we want
	}
	
	return -1;
}

And finally, a dead Ai needs to stop shooting and also respawn from a new Ai from a random waypoint.

function DemoPlayer::onDamage(%this,%obj, %delta)
{
	if(%obj.getstate() $="Dead")
   	{
		//take finger off trigger
		%obj.setImageTrigger(0, 0);
		
		//and spawn a new enemy!
		randomSpawn();
   	}
}

And now back to fully custom functions. We've spawned a player, and now we want tehm to decide what tehy will do first, as well as give them ammo for their rifle, and decide how aggressive they should be.

Aggression comes in three flavours - randomized each time for variation:
Low, will not pursue the player, and will move towards their waypoint goal - though they will shoot whilst doing this if they see the player.
Medium, will move towards their goal, and once there, if they can see the player they will attempt to purse him.
High, cheaty Ai mode, they will sniff out the player where ever he is and attack him.

Once the Ai has decided what to do, it will head for either it's previously generated randomized waypoint goal, or head straight for the player.

function AIPlayer::aiStartUp(%this, %goal)
{
	if(!isObject(%this))
		return;
		
	if(%this.getState() $="Dead")
		return;
		
	//here the Ai is going to decide what to do when it spawns
	
	//tool up with some bullets
	%this.setInventory("LurkerAmmo", 30);
	
	//and let's randomize his "Level Of Aggression"
	//1 = low, will shoot at player but won't deviate from original goal
	//2 = medium, will attack player and follow player when he sees him
	//3 = high, is telepathically drawn to the player's location at all times
	%aggro = getRandom(1, 3);
	%this.LoA = %aggro;
	
	//get an initial path
	%start = %this.getPosition();
	if(%aggro < 3)
		%end = %goal.getPosition();
	else
	{
		%enemy = %this.getNearestPlayerTarget();
		if(!isObject(%enemy))
		{
			echo("Looking for but can't find a player to hunt, original goal");
			%end = %goal.getPosition();
		}
		else
		{
			%end = %enemy.getPosition();
		}
	}
	
	//create the variables for the path
	navPath1.from = %start;
	navPath1.to = %end;
	navPath1.plan();
	
	//check to see if we can shoot
	%this.canWeMakeBangBang();
	
	//and go down our new path
	%this.followPath("MissionGroup/navPath1", 1024);
}

When an Ai has ended their path, they need to decide what to do. Again this is dependant on their Level Of Aggression, and whether the Player is in view.

function AIPlayer::aiDecide(%this)
{
	if(!isObject(%this))
		return;
		
	if(%this.getState() $="Dead")
		return;
		
	%start = %this.getPosition();
	
	//if we are super aggressive just go at them
	if(%this.LoA == 3)
	{
		//find that player!
		%enemy = %this.getNearestPlayerTarget();
		if(isObject(%enemy))
			if(%enemy.getState() !$="Dead")
			{
				%end = %enemy.getPosition();
				navPath1.from = %start;
				navPath1.to = %end;
				navPath1.plan();
	
				%this.followPath("MissionGroup/navPath1", 1024);//hunt!
				return;
			}
	}
	
	//can we see the player?
	if(%this.LoA == 2)
	{
		//find that player!
		%enemy = %this.getNearestPlayerTarget();
		if(isObject(%enemy))
			if(%enemy.getState() !$="Dead")
			{
				//can we see them? If not randompath
				%los = %this.playerLOS(%enemy);
				if(%los == true)
				{
					%end = %enemy.getPosition();
					navPath1.from = %start;
					navPath1.to = %end;
					navPath1.plan();
	
					%this.followPath("MissionGroup/navPath1", 1024);//hunt!
					return;
				}
			}
	}

	//randomPath
	%this.randomPath();
}

If the Ai is of "low" aggression, or if they become stuck or cannot find a human player which is alive, they will choose a randomized goal and keep moving.

function AIPlayer::randomPath(%this)
{
	if(!isObject(%this))
		return;
		
	if(%this.getState() $="Dead")
		return;
		
	echo("Random Path called by " @ %this);
	%this.stuck = 0;
	%this.currentNode = 0;
	%this.targetNode = 0;
	
	%start = %this.getPosition();

	//get a random goal, count available up
	%count = aiSpawnGroup.getCount();
	
	//get the max. index from the group. 
	//Index starts at 0 and not 1.
	//so we need to take 1 off the count
	%max = %count - 1;
	
	//randomize!
	%index = getRandom(0, %max);
	
	//get the spawnSphere from the simgroup at the index
	%spawn = aiSpawnGroup.getObject(%index);
	%end = %spawn.getPosition();
	
	navPath1.from = %start;
	navPath1.to = %end;
	navPath1.plan();
	
	%this.followPath("MissionGroup/navPath1", 1024);
}

That's pretty much pathfinding and decision-making done, now for the actual combat.

The Ai needs to be able to visually identify the player to be able to tell whether they have a clean shot or not. For this we use a raycast, directed from the Ai's eyeNode to the Player's eyeNode. If anything gets in the way, the Ai cannot see the Player. If the raycast hits the Player, obviously they are in view. But also if the raycast doesn't hit anything, then it counts as being able to see the Player. This is because sometimes the Player/Ai model has it's eyeNode outside of the collidable bounding-box, and sometimes the eyeNode is pushed outside of the bounding-box via an animation.

function AIPlayer::playerLOS(%this, %them)
{
	//from our eye to the player's eye
	%ai = %this.getEyePoint();
	%player = %them.getEyePoint();
   
	//things to get in the way
	%mask = $TypeMasks::TerrainObjectType | $TypeMasks::StaticObjectType;

	// see if anything gets hit
	%collision = containerRayCast(%ai, %player, %mask, %this);
	
	//nothing hit, this happens when EyeNode is outside of boundsBox
	//and thus raycast has no collision to record against
	//still means that they are viewable
	if(!%collision)
		return true;
	
	%hit = firstWord(%collision);
   
	//we can see them
	if(%hit == %them)
		return true;
	else
		return false;

}

And now the actual combat routine. This loops every 500m/s (half a second), checking whether or not the Ai can shoot, whether it needs to reload (here handled with a slightly cheaty resetting rifle ImageAmmo), and whether it is not already but needs to close and pursue the player. Shooting is delayed by a split second to help give the Ai time to aim, and the release of the trigger is delayed to allow a few of shots rather than just one.

function AIPlayer::canWeMakeBangBang(%this)
{
	if(!isObject(%this))
		return;
		
	if(%this.getState() $="Dead")
		return;
	
	//if we're out of rounds reload!
	if(%this.getInventory("LurkerAmmo") < 1)
		%this.setInventory("LurkerAmmo", 30);
		
	//the main Ai loop checking for shooting
	%attack = %this.attack;
	%loa = %this.LoA;
	
	//find that player!
	%enemy = %this.getNearestPlayerTarget();
	echo(%enemy SPC %enemy.getclassname());
	if(isObject(%enemy))			
		if(%enemy.getState() !$="Dead")
		{
			//can we see them? If not randompath
			%los = %this.playerLOS(%enemy);
			if(%los == true)
			{
				echo("can we make bang bang? YES!");
				//aim at the centre
				%this.setAimObject(%enemy, "0 0 1.0");
				%this.schedule(500, "canWeMakebangBang");
				if(%attack == 0 && %loa > 1)
				{
					//we need to break off our and move towards the player
					%this.stop();
					%this.currentNode = 0;
					%this.targetNode = 0;
	
					%this.attack = 1;
					
					navPath1.from = %this.getPosition();
					navPath1.to = %enemy.getPosition();
					navPath1.plan();
	
					%this.followPath("MissionGroup/navPath1", 1024);
				}
				
				//and finally open up the swine! Just a little pause to help with aiming
				%this.schedule(200, "aiShoot");
				return;
			}
		}
	
	//nope ...
	if(%loa < 3 && %attack == 1)
		%this.attack = 0;
		
	%this.clearAim();
	%this.schedule(500, "canWeMakebangBang");
	echo("can we make bang bang? No");
}

function AIPlayer::aiShoot(%this)
{
echo("SHOOT!");
   %this.setImageTrigger(0, true);
   %this.schedule(%this.shootingDelay, "aiStopShoot");
}

function AIPlayer::aiStopShoot(%this)
{
   %this.setImageTrigger(0, false);
}

The Ai aims for the centre of the Player, not the best when spotting targets by the visibility of their head. The stock weapons are somewhat inaccurate in Ai hands and could do with some customizations such as using an offset and not firstPerson (or entirely new dedicated Ai weapon versions), but none of these things have been changed here.

Lastly, we require to start the whole process of spawning the Ai when the game starts. In "scripts/server/gameCore.cs", add the following right at the bottom of the "startGame" function.

function GameCore::startGame(%game)
{
//...

	//get THE GAME to schedule the startup of the Ai
	schedule(3000, Game, "randomSpawn");//yorks in
}

Save everthing, boot up Torque3D FPS Tutorial Project, host a multiplayer (or open the level via "World Editor" button again), choose "ChinaTown Day" level, and if you've done it right (check the console for errors if you're not using Torsion) ... and I haven't missed anything :P ... you should have something that works akin to the video below!




Remember, ChinaTown is a difficult environment for Ai to navigate so they will get jammed stuck every so often. With that in mind - Happy Dueling and show that Ai why you should never send a machine to do a man's job! 8D



============================

And there's more!

Surprise Part THREE with hot multiple Ai Deathmatch Action! Expand what you've already worked on to include many Ai battling it out in an every-man-or-bot-for-himself style Deathmatch gameplay. Who ever said function creep was bad!?

#1
03/23/2012 (9:21 pm)
Nice work! Great basic tutorial. Just wanted to add a few comments on the navigation. If you find characters getting stuck on corners a lot, try increasing the 'actorRadius' parameter. This might mean the navmesh doesn't get created at all in small areas, so reducing the player bounding box instead is a good option. And if you can, decrease the cell size to 0.2 or 0.1. Makes for a longer build and a denser navmesh, but it makes it much truer to the actual level geometry, and won't cut corners and so on.
#2
03/24/2012 (7:26 pm)
awesome, this ought to be part of "stock" t3d. and building in recast was SO, SO much easier than I had feared. Great resources, thanks to both of you guys!
#3
03/25/2012 (3:08 pm)
Seriously, can you guys really get any better?? (outside of buying a pre-made game) You guys have taught me so much since I started messing with Torque back in '07', Hopefully soon I can get a copy of the full engine code for Torque3D 1.2. TGE1.5.2 still works great and somethings I've learnt to port back for here, I suggest any new person to pay attention and don't be afraid to try new things and learn from these masters who willing give their time to teach us so well. Ask questions, but first try untill your ears bleed and this community will help, trust me I know and I'm fully hooked on Torque and guys like Steve and Daniel should get paid for their passion to help for nothing at all. Thanks again Steve for all your help and maybe one day I can help this community as much as its helped me.......
#4
03/28/2012 (12:45 pm)
Yup - these guys rock.