LuaSocket questions

@Ignatz - strange, I’ll have to check. from the telnet connection you’ll have to type lua code followed by carriage return, the server wait for “valid code” to execute. Even with a simple print("Hello") it does not work ? Sorry for that.

Not your fault, @toffer

I changed your code so it just prints the variable “lin”

But even the test for lin=“bye” doesn’t recognise the word bye, for some reason. It gets the message but doesn’t treat it as text.

@Ignatz - odd can you paste your code ?

I am using your exact code with a statement that prints “lin” when it is received. It just prints blank, and the program doesn’t recognise “bye” as it should.

Funnily, when I first tried it, it did recognise bye. Maybe it’s something on the PC side, but I can’t think what.

Just popping back in to say I got the opportunity to test my above code on two devices, (apple testflight is linked to id not device, so I signed in on a friend’s), and it worked great.

I plan to put together a Multiplayer class of sorts to make connecting to, and sending data between, instances of a game easier.

Can’t wait @JakAttak

That would be great, thanks!

As promised, here is the first second version, along with a main tab that shows how to use it.



--# Main
function setup()
    local connectionMade = function()
        output.clear()
        parameter.clear()
        print("Connected!")
    end
    
    multihandler = Multiplayer(receiveData, connectionMade)
    
    parameter.action("Host Game", function()
        multihandler:hostGame()
    end)
    
    parameter.action("Search for and Join Game", function()
        multihandler:findGame()
    end)
    
    parameter.action("Join Game", function()
        if other_ip then
            multihandler:joinGame(other_ip, other_port)
        else
            parameter.text("other_ip", "")
            parameter.text("other_port", "")
            
            print("Fill in the host's ip and port, then click join game again")
        end
    end)
    
    opos, cpos = vec2(WIDTH / 2, HEIGHT / 2), vec2(WIDTH / 2, HEIGHT / 2)
end

function receiveData(d)
    cpos = loadstring("return " .. d)()
end

function draw()
    background(255, 255, 255, 255)

    multihandler:update()
    
    if multihandler.connected then
        local p1, p2 = opos, cpos
        if not multihandler.is_host then p1, p2 = cpos, opos end
        
        fill(255, 0, 0)
        ellipse(p1.x, p1.y, WIDTH / 4)
        
        fill(0, 0, 255)
        ellipse(p2.x, p2.y, WIDTH / 4)
    else
        fill(0)
        text("Waiting for connection...", WIDTH / 2, HEIGHT / 2)
    end
end

function touched(t)
    if multihandler.connected then
        opos = vec2(t.x, t.y)
        
        multihandler:sendData("vec2(" .. t.x .. ", " .. t.y .. ")")
    end
end

--# Multiplayer
local socket = require("socket")

Multiplayer = class()

function Multiplayer:init(dcb, ccb)
    self.my_ip, self.my_port = self:getLocalIP(), 5400
    self.peer_ip, self.peer_port = nil, self.my_port

    self.client = socket.udp()
    self.client:settimeout(0)
    
    self.connected = false
    self.is_host = false
    self.searching = false
    
    self.dataCallback = dcb or function() end
    self.connectedCallback = ccb or function() end
end

-- Returns this iPad's local ip
function Multiplayer:getLocalIP()
    local randomIP = "192.167.188.122"
    local randomPort = "3102" 
    local randomSocket = socket.udp() 
    randomSocket:setpeername(randomIP,randomPort) 

    local localIP, somePort = randomSocket:getsockname()

    randomSocket:close()
    randomSocket = nil

    return localIP
end

-- Set the connected status and call the connection callback if needed
function Multiplayer:setConnectedVal(bool)
    self.connected = bool
    
    if self.connected then
        self.connectedCallback()
    end
end

function Multiplayer:setHostVal(bool)
    self.is_host = bool
end

-- Prepare to be the host
function Multiplayer:hostGame()
    print("Connect to " .. self.my_ip .. ":" .. self.my_port)

    self.client:setsockname(self.my_ip, self.my_port)
    
    self:setConnectedVal(false)
    self.is_host = true
    self.searching = false
end

-- Find a host
function Multiplayer:findGame()
    print("Searching for games...")
    
    self.searching = true
    
    local ip_start, ip_end = self.my_ip:match("(%d+.%d+.%d+.)(%d+)")
    for i = 1, 255 do
        if i ~= tonumber(ip_end) then
            tween.delay(0.01 * i, function()
                self.client:setsockname(ip_start .. i, self.my_port)
                self.client:sendto("connection_confirmation", ip_start .. i, self.my_port)
            end)
        end
    end
end

-- Prepare to join a host
function Multiplayer:joinGame(ip, port)
    self.peer_ip, self.peer_port = ip, port
    
    self.client:setsockname(ip, port)
    
    self.is_host = false
    self.searching = false
    
    self:sendData("connection_confirmation")
end

-- Send data to the other client
function Multiplayer:sendData(msg_to_send)
    if self.peer_ip then
        self.client:sendto(msg_to_send, self.peer_ip, self.peer_port)
    end
end

-- Check for data received from the other client
function Multiplayer:checkForReceivedData()
    local data, msg_or_ip, port_or_nil = self.client:receivefrom()
    if data then
            -- Store the ip of this new client so you can send data back
            self.peer_ip, self.peer_port = msg_or_ip, port_or_nil
            
            if not self.connected and data == "connection_confirmation" then
                self:sendData("connection_confirmation")
                self:setConnectedVal(true)
            end
            
            -- Call callback with received data
            if data ~= "connection_confirmation" then
                self.dataCallback(data)
            end
    end
end

function Multiplayer:update()
    self:checkForReceivedData()
end

enjoy!

EDIT: updated to allow one to automatically find another that is hosting

A quick description of the class: it simply handles all of the connections between two iPad’s and allows you to send and receive data. That’s all. It’s up to your project to tell it when to start, what to connect to, what to send, and to handle the received data.

Nice work @JakAttak =D>

B-)

I’ve updated the above code to add the ability to auto find and connect to a broadcasting client, though currently it just joins the first one it finds. I think I may work on it more to end up with a list of games you could join.

Man, I can’t wait for this update!! @Luatee I see your upcoming game may have multiplayer, eh? I can help with that, I was quite good with Java sockets and I can try to help you once the update comes out. As always, super interested in Aedifico, seems like the game I’ve always wanted to play/make. Good job so far. Thanks everyone for the examples as well!

@Luatee, great idea. yep that is the most reliable, with only a slight lag between a click and a result on the client player.

@JakAttak how slow is ‘slight lag’? What’s the average refresh rate? Thanks for the tips.

@Luatee, I would say it takes about a hundredth of a second for the action to make it to the host, then back.

I will try to take a short video.

@Luatee, here is the code:

function setup()
    physics.continuous = true
    
    multihandler = Multiplayer(receiveData, gameSetup)
    
    parameter.action("Host Game", function() multihandler:hostGame() end)
    parameter.action("Find Game", function() multihandler:findGame() end)
    parameter.action("Join Game", function()
        if other_ip then
            multihandler:joinGame(other_ip, other_port)
        else
            parameter.text("other_ip", "")
            parameter.text("other_port", "")
            
            print("Fill in the host's ip and port, then click join game again")
        end
    end)
end

function gameSetup()
    parameter.clear()
    output.clear()
    print("Connected!")
    
    -- Game walls to keep balls on screen
    walls = {}
    walls[1] = physics.body(EDGE, vec2(0,0), vec2(WIDTH, 0))
    walls[2] = physics.body(EDGE, vec2(WIDTH,0), vec2(WIDTH, HEIGHT))
    walls[3] = physics.body(EDGE, vec2(WIDTH,HEIGHT), vec2(0, HEIGHT))
    walls[4] = physics.body(EDGE, vec2(0,HEIGHT), vec2(0, 0))
    
    
    local pos1, pos2 = vec2(WIDTH / 4, HEIGHT / 2), vec2(WIDTH * 3/4, HEIGHT / 2)
    local rad = WIDTH / 16
    
    -- Variables
    balls = {}
    
    if multihandler.is_host then
        -- Create physics bodies
        balls[1] = physics.body(CIRCLE, rad)
        balls[2] = physics.body(CIRCLE, rad)
        balls[1].position, balls[2].position = pos1, pos2
        
        controlling, other = balls[1], balls[2]
    else
        balls[1] = { position = pos1, radius = rad }
        balls[2] = { position = pos2, radius = rad }
        controlling, other = balls[2], balls[1]
    end
    
    moveForce = 100
    target = vec2(0, 0)
    syncdata = true
end

function receiveData(d)
    local tb = loadstring("return " .. d)()
    
    if syncdata then
        if multihandler.is_host then
            other:applyForce(tb.move)
        else
            balls[1].position, balls[2].position = tb.pos, tb.pos2
        end
    end
end

function draw()
    background(255)

    multihandler:update()
    
    if multihandler.connected then
        fill(255, 0, 0)
        ellipse(balls[1].position.x, balls[1].position.y, balls[1].radius * 2)
        
        fill(0, 0, 255)
        ellipse(balls[2].position.x, balls[2].position.y, balls[2].radius * 2)
        
        fill(0)
        text("Tap the screen to move", WIDTH / 2, HEIGHT / 2)
        
        if multihandler.is_host then
            multihandler:sendData("{ pos = " .. vec2ToStr(balls[1].position) .. ", pos2 = " .. vec2ToStr(balls[2].position) .. " }")
        end
    else
        fill(0)
        text("Waiting for connection...", WIDTH / 2, HEIGHT / 2)
    end
end

function vec2ToStr(vec)
    return "vec2" .. tostring(vec)
end

function touched(t)
    if multihandler.connected then
        if t.state == ENDED then
            local target = vec2(t.x, t.y)
            local move = (target - controlling.position) * moveForce
            if multihandler.is_host then
                controlling:applyForce(move)
            else
                multihandler:sendData("{ move = " .. vec2ToStr(move) .. " }")
            end
        end
    end
end

and here is a short video of it in action.

https://www.youtube.com/watch?v=Ym1Fg77MsJM

the left iPad is the ‘client’, so you hopefully can get an idea of how minimal the lag is.

@JakAttak Oh lordie! I’m a long way away from multiplayer but I think a good test would be using the physics engine, from the look of the code though it seems very plausible.

@Luatee, physics certainly seems possible, but it might be a bit difficult to implement for a couple of reasons.

  1. If you simply send the positions of the bodies, collisions may not work right

  2. If you send the movement data (ex. anything you’d pass to applyForce), the two instances aren’t guaranteed to keep in sync (positions could get off)

  3. If you send the movement data and a packet gets dropped, things get off

So, a combination of 1 and 2 might be possible, and I will certainly play around with it, but it might be best if I implement TCP for this kind of thing, which I will also look into doing.

EDIT: After playing with it, I found that having one client do all the physics and another simply draw objects using data it gets from the ‘server’ client is the most accurate physics, with only a slight lag on the ‘client’ client.

@JakAttak what if the you have a host player and a connecting player. Use the whole physics engine from the host’s iPad and allow the connecting player to manipulate it? Send the positions, angles and velocities to physics bodies local to the connecting player, this does mean the connecting player and the host will have different experiences. It shouldn’t be buggy at all, maybe a bit slower to react for the connecting player.

@JakAttak Nice code. Too bad my second iPad is an iPad 1 and I can’t try your code with it. It almost works if I try it just on my iPad Air. I get as far as it showing the 2 balls.