DelphiX TDXPlay Component

I'm making me a multiplayer game!

By DragonPhinn [Howard N Smith]

This tutorial is written for users familiar with Borland Delphi with access to the DelphiX components. If you don't have them yet, go ahead and steal bandwidth from me and download them Here. DelphiX provides a direct link to Windows DirectX, which means you can quickly and easily make some pretty impressive features. The best part? Half of it is automated! I've been working with the creator of the components, author of the Japaneese book, and Wordware Publishing to help release a DelphiX book in the good ol' US of A, but until then we'll have to make due with my craptacular tutorials. Also, please note that I taught myself everything about this component reading other source, so if my explantion is weak, I apologize.

Today's topic has to do with the sending and recieving of information over the internet using the component TDXPlay - one of my favorites. While learning to use the TDXPlay component, I wrote two simple programs. They are Connection Tester and RPG Fighter. Connection Tester is an instant messaging tool, and RPG Fighter is a cheesy RPG style turn based fighting game. Neither of them contain any readme's or anything since I just zipped and uploaded them now (on the fly) and didn't feel like writing any readme's or anything. If you really want to rip them off, then go ahead. The program that will be created in this program can be downloaded through the link at the end of the tutorial.

First off, lets add a TDXPlay component to our form. Rename it to "play". There are immediatly some important features we have to modify. The first being the GUID. This crazy long number is the ID of your program. Basically, when someone tries to connect to your game their ID checks to see if it's the same as your ID. That way the two programs know they are the same program, and not get confused. You should change the numbers around and then use the random number generation tool.

The other properties are self explanitory. TCPIPSetting allows you to turn on and off TCP/IP support, and ModemSetting does the same. MaxPlayers asks how many players can be in the game at once. I'm not sure what ASync does. I leave it false and everything works out OK for me, however, in the sample source they set it to true.

Next, add a button to the form. On the click event activated write:



play.open;


Fancy eh? Now your program is ready to connect to other computers. Think I'm kidding? Go ahead and run two copies of it, and press the button. Set one as "new game" and the other as "Join game". The new game should set up a name and session name (what people see it's called when they try and connect) and the join game should input your IP Address. Of course you connect but nothing happens.

Add a TListBox to your form, and then double click the OnAddPlayer event from the TDXPlay component. As the name implies, when a player joins the game, this event is triggered. What we are going to do is add the players name to the listbox. As you can see in the OnAddPlayer event of the TDXPlay component, a TDXPlayPlayer called player is passed as a variable. This is the player who just joined. Add the following to the OnAddPlayer event to add the players name to the listbox:



ListBox1.Items.Add(player.name);


Thought this whole internet thing would be more difficult? I know I did. This adds the players name to the listbox. You should note that even the host create the game, he/she is processed through the OnAddPlayer, so if you ran the program, and hosted a game your name would be added to the listbox. This is a good thing, and you'll see why later.

OK, so now we are connected, but thats all. We can see eachother in the room, but something tells me that this isn't the "game" you had in mind. In order to have a real game you need to be able to send information to and from eachother. This is a bit more complicated than the other actions we've gone through so far.

First off, we need to set up some constants, so in your Form1 constants area, add this:



const
     DXCHAT_MESSAGE = 0;


What this does is whenever I say "TDXMessage" it's the same as saying "0". Why do I do this? It makes code clearer to understand. Read on to understand. Next we have to create a message "type". This message type will be how we send and recieve information to and from our program to the other program(s).



type
  TChatMessage = record
    Mtype: DWORD; //THIS is a MUST for all Records you plan on sending
    Len: Integer;
    C: array[0..0] of Char;
  end;


OK, MType will specify the type of message being sent. It can be "TDXMessage" or 0, or any other constant I previously declare. The Len is an integer specifying the lenght of the message and the "c" is an array (to be dynamically set) of characters.

The MType is a must when making records you plan on sending, because the TDXPlay component has a function called DXPlayMessageType which will return this value. If you don't have it then your program will not work.

Now lets add another button to our form, as well as an edit box. Add this to the button2.click event:



procedure TForm1.Button2Click(Sender: TObject);
var
  Msg: ^TChatMessage; //Message is a pointer to a Chat Message
  MsgSize: Integer;
  choice: integer;
begin
  MsgSize := SizeOf(TChatMessage)+Length(Edit1.Text); //Message size = text+normal size of a ChatMessage
  GetMem(Msg, MsgSize); //set the pointer to the right amount of memory
  try
    Msg.MType := DXCHAT_MESSAGE; //set the message type to equal a chat message
    Msg.Len := Length(Edit1.Text); //set the message lenght to equil length of test
    StrLCopy(@Msg^.c, PChar(Edit1.Text), Length(Edit1.Text)); //set the message to the edit.

    choice := ListBox1.
    Play.SendMessage(play.Players.Players[choice].ID, Msg, MsgSize);

    Edit1.Text := ''; //clear the edit boooox
  finally
    FreeMem(Msg); //free memory
  end;
end;


Despite the comments, I'll explain this bit of source further. First off, you have to remember that TDXPlay sends messages via pointers, and not actual data, which is why we have the pointer to the chat message. Also note that all actions are placed in the try..finally block in case something goes wrong.

The first thing we do is set the message type to a DXCHAT_MESSAGE, and then set the message length, which is the length of the edit1.text (the text we wish to send.) We then use the StrLCopy function which copies a certain amount of text from one string to another. Note we say @Msg and not just Msg because we are working with pointers. Like I said, pointers are my weak point, but I believe the "@" character references the data the pointer is pointing to. For more information refer to the Borland help files.

Now that we've filled up all the required information from out Message record, we can send it on it's merry way. But, uh, how do we do that again? First, we need to aquire a target. In the above source I used the ListBox to select a target. However, and I quote here from the DelphiX help files:

When DPID_ALLPLAYERS is specified for the ToID argument,the message is sent to all players except me.

However, I've tried to use DPID_ALLPLAYERS as an argument, but it doesn't comply. If you can figure it out, just drop me a line. Anyway, as I was saying, I used the ListBox to choose a target. Players are assigned numbers in the order they joined the game, starting with 0 and ending at MaxPlayer. Now, coincidentally, if you added the ListBox1.Items.Add statment to the OnAddPlayer event, then the ListBox1.Items will have number references equivalent to the player ID. In other words, the listbox and the TDXPlay components will be in sync. Because of this, we can use the ListBox1.ItemIndex (whatever player is selected from the ListBox) as the target of the message.

Choice retrieves the itemindex, and then we use a function called SendMessage, which will send (as the name implies) a trigger along with some data to another program. The format is as follows:



Play.SendMessage([Player ID], [Message Pointer], [Size of Message]);


The player ID can be retrieved easily. Play.players.players[choice] secifies the player who is selected on the listbox, Play.players.players[choice].id refrences that players ID number. We then put Msg, the pointer we created and filled with data earlier into the message pointer, and then we pass MsgSize as the size of the message. So now the message is sent... but...nothing happen? Thats because once the program recieves it, it doesn't know what to do with it. That is where the OnMessage event comes in.

Add the following code to the OnMessage event of the TDXPlay component:



procedure TForm1.playMessage(Sender: TObject; From: TDXPlayPlayer;
  Data: Pointer; DataSize: Integer);
var
  s: string;
begin
  case DXPlayMessageType(Data) of
    DXCHAT_MESSAGE:
        begin
          if TChatMessage(Data^).Len<=0 then
            s := ''
          else begin
            SetLength(s, TChatMessage(Data^).Len);
            StrLCopy(PChar(s), @TChatMessage(Data^).c, Length(s));
          end;

          ShowMessage(From.Name+'>  '+msg.s);
        end;
  end;
end;


Here, we check to see of what type is the data. Remember when we made that const value for DXCHAT_MESSAGE? Well, we could have also put 0 here, but it is more legible if we do it this way. The first if statment checks to make sure the length is larger than 0. Then if it is, it copies the ChatMessage to the string "s". Afterwards we display the message with a showmessage command. Note that I used the From argument to use the correct senders name.

Ok from here you can experiment on your own with the TDXPlay component to make your own multiplayer engines. Take a look at some source for RPG Fighter to see what you would have to do to send other types of information.



type
    TAction = record
       act: DWord;
       Len, Len2: Integer;
       c: Char;
       dam: Char;
    end;


For starters I changed some of the names. Instead of TChatMessage I changed it to TAction. Mostly because there are two actions to do in RPG Fighter, attack or send a textual message. There are also 2 len integers, one for the message, and another for the damage length. To make things easy, I make everything into strings, and then send them, afterwhich I convert them into integers if need be. Lastly, "c" is the message and "dam" is the amount of damage delt.

Now take a look at the OnMessage event. It's much more complicated.



procedure TForm1.playMessage(Sender: TObject; From: TDXPlayPlayer;
  Data: Pointer; DataSize: Integer);
var
s, s2: string;
compare,damage: integer;
begin
case DXPlayMessageType(data) of
   DXAction:
   begin
        if TAction(Data^).Len<=0 then
        s := ''
        else begin
        SetLength(s, TAction(Data^).Len);
        SetLength(s2, TAction(Data^).Len2);
        StrLCopy(PChar(s), @TAction(Data^).c, Length(s));
        StrLCopy(PChar(s2), @TAction(Data^).dam, Length(s2));
        end;
   compare := strtoint(s);
   damage := strtoint(s2);
   case compare of
   0: Memo1.Lines.Add(from.name+' Punches, dealing '+inttostr(damage)+' damage.');
   1: Memo1.Lines.Add(from.name+' Kicks, dealing '+inttostr(damage)+' damage.');
   2: Memo1.Lines.Add(from.name+' Swings a Short Sword, dealing '+inttostr(damage)+' damage.');
   3: Memo1.Lines.Add(from.name+' Swings a Long Sword, dealing '+inttostr(damage)+' damage.');
   4: Memo1.Lines.Add(from.name+' Swings a Broad Sword, dealing '+inttostr(damage)+' damage.');
   end;
   Button1.Enabled := true;
   php.Position := php.position-damage;
   end;

   DXMessage:
   begin
   if TAction(Data^).Len<=0 then
      s := ''
   else begin
      SetLength(s, TAction(Data^).Len);
      StrLCopy(PChar(s), @TAction(Data^).c, Length(s));
   end;
   Memo1.Lines.Add(From.Name+'>  '+s]));
   end;

   end;
end;


Just like in the other program, the first thing I do is test the type of message sent by the other player. TAction was a const I created earlier in the program. If it was an action then there was some work to be done. I checked to see if the string had any length, and if it didn't, then I set the "s" variable to empty. I set the length of the s and s2 strings to the appropreate size (to handle the data sent) and then I use the StrLCpy function to place the data into the corresponding area. The first string tells the program what attack was selected. So I use a case statement to check which attack was performed, and output the proper text to the memo1 component. Then I set it to the local players turn, and subtract damage delt.

The next section is identical to the OnMessage event you had in your program. It only reads the variables it needs, and the other information is superfluous.

OK! Thats all I'm going to talk about in this tutorial, but if you still need help, do not hesitate to e-mail me at DragonPhinn@juno.com or IM me at my AIM screen name DragnPhinn. Good luck, and happy coding!

The Source for the project written here. I apologize for the sloppyness of this tutorial and of the source. I wrote everything on the fly. Everything worked as far as I know, but if there are any errors please let me know.


DragonPhinn




© 2001 The Dragon's Lair and Howard N Smith.