skynet开发一个猜数字游戏
- 游戏简介
- 接口设计和实现
- agent服务接口
- room服务接口
- hall服务接口
- redis服务
- gate服务接口
- 编写skynet的config文件
- 游戏演示
- 总结
游戏简介
猜数字游戏目的是掌握 actor 模型开发思路。
规则:
- 满三个人开始游戏,游戏开始后不能退出,直到游戏结束。
- 系统会随机生成1-100 之间的数字,玩家依次猜规则内的数字。
- 玩家猜测正确,那么该玩家就输了;如果猜测错误,游戏继续。
- 直到有玩家猜测成功,游戏结束。
游戏设计时,首先是简单可用,然后持续优化,而不是一开始就过度优化。
接口设计和实现
skynet 中,从 actor 底层看是通过消息进行通信;从 actor 应用层看是通过 api 来进行通信。
遵循接口隔离原则:
- 不应该强迫客户依赖于他们不用的方法。
- 从安全封装的角度出发,只暴露客户需要的接口。
- 服务间不依赖彼此的实现。
agent服务接口
agent服务主要是用户。具有如下功能:
- login:实现登录功能;断线重连。
- ready:准备,转发到大厅,加入匹配队列。
- guess:猜测数字,转发到房间。
- help:列出所有操作说明。
- quit:退出。
local skynet = require "skynet"
local socket = require "skynet.socket"local tunpack = table.unpack
local tconcat = table.concat
local select = selectlocal clientfd, addr = ...
clientfd = tonumber(clientfd)local halllocal function read_table(result)local reply = {}for i = 1, #result, 2 do reply[result[i]] = result[i + 1] endreturn reply
end
-- 读取redis的相关信息
local rds = setmetatable({0}, {__index = function (t, k)if k == "hgetall" thent[k] = function (red, ...)return read_table(skynet.call(red[1], "lua", k, ...))endelset[k] = function (red, ...)return skynet.call(red[1], "lua", k, ...)endendreturn t[k]end
})local client = {fd = clientfd}
local CMD = {}local function client_quit()skynet.call(hall, "lua", "offline", client.name)if client.isgame and client.isgame > 0 thenskynet.call(client.isgame, "lua", "offline", client.name)endskynet.fork(skynet.exit) --强制关闭进程,退出
end-- 发送信息
local function sendto(arg)-- local ret = tconcat({"fd:", clientfd, arg}, " ")-- socket.write(clientfd, ret .. "\n")socket.write(clientfd, arg .. "\r\n")
end-- 用户登录
function CMD.login(name, password)if not name and not password thensendto("没有设置用户名或者密码")client_quit()returnendlocal ok = rds:exists("role:"..name)if not ok thenlocal score = 1000-- 满足条件唤醒协程,不满足条件挂起协程rds:hmset("role:"..name, tunpack({"name", name,"password", password,"score", score,"isgame", 0,}))client.name = nameclient.password = passwordclient.score = scoreclient.isgame = 0client.agent = skynet.self()elselocal dbs = rds:hgetall("role:"..name)if dbs.password ~= password thensendto("密码错误,请重新输入密码")returnendclient = dbsclient.fd = clientfdclient.isgame = tonumber(client.isgame) or 0client.agent = skynet.self()endif client.isgame > 0 thenok = pcall(skynet.call, client.isgame, "lua", "online", client)if not ok thenclient.isgame = 0sendto("请准备开始游戏。。。")endelsesendto("请准备开始游戏。。。")end
endfunction CMD.ready()if not client.name thensendto("请先登陆")returnendif client.isgame and client.isgame > 0 thensendto("在游戏中,不能准备")returnendlocal ok, msg = skynet.call(hall, "lua", "ready", client) --发起一个远程调用,调用hall服务的readyif not ok thensendto(msg)returnendclient.isgame = okrds:hset("role:"..client.name, "isgame", ok)
endfunction CMD.guess(number)if not client.name thensendto("错误:请先登陆")returnendif not client.isgame or client.isgame == 0 thensendto("错误:没有在游戏中,请先准备")returnendlocal numb = math.tointeger(number)if not numb thensendto("错误:猜测时需要提供一个整数而不是 "..number)returnendskynet.send(client.isgame, "lua", "guess", client.name, numb)
endlocal function game_over()client.isgame = 0rds:hset("role:"..client.name, "isgame", 0)
endfunction CMD.help()local params = tconcat({"*规则*:猜数字游戏,由系统随机1-100数字,猜中输,未猜中赢。","help: 显示所有可输入的命令;","login: 登陆,需要输入用户名和密码;","ready: 准备,加入游戏队列,满员自动开始游戏;","guess: 猜数字,只能猜1~100之间的数字;","quit: 退出",}, "\r\n")socket.write(clientfd, params .. "\r\n")
endfunction CMD.quit()client_quit()
end--处理数据接受
local function process_socket_events()while true dolocal data = socket.readline(clientfd)-- "\n" read = 0,telnet的分隔符是\nif not data thenprint("断开网络 "..clientfd)client_quit()returnend-- 开始解析数据包local pms = {}for pm in string.gmatch(data, "%w+") dopms[#pms+1] = pmendif not next(pms) thensendto("error[format], recv data")goto __continue__end-- 分发命令local cmd = pms[1]if not CMD[cmd] thensendto(cmd.." 该命令不存在")CMD.help()goto __continue__endskynet.fork(CMD[cmd], select(2, tunpack(pms)))
::__continue__::end
end
-- 开始agent服务
skynet.start(function ()print("recv a connection:", clientfd, addr)rds[1] = skynet.uniqueservice("redis") --进入redis服务hall = skynet.uniqueservice("hall") -- 进入hall服务socket.start(clientfd) -- 绑定 clientfd agent 网络消息skynet.fork(process_socket_events) --创建协程,处理数据接受skynet.dispatch("lua", function (_, _, cmd, ...)if cmd == "game_over" thengame_over()endend)
end)
room服务接口
room服务是一个游戏空间。具有如下功能:
- start:初始化房间。
- online:用户上线,如果用户在游戏中,告知游戏进度。
- offline:用户下线,通知房间内其他用户。
- guess:猜测数字,推动游戏进程。
local skynet = require "skynet"local socket = require "skynet.socket"local CMD = {}local roles = {}local redisdlocal game = {random_value = 0,user_turn = 0,up_limit = 100,down_limit = 1,turns = {},
}local function sendto(clientfd, arg)socket.write(clientfd, arg .. "\r\n")
endlocal function broadcast(msg)for _, role in pairs(roles) doif role.isonline > 0 thensendto(role.fd, msg)endend
endfunction CMD.start(members)for _, role in ipairs(members) dorole.isonline = 1roles[role.name] = rolegame.turns[#game.turns+1] = role.nameendgame.random_value = math.random(1, 100)broadcast(("房间:%d 系统已经随机一个数字"):format(skynet.self()))local rv = math.random(1, 1500)if rv <= 500 thengame.user_turn = 1elseif rv <= 1000 thengame.user_turn = 2elsegame.user_turn = 3endlocal name = game.turns[game.user_turn]broadcast(("请玩家%s开始猜数字"):format(name))
endfunction CMD.offline(name)if roles[name] thenroles[name].isonline = 0broadcast(("%s 玩家已经掉线,请求呼叫他上线"):format(name))endskynet.retpack()
endfunction CMD.online(client)local name = client.nameif roles[name] thenroles[name] = clientroles[name].isonline = 1broadcast(("%s 玩家已经上线"):format(name))sendto(client.fd, ("范围变为 [%d - %d], 接下来由 %s 来操作"):format(game.down_limit, game.up_limit, game.turns[game.user_turn]))endskynet.retpack()
endlocal function game_over()for _, role in pairs(roles) doif role.isonline == 0 thenskynet.call(redisd, "hset", "role:"..role.name, "isgame", 0)elseskynet.send(role.agent, "lua", "game_over")sendto(role.fd, "离开房间")endendskynet.fork(skynet.exit)
endfunction CMD.guess(name, val)local role = assert(roles[name])if game.turns[game.user_turn] ~= name thensendto(role.fd, ("错误:还没轮到你操作,现在由 %s 来操作"):format(game.turns[game.user_turn]))returnendif not val or val < game.down_limit or val > game.up_limit thensendto(role.fd, ("错误:请输入[%d - %d]之间的数字"):format(game.down_limit, game.up_limit))returnendgame.user_turn = game.user_turn % 3+1local next = game.turns[game.user_turn]if val == game.random_value thenbroadcast(("游戏结束,%s猜中了数字%d,输了"):format(name, val))game_over()returnendif val < game.random_value thengame.down_limit = val+1if game.down_limit == game.up_limit thenbroadcast(("游戏结束,只剩下一个数字%d %s输了"):format(val+1, next))game_over()returnendbroadcast(("%s输入的数字太小,范围变为 [%d - %d], 接下来由 %s 来操作"):format(name, game.down_limit, game.up_limit, next))returnendif val > game.random_value thengame.up_limit = val-1if game.down_limit == game.up_limit thenbroadcast(("游戏结束,只剩下一个数字%d %s输了"):format(val-1, next))game_over()returnendbroadcast(("%s输入的数字太大,范围变为 [%d - %d], 接下来由 %s 来操作"):format(name, game.down_limit, game.up_limit, next))returnend
endskynet.start(function ()math.randomseed(math.tointeger(skynet.time()*100), skynet.self()) --生成随机数redisd = skynet.uniqueservice("redis") --进入redis服务skynet.dispatch("lua", function (_, _, cmd, ...)local func = CMD[cmd]if not func thenreturnendfunc(...)end)
end)
hall服务接口
hall服务类似《斗地主》的大厅。具有如下功能:
- ready:加入匹配队列。
- offline:用户掉线,需要从匹配队列移除用户。
local skynet = require "skynet"
local queue = require "skynet.queue"
local socket = require "skynet.socket"local cs = queue()local tinsert = table.insert
local tremove = table.remove
-- local tconcat = table.concat
local CMD = {}local queues = {}local resps = {}local function sendto(clientfd, arg)-- local ret = tconcat({"fd:", clientfd, arg}, " ")-- socket.write(clientfd, ret .. "\n")socket.write(clientfd, arg .. "\r\n")
endfunction CMD.ready(client)if not client or not client.name thenreturn skynet.retpack(false, "准备:非法操作")endif resps[client.name] thenreturn skynet.retpack(false, "重复准备")endtinsert(queues, 1, client)resps[client.name] = skynet.response()if #queues >= 3 thenlocal roomd = skynet.newservice("room") local members = {tremove(queues), tremove(queues), tremove(queues)}for i=1, 3 dolocal cli = members[i]resps[cli.name](true, roomd)resps[cli.name] = nilendskynet.send(roomd, "lua", "start", members)returnendsendto(client.fd, "等待其他玩家加入")
endfunction CMD.offline(name)for pos, client in ipairs(queues) doif client.name == name thentremove(queues, pos)breakendendif resps[name] thenresps[name](true, false, "退出")resps[name] = nilendskynet.retpack()
endskynet.start(function ()-- 消息路由skynet.dispatch("lua", function(session, address, cmd, ...)local func = CMD[cmd]if not func thenskynet.retpack({ok = false, msg = "非法操作"})returnendcs(func, ...) --开始执行end)
end)
redis服务
用于保存玩家名称、密码、分数、游戏状态等信息。
开启redis服务,redis的键值对、数据结构操作在agent服务进行。
local skynet = require "skynet.manager"
local redis = require "skynet.db.redis"
-- 连接redis
skynet.start(function ()local rds = redis.connect({host = "127.0.0.1",port = 6379,db = 0,-- auth = "123456",})skynet.dispatch("lua", function (session, address, cmd, ...)skynet.retpack( rds[cmd:lower()](rds, ...) )end)
end)
gate服务接口
gate服务是网关,主要处理网络连接,也是游戏的入口函数文件。具有如下功能:
- 绑定网络连接,推送信息到agent服务。
- 进入redis服务。
实现:
main.lua
local skynet = require "skynet"
local socket = require "skynet.socket"local function accept(clientfd,addr)skynet.newservice("agent",clientfd,addr)--创建一个agent服务(lua虚拟机)
endskynet.start(function()-- bodylocal listenfd=socket.listen("0.0.0.0",8888)skynet.uniqueservice("redis")skynet.uniqueservice("hall")socket.start(listenfd,accept) --绑定listenfd到accept函数
end)
编写skynet的config文件
编写的game代码放在app目录下。
thread=4 --工作线程
logger=nil
harbor=0
start="main" -- 启动第一个服务
lua_path="./skynet/lualib/?.lua;".."./skynet/lualib/?/init.lua;".."./lualib/?.lua;"
luaservice="./skynet/service/?.lua;./app/?.lua"
lualoader="./skynet/lualib/loader.lua"
cpath="./skynet/cservice/?.so"
lua_cpath="./skynet/luaclib/?.so"
游戏演示
(1)服务端:
先启动 redis,然后启动 skynet。
redis-server redis.conf
./skynet/skynet config
(2)客户端:使用telnet。
telnet <IP> 8888
总结
- 这些服务接口还可以进一步进行优化,比如 agent 服务可以不要实时创建而是采用预先创建、如果某个服务相对简单,可以创建固定数量。
- 如果是万人同时在线游戏,agent、room 需要预先分配,长时间运行会让服务内存膨胀,同时也会造成 luagc 负担会加重。