Upcoming Events
Unite 2010
11/10 - 11/12 @ Montréal, Canada

GDC China
12/5 - 12/7 @ Shanghai, China

Asia Game Show 2010
12/24 - 12/27  

GDC 2011
2/28 - 3/4 @ San Francisco, CA

More events...
Quick Stats
99 people currently visiting GDNet.
2406 articles in the reference section.

Help us fight cancer!
Join SETI Team GDNet!
Link to us Events 4 Gamers
Intel sponsors gamedev.net search:

  Contents

 Introduction
 Let's get started
 Implementation
 Setting up
 the Connection

 Message
 Management

 Putting it
 Together


 Printable version

 


Setting up the Connection

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.

Popup : 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 // empty string is enough Create_TCP_Connection(""); else // pass the global ip from user Create_TCP_Connection(tcp_address);

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.

Popup : DirectPlay_Shutdown

Stick this function at the close connection section.

Sessions

You will have no choice but to do a callback function here. The main functions you mainly need to do session management are:

EnumSessionsenumerates all the available sessions
Openjoins or hosts a new session
Closeclose the session
GetSessionDescget session properties
SetSessionDescset 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.

// enumerate all the sessions lpdp->EnumSessions(..);

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 // clear the desc ZeroMemory(&session_desc, sizeof(DPSESSIONDESC2)); session_desc.dwSize = sizeof(DPSESSIONDESC2); // we defined our_program_id earlier session_desc.guidApplication = our_program_id;

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.

Popup : 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_KEEPALIVEkeeps the session alive when players are abnormally dropped
DPSESSION_MIGRATEHOSTif 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 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, 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.


Next : Message Management