You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Torque 3D's stock demos and templates are setup for first person
shooter (FPS) games. However, the engine has multiple camera modes that
can change the perspective and how the game is controlled. In this
tutorial, we are going to modify the camera and mouse controls to
emulate different game types: Hack & Slash and RTS.
Some of the topics that will be covered are:
Advanced scripting
Camera manipulation
Simple AI
Object spawning
Mouse and keyboard input
Basic RTS and Hack & Slash mechanics
Before you begin, you should be familiar with the following guides. Make sure you have read these before proceeding:
TorqueScript Syntax
Camera Modes
World Editor Interface
Adding 3D Shapes
Material Editor
Decal Editor
Datablock Editor
Create A Fresh Project
We're going to start with a clean project, so open the Toolbox and create one
using the Full Template. This will give us a blank terrain to work with.
Name your new project RTS_Prototype and select the Full template.
When the project has been created, select your project (1), select the
Empty Terrain mission (2) and open it in the World Editor (3).
The Full Template used for this tutorial project should contain base art and scripts needed to run the game, but we want to use some custom models. Start by CLICKING HERE to download a zip file containing the sample assets.
Copy the art folder in the zip file on top of your project's art file. This will add the models, their datablocks, and their materials to your project if they do not exist. When you are ready, continue reading to configure your mouse controls.
BoomBot.cs, which contains the should be located in the
game/art/datablocks folder (1). The image for the
building placement button should be in the game/art/gui folder
(2). The files associated with Boom Bot's model, textures and animations should be located in the
game/art/shapes/actors/BoomBot folder (3).
The zip file adds two new models to the project. One is a simple, static building. The other is a custom player model called Boom Bot. While you can simply drag and drop these models in via the World Editor, we need to hook up a script file prepared specifically for Boom Bot.
To get Boom Bot fully working, open game/art/datablocks/datablockExec.cs Scroll down until you see the following code:
exec("./aiPlayer.cs");
Just below that line, add the following:
exec("./BoomBot.cs");
In Torsion it should look something like this:
Also, check your BoomBot datablock if you are not using the files from the download. It should look like this:
This gives us the ability to tell our unit where to go and let the AI class
handle getting it there. It also lets us have our default player be the BoomBot
without having to specify it at the spawnpoint.
Camera Setup
First we're going to set up our camera mode framework. In
scripts/server/commands.cs we're going to add some functions to set and
toggle our camera modes. The following code can sit at the end of the script file:
Ensure that any other binds for zaxis and alt m
are commented out to avoid conflicts. Also, if the file
scripts/client/config.cs exists it will need to be deleted before
changes to default.bind.cs can take effect.
Mouse Setup
The following code will change the way mouse input affects movement and click interaction.
Mouse Cursor Toggling
Normally, the camera is controlled by an actor in FPS (aim) mode. To
focus on just mouse and camera work, we need to change how the default
camera is controlled. Open game/scripts/server/gameCore.cs. In function GameCore::preparePlayer(%game, %client), locate the following line:
%game.spawnPlayer(%client, %playerSpawnPoint);
Change this code by adding a third argument to the function call:
The function call being modified is GameCore::spawnPlayer(%game, %this, %spawnPoint, %noControl), located in game/core/scripts/server/gameCore.cs. The last two arguments determine the location of spawning (%spawnPoint) and
whether or not the actor object controls the camera (%noControl). We need to address that next.
Immediately below the %game.spawnPlayer() function, add the following code:
// Set camera to Overhead mode
commandToServer('overheadCam');
If you run the game, you will now be using an
orbit
camera instead of an FPS view controlled by the actor. Next, we need to
be able to control the on/off state of the in-game mouse cursor. Open game/scripts/client/default.bind.cs. At the end of the file, add the following:
// Turn mouse cursor on or off
// If %val is true, the button was pressed in
// If %val is false, the button was released
function toggleMouseLook(%val)
{
// Check to see if button is pressed
if(%val)
{
// If the cursor is on, turn it off.
// Else, turn it on
if(Canvas.isCursorOn())
hideCursor();
else
showCursor();
}
}
// Bind the function toggleMouseLook to the keyboard 'm' key
moveMap.bind(keyboard, "m", "toggleMouseLook");
Next, open your file browser and delete scripts/client/config.cs,
if it exists. This file contains custom keybinds created for your game.
It will override the variables and functions you add to default.bind.cs.
However, if you delete this file and run your game, a new one will be
regenerated with your updated keybinds.
If you start the game now, it will still default to a free
flying (mouse look) camera. By hitting the 'm' key you will be able to toggle "mouse
look" mode. If mouse look is on, you can control your view direction by
moving the mouse. If it is off, you can move your cursor around on the
screen. You can switch back to an actor controlled camera by pressing
Alt + C.
We will go ahead and force the cursor to be on as soon as the level loads. Open game/art/gui/playGui.gui. You can edit .gui files just like any other script file. Look for the noCursor field. Make the following change to this field:
noCursor = "0";
Now that you've freed up the mouse from aiming duties, it's time to put it to other uses.
Placing Structures Using The GUI
First, open art/gui/PlayGui.gui and find new
GuiControl(DamageHUD) toward the end of the file and remove the entire
block of code (ensure that you do not delete the main block's closing brace - it's the
one with the semi-colon after it). It occupies the center of the screen
when you are in play mode and it will block mouse clicks to that area.
Optionally, you can remove this control element using the Gui Editor.
Next, open the GUI Editor by pressing F10 or by clicking the GUI Editor
button from the World Editor.
Open the Library tab, pull down the
Buttons rollout, then click and drag
the GuiBitmapButtonCtrl onto the GUI to create a new button.
Select your new button and change the settings as follows:
Browse to your project's art/gui folder and select the
orcburrow.png file for
the button image.
If you close the GUI Editor ( F10 ) you should now see your
button in the game UI.
Next, open scripts/gui/playGui.cs and add the following code at
the end:
// onMouseDown is called when the left mouse
// button is clicked in the scene
// %pos is the screen (pixel) coordinates of the mouse click
// %start is the world coordinates of the camera
// %ray is a vector through the viewing
// frustum corresponding to the clicked pixel
function PlayGui::onMouseDown(%this, %pos, %start, %ray)
{
// If we're in building placement mode ask the server to create a building for
// us at the point that we clicked.
if (%this.placingBuilding)
{
// Clear the building placement flag first.
%this.placingBuilding = false;
// Request a building at the clicked coordinates from the server.
commandToServer('createBuilding', %pos, %start, %ray);
}
else
{
// Ask the server to let us attack a target at the clicked position.
commandToServer('checkTarget', %pos, %start, %ray);
}
}
// This function is the callback that handles our new button. When you click it
// the button tells the PlayGui that we're now in building placement mode.
function orcBurrowButton::onClick(%this)
{
PlayGui.placingBuilding = true;
}
Then, in scripts/server/commands.cs add the following function to the end:
function serverCmdcreateBuilding(%client, %pos, %start, %ray)
{
// find end of search vector
%ray = VectorScale(%ray, 2000);
%end = VectorAdd(%start, %ray);
// set up to look for the terrain
%searchMasks = $TypeMasks::TerrainObjectType;
// search!
%scanTarg = ContainerRayCast( %start, %end, %searchMasks);
// If the terrain object was found in the scan
if( %scanTarg )
{
// get the world position of the click
%pos = getWords(%scanTarg, 1, 3);
// Note: getWord(%scanTarg, 0) will get the SimObject id of the object
// that the button click intersected with. This is useful if you don't
// want to place buildings on certain other objects. For instance, you
// could include TSStatic objects in your search masks and check to see
// what you clicked on - then don't place if it's another building.
// spawn a new object at the intersection point
%obj = new TSStatic()
{
position = %pos;
shapeName = "art/shapes/building/orcburrow.dts";
collisionType = "Visible Mesh";
scale = "0.5 0.5 0.5";
};
// Add the new object to the MissionCleanup group
MissionCleanup.add(%obj);
}
}
If you run the game now, you should be able to click the button, then click
on the ground to place a new orc burrow.
Optionally, you can remove the DamageHUD element from the PlayGui - it is not useful in a strategy game and it happens
to block mouse clicks.
Mouse-Driven Input
Without FPS controls and player aiming, we need a new way to control
the Player object. The best examples of a mouse driven game genre are RTS
and Hack & Slash. Typically, these game types allow you to move and
attack using the mouse buttons. Let's start with movement.
Player Spawning
At this point, we can spawn an AI player to stand in for the stock player using
the default player class settings we've provided in the
DeathMatchGame::initGameVars() method.
This AI will be controlled by our mouse inputs. In addition, Torque 3D uses a
simple spawn system which can be easily modified to spawn any kind of
object (of any class). This section will demonstrate how to select what type
of player or NPC you would like to spawn at a particular spawn point.
Open Toolbox, select the empty terrain level, then click the World Editor button.
Once you are in the editor, locate the spawn sphere in the
scene. It is represented by a green octahedron, which will display a green
sphere when you click on it:
(click to enlarge)
You can also locate a spawn sphere by browsing the Scene Tree, under the PlayerDropPoints SimGroup:
If you have multiple spawn spheres, delete all except for one. We can
control what type of actor is spawned by changing the properties of the
remaining spawn sphere. Select the sphere, then change the spawnClass
to AIPlayer and spawnDatablock to BoomBotData. Also, change the name of the spawn sphere to PlayerSpawn.
Technically this step is optional, but if you add other player types you will
want to be able to specify who spawns where. This mechanism allows you to
do that with minimal effort.
This basically replicates the change we made in script earlier, but only for this
specific spawn point. You could as easily used MyBossData for the
spawnDatablock field and then that spawnpoint would spawn MyBoss objects.
Movement
Now that we have an AI player spawning in the game, we can send it commands. Open game/scripts/gui/playGui.cs. Add the function onRightMouseDown as follows:
// onRightMouseDown is called when the right mouse
// button is clicked in the scene
// %pos is the screen (pixel) coordinates of the mouse click
// %start is the world coordinates of the camera
// %ray is a vector through the viewing
// frustum corresponding to the clicked pixel
function PlayGui::onRightMouseDown(%this, %pos, %start, %ray)
{
commandToServer('movePlayer', %pos, %start, %ray);
}
At the end of scripts/server/commands.cs add the following:
function serverCmdmovePlayer(%client, %pos, %start, %ray)
{
//echo(" -- " @ %client @ ":" @ %client.player @ " moving");
// Get access to the AI player we control
%ai = %client.player;
%ray = VectorScale(%ray, 1000);
%end = VectorAdd(%start, %ray);
// We want to allow the AI Player to walk on TSStatics, Interiors, Terrain, etc., so
// I broadened the search mask selection.
%searchMasks = $TypeMasks::TerrainObjectType | $TypeMasks::StaticTSObjectType |
$TypeMasks::InteriorObjectType | $TypeMasks::ShapeBaseObjectType |
$TypeMasks::StaticObjectType;
// search!
%scanTarg = ContainerRayCast( %start, %end, %searchMasks);
// If the terrain object was found in the scan
if( %scanTarg )
{
%pos = getWords(%scanTarg, 1, 3);
// Get the normal of the location we clicked on
%norm = getWords(%scanTarg, 4, 6);
// Set the destination for the AI player to
// make him move
%ai.setMoveDestination( %pos );
}
}
Save your script and run the game. You should now be able to
direct the AI player to wherever you right-click on the terrain. This
only works if you have mouse look disabled, and your cursor is present
on screen.
Spawning Enemy Targets
Our player looks lonely and bored. We should give him some targets, and the means of disposing them. Open game/scripts/client/default.bind.cs, and add the following to the bottom of the file:
// Spawn an AI guy when key is pressed down
function spawnAI(%val)
{
// If key was pressed down
if(%val)
{
// Create a new, generic AI Player
// Position will be at the camera's location
// Datablock will determine the type of actor
new AIPlayer()
{
position = LocalClientConnection.camera.getPosition();
datablock = "DefaultPlayerData";
};
}
}
// Bind the function spawnAI to the keyboard 'b' key
moveMap.bind(keyboard, b, spawnAI);
In the above code, a new example of accessing a client
connection is shown. Instead of ClientGroup, the code uses
LocalClientConnection. In a "single player" environment, you can use
these two interchangeably. Due to Torque 3D's architecture, there will
always be a server and at least one client connection.
The common practice for choosing which to use is as follows:
Accessing From A Client - Use LocalClientConnection. This will always access your connection, player, camera, etc.
Accessing From Server - Use
ClientGroup.getObject(%index). Multiple connections to choose from.
This is good for applying the same functionality to all connections, or
isolating specific ones based on ID.
Again, do not forget to delete game/scripts/client/config.cs.
You can run the game, then press the 'b' key to spawn stationary AI
targets in the same position as your camera. If gravity is enabled,
they will fall until they hit the terrain.
Attacking
Currently, we have a player we can control, and targets that can die. Let's give the player some combat skills. In game/scripts/server/commands.cs, add the following
two functions to the bottom of the script:
function serverCmdcheckTarget(%client, %pos, %start, %ray)
{
%player = %client.player;
%ray = VectorScale(%ray, 1000);
%end = VectorAdd(%start, %ray);
// Only care about players this time
%searchMasks = $TypeMasks::PlayerObjectType;
// Search!
%scanTarg = ContainerRayCast( %start, %end, %searchMasks);
// If an enemy AI object was found in the scan
if( %scanTarg )
{
// Get the enemy ID
%target = firstWord(%scanTarg);
if(%player != %target)
{
// Cause our AI object to aim at the target
// offset (0, 0, 1) so you don't aim at the target's feet
%player.setAimObject(%target, "0 0 1");
// Tell our AI object to fire its weapon
%player.setImageTrigger(0, 1);
}
else
{
serverCmdstopAttack(%client);
}
}
else
{
serverCmdstopAttack(%client);
}
}
function serverCmdstopAttack(%client)
{
// If no valid target was found, or left mouse
// clicked again on terrain, stop firing and aiming
%unit = %client.player;
%unit.setAimObject(0);
%unit.schedule(150, "setImageTrigger", 0, 0);
}
Notice that we're using the full function name when calling serverCmdstopAttack() from within serverCmccheckTarget(). Since we're in a serverCmd function we're
not actually sending a command to the server because right now we are the server. So in this circumstance (and, coincidentally the same applies to clientCmd functions)
we just call the full function directly. It is good to leave this as a serverCmd function because you might want the client to be able to request a unit (or
all of his units) to stop attacking in some other situation.
Now, your player will continuously shoot at any other player you left click on (accuracy not guaranteed). Press the 'b' key to spawn targets to shoot at and blast away. The AI player will be locked in auto-fire mode until you left click on the terrain or on another target. or on another target.
We now have the base functionality for moving the player and the camera, selecting a target, and attacking is now complete.
Tweaking Attacks
You might have noticed some flaws with the base code:
The first shot usually misses
AI keeps shooting after enemy is dead
Enemy does not appear to "die" when health reaches 0
We are going to try and correct these one at a time using
TorqueScript and the editors. Let's start by making our first shot be
on target. The reason the first shot may miss entirely is because the
AI is firing before it has fully turned to aim at the target.
To fix this, edit scripts/server/commands.cs., scroll down to the
serverCmdcheckTarget() function, and locate the following line of code:
// Tell our AI object to fire its weapon
%player.setImageTrigger(0, 1);
Replace the above code with the following:
// Tell our AI object to fire its weapon in 100 milliseconds
%player.schedule(100, "setImageTrigger", 0, 1);
Remember, the %ai variable contains a handle to our AI player
object. The AIPlayer object, which is a child class of SimObject, can
make use of a method named schedule. Instead of calling the setImageTrigger function immediately, we can schedule it to go off in delayed manner.
Schedule (ConsoleMethod) Parameters
simObject.schedule(time, command, arg1...argN)
time - Number of milliseconds to wait before calling the command.
command - Member function (belonging to the simObject using schedule) to call
arg1...argN - Parameters, comma separated, to pass into the command.
The AI we control should now have time to turn and face the target
before firing off the first shot. The code is currently delayed by 100
milliseconds, so you can adjust that number based on desired
performance.
Next, we will change the auto-fire behavior. Instead of having the AI
constantly attack a target, even after it is dead, we are going to
modify the code to only cause our player to attack when a mouse button
is clicked. In the same function we were just working in, locate the
first schedule line we created
// Tell our AI object to fire its weapon in 100 milliseconds
%player.schedule(100, "setImageTrigger", 0, 1);
Then add the following directly under it:
// Stop firing in 150 milliseconds
%player.schedule(150, "setImageTrigger", 0, 0);
If you have not been saving after every script change, you
should definitely do so. Save, then run your game to test the changes
made to the attack routine. Your AI should now be facing the target on
the first shot, and only attack when you click on the target.
There is one more change we can make to make the combat provide more feedback. Each enemy AI starts with health, which is diminished each time it gets shot. The Full template this tutorial is based on is originally intended for a FPS deathmatch game. When an actor dies, a death animation is played.
The death animation code can be found in game/scripts/server/player.cs. Open this file, then scroll down to the following function:
function Player::playDeathAnimation(%this)
{
if (isObject(%this.client))
{
if (%this.client.deathIdx++ > 11)
%this.client.deathIdx = 1;
%this.setActionThread("Death" @ %this.client.deathIdx);
}
else
{
%rand = getRandom(1, 11);
%this.setActionThread("Death" @ %rand);
}
}
The template uses Gideon as the stock actor. The model ships with 11 death animations, which are labeled as "death#" (where # is 1 - 11). This works well for a Player constantly dying, but for an AIPlayer in this tutorial, we only need 1 death animation. This tutorial also mainly works as a client side (single player) prototype.
In simpler terms, we do not need to use the death index (.deathIdx) or %client variables. We can simply call the first death animation available. Change the
::playDeathAnimation(...) function to the following:
function Player::playDeathAnimation(%this)
{
%this.setActionThread("Death1");
}
Now, when the target AI loses all its health it will play a death animation and eventually disappear.
Next, you may notice that the AI units still have terrible aim. This is because the weapons are all set up to be used in first person mode. Any weapon that you intend to use with these
AI units needs to be modified before they can be used effectively. Find your weapon datablock files in game/art/datablocks/weapons and search for
LurkerWeaponImage (to use the Lurker datablock file as an example) then find useEyeNode and firstPerson.
Change any instances of these from true to false. You can also experiment with correctMuzzleVector as well. You should
find a block like the following, except at the end you will see some additions that should help.
// Specify mount point & offset for 3rd person, and eye offset
// for first person rendering.
mountPoint = 0;
firstPerson = false;
useEyeNode = false;
animateOnServer = true;
// Add these to get BoomBot to aim correctly
useEyeOffset = true;
eyeOffset = "0 0 -0.35";
Fiddling with the eyeOffset vector may provide fine-tuning control over aiming, but I haven't been able to see any drastic difference. Don't be afraid to experiment.
As a bit of an aside, if you intend to be able to control units in a game with mixed AI and human players where the humans might be in first person control you will
want to make a copy of all of your weapon datablocks and make the above modifications to them instead. You'll have to name your new datablocks to something like
BotLurkerWeaponImage and load your AI players out with these versions of the weapons instead of the unmodified weapons that you use for the human
players.
Destination Markers
In most RTS or Hack & Slash games, some kind of marker is placed
on the ground where you clicked. This is usually a visual aid to let
you know the move assignment was given, the destination has been set,
and the AI is moving. .
We are going to add this functionality to our prototype to
make it easier to track our AI player using the Material Editor, Decal
Editor, and TorqueScript. First, we need to create a material for the
marker.
Creating a Material
To get started on our marker creation, run your project in the World Editor. Next, open the
Material Editor and click on the Create New Material button.
At this point, the current material will be switched to an
orange warning texture signifying that no diffuse map has been applied. Change the Material name to "gg_marker" and press enter to apply the
change. Next, click on the Diffuse Map box to open the file browser.
Navigate to the game/art/decals folder and select the g_marker.png file. This asset was given to you at the beginning of this guide:
(click to enlarge)
Your new material is nearly complete. However, you should notice that
the marker file and the material do not look the same. Compare the two:
Marker File
Material
This is easy to fix. While editing the gg_marker material,
go to the very bottom in the Advanced Properties section and make the indicated
changes:
This will immediately change the material preview. If you see something like
the following, don't worry; this will probably look fine when we apply it to the
decal.
You are finished with the material. Click save the save button, which will write out the following data to game/art/material.cs:
This is the benefit of using the visual editor to create your
materials and decals, instead of manually writing them out in
TorqueScript. Let's move on to creating the decal.
Creating a Decal
To create a marker decal, run the World Editor and then open the
Decal Editor.
Click on the New Decal Data button (
), next to the garbage bin
( ), and name your new entry "gg_decal".
Next, click on the box in the Material Field of the decal properties, as shown below:
This should open the Material Selector. Locate the gg_maker material we created earlier, click on it, then press the Select button:
(click to enlarge)
The Decal Editor's preview box will display what your new decal will look like in the scene.
(click to enlarge)
That's all that needs to be done to create the decal. Save your level, and your decal data will automatically be written out to game/art/decals/managedDecalData.cs:
datablock DecalData(gg_decal)
{
textureCoordCount = "0";
Material = "gg_marker";
};
Spawning the Marker
Now that we have a destination marker, we need to add it upon
clicking on the terrain and then delete it when our player reaches its
destination. Start by opening game/scripts/gui/playGui.cs. Find the PlayGui::onRightMouseDown function. At the end of this function, add the following code:
%ray = VectorScale(%ray, 1000);
%end = VectorAdd(%start, %ray);
// only care about terrain objects
%searchMasks = $TypeMasks::TerrainObjectType | $TypeMasks::StaticTSObjectType |
$TypeMasks::InteriorObjectType | $TypeMasks::ShapeBaseObjectType |
$TypeMasks::StaticObjectType;
// search!
%scanTarg = ContainerRayCast( %start, %end, %searchMasks);
if (%scanTarg)
{
// Get access to the AI player we control
%ai = LocalClientConnection.player;
// 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 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);
}
// 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);
}
Save your script, then run the game. When you right click on the
terrain, the GarageGames symbol should render as a decal at the
destination.
(click to enlarge)
Erasing the Marker
The last thing we need to do is erase the destination marker when our AI player gets to it. Open the game/art/datablocks/BoomBot.cs file, then add the following:
// This is a callback function
// This is automatically called by the engine as part
// of the AI routine
// %this - The BoomBotData datablock
// %obj - The instance of this datablock (our AI Player)
function BoomBotData::onReachDestination(%this, %obj)
{
// If there was a decal placed, then it was
// stored in this %obj variable (see playGui.cs)
// Erase the decal using the decal manager
if( %obj.decal > -1 )
decalManagerRemoveDecal(%obj.decal);
}
Now, when the AI player reaches its destination the marker will be deleted.
Camera Modes
Now that you've got control of your character, it's time to discuss the camera controls. We've
decided on a two-mode approach so that you can use the Overhead mode to observe
the battlefield and the OrbitObject mode so that you can follow a specific unit.
Orbit Camera
Open the scripts/server/commands.cs script and find the
serverCmdorbitCam() function:
Lets break this command down.
%client.camera.setOrbitObject() puts the camera into OrbitObject mode. The first
argument is the object to orbit around. %client is provided by our caller
when the server command is called.
The second argument is a vector representing the angle of
the camera in (x, y, z) or (pitch, roll, yaw) if you prefer. Here it is
pitched 20 degrees down, with 0 roll and 0 yaw.
The next three arguments are the allowed distances from the target: min
distance, max distance and current distance: here 0, 5.5 and 5.5
respectively. The last two arguments should be sent as floating point numbers or
odd results can occur. This function may take additional optional parameters: an
ownership flag denoting if the object orbited by the camera belongs to
the camera's client, an offset if the camera should focus
somewhere other than the object's center, and a flag specifying if the
camera should be locked.
The next line sets the camera distance from the orbit target to 5.5 units.
The final line sets the controlMode to OrbitObject.
Overhead Camera
Cameras used by RTS games are slightly different from the Hack & Slash or Fly
cameras. They are characterized by a camera that moves laterally along the x and
y axis, but generally not in z. This can be realized in T3D by using the
"Overhead" camera mode.
In scripts/server/commands.cs find the
serverCmdoverheadCam() function:
With this setup, the camera will be free to move around with the
standard "wasd" controls, but it will not move vertically in the world.
By default the 'e' key should move the camera up and the 'c' key
should move the camera down if you want to adjust the camera's height above the
terrain.
For the traditional RTS players who wish to use the mouse
wheel, we implemented mouse wheel zoom in this function from the Camera Setup
section (scripts/server/commands.cs):
// Adjusts the height of the camera using the mouse wheel
function serverCmdadjustCamera(%client, %adjustment)
{
if(%client.camera.controlMode $= "OrbitObject")
{
if(%adjustment == 1)
%n = %client.camera.camDist + 0.5;
else
%n = %client.camera.camDist - 0.5;
if(%n < 0.5)
%n = 0.5;
if(%n > 15)
%n = 15.0;
%client.camera.setOrbitObject(%client.player, %client.camera.getRotation(),
0, %n, %n);
%client.camera.camDist = %n;
}
if(%client.camera.controlMode $= "Overhead")
{
%client.camera.position = VectorAdd(%client.camera.position, "0 0 " @ %adjustment);
}
}
Notice that this function catches the camera mode and uses an appropriate method
for adjusting the camera's position by checking the controlMode
member's value.
In the
above code, we are sticking to the client/server architecture of Torque
3D. Typically, actions such as navigating through GUIs, rendering, and
input are handled on the client. However, when actions have an effect
on the game, they should be performed on the server.
Camera location can usually be handled as a client operation, but this
is a good opportunity to show off the client/server communication. Also, in
multiplayer games it is important to remember that the server scopes visibility
for the clients. Camera position should stay in sync to ensure that this scoping
is accurate. The
default.bind.cs is a client script, which contains the client function
mouseZoom(...). This is only called when there is a client action, such
as the mouse wheel input.
Once the client action has been performed, a message is
sent to the server to act on it: commandToServer('adjustCamera',
-1);. The first parameter is the name of the server command/function to
call (minus the serverCmd prefix), and the rest of the parameters are arguments used by the command.
In this situation, based on the direction of the mouse wheel rotation a
positive or negative 1 will be sent to the server command which uses this value to adjust the camera.
Now that the functions are set up, all that is left is creating a key bind to call them. Back in
default.bind.cs
we added the following binding to the script:
moveMap.bind( mouse, zaxis, mouseZoom );
This allows you to zoom in and out on your actor using your mouse's scroll wheel
in orbit mode and adjust camera height in overhead mode..
If you want to play around with the camera settings created in this tutorial,
examine the following code that we added at the bottom of game/scripts/server/commands.cs in the
Camera Setup section.
Here we're going to talk about what to do with that orc burrow. We need a
place to get more friendly units and the burrow seems the logical place, but it
needs a few adjustments to work. I'm not going into resource costs or
build times - call them exercises for the reader - we're just going to pop out a
new friendly unit when you left-click on the burrow.
First, we'll add some code to the serverCmdcheckTarget()
function serverCmdcheckTarget(%client, %pos, %start, %ray)
{
%ray = VectorScale(%ray, 1000);
%end = VectorAdd(%start, %ray);
// Add new typemasks to the search so we can find clicks on barracks too
%searchMasks = $TypeMasks::PlayerObjectType | $TypeMasks::StaticTSObjectType
| $TypeMasks::StaticObjectType;
// Search!
%scanTarg = ContainerRayCast( %start, %end, %searchMasks);
// If an enemy AI object was found in the scan
if( %scanTarg )
{
// Get the enemy ID
%target = firstWord(%scanTarg);
if (%target.class $= "barracks")
{
serverCmdspawnTeammate(%client, %target);
}
else if (%target.getClassName() $= "AIPlayer")
{
if (%target.team != 1)
{
// Cause our AI object to aim at the target
// offset (0, 0, 1) so you don't aim at the target's feet
if (isObject(Team1List))
{
%c = 0;
%unit = Team1List.getObject(0);
while (isObject(%unit))
{
if (%unit.isSelected)
{
%unit.mountImage(Lurker, 0);
%targetData = %target.getDataBlock();
%z = getWord(%targetData.boundingBox, 2) * 2;
%offset = "0 0" SPC %z;
%unit.setAimObject(%target, %offset);
// Tell our AI object to fire its weapon
%unit.setImageTrigger(0, 1);
}
%c++;
%unit = Team1List.getObject(%c);
}
}
}
else
{
if ($SelectToggled)
{
multiSelect(%target);
}
else
{
cleanupSelectGroup();
%target.isSelected = true;
%target.isLeader = true;
}
}
}
else
{
serverCmdstopAttack(%client);
if (!$SelectToggled)
cleanupSelectGroup();
}
}
else
{
serverCmdstopAttack(%client);
if (!$SelectToggled)
cleanupSelectGroup();
}
}
I also added some support code for handling selection of multiple units and a little bit for
spawning new bots from our orc burrows.
Add the following code in scripts/server/gameCore.cs at the end of the GameCore::spawnPlayer() function.
%player.team = 1;
if (!isObject(Team1List))
{
new SimSet(Team1List);
MissionCleanup.add(Team1List);
}
Team1List.add(%player);
This ties in with our new multi-select system so that you can continue to select and control the original player object.
We'll need to revisit serverCmdstopAttack() to handle our multi-select system, too. It should look like this:
function serverCmdstopAttack(%client)
{
// If no valid target was found, or left mouse
// clicked again on terrain, stop firing and aiming
for (%c = 0; %c < Team1List.getCount(); %c++)
{
%unit = Team1List.getObject(%c);
%unit.setAimObject(0);
%unit.schedule(150, "setImageTrigger", 0, 0);
}
}
Next, we'll add some more server commands to help us with managing our army.
Our first step is to extend our serverCmdcreateBuilding() function to include a
new spawn point that is associated with the structure to use as our troop
creation point. The new version should look like this:
function serverCmdcreateBuilding(%client, %pos, %start, %ray)
{
// find end of search vector
%ray = VectorScale(%ray, 2000);
%end = VectorAdd(%start, %ray);
%searchMasks = $TypeMasks::TerrainObjectType;
// search!
%scanTarg = ContainerRayCast( %start, %end, %searchMasks);
// If the terrain object was found in the scan
if( %scanTarg )
%obj = getWord(%scanTarg, 0);
%pos = getWords(%scanTarg, 1, 3);
// spawn a new object at the intersection point
%obj = new TSStatic()
{
position = %pos;
shapeName = "art/shapes/building/orcburrow.dts";
class = "barracks";
collisionType = "Visible Mesh";
scale = "0.5 0.5 0.5";
};
// Add the new object to the MissionCleanup group
MissionCleanup.add(%obj);
// Set up a spawn point for new troops to arrive at.
if (!isObject(Team1SpawnGroup))
{
new SimGroup(Team1SpawnGroup)
{
canSave = "1";
canSaveDynamicFields = "1";
enabled = "1";
};
MissionGroup.add(Team1SpawnGroup);
}
%spawnName = "team1Spawn" @ %obj.getId();
%point = new SpawnSphere(%spawnName)
{
radius = "1";
dataBlock = "SpawnSphereMarker";
spawnClass = $Game::DefaultPlayerClass;
spawnDatablock = $Game::DefaultPlayerDataBlock;
};
%point.position = VectorAdd(%obj.getPosition(), "0 5 2");
Team1SpawnGroup.add(%point);
MissionCleanup.add(%point);
}
}
Next we'll add a function to spawn a new bot and equip it with a weapon and some ammo.
function serverCmdspawnTeammate(%client, %source)
{
// Create a new, generic AI Player
// Position will be at the camera's location
// Datablock will determine the type of actor
%spawnName = "team1Spawn" @ %source.getId();
// Defaults
%spawnClass = $Game::DefaultPlayerClass;
%spawnDataBlock = $Game::DefaultPlayerDataBlock;
// Overrides by the %spawnPoint
if (isDefined("%spawnName.spawnClass"))
{
%spawnClass = %spawnName.spawnClass;
%spawnDataBlock = %spawnName.spawnDatablock;
}
else if (isDefined("%spawnName.spawnDatablock"))
{
// This may seem redundant given the above but it allows
// the SpawnSphere to override the datablock without
// overriding the default player class
%spawnDataBlock = %spawnName.spawnDatablock;
}
%spawnProperties = %spawnName.spawnProperties;
%spawnScript = %spawnName.spawnScript;
// Spawn with the engine's Sim::spawnObject() function
%newBot = spawnObject(%spawnClass, %spawnDatablock, "",
%spawnProperties, %spawnScript);
%spawnLocation = GameCore::pickPointInSpawnSphere(%newBot, %spawnName);
%newBot.setTransform(%spawnLocation);
%newBot.team = 1;
%newBot.clearWeaponCycle();
%newBot.setInventory(Lurker, 1);
%newBot.setInventory(LurkerClip, %newBot.maxInventory(LurkerClip));
%newBot.setInventory(LurkerAmmo, %newBot.maxInventory(LurkerAmmo));
%newBot.addToWeaponCycle(Lurker);
if (%newBot.getDatablock().mainWeapon.image !$= "")
{
%newBot.mountImage(%newBot.getDatablock().mainWeapon.image, 0);
}
else
{
%newBot.mountImage(Lurker, 0);
}
// This moves our new bot away from the front door a ways to make room for
// other bots as we spawn them.
%x = getRandom(-10, 10);
%y = getRandom(4, 10);
%vec = %x SPC %y SPC "0";
%dest = VectorAdd(%newBot.getPosition(), %vec);
%newBot.setMoveDestination(%dest);
addTeam1Bot(%newBot);
}
We also need our addTeam1Bot() support function:
function addTeam1Bot(%bot)
{
// We'll create a SimSet to track our Team1 bots if it hasn't been created already
if (!isObject(Team1List))
{
new SimSet(Team1List);
MissionCleanup.add(Team1List);
}
// And then add our bot to the Team1 list.
Team1List.add(%bot);
}
At this point we're ready to spawn units from our structures. If you
test the game now, you should be able to create a new burrow and it should spawn
bots when you click on it.
A real-time strategy game isn't much unless you can select and direct your
units. Next, we'll add a few more server commands and a client command to
help with selecting and moving single and multiple units.
First we'll add the ability to "select" multiple units.
Still in scripts/server/commands.cs, add the following
functions:
function serverCmdtoggleMultiSelect(%client, %flag)
{
if (%flag)
$SelectToggled = true;
else
$SelectToggled = false;
}
function multiSelect(%target)
{
if (!isObject(Team1List))
{
new SimSet(Team1List);
MissionCleanup.add(Team1List);
}
%leader = findTeam1Leader();
if (isObject(%leader))
{
%target.destOffset = VectorSub(%leader.getPosition(), %target.getPosition());
}
else
{
%target.destOffset = "0 0 0";
%target.isLeader = true;
}
%target.isSelected = true;
}
function findTeam1Leader()
{
if (!isObject(Team1List))
{
new SimSet(Team1List);
MissionCleanup.add(Team1List);
}
for (%c = 0; %c < Team1List.getCount(); %c++)
{
%unit = Team1List.getObject(%c);
if (%unit.isLeader)
return %unit;
}
return 0;
}
function cleanupSelectGroup()
{
if (!isObject(Team1List))
{
new SimSet(Team1List);
MissionCleanup.add(Team1List);
}
for (%c = 0; %c < Team1List.getCount(); %c++)
{
%temp = Team1List.getObject(%c);
%temp.isSelected = false;
%temp.isLeader = false;
%temp.destOffset = "0 0 0";
}
}
First,
serverCmdtoggleMultiSelect() just takes a flag and sets a global variable to let
the system know when we want to start adding units to our selection list.
The multiSelect() function actually handles setting up
the list by setting a member variable on the bot to indicate that it has been
selected. Additionally, if there is no other bot in the Team1List SimSet
that is selected this bot is designated the "leader." All of the
subsequently selected bots will calculate offset destination targets based on
this bot's destination. The findTeam1Leader()
utility function just searches the Team1List for a designated "leader" unit.
Finally, the cleanupSelectGroup() utility function just clears the
selection variables from all of Team1List's members.
Next, modify PlayGui::onRightMouseDown() in scripts/gui so that it
looks like the following:
function PlayGui::onRightMouseDown(%this, %pos, %start, %ray)
{
commandToServer('movePlayer', %pos, %start, %ray);
%ray = VectorScale(%ray, 1000);
%end = VectorAdd(%start, %ray);
// only care about terrain objects
%searchMasks = $TypeMasks::TerrainObjectType | $TypeMasks::StaticTSObjectType |
$TypeMasks::InteriorObjectType | $TypeMasks::ShapeBaseObjectType
| $TypeMasks::StaticObjectType;
// search!
%scanTarg = ContainerRayCast( %start, %end, %searchMasks);
if (%scanTarg)
{
%obj = getWord(%scanTarg, 0);
// 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);
// Create a new decal using the decal manager
// arguments are (Position, Normal, Rotation, Scale, Datablock, Permanent)
// We are now just letting the decals clean up after themselves.
decalManagerAddDecal(%pos, %norm, 0, 1, "gg_decal", false);
}
}
This change eliminates the need to track which bot is the "leader" and which bot
which decal belongs to - it simply lets the decal expire.
In order to move our group of selected units together we'll have to update our serverCmdmovePlayer() function to
tell all of our units where to go.
function serverCmdmovePlayer(%client, %pos, %start, %ray)
{
//echo(" -- " @ %client @ ":" @ %client.player @ " moving");
// Get access to the AI player we control
%ai = findTeam1Leader();
%ray = VectorScale(%ray, 1000);
%end = VectorAdd(%start, %ray);
// only care about terrain objects
%searchMasks = $TypeMasks::TerrainObjectType | $TypeMasks::StaticTSObjectType |
$TypeMasks::InteriorObjectType | $TypeMasks::ShapeBaseObjectType |
$TypeMasks::StaticObjectType;
// search!
%scanTarg = ContainerRayCast( %start, %end, %searchMasks);
// If the terrain object was found in the scan
if( %scanTarg )
{
%pos = getWords(%scanTarg, 1, 3);
// Get the normal of the location we clicked on
%norm = getWords(%scanTarg, 4, 6);
// Set the destination for the AI player to
// make him move
if (isObject(Team1List))
{
%c = 0;
%end = Team1List.getCount();
%unit = Team1List.getObject(0);
while (isObject(%unit))
{
if (%unit.isSelected)
{
%dest = VectorSub(%pos, %unit.destOffset);
%unit.setMoveDestination( %dest );
}
%c++;
if (%c < %end)
%unit = Team1List.getObject(%c);
else
%unit = 0;
}
}
else
%ai.setMoveDestination( %pos );
}
}
Now we have to modify scripts/player/default.bind.cs to add some new functions
and a new bind right after our bind to spawn enemy targets.
Now we have ctrl-X bound to tell our system to toggle multi-selection
via the addSelect() and dropSelect() functions
on make and break respectively. This key combination was chosen
arbitrarily and you can of course use any key you like. Note that at the
moment shift only catches the "make" (in other words, it only
catches the event when you press the key down) and not the "break," so if
you use it you'll have to write the function to toggle between multi-selection
and single selection when shift is pressed.
If you test things now you should be able to select multiple units after you
have spawned them and right-click to send them all off together. Note that
you will need to click somewhere very near the units' pelvis node to actually
select them.
If you use the 'b' key to spawn a target and then left-click on it while
multiple units are selected they will all attack the target. You will need
to left-click the terrain to stop them from attacking. Left-clicking the
terrain will also clear your current selection group.
Now open scripts/server/player.cs, find Armor::onDisabled() and
add the following code after the call to %obj.playDeathAnimation():
This will search our Team1List and stop any units who are currently attacking %obj (the dying object) to stop when
%obj dies.
That about wraps it up. You should now have a pretty functional RTS prototype with unit control, unit spawning and some
other basic features that are typical of the genre.
Conclusion
The purpose of this tutorial was to show you some of the more
advanced capabilities of TorqueScript, and combine the language with
Torque 3D's visual editors to create a prototype game. As you just
experienced, getting a non-FPS prototype game started does not take
long.
Make sure you have read through all the comments
accompanying the new code, as they are part of the tutorial. At this
point you can move on to other tutorials, or improve upon the code to
create something more unique. There is always room for improvement,
such as:
Changing the units' weapons out for rocket launchers or grenades
Make the targets move around and attack the player or the players units
Add key bindings to change camera modes on the fly
You can download the completed scripts by CLICKING HERE.
If you wish to download the scripts and assets in a single file, CLICK HERE.