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)
			entity, 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[entity] = {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.