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 WindowModeIn 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 ' QuitHere 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 FunctionThis 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 TypeThe 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 UpFirst 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 ArrayThis 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 EndMain 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 FunctionThe 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 FunctionThe 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 FunctionAnother 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 FunctionUpdateUDP() 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 FunctionHere 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 FunctionRemoveNetObj() 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 FunctionNetAck() 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