Local Multiplayer Game

@dave1707, I fixed it again. Hopefully there are no more errors.

Same error.

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

                    if server_info["world_state"][clientState]["timestamp"] > server_info["serverTimestamp"] then
                        server_info["serverTimestamp"] = server_info["world_state"][clientState]["timestamp"]
                    end

@dave1707, change the code to this:

if server_info["world_state"][clientState]["serverTick"] > server_info["serverTimestamp"] then
    server_info["serverTimestamp"] = server_info["world_state"][clientState]["serverTick"]
end

Same error.

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

@dave1707, sorry there was another spelling error. Try it now.

Same error. I don’t think this is working. Maybe you should wait until you have access to multiple iPads so you can write and test your code and actually see what’s happening.

@dave1707, I was referencing a nil value and I didn’t know. Here’s the updated 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, 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[“serverTick”] < 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
                       server_info["world_state"][clientState] = clientState
                        if server_info["world_state"][clientState]["serverTick"] < server_info["serverTimestamp"] then
                            server_info["serverTimestamp"] = server_info["world_state"][clientState]["serverTick"]
                        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, serverTick = 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

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

@dave1707, I don’t know why I keep spelling things wrong, but I fixed another spelling error. HOPEFULLY it works now.

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

                    if server_info["world_state"][clientState]["serverTick"] > server_info["serverTimestamp"] then

@dave1707, changed the code again. I know something won’t work because that what always happens, but yeah.

I’m now getting an error on line 188.

                if clientState.timestamp > server_info["serverTimestamp"] then

I checked clientState.timestamp and it’s nil.

@dave1707, try it now, I fixed it.

                if clientState[“serverTick”] > server_info["serverTimestamp"] then

The above line, around 188, gives an error.

The “ ” around serverTick aren’t the correct “”. I had to correct them. Apparently you didn’t try to test anything or you would have run into that error.

Anyways, after I corrected the error and ran the code, a circle appeared on iPad 1, but didn’t move when I moved my finger around iPad 2.

@dave1707, I don’t know why the speech marks worked. I did fix the player not moving on iPad 1 (hopefully).

Line 188 is still giving an error. The circle still doesn’t move.

@dave1707, I took one of the earlier versions of this project (not too early, maybe two or three posts up from the bottom of this discussion) and added some more code. I also changed the code from having the old time stamp checking code (which was confusing and difficult) and changed it to a number which would increase every time you sent a packet to the server. The server would then check if the send number is greater than the send number which was held in the existing client state. 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 = {}, 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
    sendNumber = 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
    player:send("disconnection confirmation")
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.sendNumber > existingState.sendNumber 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!")
                viewer.preferredFPS = 30
                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 set our player name to the ID that the server sent us
                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 touched(touch)
    if touch.state ~= ENDED and touch.state ~= CANCELLED then
        if touch.state == BEGAN or touch.state == CHANGED then
            if joinedRoom == true then
                if myPlayerClient then
                    myPlayerClient.playerX = CurrentTouch.x
                    myPlayerClient.playerY = CurrentTouch.y
                    sendNumber = sendNumber + 1
                    local playerTab = {playerName = myPlayerClient.playerName, playerX = myPlayerClient.playerX, playerY = myPlayerClient.playerY, sendNumber = sendNumber}
                    client:send("client state"..json.encode(playerTab))
                end
            end
        end
    else
        if joinedRoom == true then
            if myPlayerClient then
                myPlayerClient.playerX = CurrentTouch.x
                myPlayerClient.playerY = CurrentTouch.y
                sendNumber = sendNumber + 1
                local playerTab = {playerName = myPlayerClient.playerName, playerX = myPlayerClient.playerX, playerY = myPlayerClient.playerY, sendNumber = sendNumber}
                client:send("client state"..json.encode(playerTab))
            end
        end
    end
end

function updatePlayers()
    -- Here we send a packet to the server every frame, telling them our player position so it can properly update the whole game
    -- 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

-- 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()
end

Both balls show up on iPad 1. Moving just one ball and the response was ok. Moving 2 balls and there is a large delay after awhile. The balls continued to move for about 15 seconds after I stopped moving both fingers.

@dave1707, Try the code now. I edited the code again after doing a little bit of research on multiplayer network latency. I decided to try setting the client’s frame rate to 30 fps when they join the server so even though less packets are being sent, theoretically the server should not get as many packets and so there won’t be as much latency. I even tested a mathematical calculation that allows me to linearly interpolate between the previous position of the player and the new position.

Moving just 1 circle, there’s a delay partway through the move. If I move from bottom to top, halfway up the screen the circle will pause for a fraction of a second. When I move 2 circles for maybe 30 seconds, the circles will continue to move for about 20 seconds after I stop moving my fingers on the screen.