Using DirectPlay


For the most part, the DirectX Application Programming Interfaces (APIs) have included components that cover just about every aspect of game development. Naturally, graphics is a big part of that, but as you've seen in earlier chapters, it includes other types of things, such as sound and user input. Networking is included as well.

Construction Cue

Beginning with the Summer 2004 release of the Managed DirectX assemblies, the DirectPlay assemblies are no longer being updated.


In the world of networking, you can follow two major types of designs: peer-to-peer and client/server. You should take a moment to understand the differences between these two architectures.

In the peer-to-peer architecture, every member of the session is responsible for its own state and data, and it provides every other peer in the session with this information. In small games or games with a small number of players, this arrangement is sometimes adequate, but I'm sure you can imagine that as the number of players increases, the amount of data each player needs to send increases as well. See Figure 16.1 for a small peer-to-peer session.

Figure 16.1. A small peer-to-peer session.


As you see here, two players talking to each other over the Internet doesn't require that much data. If they're both sending a mere 30 bytes per second of data to the peers in the session, they are each only sending 30 bytes of data. Imagine a large-scale game, with many peers, as in Figure 16.2.

Figure 16.2. A large peer-to-peer session.


As you saw in the first figure, a small networking session, each player was sending a total of 30 bytes per second. Multiply that by the number of players (2), and you have a grand total of 60 bytes per second of data being sent. Looking at the large-scale session here, what do you see? Each player is still sending 30 bytes of data; however, he is sending that data to every player in the session (9), so each player is sending out a total of 270 bytes of data per second. As a whole, the group is sending out 2,430 bytes of data per second. The amount of data being sent doesn't scale linearly.

In the client/server architecture, this problem is eliminated by making one of the members of the networking session the master, or the server. Each client in the session speaks only to the server, and the server in turn relays any necessary data to only the clients that require it. See Figure 16.3 for a visual representation.

Figure 16.3. A typical client/server session.


You might be able to tell here that for the clients, the amount of data sent is much lower. Regardless of the number of session members that exist, the client only ever needs to send the 30 bytes of data to the server. However, the server's resource requirements are much higher than any of the clients', even those clients that exist in a large peer-to-peer session. The server is responsible not only for receiving the constant updates from the clients, but also for sending out any necessary data to all required constants, plus maintaining the entire game world state instead of just a small portion of it.

Because Tankers will only be a two-player game, it wouldn't make sense to add the overhead of requiring a client/server architecture, so it uses a peer-to-peer methodology instead. If you begin getting above say four peers in a session (or you have peers with a slow network connection, such as a modem), you might want to look into the client/server model. With a little groundwork out of the way, now you're ready to start writing some code. Before you can start, you need to make sure that you've added your reference to the networking API. In your project, add a reference to Microsoft.DirectX.DirectPlay and add a new code file called network.cs, which will hold all the code. You can find a initial implementation in Listing 16.1.

Listing 16.1. The Networking Engine
 using System; using System.Drawing; using System.Windows.Forms; using Microsoft.DirectX; using Microsoft.DirectX.DirectPlay; namespace Tankers {     /// <summary>     /// Stores each possible network message (with a byte backend)     /// </summary>     public enum NetworkMessage : byte     {         UpdatePlayerLocation,         UpdateTankTurret,         UpdateTankTexture,         PlayerFired,     }     /// <summary>     /// This class will maintain the network engine for this game     /// </summary>     public class NetworkEngine : IDisposable     {         private unsafe static readonly int PacketSizePlayerLocation =             sizeof(byte) + sizeof(Vector3) + sizeof(float);         private unsafe static readonly int PacketSizePlayerTurret =             sizeof(byte) + sizeof(float) + sizeof(float);         private unsafe static readonly int PacketSizePlayerFire =             sizeof(byte);         private unsafe static readonly int PacketSizeColorChange =             sizeof(byte) + sizeof(byte);         private const string SessionName = "'s game.";         private static readonly Guid gameGuid =          new Guid("{4695EAA3-B9A6-4388-A2FC-D2A36BA9A235}");         private const int GamePort = 9897;         private const int MaximumMessagesInQueue = 5;         private Peer networkDevice = null;         // Will be used to maintain network state         private Address deviceAddress = null;         // Address for the device being used         private bool isConnected = false; // Are you connected yet?         private Player remotePlayer = null; // The remote player         private int remotePlayerId = 0; // The remote player id         public event PlayerCreatedEventHandler NewPlayer;         public event PlayerDestroyedEventHandler DestroyedPlayer;     } } 

First, you see an enumeration that holds the possible messages you will send to the other player in this game. The enumeration itself is derived from byte so you can minimize the amount of data passed over the network (which is always a good thing). The possible messages you want to send are the player's current location (and orientation), the turret (and barrel) orientation, the texture color the tank is using, and whether the player fired a round. Later in this chapter, you will see how these enumeration values are used to send the data to the other players.

In the actual class, though, did you remember to put your project back into the mode that allows unsafe code? Go back and look at Figure 8.1 for a refresher if you've forgotten. The static read-only members that are declared are used to determine the packet size for the particular messages that will be sent. The packet size is important so you send a minimal amount of data. Each "application" in the DirectPlay world must be uniquely identified, so the gameGuid member is this identifier for Tankers. The session name and port constants aren't anything the user would see but make things easier for us later.

The instance members are somewhat interesting as well. You have the Peer object that will represent a single player in the peer-to-peer session (in this case, yourself). You also need to maintain an "address" for the device you are using for the connection (which I explain shortly). You're also interested in whether the connection has been established, as well as information about the remote player. Last, you want to have two events that can be fired when a player either joins the session (NewPlayer) or leaves the session (DestroyedPlayer). These events use event handlers from DirectPlay that I discuss in just a moment.

The constructor for this object should get everything ready for you to either host your own session or join an existing one. See Listing 16.2 for the constructor's code.

Listing 16.2. Initializing Networking Engine
 /// <summary> /// Constructor where the device will be created. /// </summary> public NetworkEngine() {     // Create the peer object     #if (DEBUG)     networkDevice = new Peer();     #else     networkDevice = new Peer(InitializeFlags.DisableParameterValidation);     #endif     // Check if the Internet provider is available     if(!IsInternetAvailable())     {         throw new InvalidOperationException             ("You must have a valid TCP/IP connection to play this game.");     }     // Hook the events that we will care about     networkDevice.Receive += new ReceiveEventHandler(OnDataReceive);     networkDevice.ConnectComplete += new      ConnectCompleteEventHandler(OnConnectComplete);     networkDevice.PlayerCreated += new      PlayerCreatedEventHandler(OnPlayerCreated);     networkDevice.PlayerDestroyed += new      PlayerDestroyedEventHandler(OnPlayerDestroyed);     networkDevice.SessionTerminated += new      SessionTerminatedEventHandler(OnSessionLost);     networkDevice.FindHostResponse += new      FindHostResponseEventHandler(OnFoundHost);     // Create any addresses     deviceAddress = new Address();     // Use the TCP/IP provider on our port     deviceAddress.ServiceProvider = Address.ServiceProviderTcpIp;     deviceAddress.AddComponent(Address.KeyPort, GamePort); } 

When the peer object is created in release mode, the flags passed in turn off parameter validation. It is more efficient to skip the validation of the parameters, so during debugging, you should leave the option on, but in release mode, just skip the parameter validation. If the parameters being passed in are so different between debug and release that this choice causes problems, you have bigger problems than the parameter validation code. After the peer object is created, you can check whether the Internet service provider is available (see Listing 16.3).

Listing 16.3. Checking for Internet
 /// <summary> /// Checks to see if the TCP/IP service provider is available /// </summary> /// <returns>true if available; false otherwise</returns> private bool IsInternetAvailable() {     // Ask about the providers attached     ServiceProviderInformation[] providers = networkDevice.GetServiceProviders(         false);     // See if we have TCP/IP     foreach(ServiceProviderInformation info in providers)     {         if (info.Guid == Address.ServiceProviderTcpIp)         {             // we do, return true             return true;         }     }     // We've made it here, the Internet provider must not be available     return false; } 

DirectPlay provides quite a few different ways of connecting to a session. You have the Internet, which I'm sure most people are familiar with, but other service providers can be accessed as well, such as Internetwork Packet Exchange (IPX) and a direct modem-modem dialup service. Although these items might be interesting, they go beyond what we want to do, which is to simply play the game over the Internet. This method checks the service providers that are installed by the system and looks at each one to determine whether a TCP/IP provider is found. Assuming one is, it returns TRue immediately. If you got to the end and haven't found one yet, the method throws an exception informing you of this failure.

After the check, a number of events are hooked from the networking device. DirectPlay uses these events to update the status on the peers in the session. DirectPlay can trigger quite a few events, but only a small portion of them are hooked here. When data is received is an obvious one because what point would there be to a networking layer if you couldn't receive data? Other interesting things include a connection being complete (either successful or failure), players being created and destroyed, the session being lost, and an event called FindHostResponse, which I discuss shortly.

Finally, after the events are hooked, you want to set up your address. If you've been on the Internet a while, you recognize that you have something called an IP address. In DirectPlay, an address is simply a way of uniquely identifying a piece of networking hardware, whether it is a network card, a modem, or the IPX protocol. With Tankers, you only use a TCP/IP address, so after the device is created, you set the ServiceProvider to it. Finally, the port that will be used for this game is added to the address. TCP/IP can use a range of ports. In most cases, ports under 1024 are reserved, but you can use any port above that. There's no symbolic reason to use 9897 (the value of the constant used here); it just sounded good at the time. As long as the value is not in the reserved range and it is unused, you should be good to go.



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