Amazon Honor System Click Here to Pay Learn More

by Forest J. Handford

In this chapter we will add DirectPlay to Space Adventure.

In this chapter you will learn the basics of DirectPlay.  DirectPlay is an easy way for applications to communicate to each other.  For our newest version of Space Adventure we will also have to cover multi-threaded programming.

Multi-threading

If your already familiar with multi-threading skip straight to the DirectPlay section.  Multi-threading gets its name from rope.  Consider a piece of of rope.  A piece of rope can be one thread or it can be several threads wound together.  Rope made out of more than one thread gets its strength from each thread.  If it loses a thread the whole rope will probably fray and become useless.

In programming a thread is a set of instructions.  A program can be made up of more than one thread.  Up till now all of our sample applications have had one thread.  Multi-threaded applications are needed so that a program doesn't stall waiting for a response from hardware or Windows.  Multi-threaded applications also are used to speed up response time.  All threads in a multi-threaded application run virtually simultaneously.  A thread can send a message to other threads.
 
To create a thread you need to decide the stack size for the thread.  A stack is an amount of memory available to programs.  If you set zero as the initial stack size the thread will receive a stack as large as the thread that created it.  If you use a stack size greater than zero it will be rounded to the closest page size.  Pages are how memory is divided up and given addresses in a IBM compatible PC.

You'll also have to provide a pointer to the thread function.  You can pass one argument to the thread function.  When you create a thread you can pass it flags or zero.  One flag you could pass is CREATE_SUSPENDED which would mean the thread would not start running until the ResumeThread function was called.  Every thread has a thread id.  The id is a unique 32 bit value.

To create a thread use the CreateThread function.  The CreateThread function returns a handle to the new thread if it succeeds or NULL if it fails.  A handle is a pointer to a window.  Here's the CreateThread function:
CreateThread( LPSECURITY_ATTRIBUTES lpThreadAttributes,  // pointer to security attributes
          DWORD dwStackSize,                             // initial thread stack size
          LPTHREAD_START_ROUTINE lpStartAddress,         // pointer to thread function
          LPVOID lpParameter,                            // argument for new thread
          DWORD dwCreationFlags,                         // creation flags
          LPDWORD lpThreadId )                           // pointer to receive thread ID

Here is an example of CreateThread as used in this version of Space Adventure:
hReceiveThread = CreateThread(NULL, // security attributes
                                   0,           //  initial thread size
                                   ReceiveThread, // Thread function
                                   g_hwnd, // argument for thread function
                                   0,           // creation flags
                                   &idReceiveThread); // pointer to thread ID

To exit a thread use the ExitThread function.  The ExitThread function takes one argument.  The argument should be a pointer to code to execute when you exit the thread or zero.  You can use this argument like a class destructor.  If the thread has any dynamically allocated structure you should destroy it there or before the code is called.  A typical thread function would end as follows:
    ExitThread( 0 );

    return ( 0 );
}

When using multiple threads in an application you will want to use critical sections.  Threads can access global variables.  If multiple threads access a variable at the same time the data could get distorted.  To prevent this you will want to create a critical section.  No two threads can enter the same critical section at the same time.  If one thread enters a critical section all other threads must wait to enter the critical section.

To initialize a critical section call the InitializeCriticalSection function and pass it a pointer to your critical section.  When your done with the critical section you can delete it by calling DeleteCriticalSection and passing the pointer to your critical section.  Before your critical code call the EnterCriticalSection function and at the end of the code call LeaveCriticalSection, both functions need to be passed a pointer to your critical section.  To create a variable for a critical section use the CRITICAL_SECTION type.

To find out more about multi-threading visit your local book store or amazon.com.  Although I don't have any multi-threading books I know there are several good books dedicated to the subject.

DirectPlay

Before DirectPlay developers had to write code that catered to as many communication methods as possible.  The code for a modem was different from the code for a serial connection which was different from the TCP/IP code, which was different from the network code.  With DirectPlay Windows deals with the differences between the methods for you.

A key to DirectPlay is identifying sessions to join.  This identification process will prevent a StarCraft player from joining a Quake game.  Each DirectPlay program use a globally unique identifier (GUID).  These identifiers are made up of hex values.  If you create two versions of a game that are incompatible with each other they should each have a different GUID.  The hexadecimal number system is a base 16 number system.  Our number system is base ten.  Base ten contains ten possible digits 0 through 9.  Base 16 or hex has 0 through F.  The current version of Space Adventure's GUID is:
BAF45162-47DA-46DF-847E-5A5910AEEF84

It is created with the following code:
GUID SPACE_GUID = {
    0xbaf45162,
    0x47da,
    0x46df,
    {0x84, 0x7e, 0x5a, 0x59, 0x10, 0xae, 0xef, 0x84}
};

Before you send any messages you must first do the following things:

  1. Create the DirectPlay object.
  2. Enumerate the connection methods.
  3. Allow a user to select a shortcut or choose one for them.
  4. Initialize the DirectPlay object with the connection shortcut.
  5. Enumerate any existing sessions.
  6. Open an existing section or create a new one.

There are two formats DirectPlay can use to communicate, the first is ANSI and the second is UNICODE.  Windows NT is based on UNICODE.  If your program will run on Windows NT use UNICODE so that string operations will be done faster.  ASCII is a a familiar subset of ANSI.  ANSI is 8 bit and UNICODE is 32 bit.

To create a DirectPlay object for UNICODE use the following code:
CoCreateInstance( CLSID_DirectPlay,
        NULL,
        CLSCTX_ALL,
        IID_IDirectPlay3,
        ( LPVOID * ) &lpDP );

To create a DirectPlay object for ANSI use the following code:
CoCreateInstance( CLSID_DirectPlay,
        NULL,
        CLSCTX_ALL,
        IID_IDirectPlay3A,
        ( LPVOID * ) &lpDP );

Use the following EnumConnections function to enumerate the connection methods:
EnumConnecions( LPCGUID lpguidApplication,    // The GUID
        LPDPENUMCONNECTIONSCALLBACK lpEnumCallback,    // The address of the callback function
        LPVOID lpContext,  // An argument to be passed to the callback function
        DWORD dwFlags);    // DPCONNECTION_DIRECTPLAY or DPCONNECTION_DIRECTPLAYLOBBY

Your callback function must have the following form:
BOOL FAR PASCAL EnumConnectionsCallback( LPCGUID lpguidSP,    // A GUID for the shortcut
        LPVOID lpConnection,    // A pointer to the connection data
        DWORD dwConnectionSize,    // Must be zero
        LPVOID lpContext);    // The context value passed to the EnumConnections method

Now that the connection shortcut is setup you must initialize the connection.  The following is the InitializeConnection function:
InitializeConnection( LPVOID lpConnection,    // The address of the connection info
        DWORD dwFlags);    // Must be zero

To enumerate sessions use the following function:
EnumSessions( LPDPSESSIONDESC2 lpsd,    // The defining structure for desired sessions
        DWORD dwTimeout,    // The total time in milliseconds to wait for a session response
        LPVOID lpContext,    // To be passed to the callback function
        DWORD dwFlags);   // DPENUMSESSIONS_ALL for all sessions without passwords
                        // DPENUMSESSIONS_AVAILABLE for all sessions that can have more players
                        // DPENUMSESSIONS_PASSWORDREQUIRED for all sessions needing a password

Your callback function for EnumSessions must have the following format:
BOOL FAR PASCAL EnumSessionsCallback2( LPCDSESSIONDESC2 lpThisSD,    // A description of the session
        LPDWORD lpdwTimeOut,    // A pointer to the current time-out value
        DWORD dwFlags,    // Zero or DPESC_TIMEDOUT
        LPVOID lpContext);    //The context variable from EnumSessions

For Space Adventure I created the following dialog with the resource editor to choose a connection and session:

The dialog gets saved in the resource file and is built into the executable file.  After they define their name, a connection, and a session they join the game.  To join the game use the following:
Open( LPDPSESSIONDESC2 lpsd,    // The sessions description
        DWORD dwFlags);    // DPOPEN_CREATE to create a new session or
                           // DPOPEN_JOIN to join a session

DirectPlay sessions have session hosts.  The session host starts as the person who created the session.  If the host leaves the session and you want the game to continue to accept new players a new host must be defined.  To have a new host your session must be migrating.  After the original host leaves the person with the smallest ping will become the host.  A ping is the time it takes for a message to get sent from one player to another.  To have a session be migrating set dpDesc.dwFlags = DPSESSION_MIGRATEHOST.

To close a DirectPlay session use the Close function.  The Close function takes no parameters.

Each player's computer must use the CreatePlayer function when a new player joins the session as follows:
CreatePlayer( LPDPID lpidPlayer,  // Address for the new player's ID
        LPDPNAME lpPlayerName,    // Address of the player's name or NULL for no name
        HANDLE hEvent,    // A handle for when the player sends a message or NULL
        LPVOID lpData,    // A pointer to shared data or NULL
        DWORD dwDataSize, // Size of the shared data
        DWORD dwFlags);   // Zero for a normal player or DPPLAYER_SPECTATOR for a spectator

A player is either a remote or local player.  When I start a session I am a local player and people who are also in the session are remote players.  Local players are all in the same DirectPlay object.  To delete a local player use the DeletePlayer function and use the players ID as the argument.

To enumerate players use the following function:
EnumPlayers( LPGUID lpguidInstance,    // NULL for the current session or a GUID to another session
        LPDPENUMPLAYERSCALLBACK2 lpEnumPlayersCallback2,    // Our callback function
        LPVOID lpContext,    // The address of a user defined context variable
        DWORD dwFlags);      // DPENUMPLAYERS_ALL for all players
                             // DPENUMPLAYERS_GROUP for a group of players
                             // DPENUMPLAYERS_LOCAL for players in the local DirectPlay object
                             // DPENUMPLAYERS_REMOTE for remote players
                             // DPENUMPLAYERS_SESSION for players in the specified GUID
                             // DPENUMPLAYERS_SPECTATOR for spectators

The callback function for EnumPlayers should be in the following form:
BOOL FAR PASCAL EnumPlayersCallback2( DPID dpId,    // The id of what is being enumerated
        DWORD dwPlayerType,  // DPPLAYERTYPE_PLAYER or DPPLAYERTYPE_GROUP
        LPCDNAME lpName,     // The name of the player or group
        DWORD dwFlags,       // Flags describing the player
        LPVOID lpContext);   // The context value from EnumPlayers

To change or create a name for a player you can use the following:
SetPlayerName( DPID idPlayer,    // The players id
        LPDPNAME lpPlayerName,   // The address of the name structure
        DWORD dwFlags);    // DPSET_GUARANTEED to have the players confirm the name change
                           // DPSET_LOCAL to store the data locally only
                           // DPSET_REMOTE to store the data with all session members

To get a players name call the following:
GetPlayerName( DPID idPlayer,    // The players id
        LPVOID lpData,           // The address to receive the name
        LPDWORD lpdwDataSize);   // The buffer size or NULL to have the required size set

In DirectPlay you can have groups.  Groups allow you to send messages to an entire group instead of each individual player.  If you send a message to three players it will take three times longer than sending it to a group with three people.  You can use the following to create a group:
CreateGroup( LPID lpidGroup,    // The groups id
        LPDNAME lpGroupName,    // The name of the group
        LPVOID lpData,     // A pointer to shared data if the groups has some, else NULL
        DWORD dwDataSize,  // The size of the shared data area
        DWORD dwFlags);    // Zero for a normal group or DPGROUP_STAGINGAREA for a group
                           // that can launch a new session

You can create a group in a group as follows:
CreateGroupInGroup( DPID idParentGroup,    // The id of the group that we are joining
        LPDPID lpidGroup,  // The new groups id
        LPVOID lpData,     // A pointer to shared data if the groups has some, else NULL
        DWORD dwDataSize,  // The size of the shared data area
        DWORD dwFlags);    // Zero for a normal group or DPGROUP_STAGINGAREA for a group
                           // that can launch a new session

You can add a player to a group with the following:
AddPlayerToGroup( DPID idGroup,    // The ID of the destination group
        DPID idGroup);    // The ID of the group to be removed

You can remove a player from a group:
DeletePlayerFromGroup( DPID idGroup,    // The group's id
        DPID Player);    // The players id

To add a group to a group:
AddGroupToGroup( DPID idParentGroup,    // The id of the group to join
        DPID idGroup);    // The id of the group being added

To delete a group from a group:
DeleteGroupFromGroup( DPID idParentGroup,    // The id of the parent group
        DPID idGroup);    // The id of the group to remove

To destroy a group:
DestroyGroup( DPID idGroup);    // The group id

To enumerate groups use the following:
EnumGroups( LPGUID lpguidInstance,    // NULL for the current session or another sessions GUID
        LPDPENUMPLAYERSCALLBACK2 lpEnumPlayersCallback2,    // Our callback function
        LPVOID lpContext,  // A variable to be passed to the call back function
        DWORD dwFlags);    // DPENUMGROUPS_ALL for all groups
                           // DPENUMGROUPS_LOCAL for local groups
                           // DPENUMGROUPS_REMOTE for all remote groups
                           // DPENUMGROUPS_SESSION for all groups in the specified session
                           // DPENUMGROUPS_STAGINGAREA for groups created as staging areas

To enumerate groups in groups:
EnumGroupsInGroup( DPID idGroup,  // The group id
        LPGUID lpguidInstance,    // Null for the current session or a GUID
        LPDPENUMPLAYERSCALLBACK2 lpEnumPlayersCallback2,    // Our callback function
        LPVOID lpContext,  // A variable to be passed to the call back function
        DWORD dwFlags);    // DPENUMGROUPS_ALL for all groups
                           // DPENUMGROUPS_LOCAL for local groups
                           // DPENUMGROUPS_REMOTE for all remote groups
                           // DPENUMGROUPS_SESSION for all groups in the specified session
                           // DPENUMGROUPS_STAGINGAREA for groups created as staging areas

To enumerate players in a group:
EnumGroupPlayers( DPID idGroup,   // The group id
        LPGUID lpguidInstance,    // Null for the current session or a GUID
        LPDPENUMPLAYERSCALLBACK2 lpEnumPlayersCallback2,    // Our callback function
        LPVOID lpContext,  // A variable to be passed to the call back function
        DWORD dwFlags);    // DPENUMGROUPS_ALL for all groups
                           // DPENUMGROUPS_LOCAL for local groups
                           // DPENUMGROUPS_REMOTE for all remote groups
                           // DPENUMGROUPS_SESSION for all groups in the specified session
                           // DPENUMGROUPS_STAGINGAREA for groups created as staging areas

To set a group name:
SetGroupName( DPID idGroup,    // The group's id
        LPDPNAME lpGroupName,  // The groups name
        DWORD dwFlags);    // DPENUMGROUPS_ALL for all groups
                           // DPENUMGROUPS_LOCAL for local groups
                           // DPENUMGROUPS_REMOTE for all remote groups
                           // DPENUMGROUPS_SESSION for all groups in the specified session
                           // DPENUMGROUPS_STAGINGAREA for groups created as staging areas

To get a groups name:
GetGroupName( DPID idGroup, // The groups id
        LPVOID lpData,      // The address to receive the name or NULL
        LPDWORD lpdwDataSize); // DPENUMGROUPS_ALL for all groups
                               // DPENUMGROUPS_LOCAL for local groups
                               // DPENUMGROUPS_REMOTE for all remote groups
                               // DPENUMGROUPS_SESSION for all groups in the specified session
                               // DPENUMGROUPS_STAGINGAREA for groups created as staging areas

DirectPlay messages are similar to Windows messages.  Each DirectPlay object has a message queue to receive a message.  When a message is sent an integrity check is run to see if the data was corrupted.  Any messages that fail the check don't get added to the message queue.  To ensure that a message gets to all the other players use the DPSEND_GAURANTEED flag.
Send( DPID idFrom,    // The id of the sender
     DPID idTo,// The id of the player or group, to send it to all players use DPID_ALLPLAYERS
        DWORD dwFlags,     // DPSEND_GAURANTEED or DPSEND_ENCRYPTED to encrypt
        LPVOID lpData,     // The address to receive the data
        DWORD dwDataSize); // The data size

Your message will be broken up by DirectPlay and sent through packets so your not restricted to a size limit.  If you do send large messages all of the packets must be delivered for the message to get in the message queue.  If you use the DPSEND_GAURANTEED and a packet is lost it will be resent.

To receive a message use the following:
Receive( LPDPID lpidFrom,    // The data to be set to the sender's id
        LPDPID lpidTo,     // Who the message is to
        DWORD dwFlags,   // DPRECEIVE_ALL to get the first available message
                       // DPRECEIVE_PEEK to get a message but leave it on the queue
                     // DPRECEIVE_TOPLAYER to get the first message from the lpidTo argument
                   // DPRECEIVE_FROMPLAYER to get the first message from the lpidFrom argument
        LPVOID lpData,          // The address for the data
        LPDWORD lpdwDataSize);  // A pointer to the max size.  When finished it is
                                // the size sent and if it failed the size needed

Here's the code from Space Adventure to receive messages and to size the buffer.  By sizing the buffer this way we allow for better and bigger buffers of the future:
    // Don't let Receive work use the global value directly,
    // as it changes it.
    nBytes = dwReceiveBufferSize;

    while( TRUE )
    {
  dprval = lpDP->Receive( &fromID, &toID,
       DPRECEIVE_ALL, lpReceiveBuffer, &nBytes);
 

  if ( dprval == DPERR_BUFFERTOOSMALL )
  // The receive buffer size must be adjusted.
  {
   if ( lpReceiveBuffer == NULL)
   {
    // We haven't allocated any buffer yet -- do it.
    lpReceiveBuffer = malloc( nBytes );
    if ( lpReceiveBuffer == NULL ) {
     OutputDebugString( "Couldn't allocate memory.\n" );
     return;
    }
   }
   else
   {
    // The buffer's been allocated, but it's too small so
    // it must be enlarged.
    free( lpReceiveBuffer );
    lpReceiveBuffer = malloc( nBytes );
    if ( lpReceiveBuffer == NULL ) {
     OutputDebugString( "Couldn't allocate memory.\n" );
     return;
    }
   }
   // Update our global to the new buffer size.
   dwReceiveBufferSize = nBytes;
  }
  else if ( dprval == DP_OK )
  // A message was successfully retrieved.
  {
   if ( fromID == DPID_SYSMSG )
   {
    pGeneric = (DPMSG_GENERIC *) lpReceiveBuffer;
    OutputDebugString( "Processing system message.\n" );
    EvaluateSystemMessage ( pGeneric, hWnd );
   }
   else
   {
    pGameMsg = (LPGENERICMSG) lpReceiveBuffer;
    OutputDebugString("Processing game message.\n");
    EvaluateGameMessage( pGameMsg, fromID );
   }
  }
  else
  {
   return;
  }
    }

The above code should be run in a separate thread so that we avoid wasting time looking for messages when they aren't there.  Once you get the messages you'll have to know how to read them.  Messages can have any type of structure but all of them must have a dwType that is a DWORD.  dwType is a value of a custom message made specifically for your message or a value equal to any of the following flags:
 
 

DPSYS_CREATEPLAYERORGROUP DPMSG_CREATEPLAYERORGROUP A player has been created
DPSYS_DESTROYPLAYERORGROUP DPMSG_DESTROYPLAYERORGROUP A group or player has been destroyed
DPSYS_ADDGROUPTOGROUP DPMSG_ADDGROUPTOGROUP A group has been added to a group
DPSYS_ADDPLAYERTOGROUP DPMSG_ADDPLAYERTOGROUP A player has been added to a group
DPSYS_DELETEGROUPFROMGROUP DPMSG_DELETEGROUPFROMGROUP A group has been removed from a group
DPSYS_DELETEPLAYERFROMGROUP DPMSG_DELETEPLAYERFROMGROUP A player has been removed from a group
DPSYS_HOST DPMSG_HOST This object is the new host
DPSYS_SESSIONLOST DPMSG_SESSIONLOST Connection to the session has been lost
DPSYS_SETPLAYERORGROUPDATA DPMSG_SETPLAYERORGROUPDATA Player or group data has changed
DPSYS_SETPLAYERORGROUPNAME DPMSG_SETPLAYERORGROUPNAME A player's or a group's name has changed
DPSYS_SETSESSIONDESC DPMSG_SETSESSIONDESC The session description has changed

 
To identify messages you can use the switch statement with cases for all the message types your interested in.  The create player or group has the player type in dwPlayerType.  The id of the player is in dpId.  The number of the current players is in dwCurrentPlayers.  The address of the remote data is lpData.  The size of the remote data buffer is in dwDataSize.  To get the name of the group or player use dpnName.  The parent's id is dpIDParent.  The flags used to create the player or group is in dwFlags.  The destroy player or group message also has the player type, the player id, the name, the parent's id and flags.  The destroy player or group message also has a pointer to local data that is lpLocalData and dwLocalDataSize holds the size of the data.  The destroy player or group also has lpRemoteData that points to remote data and dwRemoteDataSize for the size of the data.  The host message, session lost, set player or group data, set player or group name, and set session description only has the type.

Shared data can be used to allow all players to have a copy of data.  This data isn't sent in messages that have to be cracked.  Instead after you receive a message that the data has changed you must use a receive function.  You will only want to use data that doesn't change often, like team names, as shared data.  The reason for this is that every time any player changes any of the shared data it must be resent!

To set player data use the following:
SetPlayerData( DPID idPlayer,    // The players id
        LPVOID lpData,     // The address of the data to be set
        DWORD dwDataSize,  // The size of the data to be set
        DWORD dwFlags);    // DPSET_GUARANTEED the data is guaranteed
                           // DPSET_LOCAL the data will just be stored locally
                           // DPSET_REMOTE the data will be stored throughout the session

To set group data use the following:
SetGroupData( DPID idGroup,    // The group id
        LPVOID lpData,     // A pointer to the data
        DWORD dwDataSize,  // The size of the data
        DWORD dwFlags);    // DPSET_GUARANTEED the data is guaranteed
                           // DPSET_LOCAL the data will just be stored locally
                           // DPSET_REMOTE the data will be stored throughout the session

To get player data:
GetPlayerData( DPID idPlayer,    // The id of the player
        LPVOID lpData,    // The address where the data should go or NULL to find our
                          // how much is needed
        LPDWORD lpdwDataSize, // The size of the data, when returned it will be the actual
                              // used or if lpData was NULL it will be the actual required
        DWORD dwFlags);   // DPGET_LOCAL to get local data or DPGET_REMOTE to get remote data

To get group data:
GetGroupData( DPID idPlayer,    // The id of the player
        LPVOID lpData,    // The address where the data should go or NULL to find our
                          // how much is needed
        LPDWORD lpdwDataSize,  // The size of the data, when returned it will be the actual
                               // used or if lpData was NULL it will be the actual required
        DWORD dwFlags);    // DPGET_LOCAL to get local data or DPGET_REMOTE to get remote data

All of DirectPlay, including data areas, is thread safe.  If you have two threads try to access data at the same time the second will be blocked till the first is done.

Every session has a description and any player can change it.  To set the description use the following:
SetSessionDesc( LPDPSESSIONDESC2 lpSessDesc,    // The new session description
        DWORD dwFlags);    // Unused

To get the session description use the following:
GetSessionDesc( LPVOID lpData,    // A pointer to the description data area
        LPDWORD lpdwDataSize);    // The size of the data area

Now you are ready for our newest version of Space Adventure:

< Space Adventure Files >

That's all for this chapter.  In the next chapter I'll either go over DirectSetup and lobbies.

PREVIOUS CHAPTER       HOME       CHAPTER 4

Amazon Honor System Click Here to Pay Learn More