A DirectPlay Tutorial
DirectX Programming
By Sobeit Void
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 darkaurora@yahoo.com
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.
Let's get started
What DirecPlay 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.
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.
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.
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.
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
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 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.
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
CoUnintialize(); // unregister the comp
return(0);
}
// creating lobby object
DirectPlayLobbyCreate(NULL, &old_lpdplobbyA, NULL, NULL, 0);
// get new interface of lobby
old_lpdplobbyA->QueryInterface(IID_LPDIRECTPLAYLOBBY2A,
(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.
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
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 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);
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 Management
Now that we have a player, we need to know how to send and receive messages.
I will talk about sending messages first.
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 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 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
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 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();
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.
|