此文簡單翻譯自官方教程,由於涉及了網絡編程,我也不熟,可以先看這篇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