https://coronalabs.com/blog/2014/09/23/tutorial-local-multiplayer-with-udptcp/
Tutorial: Local multiplayer with UDP/TC
- For turn-based RPG incorporating local multiplayer game
Make devices find each other with UDP
- local multiplayer
- cross-platform.
- for both turn-based and action games
Prerequisites
- Corona SDK, Lua, and peer-to-peer networking.
- use LuaSocket which is included in Corona SDK under the socket library.
- assume that the player has a LAN, that network doesn’t need to be connected to the internet.
Including LuaSocket
```
local socket = require( "socket" )
```
UDP and TCP
- a brief explanation of UDP and TCP.
- protocols which allow computers to talk to each other
UDP
- the ability to send messages to an address without knowing if anything is there.
- It doesn’t check to see if the message made it anywhere — it simply transmits the message.
- UDP also allows you to listen to an address without being connected to a computer at that address.
TCP
- a reliable protocol.
- It sends messages to another computer that it’s connected to via a TCP socket.
- If the other computer responds that there was a problem with the message, it sends the message again.
Knowing this, you might ask “Why would anyone use UDP when TCP is so reliable?”
Well, there are several reasons, but the reason most relevant to this tutorial is the fact that you must know the IP address of the server in order to use TCP. Any network which you can join without visiting the LAN administrator is assigning you a random IP address using DHCP. This means that we’ll need to discover the IP address of the server on our own. Fortunately, UDP can help with that.
Advertise the server
For local multiplayer to work, one of the devices must act as the server/host. The host doesn’t need to have special privileges in your game, but the primary record of the game in progress will be stored on this device. In the language of role-playing games, let’s call this player the game master. This game master is going to announce his/her presence to the local network via some method. I utilize an “Invite” button which calls the following function upon being pressed.
```
local advertiseServer = function( button )
local send = socket.udp()
send:settimeout( 0 ) --this is important (see notes below)
local stop
local counter = 0 --using this, we can advertise our IP address for a limited time
local function broadcast()
local msg = "AwesomeGameServer"
--multicast IP range from 224.0.0.0 to 239.255.255.255
send:sendto( msg, "226.192.1.1", 11111 )
--not all devices can multicast so it's a good idea to broadcast too
--however, for broadcast to work, the network has to allow it
send:setoption( "broadcast", true ) --turn on broadcast
send:sendto( msg, "255.255.255.255", 11111 )
send:setoption( "broadcast", false ) --turn off broadcast
counter = counter + 1
if ( counter == 80 ) then --stop after 8 seconds
stop()
end
end
--pulse 10 times per second
local serverBroadcast = timer.performWithDelay( 100, broadcast, 0 )
button.stopLooking = function()
timer.cancel( serverBroadcast ) --cancel timer
button.stopLooking = nil
end
stop = button.stopLooking
end
```
Here are some notes about this code:
- Multicast is a device feature which allows one device to communicate with several. iPhones and iPads have it, but I’ve been told that iPods do not. I haven’t tried it on any Android devices, so maybe somebody can test it and report their results in the comments section. As a result of this inconsistency, we also use broadcast. “Why don’t we just use broadcast?” you might ask. Well, the catch with broadcast is that the LAN has to allow broadcasts. By using both, we are maximizing the chance of finding each other.
- The “pulse” of the timer is ten times per second. I don’t recommend setting your timer pulse faster than that unless you have a good reason — after all, your game needs time to do other things. This is a standard pulse speed for most action games including MMOs.
- The port you choose can be anything between 1 and 65535, however, applications almost always block the port that they use and you’ll get an error if you try to bind to a port that is currently in use. Likewise, if you bind to a port, you need to unbind/close the port when you end the game so you don’t block it indefinitely on the players device. Lower number ports are used by commonly run applications, so it’s best to use a port between 1024 and 65535.
- The settimeout() function allows you to tell the socket how long to wait for a message before moving on. Default is to wait indefinitely, meaning that your game freezes until it gets a message. Setting it to 0 tells it to just check and if there’s nothing to receive and move on to the next task.
Finding the server
The client will need to know its own IP address for the next step. Fortunately, UDP in LuaSocket can help with that:
```
-- Get my IP address
local getIP = function()
local s = socket.udp() --creates a UDP object
s:setpeername( "www.google.com", 80 ) --Google website
local ip, sock = s:getsockname()
print( "myIP:", ip, sock )
return ip
end
```
The IP address in the above function is arbitrary — I used the Google address because I know it. You don’t even need to be connected to the internet for this function to return your IP address, but you must at least be on a local network.
Listening for the server
Now we are prepared to listen for the server. We will recognize the server because of the message AwesomeGameServer. Obviously, this could be any string; we are just going to match strings.
```
local function findServer( button )
local newServers = {}
local msg = "AwesomeGameServer"
local listen = socket.udp()
listen:setsockname( "226.192.1.1", 11111 ) --this only works if the device supports multicast
local name = listen:getsockname()
if ( name ) then --test to see if device supports multicast
listen:setoption( "ip-add-membership", { multiaddr="226.192.1.1", interface = getIP() } )
else --the device doesn't support multicast so we'll listen for broadcast
listen:close() --first we close the old socket; this is important
listen = socket.udp() --make a new socket
listen:setsockname( getIP(), 11111 ) --set the socket name to the real IP address
end
listen:settimeout( 0 ) --move along if there is nothing to hear
local stop
local counter = 0 --pulse counter
local function look()
repeat
local data, ip, port = listen:receivefrom()
--print( "data: ", data, "IP: ", ip, "port: ", port )
if data and data == msg then
if not newServers[ip] then
print( "I hear a server:", ip, port )
local params = { ["ip"]=ip, ["port"]=22222 }
newServers[ip] = params
end
end
until not data
counter = counter + 1
if counter == 20 then --stop after 2 seconds
stop()
end
end
--pulse 10 times per second
local beginLooking = timer.performWithDelay( 100, look, 0 )
function stop()
timer.cancel( beginLooking )
button.stopLooking = nil
evaluateServerList( newServers ) --do something with your found servers
listen:close() --never forget to close the socket!
end
button.stopLooking = stopLooking
end
```
I put a lot of inline comments above, but I’ll elaborate on a few things:
- Notice that we account for the fact that not all devices have Multicast.
- The receivefrom() function is going to just pull in anything that’s at that address, so we need to filter it. This is why we have the string message to compare with.
- When two devices find each other, it can get painful if they both have a short duration. I like to make the server wait much longer than the clients. If the server is advertising, the client finds them pretty quick. Basically, I just want to avoid “Can you try that again? I missed it.”
- In this example, I’m passing in a reference to the button that the player pressed to activate the function. I do this because so the player can push it again and stop broadcasting. If you don’t want to do that, you don’t need the button reference.
So, at this point, we know how to let the game master be discovered by the players. The essential IP address required to use TCP is attached to the UDP message. Now that we have the game master’s IP address, we can connect to their device using TCP.
Swapping strings
Now we’ll discuss how to create a TCP server, connect to it, and send messages back and forth.
First, let’s discuss what TCP will provide and what it won’t. Like I said above, once we have a connection between devices, they’ll be able to send messages back and forth. These messages will just be strings. Imagine it like a text message app — in this case, the app on one device sends texts to the app on another device. These messages are then interpreted by the apps on each device and some action occurs.
Security
This tutorial will not go in depth about security, but a couple points should be covered:
- The server and client can only control each other as far as you allow it. As I’ve iterated several times now, TCP just sends and receives text strings. For a Pac-Man clone that could be controlled by a second device, about the only information the server would need is “BEGIN”, “UP”, “DOWN”, “LEFT”, and “RIGHT” — all else could simply be ignored.
- You should never try to make your app accept functions that have been turned into a string. Let the client and the server have their own functions and just use the transmitted text to call the functions. If your app accepts functions, you open up a very serious security vulnerability, so don’t do it! Instead, just pass commands with parameters.
In any case, don’t lie awake at night worrying about this. Neither iOS nor Android will let you damage someone’s device with such foolishness, but it may ruin the install of your game!
Starting the server
The server runs in a periodic loop. On each iteration of the loop, it checks to see if any clients want to join and whether connected clients sent a message. If the buffer has any messages to send out, it sends them. Here’s a basic TCP server module with further explanation following:
```
local S = {}
local socket = require( "socket" )
local clientList = {}
local clientBuffer = {}
S.getIP = function()
local s = socket.udp()
s:setpeername( "74.125.115.104", 80 )
local ip, sock = s:getsockname()
print( "myIP:", ip, sock )
return ip
end
S.createServer = function()
local tcp, err = socket.bind( S.getIP(), 22222 ) --create a server object
tcp:settimeout( 0 )
local function sPulse()
repeat
local client = tcp:accept() --allow a new client to connect
if client then
print( "found client" )
client:settimeout( 0 ) --just check the socket and keep going
--TO DO: implement a way to check to see if the client has connected previously
--consider assigning the client a session ID and use it on reconnect.
clientList[#clientList+1] = client
clientBuffer[client] = { "hello_client\n" } --just including something to send below
end
until not client
local ready, writeReady, err = socket.select( clientList, clientList, 0 )
if err == nil then
for i = 1, #ready do --list of clients who are available
local client = ready[i]
local allData = {} --this holds all lines from a given client
repeat
local data, err = client:receive() --get a line of data from the client, if any
if data then
allData[#allData+1] = data
end
until not data
if ( #allData > 0 ) then --figure out what the client said to the server
for i, thisData in ipairs( allData ) do
print( "thisData: ", thisData )
--do stuff with data
end
end
end
for sock, buffer in pairs( clientBuffer ) do
for _, msg in pairs( buffer ) do --might be empty
local data, err = sock:send( msg ) --send the message to the client
end
end
end
end
--pulse 10 times per second
local serverPulse = timer.performWithDelay( 100, sPulse, 0 )
local function stopServer()
timer.cancel( serverPulse ) --cancel timer
tcp:close()
for i, v in pairs( clientList ) do
v:close()
end
end
return stopServer
end
return S
```
And that is a basic server. Let’s start at the top with some explanation:
- socket.bind() creates a server object which you bind to the port of your choice. I used 11111, but you can use any that we listed in the earlier section. Remember to close the TCP object when you shut down the server via the stopServer() function!
- settimeout( 0 ) tells LuaSocket to move on if there’s no information waiting at the socket.
- accept() returns a client object which represents the connection to the other device. Each client will get their own object and each one will need to be closed when the game is done. We do this in the function at the bottom called stopServer().
- socket.select() goes through our list of client connections to see which are available. Any that are not available are ignored but not closed.
- receive() receives one line of data. You can designate a line of data in a string by putting \n at the end. It’s simple and you’ll be able to create bite-sized pieces of data. This function is structured so that you end up with a numbered table of string lines. They are numbered in the order that they were received, but you can’t rely on the lines you send being received in the order you sent them. If this is important, and it often is, you’ll need to create a way for the server to know if a line is in the right order.
- Next we go through the list of lines and interpret them. This is usually just a series of if–then statements with a liberal use of the string library.
- Finally, we send whatever is in the buffer. The buffer is another list of strings. Again, you can’t absolutely control the order in which they are received. You don’t have to use a buffer, but when you are using a multi-use device like a phone as a server, it’s a good idea. You may just :send() to a client socket at any time but the only way the device knows that the message didn’t go through is if the other device responds. If the other device is taking a call, it will ignore your message and the message will be lost. If you implement a buffer, it sends the message every pulse until something happens that removes the message from the buffer, however you’ll need to implement a way of knowing when to remove items from the buffer.
Connecting to the server
Connecting to the server is much simpler:
```
local function connectToServer( ip, port )
local sock, err = socket.connect( ip, port )
if sock == nil then
return false
end
sock:settimeout( 0 )
sock:setoption( "tcp-nodelay", true ) --disable Nagle's algorithm
sock:send( "we are connected\n" )
return sock
end
```
To elaborate on this slightly:
- socket.connect is pretty self explanatory: attempt to connect to the server at that address.
- settimeout( 0 ) again lets the socket know that you want it to just check the socket and move on if there’s no incoming message.
- Nagle’s algorithm is a standard function that causes the socket to aggregate data until the data is of a certain size, then send it. If you are just going to send “UP” and you want it sent right away, you’ll want this off.
What’s not included in this example is a method to determine if the client is connecting for the first time or reconnecting (return session). This is outside the scope of this tutorial, but one option is to use a session ID which the client gets the first time it connects to the server. In this case, both the client and the server save the ID. Then, if the client loses the connection, this ID is sent upon reconnection and the server can update the client’s data with the new client socket.
Client loop
The final piece of the puzzle is the client loop. This will look very much like the server loop, but it never tries to accept connections.
```
local function createClientLoop( sock, ip, port )
local buffer = {}
local clientPulse
local function cPulse()
local allData = {}
local data, err
repeat
data, err = sock:receive()
if data then
allData[#allData+1] = data
end
if ( err == "closed" and clientPulse ) then --try again if connection closed
connectToServer( ip, port )
data, err = sock:receive()
if data then
allData[#allData+1] = data
end
end
until not data
if ( #allData > 0 ) then
for i, thisData in ipairs( allData ) do
print( "thisData: ", thisData )
--react to incoming data
end
end
for i, msg in pairs( buffer ) do
local data, err = sock:send(msg)
if ( err == "closed" and clientPulse ) then --try to reconnect and resend
connectToServer( ip, port )
data, err = sock:send( msg )
end
end
end
--pulse 10 times per second
clientPulse = timer.performWithDelay( 100, cPulse, 0 )
local function stopClient()
timer.cancel( clientPulse ) --cancel timer
clientPulse = nil
sock:close()
end
return stopClient
end
```
Note that the client is always responsible for making the connection to the server. The server never tries to reach the client — it has enough to handle already. Beyond that, there isn’t anything clarify that wasn’t already covered in the server loop section.
Additional Information
- As for demonstrating this code, just take all the code above and put it in a “main.lua” file. Then make two buttons, one to call the function to advertise the server and one to call the function to look for the server. Set up the server loop to kick off when you start the app. Now load the app on a device and run it. Also run it in the emulator. Push the server button in the emulator and the client button on the device (or vis versa) and you will see print messages in the console of each that will confirm it is working. This is assuming that both the emulator and the device are on the same network.
- I had to use the following code to get the local broadcast working. It basically replaces the last octet of your IP address with 255. A more correct solution would probably be to use the correct broadcast address based on the subnet mask but I was unsure of how to do this in CoronaSDK and I believe that this will work on most home networks (hopefully other networks support multicast or 255.255.255.255 broadcast).-- replace last octet of our ip address with '255'
function get_local_broadcast_address()
local ip = getIP()
local index = findLast(ip, "%.")
local substr = string.sub(ip,1,index) .. "255"
return substr
end - I have to say it needs a few changes for working correctly on android devices :
I changed your client listener to 0.0.0.0
——————————————
Client:
———————————————————
listen = socket.udp() –make a new socket
listen:setsockname( “0.0.0.0”, 1119 ) –set the socket name to the real IP address
———————————————————
Server
————————————————-
print(“Advertise server”)
assert(send:setoption( “broadcast”, true )) –turn on broadcast
assert(send:sendto( msg, “255.255.255.255”, 1119 ))
assert(send:setoption( “broadcast”, false )) –turn off broadcast
'tech > game' 카테고리의 다른 글
21/07/15 Godot Engine 무작정 따라하며 배우기 (0) | 2021.07.15 |
---|---|
21/07/15 Lua Quick Start Guide (0) | 2021.07.15 |
21/07/15 Awesome-Lua (0) | 2021.07.15 |
21/07/15 Awesome-corona (solar2d) (0) | 2021.07.15 |
21/07/15 Awesome-love2d Lists (0) | 2021.07.15 |