Tutorial:Networking with UDP-TheClient
The complete source for the UDP client
-- to start with, we need to require the 'socket' lib (which is compiled-- into love). socket provides low-level networking features.local socket = require "socket" -- the address and port of the server local address, port = "localhost", 12345 local entity -- entity is what we'll be controlling local updaterate = 0.1 -- how long to wait, in seconds, before requesting an update local world = {} -- the empty world-state local t -- love.load, hopefully you are familiar with it from the callbacks tutorial function love.load() -- first up, we need a udp socket, from which we'll do all -- out networking. udp = socket.udp() -- normally socket reads block until they have data, or a -- certain amout of time passes. -- that doesn't suit us, so we tell it not to do that by setting the -- 'timeout' to zero udp:settimeout(0) -- unlike the server, we'll just be talking to the one machine, -- so we'll "connect" this socket to the server's address and port -- using setpeername. -- -- [NOTE: UDP is actually connectionless, this is purely a convenience -- provided by the socket library, it doesn't actually change the --'bits on the wire', and in-fact we can change / remove this at any time.] udp:setpeername(address, port) -- seed the random number generator, so we don't just get the -- same numbers each time. math.randomseed(os.time()) -- entity will be what we'll be controlling, for the sake of this -- tutorial its just a number, but it'll do. -- we'll just use random to give us a reasonably unique identity for little effort. -- -- [NOTE: random isn't actually a very good way of doing this, but the -- "correct" ways are beyond the scope of this article. the *simplest* -- is just an auto-count, they get a *lot* more fancy from there on in] entity = tostring(math.random(99999)) -- Here we do our first bit of actual networking: -- we set up a string containing the data we want to send (using 'string.format') -- and then send it using 'udp.send'. since we used 'setpeername' earlier -- we don't even have to specify where to send it. -- -- thats...it, really. the rest of this is just putting this context and practical use. local dg = string.format("%s %s %d %d", entity, 'at', 320, 240) udp:send(dg) -- the magic line in question. -- t is just a variable we use to help us with the update rate in love.update. t = 0 -- (re)set t to 0 end -- love.update, hopefully you are familiar with it from the callbacks tutorial function love.update(deltatime) t = t + deltatime -- increase t by the deltatime -- its *very easy* to completely saturate a network connection if you -- aren't careful with the packets we send (or request!), we hedge -- our chances by limiting how often we send (and request) updates. -- -- for the record, ten times a second is considered good for most normal -- games (including many MMOs), and you shouldn't ever really need more -- than 30 updates a second, even for fast-paced games. if t > updaterate then -- we could send updates for every little move, but we consolidate -- the last update-worth here into a single packet, drastically reducing -- our bandwidth use. local x, y = 0, 0 if love.keyboard.isDown('up') then y=y-(20*t) end if love.keyboard.isDown('down') then y=y+(20*t) end if love.keyboard.isDown('left') then x=x-(20*t) end if love.keyboard.isDown('right') then x=x+(20*t) end -- again, we prepare a packet *payload* using string.format, -- then send it on its way with udp:send -- this one is the move update mentioned above local dg = string.format("%s %s %f %f", entity, 'move', x, y) udp:send(dg) -- and again! this is a require that the server send us an update for -- the world state -- -- [NOTE: in most designs you don't request world-state updates, you -- just get them sent to you periodically. theres various reasons for -- this, but theres one *BIG* one you will have to solemnly take note -- of: 'anti-griefing'. World-updates are probably one of biggest things -- the average game-server will pump out on a regular basis, and greifing -- with forged update requests would be simple effective. so they just -- don't support update requests, instead giving them out when they feel -- its appropriate] local dg = string.format("%s %s $", entity, 'update') udp:send(dg) t=t-updaterate -- set t for the next round end -- there could well be more than one message waiting for us, so we'll -- loop until we run out! repeat -- and here is something new, the much anticipated other end of udp:send! -- receive return a waiting packet (or nil, and an error message). -- data is a string, the payload of the far-end's send. we can deal with it -- the same ways we could deal with any other string in lua (needless to -- say, getting familiar with lua's string handling functions is a must. data, msg = udp:receive() if data then -- you remember, right? that all values in lua evaluate as true, save nil and false? -- match is our freind here, its part of string.*, and data is -- (or should be!) a string. that funky set of characters bares some -- explanation, though. -- (need summary of patterns, and link to section 5.4.1) ent, cmd, parms = data:match("^(%S*) (%S*) (.*)") if cmd == 'at' then -- more patterns, this time with sets, and more length selectors! local x, y = parms:match("^(%-?[%d.e]*) (%-?[%d.e]*)$") assert(x and y) -- validation is better, but asserts will serve. -- don't forget, even if you matched a "number", the result is still a string! -- thankfully conversion is easy in lua. x, y = tonumber(x), tonumber(y) -- and finally we stash it away world[ent] = {x=x, y=y} else -- this case shouldn't trigger often, but its always a good idea -- to check (and log!) any unexpected messages and events. -- it can help you find bugs in your code...or people trying to hack the server. -- never forget, you can not trust the client! print("unrecognised command:", cmd) end -- if data was nil, then msg will contain a short description of the -- problem (which are also error id...). -- the most common will be 'timeout', since we settimeout() to zero, -- anytime there isn't data *waiting* for us, it'll timeout. -- -- but we should check to see if its a *different* error, and act accordingly. -- in this case we don't even try to save ourselves, we just error out. elseif msg ~= 'timeout' then error("Network error: "..tostring(msg)) end until not data end -- love.draw, hopefully you are familiar with it from the callbacks tutorial function love.draw() -- pretty simple, we just loop over the world table, and print the -- name (key) of everything in their, at its own stored co-ords. for k, v in pairs(world) do love.graphics.print(k, v.x, v.y) end end -- And thats the end of the udp client example.