Local Multiplayer Game

@dave1707, could you please test this with 3 iPads and just see if you can see two spheres moving around on the first iPad. Here’s the code:

function setup()
    socket = require("socket")
    parameter.action("Create Room", createRoom)
    parameter.action("Join Room", function()
        if other_ip then
            joinRoom(other_ip, false)
        else
            parameter.text("IP", "", function(t)
                other_ip = t
            end)
        end
    end)
    parameter.action("Find Room", function()
        findRoom()
    end)
    client = socket.udp()
    client:settimeout(0)
    listener = socket.udp()
    listener:settimeout(0)
    -- We need to create a table that can hold information about our server, if you end up making one
    server_info = {players = 0, clients = {}, world_state = {}, puppet_states = {}}
    -- We also need to create a table that can hold info that the client has
    client_info = {serverIp = "", world_state = {}}
    other_ip = nil
    current_time = nil
    current_room_count = 1
    tick = 0
    finding_room = false
    server = false
    joinedRoom = false
    myPlayerServer = nil
    myPlayerClient = nil
    playerIdx = 0
    playerTick = 0
end

function createRoom()
    -- We need to find your address, so we create a new UDP socket and assign it a random IP
    local ip, port = getLocalIP()
    client:setsockname("*", "14285")
    local ip2, port2 = client:getsockname()
    client:setoption("broadcast", true)
    your_ip = ip2
    your_port = port2
    print("Room created!")
    print("Connect to "..ip)
    server = true
end

function joinRoom(ip, finding_room)
    if server == false then
        if ip ~= "*" then
            if finding_room == false then
                print("Attempting to join the room...")
            end
            -- The port for all the servers is the same, so we only need the IP address of the server
            client:setpeername(ip, "14285")
            client:send("connection confirmation")
            current_time = os.time()
        else
            print("Address not allowed!")
        end
    else
        print("You are already hosting a server!")
    end
end

function findRoom()
    if server == false then
        print("Attempting to find a room...")
        finding_room = true
        local ip, port = getLocalIP()
        local ip1, ip2 = string.match(ip, "(%d+.%d+.%d+.)(%d+)")
        local success, err = pcall(function()
            for z = 1, 255 do
                client:setpeername(ip1..z, "14285")
                client:send("requesting server info")
            end
            client:setpeername("*")
            client:setsockname("*", "14285")
        end)
        if success == false then
            -- The only error you could possibly get in this case is no internet connection, so we print it
            print("No internet connection, please try again later!")
            return
        end
        current_time = os.time()
    else
        print("You are already hosting a server!")
    end
end

function sendMessage(msg)
    if server == true then
        for i,c in pairs(server_info["clients"]) do
            client:sendto(tostring(msg), c.ip, c.port)
        end
    end
end

function leaveRoom()
    -- We need to notify the server that we are leaving, so we send a packet to them
    if server == false then
        client:send("disconnection confirmation")
    end
end

function resetParameters(params, args)
    parameter.clear()
    for i = 1, #params do
        parameter.action(params[i], args[i])
    end
end

function getLocalIP()
    local udp = socket.udp()
    udp:setpeername("192.167.188.122", "14285")
    local ip, port = udp:getsockname()
    udp:close()
    return ip, port
end

function receiveData()
    if server == true then
        local msg, ip, port = client:receivefrom()
        if msg ~= nil then
            if msg == "connection confirmation" then
                if server_info["players"] < 32 then
                    -- We can now add 1 more to our player count
                    server_info["players"] = server_info["players"] + 1
                    -- We now need to store the clients IP and port, so we can send them messages later
                    local uid = {ip = ip, port = port}
                    if not server_info["clients"][uid] then
                        table.insert(server_info["clients"], uid)
                    end
                    client:sendto("valid confirmation", ip, port)
                    -- We need to send the joining client our IP, so they can send messages to us later
                    local our_ip, our_port = getLocalIP()
                    client:sendto("server ip"..our_ip, ip, port)
                    -- We can now send everyone that has joined this room a packet telling them the amount of players in the room
                    for i,c in pairs(server_info["clients"]) do
                        client:sendto("player count"..tostring(server_info["players"]), c.ip, c.port)
                    end
                else
                    -- We don't have enough room for another player, so we send the peer a message telling them to disconnect
                    local reason = "Max players inside the room!"
                    client:sendto("invalid confirmation"..reason, ip, port)
                end
            elseif msg == "requesting server info" then
                -- We need to find your address, so we create a new UDP socket and assign it a random IP
                local our_ip, our_port = getLocalIP()
                -- We now know that somebody is trying to find a server, so we send a packet back to them with our server IP and port
                local uid = {ip = our_ip}
                client:sendto("server info"..json.encode(uid), ip, port)
            elseif msg and string.find(msg, "disconnection confirmation") then
                -- We know that one of the clients connected to this server wants to leave, so we send a packet back telling them that we have been notified
                client:sendto("notified of disconnection", ip, port)
                local uid = {ip = ip, port = port}
                local clientInfo
                local clientState
                for i = 1, #server_info["clients"] do
                    if server_info["clients"][i] == uid then
                        clientInfo = i
                    end
                end
                table.remove(server_info["clients"], clientInfo)
                -- We also need to deduct the player count
                server_info["players"] = server_info["players"] - 1
            elseif msg and string.find(msg, "client state") then
                -- A connected peer has sent their client state to us, so we update our world state accordingly
                local clientState = json.decode(string.sub(msg, 13, #msg))
                local hasClientState = false
                local existingState = nil
                for i,s in pairs(server_info["world_state"]) do
                    if s.playerName == clientState.playerName then
                        hasClientState = true
                        existingState = s
                    end
                end
                if hasClientState == true then
                    if clientState.timestamp > existingState.timestamp then
                        existingState.playerX = clientState.playerX
                        existingState.playerY = clientState.playerY
                    end
                else
                    if not server_info["world_state"][clientState] then
                        table.insert(server_info["world_state"], clientState)
                    end
                end
            end
        end
    else
        if finding_room == false then
            local result = client:receive()
            if result == "valid confirmation" then
                -- We know that we successfully joined the room, and we can now receive messages from the server!
                print("Successfully joined the room!")
                joinedRoom = true
                current_time = nil
                parameter.clear()
                -- We now create a button that allows the player to leave the room
                local paramName = {"Leave Room"}
                local paramArgs = {function() leaveRoom() end}
                resetParameters(paramName, paramArgs)
                local playerTab = {playerName = "client", playerX = WIDTH/2, playerY = HEIGHT/2, timestamp = playerTick}
                myPlayerClient = playerTab
            elseif result and string.find(result, "server ip") then
                -- Now that we have the servers IP address, we can message them later if we need to
                local serverIp = string.sub(result, 10, #result)
                client_info["serverIp"] = serverIp
            elseif result and string.find(result, "player count") then
                -- We now know the current amount of players in our room, for now at least
                local count = string.sub(result, 13, #result)
                print("Player count: "..count)
            elseif result and string.find(result, "invalid confirmation") then
                -- We can now see the reason for our invalid confirmation and display it
                local reason = string.sub(result, 21, #result)
                print("Could not join the room: "..reason)
                current_time = nil
                -- We can now also remove the address and port set for our peer
                client:setpeername("*")
            elseif result == "notified of disconnection" then
                -- We know that we can now safely leave the server
                client:setpeername("*")
                joinedRoom = false
                other_ip = nil
                print("Successfully left the room!")
                local paramNames = {"Create Room", "Join Room", "Find Room"}
                local paramArgs = {createRoom, function()
                        if other_ip then
                            joinRoom(other_ip, false)
                        else
                            parameter.text("IP", "", function(t)
                                other_ip = t
                            end)
                        end
                    end, findRoom}
                resetParameters(paramNames, paramArgs)
            elseif result == nil then
                if current_time then
                    if finding_room == false then
                        if os.time() > current_time + 5 then
                            -- If we get no response back for 5 seconds or more, we know it didn't work
                            print("Failed to connect, please try again later")
                            current_time = nil
                        end
                    end
                end
            end
        else
            local result, ip, port = client:receivefrom()
            if result and string.find(result, "server info") then
                -- Now we can attempt to join the room with the given IP address
                local tab = json.decode(string.sub(result, 12, #result))
                if finding_room == true then
                    joinRoom(tab.ip, true)
                    finding_room = false
                end
            else
                if finding_room == true then
                    if current_time and os.time() > current_time + 15 then
                        -- If we get no response back for 15 seconds or more, we know we couldn't find a room
                        print("Failed to find a room!")
                        current_time = nil
                    end
                end
            end
        end
    end 
end

function updatePlayers()
    -- We now update the position of our player puppets, so it looks like the players are being updated in realtime
    for i,p in pairs(server_info["world_state"]) do
        ellipse(p.playerX, p.playerY, 35)
    end
end

function touched(touch)
    if touch.state == BEGAN or touch.state == MOVING then
        -- Here we send a packet to the server every frame, telling them our player position so it can properly update the whole game
        if joinedRoom == true then
            if myPlayerClient then
                myPlayerClient.playerX = CurrentTouch.x
                myPlayerClient.playerY = CurrentTouch.y
                local playerTab = {playerName = myPlayerClient.playerName, playerX = myPlayerClient.playerX, playerY = myPlayerClient.playerY, timestamp = playerTick}
                player:send("client state"..json.encode(playerTab))
            end
        end
    end
end

-- This function gets called once every frame
function draw()
    -- This sets a dark background color 
    background(40, 40, 50)
    
    -- This sets the line thickness
    strokeWidth(5)
    
    -- Do your drawing here
    updatePlayers()
    receiveData()
    playerTick = playerTick + 1
end

Getting an error on
player:setpeername(ip, “14285”)

@dave1707, I fixed the code. Try it now.

2 circles eventually showed up on iPad 1, but they were flickering. When I moved my finger on iPad 2 and iPad 3, there was a long delay before they moved on iPad 1. They eventually wouldn’t move after awhile.

@dave1707, I updated the code again so that you only send your position to the server when you touch the screen.

Created a room on iPad 1. Created player 1 on iPad 2 and when I moved my finger around the screen, the circle on iPad 1 moved without a delay. I created player 2 on iPad 3 and it moved the player 1 circle. There was only 1 circle on iPad 1 even though there were 2 players.

@dave1707, I know why that’s happening and it’s a very simple fix. You see, when I create the players I gave them the same name. By giving them different names, hopefully everything should work well.

@dave1707, hopefully this fixes the problem. Here’s the code:

function setup()
    socket = require("socket")
    parameter.action("Create Room", createRoom)
    parameter.action("Join Room", function()
        if other_ip then
            joinRoom(other_ip, false)
        else
            parameter.text("IP", "", function(t)
                other_ip = t
            end)
        end
    end)
    parameter.action("Find Room", function()
        findRoom()
    end)
    client = socket.udp()
    client:settimeout(0)
    listener = socket.udp()
    listener:settimeout(0)
    -- We need to create a table that can hold information about our server, if you end up making one
    server_info = {players = 0, clients = {}, world_state = {}, ids_taken = {}}
    -- We also need to create a table that can hold info that the client has
    client_info = {serverIp = "", world_state = {}}
    other_ip = nil
    current_time = nil
    current_room_count = 1
    tick = 0
    finding_room = false
    server = false
    joinedRoom = false
    myPlayerServer = nil
    myPlayerClient = nil
    playerIdx = 0
    playerTick = 0
end

function createRoom()
    -- We need to find your address, so we create a new UDP socket and assign it a random IP
    local ip, port = getLocalIP()
    client:setsockname("*", "14285")
    local ip2, port2 = client:getsockname()
    client:setoption("broadcast", true)
    your_ip = ip2
    your_port = port2
    print("Room created!")
    print("Connect to "..ip)
    server = true
end

function joinRoom(ip, finding_room)
    if server == false then
        if ip ~= "*" then
            if finding_room == false then
                print("Attempting to join the room...")
            end
            -- The port for all the servers is the same, so we only need the IP address of the server
            client:setpeername(ip, "14285")
            client:send("connection confirmation")
            current_time = os.time()
        else
            print("Address not allowed!")
        end
    else
        print("You are already hosting a server!")
    end
end

function findRoom()
    if server == false then
        print("Attempting to find a room...")
        finding_room = true
        local ip, port = getLocalIP()
        local ip1, ip2 = string.match(ip, "(%d+.%d+.%d+.)(%d+)")
        local success, err = pcall(function()
            for z = 1, 255 do
                client:setpeername(ip1..z, "14285")
                client:send("requesting server info")
            end
            client:setpeername("*")
            client:setsockname("*", "14285")
        end)
        if success == false then
            -- The only error you could possibly get in this case is no internet connection, so we print it
            print("No internet connection, please try again later!")
            return
        end
        current_time = os.time()
    else
        print("You are already hosting a server!")
    end
end

function sendMessage(msg)
    if server == true then
        for i,c in pairs(server_info["clients"]) do
            client:sendto(tostring(msg), c.ip, c.port)
        end
    end
end

function leaveRoom()
    -- We need to notify the server that we are leaving, so we send a packet to them
    if server == false then
        client:send("disconnection confirmation")
    end
end

function resetParameters(params, args)
    parameter.clear()
    for i = 1, #params do
        parameter.action(params[i], args[i])
    end
end

function getLocalIP()
    local udp = socket.udp()
    udp:setpeername("192.167.188.122", "14285")
    local ip, port = udp:getsockname()
    udp:close()
    return ip, port
end

function receiveData()
    if server == true then
        local msg, ip, port = client:receivefrom()
        if msg ~= nil then
            if msg == "connection confirmation" then
                if server_info["players"] < 32 then
                    -- We can now add 1 more to our player count
                    server_info["players"] = server_info["players"] + 1
                    -- We now need to store the clients IP and port, so we can send them messages later
                    local uid = {ip = ip, port = port}
                    if not server_info["clients"][uid] then
                        table.insert(server_info["clients"], uid)
                    end
                    -- We now create a random ID for the connecting peer so we can differentiate between players on the server
                    local playerID = math.random(1, 100000)
                    if not server_info["ids_taken"][playerID] then
                        server_info["ids_taken"][playerID] = playerID
                    else
                        playerID = math.random(1, 100000)
                    end
                    client:sendto("valid confirmation"..playerID, ip, port)
                    -- We need to send the joining client our IP, so they can send messages to us later
                    local our_ip, our_port = getLocalIP()
                    client:sendto("server ip"..our_ip, ip, port)
                    -- We can now send everyone that has joined this room a packet telling them the amount of players in the room
                    for i,c in pairs(server_info["clients"]) do
                        client:sendto("player count"..tostring(server_info["players"]), c.ip, c.port)
                    end
                else
                    -- We don't have enough room for another player, so we send the peer a message telling them to disconnect
                    local reason = "Max players inside the room!"
                    client:sendto("invalid confirmation"..reason, ip, port)
                end
            elseif msg == "requesting server info" then
                -- We need to find your address, so we create a new UDP socket and assign it a random IP
                local our_ip, our_port = getLocalIP()
                -- We now know that somebody is trying to find a server, so we send a packet back to them with our server IP and port
                local uid = {ip = our_ip}
                client:sendto("server info"..json.encode(uid), ip, port)
            elseif msg and string.find(msg, "disconnection confirmation") then
                -- We know that one of the clients connected to this server wants to leave, so we send a packet back telling them that we have been notified
                client:sendto("notified of disconnection", ip, port)
                local uid = {ip = ip, port = port}
                local clientInfo
                local clientState
                for i = 1, #server_info["clients"] do
                    if server_info["clients"][i] == uid then
                        clientInfo = i
                    end
                end
                table.remove(server_info["clients"], clientInfo)
                -- We also need to deduct the player count
                server_info["players"] = server_info["players"] - 1
            elseif msg and string.find(msg, "client state") then
                -- A connected peer has sent their client state to us, so we update our world state accordingly
                local clientState = json.decode(string.sub(msg, 13, #msg))
                local hasClientState = false
                local existingState = nil
                for i,s in pairs(server_info["world_state"]) do
                    if s.playerName == clientState.playerName then
                        hasClientState = true
                        existingState = s
                    end
                end
                if hasClientState == true then
                    if clientState.timestamp > existingState.timestamp then
                        existingState.playerX = clientState.playerX
                        existingState.playerY = clientState.playerY
                    end
                else
                    if not server_info["world_state"][clientState] then
                        table.insert(server_info["world_state"], clientState)
                    end
                end
            end
        end
    else
        if finding_room == false then
            local result = client:receive()
            if result and string.find(result, "valid confirmation") then
                -- We know that we successfully joined the room, and we can now receive messages from the server!
                print("Successfully joined the room!")
                joinedRoom = true
                current_time = nil
                parameter.clear()
                -- We now create a button that allows the player to leave the room
                local paramName = {"Leave Room"}
                local paramArgs = {function() leaveRoom() end}
                resetParameters(paramName, paramArgs)
                -- We now find the player ID sent to us by the server and set it as our name
                local playerID = string.sub(result, 19, #result)
                local playerTab = {playerName = playerID, playerX = WIDTH/2, playerY = HEIGHT/2, timestamp = playerTick}
                myPlayerClient = playerTab
            elseif result and string.find(result, "server ip") then
                -- Now that we have the servers IP address, we can message them later if we need to
                local serverIp = string.sub(result, 10, #result)
                client_info["serverIp"] = serverIp
            elseif result and string.find(result, "player count") then
                -- We now know the current amount of players in our room, for now at least
                local count = string.sub(result, 13, #result)
                print("Player count: "..count)
            elseif result and string.find(result, "invalid confirmation") then
                -- We can now see the reason for our invalid confirmation and display it
                local reason = string.sub(result, 21, #result)
                print("Could not join the room: "..reason)
                current_time = nil
                -- We can now also remove the address and port set for our peer
                client:setpeername("*")
            elseif result == "notified of disconnection" then
                -- We know that we can now safely leave the server
                client:setpeername("*")
                joinedRoom = false
                other_ip = nil
                print("Successfully left the room!")
                local paramNames = {"Create Room", "Join Room", "Find Room"}
                local paramArgs = {createRoom, function()
                        if other_ip then
                            joinRoom(other_ip, false)
                        else
                            parameter.text("IP", "", function(t)
                                other_ip = t
                            end)
                        end
                    end, findRoom}
                resetParameters(paramNames, paramArgs)
            elseif result == nil then
                if current_time then
                    if finding_room == false then
                        if os.time() > current_time + 5 then
                            -- If we get no response back for 5 seconds or more, we know it didn't work
                            print("Failed to connect, please try again later")
                            current_time = nil
                        end
                    end
                end
            end
        else
            local result, ip, port = client:receivefrom()
            if result and string.find(result, "server info") then
                -- Now we can attempt to join the room with the given IP address
                local tab = json.decode(string.sub(result, 12, #result))
                if finding_room == true then
                    joinRoom(tab.ip, true)
                    finding_room = false
                end
            else
                if finding_room == true then
                    if current_time and os.time() > current_time + 15 then
                        -- If we get no response back for 15 seconds or more, we know we couldn't find a room
                        print("Failed to find a room!")
                        current_time = nil
                    end
                end
            end
        end
    end 
end

function updatePlayers()
    -- We now update the position of our player puppets, so it looks like the players are being updated in realtime
    for i,p in pairs(server_info["world_state"]) do
        ellipse(p.playerX, p.playerY, 35)
    end
end

function touched(touch)
    if touch.state == BEGAN or touch.state == MOVING then
        -- Here we send a packet to the server every frame, telling them our player position so it can properly update the whole game
        if joinedRoom == true then
            if myPlayerClient then
                myPlayerClient.playerX = CurrentTouch.x
                myPlayerClient.playerY = CurrentTouch.y
                local playerTab = {playerName = myPlayerClient.playerName, playerX = myPlayerClient.playerX, playerY = myPlayerClient.playerY, timestamp = playerTick}
                player:send("client state"..json.encode(playerTab))
            end
        end
    end
end

-- This function gets called once every frame
function draw()
    -- This sets a dark background color 
    background(40, 40, 50)
    
    -- This sets the line thickness
    strokeWidth(5)
    
    -- Do your drawing here
    updatePlayers()
    receiveData()
    playerTick = playerTick + 1
end

There were 2 circles. Moving 1 circle at a time worked ok, no delay. When I tried moving both circles at the same time, there was a very long delay. The circles continued to move about 10 seconds after I was done moving them.

@dave1707, gimme a bit and I can maybe possibly fix it.

@dave1707, my best guess of what is causing this problem is that the server tick and the client ticks (just a number that is increased by 1 every frame and acts like os.time but just faster) aren’t synchronized and so the player updates aren’t working how they should be. I have fixed this by creating a server tick that increases by 1 every frame, and then sending a packet to every peer connected telling them the server tick. Here’s the code:

function setup()
    socket = require("socket")
    parameter.action("Create Room", createRoom)
    parameter.action("Join Room", function()
        if other_ip then
            joinRoom(other_ip, false)
        else
            parameter.text("IP", "", function(t)
                other_ip = t
            end)
        end
    end)
    parameter.action("Find Room", function()
        findRoom()
    end)
    client = socket.udp()
    client:settimeout(0)
    listener = socket.udp()
    listener:settimeout(0)
    -- We need to create a table that can hold information about our server, if you end up making one
    server_info = {players = 0, clients = {}, world_state = {}, ids_taken = {}, serverTick = 0}
    -- We also need to create a table that can hold info that the client has
    client_info = {serverIp = "", world_state = {}, serverTick = 0}
    other_ip = nil
    current_time = nil
    current_room_count = 1
    tick = 0
    finding_room = false
    server = false
    joinedRoom = false
    myPlayerServer = nil
    myPlayerClient = nil
    serverTick = 0
end

function createRoom()
    -- We need to find your address, so we create a new UDP socket and assign it a random IP
    local ip, port = getLocalIP()
    client:setsockname("*", "14285")
    local ip2, port2 = client:getsockname()
    client:setoption("broadcast", true)
    your_ip = ip2
    your_port = port2
    print("Room created!")
    print("Connect to "..ip)
    server = true
end

function joinRoom(ip, finding_room)
    if server == false then
        if ip ~= "*" then
            if finding_room == false then
                print("Attempting to join the room...")
            end
            -- The port for all the servers is the same, so we only need the IP address of the server
            client:setpeername(ip, "14285")
            client:send("connection confirmation")
            current_time = os.time()
        else
            print("Address not allowed!")
        end
    else
        print("You are already hosting a server!")
    end
end

function findRoom()
    if server == false then
        print("Attempting to find a room...")
        finding_room = true
        local ip, port = getLocalIP()
        local ip1, ip2 = string.match(ip, "(%d+.%d+.%d+.)(%d+)")
        local success, err = pcall(function()
            for z = 1, 255 do
                client:setpeername(ip1..z, "14285")
                client:send("requesting server info")
            end
            client:setpeername("*")
            client:setsockname("*", "14285")
        end)
        if success == false then
            -- The only error you could possibly get in this case is no internet connection, so we print it
            print("No internet connection, please try again later!")
            return
        end
        current_time = os.time()
    else
        print("You are already hosting a server!")
    end
end

function sendMessage(msg)
    if server == true then
        for i,c in pairs(server_info["clients"]) do
            client:sendto(tostring(msg), c.ip, c.port)
        end
    end
end

function leaveRoom()
    -- We need to notify the server that we are leaving, so we send a packet to them
    if server == false then
        client:send("disconnection confirmation")
    end
end

function resetParameters(params, args)
    parameter.clear()
    for i = 1, #params do
        parameter.action(params[i], args[i])
    end
end

function getLocalIP()
    local udp = socket.udp()
    udp:setpeername("192.167.188.122", "14285")
    local ip, port = udp:getsockname()
    udp:close()
    return ip, port
end

function receiveData()
    if server == true then
        local msg, ip, port = client:receivefrom()
        if msg ~= nil then
            if msg == "connection confirmation" then
                if server_info["players"] < 32 then
                    -- We can now add 1 more to our player count
                    server_info["players"] = server_info["players"] + 1
                    -- We now need to store the clients IP and port, so we can send them messages later
                    local uid = {ip = ip, port = port}
                    if not server_info["clients"][uid] then
                        table.insert(server_info["clients"], uid)
                    end
                    -- We now create a random ID for the connecting peer so we can differentiate between players on the server
                    local playerID = math.random(1, 100000)
                    if not server_info["ids_taken"][playerID] then
                        server_info["ids_taken"][playerID] = playerID
                    else
                        playerID = math.random(1, 100000)
                    end
                    client:sendto("valid confirmation"..playerID, ip, port)
                    -- We need to send the joining client our IP, so they can send messages to us later
                    local our_ip, our_port = getLocalIP()
                    client:sendto("server ip"..our_ip, ip, port)
                    -- We can now send everyone that has joined this room a packet telling them the amount of players in the room
                    for i,c in pairs(server_info["clients"]) do
                        client:sendto("player count"..tostring(server_info["players"]), c.ip, c.port)
                    end
                else
                    -- We don't have enough room for another player, so we send the peer a message telling them to disconnect
                    local reason = "Max players inside the room!"
                    client:sendto("invalid confirmation"..reason, ip, port)
                end
            elseif msg == "requesting server info" then
                -- We need to find your address, so we create a new UDP socket and assign it a random IP
                local our_ip, our_port = getLocalIP()
                -- We now know that somebody is trying to find a server, so we send a packet back to them with our server IP and port
                local uid = {ip = our_ip}
                client:sendto("server info"..json.encode(uid), ip, port)
            elseif msg and string.find(msg, "disconnection confirmation") then
                -- We know that one of the clients connected to this server wants to leave, so we send a packet back telling them that we have been notified
                client:sendto("notified of disconnection", ip, port)
                local uid = {ip = ip, port = port}
                local clientInfo
                local clientState
                for i = 1, #server_info["clients"] do
                    if server_info["clients"][i] == uid then
                        clientInfo = i
                    end
                end
                table.remove(server_info["clients"], clientInfo)
                -- We also need to deduct the player count
                server_info["players"] = server_info["players"] - 1
            elseif msg and string.find(msg, "client state") then
                -- A connected peer has sent their client state to us, so we update our world state accordingly
                local clientState = json.decode(string.sub(msg, 13, #msg))
                local hasClientState = false
                local existingState = nil
                for i,s in pairs(server_info["world_state"]) do
                    if s.playerName == clientState.playerName then
                        hasClientState = true
                        existingState = s
                    end
                end
                if hasClientState == true then
                    if clientState.timestamp > existingState.timestamp then
                        existingState.playerX = clientState.playerX
                        existingState.playerY = clientState.playerY
                    elseif clientState.timestamp == existingState.timestamp then
                        existingState.playerX = clientState.playerX
                        existingState.playerY = clientState.playerY
                    end
                else
                    if not server_info["world_state"][clientState] then
                        table.insert(server_info["world_state"], clientState)
                    end
                end
            end
        end
    else
        if finding_room == false then
            local result = client:receive()
            if result and string.find(result, "valid confirmation") then
                -- We know that we successfully joined the room, and we can now receive messages from the server!
                print("Successfully joined the room!")
                joinedRoom = true
                current_time = nil
                parameter.clear()
                -- We now create a button that allows the player to leave the room
                local paramName = {"Leave Room"}
                local paramArgs = {function() leaveRoom() end}
                resetParameters(paramName, paramArgs)
                -- We now find the player ID sent to us by the server and set it as our name
                local playerID = string.sub(result, 19, #result)
                local playerTab = {playerName = playerID, playerX = WIDTH/2, playerY = HEIGHT/2, timestamp = client_info["serverTick"]}
                myPlayerClient = playerTab
            elseif result and string.find(result, "server ip") then
                -- Now that we have the servers IP address, we can message them later if we need to
                local serverIp = string.sub(result, 10, #result)
                client_info["serverIp"] = serverIp
            elseif result and string.find(result, "player count") then
                -- We now know the current amount of players in our room, for now at least
                local count = string.sub(result, 13, #result)
                print("Player count: "..count)
            elseif result and string.find(result, "invalid confirmation") then
                -- We can now see the reason for our invalid confirmation and display it
                local reason = string.sub(result, 21, #result)
                print("Could not join the room: "..reason)
                current_time = nil
                -- We can now also remove the address and port set for our peer
                client:setpeername("*")
            elseif result == "notified of disconnection" then
                -- We know that we can now safely leave the server
                client:setpeername("*")
                joinedRoom = false
                other_ip = nil
                print("Successfully left the room!")
                local paramNames = {"Create Room", "Join Room", "Find Room"}
                local paramArgs = {createRoom, function()
                        if other_ip then
                            joinRoom(other_ip, false)
                        else
                            parameter.text("IP", "", function(t)
                                other_ip = t
                            end)
                        end
                    end, findRoom}
                resetParameters(paramNames, paramArgs)
            elseif result and string.find(result, "server tick") then
                -- Now that we have received the server tick, we can properly update everything without any issues (hopefully)
                local serverTick = string.sub(result, 12, #result)
                client_info["serverTick"] = serverTick
            elseif result == nil then
                if current_time then
                    if finding_room == false then
                        if os.time() > current_time + 5 then
                            -- If we get no response back for 5 seconds or more, we know it didn't work
                            print("Failed to connect, please try again later")
                            current_time = nil
                        end
                    end
                end
            end
        else
            local result, ip, port = client:receivefrom()
            if result and string.find(result, "server info") then
                -- Now we can attempt to join the room with the given IP address
                local tab = json.decode(string.sub(result, 12, #result))
                if finding_room == true then
                    joinRoom(tab.ip, true)
                    finding_room = false
                end
            else
                if finding_room == true then
                    if current_time and os.time() > current_time + 15 then
                        -- If we get no response back for 15 seconds or more, we know we couldn't find a room
                        print("Failed to find a room!")
                        current_time = nil
                    end
                end
            end
        end
    end 
end

function updatePlayers()
    -- We now update the position of our player puppets, so it looks like the players are being updated in realtime
    for i,p in pairs(server_info["world_state"]) do
        ellipse(p.playerX, p.playerY, 35)
    end
    -- We also have to send every player our current tick so everything is properly synchronized such as player positions
    if server == false then
        for i,c in pairs(server_info["clients"]) do
            client:sendto("server tick"..server_info["serverTick"], c.ip, c.port)
        end
    end
end

function touched(touch)
    if touch.state == BEGAN or touch.state == MOVING then
        -- Here we send a packet to the server every frame, telling them our player position so it can properly update the whole game
        if joinedRoom == true then
            if myPlayerClient then
                myPlayerClient.playerX = CurrentTouch.x
                myPlayerClient.playerY = CurrentTouch.y
                local playerTab = {playerName = myPlayerClient.playerName, playerX = myPlayerClient.playerX, playerY = myPlayerClient.playerY, timestamp = client_info["serverTick"]}
                player:send("client state"..json.encode(playerTab))
            end
        end
    end
end

-- This function gets called once every frame
function draw()
    -- This sets a dark background color 
    background(40, 40, 50)
    
    -- This sets the line thickness
    strokeWidth(5)
    
    -- Do your drawing here
    updatePlayers()
    receiveData()
    if server == true then
        server_info["serverTick"] = server_info["serverTick"] + 1
    end
end

Nothing really changed. It seems the longer I keep move both circles, the longer they keep moving after I stop moving my finger. If I move both circles for about 2 seconds, they continue moving for about a second after I stop. If I move for like 20 seconds, they keep moving for a long time after I stop, maybe 10 seconds.

@dave1707, try this.

function setup()
    socket = require("socket")
    parameter.action("Create Room", createRoom)
    parameter.action("Join Room", function()
        if other_ip then
            joinRoom(other_ip, false)
        else
            parameter.text("IP", "", function(t)
                other_ip = t
            end)
        end
    end)
    parameter.action("Find Room", function()
        findRoom()
    end)
    client = socket.udp()
    client:settimeout(0)
    listener = socket.udp()
    listener:settimeout(0)
    -- We need to create a table that can hold information about our server, if you end up making one
    server_info = {players = 0, clients = {}, world_state = {}, ids_taken = {}, serverTick = 0}
    -- We also need to create a table that can hold info that the client has
    client_info = {serverIp = "", world_state = {}, serverTick = 0}
    other_ip = nil
    current_time = nil
    current_room_count = 1
    tick = 0
    finding_room = false
    server = false
    joinedRoom = false
    myPlayerServer = nil
    myPlayerClient = nil
    serverTick = 0
end

function createRoom()
    -- We need to find your address, so we create a new UDP socket and assign it a random IP
    local ip, port = getLocalIP()
    client:setsockname("*", "14285")
    local ip2, port2 = client:getsockname()
    client:setoption("broadcast", true)
    your_ip = ip2
    your_port = port2
    print("Room created!")
    print("Connect to "..ip)
    server = true
end

function joinRoom(ip, finding_room)
    if server == false then
        if ip ~= "*" then
            if finding_room == false then
                print("Attempting to join the room...")
            end
            -- The port for all the servers is the same, so we only need the IP address of the server
            client:setpeername(ip, "14285")
            client:send("connection confirmation")
            current_time = os.time()
        else
            print("Address not allowed!")
        end
    else
        print("You are already hosting a server!")
    end
end

function findRoom()
    if server == false then
        print("Attempting to find a room...")
        finding_room = true
        local ip, port = getLocalIP()
        local ip1, ip2 = string.match(ip, "(%d+.%d+.%d+.)(%d+)")
        local success, err = pcall(function()
            for z = 1, 255 do
                client:setpeername(ip1..z, "14285")
                client:send("requesting server info")
            end
            client:setpeername("*")
            client:setsockname("*", "14285")
        end)
        if success == false then
            -- The only error you could possibly get in this case is no internet connection, so we print it
            print("No internet connection, please try again later!")
            return
        end
        current_time = os.time()
    else
        print("You are already hosting a server!")
    end
end

function sendMessage(msg)
    if server == true then
        for i,c in pairs(server_info["clients"]) do
            client:sendto(tostring(msg), c.ip, c.port)
        end
    end
end

function leaveRoom()
    -- We need to notify the server that we are leaving, so we send a packet to them
    if server == false then
        client:send("disconnection confirmation")
    end
end

function resetParameters(params, args)
    parameter.clear()
    for i = 1, #params do
        parameter.action(params[i], args[i])
    end
end

function getLocalIP()
    local udp = socket.udp()
    udp:setpeername("192.167.188.122", "14285")
    local ip, port = udp:getsockname()
    udp:close()
    return ip, port
end

function receiveData()
    if server == true then
        local msg, ip, port = client:receivefrom()
        if msg ~= nil then
            if msg == "connection confirmation" then
                if server_info["players"] < 32 then
                    -- We can now add 1 more to our player count
                    server_info["players"] = server_info["players"] + 1
                    -- We now need to store the clients IP and port, so we can send them messages later
                    local uid = {ip = ip, port = port}
                    if not server_info["clients"][uid] then
                        table.insert(server_info["clients"], uid)
                    end
                    -- We now create a random ID for the connecting peer so we can differentiate between players on the server
                    local playerID = math.random(1, 100000)
                    if not server_info["ids_taken"][playerID] then
                        server_info["ids_taken"][playerID] = playerID
                    else
                        playerID = math.random(1, 100000)
                    end
                    client:sendto("valid confirmation"..playerID, ip, port)
                    -- We need to send the joining client our IP, so they can send messages to us later
                    local our_ip, our_port = getLocalIP()
                    client:sendto("server ip"..our_ip, ip, port)
                    -- We can now send everyone that has joined this room a packet telling them the amount of players in the room
                    for i,c in pairs(server_info["clients"]) do
                        client:sendto("player count"..tostring(server_info["players"]), c.ip, c.port)
                    end
                else
                    -- We don't have enough room for another player, so we send the peer a message telling them to disconnect
                    local reason = "Max players inside the room!"
                    client:sendto("invalid confirmation"..reason, ip, port)
                end
            elseif msg == "requesting server info" then
                -- We need to find your address, so we create a new UDP socket and assign it a random IP
                local our_ip, our_port = getLocalIP()
                -- We now know that somebody is trying to find a server, so we send a packet back to them with our server IP and port
                local uid = {ip = our_ip}
                client:sendto("server info"..json.encode(uid), ip, port)
            elseif msg and string.find(msg, "disconnection confirmation") then
                -- We know that one of the clients connected to this server wants to leave, so we send a packet back telling them that we have been notified
                client:sendto("notified of disconnection", ip, port)
                local uid = {ip = ip, port = port}
                local clientInfo
                local clientState
                for i = 1, #server_info["clients"] do
                    if server_info["clients"][i] == uid then
                        clientInfo = i
                    end
                end
                table.remove(server_info["clients"], clientInfo)
                -- We also need to deduct the player count
                server_info["players"] = server_info["players"] - 1
            elseif msg and string.find(msg, "client state") then
                -- A connected peer has sent their client state to us, so we update our world state accordingly
                local clientState = json.decode(string.sub(msg, 13, #msg))
                local hasClientState = false
                local existingState = nil
                for i,s in pairs(server_info["world_state"]) do
                    if s.playerName == clientState.playerName then
                        hasClientState = true
                        existingState = s
                    end
                end
                if hasClientState == true then
                    if clientState.timestamp > existingState.timestamp then
                        existingState.playerX = clientState.playerX
                        existingState.playerY = clientState.playerY
                    end
                else
                    if not server_info["world_state"][clientState] then
                        table.insert(server_info["world_state"], clientState)
                    end
                end
            end
        end
    else
        if finding_room == false then
            local result = client:receive()
            if result and string.find(result, "valid confirmation") then
                -- We know that we successfully joined the room, and we can now receive messages from the server!
                print("Successfully joined the room!")
                joinedRoom = true
                current_time = nil
                parameter.clear()
                -- We now create a button that allows the player to leave the room
                local paramName = {"Leave Room"}
                local paramArgs = {function() leaveRoom() end}
                resetParameters(paramName, paramArgs)
                -- We now find the player ID sent to us by the server and set it as our name
                local playerID = string.sub(result, 19, #result)
                local playerTab = {playerName = playerID, playerX = WIDTH/2, playerY = HEIGHT/2, timestamp = client_info["serverTic"]}
                myPlayerClient = playerTab
            elseif result and string.find(result, "server ip") then
                -- Now that we have the servers IP address, we can message them later if we need to
                local serverIp = string.sub(result, 10, #result)
                client_info["serverIp"] = serverIp
            elseif result and string.find(result, "player count") then
                -- We now know the current amount of players in our room, for now at least
                local count = string.sub(result, 13, #result)
                print("Player count: "..count)
            elseif result and string.find(result, "invalid confirmation") then
                -- We can now see the reason for our invalid confirmation and display it
                local reason = string.sub(result, 21, #result)
                print("Could not join the room: "..reason)
                current_time = nil
                -- We can now also remove the address and port set for our peer
                client:setpeername("*")
            elseif result == "notified of disconnection" then
                -- We know that we can now safely leave the server
                client:setpeername("*")
                joinedRoom = false
                other_ip = nil
                print("Successfully left the room!")
                local paramNames = {"Create Room", "Join Room", "Find Room"}
                local paramArgs = {createRoom, function()
                        if other_ip then
                            joinRoom(other_ip, false)
                        else
                            parameter.text("IP", "", function(t)
                                other_ip = t
                            end)
                        end
                    end, findRoom}
                resetParameters(paramNames, paramArgs)
            elseif result and string.find(result, "server tick") then
                -- Now that we have received the server tick, we can properly update everything without any issues (hopefully)
                local serverTick = string.sub(result, 12, #result)
                client_info["serverTick"] = serverTick
            elseif result == nil then
                if current_time then
                    if finding_room == false then
                        if os.time() > current_time + 5 then
                            -- If we get no response back for 5 seconds or more, we know it didn't work
                            print("Failed to connect, please try again later")
                            current_time = nil
                        end
                    end
                end
            end
        else
            local result, ip, port = client:receivefrom()
            if result and string.find(result, "server info") then
                -- Now we can attempt to join the room with the given IP address
                local tab = json.decode(string.sub(result, 12, #result))
                if finding_room == true then
                    joinRoom(tab.ip, true)
                    finding_room = false
                end
            else
                if finding_room == true then
                    if current_time and os.time() > current_time + 15 then
                        -- If we get no response back for 15 seconds or more, we know we couldn't find a room
                        print("Failed to find a room!")
                        current_time = nil
                    end
                end
            end
        end
    end 
end

function updatePlayers()
    -- We now update the position of our player puppets, so it looks like the players are being updated in realtime
    for i,p in pairs(server_info["world_state"]) do
        ellipse(p.playerX, p.playerY, 35)
    end
    -- We also have to send every player our current tick so everything is properly synchronized such as player positions
    if server == false then
        for i,c in pairs(server_info["clients"]) do
            client:sendto("server tick"..server_info["serverTick"], c.ip, c.port)
        end
    end
end

function touched(touch)
    if touch.state ~= ENDED and touch.state ~= CANCELLED then
        if touch.state == BEGAN or touch.state == MOVING then
        -- Here we send a packet to the server every frame, telling them our player position so it can properly update the whole game
            if joinedRoom == true then
                if myPlayerClient then
                    myPlayerClient.playerX = CurrentTouch.x
                    myPlayerClient.playerY = CurrentTouch.y
                    local playerTab = {playerName = myPlayerClient.playerName, playerX = myPlayerClient.playerX, playerY = myPlayerClient.playerY, timestamp = client_info["serverTick"]}
                    player:send("client state"..json.encode(playerTab))
                end
            end
        end
    end
end

-- This function gets called once every frame
function draw()
    -- This sets a dark background color 
    background(40, 40, 50)
    
    -- This sets the line thickness
    strokeWidth(5)
    
    -- Do your drawing here
    updatePlayers()
    receiveData()
    if server == true then
        server_info["serverTick"] = server_info["serverTick"] + 1
    end
end

Nothing changed. I moved both circles for about 15 seconds and they kept moving for 20 seconds after I stopped.

@dave1707, I edited the code again. Try it now.

The balls appear on the first iPad, but they don’t move when I move my finger on the 2nd and 3rd iPad.

@dave1707, try this. Most likely, nothing will change but yeah.

function setup()
    socket = require("socket")
    parameter.action("Create Room", createRoom)
    parameter.action("Join Room", function()
        if other_ip then
            joinRoom(other_ip, false)
        else
            parameter.text("IP", "", function(t)
                other_ip = t
            end)
        end
    end)
    parameter.action("Find Room", function()
        findRoom()
    end)
    client = socket.udp()
    client:settimeout(0)
    listener = socket.udp()
    listener:settimeout(0)
    -- We need to create a table that can hold information about our server, if you end up making one
    server_info = {players = 0, clients = {}, world_state = {}, ids_taken = {}, serverTick = 0, serverTimestamp = 0}
    -- We also need to create a table that can hold info that the client has
    client_info = {serverIp = "", world_state = {}, serverTick = 0}
    other_ip = nil
    current_time = nil
    current_room_count = 1
    tick = 0
    finding_room = false
    server = false
    joinedRoom = false
    myPlayerServer = nil
    myPlayerClient = nil
    serverTick = 0
end

function createRoom()
    -- We need to find your address, so we create a new UDP socket and assign it a random IP
    local ip, port = getLocalIP()
    client:setsockname("*", "14285")
    local ip2, port2 = client:getsockname()
    client:setoption("broadcast", true)
    your_ip = ip2
    your_port = port2
    print("Room created!")
    print("Connect to "..ip)
    server = true
end

function joinRoom(ip, finding_room)
    if server == false then
        if ip ~= "*" then
            if finding_room == false then
                print("Attempting to join the room...")
            end
            -- The port for all the servers is the same, so we only need the IP address of the server
            client:setpeername(ip, "14285")
            client:send("connection confirmation")
            current_time = os.time()
        else
            print("Address not allowed!")
        end
    else
        print("You are already hosting a server!")
    end
end

function findRoom()
    if server == false then
        print("Attempting to find a room...")
        finding_room = true
        local ip, port = getLocalIP()
        local ip1, ip2 = string.match(ip, "(%d+.%d+.%d+.)(%d+)")
        local success, err = pcall(function()
            for z = 1, 255 do
                client:setpeername(ip1..z, "14285")
                client:send("requesting server info")
            end
            client:setpeername("*")
            client:setsockname("*", "14285")
        end)
        if success == false then
            -- The only error you could possibly get in this case is no internet connection, so we print it
            print("No internet connection, please try again later!")
            return
        end
        current_time = os.time()
    else
        print("You are already hosting a server!")
    end
end

function sendMessage(msg)
    if server == true then
        for i,c in pairs(server_info["clients"]) do
            client:sendto(tostring(msg), c.ip, c.port)
        end
    end
end

function leaveRoom()
    -- We need to notify the server that we are leaving, so we send a packet to them
    if server == false then
        client:send("disconnection confirmation")
    end
end

function resetParameters(params, args)
    parameter.clear()
    for i = 1, #params do
        parameter.action(params[i], args[i])
    end
end

function getLocalIP()
    local udp = socket.udp()
    udp:setpeername("192.167.188.122", "14285")
    local ip, port = udp:getsockname()
    udp:close()
    return ip, port
end

function receiveData()
    if server == true then
        local msg, ip, port = client:receivefrom()
        if msg ~= nil then
            if msg == "connection confirmation" then
                if server_info["players"] < 32 then
                    -- We can now add 1 more to our player count
                    server_info["players"] = server_info["players"] + 1
                    -- We now need to store the clients IP and port, so we can send them messages later
                    local uid = {ip = ip, port = port}
                    if not server_info["clients"][uid] then
                        table.insert(server_info["clients"], uid)
                    end
                    -- We now create a random ID for the connecting peer so we can differentiate between players on the server
                    local playerID = math.random(1, 100000)
                    if not server_info["ids_taken"][playerID] then
                        server_info["ids_taken"][playerID] = playerID
                    else
                        playerID = math.random(1, 100000)
                    end
                    client:sendto("valid confirmation"..playerID, ip, port)
                    -- We need to send the joining client our IP, so they can send messages to us later
                    local our_ip, our_port = getLocalIP()
                    client:sendto("server ip"..our_ip, ip, port)
                    -- We can now send everyone that has joined this room a packet telling them the amount of players in the room
                    for i,c in pairs(server_info["clients"]) do
                        client:sendto("player count"..tostring(server_info["players"]), c.ip, c.port)
                    end
                else
                    -- We don't have enough room for another player, so we send the peer a message telling them to disconnect
                    local reason = "Max players inside the room!"
                    client:sendto("invalid confirmation"..reason, ip, port)
                end
            elseif msg == "requesting server info" then
                -- We need to find your address, so we create a new UDP socket and assign it a random IP
                local our_ip, our_port = getLocalIP()
                -- We now know that somebody is trying to find a server, so we send a packet back to them with our server IP and port
                local uid = {ip = our_ip}
                client:sendto("server info"..json.encode(uid), ip, port)
            elseif msg and string.find(msg, "disconnection confirmation") then
                -- We know that one of the clients connected to this server wants to leave, so we send a packet back telling them that we have been notified
                client:sendto("notified of disconnection", ip, port)
                local uid = {ip = ip, port = port}
                local clientInfo
                local clientState
                for i = 1, #server_info["clients"] do
                    if server_info["clients"][i] == uid then
                        clientInfo = i
                    end
                end
                table.remove(server_info["clients"], clientInfo)
                -- We also need to deduct the player count
                server_info["players"] = server_info["players"] - 1
            elseif msg and string.find(msg, "client state") then
                -- A connected peer has sent their client state to us, so we update our world state accordingly
                local clientState = json.decode(string.sub(msg, 13, #msg))
                local hasClientState = false
                local existingState = nil
                for i,s in pairs(server_info["world_state"]) do
                    if s.playerName == clientState.playerName then
                        hasClientState = true
                        existingState = s
                    end
                end
                if hasClientState == true then
                    if clientState.timestamp > server_info["serverTimestamp"] then
                        existingState.playerX = clientState.playerX
                        existingState.playerY = clientState.playerY
                        server_info["serverTimestamp"] = clientState.timestamp
                    end
                else
                    if not server_info["world_state"][clientState] then
                        table.insert(server_info["world_state"], clientState)
                        if server_info["world_state"][clientState]["timestamp"] > server_info["serverTimestamp"] then
                            server_info["serverTimestamp"] = server_info["world_state"][clientState]["timestamp"]
                        end
                    end
                end
            end
        end
    else
        if finding_room == false then
            local result = client:receive()
            if result and string.find(result, "valid confirmation") then
                -- We know that we successfully joined the room, and we can now receive messages from the server!
                print("Successfully joined the room!")
                joinedRoom = true
                current_time = nil
                parameter.clear()
                -- We now create a button that allows the player to leave the room
                local paramName = {"Leave Room"}
                local paramArgs = {function() leaveRoom() end}
                resetParameters(paramName, paramArgs)
                -- We now find the player ID sent to us by the server and set it as our name
                local playerID = string.sub(result, 19, #result)
                local playerTab = {playerName = playerID, playerX = WIDTH/2, playerY = HEIGHT/2, timestamp = client_info["serverTic"]}
                myPlayerClient = playerTab
            elseif result and string.find(result, "server ip") then
                -- Now that we have the servers IP address, we can message them later if we need to
                local serverIp = string.sub(result, 10, #result)
                client_info["serverIp"] = serverIp
            elseif result and string.find(result, "player count") then
                -- We now know the current amount of players in our room, for now at least
                local count = string.sub(result, 13, #result)
                print("Player count: "..count)
            elseif result and string.find(result, "invalid confirmation") then
                -- We can now see the reason for our invalid confirmation and display it
                local reason = string.sub(result, 21, #result)
                print("Could not join the room: "..reason)
                current_time = nil
                -- We can now also remove the address and port set for our peer
                client:setpeername("*")
            elseif result == "notified of disconnection" then
                -- We know that we can now safely leave the server
                client:setpeername("*")
                joinedRoom = false
                other_ip = nil
                print("Successfully left the room!")
                local paramNames = {"Create Room", "Join Room", "Find Room"}
                local paramArgs = {createRoom, function()
                        if other_ip then
                            joinRoom(other_ip, false)
                        else
                            parameter.text("IP", "", function(t)
                                other_ip = t
                            end)
                        end
                    end, findRoom}
                resetParameters(paramNames, paramArgs)
            elseif result and string.find(result, "server tick") then
                -- Now that we have received the server tick, we can properly update everything without any issues (hopefully)
                local serverTick = string.sub(result, 12, #result)
                client_info["serverTick"] = serverTick
            elseif result == nil then
                if current_time then
                    if finding_room == false then
                        if os.time() > current_time + 5 then
                            -- If we get no response back for 5 seconds or more, we know it didn't work
                            print("Failed to connect, please try again later")
                            current_time = nil
                        end
                    end
                end
            end
        else
            local result, ip, port = client:receivefrom()
            if result and string.find(result, "server info") then
                -- Now we can attempt to join the room with the given IP address
                local tab = json.decode(string.sub(result, 12, #result))
                if finding_room == true then
                    joinRoom(tab.ip, true)
                    finding_room = false
                end
            else
                if finding_room == true then
                    if current_time and os.time() > current_time + 15 then
                        -- If we get no response back for 15 seconds or more, we know we couldn't find a room
                        print("Failed to find a room!")
                        current_time = nil
                    end
                end
            end
        end
    end 
end

function updatePlayers()
    -- We now update the position of our player puppets, so it looks like the players are being updated in realtime
    for i,p in pairs(server_info["world_state"]) do
        ellipse(p.playerX, p.playerY, 35)
    end
    -- We also have to send every player our current tick so everything is properly synchronized such as player positions
    if server == false then
        for i,c in pairs(server_info["clients"]) do
            client:sendto("server tick"..server_info["serverTick"], c.ip, c.port)
        end
    end
end

function touched(touch)
    if touch.state ~= ENDED and touch.state ~= CANCELLED then
        if touch.state == BEGAN or touch.state == MOVING then
        -- Here we send a packet to the server every frame, telling them our player position so it can properly update the whole game
            if joinedRoom == true then
                if myPlayerClient then
                    myPlayerClient.playerX = CurrentTouch.x
                    myPlayerClient.playerY = CurrentTouch.y
                    local playerTab = {playerName = myPlayerClient.playerName, playerX = myPlayerClient.playerX, playerY = myPlayerClient.playerY, timestamp = client_info["serverTick"]}
                    player:send("client state"..json.encode(playerTab))
                end
            end
        end
    end
end

-- This function gets called once every frame
function draw()
    -- This sets a dark background color 
    background(40, 40, 50)
    
    -- This sets the line thickness
    strokeWidth(5)
    
    -- Do your drawing here
    updatePlayers()
    receiveData()
    if server == true then
        server_info["serverTick"] = server_info["serverTick"] + 1
    end
end

Got this error on iPad 1 when trying to join room on iPad 2.

Main:196: attempt to index a nil value (field ‘?’)
stack traceback:
Main:196: in function ‘receiveData’
Main:328: in function ‘draw’

@dave1707, fixed the code. Try it now

Got this error.

Main:196: attempt to index a nil value (field ‘?’)
stack traceback:
Main:196: in function ‘receiveData’
Main:328: in function ‘draw’

This is line 196’
if server_info[“world_state”][clientstate][“timestamp”] > server_info[“serverTimestamp”] then