Someone Has to Control the Tanks


Now that you have an actual world as well as a tank to reside in that world, you need some way to allow your players to interact with the tank (and as a side effect, the world). The player object you're about to take will be the first object you create that implements the IMoveableObject interface you defined in the last chapter. You will be able to detect whether any object that implements the interface is within the view frustum and should be rendered at all. Before you get there, though, you probably want to implement the class itself. Add a new code file player.cs, and see Listing 14.3 for an initial implementation.

Listing 14.3. The Player Class
 using System; using System.Windows.Forms; using Microsoft.DirectX; using Microsoft.DirectX.Direct3D; using Input=Microsoft.DirectX.DirectInput; namespace Tankers {     /// <summary>     /// This class will maintain a player in the game     /// </summary>     public class Player : IDisposable, IMoveableObject     {         private const float MaximumMovementSpeed = 600.0f;         private const float MaximumMovementSpeedWheels = MaximumMovementSpeed /                                                           100.0f;         private const float MaximumRotationSpeed = 1.15f;         private const float MaximumGunSpeed = 0.003f;         private const float GunCoolDown = 2.5f;         private Tank gameTank = null;         private bool isMovingForward = false;         private bool isMovingBackward = false;         private bool isRotatingLeft = false;         private bool isRotatingRight = false;         private bool isPlayerLocal = true;         private bool isPlayerActive = false; // Only for remote players         // Default the mouse stats to -1 so we know they haven't been used yet.         private int mouseX = -1;         private int mouseY = -1;         // The device for DirectInput will only be used for the local player         private Input.Device joystickDevice = null;         private bool hasHat = false; // Will be used to determine if the                                      // joystick has a hat for control         private bool isMovingForwardJoy = false;         private bool isMovingBackwardJoy = false;         private bool isRotatingLeftJoy = false;         private bool isRotatingRightJoy = false;         // Tank firing items         public event EventHandler Fired;         private float lastFiredTime = -GunCoolDown; // So you can fire right away         private float totalTime = 0.0f;         // Store the player's name and allow it to be rendered         private string playerName = string.Empty;         private Font playerFont = null;         private Sprite playerFontSprite = null;     } } 

One of the first things you might have noticed in that code was a reference to DirectInput. You can't use it yet because you haven't added a reference to this component. Do you remember how to get it added? See Figure 3.1 in Chapter 3, "Understanding the Sample Framework," for a refresher, and add a reference to the latest version of Microsoft.DirectX.DirectInput. You use this Application Programming Interface (API) to control the tank with your joystick or game pad. Don't worry, you'll still be able to use the mouse and keyboard just as easily.

The constants defined here for the player are all designed to control the tank, including the maximum speed the tank can move (and its wheels!). To give you an idea of how these constants work, notice that the maximum moving speed is 600 units. These measurements are calculated in seconds, so knowing that the level is 3,000 units in length, you could extrapolate that driving your tank from one side of the level to the other would take about 5 seconds (sec).

Notice also that the rotation speed and gun speed are defined here, as well as the "cool down" period for the gun. You don't want the player to be able to fire too fast, and you can modify this constant to allow the players to fire faster (or slower). The default is allowing them to fire every 2.5 sec, which seems adequate to me. You might want to adjust this rate to suit your needs.

Virtually all the variables here are designed to control or manipulate the tank in some way, so the first variable is the tank itself. Every player has his own tank object. Because you want to control movement based on time, you don't want to simply move the tank when the player presses the correct key or button, but instead you want the movement to be fluid. You implement this part later, but for now, all you need to know is at any given time whether you're moving or rotating in a particular direction. The variables determining whether a player is active and local are both there for networking purposes, which you'll get to in a few short chapters.

Construction Cue

It's important to understand why two sets of variables control movement and rotation, one for the joystick (or game pad) and the other for the keyboard. When a player uses both the joystick and the keyboard to try to gain an advantage (by moving twice as fast, for example), you want the ability to stop that maneuver, and using two sets of variables affords you this opportunity.


The player also has an event to let anyone who cares (the game engine) know when this player has fired a round. You'll also see that the last time the player fired is stored so you can detect whether he is firing too fast. You keep the total time in the game for this reason as well. The last section of variables is used to render the player's name over his tank, so even in multiplayer modes, you'll be able to see your opponent's name, which is always fun.

How do you create a player object? It seems logical that you want to load up the tank and do any setup work you need to get the player ready, and that's exactly what you'll be doing. See the constructor in Listing 14.4 for an implementation.

Listing 14.4. Creating the Player
 public Player(Device device, GameEngine parent, IntPtr parentHandle,     string name, bool isLocal) {     // Create a new tank     gameTank = new Tank(device);     // Store the info here     isPlayerLocal = isLocal;     // Hook the events if the player is local     if (isPlayerLocal)     {         //Hook windows events for user input         parent.KeyDown += new KeyEventHandler(OnKeyDown);         parent.KeyUp += new KeyEventHandler(OnKeyUp);         // Hook mouse events too         parent.MouseMove += new MouseEventHandler(OnMouseMove);         parent.MouseClick += new MouseEventHandler(OnMouseClick);         InitializeJoystick(parentHandle);     }     // Store the player's name     playerName = name;     float fontSize = isPlayerLocal ? 12.0f : 36.0f;     // Create the font for the player     playerFont = new Font(device, new System.Drawing.Font(         ""Arial", fontSize, System.Drawing.FontStyle.Bold |         System.Drawing.FontStyle.Italic));     playerFontSprite = new Sprite(device);     // Hook the font's events     device.DeviceLost += new EventHandler(OnDeviceLost);     device.DeviceReset += new EventHandler(OnDeviceReset); } 

As you guessed, the tank object is created first. Without it, there isn't much use to have the player do anything. When the player is created, the code defines whether or not it's a local player; if it is, the keyboard and mouse events are hooked (Listing 14.5) to allow the player to be manipulated by those items, and a new InitializeJoystick method is called (Listing 14.6) to allow use of the joystick or game pad (if it exists on the system).

Regardless of whether the player is local, you need to create the objects to render the player name. Because this text will be rendered in the world somewhere (and probably rotated and transformed to get above the tanks), you need to create a sprite as well as the font to do the rendering. You also need to hook the OnDeviceLost and OnDeviceReset events (Listing 14.7) to ensure that these objects behave correctly when you lose exclusive mode to the screen.

Listing 14.5. Handling Mouse and Keyboard Input
 private void OnKeyDown(object sender, KeyEventArgs e) {     if (e.KeyCode == Keys.W)     {         isMovingForward = true;     }     else if (e.KeyCode == Keys.S)     {         isMovingBackward = true;     }     else if (e.KeyCode == Keys.A)     {         isRotatingLeft = true;     }     else if (e.KeyCode == Keys.D)     {         isRotatingRight = true;     } } private void OnKeyUp(object sender, KeyEventArgs e) {     // Depending on which key was let up, reset the variables     if (e.KeyCode == Keys.W)     {         isMovingForward = false;     }     else if (e.KeyCode == Keys.S)     {         isMovingBackward = false;     }     else if (e.KeyCode == Keys.A)     {         isRotatingLeft = false;     }     else if (e.KeyCode == Keys.D)     {         isRotatingRight = false;     } } private void OnMouseMove(object sender, MouseEventArgs e) {     if ((mouseX == -1) || (mouseY == -1))     {         // Set the first mouse coordinates         mouseX = e.X;         mouseY = e.Y;         // Nothing else to do now         return;     }     if (gameTank != null)     {         gameTank.GunTurretAngle += ((e.X - mouseX) * MaximumGunSpeed);         gameTank.GunBarrelAngle += ((e.Y - mouseY) * MaximumGunSpeed);     }     // Save the mouse coordinates     mouseX = e.X;     mouseY = e.Y; } private void OnMouseClick(object sender, MouseEventArgs e) {     // If they clicked the left mouse button, fire     RaiseFireEvent(); } 

Pretty self-explanatory code here. If the right keys are pressed, the associated movement variables are enabled; when they are released, the variables are disabled. The mouse controls the gun turret and barrel, and clicking the mouse button calls the RaiseFireEvent method (Listing 14.8), which handles cooling down the gun and actually firing the gun.

Listing 14.6. Initializing the Joystick Device
 private void InitializeJoystick(IntPtr parent) {     // See if there is a joystick attached to the system     // Enumerate joysticks in the system     foreach (Input.DeviceInstance instance in Input.Manager.GetDevices(         Input.DeviceClass.GameControl, Input.EnumDevicesFlags.AttachedOnly))     {         // Create the device. Just pick the first one         joystickDevice = new Input.Device(instance.InstanceGuid);         break;     }     bool hasButton = false;     bool hasXAxis = false;     bool hasYAxis = false;     // If no joystick was found, simply exit this method after a quick message     if (joystickDevice == null)     {         System.Diagnostics.Debugger.Log(0, GameEngine.GameName + ": Warning"             , "No joystick found, will use mouse and keyboard");     }     else     {         // There is a joystick attached, update some state for the joystick         // Set the data format to the predefined format.         joystickDevice.SetDataFormat(Input.DeviceDataFormat.Joystick);         // Set the cooperative level for the device.         joystickDevice.SetCooperativeLevel(parent,               Input.CooperativeLevelFlags.NonExclusive             | Input.CooperativeLevelFlags.Background);         // Enumerate all the objects on the device         foreach (Input.DeviceObjectInstance d in joystickDevice.Objects)         {             if ((d.ObjectId & (int)Input.DeviceObjectTypeFlags.Axis) != 0)             {                // This is an axis, set its range                joystickDevice.Properties.SetRange(                    Input.ParameterHow.ById, d.ObjectId,                    new Input.InputRange(-5000, +5000));                // Set the dampening zone of this as well                joystickDevice.Properties.SetDeadZone(                    Input.ParameterHow.ById, d.ObjectId,                    500);             }             // See if we have a hat that will be used for gun turret movement            if (d.ObjectType == Input.ObjectTypeGuid.PointOfView)            {                hasHat = true;            }            // Make sure we have at least one button and an x/y axis            if (d.ObjectType == Input.ObjectTypeGuid.Button)            {                hasButton = true;            }            if (d.ObjectType == Input.ObjectTypeGuid.XAxis)            {                hasXAxis = true;            }            if (d.ObjectType == Input.ObjectTypeGuid.YAxis)            {                hasYAxis = true;            }        }        // Now if there isn't an x/y axis and button, reset the        // joystick to null since it can't be used        if ( (!hasButton) || (!hasXAxis) || (!hasYAxis) )        {            System.Diagnostics.Debugger.Log(0, GameEngine.GameName + ": Warning"                , "Joystick found but without necessary features,                   will use mouse and keyboard");            joystickDevice.Dispose();            joystickDevice = null;        }     } } 

This code is pretty much all brand new because you've never seen any DirectInput code. The first thing you do is enumerate all the joysticks on the system, which is what you're specifying with the Input.DeviceClass.GameControl flag. You also don't care about any joysticks that aren't connected to the system currently so you pass in the Input.EnumDevicesFlags.AttachedOnly flag to enumerate only attached devices. For the first device that you find, create it and stop the enumeration. No sense in continuing once you have a device.

If you've ever seen more than one joystick for the PC, you probably already know that there are quite a few, and each has different options. You might have a super-simple joystick that has a button and that's it or something so complex you need a degree in rocket science just to look at it. Right now, you don't know enough about the joystick device that was created (assuming one was created) to even detect whether it's capable of being used by the game. Figuring that out is what you do now.

Before you do that work, however, let DirectInput know about the joystick data, which is what the SetDataFormat method does. Because more than one object can have access to the devices at a time, you want to specify what kind of access you need, in this case, nonexclusive access in the background. Any other foreground applications could take access of the device, but otherwise, you want access to it.

With that out of the way, you can detect all the objects on the device now. For every object attached to the device, you check what type it is. If the object is an axis, you set the range of the axis to some reasonably high level (10,000 "units"). You should note that if it is a digital axis, it simply moves to the highest level instantly; if it is an analog axis, the data is returned based on how far the axis has been moved. In case you're wondering, an axis is the portion of a joystick that allows movement. For example, a flight stick allows you to move both horizontally (the x axis) and vertically (the y axis). Most joysticks have at least an x and y axis and some number of buttons. The dead zone is the zone in which the axis is considered "centered." In this case, you have a dead zone of 5%, which means you need to move the axis more than 5% away from center before it's marked as "moving."

Depending on the other features the joystick has (for example, buttons, hats), you simply store that information. After each object is detected, you detect whether this device is capable of playing the game (does it have an x axis, a y axis, and a button?), and if it does not, you dispose of the joystick and use the keyboard and mouse. Later, when you implement the updating player method, you'll see how to read data from the joystick.

Listing 14.7. Handling Sprites and Fonts During Reset
 private void OnDeviceLost(object sender, EventArgs e) {     if ( (playerFont != null) && (!playerFont.Disposed) )     {         playerFont.OnLostDevice();         playerFontSprite.OnLostDevice();     } } private void OnDeviceReset(object sender, EventArgs e) {     if (playerFont != null)     {         playerFont.OnResetDevice();         playerFontSprite.OnResetDevice();     } } 

Recognizing these event handlers should be second nature to you by now. If one of the objects was created, it's probably a safe bet that they both wereso in each event, you simply check one and then perform the appropriate operation on each.

Listing 14.8. Ready, Aim, Fire!
 public bool RaiseFireEvent() {     // Can the gun be fired again?     if ((totalTime - lastFiredTime ) > GunCoolDown)     {         // Yes it can, do so now         if (Fired != null)         {             Fired(this, EventArgs.Empty);         }         // Store the last time the gun was fired         lastFiredTime = totalTime;         // The gun was fired         return true;     }     return false; } 

This method is simply a small wrapper around the firing logic to ensure that you can't fire until the gun has cooled down enough. A simple check indicates whether enough time has passed since the last time you fired the gun; if it has, the Fired event is, well, fired, and the last fired time is stored again. This method returns a Boolean value so the caller can detect whether the gun was really fired.



Beginning 3D Game Programming
Beginning 3D Game Programming
ISBN: 0672326612
EAN: 2147483647
Year: 2003
Pages: 191
Authors: Tom Miller

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net