此文简单翻译自官方教程,由于涉及了网络编程,我也不熟,可以先看这篇socket的文章。
love2d已经把lua的网络库luasocket编译进去了,所以只需要简单的require "socket"就可。
下面我们实现一个love2d的客户端和一个纯lua的服务端(都可以直接用love运行,先运行
服务端再运行客户端,如果服务端假死不用管。开启多个客户端后,可以在客户端上看到
一些数字,使用方向键可以移动当前客户端的数字,其它客户端上相应的数字也跟着运动)
love2d的wiki上没有socket的文档,需要自行查看,这里是luasocke的文档。
客户端
导入socket,设置一些变量。
local socket = require "socket" --调用socket库 -- 服务端的ip地址和端口,localhost=127.0.0.1(本机) local address, port = "localhost", 12345 local entity --一个随机数,标示每个客户端 local updaterate = 0.1 -- 更新速率0.1s一次 local world = {} --里面存放的是键值对 world[实体]={x,y} local t --计时
首先,在load里我们和服务端连接上,并产生一个随机数来作为客户端的id(entity ),
之后发送一条消息给服务器。
function love.load() -- 创建一个没有连接的udp对象,有了它我们就可以使用网络了,若失败则返回nil和错误消息 udp = socket.udp() -- socket按块来读取数据,会产生阻塞直到数据里有信息为止,或者等待一段时间 -- 这显然不符合游戏的实时的要求,所以把等待时间设为0 udp:settimeout(0) --不像服务端,客户端只需要连接服务端就可,使用setpeername来连接服务端 --address是地址,port端口 udp:setpeername(address, port) --取随机数种子 math.randomseed(os.time()) --通过刚才的随机数种子生成0---99999之间的随机数 --entity实际就是一个字符串 entity = tostring(math.random(99999)) --现在开始使用网络,这里我们仅是产生一个字符串dg,并把它用send发送出去 --此处发送的是 “entity at 320240” local dg = string.format("%s %s %d %d", entity, 'at', 320, 240) udp:send(dg) -- 初始化t为0,t用来在love.update里计时 t = 0 end
在update里检测键盘的按下,并每隔一段时间把键盘状态发送到服务端,然后
接收来自服务端的消息,解析后放到world表里。
function love.update(deltatime) t = t + deltatime --为了防止网络堵塞,我们需要限制更新速率,对大多数游戏来说每秒10次已经足够 --(包括很多大型在线网络游戏),更新速率不要超过每秒30次 if t > updaterate then --可以每次更新都发送数据包,但为了减少带宽,我们把更新整合到一个数据包里,在 --最后的更新里发送出去 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 --把消息打包到dg,发送出去,这里发送的是 entity,move和坐标拼接的字符串 local dg = string.format("%s %s %f %f", entity, 'move', x, y) udp:send(dg) --服务器发送给我们世界更新请求 --[[ 注意:大多数设计不需要更新世界状态,而是让服务器定期发送。 这样做有很多原因,你需要仔细注意的一个是anti-griefing(反扰乱)。 世界更新是游戏服务器最大的事,服务器会定期更新,使用整合的数据将会更有效。 ]] local dg = string.format("%s %s $", entity, 'update') udp:send(dg) t=t-updaterate -- 复位t end --很可能有许多消息,因此循环来等待消息 repeat --[[这里期望另一端的udp:send! udp:receive将返回等待数据包 (或为nil,或错误消息)。 数据是一个字符串,承载远端udp:send的内容。我们可以使用lua的string库处理 ]] data, msg = udp:receive() if data then --这里的match是string.match,它使用参数中的模式来匹配 --下面匹配以空格分隔的字符串 ent, cmd, parms = data:match("^(%S*) (%S*) (.*)") if cmd == 'at' then --匹配如下形式的"111 222"的数字 local x, y = parms:match("^(%-?[%d.e]*) (%-?[%d.e]*)$") assert(x and y) -- 使用assert验证x,y是否都不为nil --不要忘记x,y还是字符串类型 x, y = tonumber(x), tonumber(y) --把x,y存入world表里 world[ent] = {x=x, y=y} else --[[ 打印日志,防止有人黑服务器,永远不要信任客户端 ]] print("unrecognised command:", cmd) end --[[ 打印错误,一般情况下错误是timeout,由于我们把timeout设为0了, ]] elseif msg ~= 'timeout' then error("Network error: "..tostring(msg)) end until not data end
draw则比较简单,只是在屏幕上x,y处打印所有的客户端entity
function love.draw() --打印world里的信息 for k, v in pairs(world) do love.graphics.print(k, v.x, v.y) print(k) end end
服务端
服务端只是一个纯lua文件,并不在love里运行(其实也可以,如果使用lua运行,在win下安装lua for windows后即可
linux下需要自己编译)。下面这几行和客户端类似。
local socket = require "socket" local udp = socket.udp() udp:settimeout(0)
-- 和客户端不同,服务器必须知道它绑定的端口,否则客户端将永远找不到它。
--绑定主机地址和端口。
--“×”则表示所有地址;端口为数字(0----65535)。
--由于0----1024是某些系统保留端口,请使用大于1024的端口。
udp:setsockname('*', 12345)
因为我们并不知道客户端来自哪里,所以需要监听相应端口来自所有ip地址的消息。
下面这些参数和客户端相同
local world = {}
local data, msg_or_ip, port_or_nil
local entity, cmd, parms
服务端当然得始终运行,所以我们使用无限循环,其实love也是一个无限循环。
local running = true
print "Beginning server loop."
while running do
udp:receivefrom() 和udp:receive()类似但它返回数据、发送者的ip地址、发送者的端口
(我们需要这些信息来回复)。我们在客户端里没这么做,主要原因是已经把端口绑定到服
务端。(必须成对使用receivefrom/sendto、receive/send)
data, msg_or_ip, port_or_nil = udp:receivefrom()
下面进行数据检测,按照客服端发过来的指令进行处理
if data then entity, cmd, parms = data:match("^(%S*) (%S*) (.*)") if cmd == 'move' then local x, y = parms:match("^(%-?[%d.e]*) (%-?[%d.e]*)$") assert(x and y) -- 验证x,y是否都不为nil --记得x,y还是字符串,要转换为数字 x, y = tonumber(x), tonumber(y) -- local ent = world[entity] or {x=0, y=0} world[entity] = {x=ent.x+x, y=ent.y+y} elseif cmd == 'at' then local x, y = parms:match("^(%-?[%d.e]*) (%-?[%d.e]*)$") assert(x and y) x, y = tonumber(x), tonumber(y) world[entity] = {x=x, y=y} elseif cmd == 'update' then for k, v in pairs(world) do --发送给客户端 udp:sendto(string.format("%s %s %d %d", k, 'at', v.x, v.y), msg_or_ip, port_or_nil) end elseif cmd == 'quit' then running = false; else print("unrecognised command:", cmd) end elseif msg_or_ip ~= 'timeout' then error("Unknown network error: "..tostring(msg)) end
让cpu休息,减少负载
socket.sleep(0.01)
end
print "Thank you."
这篇教程不易理解,可以会个图把客户端和服务端receivefrom/sendto、receive/send对应起来。
对于socket我知道的也不多,暂时也不想深究,希望高手多多指点。
接下来是角色在地图上的移动。
代码下载(已clone的直接git pull)
git clone git://gitcafe.com/dwdcth/love2d-tutor.git
或git clone https://github.com/dwdcth/mylove2d-tutor-in-chinese.git