RTS Prototype: Part 1
Introduction
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 unit navigation
- 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
NOTE: Changes between Torque 3D MIT 3.0 and 3.5 have broken this prototype.
If you are using Torque 3D MIT 3.5 or later you will need to be intimately familiar with Torque 3D and the changes
that were made between the 3.0 and 3.5 releases. The premade scripts that are provided have been reported to function
incorrectly when transplanted directly and would need extensive debugging to get them working properly.
You have been warned.
Create A Fresh Project
We're going to start with a clean project. I usually do this manually using the following steps:
- Copy the Full folder from Templates to My Projects.
- Rename the Full folder. In this instance, rename it to RTSProto.
- Rename the game/Full.torsion file to match my project folder (RTSProto).
- Edit the buildFiles/config/project.conf file:
- Change the project name in the Torque3D::beginConfig line to match your project (RTSProto).
- Add the navigation module by adding includeModule( 'navigation' ); inside the config block.
- Run the generateProjects.bat file to generate the updated solution and project files.
- Build the project in your preferred compiler.
The Full Template used for this tutorial project should contain base art and scripts needed to run the game. We will be creating a few custom assets for the project as
we go along.
First Steps
Navigation
We need to add a navigation mesh to our mission so that we know where we can place buildings and to allow units to path correctly around the mission
area. Start the game and open Empty Terrain.mis in the World Editor. Once it is open, select the Mission Area Editor from
the Editor rollout:
Next, adjust the mission area to fill most of the map by dragging the red corner nodes:
Then select the Navigation Editor from the Editor rollout:
In the New NavMesh dialog, set the name to NavEmptyTerrain and select both Fit NavMesh to mission area and Include terrain. Finally, click the Create! button.
Now we need to configure and build the navigation mesh. Set the name to NavEmptyTerrain, the fileName to levels/NavEmptyTerrain.nav, update the scale so that the third element (z) is around 500(just to be sure all objects added later are correctly added to the nav mesh), and the walkableSlope to 36 (our player datablock sets the maximum walkable slope to 38 degrees). Then select Play sound when done and click the Build NavMesh button. This will take a few minutes.
You should now have a navigation mesh that covers the entire mission area.
Loadout
Now, in scripts/server/gameDM.cs we'll change the default player class and
datablock. In DeathMatchGame::initGameVars() make the
following change:
$Game::DefaultPlayerClass = "AiPlayer";
$Game::DefaultPlayerDataBlock = "DemoPlayer";
This gives us the ability to tell our unit where to go and let the AI class
handle getting it there.
Now open art/datablocks/aiPlayer.cs and update DemoPlayer to look like this:
datablock PlayerData(DemoPlayer : DefaultPlayerData)
{
shootingDelay = 2000;
mainWeapon = Lurker;
};
Next, in game/scripts/server/gameCore.cs find GameCore::loadOut() and comment out all of the
weapons except the Lurker. Then mount the Lurker to slot 0 instead of the Ryder.
function GameCore::loadOut(%game, %player)
{
//echo (%game @"\c4 -> "@ %game.class @" -> GameCore::loadOut");
%player.clearWeaponCycle();
//%player.setInventory(Ryder, 1);
//%player.setInventory(RyderClip, %player.maxInventory(RyderClip));
//%player.setInventory(RyderAmmo, %player.maxInventory(RyderAmmo));
//%player.addToWeaponCycle(Ryder);
%player.setInventory(Lurker, 1);
%player.setInventory(LurkerClip, %player.maxInventory(LurkerClip));
%player.setInventory(LurkerAmmo, %player.maxInventory(LurkerAmmo));
%player.addToWeaponCycle(Lurker);
//%player.setInventory(LurkerGrenadeLauncher, 1);
//%player.setInventory(LurkerGrenadeAmmo, %player.maxInventory(LurkerGrenadeAmmo));
//%player.addToWeaponCycle(LurkerGrenadeLauncher);
//%player.setInventory(ProxMine, %player.maxInventory(ProxMine));
//%player.addToWeaponCycle(ProxMine);
//%player.setInventory(DeployableTurret, %player.maxInventory(DeployableTurret));
//%player.addToWeaponCycle(DeployableTurret);
if (%player.getDatablock().mainWeapon.image !$= "")
{
%player.mountImage(%player.getDatablock().mainWeapon.image, 0);
}
else
{
%player.mountImage(Lurker, 0);
}
}
Now our character will spawn with only the lurker in its inventory and will equip it automatically.
Camera Setup
There are several camera modes available in Torque 3D that are generally unused because most people associate the engine with first person shooters. While the examples and general designe of the engine are pre-disposed toward this, there are many features that let us break out of that mold. Changing the camera modes is a good place to break out of the obvious FPS box.
A note on server and client command functions
There are several instances where you will use the commandToServer() and commandToClient() functions. The remote function you wish to call must be prefixed with "serverCmd" or "clientCmd". The first argument is the name of the remote command you wish to call without the related prefix, and that name must be quoted with single quotes. For example:
// a server command - these are usually placed in scripts/server/commands.cs
serverCmdmyCommand(%client){ // do something here }
// from a client-side script
commandToServer('myCommand'); // note ' instead of "
Additionally, you can generally separate client and server scripts by their folder locations. Client-side scripts are in scripts/client and scripts/gui (GUI scripts are client-side by nature) and server scripts are in scripts/server. Game assets and datablocks are all loaded server-side and verified client-side at game start. This also applies to the content of the core folder.
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 be added to the end of the script file:
// ----------------------------------------------------------------------------
// Camera commands
// ----------------------------------------------------------------------------
function serverCmdorbitCam(%client)
{
orbitCam(%client);
}
function orbitCam(%client)
{
%client.camera.setOrbitObject(%client.player, mDegToRad(20) SPC "0 0", 0, 5.5, 5.5);
%client.camera.camDist = 5.5;
%client.camera.controlMode = "OrbitObject";
}
function serverCmdoverheadCam(%client)
{
overheadCam(%client);
}
function overheadCam(%client)
{
%client.camera.position = VectorAdd(%client.player.position, "0 0 30");
%client.camera.lookAt(%client.player.position);
%client.camera.controlMode = "Overhead";
}
function serverCmdtoggleCamMode(%client)
{
toggleCamMode(%client);
}
function toggleCamMode(%client)
{
if(%client.camera.controlMode $= "Overhead")
orbitCam(%client);
else if(%client.camera.controlMode $= "OrbitObject")
orbitCam(%client);
}
function serverCmdadjustCamera(%client, %adjustment)
{
adjustCamera(%client, %adjustment);
}
function adjustCamera(%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.0)
%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" SPC %adjustment);
}
Additionally, in scripts/client/default.bind.cs we'll add some
keybinds and utility functions to assist in controlling the camera:
function toggleCameraMode(%val)
{
if (%val)
commandToServer('toggleCamMode');
}
moveMap.bind( keyboard, "ctrl m", toggleCameraMode);
function mouseZoom(%val)
{
if(%val > 0)
{
commandToServer('adjustCamera', -1);
}
else
{
commandToServer('adjustCamera', 1);
}
}
moveMap.bind(mouse, "zaxis", mouseZoom);
Now we have to track down references to Reticle and zoomReticle in scripts/client/default.bind.cs (while we're here) and scripts/client/client.cs just to clean up console errors:
// in scripts/client/default.bind.cs
function turnOffZoom()
{
ServerConnection.zoomed = false;
setFov(ServerConnection.getControlCameraDefaultFov());
//Reticle.setVisible(true);
//zoomReticle.setVisible(false);
// Rather than just disable the DOF effect, we want to set it to the level's
// preset values.
//DOFPostEffect.disable();
ppOptionsUpdateDOFSettings();
}
// in scripts/client/client.cs
function clientCmdRefreshWeaponHUD(%amount, %preview, %ret, %zoomRet, %amountInClips)
{
if (!%amount)
AmmoAmount.setVisible(false);
else
{
AmmoAmount.setVisible(true);
AmmoAmount.setText("Ammo: " @ %amount @ "/" @ %amountInClips);
}
if (%preview $= "")
WeaponHUD.setVisible(false);//PreviewImage.setVisible(false);
else
{
WeaponHUD.setVisible(true);//PreviewImage.setVisible(true);
PreviewImage.setbitmap("art/gui/weaponHud/"@ detag(%preview));
}
if (%ret $= "")
{
// Add braces to avoid parse error - or remove this block entirely
//Reticle.setVisible(false);
}
else
{
//Reticle.setVisible(true);
//Reticle.setbitmap("art/gui/weaponHud/"@ detag(%ret));
}
if (isObject(ZoomReticle))
{
if (%zoomRet $= "")
{
ZoomReticle.setBitmap("");
}
else
{
ZoomReticle.setBitmap("art/gui/weaponHud/"@ detag(%zoomRet));
}
}
}
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:
%game.spawnPlayer(%client, %playerSpawnPoint, false);
The function call being modified is GameCore::spawnPlayer(%game, %this, %spawnPoint, %noControl), located in game/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 overhead 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 an overhead 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 the GUI Editor from the main menu by clicking the GUI Editor button, by pressing F10, or by clicking the GUI Editor
button from within the World Editor.
Open the PlayGui gui by clicking the GUI collection dropdown and selecting it from the list.
Delete the DamageHUD element and its children. First, either select the Reticle object from the element tree on the right or click in the center of the GUI, then delete it.
Delete the Reticle and ZoomReticle objects using the same methods.
Now to add our simple build button. 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:
Save this image in your art/gui folder:
Browse to your project's art/gui folder and select the
InfantryIcon.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 InfantryBld::onClick(%this)
{
PlayGui.placingBuilding = true;
}
Then, in scripts/server/commands.cs add the following function to the end:
function serverCmdcreateBuilding(%client, %pos, %start, %ray)
{
createBuilding(%client, %pos, %start, %ray);
}
function createBuilding(%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/station/station01.dts";
collisionType = "Visible Mesh";
scale = "0.5 0.5 0.5";
};
// find our nav mesh and update it so units will avoid the building
// that we just placed:
%navMesh = 0;
%count = MissionGroup.getCount();
for(%i = 0; %i < %count; %i++)
{
%missionObj = MissionGroup.getObject(%i);
if(%missionObj.getClassName() $= "NavMesh")
{
%navMesh = %missionObj;
break;
}
}
if (%navMesh > 0)
NavMeshUpdateOne(%navMesh, %obj);
// 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 building.
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. 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)
{
movePlayer(%client, %pos, %start, %ray);
}
function movePlayer(%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.setPathDestination( %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 of 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 spawnEnemy(%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
%enemy = new AIPlayer()
{
position = LocalClientConnection.camera.getPosition();
datablock = "DemoPlayer";
};
%game.loadOut(%enemy);
}
}
// Bind the function spawnEnemy to the keyboard 'b' key
moveMap.bind(keyboard, b, spawnEnemy);
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 to half unit height so you don't aim at the target's feet
%datablock = %target.getDatablock();
%offset = "0 0<EMAIL_ADDRESS>/ 2;
%player.setAimObject(%target, %offset);
%player.target = %target;
// Tell our AI object to fire its weapon
%player.setImageTrigger(0, 1);
}
else
{
stopAttack(%client);
}
}
else
{
stopAttack(%client);
}
}
function stopAttack(%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);
}
function serverCmdstopAttack(%client)
{
stopAttack(%client);
}
Open scripts/server/weapon.cs and in WeaponImage::onFire() and update it to this:
function WeaponImage::onFire(%this, %obj, %slot)
{
//echo("\c4WeaponImage::onFire( "@%this.getName()@", "@%obj.client.nameBase@", "@%slot@" )");
// Make sure we have valid data
if (!isObject(%this.projectile))
{
error("WeaponImage::onFire() - Invalid projectile datablock");
return;
}
// Decrement inventory ammo. The image's ammo state is updated
// automatically by the ammo inventory hooks.
if ( !%this.infiniteAmmo )
%obj.decInventory(%this.ammo, 1);
// Get the player's velocity, we'll then add it to that of the projectile
%objectVelocity = %obj.getVelocity();
%numProjectiles = %this.projectileNum;
if (%numProjectiles == 0)
%numProjectiles = 1;
if (%obj.isMemberOfClass("AIPlayer") && isObject(%obj.target))
{
%vec = %obj.getVectorTo(%obj.target.position);
if(%obj.aimOffset)
%vec = VectorAdd(%vec, "0 0 "@%obj.aimOffset);
%vec = VectorNormalize(%vec);
}
else
{
// getting the straight ahead aiming point of the gun
%vec = %obj.getMuzzleVector(%slot);
}
for (%i = 0; %i < %numProjectiles; %i++)
{
if (%this.projectileSpread)
{
// We'll need to "skew" this projectile a little bit. We start by
// Then we'll create a spread matrix by randomly generating x, y, and z
// points in a circle
%matrix = "";
for(%j = 0; %j < 3; %j++)
%matrix = %matrix @ (getRandom() - 0.5) * 2 * 3.1415926 * %this.projectileSpread @ " ";
%mat = MatrixCreateFromEuler(%matrix);
// Which we'll use to alter the projectile's initial vector with
%muzzleVector = MatrixMulVector(%mat, %vec);
}
else
{
// Weapon projectile doesn't have a spread factor so we fire it using
// the straight ahead aiming point of the gun
%muzzleVector = %vec;
}
// Add player's velocity
%muzzleVelocity = VectorAdd(
VectorScale(%muzzleVector, %this.projectile.muzzleVelocity),
VectorScale(%objectVelocity, %this.projectile.velInheritFactor));
// Create the projectile object
%p = new (%this.projectileType)()
{
dataBlock = %this.projectile;
};
%p.initialVelocity = %muzzleVelocity;
%p.initialPosition = %obj.getMuzzlePoint(%slot);
%p.sourceObject = %obj;
%p.sourceSlot = %slot;
%p.client = %obj.client;
%p.sourceClass = %obj.getClassName();
MissionCleanup.add(%p);
}
}
Next, open scripts/server/aiPlayer.cs and add this method:
// Return position vector to a position
///
/// This function calculates the vector to %pos from eye point
///
/// The target position.
/// Returns a 3D vector from eye pos to target pos (not normalized).
function AIPlayer::getVectorTo(%this, %target)
{
if (getWordCount(%pos) < 2 && isObject(%target))
%pos = %target.getPosition();
%z = getWord(%this.boundingBox, 2) / 2;
%offset = "0 0" SPC %z;
%vec = VectorAdd(%offset, %this.getPosition());
%z = getWord(%target.boundingBox, 2) / 2;
%offset = "0 0" SPC %z;
%pos = VectorAdd(%offset, %pos);
return VectorSub(%pos, %vec);
}
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 only fires a single shot at a time
- Enemy may 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.
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
You can download this image to use by right-clicking and selecting "save image as" from the context menu.
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:
singleton Material(DECAL_destDecal)
{
mapTo = "unmapped_mat";
diffuseMap[0] = "art/decals/destDecal.png";
castShadows = "0";
translucent = "1";
translucentZWrite = "1";
alphaTest = "1";
alphaRef = "80";
showFootprints = "0";
materialTag0 = "Miscellaneous";
};
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.
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 scripts/client/client.cs. At the end of this file, add the following code:
function clientCmdsetDestDecal(%unit, %position, %norm)
{
// If the AI player already has a decal (0 or greater)
// tell the decal manager to delete the instance of the gg_decal
if(%unit.decal > -1)
{
decalManagerRemoveDecal(%unit.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
%unit.decal = decalManagerAddDecal(%position, %norm, 0, 1, "destDecal", true);
}
Then, in scripts/server/commands.cs, modify movePlayer() to tell the client to spawn the decal:
// ....
// 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.setPathDestination( %pos );
// add this!
commandToClient(%client, 'setDestDecal', %ai, %pos, %norm);
}
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.
Erasing the Marker
The last thing we need to do is erase the destination marker when our AI player gets to it. Open the scripts/server/aiPlayer.cs file, then add the following to the end of DemoPlayer::onReachDestination():
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:
function serverCmdorbitCam(%client)
{
orbitCam(%client);
}
function orbitCam(%client)
{
%client.camera.setOrbitObject(%client.player, mDegToRad(20) SPC "0 0", 0, 5.5, 5.5);
%client.camera.camDist = 5.5;
%client.camera.controlMode = "OrbitObject";
}
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:
function serverCmdoverheadCam(%client)
{
overheadCam(%client);
}
function overheadCam(%client)
{
%client.camera.position = VectorAdd(%client.player.position, "0 0 30");
%client.camera.lookAt(%client.player.position);
%client.camera.controlMode = "Overhead";
}
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)
{
adjustCamera(%client, %adjustment);
}
function adjustCamera(%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.0)
%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" SPC %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.
function serverCmdorbitCam(%client)
{
orbitCam(%client);
}
function orbitCam(%client)
{
%client.camera.setOrbitObject(%client.player, mDegToRad(20) SPC "0 0", 0, 5.5, 5.5);
%client.camera.camDist = 5.5;
%client.camera.controlMode = "OrbitObject";
}
function serverCmdoverheadCam(%client)
{
overheadCam(%client);
}
function overheadCam(%client)
{
%client.camera.position = VectorAdd(%client.player.position, "0 0 30");
%client.camera.lookAt(%client.player.position);
%client.camera.controlMode = "Overhead";
}
function serverCmdtoggleCamMode(%client)
{
toggleCamMode(%client);
}
function toggleCamMode(%client)
{
if(%client.camera.controlMode $= "Overhead")
orbitCam(%client);
else if(%client.camera.controlMode $= "OrbitObject")
orbitCam(%client);
}
function serverCmdadjustCamera(%client, %adjustment)
{
adjustCamera(%client, %adjustment);
}
function adjustCamera(%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.0)
%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" SPC %adjustment);
}
You can call these functions using the usual commandToServer syntax. Just type the following in the console (press ~)
commandToServer('orbitCam');
commandToServer('overheadCam');
Going More Real-Time Strategy
Here we're going to talk about what to do with that station. We need a
place to get more friendly units and the station seems the logical place, but it
needs a few adjustments to work. I'm not going into resource costs or
build times - we'll cover that in part 2 - we're just going to pop out a
new friendly unit when you left-click on the station.
First, we'll overhaul GameCore::spawnPlayer() in scripts/server/gameCore.cs. We need to ditch the vestigial player spawn that comes with the FPS stuff and just drop the new player's camera directly at a spawn point. We're pretty much going to gut this since we don't need anything that has to do with setting up a player - just the client's camera.
function GameCore::spawnPlayer(%game, %client, %spawnPoint, %noControl)
{
//echo (%game @"\c4 -> "@ %game.class @" -> GameCore::spawnPlayer");
if (isObject(%client.player))
{
// The client should not already have a player. Assigning
// a new one could result in an uncontrolled player object.
error("Attempting to create a player for a client that already has one!");
}
// Attempt to treat %spawnPoint as an object
if (getWordCount(%spawnPoint) == 1 && isObject(%spawnPoint))
{
// Defaults
%spawnClass = $Game::DefaultPlayerClass;
%spawnDataBlock = $Game::DefaultPlayerDataBlock;
// Overrides by the %spawnPoint
if (isDefined("%spawnPoint.spawnClass"))
{
%spawnClass = %spawnPoint.spawnClass;
%spawnDataBlock = %spawnPoint.spawnDatablock;
}
else if (isDefined("%spawnPoint.spawnDatablock"))
{
// This may seem redundant given the above but it allows
// the SpawnSphere to override the datablock without
// overriding the default player class
%spawnDataBlock = %spawnPoint.spawnDatablock;
}
%spawnProperties = %spawnPoint.spawnProperties;
%spawnScript = %spawnPoint.spawnScript;
%client.spawnCamera(%spawnPoint);
}
// If the player's client has some owned turrets, make sure we let them
// know that we're a friend too.
if (%client.ownedTurrets)
{
for (%i=0; %i<%client.ownedTurrets.getCount(); %i++)
{
%turret = %client.ownedTurrets.getObject(%i);
%turret.addToIgnoreList(%player);
}
}
if (!isDefined("%client.skin"))
{
// Get available skins
%availableSkins = %spawnDataBlock.availableSkins; // TAB delimited list of skin names
// Set skin based on client's team. If client's team is not set, set it to 1
if(%client.team $= "")
%client.team = 1;
%client.skin = addTaggedString( getField(%availableSkins, %client.team) );
}
if(!isObject(%client.TeamList))
{
%client.TeamList = new SimSet();
MissionCleanup.add(%client.TeamList);
}
%client.TeamList.add(%player);
// Give the client control of the camera if in the editor
if( $startWorldEditor )
{
%control = %client.camera;
%control.mode = "Fly";
EditorGui.syncCameraGui();
}
else
{
%control = %client.camera;
overheadCam(%client);
}
// Allow the player/camera to receive move data from the GameConnection. Without this
// the user is unable to control the player/camera.
if (!isDefined("%noControl"))
%client.setControlObject(%control);
if (isObject(%spawnPoint))
%control.setTransform(%spawnPoint.getTransform());
}
With this all we have to do is grab the spawn sphere in the mission editor and place it so that it is where we want our player camera to spawn. Open the mission in the Mission Editor and select the spawn sphere:
Now click Object in the menu, then find Drop Location > at Camera w/Rotation and click that.
Finally, click Object and then click Drop Selection.
The spawn sphere is now placed where the camera is and it is facing the same direction as the camera. When the player enters the mission his camera will be placed at the spawn sphere's location with its rotation.
Now, we'll add some code to the serverCmdcheckTarget() function to handle spawning. The function should look something like this:
function attack(%client, %unit)
{
%target = %unit.getAimObject();
if (%target.getState() $= "Dead")
{
%unit.setAimObject(0);
}
else
{
%unit.setImageTrigger(0, 1);
%unit.schedule(64, "setImageTrigger", 0, 0);
schedule(128, 0, "attack", %client, %unit, %target);
}
}
function serverCmdcheckTarget(%client, %pos, %start, %ray)
{
checkTarget(%client, %pos, %start, %ray);
}
function checkTarget(%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")
{
spawnTeammate(%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(%client.TeamList))
{
%c = 0;
%unit = %client.TeamList.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
attack(%client, %unit);
}
%c++;
%unit = %client.TeamList.getObject(%c);
}
}
}
else
{
if ($SelectToggled)
{
multiSelect(%target);
}
else
{
cleanupSelectGroup();
%target.isSelected = true;
%target.isLeader = true;
}
}
}
else
{
stopAttack(%client);
if (!$SelectToggled)
cleanupSelectGroup();
}
}
else
{
stopAttack(%client);
if (!$SelectToggled)
cleanupSelectGroup();
}
}
We've also added some support code for handling selection of multiple units and a little bit for
spawning new bots from our stations.
We'll need to revisit serverCmdstopAttack() to handle our multi-select system, too. It should look like this:
function serverCmdstopAttack(%client)
{
stopAttack(%client);
}
function stopAttack(%client)
{
// If no valid target was found, or left mouse
// clicked again on terrain, stop firing and aiming
for (%c = 0; %c < %client.TeamList.getCount(); %c++)
{
%unit = %client.TeamList.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)
{
createBuilding(%client, %pos, %start, %ray);
}
function createBuilding(%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/station/station01.dts";
class = "barracks";
collisionType = "Visible Mesh";
scale = "0.5 0.5 0.5";
};
%navMesh = 0;
%count = MissionGroup.getCount();
for(%i = 0; %i < %count; %i++)
{
%missionObj = MissionGroup.getObject(%i);
if(%missionObj.getClassName() $= "NavMesh")
{
%navMesh = %missionObj;
break;
}
}
if (%navMesh > 0)
NavMeshUpdateOne(%navMesh, %obj);
// 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)
{
spawnTeammate(%client, %source);
}
function spawnTeammate(%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.setPathDestination(%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(%client.TeamList))
{
%client.TeamList = new SimSet();
MissionCleanup.add(%client.TeamList);
}
// And then add our bot to the Team1 list.
%client.TeamList.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 station 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(%client.TeamList))
{
%client.TeamList = new SimSet();
MissionCleanup.add(%client.TeamList);
}
%leader = findTeamLeader(%client);
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(%client.TeamList))
{
%client.TeamList = new SimSet();
MissionCleanup.add(%client.TeamList);
}
for (%c = 0; %c < %client.TeamList.getCount(); %c++)
{
%unit = %client.TeamList.getObject(%c);
if (%unit.isLeader)
return %unit;
}
return 0;
}
function cleanupSelectGroup()
{
if (!isObject(%client.TeamList))
{
%client.TeamList = new SimSet();
MissionCleanup.add(%client.TeamList);
}
for (%c = 0; %c < %client.TeamList.getCount(); %c++)
{
%temp = %client.TeamList.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 %client.TeamList 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 %client.TeamList for a designated "leader" unit.
Finally, the cleanupSelectGroup() utility function just clears the
selection variables from all of %client.TeamList'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);
}
In order to move our group of selected units together we'll have to update our movePlayer() function in scripts/server/commands.cs to
tell all of our units where to go.
function movePlayer(%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(%client.TeamList))
{
%c = 0;
%end = %client.TeamList.getCount();
%unit = %client.TeamList.getObject(0);
if(%ai $= "")
%ai = %unit;
while (isObject(%unit))
{
if (%unit.isSelected)
{
%dest = VectorSub(%pos, %unit.destOffset);
%unit.setPathDestination( %dest );
}
%c++;
if (%c < %end)
%unit = %client.TeamList.getObject(%c);
else
%unit = 0;
}
}
else
{
if (%ai !$= "")
%ai.setPathDestination( %pos );
}
commandToClient(%client, 'setDestDecal', %ai, %pos, %norm);
}
}
Now we have to modify scripts/client/default.bind.cs to add some new functions
and a new bind right after our bind to spawn enemy targets.
function addSelect()
{
$SelectToggled = true;
commandToServer('toggleMultiSelect', true);
}
function dropSelect()
{
$SelectToggled = false;
commandToServer('toggleMultiSelect', false);
}
moveMap.bindCmd( keyboard, "ctrl x", "addSelect();", "dropSelect();" );
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. The units should stop attacking when the target is dead. Left-clicking the
terrain will also clear your current selection group.
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
A special thanks to Michael Perry for the original RTS Prototype article and Steve Acaster for his
Tactics-Action Hybrid Game Tutorial
series, which saved me considerable time fiddling with the camera.
Continue in part 2.
|


Epilogues
- The "Play Scene" button now plays through to the end of the scene instead of just the current action.
- Increased the default scene size to 500x500.
Dialogue Editor
- Added a "Pose" sorting view which groups cases by pose name.
- Removed the "Add a Condition" search field because it uses the old format of conditions and its usage delays allowing the editor to fully modernize to the current condition format.
- Removed character names as valid tags. Existing cases will be converted to an opponent check.
Misc
- Tags in tags_dictionary.xml that have both gender and paired attributes will only apply the paired attribute if the character matches the gender. For example, the "chubby" tag can now imply "curvy" for only females.
- Added a button to the image import window to copy the generated code into the clipboard.
Bug Fixes
- Add Sprite, Speech Bubble, etc. buttons are now disabled while a scene transition is selected. Previously trying to use one of these with a transition active would cause a crash.
- Fixed issue where trying to name an action in an epilogue scene would make the name field disappear.
- Fixed issue where changing properties on the first keyframe of an action would sometimes not "stick".
- Fixed issue where sprites that changed their image source would rescale at the start of a new action, which is inconsistent with the in-game engine.
- Fixed issue where not all lines targeting tags were appearing in the Banter Wizard.
- Fixed a crash when trying to export a pose lineup with a missing base code.
- Added Unlisted as an epilogue status.
- Fixed issue where changing a sprite's source in a cross-stage image would display the non-cross-stage filename (ex. 3-happy.png instead of #-happy.png)
- Fixed the GIF button producing blank GIFs for cross-stage poses.


The International Conference on Advanced Production and Industrial Engineering (ICAPIE) is aimed to promote synergetic partnership between academia and industry and also to show case the growth of cutting edge technology developed in recent past in the areas of Production and Industrial Engineering. ICAPIE assembles the best talents in the academia & research institutions, industry experts, management professionals, engineers and executives to collaborate and address the current challenges in Production and Industrial Engineering, to seek new windows for discovery and exploration. It is an invitation to all scientist, engineers and technologist to create new paradigm using the new knowledge, vision and scientific intellect.
First conference ICAPIE 2016 (Dec 9-10, 2016) , second conference ICAPIE 2017 (Oct 6-7, 2017), third conference ICAPIE 2018 (Oct 5-6, 2018) and fourth conference ICAPIE 2019 (Dec 20-21, 2019) were held successfully at Delhi . The fifth conference ICAPIE 2020 (Dec 15-17, 2020) was cancelled due to the Pandemic COVID-19. The organisers are grateful to all authors and participants who have made significant contributions in the form of their papers. We also like to thank the reviewers for their comments and suggestions which helped in improving the quality of the papers. We hope that ICAPIE 2021 (June 18-20, 2021) conference also will prove to be a great success with the academia, researchers and engineers.
Dr. Ranganath M. Singari
Head, Department of Design
and
Professor , Department of Mechanical,
Production & Industrial Engineering
Delhi Technological University
Delhi-110042, India
Check our gallery from the recent events
Check our gallery from the recent events
Check our gallery from the recent events
Check our gallery from the recent events
Check our gallery from the recent events
Check our gallery from the recent events


Alternate Names
When loading a library with alternate file names:
ffi_lib ['foo', 'foo-1.2']
Linux Packages
When you install libfoo
on a Debian or RedHat based Linux system, it will install libfoo.so.1
but will not install libfoo.so
. Linux package maintainers will put the libfoo.so
file into a development package (libfoo-dev
on Debian or libfoo-devel
on RedHat). To prevent users from having to install both the library package and the development package:
ffi_lib ['foo', 'libfoo.so.1']
In-memory Libraries
When the library you need to bind against has already been loaded into memory, use FFI::CURRENT_PROCESS:
ffi_lib [FFI::CURRENT_PROCESS, 'c']
Function Visibility
Some libraries are compiled such that they depend on functions existing in the global namespace. This makes things problematic for FFI when attaching the functions. However, you can instruct FFI to load the library and make all symbols visible globally within the process.
ffi_lib_flags :now, :global
ffi_lib 'foo'


Build dApp MVP
in a day
^ Abridged tool suite is a plug-and-play SDK for rapid production-grade application development
^ Future-proof your application by integrating Web 3 components
^ Developer Portal provides more control of features and extensions
Plug-and-Play
Dev Toolkit
^ Account contracts for ease of onboarding.
^ Fiat ramps for quick crypto access.
^ Off-chain transactions via generalized state channels.
SDK - CLI - Dev Portal
Documentation
^ An open source SDK provides the foundation of the Abridged Suite, standardizing access to blockchain scalability
and UX tools for a more rapid workflow.
^ Our CLI makes integration increasingly easier, activate state channels and Web2-like login with a few commands.
^ The Dev Portal (coming soon) will provide analytics and custom add ons built to enhance product focused decision-making.
Become part of
our Ecosystem
^ Stay up-to-date with our technology and take part in the conversation


- onAlertTableReceived(List<AlertData>) - Method in interface com.esplibrary.client.ESPClientListener
-
Callback method to be invoked when an full table of
alerts
has been
received.
- onBadESPData(String) - Method in interface com.esplibrary.client.callbacks.MalformedDataListener
-
Callback method to be invoked when bad ESP data is received.
- onBindViewHolder(BluetoothDeviceAdapter.BaseViewHolder, int) - Method in class com.esplibrary.bluetooth.BluetoothDeviceAdapter
-
- onCharacteristicChanged(BluetoothGatt, BluetoothGattCharacteristic) - Method in class com.esplibrary.bluetooth.V1cGattCallback
-
- onCharacteristicChanged(BluetoothGatt, BluetoothGattCharacteristic) - Method in class com.esplibrary.bluetooth.V1connectionLEWrapper
-
- onCharacteristicWrite(BluetoothGatt, BluetoothGattCharacteristic, int) - Method in class com.esplibrary.bluetooth.V1cGattCallback
-
- onCharacteristicWrite(BluetoothGatt, BluetoothGattCharacteristic, int) - Method in class com.esplibrary.bluetooth.V1connectionLEWrapper
-
- onClick(View) - Method in class com.esplibrary.bluetooth.BluetoothDeviceAdapter.BluetoothDeviceVH
-
- onConnected() - Method in class com.esplibrary.bluetooth.V1connectionBaseWrapper
-
Finalizes the connection process and notifies the registered listener that the library is
connected.
- onConnected() - Method in class com.esplibrary.bluetooth.V1connectionWrapper
-
- onConnectionEvent(ConnectionEvent, boolean) - Method in interface com.esplibrary.bluetooth.ConnectionListener
-
Callback method to be invoked when a connection event has occurred.
- onConnectionFailed() - Method in class com.esplibrary.bluetooth.V1connectionBaseWrapper
-
- onConnectionLost() - Method in class com.esplibrary.bluetooth.V1connectionBaseWrapper
-
- onConnectionStateChange(BluetoothGatt, int, int) - Method in class com.esplibrary.bluetooth.V1cGattCallback
-
- onConnectionStateChange(BluetoothGatt, int, int) - Method in class com.esplibrary.bluetooth.V1connectionLEWrapper
-
- onCreateViewHolder(ViewGroup, int) - Method in class com.esplibrary.bluetooth.BluetoothDeviceAdapter
-
- onDataReceived(T, String) - Method in interface com.esplibrary.client.callbacks.ESPRequestedDataListener
-
Callback method to be invoked when the requested ESP data has been received.
- onDescriptorWrite(BluetoothGatt, BluetoothGattDescriptor, int) - Method in class com.esplibrary.bluetooth.V1cGattCallback
-
- onDescriptorWrite(BluetoothGatt, BluetoothGattDescriptor, int) - Method in class com.esplibrary.bluetooth.V1connectionLEWrapper
-
- onDeviceScanned(BluetoothScanner, BluetoothDevice, ConnectionType, int) - Method in interface com.esplibrary.bluetooth.BTScanListener
-
Callback method to be invoked when a BluetoothDevice
has been scanned.
- onDeviceSelected(BluetoothDevice, ConnectionType) - Method in interface com.esplibrary.bluetooth.BluetoothDeviceAdapter.BluetoothDeviceSelectionListener
-
- onDisconnected() - Method in class com.esplibrary.bluetooth.V1connectionBaseWrapper
-
Finalizes the disconnection process and notifies the registered listener that the library is
disconnected.
- onDisconnected(boolean) - Method in class com.esplibrary.bluetooth.V1connectionBaseWrapper
-
Finalizes the disconnection process and notifies the registered listener that the library is
disconnected.
- onDisconnected() - Method in class com.esplibrary.bluetooth.V1connectionLEWrapper
-
- onDisplayDataReceived(InfDisplayData) - Method in interface com.esplibrary.client.ESPClientListener
-
- onFailure(String) - Method in interface com.esplibrary.client.FailureCallback
-
Callback method to be invoked when a failure occurs.
- onFailurePacket(ESPPacket) - Method in class com.esplibrary.client.ResponseProcessor
-
Perform a failure callback for the 'failure' packet.
- onNoDataDetected() - Method in interface com.esplibrary.client.callbacks.NoDataListener
-
Callback method to be invoked when no ESP data has been received within a configurable
timeout.
- onNotificationReceived(String) - Method in interface com.esplibrary.client.callbacks.NotificationListener
-
Callback method to be invoked string message should be delivered.
- onPacketReceived(P) - Method in interface com.esplibrary.client.ESPCallback
-
Callback method to be invoked when an ESP packet is received.
- onPacketReceived(ESPPacket) - Method in interface com.esplibrary.client.ESPClientListener
-
Callback method to be invoked when an
ESPPacket
has been received.
- onPacketReceivedBlocking(ESPPacket, List<Integer>) - Method in class com.esplibrary.client.ResponseProcessor
-
Perform the success callback on any
handler
that responds to the packet
- onPacketWriteFailed(ESPPacket) - Method in class com.esplibrary.bluetooth.V1connectionBaseWrapper
-
- onPacketWritten(ESPPacket) - Method in class com.esplibrary.bluetooth.V1connectionBaseWrapper
-
Invoked when an ESPPacket has successfully been written.
- onPacketWritten(ESPPacket) - Method in interface com.esplibrary.client.callbacks.ESPWriteListener
-
Callback method to be invoked when an
ESPPacket
has been written.
- onReadRemoteRssi(BluetoothGatt, int, int) - Method in class com.esplibrary.bluetooth.V1cGattCallback
-
- onReadRemoteRssi(BluetoothGatt, int, int) - Method in class com.esplibrary.bluetooth.V1connectionLEWrapper
-
- onReceive(Context, Intent) - Method in class com.esplibrary.bluetooth.BluetoothScanner
-
- onRequestCompleted(String) - Method in interface com.esplibrary.client.callbacks.ESPRequestListener
-
Callback method to be invoked when a packet the packet has been successfully sent.
- onRssiReceived(BluetoothDevice, int) - Method in interface com.esplibrary.bluetooth.RSSICallback
-
Callback method to be invoked when RSSI has been received.
- onScanCompleted(BluetoothScanner) - Method in interface com.esplibrary.bluetooth.BTScanListener
-
Callback method to be invoked when the scan has completed.
- onServicesDiscovered(BluetoothGatt, int) - Method in class com.esplibrary.bluetooth.V1cGattCallback
-
- onServicesDiscovered(BluetoothGatt, int) - Method in class com.esplibrary.bluetooth.V1connectionLEWrapper
-
- ORIG_IDX - Static variable in class com.esplibrary.packets.PacketUtils
-
- ORIG_INDENTIFIER_BASE_CONST - Static variable in class com.esplibrary.packets.PacketUtils
-


Before Change
@staticmethod
def loadDictionary(fname):
words = [line.split("\t")[1].strip() for line in open(fname) if len(line.split("\t")) == 2]
id2word = dict(itertools.izip(xrange(len(words)), words))
return id2word
def saveDocuments(self, fname):
logging.info("saving documents mapping to %s" % fname)
After Change
result = {}
for lineNo, line in enumerate(open(fname)):
pair = line[:-1].split("\t")
if len(pair) != 2:
continue
wordId, word = pair
result[int(wordId)] = word
return result
def saveDocuments(self, fname):
logging.info("saving documents mapping to %s" % fname)


"""
"""
import numpy as np
from nltk import word_tokenize
from nltk.corpus import stopwords
from nltk.stem.snowball import FrenchStemmer
[docs]def avg_document(model, document):
""" computes the average vector of the words in document
in the word2vec model space
Parameters
----------
model : word2vec.KeyedVectors instance
document : list
tokenized document to fold into a vector
Returns
-------
avg : np.ndarray
the average of all the words in document
"""
vocab = model.vocab
n_features = model.vector_size # change to model.vector_sizes
count = 0
vectors = np.zeros((n_features,), dtype='float64')
for word in document:
if word in vocab:
new_vec = model[word]
# print(new_vec.shape, vectors.shape) #debug statement
vectors = np.vstack((vectors, new_vec))
count += 1
# print(vectors.shape)
if count > 0:
avg = np.mean(vectors[1:], axis=0)
else:
avg = vectors
return avg
[docs]def avg_corpus(model, corpus):
""" computes average vector for each document of the corpus
Parameters
----------
model : gensim.word2vec.Word2Vec instance
Trained word2vec model
corpus : iterable of iterables
Returns
-------
"""
# n, p = len(corpus), model.layer1_size
features = [avg_document(model, doc) for doc in corpus]
return np.array(features)
[docs]def text_normalize(text, stop_words, stem=False):
""" This functions performs the preprocessing steps
needed to optimize the vectorization, such as normalization
stop words removal, lemmatization etc...
stemming for french not accurate enough yet
@TODO lemmatization for french + adapt stemmer for other languages
Parameters
----------
text: string
text to normalize
stop_words : list
list of additionnal stopwords to remove from the text
stem : bool
if True, stems the words to fetch the meaning of the words
However, this functionality does not perform well with french
Returns
-------
string
same text as input but cleansed and normalized
"""
sw = stopwords.words('french') + stop_words
tokens = word_tokenize(text, 'french')
tokens_filter = [word for word in tokens if word not in sw]
if stem:
stemmer = FrenchStemmer()
tokens_filter = [stemmer.stem(word) for word in tokens_filter]
return tokens_filter # " ".join(tokens_filter) #


Online fundraiser for bushfire-hit koalas tops $1m Aussie dollars
Donations pour in from nations including the US, France and the UK for koalas caught up in Australia's deadly bushfires.
An online fundraiser for koalas injured in devastating bushfires has topped one million Australian dollars ($680,000) on Thursday, making it Australia's biggest campaign on the GoFundMe platform this year.
Deadly bushfires have ravaged vast swathes of eastern Australia, and wildlife carers estimate the blazes have killed hundreds of koalas this month in New South Wales state alone.
More:
A crowdfunding page set up by the Port Macquarie Koala Hospital to provide automatic water drinking stations for wild koalas soared past one million Australian dollars, with donations from more than 20,000 people from countries including the United States, France and England.
The wildlife charity's Help Thirsty Koalas Devastated by Recent Fires drive, which had an initial goal of just 25,000 Australian dollars, got more donations than any other campaign in Australia this year, GoFundMe said, and is the second-biggest since the platform launched in the country in 2016.
It is one of 700 bushfire-related fundraisers launched on the platform in the past 10 days, which to date have raised a combined total of two million Australian dollars.
The next-biggest has raised 60,000 Australian dollars to support firefighters and people impacted by the blazes, which have killed six people, razed hundreds of homes and scorched about 1.5 million hectares (3.7 million acres) of land since October.
Millions of acres of land have been scorched by bushfires in New South Wales [AAP Image/Dean Lewins/via Reuters]
The koala hospital, some four hours north of Sydney, has rescued 31 koalas from the scorched bush in recent weeks, and said it had been "overwhelmed by the kindness, good wishes and support from the Australian and international community".
It plans to use the excess funds to deliver koala drinking stations to other parts of the state, purchase a water-carrying vehicle to replenish the stations, and to establish a koala breeding programme.
With more than 1.3 million hectares (3.2 million acres) of land scorched in the state, the fires have also destroyed large tracts of the tree-dwelling marsupial's habitat.
A so-called "Koala Ark" would be built for recovering koalas, the hospital said, hoping they will eventually breed and be returned to the wild.


--- title: "Home" layout: default pagination: enabled: true description: "Personal webpage of Scott Morgan. Digital Lead at Bridgend College. Mathematics PhD. Interested in education, coding and all things tech." permalink: /index.html ---
{{ latest_post.excerpt | strip_html | strip_newlines | truncate: 136 }}


--- layout: page title: About me css: "/css/aboutme.css" ---
A data scientist & Google certified data engineer, currently seeking opportunities in data related space. Previously worked as a software engineer, building enterprise web applications & machine learning prototypes.
Passionate about using data science and software engineering to solve complex real-world problems.
Obtained my master's in Data Science from the University of Melbourne, Australia and my Bachelor of Computer Science & Engineering degree from Visvesvaraya
Technological University, India.
My masters thesis project involved analysing drink driving trends in the state of Victoria, Australia, and building robust statistical models to determine
factors that have influenced change in the trend.
I enjoy competing at Kaggle & other data science competitions, testing & honing my skills; I have worked across a wide range of problems in these competitions, from predictive modeling, computer vision to natural language processing.
In the past and occasionaly now, I compete in coding challenges on platforms like HackerRank & Project Euler to improve my coding skills.
I am an avid learner, learning things from first principles: taking apart the concepts one by one until there is no more but the indivisible atomic principles.
I'm a Real Madrid soccer fan; love playing ultimate frisbee; solving Rubik's cube, improving over my previous best score(current=03:41mins); and cooking!! Additionally, I enjoy engaging in deep philosophical conversations with fellow peers among many other things:)
Grew up in Mysuru, the heritage city of India and later worked in Bengaluru, India at an IT organisation before moving to Melbourne, Australia to pursue post-graduate studies.
Feel free to contact me, or to discuss potential projects/collaborate in kaggle competitions, or need any help. You can contact me via either email, or other social networks mentioned below of this page.


You are viewing an historical archive of past issues. Please
report new issues to the appropriate project issue tracker on
GitHub.
Comments
-
Greg Hurrell
Created an attachment (id=86) i386 crash log
Add a comment
Comments are now closed for this issue.


Click on My Movie List. This contains a list of movies you can explore for further details.
Click on each movie to view further details and rate it out of a score of 5 based on your liking. Take your time to explore each movie and rate it based on your opinion. The more movies you rate, the better the recommendations you will get.
Once you have rated all the movies you want, click on Calculate & Show Recommendations. It will take 2-3 minutes for the next page to load, so you can do anything else in the meanwhile. Don't click on anything else till the page loads.
The next pages will display your recommendations, and some questions related to those that you may need to answer. Take your time to explore each recommended movie (click on it for further details) and then answer the relevant questions.
That's it for one session! Explore and rate more/different movies to get more recommendations. Tell us how they differ from the previous ones!
THINGS to NOTE:
Please complete all the questions in one single session, otherwise the recommendations will reset! Movies you rated will be saved though.
Please wait for every page to completely load before using it. You can see the loading status on the browser tab name.
That's it!Enjoy!!
In case of any queries/issues/additional feedback, please email movirec_movies@gmail.com


{% extends "base.html" %} {% set page_title="not found" %} {% block content %}
Sorry, the requested resource was not found on this server.


Mo Farah to run 10,000m at Tokyo Olympics
by AGENCIESLONDON, United Kingdom, Nov 29 – Four-time Olympic champion Mo Farah will defend his 10,000m title at the 2020 Tokyo Olympics.
Great Britain’s Farah, 36, quit track events in 2017 to concentrate on the marathon.
But having won gold in the 5,000m and 10,000m at the 2012 and 2016 Olympics, he has decided to return to where he has been most successful.
“Next year, I’m going to be back on the track and I’m going to give it a go in the 10,000m,” he said on social media.
“Hopefully I haven’t lost my speed but I will train hard for it and see what I can do. I’m really excited.”
Farah, who is also a six-time world champion and has yet to run the marathon at a major championship, hinted earlier in the year that he could make the switch because he “missed” the frequency that track racing provided.
He finished eighth in the Chicago Marathon in October, having won it in 2018 in only his third race.
Farah, who will still need to qualify for the Tokyo Games, added: “It’s been really exciting competing in the marathon over the last couple of years.
“To win the Chicago Marathon was nice and to finish third in the London Marathon was OK, and it’s been a great learning curve for me.”
Farah’s track return also comes at a time when he has faced questions about his association with former coach Alberto Salazar, who has been banned for four years for doping violations.
Speaking before the Chicago Marathon, Farah said he had “not done anything wrong” and said there was an “agenda” against him.
-BBC-
(Visited 14 times, 14 visits today)


messages
See in modal
Create alert
Use these buttons to test! ^
ng-alerts
The only angular alert and notification system you need!


TZInfo
- Ruby Time Zone Library¶ ↑
TZInfo is a Ruby library that provides access to time zone data and allows times to be converted using time zone rules.
Data Sources¶ ↑
TZInfo
requires a source of time zone data. There are two options:
-
A zoneinfo directory containing timezone definition files. These files are generated from the IANA Time Zone Database using the
zic
utility. Most Unix-like systems include a zoneinfo directory. -
The TZInfo::Data library (the tzinfo-data gem). TZInfo::Data contains a set of Ruby modules that are also generated from the IANA Time Zone Database.
By default, TZInfo
will attempt to use TZInfo::Data. If TZInfo::Data is not available (i.e. if require 'tzinfo/data'
fails), then TZInfo
will search for a zoneinfo directory instead (using the search path specified by TZInfo::ZoneinfoDataSource::DEFAULT_SEARCH_PATH
).
If no data source can be found, a TZInfo::DataSourceNotFound
exception will be raised when TZInfo
is used. Further information is available in the wiki to help resolve TZInfo::DataSourceNotFound
errors.
The default data source selection can be overridden by calling TZInfo::DataSource.set
.
Custom data sources can also be used. See the TZInfo::DataSource.set
documentation for further details.
Installation¶ ↑
The TZInfo
gem can be installed by running gem install tzinfo
or by adding to gem 'tzinfo'
to your Gemfile
and running bundle install
.
To use the Ruby modules as the data source, TZInfo::Data will also need to be installed by running gem install tzinfo-data
or by adding gem 'tzinfo-data'
to your Gemfile
.
IANA Time Zone Database¶ ↑
The data returned and used by TZInfo
is sourced from the IANA Time Zone Database. The Theory and pragmatics of the tz code and data document gives details of how the data is organized and managed.
Example Usage¶ ↑
To use TZInfo
, it must first be required with:
require 'tzinfo'
The TZInfo::Timezone
class provides access to time zone data and methods for converting times.
The all_identifiers
method returns a list of valid time zone identifiers:
identifiers = TZInfo::Timezone.all_identifiers # => ["Africa/Adibdjan", "Africa/Accra", ..., "Zulu"]
A TZInfo::Timezone
instance representing an individual time zone can be obtained with TZInfo::Timezone.get
:
tz = TZInfo::Timezone.get('America/New_York') # => #<TZInfo::DataTimezone: America/New_York>
A time can be converted to the local time of the time zone with to_local
:
tz.to_local(Time.utc(2018, 2, 1, 12, 30, 0)) # => 2018-02-01 07:30:00 -0500 tz.to_local(Time.utc(2018, 7, 1, 12, 30, 0)) # => 2018-07-01 08:30:00 -0400 tz.to_local(Time.new(2018, 7, 1, 13, 30, 0, '+01:00')) # => 2018-07-01 08:30:00 -0400
Local times with the appropriate offset for the time zone can be constructed with local_time
:
tz.local_time(2018, 2, 1, 7, 30, 0) # => 2018-02-01 07:30:00 -0500 tz.local_time(2018, 7, 1, 8, 30, 0) # => 2018-07-01 08:30:00 -0400
Local times can be converted to UTC by using local_time
and calling utc
on the result:
tz.local_time(2018, 2, 1, 7, 30, 0).utc # => 2018-02-01 12:30:00 UTC tz.local_time(2018, 7, 1, 8, 30, 0).utc # => 2018-07-01 12:30:00 UTC
The local_to_utc
method can also be used to convert a time object to UTC. The offset of the time is ignored - it is treated as if it were a local time for the time zone:
tz.local_to_utc(Time.utc(2018, 2, 1, 7, 30, 0)) # => 2018-02-01 12:30:00 UTC tz.local_to_utc(Time.new(2018, 2, 1, 7, 30, 0, '+01:00')) # => 2018-02-01 12:30:00 UTC
Information about the time zone can be obtained from returned local times:
local_time = tz.to_local(Time.utc(2018, 2, 1, 12, 30, 0)) local_time.utc_offset # => -18000 local_time.dst? # => false local_time.zone # => "EST" local_time = tz.to_local(Time.utc(2018, 7, 1, 12, 30, 0)) local_time.utc_offset # => -14400 local_time.dst? # => true local_time.zone # => "EDT"
Time zone information can be included when formatting times with strftime
using the %z
and %Z
directives:
tz.to_local(Time.utc(2018, 2, 1, 12, 30, 0)).strftime('%Y-%m-%d %H:%M:%S %z %Z') # => "2018-02-01 07:30:00 -0500 EST" tz.to_local(Time.utc(2018, 7, 1, 12, 30, 0)).strftime('%Y-%m-%d %H:%M:%S %z %Z') # => "2018-07-01 08:30:00 -0400 EDT"
The period_for
method can be used to obtain information about the observed time zone information at a particular time as a TZInfo::TimezonePeriod
object:
period = tz.period_for(Time.utc(2018, 7, 1, 12, 30, 0)) period.base_utc_offset # => -18000 period.std_offset # => 3600 period.observed_utc_offset # => -14400 period.abbreviation # => "EDT" period.dst? # => true period.local_starts_at.to_time # => 2018-03-11 03:00:00 -0400 period.local_ends_at.to_time # => 2018-11-04 02:00:00 -0400
A list of transitions between periods where different rules are observed can be obtained with the transitions_up_to
method. The result is returned as an Array
of TZInfo::TimezoneTransition
objects:
transitions = tz.transitions_up_to(Time.utc(2019, 1, 1), Time.utc(2017, 1, 1)) transitions.map do |t| [t.local_end_at.to_time, t.offset.observed_utc_offset, t.offset.abbreviation] end # => [[2017-03-12 02:00:00 -0500, -14400, "EDT"], # [2017-11-05 02:00:00 -0400, -18000, "EST"], # [2018-03-11 02:00:00 -0500, -14400, "EDT"], # [2018-11-04 02:00:00 -0400, -18000, "EST"]]
A list of the unique offsets used by a time zone can be obtained with the offsets_up_to
method. The result is returned as an Array
of TZInfo::TimezoneOffset
objects:
offsets = tz.offsets_up_to(Time.utc(2019, 1, 1)) offsets.map {|o| [o.observed_utc_offset, o.abbreviation] } # => [[-17762, "LMT"], # [-18000, "EST"], # [-14400, "EDT"], # [-14400, "EWT"], # [-14400, "EPT"]]
All TZInfo::Timezone
methods that accept a time as a parameter can be used with either instances of Time
, DateTime
or TZInfo::Timestamp
. Arbitrary Time
-like objects that respond to both to_i
and subsec
and optionally utc_offset
will be treated as if they are instances of Time
.
TZInfo::Timezone
methods that both accept and return times will return an object with a type matching that of the parameter (actually a TZInfo::TimeWithOffset
, TZInfo::DateTimeWithOffset
or TZInfo::TimestampWithOffset
subclass when returning a local time):
tz.to_local(Time.utc(2018, 7, 1, 12, 30, 0)) # => 2018-07-01 08:30:00 -0400 tz.to_local(DateTime.new(2018, 7, 1, 12, 30, 0)) # => #<TZInfo::DateTimeWithOffset: 2018-07-01T08:30:00-04:00 ((2458301j,45000s,0n),-14400s,2299161j)> tz.to_local(TZInfo::Timestamp.create(2018, 7, 1, 12, 30, 0, 0, :utc)) # => #<TZInfo::TimestampWithOffset: @value=1530448200, @sub_second=0, @utc_offset=-14400, @utc=false>
In addition to local_time
, which returns Time
instances, the local_datetime
and local_timestamp
methods can be used to construct local DateTime
and TZInfo::Timestamp
instances with the appropriate offset:
tz.local_time(2018, 2, 1, 7, 30, 0) # => 2018-02-01 07:30:00 -0500 tz.local_datetime(2018, 2, 1, 7, 30, 0) # => #<TZInfo::DateTimeWithOffset: 2018-02-01T07:30:00-05:00 ((2458151j,45000s,0n),-18000s,2299161j)> tz.local_timestamp(2018, 2, 1, 7, 30, 0) # => #<TZInfo::TimestampWithOffset: @value=1517488200, @sub_second=0, @utc_offset=-18000, @utc=false>
The local_to_utc
, local_time
, local_datetime
and local_timestamp
methods may raise a TZInfo::PeriodNotFound
or a TZInfo::AmbiguousTime
exception. TZInfo::PeriodNotFound
signals that there is no equivalent UTC time (for example, during the transition from standard time to daylight savings time when the clocks are moved forward and an hour is skipped). TZInfo::AmbiguousTime
signals that there is more than one equivalent UTC time (for example, during the transition from daylight savings time to standard time where the clocks are moved back and an hour is repeated):
tz.local_time(2018, 3, 11, 2, 30, 0, 0) # raises TZInfo::PeriodNotFound (2018-03-11 02:30:00 is an invalid local time.) tz.local_time(2018, 11, 4, 1, 30, 0, 0) # raises TZInfo::AmbiguousTime (2018-11-04 01:30:00 is an ambiguous local time.)
TZInfo::PeriodNotFound
exceptions can only be resolved by adjusting the time, for example, by advancing an hour:
tz.local_time(2018, 3, 11, 3, 30, 0, 0) # => 2018-03-11 03:30:00 -0400
TZInfo::AmbiguousTime
exceptions can be resolved by setting the dst
parameter and/or specifying a block to choose one of the interpretations:
tz.local_time(2018, 11, 4, 1, 30, 0, 0, true) # => 2018-11-04 01:30:00 -0400 tz.local_time(2018, 11, 4, 1, 30, 0, 0, false) # => 2018-11-04 01:30:00 -0500 tz.local_time(2018, 11, 4, 1, 30, 0, 0) {|p| p.first } # => 2018-11-04 01:30:00 -0400 tz.local_time(2018, 11, 4, 1, 30, 0, 0) {|p| p.last } # => 2018-11-04 01:30:00 -0500
The default value of the dst
parameter can also be set globally:
TZInfo::Timezone.default_dst = true tz.local_time(2018, 11, 4, 1, 30, 0, 0) # => 2018-11-04 01:30:00 -0400 TZInfo::Timezone.default_dst = false tz.local_time(2018, 11, 4, 1, 30, 0, 0) # => 2018-11-04 01:30:00 -0500
TZInfo
also provides information about ISO 3166-1 countries and their associated time zones via the TZInfo::Country
class.
A list of valid ISO 3166-1 (alpha-2) country codes can be obtained by calling TZInfo::Country.all_codes
:
TZInfo::Country.all_codes # => ["AD", "AE", ..., "ZW"]
A TZInfo::Country
instance representing an individual time zone can be obtained with TZInfo::Country.get
:
c = TZInfo::Country.get('US') # => #<TZInfo::Country: US> c.name # => "United States"
The zone_identifiers
method returns a list of the time zone identifiers used in a country:
c.zone_identifiers # => ["America/New_York", "America/Detroit", ..., "Pacific/Honolulu"]
The zone_info
method returns further information about the time zones used in a country as an Array
of TZInfo::CountryTimezone
instances:
zi = c.zone_info.first zi.identifier # => "America/New_York" zi.latitude.to_f.round(5) # => 40.71417 zi.longitude.to_f.round(5) # => -74.00639 zi.description # => "Eastern (most areas)"
The zones
method returns an Array
of TZInfo::Timezone
instances for a country. A TZInfo::Timezone
instance can be obtained from a TZInfo::CountryTimezone
using the timezone
method:
zi.timezone.to_local(Time.utc(2018, 2, 1, 12, 30, 0)) # => 2018-02-01 07:30:00 -0500
For further detail, please refer to the API documentation for the TZInfo::Timezone
and TZInfo::Country
classes.
Time Zone Selection¶ ↑
The Time Zone Database maintainers recommend that time zone identifiers are not made visible to end-users (see Names of timezones).
Instead of displaying a list of time zone identifiers, time zones can be selected by the user's country. Call TZInfo::Country.all
to obtain a list of TZInfo::Country
objects, each with a unique code
and a name
that can be used for display purposes.
Most countries have a single time zone. When choosing such a country, the time zone can be inferred and selected automatically.
croatia = TZInfo::Country.get('HR') # => #<TZInfo::Country: HR> croatia.zone_info.length # => 1 croatia.zone_info[0].identifier # => "Europe/Belgrade"
Some countries have multiple time zones. The zone_info
method can be used to obtain a list of user-friendly descriptions of the available options:
australia = TZInfo::Country.get('AU') # => #<TZInfo::Country: AU> australia.zone_info.length # => 13 australia.zone_info.map {|i| [i.identifier, i.description] } # => [["Australia/Lord_Howe", "Lord Howe Island"], # ["Antarctica/Macquarie", "Macquarie Island"], # ... # ["Australia/Eucla", "Western Australia (Eucla)"]]
Please note that country information available through TZInfo
is intended as an aid to help users select a time zone data appropriate for their practical needs. It is not intended to take or endorse any position on legal or territorial claims.
Compatibility¶ ↑
TZInfo
v2.0.0 requires a minimum of Ruby MRI 1.9.3 or JRuby 1.7 (in 1.9 mode or later).
Thread-Safety¶ ↑
The TZInfo::Country
and TZInfo::Timezone
classes are thread-safe. It is safe to use class and instance methods of TZInfo::Country
and TZInfo::Timezone
in concurrently executing threads. Instances of both classes can be shared across thread boundaries.
Documentation¶ ↑
API documentation for TZInfo
is available on RubyDoc.info.
License¶ ↑
TZInfo
is released under the MIT license, see LICENSE for details.
Source Code¶ ↑
Source code for TZInfo
is available on GitHub.
Issue Tracker¶ ↑
Please post any bugs, issues, feature requests or questions about TZInfo
to the GitHub issue tracker.
Issues with the underlying time zone data should be raised on the Time Zone Database Discussion mailing list.


Upsilon Pi Epsilon is working hard on a number of different projects to improve the experience of students at Rensselaer Polytechnic Institute. Please review our projects below and contact us if you are interested in assisting in their development.
This semester, UPE is working in conjunction with the CS department on converting the UPE room (AE 117) into a computer lab open for all Undergraduate students at RPI. The lab will have powerful computers and mobile devices for students to learn on outside the classroom.
UPE provides rack space in Amos Eaton for technology-oriented groups and projects on campus looking for hosting.
A set of lectures created to give students core materials for developing their skills as computer scientists. These lectures are presented semesterly to anyone interested but are available online for review by anyone.
This server powers the RCOS/ACM/UPE semesterly programming competitions. Participants submit solutions to a variety of problems and the server evaluates their algorithms by testing them against a number of input conditions, checking for correctness and time efficiency.


Industrial Designer / American Woodmark Corporation.
Senior Design Specialist / WoodCrafters Home Products, LLC, a division of MasterBrand Cabinets Inc.
Buyer / WoodCrafters Home Products, LLC, a division of MasterBrand Cabinets Inc.
Junior Designer / WoodCrafters Home Products, LLC, a division of MasterBrand Cabinets Inc.
Contract Designer / Finger Lakes Wood Works
Instructor / Rochester Institute of Technology
Staff Designer / Drangonfly Design
Assistant Designer / Light.Forest Design
Intern / GE/fitch Taiwan
MFA, Rochester Institute of Technology / Rochester, NY USA
Scandinavian Furniture Design Program, DIS Design Studio / Copenhagen, Denmark
Bachelor of Science, National Taiwan Cheng-Kung University / Tainan, Taiwan
Certified PolyWorks v12 IM + Modeler / InnovMetric Software / 3D scanning
Certified SolidWorks Professional / Dassault Systèmes / 3D Modeling
Born and raised in Taiwan, I grew up watching consumer electronics industries booming. Naturally, it became my focus of design after I graduated from college with degree in Industrial Design.
Later in my career I was fortunate to enter RIT to advance my skills. While pursuing my master degree, I developed strong interests in furniture, and decided to attend Scandinavian furniture design program. Combine these training and my experience, I developed a great capability to create designs that embrace humility, user experience, and value.
In my current position, I also take lead in color & finish development on top of my design duties, and provide assistance in strategic planning. This beautiful involvement in various aspects equips me with best ability to provide service to sustain our success in the market and keep exceeding customers' expectations.
Sketching, Home furniture design, Kitchen and bath design, Electronics design, Sanitary ware design, 3D modeling, 3D render, 3D scan & 3D printing, Sourcing, Color development.


--- layout: default-breadcrumbs --- {% assign author = site.team | where:"author-id", page.author | first %}


Our company wants to assure you are 100% satisfied with any product you have purchased. We offer a 90 day period for you to return any product you are unsatsified with.
If you wish to make a return please email<EMAIL_ADDRESS>or you may contact us by phone.


Windows 10 x64 (19041.450)
PDC_RESOURCE
PdcResourceSystemForeground = 0n0
PdcResourceUniversalApplications = 0n1
PdcResourceWin32 = 0n2
PdcResourceNetwork = 0n3
PdcResourceSystemBackground = 0n4
PdcResourceMin = 0n0
PdcResourceMax = 0n4


Lao New Year's Celebrations
by Monika Suchoszek
Pi Mai Lao, celebration of the New Year in Laos during the hottest part of the year was an unexpected event we gladly joined :D Officially it is a 3-day celebration but in some cities like Vientiane or Luang Prabang it can last over a week. During the New Year celebrations the entrance to all temples is free.
That day we decided to visit Pha That Luang, a large gold-covered stupa; a national monument and the symbol of Vientiane. We again used the public transportation to get to this part of the city (bus nr 10 from the crossing near the central bus station, we found the correct bus stop based on the map with information on the central bus station).
In front of each temple, Buddha statues are placed in easy accessible places so devotes can pour scented water with flower petals over the statues. It symbolizes washing last-year’s sins away since the first day of celebrations is the last day of the old year. Everyone wants to enter the New Year without any sins. Then, they collect the water which runs over the Buddha and take it home to pour over other members of the family as a blessing.
Already around a lunchtime people started to pour water on each other and few hours later all the streets turned to a giant battle field. Pickup trucks, full of people and buckets of water were appearing more and more often on the street so it was a time to switch from a normal camera to our waterproof GoPro :) We noticed in front of most houses inflatable swimming pools to cool down and owners waiting with hoses, water pistols and buckets to pour water over the pedestrians. No one could stay dry that day :D
The second day of the celebrations is a day to relax, the one between the last day of an old year and the first day of a New Year (third day of the celebrations). We noticed that the owners of the guesthouse where we stayed were “very tired” in the morning :D We took the bus to Vang Vieng, this wasn’t easy since only one company was operating during a holiday (the price was also somewhat higher than normal). It was difficult to stay dry during the next days, there were always many kids along the road, waiting to pour water on passers. Better get a good waterproof bag* for all your documents, electronics and money! You can buy them actually on the every corner of the street. Walking from the bus station to our guesthouse, which was about one kilometer, was a very tricky task as we didn’t want to get wet. We had to basically look around each corner of the street and check if it was safe to go there. Being in a tuk-tuk was not helpful either, since whenever the tuk-tuks with tourists stopped, there were some kids with water running towards them to get everyone inside soaking wet :O
In conclusion, to survive the Laotian new Year celebration, get a waterproof bag, and feel again like a child and enjoy pouring water on the others! :D
Practical info:
- Bus ticket to Pha That Luang (bus number 10) - 4k per person, one way
- Bus ticket to Vang Vieng (minivan) - 60k Kip per person
- Get a waterproof bag ! It wil come in handy quite often if you are there in the wet season.
Subscribe via RSS


Personal Assistance Using Python Personal Assistance created using python power by Artificial Intelligence
Generate your personalized QR on the go and link it with anything, can be also used for Covid info for awareness
Animation of Walking man in the rain using C