BlitzMax BNet UDP Multiplayer Tutorial 

This tutorial aims to show users that are new to Vertex's BNet networking library how to construct a simple multiplayer game in a small amount of time. You should then be able to take the basics learned here and expand on them to create a multiplayer game of much greater complexity.
Tutorial includes:
- Ping routine
- Reliable packet scheme
- Old and duplicate packet detection scheme
- Join and quit routines
- Player animation state system, shows how to achieve smooth movement and animation at relatively any ping
- Left / Right Arrow Keys - Movement
- Space Bar - Jump
- C - Connect to Host (Client only)
- ESC - Disconnect / Quit
- ServerIP$ - Set this to the host IP (default 127.0.0.1)
- HOSTPORT - Host sets to open UDP port
- bHost - Change this to false to build the client, and true when building the server EXE

Where can I grab a compiled demo for testing?
If you would rather test the tutorial code right away without compiling it yourself, the compiled version can be downloaded from here:
Download Multi.zip ()
To test the multiplayer aspect, run server.exe, then run client.exe and hit 'C' to connect. Luigi should now be visible and animate the same across both windows.

What do I need to compile the code myself?
This tutorial relies on Vertex's BNet module, version 1.41. You may notice that newer versions are available, but version 1.41 is the version I prefer to use. There is no loss of functionality with this version, and after using it, I think you'll agree that it is superior to newer versions that change the way messages are sent and generally over complicate things. Download a special version of BNet 1.41 that has been modified to work with BlitzMax 1.20 here:
Download BNet141.zip
Place the BNet.mod folder in your BlitzMax/mod/pub.mod/ folder to be able to compile the tutorial code.

I get a "CreateUDPStream Failed!" error when running the server.
What am I doing wrong?
The most likely reason for this error is that the UDP port the server is attempting to open is being blocked by a router or firewall. The default value for the HOSTPORT constant is set to 41219. If you are unsure of how to forward or unblock a port, please refer to www.PortForward.com. If you already have a UDP port open, change the HOSTPORT value to the open port number and the server should run correctly.


+ I.Declares


The first line in the code creates a NetObj type handle, then creates a TList object to hold the packets. Each NetObj in the NetList represents one data packet. The Create function sends out the initial packet, assigns the packet its permanent SendID, and then returns the packet to be added to the NetList. The SendID is the number each packet carries to identify itself. This value is used later on to determine if the packet is old or a duplicate, so it can be discarded. Only the NET_JOIN and NET_QUIT packets need to be made reliable, the other packets will use a different send method.


Player and client publics are then declared. This basically creates two sets of positions, one for you and one for the second player. The state key lays out which value correlates to each player state. Instead of constantly sending out our position over the network, we send a single state byte for the receiver to handle. The receiver can then move and animate Mario or Luigi accordingly. This method is what accounts for the smoothness in movement and animation over the network.


+ II.Main

+ III.Functions



Packets Sent
The receive section begins with, If RecvUDPMsg(Stream):
Packets Received
Important Values
RecvID - Received packet's SendID
Data - Packet ID
IP - Client IP
Port - Client Port
Functions of Interest: IsMsgNew(RecvID, Data), RemoveNetObj(AckID) (See their descriptions below)





All of the important functions have been covered! Now, go out and make that MMORPG you always wanted! ;)

+ IV.Junk
What remains are basic functions that handle aspects of the program not directly related to networking. The commenting in each function should explain its purpose.

Enjoyed the tutorial? Comments or questions?
' Multiplayer Tutorial by Eikon
' Made for BlitzBasic.com
' Uses BNet 1.41, modified to work with
' BlitzMax version 1.20
' Need help? E-mail eikon@eiksoft.com
Framework BRL.Max2D
Import BRL.D3D7Max2D
Import PUB.BNet
Import BRL.Basic
Import BRL.Retro
Import BRL.PNGLoader
Extern "win32" ' win32 API calls
Function FindWindow(lpClassName$z, lpWindowName$z) = "FindWindowA@8"
Function GetWindowRect(hWnd, lpRect:Byte Ptr)
End Extern
Type lpRECT ' Type for holding Window Rect
Field l, t, r, b
End Type
AppTitle = "Multiplayer Test"
SetGraphicsDriver D3D7Max2DDriver() ' Use Direct3D7
Graphics 640, 480, 0, 60
WindowMode
In our first section of code, we use the Framework and Import commands to only include the modules that we need when building our EXE. This ensures the EXE will be as small as possible. Next we declare a couple of API functions that we'll be needing in a later function. The lpRect type is for holding the window position that is returned when using the GetWindowRect function. We then give our window a title, set the graphics driver to D3D, and initialize our graphics window. The WindowMode() function is then called to center the window on the user's screen. See the "Junk" section for more info.
' // Network Specific Declares
Global bHost% = False ' Are you hosting or joining?
Global bConn% = False ' Are you connected?
Global ServerIP$ = "127.0.0.1" ' Client should set to server's IP (localhost for testing)
Global intServerIP% = IntIP(ServerIP$) ' Int of server IP
Global Stream:TUDPStream ' UDP Stream
Global myPing%, PingTime ' Ping and Ping Timer
Global UpdateTime ' Main Update Timer
Global SendID%, RecvID%, lastRecv[32] ' Packet IDs and Duplicate checking Array
Global ClientIP%, ClientPort:Short ' Client IP and Port
Global DebugMode = False ' Toggles the display of debug messages
Global bSentFull ' Full update boolean
' // Networking Constants
Const HOSTPORT = 41219 ' * IMPORTANT * This UDP port must not be blocked by a router
' or firewall, or CreateUDPStream will fail
Const NET_ACK = 1 ' ACKnowledge
Const NET_JOIN = 2 ' Join Attempt
Const NET_PING = 3 ' Ping
Const NET_PONG = 4 ' Pong
Const NET_UPDATE = 5 ' Update
Const NET_STATE = 6 ' State update only
Const NET_QUIT = 7 ' Quit
Here we have some public and constant values to be used by network specific code. The commenting should help you understand what each value will be used for.
Global n:NetObj, netList:TList = New TList ' Packet Object and List
' NetObj Type (Makes UDP Packets reliable)
Type NetObj
Field ID, SID, T, Retry ' Packet ID, SendID, Timer, Retry
Function Create:NetObj(ID)
If Stream = Null Then Return
n:NetObj = New NetObj ' Create Packet
n.ID = ID ' Packet ID
n.SID = SendID ' Send ID
Select ID
Case 1 ' Join Packet
WriteInt Stream, n.SID
WriteByte Stream, NET_JOIN
Case 2 ' Quit Packet
WriteInt Stream, n.SID
WriteByte Stream, NET_QUIT
End Select
SendUDP ' Send UDP Message
n.T = MilliSecs() ' Set Timer
Return n
End Function
This very important piece of code is the foundation for our reliable UDP packet system. It is important to note that when sending UDP packets under normal circumstances, the packets are not guaranteed to arrive in order, or even to arrive at all. A method must be devised that will make packets reliable, and that can also differentiate between old, new, and duplicate packets. The above Type takes care of the former issue by adding the packets to a type list. We can then resend the packets up to a retry limit, or send them endlessly until a reply is received.The first line in the code creates a NetObj type handle, then creates a TList object to hold the packets. Each NetObj in the NetList represents one data packet. The Create function sends out the initial packet, assigns the packet its permanent SendID, and then returns the packet to be added to the NetList. The SendID is the number each packet carries to identify itself. This value is used later on to determine if the packet is old or a duplicate, so it can be discarded. Only the NET_JOIN and NET_QUIT packets need to be made reliable, the other packets will use a different send method.

' Resends Packets
Method Handle()
If Stream = Null Then Return
Select ID
Case 1 ' Join Attempt
If MilliSecs() >= T + 100 Then ' Resend every 100ms
WriteInt Stream, SID
WriteByte Stream, NET_JOIN
SendUDP
T = MilliSecs()
If Retry < 20 Then ' Retry (20 times every 100ms = 2 seconds)
Retry:+1
If DebugMode = True Then
Print "*** Resent join packet to host, attempt " + Retry + " ***"
Endif
Else
If DebugMode = True Then Print "*** Dropped join packet ***"
NetList.Remove n ' Drop Packet
EndIf
EndIf
Case 2 ' Quit Attempt (Never Drops)
If MilliSecs() >= T + 100 Then ' Resend every 100ms
WriteInt Stream, SID
WriteByte Stream, NET_QUIT
SendUDP
T = MilliSecs()
If DebugMode = True Then Print "*** Resent quit packet ***"
EndIf
End Select
End Method
End Type
The Handle method is responsible for checking each packet timer to see if it needs to be resent. The NET_JOIN packet is dropped after 20 sends over a period of two seconds. The NET_QUIT packet is never dropped until a response is received because removing a player has to be 100% reliable. Also notice the debug messages here, you can use them to diagnose packet loss issues when compiling the code.
' // Load Images
SetMaskColor 255, 0, 255
Global imgMario:TImage = LoadAnimImage("mario.png", 17, 32, 0, 14, MASKEDIMAGE|FILTEREDIMAGE)
Global imgLuigi:TImage = LoadAnimImage("luigi.png", 17, 32, 0, 14, MASKEDIMAGE|FILTEREDIMAGE)
Global imgTiles:TImage = LoadImage("tiles.png", MASKEDIMAGE|FILTEREDIMAGE)
Global imgClouds:TImage = LoadImage("clouds.png", MASKEDIMAGE|FILTEREDIMAGE)
' Cloud scrolling
Local cloudX#
' Player 1 (You)
' player X, player Y, player Frame, player Direction, player State, animation Timer
Global pX% = 20, pY# = 352, pF%, pD% = 1, pState%, animTime
Global pJ%, pG# ' player Jumping?, player Gravity
' Player 2 (Client)
Global cX%, cY#, cF%, cD%, cState%, cAnimTime ' Client variables
Global cJ%, cG# ' client Jumping?, client Gravity
' Player State Key
' ==========================
' 0 = Idle
' 1 = Running Left
' 2 = Running Right
' 3 = Jumping Left
' 4 = Jumping Right
' 5 = Jumping Up
First our game images are loaded. These PNG files can be found in the Multi.zip file at the top of the tutorial. We'll need Mario to play the host, and Luigi will be the client, the player who is joining the game. There are tiles for them to stand on, and also a scrolling cloud background to liven things up.Player and client publics are then declared. This basically creates two sets of positions, one for you and one for the second player. The state key lays out which value correlates to each player state. Instead of constantly sending out our position over the network, we send a single state byte for the receiver to handle. The receiver can then move and animate Mario or Luigi accordingly. This method is what accounts for the smoothness in movement and animation over the network.

If bHost = True Then ' Hosting preperations
pX = 595; pD = 0 ' Move Host to Right Side
InitNetwork ' Initialize Network
Stream = CreateUDPStream(HOSTPORT) ' Attempt to open port
Else ' Client preperations
Stream = CreateUDPStream() ' Attempt to open port
EndIf
If Not Stream Then Notify "CreateUDPStream Failed!"; End ' Failed to open stream
ClearLastRecv ' Clear Duplicate Array
This last piece of code before the Main section finally opens the UDP Stream that we use to send data between users. The host stream requires an open UDP port (HOSTPORT) that we talked about earlier. All in all, a pretty basic section. Now we must tackle...
+ II.Main
' // MAIN
SetClsColor 92, 148, 252 ' Blue Sky
Repeat
SetScale 2, 2 ' Render Block
DrawImage imgClouds, Int(cloudX), 0 ' Clouds
DrawImage imgTiles, 0, 416 ' Tiles
If bHost = False Then ' Client is Luigi
DrawImage imgLuigi, pX, Int(pY), pF ' Luigi
If bConn = True Then DrawImage imgMario, cX, Int(cY), cF ' Draw 2nd player
Else ' Host is Mario
DrawImage imgMario, pX, Int(pY), pF ' Mario
If bConn = True Then DrawImage imgLuigi, cX, Int(cY), cF ' Draw 2nd player
EndIf
SetScale 1, 1
RenderHUD ' Heads Up Display
If cloudX > -(ImageWidth(imgClouds) * 1.5) Then cloudX:-.3 Else cloudX = 640 ' Scrolling Clouds
HandleClient ' Handle Client based on states
HandleJump ' Jump
HandleInput ' Keyboard Input
UpdateUDP ' Handle UDP Packets
Flip; Cls
Until KeyDown(KEY_ESCAPE) Or AppTerminate() = True
If bConn = True Then NetDisconnect ' Disconnect
End
Main is where all the rendering in our game will occur. You can see, starting from the ' Render Block comment, that we draw the scrolling clouds, the tiles, then Mario and Luigi last. The second player is only drawn once bConn is checked to make sure we are connected. The last part of main is devoted to the functions that handle keyboard input and networking tasks. If the user decides to exit the game, the NetDisconnect function is invoked only if they are connected. Speaking of functions...
+ III.Functions
' // HandleInput function handles keyboard input and player animation
Function HandleInput()
If KeyDown(KEY_LEFT) And pX > 0 Then ' Move Left
If pD = 1 Then pF = 8 ' Correct frame
pD = 0; pX:-2 ' Move player, facing left
If pJ = True Then pState = 3 Else pState = 1 ' Set player state to jumping left
If MilliSecs() >= animTime + 125 Then ' Animate
If pF < 10 Then pF:+1 Else pF = 8
animTime = MilliSecs()
EndIf
If pState = 3 Then pF = 12 ' Jump Frame
bSentFull = False ' Full update needed
ElseIf KeyDown(KEY_RIGHT) And pX < 606 Then
If pD = 0 Then pF = 1 ' Correct frame
pD = 1; pX:+2 ' Move player, facing right
If pJ = True Then pState = 4 Else pState = 2 ' Set player state to jumping right
If MilliSecs() >= animTime + 125 Then ' Animate
If pF < 3 Then pF:+1 Else pF = 1
animTime = MilliSecs()
EndIf
If pState = 4 Then pF = 5 ' Jump Frame
bSentFull = False ' Full update needed
Else
If pD = 1 Then pF = 0 Else pF = 7 ' Idle
animTime = MilliSecs() - 125
If pState = 5 And pJ = True Then
If pD = 0 Then pF = 12 Else pF = 5
Else
pState = 0; pJ = False ' Set player state to idle
If bSentFull = False And bConn = True And pJ = False Then ' Send Full Update to align player
WriteInt Stream, SendID
WriteByte Stream, NET_UPDATE
WriteInt Stream, pX
WriteInt Stream, pY
WriteByte Stream, pD
SendUDP
bSentFull = True
EndIf
EndIf
EndIf
If KeyDown(KEY_SPACE) And pJ = False Then pJ = True; pG = -4.0 ' Jump
If KeyHit(KEY_C) And bConn = False Then NetConnect ' Attempt to Connect
End Function
The HandleInput() function doesn't have a lot to do with networking, only a small section of code near the bottom is relevant. The remainder is very basic input and animation stuff, things that should be pretty self explanatory. The important thing to note is that when the player is idle, the bSentFull flag is checked. If True, the player has moved, and a full update packet is sent. Unlike the NET_STATE packet that contains only the player's current state, the full update packet consists of the player's full position, direction included. This allows the receiver to align your character on his screen whenever you become idle. Lastly, the 'SPACE' key is checked to initiate a jump, and 'C' is used by the client to call up the connect routine.
Function HandleClient()
If bConn = False Then Return ' No Connection
If cState = 1 Or cState = 3 Then ' Run / Jump Left
If cD = 1 Then cF = 8
cD = 0; cX:-2
If MilliSecs() >= cAnimTime + 125 Then ' Animate
If cF < 10 Then cF:+1 Else cF = 8
cAnimTime = MilliSecs()
EndIf
ElseIf cState = 2 Or cState = 4 ' Run / Jump Right
If cD = 0 Then cF = 1
cD = 1; cX:+2
If MilliSecs() >= cAnimTime + 125 Then ' Animate
If cF < 3 Then cF:+1 Else cF = 1
cAnimTime = MilliSecs()
EndIf
ElseIf cState = 0 Then ' Idle
If cD = 1 Then cF = 0 Else cF = 7 ' Idle
cAnimTime = MilliSecs() - 125
EndIf
If cState >= 3 And cJ = False Then cJ = True; cG = -4.0 ' Jump
End Function
The HandleClient() function is almost an exact clone of HandleInput(), the difference is that this function is responsible for controlling the net player. The cState variable that we receive from our opponent's NET_STATE packet tells us how to move and animate his character on our screen.
' // Controls Player Jump for both you and client
Function HandleJump()
If pJ = False And cJ = False Then Return ' Not Jumping
pState = 5
pY:+pG
If pG < 3 Then pG:+.08 Else pG = 3 ' Limit gravity
If Int(pY) >= 352 Then
pY = 352; pJ = False ' Turn off jumping
animTime = MilliSecs() - 125
Else ' Handle animation frames
If pD = 0 Then pF = 12 Else pF = 5
EndIf
If bConn = True Then ' Client Jump
cY:+cG
If cG < 3 Then cG:+.08 Else cG = 3 ' Limit gravity
If Int(cY) >= 352 Then
cY = 352; cJ = False ' Turn off jumping
cAnimTime = MilliSecs() - 125
cState = 0
Else ' Handle animation frames
If cD = 0 Then cF = 12 Else cF = 5
EndIf
EndIf
End Function
Another mundane function, HandleJump() controls the jumping and gravity action of both the player and client.
Function UpdateUDP()
If bConn = True Then ' Updates
If MilliSecs() >= UpdateTime + 100 ' Send Main Update every 100ms (unreliable)
WriteInt Stream, SendID
WriteByte Stream, NET_STATE
WriteByte Stream, pState
SendUDP
UpdateTime = MilliSecs()
EndIf
If MilliSecs() >= PingTime + 1000 ' Ping once every second (unreliable)
WriteInt Stream, SendID
WriteByte Stream, NET_PING
SendUDP
PingTime = MilliSecs()
EndIf
EndIf
Local bMsgIsNew
If RecvUDPMsg(Stream) Then ' A UDP packet has been received
RecvID = ReadInt(Stream)
Data = ReadByte(Stream)
IP = UDPMsgIP(Stream)
Port = UDPMsgPort(Stream)
bMsgIsNew = IsMsgNew(RecvID, Data) ' Make sure packet is not a duplicate
If bMsgIsNew = True Then
Select Data
Case NET_JOIN ' JOIN PACKET
If bConn = False And bHost = True Then
ClientIP = IP
ClientPort = Port
NetAck RecvID ' Send ACK
bConn = True ' Connected
EndIf
Case NET_PING ' Received Ping, Send Pong
WriteInt Stream, SendID
WriteByte Stream, NET_PONG
SendUDP
Case NET_PONG ' Ping / Pong - Calculate Ping
myPing = (MilliSecs() - pingTime) / 2
Case NET_ACK ' ACK PACKET
RemoveNetObj ReadInt(Stream) ' Remove Reliable Packet (ACK has been received)
Case NET_STATE ' State Update
cState = ReadByte(Stream) ' Set client state
Case NET_UPDATE ' Full update, align
cX = ReadInt(Stream) ' X
cY = ReadInt(Stream) ' Y
cD = ReadByte(Stream) ' Direction
Case NET_QUIT ' Player Quits
bConn = 2 ' Quit
NetAck RecvID ' Send ACK
End Select
EndIf
EndIf
End Function
UpdateUDP() is where the bulk of our networking code lies. The function is divided into two sections, the top two timer checks are for sending, and the bottom section is for receiving. Let's start with the sending section first.Packets Sent
- NET_STATE - This packet is sent every 100 milliseconds, and is considered a partial update, one that only contains player state rather than full position
- NET_PING - A ping request is sent out once every second
The receive section begins with, If RecvUDPMsg(Stream):
Packets Received
- NET_JOIN - Join packet from client, send back a NET_ACK
- NET_PING - Ping packet, respond with a NET_PONG
- NET_PONG - This is when the ping is calculated, based on travel time / 2
- NET_ACK - Acknowledge packets. These are sent to verify that a reliable packet has been received
- NET_STATE - Client has sent a state update
- NET_UPDATE - Client sends update (when idle), contains full position
- NET_QUIT - Client sends reliable QUIT message, answer it with a NET_ACK
Important Values
RecvID - Received packet's SendID
Data - Packet ID
IP - Client IP
Port - Client Port
Functions of Interest: IsMsgNew(RecvID, Data), RemoveNetObj(AckID) (See their descriptions below)

' // IsMsgNew determines whether the current UDP packet is old or a duplicate
Function IsMsgNew(ID, Data = 0)
For i = 30 To 0 Step -1
lastRecv[i + 1] = lastRecv[ i ]
Next
lastRecv[0] = ID
For i = 1 To 31
If lastRecv[ i ] = lastRecv[0] Then ' Duplicate
'Print "ID: " + lastRecv[0]
Select Data ' ACK Anyway (Reliable packets only)
Case NET_JOIN, NET_QUIT
NetAck ID
If DebugMode = True Then Print "*** Received duplicate packet, resending ACK ***"
End Select
Return False
EndIf
Next
Return True
End Function
Here we have the answer to our duplicate packet problems, the IsMsgNew function. Every time a packet is received, this function places its SendID into an array. The LastRecv[] array is later checked for an occurance of the current packet's SendID. If the same ID is found, the packet can be discarded. If the duplicate is a reliable packet, an ACK still needs to be sent in response.
' // RemoveNetObj removes reliable UDP packets from the queue once an ACK has been received
Function RemoveNetObj(AckID)
For n:NetObj = EachIn NetList
If n.SID = AckID Then
tmpID = n.ID
NetList.Remove n
For n:NetObj = EachIn NetList ' Remove old entries
If n.ID = tmpID And n.SID <= AckID Then
NetList.Remove n
Next
Exit
EndIf
Next
End Function
RemoveNetObj() is called when an ACK packet has been received. The AckID contains the SendID of the reliable packet that can be removed.
' // NetAck sends ACKnowledge packet to client
Function NetAck(RecvID)
WriteInt Stream, SendID
WriteByte Stream, NET_ACK
WriteInt Stream, RecvID
SendUDP
End Function
NetAck() sends an acknowledge packet that contains the SendID of the reliable packet that was received.
' // SendUDP sends the current packet to either host or client Function SendUDP() If bHost = True Then ' Send to Client SendUDPMsg Stream, ClientIP, ClientPort Else ' Send to Host SendUDPMsg Stream, IntServerIP, HOSTPORT EndIf SendID:+1 ' Increment send counter End FunctionThe SendUDP function calls the command that actually sends the data packets over the network. bHost is checked to determine where to send the packet, then the SendID public is incremented.

' // NetConnect attempts to connect client to host Function NetConnect() timeOutTime = MilliSecs() NetList.AddLast netObj.Create(1) ' Send Reliable Join Attempt Local tmpConn = 0 ClearLastRecv ' Clear Duplicate Array SetClsColor 0, 0, 0 Repeat DrawText "Connecting...", 5, 5 If RecvUDPMsg(Stream) Then ' A UDP packet has been received RecvID = ReadInt(Stream) Data = ReadByte(Stream) IP = UDPMsgIP(Stream) Port = UDPMsgPort(Stream) bMsgIsNew = True ' ACK is always new If bMsgIsNew = True Then Select Data Case NET_ACK ' ACK PACKET RemoveNetObj ReadInt(Stream) ' Remove Reliable Packet (ACK has been received) tmpConn = 1 ' We have connected! End Select EndIf EndIf For n:NetObj = EachIn netList; n.Handle; Next ' Handle reliable packets If MilliSecs() >= timeOutTime + 2500 Then tmpConn = 2 ' No response received in 2.5 seconds, bail Flip; Cls Until tmpConn <> 0 SetClsColor 92, 148, 252 ' Restore Blue Sky For n:NetObj = EachIn netList; netList.Remove n ; Next ' Clear packets If tmpConn = 1 Then bConn = True Else bConn = False End Function ' // NetDisconnect closes net connections Function NetDisconnect() NetList.AddLast netObj.Create(2) ' Send Reliable Quit Attempt Local tmpConn = 0 SetClsColor 0, 0, 0 Repeat DrawText "Disconnecting...", 5, 5 If RecvUDPMsg(Stream) Then ' A UDP packet has been received RecvID = ReadInt(Stream) Data = ReadByte(Stream) IP = UDPMsgIP(Stream) Port = UDPMsgPort(Stream) bMsgIsNew = True ' ACK is always new If bMsgIsNew = True Then Select Data Case NET_ACK ' ACK PACKET RemoveNetObj ReadInt(Stream) ' Remove Reliable Packet (ACK has been received) tmpConn = 1 ' We have disconnected End Select EndIf EndIf For n:NetObj = EachIn netList; n.Handle; Next ' Handle reliable packets Flip; Cls Until tmpConn <> 0 For n:NetObj = EachIn netList; netList.Remove n ; Next ' Clear packets bConn = False CloseStream Stream Stream = Null End FunctionThese last couple of pertinent functions are what begins and closes the network connection. NetConnect adds a single reliable join packet to our NetList, then loops until a response is received, or the TimeOutTime timer is tripped. This means we will resend the join packet for roughly two seconds before giving up. NetDisconnect does basically the same thing, except that it sends a NET_QUIT packet and waits for a response. As aforementioned, the quit packet is never dropped, so we loop continually until a reply is received.
All of the important functions have been covered! Now, go out and make that MMORPG you always wanted! ;)

+ IV.Junk
What remains are basic functions that handle aspects of the program not directly related to networking. The commenting in each function should explain its purpose.
Function ClearLastRecv() For i = 0 To 31 lastRecv[i] = -1 ' Clear duplicate array Next End Function ' // Draws text at the top of the screen Function RenderHUD() If bHost = False Then ' Client If bConn = False ' Not connected DrawText "Press 'C' to connect to " + ServerIP$, 5, 5 Else If bConn = True Then DrawText "Ping: " + myPing + "ms", 5, 5 Else DrawText "Player disconnected", 5, 5 EndIf EndIf Else If bConn = False ' Not connected DrawText "Waiting for Luigi to join...", 5, 5 Else If bConn = True Then DrawText "Ping: " + myPing + "ms", 5, 5 Else DrawText "Player disconnected", 5, 5 EndIf EndIf EndIf End Function ' // WindowMode function centers our window Function WindowMode() tmpClass$ = "BBDX7Device Window Class" Local hWnd% = FindWindow(tmpClass$, "Multiplayer Test") Local desk_hWnd% = GetDesktopWindow(), l:lpRect = New lpRECT GetWindowRect desk_hWnd, l:lpRECT SetWindowPos hWnd, -2, (l.r / 2) - (640 / 2), (l.b / 2) - (480 / 2), 0, 0, 1 l:lpRECT = Null End Function
