CHAPTER 22
THE GAME SERVER

Now we have some things we’ve either added to the game world in recent chapters or simply included in our game requirements that are not yet supported in the program code.

In this chapter we’ll focus on adding the server-side code we need to support the requirements, as well as adding in some code to bring certain concepts to a more complete state.

THE PLAYER-CHARACTER

You’ve probably noticed a few things that are odd or incomplete in the player behavior or appearance in the code and art we’ve dealt with up to this point. We’ll tackle those things now.

Player Spawning

For our example games, we’ve used a fixed spawn point. Well, there is a convenient spawn point system available that we can employ.

First thing we need for this system is what we call a marker. Fortunately, Torque 3D has already created the required support code for this, so we don’t have to. If you look inside the file core/art/datablocks/markers.cs you will see this code:

datablock MissionMarkerData(SpawnSphereMarker)
{
  category = "Misc";
  shapeFile = "core/art/shapes/octahedron.dts";
};

That’s the datablock for the marker we will be using. And if you were able to look inside the file tools/worldEditor/gui/objectBuilderGui.ed.gui you would find the code similar to the following that actually creates the SpawnSphereMarker. This code is actually from the demo of an earlier version of Torque where it was released to the public in source script form.

function MissionMarkerData::Create(%block)
{
  switch$(%block)
  {
    case "SpawnMarker":
      %obj = new SpawnSphereMarker() {
        datablock = %block;
      };
      return(%obj);
  }
  return -1;
}

This has the by-now-familiar datablock, this one for a MissionMarkerData. The Create function tells the World Editor how to make the new marker in the game world. Note the use of the switch$ block, even though there is only one case—this is for later use for other kinds of markers. Save your work.

Now copy the directory 3D3ERESOURCESCH22markers and all of its contents to koobassetsmodelsmarkers, creating a new folder if you need to.

Now open the file koobcontrolserverserver.cs and locate the line:

Exec("./misc/item.cs");

Add the following entry after it:

Exec("./misc/markers.cs");

Save your file.

With that done, launch Koob, go into camera fly mode, and then move to a position overseeing the start/finish line, looking down at it.

1. Go into the World Editor, and then add a PlayerDropPoints group by choosing Scene Tree, Library, Level, System, SimGroup from the Tree view at the upper right and entering PlayerDropPoints as the object name in the Inspector pane.

2. Make PlayerDropPoints the current group by locating it in the Scene tree’s Scene tab at the upper right, expanding the Mission Group (if it isn’t expanded already) and then right-clicking the PlayerDropPoints folder to get the pop-up menu.

3. Choose Add New Objects Here, which specifies that the PlayerDropPoints folder is now the new instant group where newly created objects will appear. When you created it, the PlayerDropPoints folder icon was a gray, closed folder. After making it the instant group, the folder is now open and yellow in color.

4. Add a spawn marker by choosing the Library tab in the Scene tree pane, then drilling down through Level, then into Level once more.

Tip


When you get back to the Library Level tab, the System folder will be selected. Click the left arrow beside System at the top of the Level tab to go up the folder hierarchy. You will then see the Level folder.


5. Double-click on PlayerDropPoint. A dialog called Create New Object: SpawnSphere will appear, with the Object Name field blank.

6. Put in a name for the spawn sphere (or leave it blank), then click the Create New button. A green translucent sphere will be placed in the world in front of you.

7. Create and position about half a dozen or so of these around the start/finish area, hiding a few of them behind boulders, terrain, or other things to put them out of view of the main parking lot—but not too far away. Make sure they were all created in the PlayerDropPoints group. Also check to ensure that none of the PlayerDropPoint SpawnSpheres are underground. Otherwise a player might spawn underground and fall forever.

8. Save your level and exit to the desktop.

Now open the file koobcontrolserverserver.cs, and locate the function Spawn-Player. Change the call to createPlayer to look like this:

  %this.createPlayer(SelectSpawn());

Next, add the following method to the end of the file (or immediately after the SpawnPlayer function, if you like):

function SelectSpawn()
{
  %groupName = "MissionGroup/PlayerDropPoints";
  %group = nameToID(%groupName);
  if (%group != -1) {
     %count = %group.getCount();
     if (%count != 0) {
        %index = getRandom(%count-1);
        %spawn = %group.getObject(%index);
        return %spawn.getTransform();
     }
     else
       error("No spawn points found in " @ %groupName);
  }
  else
    error("Missing spawn points group " @ %groupName);

    return "0 0 201 1 0 0 0"; // if no spawn points then center of world
}

This function examines the PlayerDropPoints group and counts how many spawn markers are in it. It then randomly selects one of them and gets its transform (which contains the marker’s position and rotation) and returns that value. If the function can’t find the group, or can’t find anything in that group, it returns a default value that puts the player in the center of the map. In fact, this default value is what we’ve been using all along up until this chapter.

Now the spawning system used in the Torque 3D Tools Demo (and in the Torque 3D Software Development Kit, or SDK) is internally much more complex because it’s intended for more generalized use. If you decide to dig around in the scripts for the demo, feel free to, but be aware that the spawning code is not identical to the method I show you here. It’s harder to find your way around the “official” method, but if you start by searching for “PlayerDropPoint” you should be okay.

With this bit of script code done, go ahead and try your game. Notice how each time you spawn, it’s in a different place (assuming you create several different player drop points).

Tip


If you place your spawn sphere so that any part of it is below ground, when you spawn in you will fall forever. You can fix this problem by entering the World Editor, then selecting your spawn sphere in the Scene tab of the Scene tree. With the drop point or spawn sphere selected, select Drop Location, At Terrain, from the Object menu. Your spawn sphere will now be placed at ground level so you won’t fall through space anymore.


Vehicle Mounting

In recent chapters, when you’ve made your player-character get into the car, you may have noticed—especially from the third-person perspective—that the player is standing, with his head poking through the roof.

This is addressed by assigning values to a mountPose array. What we do is for each vehicle, we create mountPoints in the model (which we’ve done for the car). We need to specify in the car’s model some nodes that will act as the mount points. We’ll address the pose part of the player model in the next section, leaving the rest until a later section that covers vehicles.

The Model

In recent chapters I have been using the standard male as a placeholder for testing the code, maps, and other models. Now, however, it’s time for you to use your own model, the Hero model you created back in Chapter 14. There are a few things we need to adjust in that model, so make a copy of your Hero model, and add him to your Koob models directory at koobassetsmodelsavatarshero. You will need the myhero.dts and hero.png files. Create the Hero directory if you haven’t already done it. Copy all of your Hero model files, including the texture files, into that directory.

You also need to change the player definition file to point to your Hero model. Open koobcontrolserverplayersplayer.cs and locate the PlayerData(MaleAvatarDB) datablock.

In that datablock, find the line that reads:

shapeFile = "assets/models/avatars/male/player.dts";

and change it to:

shapeFile = "assets/models/avatars/hero/myhero.dts";

If you didn’t use the name “myhero” in the name of the .dts file for your guy, then use the name you gave him.

Also, notice that if you drill down through koobassetsmodelsavatarseast you will also find a file called player.cs.

Even though that file has the same name, “player.cs”, note by the path that it’s a different file. This is the animation sequence binding file for the beast character. It associates sequence files with animations that Torque 3D supports. It’s called “player.cs” because the shape file for the beast avatar is named “player.dts”. When Torque 3D encounters a .dts file in the context of creating an avatar with the PlayerData class, it automatically takes the shape’s model filename, strips the “dts” off the end, adds the “cs” in place of that, and then tries to open the file if it can find it. This mechanism is used to ensure that the TSShapeConstructor class is able to associate the model with its external animations.

Players, Players, Everywhere!


There is yet a third place where you will find a player.cs file in the Torque 3D Tools Demo folder tree.

GarageGames, in its collective wisdom, decided when Torque 3D was being developed to separate the code that defines a player-character (and vehicles, weapons, and other objects that have complex behaviors) into two distinct parts.

The datablocks that define the players are in one group and the methods that define how the players behave and what they do are in a second group.

The schism makes sense for this reason: in a multiplayer, server-hosted game made with Torque, datablocks are used on both the client and the server. Methods (functions that belong to objects) are only used on the server side.

For a single-player game, this is no big deal, but in a multiplayer game, where you might ship only the client-side code to your customers, you generally don’t want the client applications to have access to the behavior code that runs on the server. So you need a way to do the separating.

Thus, there exists in the art folder (in Emaga and Koob we call it the assets folder) a folder called “datablocks.” In this folder go the files that contain the datablock definitions for all of the complex objects that need defining.

The methods are contained in files located in the scripts/server folder (control/server in Emaga and Koob). These scripts are only available to the server.

And that’s how the separation is achieved. In both places you will find a file called player.cs—one for the datablocks, one for the methods.

In this book, we do it the old way, keeping it all in one place. If I had one criticism to make of the way GarageGames does it, it would be that they should not use the same filename in both places. playerDB.cs and playerMthd.cs or something similar would be less confusing.


If your model uses embedded animations (see Chapter 14) instead, then you don’t need to use this mechanism. However, it doesn’t only have to be used for its intended purpose. You can use this mechanism to provide auto-loading capabilities for any code that you want to be used whenever a specific model is loaded.

If you created your own animation sequences, then you need to create such an autoloading script. Back in Chapter 14 we used the Torque 3D Tools Demo to test our models. Since we’re using our own game now, we’ll have to put the support for animation sequences into the game.

First we’ll get the standard Torque 3D–provided animations working.

If you used the filename “myHero.dts” for your exported model, then create an empty file called “myHero.cs” and put it in the same folder as “myHero.dts”.

Then put this code in that file:

singleton TSShapeConstructor(myHeroDTS)
{
  baseShape = "./myHero.dts";
  canSaveDynamicFields = "1";
  upAxis = "DEFAULT";
  unit = "-1";

  sequence0 = "assets/models/avatars/player_root.dsq root";
  sequence1 = "assets/models/avatars/player_forward.dsq run";
  sequence2 = "assets/models/avatars/player_head.dsq head";
  sequence3 = "assets/models/avatars/player_side.dsq headside";
  sequence4 = "assets/models/avatars/player_lookde.dsq look";
  sequence5 = "assets/models/avatars/player_diehead.dsq death1";
  sequence6 = "assets/models/avatars/player_diechest.dsq death2";
  sequence7 = "assets/models/avatars/player_dieback.dsq death3";
  sequence8 = "assets/models/avatars/player_diesidelf.dsq death4";
  sequence9 = "assets/models/avatars/player_diesidert.dsq death5";
  sequence10 = "assets/models/avatars/player_dieleglf.dsq death6";
  sequence11 = "assets/models/avatars/player_dielegrt.dsq death7";
  sequence12 = "assets/models/avatars/player_dieslump.dsq death8";
  sequence13 = "assets/models/avatars/player_dieknees.dsq death9";
  sequence14 = "assets/models/avatars/player_dieforward.dsq death10";
  sequence15 = "assets/models/avatars/player_diespin.dsq death11";
  sequence16 = "assets/models/avatars/player_looksn.dsq looksn";
  sequence17 = "assets/models/avatars/player_lookms.dsq lookms";
  sequence18 = "assets/models/avatars/player_scoutroot.dsq scoutroot";
  sequence19 = "assets/models/avatars/player_sitting.dsq sitting";
  sequence20 = "assets/models/avatars/player_celsalute.dsq celsalute";
  sequence21 = "assets/models/avatars/player_celwave.dsq celwave";
  sequence22 = "assets/models/avatars/player_looknw.dsq looknw";
  sequence23 = "assets/models/avatars/player_dance.dsq dance";
  sequence24 = "assets/models/avatars/player_range.dsq range";
};

This tells Torque 3D how to go about creating the shape, and then which animation sequences to load, and how to match each sequence file with its animation name. Many of these sequences are predefined in Torque 3D. Some of them, like “dance” and “range” are not. The ones that are predefined will be automatically used at the appropriate time decided by Torque 3D.

From the RESOURCES folder for Chapter 22, copy the animation files for our hero into the assetsmodelsavatars of your game folder. There are 35 of them, and they are all .dsq files.

And while we’re talking about heroes, go back into koobcontrolserverplayers player.cs and change all instances of MaleAvatarClass to MyHeroClass. There are probably four places—once in the PlayerData datablock:

className = MaleAvatarClass;

and three times in method declarations, looking like this:

MaleAvatarClass::onAdd
MaleAvatarClass::onRemove
MaleAvatarClass::onCollision
Adjusting Model Scale

You may find that your character is not the right size for your needs. If that is the case, it is easy to resolve. Make a judgment about how his size needs to change. For the moment, let’s pretend he needs to be 50 percent bigger than he is now.

Fire up MilkShape and load your model, myhero.ms3d. Choose File, Export, Torque DTS Plus to run the DTSPlus Exporter. If the scale value in the name Scale box is 0.2, then change it to 0.3 (that’s 1.5 times larger than, or 150 percent of, 0.2).

Animations

To properly mount your character in a vehicle, you will need to create a sitting pose. In MilkShape add some more frames in the animation window—make sure to click the Anim button first!

Then select the last frame, and move the joints around until the character looks something like that shown in Figure 22.1.

Create a special material to be the sequence entry for this—it will be a one-frame sequence. Add the sequence to your model and name it “sitting” as you were shown back in Chapter 14, save your work, and then export the file to your koobassets modelsavatarshero directory. If you are exporting the animation to a .dsq file, then export it to koobassetsmodelsavatars.

Be careful, when you are exporting .dsq animations you have to export all of them together. Make sure that in the DTSPlus Exporter you have the Export Animations and Split DSQ Files checkboxes checked. When you get the export dialog, just enter xxx_ for the filename and all of the animations will be exported to their own files, with the string “xxx_” in front of their animation name, with the extension of .dsq.

Figure 22.1
Sitting pose.

image

Then you can grab the “xxx_sitting.dsq” animation file and rename it to “player_sitting.dsq”, or whatever else you choose.

The rest of the mounting stuff will be handled shortly, in the “Vehicle” section.

Tip


If you have problems exporting your sequences, use the older MilkShape 1.8.4 that is also included on the DVD.


Server Code

Back in Chapter 20 I provided you with some code to mount and unmount the vehicle, just so you could hop in and out and test sounds. I didn’t say much about what it did or why. Let’s take a look now.

Collision

The premise is that you simply run up to a car and collide with it to get in. Now you must be mindful not to hit it too hard, or you will hurt yourself when you get in. If you think that it shouldn’t be so easy to hurt yourself, then you can edit your player’s datablock to suit. Simply open koobcontrolserverplayersplayer.cs, find the line that starts with minImpactSpeed=, and increase the value—maybe to around 15 or so.

When your player collides with anything, the server makes a call, via the class name of your character’s datablock to the callback method onCollision.

Tip


The class name for any datablock can be set via script like this:

   classname = classname;

In the case of our player, classname is MyHeroClass, so the line is

   classname = MyHeroClass;

Then methods are defined as

   MyHeroClass::myMethod()
   {
   /// code in here
   }

And they are invoked as

MyHeroClass.myMethod();


OnCollision looks like this:

function MyHeroClass::onCollision(%this,%obj,%col,%vec,%speed)
{
 %obj_state = %obj.getState();
 %col_className = %col.getClassName();
 %col_dblock_className = %col.getDataBlock().className;
 %colName = %col.getDataBlock().getName();
 if ( %obj_state $= "Dead")
  return;
 if (%col_className $= "Item" || %col_className $= "Weapon" )
 {
  %obj.pickup(%col);
 }
  %this = %col.getDataBlock();                // add this
  if ( %this.className $= "Car" )             // add this
  {                                           // add this
   %node = 0;   // Find next available seat   // add this
   %col.mountObject(%obj,%node);              // add this
   %obj.mVehicle = %col;                      // add this
  }                                           // add this
}

Go ahead and add the code above that’s marked “//add this” to your hero’s OnCollision method.

In the parameters, %this refers to the datablock that this method belongs to, %obj is a handle to the instance of the avatar object that is our player in the game, %col is a handle to the object we’ve just hit, %vec is our velocity vector, and %speed is our speed.

The first thing we do is to check our object state, because if we are dead, we don’t need to worry about anything anymore. We want to do this because dead avatars can still slide down hills and bang into things, until we decide to respawn. Therefore we need to stop dead avatars from picking up items in the world.

After that we check the class name of the object we hit, and if it is an item that can be picked up, we pick it up.

Next, if the class is a Car, then this is where the mount action starts. The variable %node refers to the mount node. If %node is 0, then we are interested in the node mount0. That node is created in the model of the car, and the next section will show how we put that in. (This is not difficult—it’s just a matter of creating a joint in the right place and naming it mount0.)

Then we make the call into the engine to mountObject for the car’s object instance, and the game engine handles the details for us. We then update our player’s instance to save the handle to the car we’ve just mounted.

If the object can’t be picked up and is also not mountable, then we actually hit it. The next bit of code calculates our force based on our velocity and applies an impulse to the object we hit. So if we hit a garbage can, we will send it flying.

Mounting

Now when you call the mountObject method, the engine calls back to a method in the MyHeroClass called onMount, and it looks like this:

function MyHeroClass::onMount(%this,%obj,%vehicle,%node)
{
  %obj.setTransform("0 0 0 0 0 1 0");
  %obj.setActionThread(%vehicle.getDatablock().mountPose[%node]);
  if (%node == 0)
  {
    %obj.setControlObject(%vehicle);
    %obj.lastWeapon = %obj.getMountedImage($WeaponSlot);
    %obj.unmountImage($WeaponSlot);
  }
}

You can put this immediately after the OnCollision method.

The onMount method is interested in the %obj, %vehicle, and %node parameters. Our player is %obj, and obviously the vehicle we are mounting is %vehicle. The parameter %node refers to the mount node, as discussed earlier.

The first thing the code does is set our player to a null transform at a standard orientation, because the rest of the player object’s transform information will be handled by the game engine, with the object slaved to the car—wherever the car goes, our player automatically goes as well.

Next, the mount pose is invoked, with the call to setActionThread. The animation sequence that was defined in the datablock as referencing mount0 is set in action. The animation sequence itself is only one frame, so the player just sits there, inside the car. However, your player won’t sit until a change is made to the vehicle definition, which comes later in this chapter.

Now, if we are dealing with node 0, which by convention is always the driver, then we need to do a few things: arrange things so that our control inputs are directed to the car, save the information about what weapons we were carrying, and then unmount the weapon from our player.

Dismounting

Dismounting, or unmounting, is accomplished using whatever key is assigned to the jump action. It’s a bit more involved than the mount code. First, there is this bit:

function MyHeroClass::doDismount(%this, %obj, %forced)
{
  %pos      = getWords(%obj.getTransform(), 0, 2);
  %oldPos = %pos;
  %vec[0] = " 1 1 1";
  %vec[1] = " 1 1 1";
  %vec[2] = " 1 1 -1";
  %vec[3] = " 1 0 0";
  %vec[4] = "-1 0 0";
  %impulseVec = "0 0 0";
  %vec[0] = MatrixMulVector( %obj.getTransform(), %vec[0]);
  %pos = "0 0 0";
  %numAttempts = 5;
  %success     = -1;
  for (%i = 0; %i < %numAttempts; %i++)
  {
    %pos = VectorAdd(%oldPos, VectorScale(%vec[%i], 3));
    if (%obj.checkDismountPoint(%oldPos, %pos))
    {
      %success = %i;
      %impulseVec = %vec[%i];
      break;
    }
  }
  if (%forced && %success == -1)
    %pos = %oldPos;
  %obj.unmount();
  %obj.setControlObject(%obj);
  %obj.mountVehicle = false;
  %obj.setTransform(%pos);
  %obj.applyImpulse(%pos, VectorScale(%impulseVec, %obj.getDataBlock().mass));
}

Most of the code here is involved in deciding if the point chosen to deposit the player after removing him from the car is a safe and reasonable spot or not. We start by setting a direction vector, applying that vector to our player to figure out in advance where the proposed landing site for the freshly dismounted player will be, and then making sure it’s okay using the checkDismountPoint method. If it isn’t okay, the algorithm keeps moving the vector around until it finds a place that is suitable.

Once the site is determined, the unMount method is invoked, and we return control back to our player model, deposit the model at the computed location, and give our player a little nudge.

When unMount is called, the game engine does its thing, and then it summons the callback onUnmount. What we do here is restore the weapon we unmounted.

function MyHeroClass::onUnmount( %this, %obj, %vehicle, %node )
{
  %obj.mountImage(%obj.lastWeapon, $WeaponSlot);
}

Need I say to add this to player.cs. Well, maybe I do, so consider it said.

VEHICLE

We need to revisit the runabout to prepare it for use as a mountable vehicle. The enhancement is not complex—just some changes to its datablock.

Oh Yeah, the Model

If you recall, in Chapter 15 we created a vehicle with two mount nodes. These mount nodes are the locations in the vehicle where the player model will be mounted. I have slapped Figure 22.2 here just to give you a convenient visual reminder of what that looks like.

Figure 22.2
Car mount nodes.

image

Before we get too far, first copy the file RESOURCESch22car.cs into the folder controlservervehicles. Then create a folder called “vehicles” inside the assetsmodels folder. Copy all of your runabout files into this folder, or at least the ones that matter to the game: runabout.dts, wheel.dts, runabout.jpg, and wheel.jpg. Obviously if you used different names for the files, use your versions instead. Just make sure the code that we are about to discuss uses the correct filenames.

Datablock

We need to add a few things to the datablock. Open car.cs, and find the datablock. It looks like this:

datablock WheeledVehicleData(DefaultCar)

If they aren’t already there, add the following to the end of the datablock:

mountPose[0]         = "sitting";
mountPose[1]         = "sitting";
numMountPoints       = 2;

The properties are pretty straightforward—“sitting” refers to the name of sequence in the model that we created earlier with the Hero model in the sitting pose. The name was defined in a special material.

Table 22.1 contains descriptions of the most significant properties available for adjustment in the WheeledVehicleData datablock, even if we aren’t using them all with the runabout.

Table 22.1 WheeledVehicleData Properties

image

image

image

Two other datablocks have significant effect on the behavior of the car: WheeledVehicleTire and WheeledVehicleSpring, shown here (you don’t need to type these in, they’re already there):

datablock WheeledVehicleTire(DefaultCarTire)
{
  shapeFile = "~/data/models/vehicles/wheel.dts";
  staticFriction = 4;
  kineticFriction = 1.25;
  lateralForce = 18000;
  lateralDamping = 4000;
  lateralRelaxation = 1;
  longitudinalForce = 18000;
  longitudinalDamping = 4000;
  longitudinalRelaxation = 1;
};
datablock WheeledVehicleSpring(DefaultCarSpring)
{
  // Wheel suspension properties
  length = 0.85;       // Suspension travel
  force = 3000;        // Spring force
  damping = 600;         // Spring damping
  antiSwayForce = 3;     // Lateral anti-sway force
};

In the WheeledVehicleTire datablock you can see that tires act as springs in two ways. They generate lateral and longitudinal forces to move the vehicle. These distortion/spring forces are what convert wheel angular velocity into forces that act on the rigid body.

Making the Car Go

To make the car load and work in the game, there are a few more things we need to do.

For one thing, the game needs to exec the car.cs file. Open koobcontrolserver server.cs and put the following line:

Exec("./vehicles/car.cs");

in the function OnServerCreated, directly below the line that execs the particles.cs file. That gets the car datablocks loaded and ready to use.

You’ll also need some art resources, specifically a particle for dust, and some sound effects. Check the folder koobassetsparticles and look for dustparticle.png. If it isn’t in there, copy it over from RESOURCESch22 and into that particles folder.

Then check koobassetssound. You want to see if these specific files are in there, and if they are not, copy them over from RESOURCESch22 as well:

Image vcrunch.ogg

Image vcrash.ogg

Image impact.ogg

Image caraccel.ogg

Image caridle.ogg

Image squeal.ogg

Once you have all of your metaphorical ducks in a row, you can test to ensure you’ve got everything right by running Koob, and switching to camera fly mode, then getting up off the ground a bit, somewhere in that parking lot in front of your avatar, and opening the World Editor. While in the Object Editor (F1) go to the Scene tree’s Library tab, and click on the Scripted tab. Double-click on the Vehicles folder and then double-click on the DefaultCar. The car will appear in front of you and fall to the ground (miraculously undamaged).

Run up to the car giving it a nice big belly bump. Now do your darndest to wreck that thing.

TRIGGERING EVENTS

When you need your players to interact with the game world, there is a lot that is handled by the engine through the programming of various objects in the environment, as we saw with collisions with vehicles. Most other interactions not handled by an object class can be dealt with using triggers.

A trigger is essentially a location in the game world, and the engine will detect when the player enters and leaves that space (trigger events). Based on the event detected we can define what should happen when that event is triggered using event handlers or trigger callbacks. We can organize our triggering to occur when there is an interaction with a specific object.

Creating Triggers

If you recall, some of our Koob specifications require us to count the number of laps completed. What we’ll do is add a trigger to the area around the start/finish line, and every time a car with a player in it passes through this area, we’ll increment the lap counter for that player.

For the trigger to know what object to call onTrigger for, you need to add an additional dynamic field with the name of the instance of the trigger when it is created using the Mission Editor.

Open the file koobcontrolserverserver.cs, and at the end of the onServer Created function, add this line:

Exec("./misc/tracktriggers.cs");

This will load in our definitions.

Now create the file koobcontrolservermisc racktriggers.cs, and put the following code in it:

datablock TriggerData(LapTrigger)
{
  tickPeriodMS = 100;
};

function LapTrigger::onEnterTrigger(%this,%trigger,%obj)
{
  if(%trigger.cp $= "")
    echo("Trigger checkpoint not set on " @ %trigger);
  else
    %obj.client.UpdateLap(%trigger,%obj);
}

The datablock declaration contains one property that specifies how often the engine will check to see if an object has entered the area of the trigger. In this case it is set to a 100-millisecond period, which means the trigger is checked 10 times per second.

There are three possible methods you can use for the trigger event handlers: onEnterTrigger, onLeaveTrigger, and onTickTrigger.

The onEnterTrigger and onLeaveTrigger methods have the same argument list. The first parameter, %this, is the trigger datablock’s handle. The second parameter, %trigger, is the handle for the instance of the trigger object in question. The third parameter, %obj, is the handle for the instance of the object that entered or left the trigger.

In this onEnterTrigger the method is called as soon as (within a tenth of a second) the engine detects that an object has entered the trigger. The code checks the cp property of the trigger object to make sure that it has been set (not set to null or “” ). If the cp property (which happens to be the checkpoint ID number) is set, then we call the client’s UpdateLap method, with the trigger’s handle and the colliding object’s handle as arguments.

You can use onLeaveTrigger in exactly the same way, if you need to know when an object leaves a trigger.

The onTickTrigger method is similar but doesn’t have the %obj property. This method is called every time the tick event occurs (10 times a second), as long as any object is present inside the trigger.

Tip


Since we’re going to be using the World Editor a lot in this next section, we’ll need to change the resolution so we can work with the various editors’options comfortably. Launch Koob and at the Startup screen, click the Setup button. Change the Resolution to 1024×768, or even higher if you can. Be sure to leave the program in windowed mode, in case something goes wrong and you have to force-kill the program with Alt+F4 or by choosing Debug, Stop Debugging in Torsion.

Click Done and start the game.


Next, we need to place the triggers in our world. We are going to put five triggers in: one at the start/finish line and one at each of the checkpoints.

Go into camera fly mode, and then move to a position overseeing (looking down at) the start/finish line. Go into the Object Editor (in the World Editor), and then add a trigger by choosing Scene Tree, Library tab, Level tab, Level folder, and then double-clicking on Trigger.

Tip


Don’t forget that when you are placing a new trigger, you need to give it a relevant name. You also need to select the LapTrigger datablock from the datablock pop-up in the Create Object: Trigger dialog.

Also, once you create the trigger, establish the extents of your trigger using the scale tool (press 4)—don’t bother fiddling with the polyhedron values.


Once you have your trigger placed, rotate and position it as necessary underneath the start/finish banner, and resize it to fill the width and the height of the area under the banner. Make the thickness roughly about one-tenth of the width, as shown in Figure 22.3.

Figure 22.3
Placing a trigger.

image

Locate your new object in the Scene tree’s Scene pane at the upper right, and click on it to select it, if it isn’t already selected. In the Inspector frame, locate the Dynamic Fields section at the very bottom of the properties list, and then click the round green button with the plus ( + ) sign in it (the arrow cursor in Figure 22.4 points to it). You will see a new entry appear in the Dynamic Fields section that says “dynamicField” on the left side in the name box, and “defaultValue” on the right side in the value box. Enter cp in the name box and 0 in the value box. What we’ve done is added a property to the object and named it “cp” with the value 0. We can access this property later from within the program code. The next checkpoint will be numbered 1, the one after that will be 2, next is 3, and finally 4, which is the fifth checkpoint. The numbering proceeds in a counterclockwise direction.

Figure 22.4
The Add dynamic field button.

image

Tip


If you need to get rid of a dynamic field, just click on the red circle with the white dash inside to the right side of the dynamic field.


Go ahead and add those checkpoints now, using the same technique I just described. You can copy and paste the first trigger object to create the rest if you like—just remember to change the cp property accordingly (and the object names, if you are giving them names).

Tip


Some objects behave a little oddly when added via copy and paste. After pasting an object into the world, even though it will be visually selected in the view of the world, it sometimes still needs to be selected in the Inspector hierarchy in the upper-right frame. There are times when this may not be strictly necessary, but if you move, rotate, or resize the object by directly manipulating it via the gizmo handles, the changes are sometimes not reflected in the Inspector frame until you reselect the object in the hierarchy.


Now we have the ability to measure progress around the track. We have to add code to use these triggers, and that will be done as part of the scoring system, which is in the next section.

Scoring

We need to keep track of our accomplishments and transmit those values to the client for display.

To kick off the festivities, let’s add some code to controlclientinterfaces playerinterface.gui. Open that file and look for the two GuiTextCtrl objects: scoreLabel and scoreBox. Replace those two controls with these more capable versions:

   new GuiTextCtrl(scorelabel) {
     text = "Score:";
     maxLength = "255";
     margin = "0 0 0 0";
     padding = "0 0 0 0";
     anchorTop = "1";
     anchorBottom = "0";
     anchorLeft = "1";
     anchorRight = "0";
     position = "10 3";
     extent = "39 20";
     minExtent = "8 8";
     horizSizing = "right";
     vertSizing = "bottom";
     profile = "ScoreTextProfile";
     visible = "1";
     active = "1";
     tooltipProfile = "GuiToolTipProfile";
     hovertime = "1000";
     isContainer = "1";
     canSave = "1";
     canSaveDynamicFields = "0";
   };
   new GuiTextCtrl(Scorebox) {
     text = "100";
     maxLength = "255";
     margin = "0 0 0 0";
     padding = "0 0 0 0";
     anchorTop = "1";
     anchorBottom = "0";
     anchorLeft = "1";
     anchorRight = "0";
     position = "60 3";
     extent = "100 20";
     minExtent = "8 8";
     horizSizing = "right";
     vertSizing = "bottom";
     profile = "ScoreTextProfile";
     visible = "1";
     active = "1";
     tooltipProfile = "GuiToolTipProfile";
     hovertime = "1000";
     isContainer = "1";
     canSave = "1";
     canSaveDynamicFields = "0";
   };

The first control is just a label, telling us what the second control contains. That second control is where we stuff our score information.

Laps and Checkpoints

Open the file koobcontrolserverserver.cs, and put the following code at the end of the GameConnection::CreatePlayer method:

  %this.lapsCompleted = 0;
  %this.cpCompleted = 0;
  %this.ResetCPs();
  %this.position = 0;
  %this.money = 0;
  %this.deaths = 0;
  %this.kills = 0;
  %this.score = 0;
  %this.DoScore();

These are the variables we use to track various scores. Now add the following methods to the end of the file:

function GameConnection::ResetCPs(%client)
{
   for (%i = 0; %i < $Game::NumberOfCheckpoints; %i++)
     %client.cpCompleted[%i]=false;
}
function GameConnection::CheckProgress(%client, %cpnumber)
{
   for (%i = 0; %i < %cpnumber; %i++)
   {
    if (%client.cpCompleted[%i]==false)
      return false;
   }
    %client.cpCompleted = %cpnumber;
    return true;
}
function GameConnection::UpdateLap(%client,%trigger,%obj)
{
 if (%trigger.cp==0)
 {
  if (%client.CheckProgress($Game::NumberOfCheckpoints))
  {
      %client.ResetCPs();
      %client.cpCompleted[0] = true;
      %client.lapsCompleted++;
      %client.DoScore();
      if(%client.lapsCompleted >= $Game::NumberOfLaps)
      EndGame();
  }
  else
  {
   %client.cpCompleted[0] = true;
   %client.DoScore();
  }
 }
 else if (%client.CheckProgress(%trigger.cp))
 {
   %client.cpCompleted[%trigger.cp] = true;
   %client.DoScore();
 }
}
function GameConnection::DoScore(%client)
{
 %client.score = (%client.lapsCompleted * $Game::Laps_Multiplier) +
                 (%client.money * $Game::Money_Multiplier) +
                 (%client.deaths * $Game::Deaths_Multiplier) +
                 (%client.kills * $Game::Kills_Multiplier) ;


 %scoreString =        %client.score           @
               " Lap:" @ %client.lapsCompleted @
               " CP:"  @ %client.cpCompleted+1 @
               " $:"   @ %client.money         @
               " D:"   @ %client.deaths        @
               " K:"   @ %client.kills;
 commandToClient(%client, 'UpdateScore', %scoreString);

 if (%client.score >= $Game::MaxPoints &&
     $Game::MaxPoints !$= "" &&
     $Game::MaxPoints != 0) // set maxpoints to "" or 0 to ignore maxpoints
 DoGameDurationEnd();
}

Starting from the last, the DoScore method merely sends a string containing scores to the client using the messaging system. The client code to handle this string will be presented in Chapter 23.

Before that is the meat of these particular functions: UpdateLap. You will recall that this is the method that is called for the client from the onEnterTrigger method.

The first thing UpdateLap does is to check to see if this is the first checkpoint, because it has a special case. Because we will start and drive through the first checkpoint at the start/finish line, it can be legitimately triggered without any other trigger events having occurred. We want to check for this condition. We check this by calling CheckProgress to see how many triggers have been passed. If the answer is none (a false return value), then we are starting the race, so we mark this checkpoint as having been completed and update our score to reflect that fact.

If this isn’t the first checkpoint, then we want to check if all the checkpoints up until this checkpoint have been completed for this lap. If so, then mark this one completed and update the score; otherwise, just ignore it.

Now finally, if we are back at checkpoint 0 and if we check to see if all the other checkpoints have been passed and the result is true, then we are finishing a lap. So we increment the lap, reset the checkpoint counters, mark this checkpoint completed, update the score, and then check to see if the race is over; if not, we continue.

The previous method, CheckProgress, is called from UpdateLap and receives the current checkpoint ID number as a parameter. It then loops through the checkpoint array for this client and verifies that all lower-numbered checkpoints have been set to true (they have been passed). If any one of them is false, then this checkpoint is out of sequence and not legitimate. The function then returns false; otherwise, all is in order, and it returns true.

And then first, but not least (grins), is the method ResetCPs. This simple method just riffles through the checkpoint array setting all entries to false.

We also need to put some initialization code for some of the global variables used in that scoring code we just typed in. At the top of the same file, server.cs, put the following lines:

$Game::Laps_Multiplier = 1; // change these multipliers to weight the scores
$Game::Money_Multiplier = 1;
$Game::Deaths_Multiplier = 1;
$Game::Kills_Multiplier = 1;

You can twiddle with these values to adjust their importance to the single-number score, to your heart’s desire.

Now that call made by commandToClient in the DoScore function is “remotely” invoking the UpdateScore function on the client side. That means we need to add that code in as well. We’ve already put the new score box into the Player Interface object a few pages back, so now we link it all together by adding this next function definition to the end of the file control/client/client.cs:

function clientCmdUpdateScore(%value)
{
    Scorebox.SetValue(%value);
}

That will receive the score values from the DoScores function on the server in one long string that is displayed in the ScoreBox in the PlayerInterface.

Now there are a few odds and ends to deal with. Earlier in this file, server.cs, is the StartGame function. Locate it near the top of the file, and add these lines after the last code in there:

  $Game::NumberOfLaps = 10;
  $Game::NumberOfCheckpoints = 5;

Of course, you should adjust these values to suit yourself. You might want to set NumberOfLaps to a lower number, like 2, for testing purposes. Speaking of testing, if you want to test this, but before addressing the client-side code, then you can add some echo statements and view the output in the console window (invoked by pressing the Tilde key). A good place to put such a statement would be just before the CommandToClient call in DoScore. It would look like this:

echo( "Score " @ %scoreString );
Money

Another requirement is to have randomly scattered coins in the game world.

Open koobcontrolserverserver.cs, again locate the function StartGame, and add the following line to the end of the function:

   PlaceCoins();

Then place the following function just after the StartGame function:

function PlaceCoins()
{
%W=GetWord(MissionArea.area,2);
%H=GetWord(MissionArea.area,3);
%west = GetWord(MissionArea.area,0);
%south = GetWord(MissionArea.area,1);
 new SimSet (CoinGroup);
 for (%i = 0; %i < 4; %i++)
 {
  %x = GetRandom(%W) + %west;
  %y = GetRandom(%H) + %south;
  %searchMasks = $TypeMasks::PlayerObjectType |
      $TypeMasks::InteriorObjectType | $TypeMasks::TerrainObjectType |
      $TypeMasks::ShapeBaseObjectType;
  %scanTarg = ContainerRayCast(%x SPC %y SPC "500", %x SPC %y SPC "-100",
      %searchMasks);
  if(%scanTarg && !(%scanTarg.getType() & $TypeMasks::InteriorObjectType))
  {
   %newpos = GetWord(%scanTarg,1) SPC GetWord(%scanTarg,2) SPC
      GetWord(%scanTarg,3) + 1;
  }
  %coin = new Item("Gold "@%i) {
   position = %newpos;
   rotation = "1 0 0 0";
   scale = "5 5 5";
   dataBlock = "Gold";
   collideable = "0";
   static = "0";
   rotate = "1";
  };
  MissionCleanup.add(%coin);
  CoinGroup.add(%coin);
 }
 // repeat above for silver coin
 for (%i = 0; %i < 8; %i++)
 {
  %x = GetRandom(%W) + %west;
  %y = GetRandom(%H) + %south;
  %searchMasks = $TypeMasks::PlayerObjectType |
      $TypeMasks::InteriorObjectType | $TypeMasks::TerrainObjectType |
      $TypeMasks::ShapeBaseObjectType;
  %scanTarg = ContainerRayCast(%x SPC %y SPC "500", %x SPC %y SPC "-100",
      %searchMasks);
  if(%scanTarg && !(%scanTarg.getType() & $TypeMasks::InteriorObjectType))
  {
   %newpos = GetWord(%scanTarg,1) SPC GetWord(%scanTarg,2) SPC
      GetWord(%scanTarg,3) + 1;
  }
  %coin = new Item("Silver "@%i) {
   position = %newpos;
   rotation = "1 0 0 0";
   scale = "5 5 5";
   dataBlock = "Silver";
   collideable = "0";
   static = "0";
   rotate = "1";
  };
  MissionCleanup.add(%coin);
  CoinGroup.add(%coin);
 }
 // repeat above for copper coin
 for (%i = 0; %i < 32; %i++)
 {
  %x = GetRandom(%W) + %west;
  %y = GetRandom(%H) + %south;
  %searchMasks = $TypeMasks::PlayerObjectType |
      $TypeMasks::InteriorObjectType | $TypeMasks::TerrainObjectType |
      $TypeMasks::ShapeBaseObjectType;
  %scanTarg = ContainerRayCast(%x SPC %y SPC "500", %x SPC %y SPC "-100",
      %searchMasks);
  if(%scanTarg && !(%scanTarg.getType() & $TypeMasks::InteriorObjectType))
  {
   %newpos = GetWord(%scanTarg,1) SPC GetWord(%scanTarg,2) SPC
      GetWord(%scanTarg,3) + 1;
  }
  %coin = new Item("Copper "@%i) {
   position = %newpos;
   rotation = "1 0 0 0";
   scale = "5 5 5";
   dataBlock = "Copper";
   collideable = "0";
   static = "0";
   rotate = "1";
  };
  MissionCleanup.add(%coin);
  CoinGroup.add(%coin);
 }
}

The first thing this function does is obtain the particulars of the MissionArea. For this game you should use the Mission Area Editor to expand the MissionArea to fill the entire available terrain tile.

The %H and %W values are the height and width of the MissionArea box. The variables %west and %south combined make the coordinates of the southwest corner. We use these values to constrain our random number selection.

Then we set up a search mask. All objects in the Torque Engine have a mask value that helps to identify the object type. We can combine these masks using a bitwise-or operation, in order to identify a selection of different types of interest.

Then we use our random coordinates to do a search from 500 world units altitude downward until we encounter terrain, using the ContainerRayCast function.

When the ray cast finds terrain, we add one world unit to the height and then use that plus the random coordinates to build a position at which to spawn a coin. Then we spawn the coin using the appropriate datablock, which can be found in your new copy of item.cs.

Next, we add the coin to the MissionCleanup group so that Torque will automatically remove the coins when the game ends. We also add it to the CoinGroup in case we want to access it later.

Now take a look at the two properties, static and rotate in each of the object definitions.

When set to true, the static property tells Torque 3D that after the coin is picked up by the player, a new coin should respawn in to replace the one that was removed. If we are in the business of collecting coins for maximum accumulated value, then we don’t want the coins to return to be picked up again, adding to the total. So we set the static property to false.

The rotate property tells Torque 3D to rotate the coin around its vertical axis. This is a common technique used to bring attention to power-ups and value items in a game. So we do want the coins to rotate, therefore we set that property to true.

However, there is a bug. Not a huge bug, just an annoying one. If we set an object to rotate but set it to not be static, by setting rotate to true and static to false, then we only get a partial rotation in the item. When the item gets so far in its rotation, it jumps back to its zero angle in the rotation and starts over. You can see for yourself by playing with those two properties.

Improving Your Code


The coin placement code provides a good example of how to improve your code through the use of loops. The code I gave you here can be much smaller, thus using up less memory when your game runs.

If you look at each of the blocks of code for each of the coin types you will see that most of the code is identical. There are only a few things that are different between each coin type: the number of coins placed, the datablock used, and the prefix portion of the object’s name.

If we can find a way so that those three pieces of information can be changed each time we iterate through each coin type, we can have much smaller code. Here’s how we can do it.

We’ll store some of the changing data in an array, and some of the changing data we will compute on the fly.

If you look at the numbers of each coin, notice that the lower the value of the coin (gold is higher than silver, which is higher than copper), the more coins are put in the scene. In fact, each successive drop in value yield twice as many coins. So all we need to do is increment the number of coins to place by the appropriate amount each time through the loop. You don’t have to do it this way. With software, there are usually dozens of way to tackle any given problem. You could also use an array for the coins, for example.

But anyway, what we’ll do is multiply the number of coins by two each through the outside loop (that we are going to be adding to the code).

Next, with the name strings, we’ll store them in a 3-element, one-dimensional array of strings, called

$GoldIndex = 0;        // some useful variables
$SilverIndex = 1;
$CopperIndex = 2;
$CoinName[$GoldIndex]="Gold";
$CoinName[$SilverIndex]="Silver";
$CoinName[$CopperIndex]="Copper";

function PlaceCoins()
{
%maxCoinsOfType = 2;
%typeMultiplier = 2;

  %W=GetWord(TheMissionArea.area,2);
  %H=GetWord(TheMissionArea.area,3);
  %west = GetWord(TheMissionArea.area,0);
  %south = GetWord(TheMissionArea.area,1);
  new SimSet (CoinGroup);
    for (%typeIndex = 0; %typeIndex <= $CopperIndex; %typeIndex++)
  {
    %maxCoinsOfType *= %typeMultiplier;
    for (%i = 0; %i < %maxCoinsOfType; %i++)
    {
      %x = GetRandom(%W) + %west; // get random locations in mission area
      %y = GetRandom(%H) + %south;
      %searchMasks = $TypeMasks::TerrainObjectType; // make search mask
      %scanTarg = ContainerRayCast(%x SPC %y SPC "500",
                                  %x SPC %y SPC "0",
                                  %searchMasks);
      %newpos = GetWord(%scanTarg,1) SPC GetWord(%scanTarg,2) SPC
      GetWord(%scanTarg,3) + 100;
      %typeName = $CoinName[%typeIndex];
      %coin = new Item(%typeName @ %i) {
        position = %newpos;
        rotation = "1 0 0 0";
        scale = "5 5 5";
        dataBlock = %typeName;
        collideable = "0";
        static = "0";
        rotate = "1";
      };
      MissionCleanup.add(%coin);
      CoinGroup.add(%coin);
    }
  }
}

Put that in place of the previous code I gave you, and give it a whirl. Now if you are wondering why I would give you code that I’m just going to go and replace later, I’ll explain it this way: prototyping. A prototype is an experimental version of a product that you are creating; you don’t intend to keep the product in its experimental form.

When you are developing code, and working out an algorithm, and you are not quite sure of the detailed particulars, creating the code in “long form” without using loops that you just know are going to be needed sometimes allows you to work your way through the problem more easily. You can more readily see the relationships between parts, re-arrange them, and so on. Once you have the basic functionality down pat, then you can optimize. The two most common kinds of optimization are for speed and memory footprint. The rewrite of the PlaceCoins function I just gave affects the memory footprint with minimal impact on speed.


After putting that code in, copy RESOURCESCH22item.cs over to koobcontrol servermisc, replacing the existing item.cs. You will find the datablocks for the coins (where the coin values are assigned) in there.

Note that when we added the coins in the preceding code, the static parameter was set to 0. This means that the game will not create a new coin at the place where the coin was picked up, if it is picked up. The weapons of the ammo do this, but we don’t want our coins to do it. It’s a game play design decision.

In addition to the datablocks for the coins in item.cs, you will also find this code:

    if (%user.client)
    {
     messageClient(%user.client,     'MsgItemPickup',     'c0You    picked    up    %1',    %
this.pickupName);
     %user.client.money += %this.value;
     %user.client.DoScore();
    }

The last two statements in there allow the player to accumulate the money values, and then the server notifies the client of the new score. Note that it is similar in that small way to the checkpoint scoring.

Again, until the client code is in place, you can insert echo statements there to verify that things are working properly.

Deaths

We want to track the number of times we die to further satisfy requirements, so open koobcontrolserverserver.cs, locate the method GameConnection::onDeath, and add these lines at the end:

 %this.deaths++;
 %this.DoScore();

By now these lines should be familiar. We can expand the player death by adding some animation. Add the following to the end of koobcontrolserverplayers player.cs:

function Player::playDeathAnimation(%this)
{
  %this.setActionThread("Die1");
}

Now “Die1” should actually be the name of whatever animation you made for the character’s death back in Chapter 14. If you are using Torque’s sequences, then you want to use “Death1” instead. In fact, there are 11 Torque death sequences, so it would be good practice for you to create code in the above function to randomly pick one of the 11 animations.

We covered how to randomly pick a number and add it to a string back in Chapter 20, when we were hurling insults at each other.

Kills

The victim, who notifies the shooter’s client when he dies, actually does the kill tracking. So we go back to GameConnection::onDeath and add this to the end of the function:

  %sourceClient = %sourceObject ? %sourceObject.client : 0;
  if (%obj.getState() $= "Dead")
  {
    if (isObject(%sourceClient))
    {
       %sourceClient.incScore(1);
       if (isObject(%client))
          %client.onDeath(%sourceObject, %sourceClient, %damageType, %location);
    }
}

This bit of code figures out who shot the player and notifies the shooter’s client object of this fact.

Now it is important to remember that all this takes place on the server, and when we refer to the client in this context, we are actually talking about the client’s connection object and not about the remote client itself.

Okay, so now let’s move on to the client side and finish filling the requirements!

MOVING RIGHT ALONG

So, now we have our player’s model ready to appear in the game as our avatar, wheels for him to get around in, and a way to figure out where he’s been.

We also put some things in the game world for the player to pursue to accumulate points and a way to discourage other players from accumulating too many points for themselves (by killing them).

All of these features are created on the server. In the next chapter we will add the features that will be handled by the game client.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.189.23