ChinaTown Multi AI DeathMatch With Recast Pathfinding Part 3
by Steve Acaster · 03/25/2012 (6:36 pm) · 12 comments
Part Two
-------------------------------
This wasn't actually planned, it just occurred to me that it wouldn't be much of a leap forward to do it. And so I bring you ... Part THREE! This Time It's War/Pesonal/TeaTime! (delete cheezy tagline as applicable).
Actually this time it's Multi-Ai DeathMatch.
This third - and definately final - part of the resource expects that you have completed the first two parts, and that you now have a lone Ai to duel it out with inside the stock Torque3D 1.2 ChinaTown map. Now we're going to add more Ai, so that there's a real every-man-for-himself battle on the go.
We're going to create an arrayObject, and list all of the available Ai and the playerObject there. This will be our "master" list for checking targets.
First up, let's spam Ai into the map. Open up "scripts/server/gameCore.cs" and find your previous custom script at the end of "startGame".
In the "multiSpawnAI" function, change the number 8 to however many Ai you want to spawn. Remember, ChinaTown is a small map and Ai are processor overhead, so don't throw in anything mad.
When the game loads up, it will now randomly spawn that number of Ai. The spawning system is the same, quite simple, and it's not going to bother to check for using duplicate spawnPoints.
Whilst we have "gameCore.cs" open, go down to the "loadOut" function and add:
Now that the human player has been added to the master "$botList", we also need to be able to remove that object (the player avatar) when it is killed. Open up "scripts/server/player.cs" and find the "Armor::onDisabled" function.
You might also want to reduce the time it takes for a corpse to fadeout and be deleted, as they can stack up in game.
Next up we need to do the same for the Ai. Open up "scripts/server/aiplayer.cs".
Find our custom "DemoPlayer::onDisabled" function and add this new part.
We're going to give each spawned Ai there own "NavPath" object, rather than use the one we placed in the map in Part One. This unique navpath will be deleted when they are killed and removed from the master targeting list; "$botList". We will add a function to create this navpath, and have it called by modifying our Ai spawn script.
And we need to update the "AiStartUp" to include the new path.
Also notice that we are changing the weapon which will be mounted in the "customAiSpawn" function, and giving the Ai a new version of the Lurker rifle that they will actually be able to hit something with occaissionally. Open up "art/datablocks/weapons/lurker.cs", find the "datablock ShapeBaseImageData(LurkerWeaponImage)" and copy and paste the whole thing at the bottom - making sure that you're pasting the correct FSM list and not the one for the grenade launcher! Next rename this copied datablock as "AiLurkerWeaponImage", and disable all the player relevant information.
EyeOffset is the real helper for the Ai, if the value is not "0 0 0", it will offset shooting from where it aims from. You can leave the rest of this huge FSM function which you have copied as it is, as it will use the same item and ammunition as the standard Lurker rifle. Remember you are editing the copy/paste not replacing the original! Or else the player won't have a rifle!
Now, back to "scripts/server/aiplayer.cs".
Previously the Ai had it easy finding the lone player, but now, each Ai is going to have decide which of many targets it will attack and/or purse. So we need to create some new functions to find both our closest other Ai/Player for aggressive hunting, and also decide what we can see.
With these two new functions we also update "aiDecide" to use them. Now our super aggressive Ai types will look for visible targets to purpse, and if they cannot find any they will "telepathically" hunt down the nearest. The standard aggression type will only try to purse visual targets and will default to a random goal in there absence.
And finally, we need a new way to decide who and how to shoot, so we modify "canWeMakeBangBang". We add in the new individual navpath system for hunting an opponent, and rely on finding the nearest Ai/player in view to shoot at. Shooting itself is now changed to immediately opening fire, and shooting only stops when we no longer have a target rather than the previous timed burst system.
Note: video is a bit laggy because I don't have a very modern or high-spec PC, but even with 8 Ai (and the player making an extra server object) it's still perfectly playable.
-------------------------------
This wasn't actually planned, it just occurred to me that it wouldn't be much of a leap forward to do it. And so I bring you ... Part THREE! This Time It's War/Pesonal/TeaTime! (delete cheezy tagline as applicable).
Actually this time it's Multi-Ai DeathMatch.
This third - and definately final - part of the resource expects that you have completed the first two parts, and that you now have a lone Ai to duel it out with inside the stock Torque3D 1.2 ChinaTown map. Now we're going to add more Ai, so that there's a real every-man-for-himself battle on the go.
We're going to create an arrayObject, and list all of the available Ai and the playerObject there. This will be our "master" list for checking targets.
First up, let's spam Ai into the map. Open up "scripts/server/gameCore.cs" and find your previous custom script at the end of "startGame".
//get THE GAME to schedule the startup of the Ai
//schedule(3000, Game, "randomSpawn");//yorks in
if(!isObject($botList))
{
$botList = new arrayobject();
MissionCleanup.add($botList);
}
multiSpawnAi();
}
function multiSpawnAi()
{
echo("Initial Spawning of Multiple AI");
%count = $botList.count();
if(%count < 8)
{
randomSpawn();
//and loop through until we have a load of Ai
schedule(300, Game, "multiSpawnAi");
}
}In the "multiSpawnAI" function, change the number 8 to however many Ai you want to spawn. Remember, ChinaTown is a small map and Ai are processor overhead, so don't throw in anything mad.
When the game loads up, it will now randomly spawn that number of Ai. The spawning system is the same, quite simple, and it's not going to bother to check for using duplicate spawnPoints.
Whilst we have "gameCore.cs" open, go down to the "loadOut" function and add:
//...
%player.mountImage(PistolImage, 0);
}
//yorks new at the end
//delete the array index for this dead Ai
%index = $botList.getIndexFromKey(%player);//shouldn't be here but let's check anyhow
if(%index == -1)
$botList.add(%player, 0);//zero value shows it's a player
}Now that the human player has been added to the master "$botList", we also need to be able to remove that object (the player avatar) when it is killed. Open up "scripts/server/player.cs" and find the "Armor::onDisabled" function.
function Armor::onDisabled(%this, %obj, %state)
{
//delete the array index for this dead Player so Ai don't keep trying to hunt him down
%index = $botList.getIndexFromKey(%obj);
if(%index != -1)
$botList.erase(%index);
//...You might also want to reduce the time it takes for a corpse to fadeout and be deleted, as they can stack up in game.
Next up we need to do the same for the Ai. Open up "scripts/server/aiplayer.cs".
Find our custom "DemoPlayer::onDisabled" function and add this new part.
function DemoPlayer::onDamage(%this,%obj, %delta)
{
if(%obj.getstate() $="Dead")
{
//new yorks!
%path = %this.path;
//delete the array index for this dead Ai
%index = $botList.getIndexFromKey(%obj);
if(%index != -1)
$botList.erase(%index);
//delete the Ai's path
if(isObject(%path))
if(%path.getClassname() $="navPath")
%path.delete();
//end of new yorks
//take finger off trigger
%obj.setImageTrigger(0, 0);
//and spawn a new enemy!
randomSpawn();
}
}We're going to give each spawned Ai there own "NavPath" object, rather than use the one we placed in the map in Part One. This unique navpath will be deleted when they are killed and removed from the master targeting list; "$botList". We will add a function to create this navpath, and have it called by modifying our Ai spawn script.
function AIPlayer::customAiSpawn(%spawn, %goal)
{
//create a new path for this Ai's personal use
%path = createNewPath();
// Create the demo player object
%player = new AiPlayer()
{
dataBlock = DemoPlayer;
position = %spawn.getTransform();
path = %path;
};
MissionCleanup.add(%player);
//add this Ai and his path to the $botList
$botList.add(%player, %path);
//are we moving to attack the player or are we moving towards a node
%player.attack = 0;
//are we stuck?
%player.stuck = 0;
//movement nodes
%player.currentNode = 0;
%player.targetNode = 0;
//pause a moment to let behind the scene stuff work out
%player.schedule(1000, "aiStartUp", %goal);
//give the Ai a rifle
%player.mountImage("AiLurkerWeaponImage", 0);
return %player;
}
function createNewPath()
{
%path = new NavPath()
{
from = "0 1 1";
to = "10 1 1";
mesh = "TheNavMesh";
};
MissionCleanup.add(%path);
return %path;
}And we need to update the "AiStartUp" to include the new path.
function AIPlayer::aiStartUp(%this, %goal)
{
if(!isObject(%this))
return;
if(%this.getState() $="Dead")
return;
%path = %this.path;//temporary alias for our path
//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.getNearestTarget();//get nearest other playerObject, human or Ai
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
%this.path.from = %start;//add variable
%this.path.to = %end;//add variable
//use temporary alias and plan path
%path.plan();
//check to see if we can shoot
%this.canWeMakeBangBang();
//and go down our new path
%this.followPath(%path, 1024);
}Also notice that we are changing the weapon which will be mounted in the "customAiSpawn" function, and giving the Ai a new version of the Lurker rifle that they will actually be able to hit something with occaissionally. Open up "art/datablocks/weapons/lurker.cs", find the "datablock ShapeBaseImageData(LurkerWeaponImage)" and copy and paste the whole thing at the bottom - making sure that you're pasting the correct FSM list and not the one for the grenade launcher! Next rename this copied datablock as "AiLurkerWeaponImage", and disable all the player relevant information.
datablock ShapeBaseImageData(AiLurkerWeaponImage)
{
// Basic Item properties
shapeFile = "art/shapes/weapons/Lurker/TP_Lurker.DAE";
//shapeFileFP = "art/shapes/weapons/Lurker/FP_Lurker.DAE";
emap = true;
imageAnimPrefix = "Rifle";
//imageAnimPrefixFP = "Rifle";
// Specify mount point & offset for 3rd person, and eye offset
// for first person rendering.
mountPoint = 0;
firstPerson = false;
useEyeNode = false;
animateOnServer = false;
eyeOffset = "0.0 0.6 -0.1"; // right/left forward/backward, up/down
//...EyeOffset is the real helper for the Ai, if the value is not "0 0 0", it will offset shooting from where it aims from. You can leave the rest of this huge FSM function which you have copied as it is, as it will use the same item and ammunition as the standard Lurker rifle. Remember you are editing the copy/paste not replacing the original! Or else the player won't have a rifle!
Now, back to "scripts/server/aiplayer.cs".
Previously the Ai had it easy finding the lone player, but now, each Ai is going to have decide which of many targets it will attack and/or purse. So we need to create some new functions to find both our closest other Ai/Player for aggressive hunting, and also decide what we can see.
function AIPlayer::getNearestTarget(%this)
{
%index = -1;
%enemyID = -1;
%tempDist = 0;
%dist = 9000;
%ourPos = %this.getPosition();
%count = $botList.count();
for(%i = 0; %i < %count; %i++)
{
//get the ID from the array
%enemy = $botList.getKey(%i);
//don't target ourself!
if(%enemy != %this)
{
%enemyPos = %enemy.getPosition();
%tempDist = VectorDist(%enemyPos, %ourPos);
if (%tempDist < %dist)
{
%dist = %tempDist;
%index = %i;
%enemyID = %enemy;
}
}
}
return %enemyID;//return the closest enemy or else return -1;
}
function AIPlayer::getNearestTargetInView(%this)
{
%index = -1;
%enemyID = -1;
%tempDist = 0;
%dist = 9000;
%ourPos = %this.getPosition();
%count = $botList.count();
for(%i = 0; %i < %count; %i++)
{
//get the ID from the array
%enemy = $botList.getKey(%i);
echo(%this @ " loop finds " @ %enemy @ " at " @ %i);
//don't target ourself!
if(%enemy != %this)
{
//can we see them? If not move on
%los = %this.playerLOS(%enemy);
if(%los == true)
{
%enemyPos = %enemy.getPosition();
%tempDist = VectorDist(%enemyPos, %ourPos);
if (%tempDist < %dist)
{
echo(%this @ " finds closer enemy " @ %enemy @ " than previous " @ %enemyID);
%dist = %tempDist;
%index = %i;
%enemyID = %enemy;
}
else
{
echo(%this @ " enemy not closer " @ %enemy @ " than " @ %enemyID);
}
}
}
}
echo(%this @ " closest enemy = " @ %enemyID);
return %enemyID;//return the closest enemy or else return -1;
}With these two new functions we also update "aiDecide" to use them. Now our super aggressive Ai types will look for visible targets to purpse, and if they cannot find any they will "telepathically" hunt down the nearest. The standard aggression type will only try to purse visual targets and will default to a random goal in there absence.
function AIPlayer::aiDecide(%this)
{
if(!isObject(%this))
return;
if(%this.getState() $="Dead")
return;
%path = %this.path;//temporary alias for our path
%start = %this.getPosition();
//if we are super aggressive just go at them
if(%this.LoA == 3)
{
//find an enemy in view
%enemy = %this.getNearestTargetInView();
if(isObject(%enemy))
{
%end = %enemy.getPosition();
%this.path.from = %start;
%this.path.to = %end;
%path.plan();
%this.followPath(%path, 1024);
return;
}
//find any enemy
%enemy = %this.getNearestTarget();
if(isObject(%enemy))
{
%end = %enemy.getPosition();
%this.path.from = %start;
%this.path.to = %end;
%path.plan();
%this.followPath(%path, 1024);
return;
}
}
//can we see the player?
if(%this.LoA == 2)
{
//find enemy in view
%enemy = %this.getNearestTargetInView();
if(isObject(%enemy))
{
%end = %enemy.getPosition();
%this.path.from = %start;
%this.path.to = %end;
%path.plan();
%this.followPath(%path, 1024);
return;
}
}
//randomPath
%this.randomPath();
}And finally, we need a new way to decide who and how to shoot, so we modify "canWeMakeBangBang". We add in the new individual navpath system for hunting an opponent, and rely on finding the nearest Ai/player in view to shoot at. Shooting itself is now changed to immediately opening fire, and shooting only stops when we no longer have a target rather than the previous timed burst system.
function AIPlayer::canWeMakeBangBang(%this)
{
if(!isObject(%this))
return;
if(%this.getState() $="Dead")
return;
%path = %this.path;
//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.getNearestTargetInView();
if(isObject(%enemy))
{
echo(%this @ " attacks " @ %enemy);
echo("can we make bang bang? YES!");
//aim at the centre and a little up
%this.setAimObject(%enemy, "0 0 1.5");
if(%attack == 0)
{
%this.attack = 1;
%this.path.from = %this.getPosition();
%this.path.to = %enemy.getPosition();
%path.plan();
%this.followPath(%path, 1024);
}
//and open up the swine! Just a little pause to help with aiming
%this.aiShoot();
%this.schedule(500, "canWeMakebangBang");
return;
}
//nope ...
if(%loa < 3 && %attack == 1)
%this.attack = 0;
%this.aiStopShoot();
%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");
}And that's it. As long as everything has gone okay, you should be able to boot up ChinaTown, spawn in game and by the time you've moved into the centre of the map you should already be hearing the Ai going at each other in true Deathmatch style.Note: video is a bit laggy because I don't have a very modern or high-spec PC, but even with 8 Ai (and the player making an extra server object) it's still perfectly playable.
About the author
One Bloke ... In His Bedroom ... Making Indie Games ...
#2
03/28/2012 (1:23 pm)
What, no Team Deathmatch?!? No Kinect team mate voice commands? No driveable vehicles?
#3
03/29/2012 (11:03 am)
Lol.... this is a nice resource to get some basic AI going. I have something similar in place for mine... except my AI is much... eviler.
#4
03/30/2012 (6:23 am)
Bring back Doom "AI." If you make a sound, they will follow.
#5
03/30/2012 (2:57 pm)
Hey, Steve, can you send me an email to the email address listed in my profile? I want to talk to you about testing some code for me, if possible.
#6
03/31/2012 (3:56 am)
Thanks Steve, these 3 resources were very useful.
#9
10/08/2012 (2:39 pm)
Excellent as always steve, now if i can figure out how to make my ai seek cover and maybe crouch when shot at???
#10
Daniel will likely add the updated how-to details here as well->
[url]https://github.com/eightyeight/Torque3D-wiki/blob/master/Using-Recast-Navigation.md [/url]
12/28/2013 (9:37 pm)
If you try this with T3D 3.5 to use the built in re-cast support, keep in mind its now %path.replan() instead of %path.plan().Daniel will likely add the updated how-to details here as well->
[url]https://github.com/eightyeight/Torque3D-wiki/blob/master/Using-Recast-Navigation.md [/url]
#11
Some research into this is needed.
01/23/2014 (12:37 pm)
@Jeff: There's more than just that changed apparently. This resource no longer functions with just a simple change from plan() to replan(). Getting lots of errors regarding out of bounds index and path length = 0.Some research into this is needed.
#12
In function createNewPath() add:
allowWalk = true;
to the list of properties in the new NavPath() declaration.
It seems this defaulted to false which caused the navigation computation to think there was no way to get anywhere returning zero nodes.
I also changed all the instances of path.getCount() to path.size(), not sure it was needed but it works now for me. Also it appears to once again be path.plan() and not .replan()
This was using version 3.10
Great resource and hope this helps!
10/16/2018 (8:23 am)
I just revisited this resource after many years away from Torque for some prototyping.In function createNewPath() add:
allowWalk = true;
to the list of properties in the new NavPath() declaration.
It seems this defaulted to false which caused the navigation computation to think there was no way to get anywhere returning zero nodes.
I also changed all the instances of path.getCount() to path.size(), not sure it was needed but it works now for me. Also it appears to once again be path.plan() and not .replan()
This was using version 3.10
Great resource and hope this helps!

Torque 3D Owner orsteam