Game Development Community

dev|Pro Game Development Curriculum

Tactics-Action Hybrid Game Tutorial Part7: Tactics GameType

by Steve Acaster · 05/08/2011 (3:59 pm) · 5 comments

Part Six: Fix Gui Calls

It has been mentioned earlier that the default gameType for stock T3D is "Deathmatch". If a mission/level has theLevelInfo object with a "Dynamic Field" -
gameType = "TorqueTactics";
- this information will override the default/stock gameType. So you have a choice, add this "Dynamic Field" to every mission file, or change the default gameType en masse. To do that, open up scripts/server/gameCore.cs and edit:

function loadMissionStage2()
   {
//...

      // Create Game Objects
      // Here begins our gametype functionality
      $Server::MissionType = theLevelInfo.gameType;  //MissionInfo.gametype;
      //echo("\c4 -> Parsed mission Gametype: "@ theLevelInfo.gameType); //MissionInfo.gametype);
      if ($Server::MissionType $= "")
         $Server::MissionType = "TorqueTactics";//"Deathmatch"; //Default gametype, just in case//yorks change!

//...

Either way, it's up to you. Now let's create this new gametype. In server/scripts copy and paste the file gameDM.cs before renaming the copy gameTorqueTactics. Open this new file and change every reference of "Deathmatch" to "TorqueTactics".

Initially let's change create the important pieces to establish the gameType on start. It's a turnbased game, so we'll create a "Turn Manager" to keep track of a few variables such as the number of turns, whether it's the enemy teams action phase, and whether the victory conditions have been met. We'll also create arrays to keep track of the members of each team (Ally Team for the Client and Enemy Team for the Ai). Finally, we'll make sure that we have a fail safe for having the action buttons and endTurn button visible. They should be, but it's always nice to check that everything has been reset.

function TorqueTacticsGame::startGame(%game)
{
   echo (%game @"\c4 -> "@ %game.class @" -> TorqueTacticsGame::startGame");
   
	if(!isObject($AllyList))
	{
		$AllyList = new arrayobject();
		MissionCleanup.add($AllyList);
	}
	
	if(!isObject($EnemyList))
	{
		$EnemyList = new arrayobject();
		MissionCleanup.add($EnemyList);
	}
	
	if(!isObject(turnManager))
	{
		new ScriptObject(turnManager) 
		{
			enemyPhase = 0;//not the enemies turn
			turnNum = 0;//start the game at the beginning! Turns have not yet started
			victory = 0;//victory not achieved - we've only just started
		};
		MissionCleanup.add(turnManager);
		echo("Establishing Turn Management System");
	}
	
	TacticsHud.setVisible(true);
	TurnHud.setVisible(true);

//make sure buttons are reset
	tacticsMove.setStateOn(false);
	tacticsShoot.setStateOn(false);
	
	tacticsMove.pressed=0;
	tacticsShoot.pressed=0;
   
   parent::startGame(%game);
}

Next, we'll clean up a few things when we quit, mainly the team arrays and turnManager.

function TorqueTacticsGame::endGame(%game)
{
   echo (%game @"\c4 -> "@ %game.class @" -> TorqueTacticsGame::endGame");

//reset buttons
	tacticsMove.setStateOn(false);
	tacticsShoot.setStateOn(false);
	
	tacticsMove.pressed=0;
	tacticsShoot.pressed=0;
   
   	if(isObject($AllyList))
	{
		$AllyList.empty();//empty first
		$AllyList.delete();//then delete
	}
	
	if(isObject($EnemyList))
	{
		$EnemyList.empty();//empty first
		$EnemyList.delete();//then delete
	}
	
	if(isObject(turnManager))
	{	
		TurnManager.delete();
		echo("Deleting Turn Management System");
	}

   parent::endGame(%game);
   // Inform the client the game is over
   for (%clientIndex = 0; %clientIndex < ClientGroup.getCount(); %clientIndex++)
   {
      %cl = ClientGroup.getObject(%clientIndex);
      commandToClient(%cl, 'GameEnd');
   }
}

When the Client enters the game just after it starts, we want to spawn the camera or else they can't see the game.

function TorqueTacticsGame::onClientEnterGame(%game, %client)
{
   echo (%game @"\c4 -> "@ %game.class @" -> TorqueTacticsGame::onClientEnterGame");
   
	messageAll('MsgAll', '\c4Turn %1', TurnManager.turnNum);
	
	   // Sync the client's clocks to the server's
   commandToClient(%client, 'SyncClock', $Sim::Time - $Game::StartTime);

	//!camera = !view, so we better spawn one!
   %cameraSpawnPoint = pickCameraSpawnPoint($Game::DefaultCameraSpawnGroups);
   %client.spawnCamera(%cameraSpawnPoint);
   
     // Prepare the player object.
   %game.preparePlayer(%client);
	
 //  parent::onClientEnterGame(%game, %client);// - we've got this - disable stock function
}

Next, we use a custom spawning sequence to get three Ally Team members called Tom, Dick and Harry at the beginning of the first turn. Remember those three spawnSpheres we made in Part 2 called "PlayerSpawn1-3"? That's what they are for. We will use a custom spawning function inside scripts/server/AiPlayer.cs - more on that later.

function TorqueTacticsGame::preparePlayer(%game, %client)
{
   echo (%game @"\c4 -> "@ %game.class @" -> TorqueTacticsGame::preparePlayer");
   
   //spawn our allied team at the start only
	if(isObject(turnManager) && turnManager.turnNum == 0)
	{
		if(isObject(PlayerSpawn1))
			AiPlayer::tacticsSpawn(Tom, PlayerSpawn1, 1);
		else
			echo("Oh Noes! Fail: No SpawnPoint to spawn Tom!");
		
		if(isObject(PlayerSpawn2))
			AiPlayer::tacticsSpawn(Dick, PlayerSpawn2, 1);
		else
			echo("Oh Noes! Fail: No SpawnPoint to spawn Dick!");
		
		if(isObject(PlayerSpawn1))
			AiPlayer::tacticsSpawn(Harry, PlayerSpawn3, 1);
		else
			echo("Oh Noes! Fail: No SpawnPoint to spawn Harry!");
	}
	else
	{
		echo("Not spawning an allied team as either there is no Turn Management System Active or we are not at the beginning of the game");
	}
		
	%count = $AllyList.count();
	if(%count > 0)
	{
		%start = $AllyList.getKey($AllyList.moveFirst());
		%client.player = %start.getID();//should return ID anyway but check
		%client.player.updateEnergy();
		%client.player.updateHealth();
		%wpn = %client.player.getmountedimage(%slot);
		%wpn.UpdateWeaponHud(%client.player, %slot);
	}
	else
		echo("AllyList is empty!");
		
	commandToServer('tacticsCam');
	commandToServer('NewTurn');//start turn one!
	

   // Prepare the actual player object for spawning and beginning equipment.
//   parent::preparePlayer(%game, %client);// - we've got this - disable stock function
}

Other notable changes, we don't want the game to cycle. onDeath will not get called, but we do want "GameSpecificDeath" to get called so we can message the chatHUD with who was killed. The contents of this message will depend on whether the deceased was an ally or an enemy to the Client.

function TorqueTacticsGame::checkScore(%game, %client)
{
   echo (%game @"\c4 -> "@ %game.class @" -> TorqueTacticsGame::checkScore");

   echo("score: "@ %client.score @"  "@ %game.endgameScore @" "@ $Game::EndGameScore);
//   if (%client.score >= %game.endgameScore)
    //  cycleGame();//yorks we never want to cycle the game!
}

function TorqueTacticsGame::onDeath(%game, %client, %sourceObject, %sourceClient, %damageType, %damLoc)
{
   echo (%game @"\c4 -> "@ %game.class @" -> TorqueTacticsGame::onDeath");
   
//this should never get called anyway - but disable the parent just incase
 //  parent::onDeath(%game, %client, %sourceObject, %sourceClient, %damageType, %damLoc);
//   %game.checkScore(%sourceClient);
}

function TorqueTacticsGame::GameSpecificDeath(%game, %obj)
{
   echo (%game @"\c4 -> "@ %game.class @" -> TorqueTacticsGame::GameSpecificDeath");
   
	if(%obj.team == 1)
		messageClient(localClientConnection, 'MsgPlayer', '\c2Oh Noes! They got %1!', %obj.getName());
	else
		messageClient(localClientConnection, 'MsgPlayer', '\c2Bad Guy Down!');
   
//   parent::GameSpecificDeath(%game);
}

To get the callback for this "GameSpecificDeath", open up scripts/server/player.cs and edit:

function Armor::damage(%this, %obj, %sourceObject, %position, %damage, %damageType)
{
//...
//yorks at the bottom of this function add
	  
	 if(%obj.getState() $="Dead")//yorks <---- add
		game.GameSpecificDeath(%obj);//yorks <---- add
}

Lastly, you need to add this new file to the scripts/server/scriptExec.cs file. Just place it at the end to have the game load it.

//...

// Load our gametypes
exec("./gameCore.cs"); // This is the 'core' of the gametype functionality.
exec("./gameDM.cs"); // Overrides GameCore with DeathMatch functionality.
exec("./gameTorqueTactics.cs"); // Overrides GameCore with custom TorqueTactics - turn-based tactics/action hybrid functionality.

Now, open script/server/commands.cs as we need to totally replace "TurnOver" function to cope with the new TurnManager and Teams and add another funtion.

function serverCmdTurnOver(%client)
{
	%count = $AllyList.count();
	if(%count > 0)
	{
		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 );
		}
		
		TacticsHud.setVisible(false);
		TurnHud.setVisible(false);
		
		//enemy turn
		%count = $EnemyList.count();
		if(%count > 0)
		{
			if(turnManager.victory == 1)
			{
				MessageBoxOK( "You Have Defeated The Enemy", "Huzzar! You Won! Game Over", "disconnect();");
				return;
			}
			
			for(%i=0; %i < %count; %i++)
			{
				%bot = $EnemyList.getkey(%i);
	
				%bot.action = 0;
				%bot.hasFired = 0;
				%bot.restoreEnergy();
			}
		}
		else
		{
		//	MessageBoxOK( "You Have Defeated The Enemy", "Huzzar! You Won! Game Over", "disconnect();");
			if(isObject(TurnManager))
			{
				if(turnManager.victory == 1)
				{
					MessageBoxOK( "You Have Defeated The Enemy", "Huzzar! You Won The Game!", "disconnect();");
					return;
				}
				else
				{
				
					TurnManager.enemyPhase = 1;
					echo("Enemy Turn");
					messageAll('MsgNewTurn', '\c4Turn %1 Enemy Phase', TurnManager.turnNum);
				
					//make a slight pause
					schedule(2000, turnManager, "commandToServer", 'NewTurn');
				}
			}
			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();");
			}
		}
	}
	else
	{
	    MessageBoxOK( "You Have Been Defeated", "You Lost The Game", "disconnect();");
	}

}

//now add:

function serverCmdNewTurn(%client)
{
	if(isObject(TurnManager))
	{
		TurnManager.enemyPhase = 0;
		echo("Ally Turn");
		
		TurnManager.turnNum++;
		messageAll('MsgNewTurn', '\c4Turn %1 Player Phase', TurnManager.turnNum);
		echo("Turn " @ TurnManager.turnNum);
	}
	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();");
		return;
	}

	TacticsHud.setVisible(true);
	TurnHud.setVisible(true);

	%count = $AllyList.count();
	if(%count > 0)
	{
		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.action = 0;
			%bot.hasFired = 0;
			%bot.restoreEnergy();
		}
	
	
		%start = $AllyList.getKey($AllyList.moveFirst());
		%start = %start.getID();
		%client.player = %start;
		
		commandToServer('tacticsCam');
		
		%start.updateEnergy();
		%start.updateHealth();
		%wpn = %start.getmountedimage(%slot);
		%wpn.UpdateWeaponHud(%start, %slot);
	}
	else
	{
	    MessageBoxOK( "You Have Been Defeated", "Game Over", "disconnect();");
	}

}

In scripts/server/player.cs add this to the end of "Armor::onDisabled", so that the appropriate team array will be updated when the team member is deceased:

function Armor::onDisabled(%this, %obj, %state)
{

//...

   	if(%obj.team == 1)
	{
		if($AllyList.countKey(%obj) != 0)
		{
			%remove = $AllyList.getIndexfromKey(%obj);
			echo("remove index = " @ %remove);
			$AllyList.erase(%remove);
		}
			
		echo("allylist count = " @ $AllyList.count());	
		$AllyList.echo();
	}
	else
	{
		if($EnemyList.countKey(%obj) != 0)
		{
			%remove = $EnemyList.getIndexfromKey(%obj);
			echo("remove index = " @ %remove);
			$EnemyList.erase(%remove);
		}
			
		$EnemyList.echo();
	}
}

Now, we just need some custom Ai Types, and the "Team" button working to test all of this.

Part Eight: Custom Ai

#1
05/08/2011 (5:52 pm)
@Steve - Dude. When do you sleep? This is another great series! I'm gonna start linking to your guides from the official docs and site.
#2
05/08/2011 (6:13 pm)
... zzzzzz ... :P
#3
05/09/2011 (1:43 pm)
So, Steve...

Now it's official, even Mich say it... You never sleep!

Stop to deny the evidence... ! :-D
#4
05/09/2011 (7:21 pm)
This is way cool. Please please please PLEASE write a book.
#5
05/09/2011 (8:40 pm)
Write a book on what? I'm making this up as I go. Which is why it isn't finished yet. :P