T3D/Tutorials/SimpleFPSTutorial/Part9

From TDN

SIMPLE FPS TUTORIAL for Torque 3D



Back to Part Eight: Create A Custom AIPlayer

Part NINE




Simple Ai Combat Scripts:

We're going to give the custom bot a rudimentary thought process with a list of circumstances to check for to determine whether or not it should try shooting at the player.

1. Is there a Player? Not much point in trying to shoot something which is not there. 2. Is the Player in range of the weapon the bot has? No point shooting if your bullets won't reach. 3. Is the Player in view? No point shooting if an object is blocking our line of sight.

Firstly, add a function to allow the bot to loop checks, let's call it "botThink" and schedule it for every 1 second. In the "spawnBot" function, above "return %player;" we'll call this new function for the first time, giving a slight delay to make sure that the bot is in place.

	%player.equipBot(%weapon);
   
	%player.schedule(200, "botThink");// <-----start think routine
   
	return %player;
}


In the new "botThink" function we shall start with a check to determine whether or not the bot should be thinking at all because "deadmen don't think". If the Player has killed the bot, then continuing to loop a unnecessary function is a waste of CPU resources. Rather than clog up this new function with all of the checks for initiating combat, we'll create a second new function called "combatMode". Thus the "botThink" routine will loop every second, calling the function to check for combat.

function AIPlayer::botThink(%this)
{
	if(%this.getState() $= "Dead")
		return;
	
	%this.combatMode();
	echo(%this.getname() @ " thinking");
		
	%this.schedule(1000, "botThink");
}


The function "combatMode" will collect together all of the the results from the other functions that will be used to determine whether the bot should be shooting. The first of which is "targetPlayer". "targetPlayer" first checks against the ClientGroup to see how many clients (Players) there are - we know that there is only one, it's a single player FPS tutorial - and if it find a client who has a "Player object" (the physical player model in a game - a disembodied camera (CTRL C) is still a client, but there's no point trying to shoot that) it will send the Player model's ID numbers back to be processed in the "combatMode" function. If there is no Player object, it will send "-1" and the "combatMode" function will end until the next sheduled check from "botThink".

function AIPlayer::targetPlayer(%this)
{
	%count = ClientGroup.getCount();
	for(%i = 0; %i < %count; %i++)
	{
		%client = ClientGroup.getObject(%i);
		if (%client.player $= "" || %client.player == 0)
        return -1;
		
		%target = %client.player;
		echo(%this.getname() @ " has target " @ %target);
		return %target;
	}
}


Providing that there is a Player Object, next "combatMode" needs to determine if it is in range of whichever weapon it is using. This is where the "%this.range;" variable that is set in "equipBot" comes into use. We check the distance between the bot and it's target with a "VectorDist" function, and then check that distance with the bot's "range", before sending a bool to "combatMode" (true if distance is shorter than the bot's range, false if longer).

function AIPlayer::checkRange(%this, %target)
{
	%rangeFrom = %this.range;
	
	%fromMe = %this.getPosition();
	%toYou = %target.getPosition();
	%rangeTo = VectorDist(%fromMe, %toYou);

	if(%rangeTo > %rangeFrom)
	{
		echo(%this.getname() @ " target too far");
		return false;
	}
	else
	{
		echo(%this.getname() @ " target in range");
		return true;
	}
}


Finally, the bot needs to check that it has a "line of sight" (LOS) so that it can fire at it's target without hitting anything else in the way. We use a "raycast" to check the LOS between the bot's eye and target's eye.

function AIPlayer::clearShot(%this, %target)
{
	
	%ISpy = %this.getEyePoint();
	%YouSpy = %target.getEyePoint();

	%search = containerRayCast(%ISpy, %YouSpy, $TutorialBot::Obstacles, %this.getID());

	%impactID = firstWord(%search);
   echo("impactID of targeted object = " @ %impactID);
  
	if(%impactID == %target || %impactID == 0) //NOTE: see below
	{
		echo(%this.getname() @ " has a clearShot");
		return true;
	}
	else
	{
		echo(%this.getname() @ " has no shot");
		return false;
	}
}


NOTE: Depending on the artwork of custom player models and their boundingbox size, sometimes their eyeNode can be outside of their collision boundingbox and thus the raycast will fall short of striking the collisionbox. So to make sure this doesn't happen we also accept "zero" as a clean shot. "zero" signifies that nothing has got in the way to the target. The "CombatMode" function checks that the target is in range before attempting the "clearShot" function - thus we know that the range is good already, so the raycast finding "zero" is safe.


The "raycast" will check against a list of known objects types to see if the ray hits one of them, and has a final check to ignore one object which should (in these circumstances) be used to ignore itself in case part of it's own body gets in the way of it's eyeNode. To make the list of object types the ray can collide with, we'll make a global "Typemask" called "Obstacles".

$TutorialBot::Obstacles = 
  $TypeMasks::VehicleObjectType |
  $TypeMasks::PlayerObjectType |      
  $TypeMasks::TerrainObjectType |     
  $TypeMasks::StaticTSObjectType |    
  $TypeMasks::StaticShapeObjectType |
  $TypeMasks::InteriorObjectType |
  $TypeMasks::ForestObjectType;   


Place this at the top of the "TutorialBot.cs" file, and make sure it is NOT inside any other function. Just to note: PlayerObjectType includes AiPlayers as well as Players, whilst an AiObjectType would not include human Players. Back to the "combatMode" function.

function AIPlayer::combatMode(%this)
{
	%targetID = %this.targetPlayer(%target);

	if(%targetID != -1)
		if(%this.checkRange(%targetID) == true)
			if(%this.clearShot(%targetID) == true)
				%this.attackTarget(%targetID);
}


So, combatMode get's sent the Player's ID from "targetPlayer", if it is not "-1" the Player's ID is sent to "checkRange" which will return a true/false as to whether the Player is inside the bot's weapon range. If true, the Player's ID is once again sent to check whether the LOS is clear and "clearShot" sends back a true/false. If this is true, the bot has finally decided it can shoot and sends the target's ID to a new function "attackTarget".

"attackTarget" set's the target's ID as the object to aim at using the stock function "setAimObject". This automatically find the target's position and aims the bot at it. We give this function an offset of 1.5 units, because the position is calculated at the base of the target, and we don't want to shoot at it's feet. To keep things "simple", rather than use another raycast to check that the bot is aiming at the target, we leave a slight gap in time between aiming and firing to make certain that the bot has turned to aim in the correct direction, before calling the "shoot" function. From the "shoot" function the bot pulls the trigger on it's weaponImage and then schedules a ceasefire using it's "shootingDelay" parameter. If it doesn't schedule the ceasefire, if the bot had the "semiauto" weapon it would never allow the weapon to reload and thus could only ever fire one shot, and if the bot had the "fullauto" weapon it would never stop firing regardless of whether it could hit the target or not.

function AIPlayer::attackTarget(%this, %target)
{
	echo(%this.getname() @ " attacking!");
	%this.setAimObject(%target, "0 0 1.5");
	%this.schedule(150, "shoot");
	// a slight delay to make sure we're aiming at the target
	//you could always use a raycast to see if the target is in the bot's sights instead
}

function AIPlayer::shoot(%this)
{
	%this.setImageTrigger(0, true);
	
	%this.schedule(%this.shootingDelay, "ceasefire");
}

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


And that's it for scripting our custom bot and it's simple combat routine.

We want to spawn our bot, but don't want to type it into the console every time, so we're going to make a trigger. Copy the "game/scripts/server/triggers.cs" file and call the new copy "TutorialTriggers.cs". Change every instance of "DefaultTrigger" to "TutorialTrigger", and delete all the lines which start with "Parent". Finally add a new datablock for "TriggerData" so we can adjust the timing of the "onTick" checks for checking when the Player is inside the trigger.

datablock TriggerData(TutorialTrigger)
{
   tickPeriodMS = 1000;
};

function TutorialTrigger::onEnterTrigger(%this,%trigger,%obj)
{
//activate when player enters
}

function TutorialTrigger::onLeaveTrigger(%this,%trigger,%obj)
{
//activate when player leaves
}

function TutorialTrigger::onTickTrigger(%this,%trigger)
{
//activate when player is inside - every 1000 m/s
}


Don't forget to exec this new file in "game/scripts/server/scriptExec.cs".

//custom scripts
exec("./semiauto.cs");
exec("./fullauto.cs");
exec("./gameSFPST.cs"); // Overrides GameCore with Simple FPS Tutorial functionality.
exec("./TutorialBot.cs");
exec("./TutorialTriggers.cs");


(re)Start up T3D and load your Tutorial level. Open up the World Editor (F11) and from the toolbar press the "toggleVisibilityModes" button and select "Render Triggers". When we place out trigger in-game we will be able to see it (obviously in a final game, you wouldn't want the player seeing your triggers, which is why they are invisible by default.

Looking at the ground between the player and the 3 items in front of him, go to "Scene Tree-> Library-> Level-> Level" and create a trigger. Call it "trig1", from the "data block" drop down list select our new "TutorialTrigger" and press create new. You should now have a yellow box, set it's size to be bigger by editing the "scale" in the Object Inspector, to around "30 5 5". Position the now much larger trigger over the row of items which we placed earlier and also make sure that the trigger penetrates the ground slightly, making sure that the player can't get around the side of the trigger and avoid it in any way.

Now we're going to script our bot's spawn by editing the trigger's "enterCommand". First we want to check whether the bot called "bot1" already exists, because we don't want 2 objects with the same name. If it doesn't already exist we want to spawn it.

if(!isObject(bot1))
   aiplayer::spawnBot(bot1, botSpawn1, semiauto);


Spawn Trigger

Press "okay" and then let's save and "Play Game". Walk the player through the trigger and pick up the weapon and ammo. Now walk up to the corner and peak around until you can see the bot. He should start shooting at you. Shoot back. When you've killed him, wait for him to fade out and delete, then walk back into the trigger and he should respawn.

Currently any object which can interact with a trigger will set the trigger off, but we really only want the player to activate our triggers. Open up the World Editor (F11) and reselect "trig1" and edit the "enterCommand".

%checkclass = %obj.getclassname();
if(%checkclass $= "Player")
   if(!isObject(bot1))
      aiplayer::spawnBot(bot1, botSpawn1, semiauto);

Now the bot will only spawn when the (human) Player enters the trigger and not when anything else enters it.



Part Ten: Simple Pathed Bots