Game Development Community

dev|Pro Game Development Curriculum

Tactics-Action Hybrid Game Tutorial Part9: Ai Combat

by Steve Acaster · 05/10/2011 (8:33 am) · 4 comments

Back to Part8: Custom Ai

Now we have our Allied Team members, we need an opposition. First, the theory.

Ai will respond in 2 ways:
1: Actively - select a player and move/shoot during their team's phase of a turn (eg: the Client moves his team members during the Ally Team Phase of the Turn)

2: Passively - it is the opposing team's Phase of the Turn, and the passive Ai team members will attempt to shoot at any moving targets. (eg: as in Valkyria Chronicles, the static Enemy Team members will shoot at active Allied Team memebers when they are moving)

Let's deal with "passive" Ai thinking initially, as this will affect both Ally and Enemy Teams when it is not their active Phase of the Turn. Open up scripts/server/aiplayer.cs, we need to edit our custom spawning function. We want to make sure that the new Ai has a timer for scheduling checks during their passive Phase, and to start them off with a passive thought routine if the spawn into the applicable phase.

function AIPlayer::tacticsSpawn(%name, %spawnPoint, %team)
{
//...

   %player.hasFired = 0;
   
   %rand = GetRandom(2000, 2500);//yorks new
   %player.timer = %rand;//yorks new
   
//...

//yorks new start		
	if(isObject($AllyList) && %team == 1)
	{
		$AllyList.add(%player, %playerType);
		
		if(isObject(turnManager))
			if(turnManager.enemyPhase == 1)
				%player.schedule(%player.timer, "passiveThink");
		
	}
		
	if(isObject($EnemyList) && %team != 1)
	{
		$EnemyList.add(%player, %playerType);

		if(isObject(turnManager))
			if(turnManager.enemyPhase == 0)
				%player.schedule(%player.timer, "passiveThink");
	}
	//yorks end new!
	
   return %player;
}

For the Ai's passive thought routine, we want them to try and attack only the hostile team's active playerObject, and only when they are moving and not when they are shooting. The Client controlled team has action variables derived from mouse input to declare whether or not they are in "movement mode" (and thus a target for passive attack). For the Enemy ai team we will simply check the selected playerObject's velocity to determine whether or not it is moving. We also need to check to see whether the Turn/Phase has changed and thus abort the passive thinking routine.

function AIPlayer::passiveThink(%this)
{
	if(%this.getState() $="Dead")
		return;
		
	echo(%this.getname() @ " passiveThink");

	if(%this.team != 1)
	{
		//must be enemy team member
		
		//check if the turn has changed
		//for AI enemy team they need to go into activeThink mode
		if(turnManager.enemyPhase == true)
			return;
		
		//we only attack the active player
		%target = localClientConnection.player;
		
		//can only shoot at active hostile playerObject in action = move
		if(%target.action != 1)
			%target = 0;
	}
	else
	{
		//must be an allied team member
		
		//check if the turn has changed
		//for Human Ally team they need to go back to being Client Controlled
		if(turnManager.enemyPhase == false)
			return;
			
		%target = turnManager.activeEnemy;
		
		if(isObject(%target))
		{
			if(%target.getVelocity() $="0 0 0")
				%target = 0;
		}
		else
		{
			%target = 0;
		}
	}
	
	if(%target != 0)
	{
		if(%this.targetClearView(%target) == true)
		{
			//distance check - Client can only shoot to 100 units so Enemy Ai is the same
			%dist = VectorDist(%me, %you);
			
			if(%dist < 100)
			{
				%this.setAimObject(%target, "0 0 1.5");//aim a little up
				%this.schedule(200, "autoShoot");//to help aiming delay
			}
		}
		else
		{
			//not able to shoot
			%this.clearAim();
		}
	}
		
	//And loop
	%this.schedule(%this.timer, "passiveThink");
}

The key points to note here is a new set of functions to help with Ai automated targeting (eg: targetClearView, autoShoot) and the variable "activeEnemy" which will return the equivalent of the selected playerObject for the Ai controlled Enemy Team.

function AiPlayer::autoShoot(%this)//automated shooting
{
	if(%this.getState() $="Dead")
		return;
		
	echo(%this.getname() @ " autoShoot");
	// Tell our passive AI object to fire its weapon
	%this.setImageTrigger(0, true);
			
	// Stop firing in 50 milliseconds
	%this.schedule(50, "setImageTrigger", 0, 0);
	%this.schedule(100, "clearAim");
}

function AIPlayer::TargetClearView(%this, %target)
{
	if(!isObject(%target))
		return false;
   
	%searchMasks =   $TypeMasks::VehicleObjectType | 
	$TypeMasks::PlayerObjectType | 
	$TypeMasks::TerrainObjectType | 
	$TypeMasks::StaticTSObjectType |  
	$TypeMasks::StaticShapeObjectType;
		
	%me = %this.getEyePoint();
	%you = %target.getEyePoint();
		
	//check for Line OF Sight
	%clear = containerRayCast(%me, %you, %searchMasks, %this);
			
	if(%clear == 0 || %clear == %target) 
		return true;   // path is clear
	else 
		return false;
}

Now that we have both of Team's Ai capable of defending themselves when it is the opponents phase, we'll sort out the slightly trickier task of having the Ai Enemy Team act in the same way during their active Phase as the Ally Team does when it is controlled by the Client. The theory is:

1: Each member of the Enemy Team get's a chance to be the active playerObject one after another
2: Movement first - check whether they can move (energy level) and whether they want to move (have a goal)
3: Shooting - attack the nearest target in view
4: Set the active playerObject to the next team member - if all team members have been active, end the Phase and start a new Turn.

In scripts/server/gameTorqueTactics.cs we need to create this new arrayObject to manage the Enemy Phase and add a variable for the current active Enemy playerObject to "turnManager".

function TorqueTacticsGame::startGame(%game)
{
//...
	
	if(!isObject($EnemyList))
	{
		$EnemyList = new arrayobject();
		MissionCleanup.add($EnemyList);
	}
	
	if(!isObject($EnemyManager))// <--- yorks add this object
	{
		$EnemyManager = new arrayobject();
		MissionCleanup.add($EnemyManager);
	}
	
	if(!isObject(turnManager))
	{
		new ScriptObject(turnManager) 
		{
			enemyPhase = 0;//not the enemies turn
			turnNum = 0;//start the game at the beginning!
			victory = 0;//victory not achieved - we've only just started
			activeEnemy = 0;// <--- yorks new!
		};
		MissionCleanup.add(turnManager);

//...

}

function TorqueTacticsGame::endGame(%game)
{

//...

	if(isObject($EnemyList))
	{
		$EnemyList.empty();//empty first
		$EnemyList.delete();//then delete
	}
	
	if(isObject($EnemyManager))// <--- yorks add this cleanup
	{
		$EnemyManager.empty();//empty first
		$EnemyManager.delete();//then delete
	}
	
	if(isObject(turnManager))
	{	
		TurnManager.delete();
		echo("Deleting Turn Management System");
	}

   parent::endGame(%game);

//...

   }
}

Now we need to manage the Turns and Phases. Back in Part7 we created a new server command function to deal with starting a "NewTurn" so that the Ally Team could move and shoot again. Now we need to add the Enemy Team to this to start their "passiveThink". In scripts/server/commands.cs edit:

function serverCmdNewTurn(%client)
{
//...

		%wpn.UpdateWeaponHud(%start, %slot);
		
		//and startup the Enemy Team Passive Turn - yorks add start
		%num = $EnemyList.count();
		if(%num > 0)
		{
			for(%x=0; %x < %num; %x++)
			{
				%ai = $EnemyList.getkey(%x);
			
				if(%ai.getVelocity() !$="0 0 0")//this should never happen but safety first ;)
					%ai.stop();
		
				%ai.schedule(%ai.timer, "passiveThink");
			}
		}//yorks end add
	}
	else
	{
	    MessageBoxOK( "You Have Been Defeated", "Game Over", "disconnect();");
	}
}

We need to do the same for the Ally Team when it is passive and startup the Enemy Team's active Phase after the Ally Phase is over. As there is a fair bit of change, feel free to replace the whole function.

function serverCmdTurnOver(%client)
{
	%count = $AllyList.count();
	if(%count > 0)
	{
	
		if(isObject(TurnManager))
		{
			TurnManager.enemyPhase = 1;
			echo("Enemy Team Turn");

			messageAll('MsgNewTurn', '\c4Turn %1 Enemy Phase', TurnManager.turnNum);
			
			if(turnManager.victory == 1)
			{
				MessageBoxOK( "You Have Defeated The Enemy", "Huzzar! You Won The Game!", "disconnect();");
				return;
			}
		}
		else
		{
			echo("There is no Turn Management System in place - something has gone horribly wrong!");
			MessageBoxOK( "There is no Turn Management System in place - something has gone horribly wrong!", "Quit", "disconnect();");
		}
	
		//sort out the player
		for(%i=0; %i < %count; %i++)
		{
			%bot = $AllyList.getkey(%i);
			
			if(%bot.getVelocity() !$="0 0 0")//this should never happen but safety first ;)
				%bot.stop();
		
			if( %bot.decal > -1 )//just checking
				decalManagerRemoveDecal( %bot.decal );
				
			%bot.schedule(%bot.timer, "passiveThink");
		}
		
		TacticsHud.setVisible(false);
		TurnHud.setVisible(false);
		
		//sort out the enemy turn
		%count = $EnemyList.count();
		if(%count > 0)
		{
			if(!isObject($EnemyManager))//this should never happen
			{
				$EnemyManager = new arrayobject();
				MissionCleanup.add($EnemyManager);
			}
			
			for(%i=0; %i < %count; %i++)
			{
				%ai = $EnemyList.getkey(%i);
	
				%ai.action = 0;
				%ai.hasFired = 0;
				%ai.restoreEnergy();
				
				$EnemyManager.add(%ai, %ai.goal);
			}

			schedule(2000, turnManager, "commandToServer", 'enemyPhase');
		}
		else
		{
			//make a slight pause before starting a fresh turn
			schedule(2000, turnManager, "commandToServer", 'NewTurn');
		}
	}
	else
	{
	    MessageBoxOK( "You Have Been Defeated", "You Lost The Game", "disconnect();");
	}
}

Finally in commands.cs we activate the "EnemyManager".

function serverCmdenemyPhase(%client)
{
	echo("\c2 -> command to server -> enemyPhase");
	%count = $EnemyManager.count();
	if(%count > 0)
	{
		%select = $EnemyManager.getKey($Enemymanager.moveFirst());
		turnManager.activeEnemy = %select;
		%select.schedule(%select.timer, "activeThink");
	}
	else
	{
		turnManager.activeEnemy = 0;
		//make a slight pause before starting a fresh turn
		schedule(2000, turnManager, "commandToServer", 'NewTurn');
	}
}

The "enemyManager" works by adding all Enemy Team members to it's array (in "turnOver") and then working through the active thinking routine of the members one at a time, deleting them from the array when they are finished, before moving on to the next. When the arrayObject is empty, all Enemy Team members will have had a chance to move and shoot, and the Enemy Phase will end, and a new Turn begin with the Ally Team's Phase (and thus control for the Client is reestablished).

Now back to scripts/server/aiplayer.cs to add the functions for a selected Enemy Team playerObject (activeEnemy) during their active Phase.

function AIPlayer::activeThink(%this)
{
	//Ai Enemy's turn to be active
	echo(%this.getname() @ " Enemy activeThink");
	
	//need to check for:
	//can we move? Energy > 0
	//do we want to move?
	//are we already moving?
	//can we shoot? hasFired == 0
	//do we have a target?
	
	if(%this.getState() $="Dead")
	{
		%k = $EnemyManager.getindexfromkey(%this);
   		$EnemyManager.erase(%k);
		
		turnManager.activeEnemy = 0;
		schedule(1000, turnManager, "commandToServer", 'enemyPhase');
		
		return;
	}
	
	if(turnManager.activeEnemy != %this)
	{
		echo("\c0******** " @ %this.getname @ " ends active turn");
		schedule(1000, turnManager, "commandToServer", 'enemyPhase');
		return;
	}
	
	%energy = %this.getEnergyLevel();
	
	if(%energy > 0)
	{
		//we have energy to move

		//do we have a goal to move to?
		if(isObject(%this.goal))
		{
			%range = VectorDist(%this.getPosition(), %this.goal.getPosition());
		
			if(%range > 1)
			{
				echo(%this.getname() SPC %this.getenergyLevel());
				%this.setmovedestination(%this.goal.getposition());
				
				//if the client's playerObject usese up energy, so does the Ai
				%this.decEnergy();
				
				%this.schedule(%this.timer, "activeThink");
				return;
			}
			else
				%this.goal = 0;
		}
	}
	
	if(%this.hasFired == 0)
	{
		%target = %this.AttackTarget(%obj);
		
		if(%target != 0)
		{
			%this.setAimObject(%target, "0 0 1.5");//aim a little up
			%this.schedule(200, "autoShoot");//to help aiming delay
			%this.hasFired = 1;
		}
	}
	
	//if we get down here we must be done!
	//remove us from the EnemyManager and get the next Ai
	%k = $EnemyManager.getindexfromkey(%this);
   	$EnemyManager.erase(%k);
	turnManager.activeEnemy = 0;
	
	%this.schedule(%this.timer, "activeThink");
}

There's a little list of comments in the above code which explains succinctly how the theory works. You'll also notice that you need a new "targeting" and "attackTarget" function. These simply find the closest available hostile playerObject and sets it as the target to attack with the single shot that the active playerObject has that turn.

function AIPlayer::Targeting(%this)
{
	%dist = 0;
	%index = -1;
	%mypos = %this.getposition();

	%count = $AllyList.count();
	for(%i=0; %i < %count; %i++)
	{
		%bot = $AllyList.getkey(%i);
		if(isObject(%bot))
		{
			if(%bot.getstate() !$="Dead")		
			{
				%tgtpos = %bot.getposition();		
				%tempdist = vectorDist(%tgtpos, %mypos);

				if(%tempdist < 100)//100 is max range for Client so same for Ai
					if(%tempdist < %dist || %dist == 0)
					{
						%dist = %tempdist;
						%index = %i;
					}
			}
		}
	}
	
	if(%index != -1)
	{
		%tgt = $AllyList.getkey(%index);
	 	return %tgt;
	}

	return -1;
}

function Aiplayer::AttackTarget(%this)
{
	%tgtid = %this.Targeting(%obj);
	
	if(%tgtid != -1 && isObject(%tgtid))//was tgt != 0 but -1 is what would get returned ...
		return %tgtid;
}

Part Ten: GamePlay Test

#1
05/10/2011 (9:04 am)
really cool, loving it so far.
#2
05/10/2011 (9:47 am)
These tutorials are so cool Steve that I have converted one of my game concepts to use this style of game play instead ... with your permission of course.

Love the style of play and have watched a few of the videos (not having seen this style before) ... so I am keen to make this happen.

Thanks again for all your efforts ... very cool indeed.
#3
05/10/2011 (10:07 am)
Quote:
I have converted one of my game concepts to use this style of game play instead ... with your permission of course.

That's what they're there for!

In case anyone has been following this whilst I've writing it, I've made a few alterations to some of the previous parts, mainly playGui, gameTorqueTactcs.cs, player.cs, ... maybe aiplayer.cs, commands.cs - mostly cleaning stuff up and making stuff better rather than wholesale replacements of code though.

Edited again - guess who missed off the "attackTarget" function at the bottom of this page ... doh!
#4
05/10/2011 (10:43 pm)
Neat write up Steve, its always fun to read your punny typings...