love2d教程20--UDP网络连接

此文简单翻译自官方教程,由于涉及了网络编程,我也不熟,可以先看这篇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."

 

这里使用了很多lua模式匹配可以参考文章1文章2

 

这篇教程不易理解,可以会个图把客户端和服务端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

posted @ 2013-03-03 21:28  半山th  阅读(1586)  评论(9编辑  收藏  举报