UpdatesV1.2 (21-11-2001)
V1.1 (11-4-2000) AbstractSeeing 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 robin@cyberversion.com What This Tutorial is AboutThe 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. Let's get startedWhat DirecPlay is AboutDirectPlay 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. Sessions in DirectPlayA 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. Players in DirectPlayAn 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. Messages in DirectPlayNow 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. Actual implementationWhew. 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 <dplay.h> // directplay main #include <dplobby.h> // the directplay lobby Also add DPLAYX.LIB to your project. 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 Next you need to give you application a GUID (Global Unique Id). This ID is to distinguish the application in the computer. You don't want your application to send messages to your browser, only your application. You can use guidgen.exe to create a id for your application. Microsoft guarantees that it will never mathematically create the same GUID twice, so we take their word for it. It will look something like DEFINE_GUID(our_program_id, 0x5bfdb060, 0x6a4, 0x11d0, 0x9c, 0x4f, 0x0, 0xa0, 0xc9, 0x5, 0x42, 0x5e); Now to define our globals LPDIRECTPLAY3A lpdp = NULL; // interface pointer to directplay LPDIRECTPLAYLOBBY2A lpdplobby = NULL; // lobby interface pointer If you are wondering what the A behind the interface stands for, it means ANSI version. There are two versions for DirectPlay – ANSI and Unicode. (Unicode is a standard for using 16 bits to represent a character instead of 8 bits, just for internationalization. Just use the ANSI version and forget about supporting multiple languages. Makes everybody happy.) 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 How you get information from the user is up to you. You can do it via a dialog box or any other way you deem. The main thing you need to get is the name and whether if it is the server or client. If it is the server, you get the session name. If it is the client, you get the TCP/IP address to connect to. Anyway, we store those in a global below: 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 I'm sorry if you are firmly against globals. Feel free encapsulate them, but I think globals simplify the learning process here. Also I do not do much error checking here; theoretically you should test the result of every function call. 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 }; You need to implement a list class that adds a player and deletes a player with a specific dpid. Do not reference the players by their names because players can have the same name. Use the id to differentiate players. Due to space constraints, I will not include any code here. Alternatively you can use arrays to hold the global player information, but this is not scalable and more troublesome. Then we define a global pointer to the class: DP_PLAYER_LIST *dp_player_list; // list of players in current session That is about all the globals you need. You should also create a local player struct for additional information for local players only. Setting up ConnectionInitializationTo 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(..) First we initialize COM to increment its count by 1 and we create the DirectPlay object using CoCreateInstance. This is another method of creating DirectX objects, which is actually the method the wrapper function uses. We have to pass in the class identifier and such but the main thing to note is the IID_DirectPlay3A parameter. This is the identifier of the IDirectPlay3A interface. So if you want to get an IDirectPlay2A interface, set the parameter to IID_DirectPlay2A. Similarly if you want an IDirectPlay4A interface. 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 initalize 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 If you think this is the end of connecting, think again. Remember, we need to have a session and a player before we send messages. But before that let's close the connection. 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(); Stick this function at the close connection section. SessionsYou 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 I will only talk about the 3 functions above that we will use in this tutorial. Once you understand them, it is very easy to understand the others. lpdp->Close(); Simple. Just close the session before you call DirectPlay_Shutdown(): All the local players created will be destroyed and the DPMSG_DESTROYPLAYERORGROUP will be sent to other players in the session. lpdp->EnumSessions(..); // enumerates all the sessions This function will only be called by the client side so if you are hosting the session, call Open. What is so troublesome is that the client needs to search for a session to join when they have made a connection, and since there may be more than one session in the host, we need to get every available session and present them to the user. This will require the use of a callback function. The callback function prototype is: BOOL FAR PASCAL EnumSessionsCallback2(LPCDPSESSIONDESC2 lpThisCD, LPDWORD lpdwTimeOut, DWORD dwFlags, LPVOID lpContext); Things to note: This callback function that we implement will be called once for each session that is found using EnumSessions. Once all the sessions are enumerated, the function will be called one more time with the DPESC_TIMEOUT flag. 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); If you wondering why there is a 2 behind certain typedefs, the two means the second version of the type. A rule of thumb is to always use the latest typedefs as they encapsulate more things. 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 This will ensure our program only returns sessions hosted by our program. ZeroMemory is similar to memset to 0 and as you should know, many DirectC calls require you to put the size in the structure passed. 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 I suggest sending a listbox handle as the context so we can save the information and display it to the user. In the callback function, you must allocate space to hold the guidInstance. This is the instance of the program that is hosting the session. You need to pass this information for the user to select which one session. I have commented out the name and the instance. You save them in whatever way you want. Declare a global array and fill in the member. Whatever. I suggest a listbox so you can send messages via the hwnd parameter. Remember, the instance and name are only valid inside the callback function so you must save them to be able to present them later. 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) This functions hosts or joins a session using the dwFlags. If you are joining a session, you only need to fill the dwSize and guidInstance (you saved it somewhere) of the descriptor. 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); If you are hosting a session, you need to fill in, in addition to the above, the name of the session, the maximum number of players allowed in the session and the session flags. You set the flag to Open as DPOPEN_CREATE | DPOPEN_RETURNSTATUS. 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. You can Or the flags together like so: FLAG_1 | FLAG_2 | FLAG_3. Check out the MSDN for more flag options. Player CreationWe 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); This function will return a unique id for the local player within the session. Use this to identify the player rather than using the name. Save this in the local player struct. The player_name passed is obtained earlier in the information asked from user. The middle parameters are used if you do not want to poll the receive queue. Use them if you want to do multithreading. The player flags is either DPPLAYER_SERVERPLAYER or 0, which means non-server player. There can only be one server player in a session. There is also a spectator player but its meaning is defined by the application so we don't use it here. 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. Message ManagementNow that we have a player, we need to know how to send and receive messages. I will talk about sending messages first. Sending MessagesThere 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 idFrom is the id of the local player. This must be set to a locally created player only (we don't want to impersonate another player, do we?) which in most cases is only one. The idTo is the receiver's id. Use DPID_SERVERPLAYER to send to the server only and DPID_ALLPLAYERS to send to everybody (except yourself). Note you cannot send a message to yourself. If you want to direct the message to a specific player, use the player list. You do not have to use a player list in a chat; instead you can add everything to a listbox. But later in the game, a list of players comes handy. 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(..) We first define the types of messages we can have. Since this is a chat program, there can only be one type, which I set to DP_MSG_CHATSTRING. You may add others and set the type so you can reuse the string sending function for different things. That is why the string message struct has a type to differentiate the string contents. The send function basically allocates space and sends the function to the desired player. Note the local_player_id is stored somewhere globally, or you can set it to pass another variable to set the local_player_flag. Do check the errors returned especially with allocation routines. Receiving MessagesReceiving 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 Some of those messages are only sent if certain flags are specified when then host creates the session. Consult the MSDN. 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() First we check if there are any messages in the loop. If not, we break out of this function. We then keep trying to receive the message in the buffer by allocating the new buffer. When the return value is not DPERR_BUFFERTOOSMALL, it means either we have received the message or another serious error has occurred. So we check if the hresult is successful before determining whether it is a system or application message, by which we call the appropriate functions. System messages come from DPID_SYSMSG, which is a reserved value in DirectPlay. 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(..) The two functions are very similar. To get the actual message, we need to cast the message to the appropriate type before extracting the individual components. Even if the application message comes through, we need to cast it get the data within. The dwType parameter we defined in the chat string struct above enables us to differentiate the different messages the application defines. Although the chat requires only one message, which is the chat message, there is definitely a need for more messages types the application should handle. Every new message type you define should include a dwType parameter so the application can differentiate the messages. Putting it TogetherNow 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(); The reason I do not have a sample application to show is because the interface code would be as long as all this code and it would require another tutorial as long as this to explain how to do interface. However if you really do not know how to do a chat interface, I recommend looking at the control EDIT in the MSDN. For simplicity, the chat window can be implemented as a multi-line edit. Anyway I hope this makes DirectPlay clearer and the documentation makes more sense. Discuss this article in the forums
See Also: © 1999-2011 Gamedev.net. All rights reserved. Terms of Use Privacy Policy
|