
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:
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: 
That's all for this chapter. In the next chapter I'll
either go over DirectSetup and lobbies.
PREVIOUS CHAPTER HOME CHAPTER 4