V1.2 (21-11-2001)
- Fixed typo in Create_TCP_Connection code snippet
- CoUnIntialize() should be CoUninitialize()
- IID_LPDIRECTPLAYLOBBY2A should be IID_IDirectPlayLobby2A
V1.1 (11-4-2000)
[size="5"]Abstract
Seeing that there are few (if any) tutorials on DirectPlay, and that it is such a pain to learn DirectPlay from the MSDN (like I have), I thought I would alleviate some of the agony by writing this tutorial. Also I have this day off since I'm waiting for the monstrous 100 MB DX 7 SDK download.
If you find any errors, please email me at [email="robin@cyberversion.com"]robin@cyberversion.com [/email]
[size="5"]What This Tutorial is About
The tutorial will use Directx 5.0 or the IDirectPlay3 interface. I know DX 7.0 is out but I am still downloading it and I passed with DC 6. Although DC 5.0 is inefficient (every packet has high overhead), it seems the new versions are getting better. Anyway, the idea should mainly be the same.
The demo will be a chat program that I am using for my game. It is not a lobby server (a lobby server is something like Blizzard's BattleNet), just a client/server program. The user is assumed to be familiar with C/C++ and some Win32 programming (Of course, DX too). Some networking concepts would help. No MFC, I hate MFC. Putting another layer on Win32 API bothers me when you can access Win32 functions directly. Also, I will only use TCP/IP. If you want to use IPX, modem, or cable, check out the MSDN. They are roughly equivalent.
This tutorial by no means teaches you everything about DirectPlay. Any networking application is a pain to write, and you should consult the MDSN documentation for more information (mostly where I learnt DirectPlay from).
There is no sample demo exe too as I don't have time to write one out to show you.
[size="5"]Let's get started
[size="3"]What DirectPlay is About
DirectPlay is a layer above your normal network protocols (IPX, TCP/IP etc). Once the connection is made, you can send messages without needing to know what the user is connecting with. This is one of the best features (IMO) and though some of you may come up with more efficient messaging protocols with WinSock, I'd rather use DirectPlay and save me hordes of trouble. BTW, DirectPlay uses Winsock too.
[size="3"]Sessions in DirectPlay
A DirectPlay session is a communication channel between several computers. An application must be in a session before it can communicate with other machines. An application can either join an existing session or create a new session and wait for people to join it. Each session has one and only one host, which is the application that creates it. Only the host can change the session properties (we will get to that detail later).
The default mode in a session is peer-to-peer, meaning that the session complete state is replicated on all the machines. When one computer changes something, other computers are notified.
The other mode is client/server, which means everything is routed through a server. You manage the data in this mode, which is probably the best in a chat program. However, I used sort of a hybrid in this tutorial.
[size="3"]Players in DirectPlay
An application must create a player to send and receive message. Messages are always directed to a player and not the computer. At this point, your application should not even know about the computer's location. Every message you sent is directed at a specific player and every received message is directed at a specific local player (except for system messages; more on that later). Players are identified only as local (exist on your computer) or remote (exist on another computer). Though you can create more than one local player on your machine, I find it not very useful. DirectPlay offers additional methods to store application specific data so you don't have to implement a list of players but I'd rather do my own list.
You can also group players in the same session together so any message sent to you will get directed to the group. This is great for games where you can ally with other people and you want to send messages to your allies only. Unfortunately, you have to explore that area on your own.
If at this point you find all this very troublesome, try writing your own network API. You will be glad what DirectPlay does for you.
[size="3"]Messages in DirectPlay
Now we have a session and a player, we can start sending messages. As I said, each message sent is marked by a specific local player and can be sent to any player. Each message received is placed in a queue for that local player. Since I create only one player, all the messages belong to that player. You don't need to bother about this queue; all you need is to extract the messages from it and act on them. The application can poll the receive queue for messages or use a separate thread and events. I tried the thread method and it works great, except for when I use MessageBox to notify the user (in case you don't know, MessageBox displays a message box in Windows). If you want to use threads, you need to pause the thread when the application displays a message box and somehow it gets very messy with synchronization. So I opted for the poll method. Feel free to use threads if you think you can handle it.
There are two types of messages: player and system. Player messages have a sender and receiver. System messages are sent to every player and are marked sent from the system (DPID_SYSMSG). DP stands for DirectPlay, ID for identification. If you cannot understand messages, go and learn more about DirectDraw first. System messages are generated when the session state is changed, i.e. when a new player joins the session.
Note: There are also security features using the Security Support Provider Interface (SSPI) on windows. These messages are encrypted and such. I don't think this is of much use in gaming.
[size="5"]Actual implementation
Whew. Now we get to more details. If you didn't quite understand any of the above, please read them again till you do. If you have any questions like "What if..." it will be answered soon. So let's move on.
The first thing to do is to include the DirectPlay header files:
#include // directplay main
#include // the directplay lobby
If you are wondering why there is a dplay.lib and dplayx.lib, add the dplayx.lib cause I think there are more methods there used in the lobby. Also if you are asking why am I including the dplobby methods when I am not using a lobby server, it will become clearer later.
Also you need to define INITGUID or add the dxguid.lib. Define this at the very top of your project.
#define INITGUID // to use the predefined ids
DEFINE_GUID(our_program_id,
0x5bfdb060, 0x6a4, 0x11d0, 0x9c, 0x4f, 0x0, 0xa0, 0xc9, 0x5, 0x42, 0x5e);
LPDIRECTPLAY3A lpdp = NULL; // interface pointer to directplay
LPDIRECTPLAYLOBBY2A lpdplobby = NULL; // lobby interface pointer
The next thing is the main loop of the program. This is the bare skeleton of what it looks like.
// Get information from local player
// Initialize Directplay connection from the information from above
while(1)
{
if (PeekMessage(&msg,NULL,0,0,PM_REMOVE)
{
// Your normal translating message
} // end if PeekMessage(..)
else
{
// Your game loop
// Receive messages - We will implement this
} // end else
} // end of while(1)
// Close Directplay Connection
BOOL gameServer; // flag for client/server
char player_name[10]; // local player name, limit it to 10 chars
char session_name[10]; // name of session, also limit to 10 chars
char tcp_address[15]; // tcp ip address to connect to
Now before we move on, we should implement a list of players in the current session. Although it will not be necessary here, you will need it in larger applications.
You create a list with the following item element:
Class DP_PLAYER_LIST_ELEM{
DPID dpid; // the directplay id of player
char name[10]; // name of player
DWORD flags; // directplay player flags
// any other info you might need
};
Then we define a global pointer to the class:
DP_PLAYER_LIST *dp_player_list; // list of players in current session
[size="5"]Setting up Connection
[size="3"]Initialization
To set up the connection, we will write a function that takes a TCP/IP string and create a TCP/IP connection. This is also where the lobby interface comes in.
Side note: Although DirectPlay has a method for enumerating the connections available, the method will enumerate all connections even if they are not available. So if you do not have an IPX connection, the enumeration will return an IPX option to connect and the connection would fail only when the user tries to make the connection. This seems redundant to me so I recommend you skip this part and give the options to the user straight, failing only when the user tries to make the connection.
In case you don't know about enumeration, we will talk more about it later. Enumerating things in DirectX is for the user to provide a function that is called (repeatedly) by a process in DirectX. This is known as a callback function.
Here goes the function. Remember you should do error checking for every function call.
int Create_TCP_Connection(char *IP_address)
{
LPDIRECTPLAYLOBBYA old_lpdplobbyA = NULL; // old lobby pointer
DPCOMPOUNDADDRESSELEMENT Address[2]; // to create compound addr
DWORD AddressSize = 0; // size of compound address
LPVOID lpConnection= NULL; // pointer to make connection
CoInitialize(NULL); // registering COM
// creating directplay object
if ( CoCreateInstance(CLSID_DirectPlay, NULL, CLSCTX_INPROC_SERVER,
IID_IDirectPlay3A,(LPVOID*)&lpdp ) != S_OK)
{
// return a messagebox error
CoUninitialize(); // unregister the comp
return(0);
}
// creating lobby object
DirectPlayLobbyCreate(NULL, &old_lpdplobbyA, NULL, NULL, 0);
// get new interface of lobby
old_lpdplobbyA->QueryInterface(IID_IDirectPlayLobby2A, (LPVOID *)&lpdplobby));
old_lpdplobbyA->Release(); // release old interface since we have new one
// fill in data for address
Address[0].guidDataType = DPAID_ServiceProvider;
Address[0].dwDataSize = sizeof(GUID);
Address[0].lpData = (LPVOID)&DPSPGUID_TCPIP; // TCP ID
Address[1].guidDataType = DPAID_INet;
Address[1].dwDataSize = lstrlen(IP_address)+1;
Address[1].lpData = IP_address;
// get size to create address
// this method will return DPERR_BUFFERTOOSMALL - not an error
lpdplobby->CreateCompoundAddress(Address, 2, NULL, &Address_Size);
lpConnection = GlobalAllocPtr(GHND, AddressSize); // allocating mem
// now creating the address
lpdplobby->CreateCompoundAddress(Address, 2, lpConnection, &Address_Size);
// initialize the tcp connection
lpdp->InitializeConnection(lpConnection, 0);
GlobalFreePtr(lpConnection); // free allocated memory
return(1); // success
} // end int Create_TCP_Connection(..)
Then we create the DirectPlay lobby object and we query for the 2A version. Since there is a macro DirectPlayLobbyCreate, we do not need to initialize COM like above. Underneath, this function does the same (COM and CoCreateInstance) except it gets the lowest interface, ie IDirectPlayLobbyA. So we need to get the 2A version, which is done by querying it with the interface identifier (note you query all DirectX objects in the same manner). Then we close the old lobby since we have a new one.
Next we create an address that holds the information about the TCP connection. Since we did not use EnumConnections, we need to build that information ourselves. You can use the information from the enumeration and jump straight to initialize but I prefer to reduce callback functions to a minimum. We set the fields of the address structure and we set the type to the TCP/IP service provider id. This is from defined in dxguid.lib or the including INITGUID above. We set the second element to the address string. You can get all these information from the MSDN, and which parameters to pass to create other types of connection.
Then we get the size of buffer required for the connection by passing in a NULL parameter. The size required will be stored in the variable Address_Size. Note this method will return DPERR_BUFFERTOOSMALL since we are getting the size. Do not interpret this as an error condition. We then allocate memory from the system heap using GlobalAllocPtr, rather than using malloc. We then create the address information by passing the allocated buffer to the function again. Using that we call the DirectPlay Initialize method and we would have created a TCP/IP connection. If InitalizeConnection returns DPERR_UNAVAILABLE, it means the computer cannot make such a connection and it's time to inform the user no such protocol exists on the computer.
People may wonder why I chose to do things the hard way when I could have used DirectPlayCreate and be happy. Well, the main thing is I want to override the default dialog boxes that would pop up to ask the user to enter the information. You don't see that in StarCraft, do you? (Sorry, love Blizzard)
Do note that you should test every function call and return the appropriate error. I do not do that here because of space and also I'm lazy. Also as for how this function works, you pass "" as the string if you are the hosting the session, else you pass the IP of the machine you are connecting to. So in the "Getting user information", you should have enough information to call this function like:
if (gameServer) // if host machine
Create_TCP_Connection(""); // empty string is enough
else
Create_TCP_Connection(tcp_address); // passed the global ip from user
int DirectPlay_Shutdown()
{
if (lpdp) // if connection already up, so it won't be null
{
if (lpdplobby)
lpdplobby->Release();
lpdp->Release();
CoUninitialize(); // unregister the COM
}
lpdp = NULL; // set to NULL, safe practice here
lpdplobby = NULL;
return(1); // always success
} // end int DirectPlay_Shutdown();
[size="3"]Sessions
You will have no choice but to do a callback function here. The main functions you mainly need to do session management are:
EnumSessions - enumerates all the session available sessions
Open - joins or hosts a new session
Close - close the session
GetSessionDesc - get session properties
SetSessionDesc - set session properties
lpdp->Close();
lpdp->EnumSessions(..); // enumerates all the sessions
BOOL FAR PASCAL EnumSessionsCallback2(LPCDPSESSIONDESC2 lpThisCD,
LPDWORD lpdwTimeOut,
DWORD dwFlags,
LPVOID lpContext);
Any pointers returned in a callback function are only temporary and are only valid in the callback function. We must save any information we want from the enumeration. This applies to the player enumeration later. EnumSessions has the prototype:
EnumSessions(LPDPSESSIONDESC2 lpsd, DWORD dwTimeOut,
LPDPENUMSESSIONSCALLBACK2 lpEnumSessionCallback2,
LPVOID context, DWORD dwFlags);
The first parameter is a session descriptor so you need to initialize one.
DPSESSIONDESC2 session_desc; // session desc
ZeroMemory(&session_desc, sizeof(DPSESSIONDESC2)); // clear the desc
session_desc.dwSize = sizeof(DPSESSIONDESC2);
session_desc.guidApplication = our_program_id; // we define this earlier
The second parameter should be set to 0 (recommended) for a default timeout value.
The third parameter is the callback function so we pass the function to it
The fourth parameter is a user-defined context that is passed to the enumeration callback. I will describe how to use it later.
The fifth is the type of sessions to enumerate. Just pass it the default 0 meaning it will only enumerate available sessions. Check out the MSDN for more options.
All these seem to be a good candidate for a function so let's do it.
// our callback function
BOOL FAR PASCAL EnumSessionsCallback(LPCDPSESSIONDESC2 lpThisSD,
LPDWORD lpdwTimeOut,
DWORD dwFlags, LPVOID lpContext)
{
HWND hwnd; // handle. I suggest as listbox handle
if (dwFlags & DPESC_TIMEOUT) // if finished enumerating stop
return(FALSE);
hwnd = (HWND) lpContext; // get window handle
// lpThisSd-> lpszSessionNameA // store this value, name of session
// lpThis->guidInstance // store this, the instance of the host
return(TRUE); // keep enumerating
} // end callback
// our enumeration function
int EnumSessions(HWND hwnd, GUID app_guid, DWORD dwFlags)
{
DPSESSIONDESC2 session_desc; // session desc
ZeroMemory(..); // as above
// set size of desc
// set guid to the passed guid
// enumerate the session. Check for error here. Vital
lpdp->EnumSessions(&session_desc, 0, EnumSessionsCallback, hwnd, dwFlags);
return(1); // success
} // end int EnumSessions
Note: If you return false in the callback, the enumeration will stop and return control the EnumSessions. If you don't stop it, it will loop forever. Also return false if you encounter an error inside. It is imperative you check the value from EnumSessions especially if you are not enumerating available sessions only. Also, the whole program will block while you are enumerating because it has to search for sessions. You can do an asynchronous enumeration too. Check it out yourself.
lpdp->Open(LPDPSESSIONDESC2 lpsd, DWORD dwFlags)
So we define a descriptor as:
DPSESSIONDESC2 session_desc;
ZeroMemory(&session_desc, sizeof(DPSESSIONDESC2));
session_desc.dwSize = sizeof(DPSESSIONDESC2);
session_desc.guidInstance = // instance you have save somewhere;
// and join the session
lpdp->Open(&session_desc, DPOPEN_JOIN);
The return status flag hides a dialog box displaying the progress status and returns immediately. Although the documentation says I should keep calling Open with the return status flag, I have not found a need to do so (nor can I comprehend the reason they gave). You can change the session properties if you are a host using the other two methods I didn't cover.
The flags in the session desc you should set in this tutorial are:
DPSESSION_KEEPALIVE - keeps the session alive when players are abnormally dropped.
DPSESSION_MIGRATEHOST - if the current host exits, another computer will become the host.
[size="3"]Player Creation
We are just about done setting up. Now we create a local player using CreatePlayer. You have to define a name struct like so:
DPNAME name; // name type
DPID dpid; // the dpid of the player created given by directplay
ZeroMemory(&name,sizeof(DPNAME)); // clear out structure
name.size = sizeof(DPNAME);
name.lpszShortNameA = player_name; // the name the from the user
name.lpszLongNameA = NULL;
lpdp->CreatePlayer(&dpid, &name, NULL, NULL, 0, player_flags);
The other function needed is EnumPlayers. I know you all are masters at enumerating now so I leave you all to implement this. Remember the global list of players we defined earlier? Just add the player inside the callback. It works the same way as the enumeration above. You don't have to enumerate the players in this chat but it is cool that you can see who is also connected at the same time.
You do not need to destroy the player because closing the session does that automatically and I don't see why you need to destroy and create another player while you are still connected. Still, it is your application.
[size="5"]Message Management
Now that we have a player, we need to know how to send and receive messages. I will talk about sending messages first.
[size="3"]Sending Messages
There is only one way to send a message and that is through the Send function. (Actually there is another if you use a lobby). If you remember, sending a message requires the id of the sender and the receiver. Good thing you have saved the local player id in a local player struct and all the players' ids in a global player list. So call the function as follows:
lpdp->Send(idFrom, idTo, dwFlags, lpData, dwDataSize);
The flag parameter is how should the message be sent. The default is 0, which means non-guaranteed. The other options are DPSEND_GUARANTTED, DPSEND_ENCRYPTED and DPSEND_SIGNED. Sending a guaranteed message can take more than 3 times longer than a non-guaranteed one so only use guaranteed sending for important messages (like text). The signed and encrypted messages require a secure server, which we did not setup. Also any message received is guaranteed to be free of corruption (DirectPlay performs integrity checks on them).
Something to note: If you create a session that specifies no message id, their message idFrom will make no sense and the receiver will receive a message from DPID_UNKNOWN. Why anyone would want to disable message id is beyond me.
The last two parameters are a pointer to the data to send and the size of that block. Note that DirectPlay has no upper limit of the size you can send. DirectPlay will break large messages into smaller packets and reassemble them at the other end. Beware when sending non-guaranteed messages too; if one packet is lost, the whole message is discarded.
Since we are doing a chat program, I will show an example of sending a chat message
// types of messages the application will receive
const DWORD DP_MSG_CHATSTRING = 0; // chat message
// the structure of a string message to send
typedef struct DP_STRING_MSG_TYP // for variable string
{
DWORD dwType; // type of message
char szMsg[1]; // variable length message
} DP_STRING_MSG. *DP_STRING_MSG_PTR;
// function to send string message from local player
int DP_Send_String_Mesg(DWORD type, DPID idTo, LPSTR lpstr)
{
DP_STRING_MSG_PTR lpStringMsg; // message pointer
DWORD dwMessageSize; // size of message
// if empty string, return
dwMessageSize = sizeof(DP_STRING_MSG)+lstrlen(lpstr); // get size
// allocate space
lpStringMsg = (DP_STRING_MSG_PTR)GlobalAllocPtr(GHND, dwMessageSize);
lpStringMsg->dwType = type; // set the type
lstrcpy(lpStringMsg->szMsg, lpstr); // copy the string
// send the string
lpdp->Send(local_player_id,idTo, DP_SEND_GUARANTEED,
lpStringMsg, dwMessageSize);
GlobalFreePtr(lpStringMsg); // free the mem
return(1); // success
} // end int DP_Send_String_Mesg(..)
[size="3"]Receiving Messages
Receiving messages requires slightly more work than sending. There are two types of messages we can receive - a player message and a system message. A system message is sent when a change in the session state occurs. The system messages we trapped in this chat are:
DPSYS_SESSIONLOST - the session was lost
DPSYS_HOST - the current host has left and you are the new host
DPSYS_CREATEPLAYERORGROUP - a new player has join
DPSYS_DESTROYPLAYERORGROUP - a player has left
The Receive function has similar syntax to the Send function. The only different thing worth mentioning is the third parameter. Instead of the sending parameter, it is a receiving parameter. Set that to 0 for the default value, meaning extract the first message and delete it from the queue.
The whole difficult part about the receiving is that we need to cast the message to DPMSG_GENERIC and it gets messy there. So I give you the function and explain it below.
void Receive_Mesg()
{
DPID idFrom, idTo; // id of player from and to
LPVOID lpvMsgBuffer = NULL; // pointer to receiving buffer
DWORD dwMsgBufferSize; // sizeof above buffer
HRESULT hr; // temp result
DWORD count = 0; // temp count of message
// get number of message in the queue
lpdp->GetMessageCount(local_player_id , &count);
if (count == 0) // if no messages
return; // do nothing
do // read all messages in queue
{
do // loop until a single message is read successfully
{
idFrom = 0; // init var
idTo = 0;
// get size of buffer required
hr = lpdp->Receive(&idFrom, &idTo, 0, lpvMsgBuffer, &dwMsgBufferSize);
if (hr == DPERR_BUFFERTOOSMALL)
{
if (lpvMsgBuffer) // free old mem
GlobalFreePtr(lpvMsgBuffer);
// allocate new mem
lpvMsgBuffer = GlobalAllocPtr(GHND, dwMsgBufferSize);
} // end if (hr ==DPERR_BUFFERTOOSMALL)
} while(hr == DPERR_BUFFERTOOSMALL);
// message is received in buffer
if (SUCCEEDED(hr) && (dwMsgBufferSize >= sizeof(DPMSG_GENERIC)
{
if (idFrom == DPID_SYSMSG) // if system mesg
Handle_System_Message((LPDPMSG_GENERIC)lpvMsgBuffer,
dwMsgBuffersize, idFrom, idTo);
else // else must be application message
Handle_Appl_Message((LPDPMSG_GENERIC)lpvMsgBuffer,
dwMsgBufferSize,idFrom,idTo);
}
} while (SUCCEEDED(hr));
if (lpvMsgBuffer) // free mem
GlobalFreePtr(lpvMsgBuffer);
} // end void Receive_Mesg()
The 2 functions should be implemented as follows:
int Handle_System_Message(LPDPMSG_GENERIC lpMsg, DWORD dwMsgSize,
DPID idFrom, DPID idTo)
{
switch(lpMsg->dwType)
{
case DPSYS_SESSIONLOST:
{
// inform user
// PostQuitMessage(0)
} break;
case DPSYS_HOST:
{
// inform user
} break;
case DPSYS_CREATEPLAYERORGROUP: // a new player
{
// cast to get message
LPDPMSG_CREATEPLAYERORGROUP lp = (LPDPMSG_CREATEPLAYERORGROUP)lpMsg;
// inform user a new player has arrived
// name of this new player is lp->dpnName.lpszShortNameA
} break;
case DPSYS_DESTROYPLAYERORGROUP: // a lost player
{
// cast to get message
LPDPMSG_DESTROYPLAYERORGROUP lp = (LPDPMSG_DESTROYPLAYERORGROUP)lpMsg;
// inform user a player has left
// name of this new player is lp->dpnName.lpszShortNameA
} break;
default:
// an uncaptured message. Error here
} // end switch
return(1); // success
} // end int Handle_System_Message(..)
int Handle_Appl_Message(LPDPMSG_GENERIC lpMsg, DWORD dwMsgSize,
DPID idFrom, DPID idTo)
{
switch(lpMsg->dwType)
{
case DP_MSG_CHATSTRING:
{
// cast to get the message we defined
DP_STRING_MSG_PTR lp = (DP_STRING_MGS_PTR)lpMsg;
if (gameServer) // if server, relay the message to all players
{
lpdp->Send(local_player_id,DPID_ALLPLAYERS,
DPSEND_GUARANTEED, lp, dwMsgSize);
}
// update your chat window
} break;
default:
// unknown application message, bad
} // end switch
return(1); // success
} // end int Handle_Appl_Message(..)
[size="5"]Putting it Together
Now you know how it should be implemented, I will piece the various parts together (in WinMain)
// Get information from local player
// Connection creation
if (gameServer)
Create_TCP_Connection("");
else
Create_TCP_Connection(tcp_address);
// Session part
if (gameServer) // if host
// open connection
else // if client
{
// EnumSessions
// Open connection
}
// Player creation
if (gameServer) // set flags to create serverplayer
// set flags to DPID_SERVERPLAYER
// create local player
while(1)
{
if (PeekMessage(&msg,NULL,0,0,PM_REMOVE)
{
if (msg.message == WM_QUIT)
break;
// translate and dispatch message
} // end if PeekMessage(..)
else
{
// Your game loop
Receive_Mesg();
} // end else
} // end of while(1)
// close session
DirectPlay_Shutdown();