socket = require("socket") -- bind to all local interfaces local udp = socket.udp() udp:settimeout(0) udp:setsockname("*", 14285) local clients = {} function love.update() while true do -- receive local data, ip, port = udp:receivefrom() if data == nil then break end -- manage clients local uid = ip..":"..port if not clients[uid] then clients[uid] = { ip = ip, port = port } end -- re-broadcast for _, c in pairs(clients) do udp:sendto(uid..":"..data, c.ip, c.port) end end end function love.draw() love.graphics.print("Clients:", 0, 0) local i = 0 for uid in pairs(clients) do i = i + 1 love.graphics.print(uid, 0, i*16) end end
Since the server could be communicating with several clients, it's important to note the "sendto" function. It is similar to "send" except that "sendto" requires the explicit IP address and port of the receiving client.
local socket = require("socket") -- connect to server local udp = socket.udp() udp:settimeout(0) udp:setpeername("localhost", 14285) -- send messages local input = {} function love.textinput(text) table.insert(input, text) end function love.keypressed(key) if key == "backspace" then table.remove(input) elseif key == "return" then udp:send(table.concat(input)) input = {} end end -- receive messages local history = {} function love.update(dt) repeat local data = udp:receive() if data then table.insert(history, 1, data) end until data end function love.draw() love.graphics.print(table.concat(input), 0, 0) love.graphics.print(table.concat(history, "\n"), 0, 16) end
If we want to run this script over the Internet, things get more complicated. Most home routers will usually reassign the ports in order to share one Internet connection among several devices. Consequently, the addresses and ports that we are working with are only valid within our home network.
local connected = false local socket = require("socket") local ip = "127.0.0.1" local port = 14285 -- host or join locally local udp = socket.udp() udp:settimeout(0) udp:setsockname(ip, port) if not udp:getsockname() then -- join udp:setpeername(ip, port) udp:send("join") connected = true endTo run this script on your local area network, you may need to find your local IP address. Luckily, your local network IP address can be found using "socket.dns.toip":
local ip = socket.dns.toip("localhost")If you have trouble establishing a connection, it's good to check which ports are currently "open" on your machine. On Windows, this is done using the NETSTAT utility:
netstat -a -nRunning the script over a public network like the Internet is considerably more difficult. Many people today access the internet using routers where one public IP address is often shared between multiple devices. Each device connected to your home router is assigned its own private IP address that is not accessible publicly. In short, not every device using the Internet has its own public IP address. Routers perform network address translation (NAT) to connect devices on your home network with the outside world.
Before our P2P game can begin, the first peer needs to receive that "join" message. This message also contains the IP and port of the second peer which up to that point are unbeknown. Once both peers have connected via "setpeername" we are ready to start!
function love.update(dt) if not connected then -- wait for somebody to join repeat local data, ip, port = udp:receivefrom() if data == "join" then udp:setpeername(ip, port) connected = true break end until not data else
132843 0010The first number contained in this messages is the tick counter which serves as a time stamp. The second number is the state of our keyboard (we only need to track four buttons). In fact, we can exclude certain combinations like the up and down keys being pressed at the same time. The more observant readers may notice that these messages could be packed into binary for improved performance.
local keys = { "up", "down", "left", "right" } ... -- store and send local input local state = {} for i, key in ipairs(keys) do state[i] = love.keyboard.isDown(key) and 1 or 0 end local msg = table.concat(state) udp:send(tick.." "..msg) player[tick] = state
Any messages arriving over the network are also parsed and stored. This way, we have the complete input history of each player.
repeat local data = udp:receive() if data then local stamp, state = data:match("(%d+)%s(.+)") stamp = tonumber(stamp) -- track the last received message last = math.max(last, stamp) -- parse and store peer input opponent[stamp] = { state:match("(%d)(%d)(%d)(%d)") } end until not data
repeat local data = udp:receive() if data then ... parse and store peer input end until not data accum = accum + dt while accum > int do accum = accum - int tick = tick + 1 ... store and send local input end for i = sync, last do if not player[i] or not opponent[i] then break end sync = i for _, agent in pairs(agents) do local state = agent[i] assert(state, i) local vx, vy = 0, 0 if state[1] == "1" then vy = -25 elseif state[2] == "1" then vy = 25 end if state[3] == "1" then vx = -25 elseif state[4] == "1" then vx = 25 end agent.x = agent.x + vx*int agent.y = agent.y + vy*int end end
Before reacting to any input, we have to make sure that everyone is on the same page. Both peers must be running using a fixed time step. This is achieved using an accumulator and a constant update interval. Messages are indexed based on their corresponding time stamp so the order of their arrival is irrelevant. Basically, the simulation moves forward depending on the buffered input.