Tactics-Action Hybrid Game Tutorial Part4: Player Actions
by Steve Acaster · 05/08/2011 (9:52 am) · 6 comments
Back to Part Three: Camera System
Our playerObject needs to move and shoot.

Remember this? Save it because we'll use it as a marker for movement. Place it in art/decals. If you want to know the ins-and-outs of creating a new decal in the editors I suggest you read the RtsPrototype Desintation Markers Section. Here, I will just be dealing with the resulting data added manually to the correct files.
Open art/decals/materials.cs and add:
In art/decals/managedDecalData.cs add:
When we click on a location for the playerObject to move to, this marker will appear there, and thus give the Client a visual reference. Before we can make the player move, we need to differentiate between the player's available actions. As we are only using the mouse for all gameplay, we need to toggle between moving and shooting with the left mouse button. Remember, mouse right button is for swapping between rotating the camera and giving us a cursor to point and click in the scene (actually to point and click at a location on the PlayGui) and "scroll button" is for zooming in and out with the camera, so we only left mouse button available for actions.
To create this toggle, we will create some on-screen buttons using the T3D GUI Editor. Load up T3D, get in-game and start the GUI Editor (F10). Note that we will always be working with Gui Editor on size "1024x768 (XGA, 4:3)" so all positions will reflect that.
First create a new "library->images->GuiBitmapBorderCtrl" to keep our buttons inside. Call it "tacticsHud" and give it horizSizing = "left" and vertSizing = "top". Give it the control->Profile = "chatHudBorderProfile" and check "isContainer". Give it the position = "800 720" and extent = "216 48".
N.B. When resizing screen, to keep position the vert/horizSizing should be the OPPOSITE of their nearest edge. We want this gui element to be at the bottom right of the screen so we use "top left" for their vert/horizSizing.
Next create a background (library->Images->BitmapGuiCtrl with bitmap "core/art/gui/images/hudfill.png") for this and make sure it is a child of "tacticsHUD". Position = "8 8", extent = "200 32", sizings are "width" and "height".
Finally we need to make 3 buttons (library->buttons->GuibuttonCtrl) and call them "tacticsMove", "tacticsShoot" and "tacticsTeam" each with the profile = "GuiButtonProfile" (this profile should be auto assigned but always check). Make them all the same size ("64 32")), and position them at "8 8", "76 8" and "144 8" respectively. Set all sizing to "width" and "height". For "Move" and "Shoot" set buttonType = "toggleButton", leave "Team" as "pushButton".
Save and save often.
Hopefully it looks like the buttons in the lower right corner.

Exit T3D and open up art/gui/playGui.gui in your prefered IDE/script/text editor.
Find the variable noCusor = "1"; under %guiContent = new GameTSCtrl(PlayGui) { and change it to:
The Client (you) selects either "Move" (action 1) or "Shoot" (action 2).
The buttons are "toggleButtons" so when selected they show a "pushed-down" button image.
If you press "Move" down (setOn), "Shoot" button must be toggled up (setOff).
There are three states of "action":
0 = no action selected
1 = Move
2 = Shoot
We reset the functions and variables at startup. "PlayGui" has it's standard system functions in the file scripts/gui/playGui.cs, so edit it. Whilst we can set the states of the toggleButtons on and off - we cannot read them in stock script, so we need a new variable to read whether the button is "pressed" or not.
Each button sends a command to the server to go into the selected mode (setActionMove/Shoot), or if the mode is already inuse, to disable it and reset the actions to zero (clearActions). Let's make those server functions now. In scripts/server/commands.cs add:
Now you'll have noticed a reference to "decal" there, this is the GG_decal we made early and will be the marker that is shown as a destination when we select to move. Let's go back to playGui.gui and make the final function for pressing the left mouse button when we have selected to move or shoot.
N.B. As this is a "singleplayer" gametype tutorial it is envisioned that the client playing the game will be on the same computer as the game, rather than being a remote Client on another machine connected via LAN/Interweb. For this reasoning we shall use localClientConnection to determine the Client rather than retrieving the appropriate ClientConnection via the server's ClientGroup.
Now there's a few things you will have noticed in there. First, Movement depends on energy/action points. Second, we can only fire once a turn. Third, we require a new function called "OpenFire". Let's make that now.
Open scripts/server/aiplayer.cs and add:
Right, if all works correctly, you should be able to press the "Move" button, press a location on screen (within 100 units), see the marker appear and the playerObject will move there. You can abort the move at anytime by pressing another button or left clicking anywhere again. You can press the "Shoot" button, aim at a location (within 100 units) and if the playerObject has a clean shot, they'll open fire.
Notice that we have another check to see if the playerObject can shoot at the target clearly - this is because the camera is not viewed from the playerObject's eyeNode or it's weapon's muzzlePoint. This makes it easy to try and shoot something which is not in "Line Of Sight" of the playerObject, and thus it could never accurately hit.
Part Five: Energy and Turns
Our playerObject needs to move and shoot.

Remember this? Save it because we'll use it as a marker for movement. Place it in art/decals. If you want to know the ins-and-outs of creating a new decal in the editors I suggest you read the RtsPrototype Desintation Markers Section. Here, I will just be dealing with the resulting data added manually to the correct files.
Open art/decals/materials.cs and add:
singleton Material(gg_marker)
{
mapTo = "G_marker";
diffuseMap[0] = "art/decals/G_marker.png";
useAnisotropic[0] = "1";
emissive[0] = "1";
castShadows = "0";
alphaTest = "1";
showFootprints = "0";
materialTag0 = "RoadAndPath";
};In art/decals/managedDecalData.cs add:
datablock DecalData(GG_decal)
{
Material = "gg_marker";
textureCoordCount = "0";
size = "2";
};When we click on a location for the playerObject to move to, this marker will appear there, and thus give the Client a visual reference. Before we can make the player move, we need to differentiate between the player's available actions. As we are only using the mouse for all gameplay, we need to toggle between moving and shooting with the left mouse button. Remember, mouse right button is for swapping between rotating the camera and giving us a cursor to point and click in the scene (actually to point and click at a location on the PlayGui) and "scroll button" is for zooming in and out with the camera, so we only left mouse button available for actions.
To create this toggle, we will create some on-screen buttons using the T3D GUI Editor. Load up T3D, get in-game and start the GUI Editor (F10). Note that we will always be working with Gui Editor on size "1024x768 (XGA, 4:3)" so all positions will reflect that.
First create a new "library->images->GuiBitmapBorderCtrl" to keep our buttons inside. Call it "tacticsHud" and give it horizSizing = "left" and vertSizing = "top". Give it the control->Profile = "chatHudBorderProfile" and check "isContainer". Give it the position = "800 720" and extent = "216 48".
N.B. When resizing screen, to keep position the vert/horizSizing should be the OPPOSITE of their nearest edge. We want this gui element to be at the bottom right of the screen so we use "top left" for their vert/horizSizing.
Next create a background (library->Images->BitmapGuiCtrl with bitmap "core/art/gui/images/hudfill.png") for this and make sure it is a child of "tacticsHUD". Position = "8 8", extent = "200 32", sizings are "width" and "height".
Finally we need to make 3 buttons (library->buttons->GuibuttonCtrl) and call them "tacticsMove", "tacticsShoot" and "tacticsTeam" each with the profile = "GuiButtonProfile" (this profile should be auto assigned but always check). Make them all the same size ("64 32")), and position them at "8 8", "76 8" and "144 8" respectively. Set all sizing to "width" and "height". For "Move" and "Shoot" set buttonType = "toggleButton", leave "Team" as "pushButton".
Save and save often.
Hopefully it looks like the buttons in the lower right corner.

Exit T3D and open up art/gui/playGui.gui in your prefered IDE/script/text editor.
Find the variable noCusor = "1"; under %guiContent = new GameTSCtrl(PlayGui) { and change it to:
noCusor = "0";Now when we start a game we will have a cursor on screen to select which actions button we want, rather than starting in camera rotation mode.
The Client (you) selects either "Move" (action 1) or "Shoot" (action 2).
The buttons are "toggleButtons" so when selected they show a "pushed-down" button image.
If you press "Move" down (setOn), "Shoot" button must be toggled up (setOff).
There are three states of "action":
0 = no action selected
1 = Move
2 = Shoot
function tacticsMove::onAction(%this)
{
//move has been pressed
//if move's pressed variable is not active, make it so now ->SetActionMove
//if shoot is on, turn it off
//if move is already pressed the client must be toggling it off by pressing it again -> clearActions
if(tacticsMove.pressed == 0)
{
tacticsMove.pressed = 1;
tacticsShoot.setStateOn(false);
tacticsShoot.pressed = 0;
commandToServer('SetActionMove');
}
else
{
tacticsMove.pressed = 0;
commandToServer('ClearActions');
}
}
function tacticsShoot::onAction(%this)
{
//shoot has been pressed
//if shoot's pressed variable is not active, make it so now ->SetActionShoot
//if move is on, turn it off
//if shoot is already pressed the client must be toggling it off by pressing it again -> clearActions
if(tacticsShoot.pressed == 0)
{
tacticsShoot.pressed = 1;
tacticsMove.setStateOn(false);
tacticsMove.pressed = 0;
commandToServer('SetActionShoot');
}
else
{
tacticsShoot.pressed = 0;
commandToServer('ClearActions');
}
}We reset the functions and variables at startup. "PlayGui" has it's standard system functions in the file scripts/gui/playGui.cs, so edit it. Whilst we can set the states of the toggleButtons on and off - we cannot read them in stock script, so we need a new variable to read whether the button is "pressed" or not.
function PlayGui::onWake(%this)
{
//...
//yorks
if(isObject(TacticsHud))
{
TacticsHud.setVisible(true);
tacticsMove.setStateOn(false);
tacticsMove.pressed=0;
tacticsShoot.setStateOn(false);
tacticsShoot.pressed=0;
}
if(isObject(TurnHud))
TurnHud.setVisible(true);
}Each button sends a command to the server to go into the selected mode (setActionMove/Shoot), or if the mode is already inuse, to disable it and reset the actions to zero (clearActions). Let's make those server functions now. In scripts/server/commands.cs add:
function serverCmdSetActionMove(%client)
{
%client.player.action = 1;
}
function serverCmdSetActionShoot(%client)
{
%client.player.action = 2;
//if the playerObject is moving stop it and remove any destination marker
if(%client.player.getVelocity() !$="0 0 0")
{
if( %client.player.decal > -1 )
decalManagerRemoveDecal( %client.player.decal );
%client.player.stop();
}
}
function serverCmdClearActions(%client)
{
%client.player.action = 0;
//if the playerObject is moving stop it and remove any destination marker
if(%client.player.getVelocity() !$="0 0 0")
{
if( %client.player.decal > -1 )
decalManagerRemoveDecal( %client.player.decal );
%client.player.stop();
}
}Now you'll have noticed a reference to "decal" there, this is the GG_decal we made early and will be the marker that is shown as a destination when we select to move. Let's go back to playGui.gui and make the final function for pressing the left mouse button when we have selected to move or shoot.
N.B. As this is a "singleplayer" gametype tutorial it is envisioned that the client playing the game will be on the same computer as the game, rather than being a remote Client on another machine connected via LAN/Interweb. For this reasoning we shall use localClientConnection to determine the Client rather than retrieving the appropriate ClientConnection via the server's ClientGroup.
function PlayGui::onMouseDown(%this, %pos, %start, %ray)
{
//actions
//0 = no action
//1 = move action
//2 = shoot action
// Get access to the AI player we control
%ai = LocalClientConnection.player;
//if they are already moving,
//interrupt and stop them,
//also remove any marker they may have
//and abort this action sequence as the Client may want to give new orders
if(%ai.getVelocity() !$="0 0 0")
{
if( %ai.decal > -1 )
decalManagerRemoveDecal( %ai.decal );
%ai.stop();
return;
}
echo("ai has action = " @ %ai.action);
if(%ai.action == 0)
return;
//find end of search vector from the cursors point on screen
// make it no more 100 units from the camera
//anymore and have it miss
%ray = VectorScale(%ray, 100);
%end = VectorAdd(%start, %ray);
if(%ai.action == 1)//1 is move
{
//we can only move if we have energy!
if(%ai.getEnergyLevel() < 1)
{
messageClient(localclientconnection, 'MsgPlayer', '\c0%1 - Cannot Move - Out Of Energy!', %ai.getName());
return;
}
// only care about terrain objects for this tutorial
%searchMasks = $TypeMasks::TerrainObjectType;
// search!
%scanTarg = ContainerRayCast( %start, %end, %searchMasks );
// If the terrain object was found in the scan
if( %scanTarg )
{
// Get the X,Y,Z position of where we clicked
%pos = getWords(%scanTarg, 1, 3);
// Get the normal of the location we clicked on
%norm = getWords(%scanTarg, 4, 6);
//if we are not moving already, start to deplete energy/action points
if(%ai.getVelocity() $="0 0 0")
%ai.schedule(500, "decEnergy");
// Set the destination for the AI player
%ai.setMoveDestination( %pos );
//clear his aim so he looks where he is running
%ai.clearAim();
// If the AI player already has a decal (0 or greater)
// tell the decal manager to delete the instance of the gg_decal
if( %ai.decal > -1 )
{
decalManagerRemoveDecal( %ai.decal );
if(%ai.getVelocity() !$="0 0 0")
{
%ai.stop();
return;
}
}
// Create a new decal using the decal manager
// arguments are (Position, Normal, Rotation, Scale, Datablock, Permanent)
// AddDecal will return an ID of the new decal, which we will
// store in the player
%ai.decal = decalManagerAddDecal( %pos, %norm, 0, 1, "gg_decal", true );
}
else
{
//raycast was too far, more than 100 units from the camera
messageClient(localclientconnection, 'MsgPlayer', '\c0%1 - Location Out Of Range!', %ai.getName());
}
}
else//action 2 is shoot
{
if(%ai.hasFired == true)
{
//As in Valkyria Chronicles you can only shoot once per turn
messageClient(localclientconnection, 'MsgPlayer', '\c0%1 Has Already Fired This Turn!', %ai.getName());
return;
}
// Only care about players this time - we don't use interiors -
//note you might need to alter these depending on what version of T3D you are using
//T3D 1.1 Final and beyond may use different typemasks from T3D 1.1 Beta 3
%searchMasks = $TypeMasks::VehicleObjectType | $TypeMasks::PlayerObjectType | $TypeMasks::TerrainObjectType | $TypeMasks::StaticTSObjectType | $TypeMasks::StaticShapeObjectType;
// Search!
%scanTarg = ContainerRayCast( %start, %end, %searchMasks, %ai );
if( %scanTarg )
{
// Get the enemy ID
%target = firstWord(%scanTarg);
%xyz = restWords(%scanTarg);
//aim at the location we hit
%ai.setAimLocation(%xyz);
//pause for 200m/s to give time to aim and then shoot
%ai.schedule(200, "OpenFire", %xyz);
}
else
{
//aimpoint is more than 100 units abort
messageClient(localclientconnection, 'MsgPlayer', '\c0%1 - AimPoint Out Of Range!', %ai.getName());
//clear aim and if he's shooting stop it
%ai.clearAim();
%ai.setImageTrigger(0, 0);
}
}
}Now there's a few things you will have noticed in there. First, Movement depends on energy/action points. Second, we can only fire once a turn. Third, we require a new function called "OpenFire". Let's make that now.
Open scripts/server/aiplayer.cs and add:
function AiPlayer::openFire(%this, %xyz)
{
%muzzlePoint = %this.getMuzzlePoint(%slot);
%searchMasks = $TypeMasks::VehicleObjectType | $TypeMasks::PlayerObjectType | $TypeMasks::TerrainObjectType | $TypeMasks::StaticTSObjectType | $TypeMasks::StaticShapeObjectType;
%aimpoint = VectorAdd(%muzzlePoint, VectorScale(%this.getEyeVector(), 200));
%targetsearch = containerRayCast(%muzzlePoint, %aimpoint, %searchMasks, %this);
%target = restWords(%targetSearch);
if(%target)
{
%aim = VectorDist(%target, %xyz);
echo("difference between muzzleVector and targetpoint is " @ %aim);
if(%aim <= 5)
{
// Tell our AI object to fire its weapon
%this.setImageTrigger(0, true);
%this.hasFired = 1;
// Stop firing in 150 milliseconds
%this.schedule(50, "setImageTrigger", 0, 0);
%this.schedule(100, "clearAim");
}
else
{
messageClient(localClientConnection, 'MsgNoLOS', '%1 - No Line of Sight to Aimpoint!', %this.getName());
%this.clearAim();
%this.setImageTrigger(0, 0);
}
}
else
{
messageClient(localClientConnection, 'MsgNoLOS', '%1 - No Line of Sight to Aimpoint!', %this.getName());
%this.clearAim();
%this.setImageTrigger(0, 0);
}
}Right, if all works correctly, you should be able to press the "Move" button, press a location on screen (within 100 units), see the marker appear and the playerObject will move there. You can abort the move at anytime by pressing another button or left clicking anywhere again. You can press the "Shoot" button, aim at a location (within 100 units) and if the playerObject has a clean shot, they'll open fire.
Notice that we have another check to see if the playerObject can shoot at the target clearly - this is because the camera is not viewed from the playerObject's eyeNode or it's weapon's muzzlePoint. This makes it easy to try and shoot something which is not in "Line Of Sight" of the playerObject, and thus it could never accurately hit.
Part Five: Energy and Turns
About the author
One Bloke ... In His Bedroom ... Making Indie Games ...
#2
05/21/2011 (6:31 pm)
Where do the tacticsMove functions go - in PlayGui.cs ?
#3
Exit T3D and open up art/gui/playGui.gui in your prefered IDE/script/text editor.
You could put it in the cs if you want.
05/22/2011 (5:09 am)
quote:Exit T3D and open up art/gui/playGui.gui in your prefered IDE/script/text editor.
You could put it in the cs if you want.
#4
Function PlayGui::onMouseDown(%this, %pos, %start, %ray) does work as I can see output from echo("ai has action = " @ %ai.action) however %ai.action is empty
can you guess where the error come from ? Thanks.
p/s: In GuiButtonCtrl there's 1 property: "UseMouseEvent" which is false (default value). I tried with true but it can't solve the problem.
09/09/2011 (10:18 pm)
hi Steve, I'm using 1.1. It seems tacticsMove::onAction(%this) & tacticsMove::onAction(%this) dont receive events when I click on Move&Shoot buttons. I put echo there but not seen the msg.Function PlayGui::onMouseDown(%this, %pos, %start, %ray) does work as I can see output from echo("ai has action = " @ %ai.action) however %ai.action is empty
can you guess where the error come from ? Thanks.
p/s: In GuiButtonCtrl there's 1 property: "UseMouseEvent" which is false (default value). I tried with true but it can't solve the problem.
#5
09/12/2011 (9:32 am)
it's solved. I put the background of tacticsHud on top of 3 buttons. Now I just move it to the back and everything works OK !!! 
Associate Steve Acaster
[YorkshireRifles.com]