skynet开发一个猜数字游戏

news2025/2/2 0:46:11

skynet开发一个猜数字游戏

  • 游戏简介
  • 接口设计和实现
    • agent服务接口
    • room服务接口
    • hall服务接口
    • redis服务
    • gate服务接口
  • 编写skynet的config文件
  • 游戏演示
  • 总结
  • 后言

游戏简介

猜数字游戏目的是掌握 actor 模型开发思路。

规则:

  1. 满三个人开始游戏,游戏开始后不能退出,直到游戏结束。
  2. 系统会随机生成1-100 之间的数字,玩家依次猜规则内的数字。
  3. 玩家猜测正确,那么该玩家就输了;如果猜测错误,游戏继续。
  4. 直到有玩家猜测成功,游戏结束。
server
TCP
TCP
TCP
hall
agent
gate
agent
agent
room
client
client
client

游戏设计时,首先是简单可用,然后持续优化,而不是一开始就过度优化。

接口设计和实现

skynet 中,从 actor 底层看是通过消息进行通信;从 actor 应用层看是通过 api 来进行通信。

遵循接口隔离原则:

  1. 不应该强迫客户依赖于他们不用的方法。
  2. 从安全封装的角度出发,只暴露客户需要的接口。
  3. 服务间不依赖彼此的实现。

agent服务接口

agent服务主要是用户。具有如下功能:

  1. login:实现登录功能;断线重连。
  2. ready:准备,转发到大厅,加入匹配队列。
  3. guess:猜测数字,转发到房间。
  4. help:列出所有操作说明。
  5. quit:退出。
local skynet = require "skynet"
local socket = require "skynet.socket"

local tunpack = table.unpack
local tconcat = table.concat
local select = select

local clientfd, addr = ...
clientfd = tonumber(clientfd)

local hall

local function read_table(result)
	local reply = {}
	for i = 1, #result, 2 do reply[result[i]] = result[i + 1] end
	return reply
end
-- 读取redis的相关信息
local rds = setmetatable({0}, {
    __index = function (t, k)
        if k == "hgetall" then
            t[k] = function (red, ...)
                return read_table(skynet.call(red[1], "lua", k, ...))
            end
        else
            t[k] = function (red, ...)
                return skynet.call(red[1], "lua", k, ...)
            end
        end
        return 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 then
        skynet.call(client.isgame, "lua", "offline", client.name)
    end
    skynet.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 then
        sendto("没有设置用户名或者密码")
        client_quit()
        return
    end
    local ok = rds:exists("role:"..name)
    if not ok then
        local score = 1000
        -- 满足条件唤醒协程,不满足条件挂起协程
        rds:hmset("role:"..name, tunpack({
            "name", name,
            "password", password,
            "score", score,
            "isgame", 0,
        }))
        client.name = name
        client.password = password
        client.score = score
        client.isgame = 0
        client.agent = skynet.self()
    else
        local dbs = rds:hgetall("role:"..name)
        if dbs.password ~= password then
            sendto("密码错误,请重新输入密码")
            return
        end
        client = dbs
        client.fd = clientfd
        client.isgame = tonumber(client.isgame) or 0
        client.agent = skynet.self()
    end
    if client.isgame > 0 then
        ok = pcall(skynet.call, client.isgame, "lua", "online", client)
        if not ok then
            client.isgame = 0
            sendto("请准备开始游戏。。。")
        end
    else
        sendto("请准备开始游戏。。。")
    end
end

function CMD.ready()
    if not client.name then
        sendto("请先登陆")
        return
    end
    if client.isgame and client.isgame > 0 then
        sendto("在游戏中,不能准备")
        return
    end
    
    local ok, msg = skynet.call(hall, "lua", "ready", client)   --发起一个远程调用,调用hall服务的ready
    if not ok then
        sendto(msg)
        return
    end
    client.isgame = ok
    rds:hset("role:"..client.name, "isgame", ok)
end

function CMD.guess(number)
    if not client.name then
        sendto("错误:请先登陆")
        return
    end
    if not client.isgame or client.isgame == 0 then
        sendto("错误:没有在游戏中,请先准备")
        return
    end
    local numb = math.tointeger(number)
    if not numb then
        sendto("错误:猜测时需要提供一个整数而不是 "..number)
        return
    end

    skynet.send(client.isgame, "lua", "guess", client.name, numb)
end

local function game_over()
    client.isgame = 0
    rds:hset("role:"..client.name, "isgame", 0)
end

function CMD.help()
    local params = tconcat({
        "*规则*:猜数字游戏,由系统随机1-100数字,猜中输,未猜中赢。",
        "help: 显示所有可输入的命令;",
        "login: 登陆,需要输入用户名和密码;",
        "ready: 准备,加入游戏队列,满员自动开始游戏;",
        "guess: 猜数字,只能猜1~100之间的数字;",
        "quit: 退出",
    }, "\r\n")
    socket.write(clientfd, params .. "\r\n")
end

function CMD.quit()
    client_quit()
end

--处理数据接受
local function process_socket_events()
    while true do
        local data = socket.readline(clientfd)-- "\n" read = 0,telnet的分隔符是\n
        if not data then
            print("断开网络 "..clientfd)
            client_quit()
            return
        end
        -- 开始解析数据包
        local pms = {}
        for pm in string.gmatch(data, "%w+") do
            pms[#pms+1] = pm
        end
        if not next(pms) then
            sendto("error[format], recv data")
            goto __continue__
        end
        -- 分发命令
        local cmd = pms[1]
        if not CMD[cmd] then
            sendto(cmd.." 该命令不存在")
            CMD.help()
            goto __continue__
        end
        skynet.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" then
            game_over()
        end
    end)
end)

room服务接口

room服务是一个游戏空间。具有如下功能:

  1. start:初始化房间。
  2. online:用户上线,如果用户在游戏中,告知游戏进度。
  3. offline:用户下线,通知房间内其他用户。
  4. guess:猜测数字,推动游戏进程。
local skynet = require "skynet"

local socket = require "skynet.socket"

local CMD = {}

local roles = {}

local redisd

local 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")
end

local function broadcast(msg)
    for _, role in pairs(roles) do
        if role.isonline > 0 then
            sendto(role.fd, msg)
        end
    end
end

function CMD.start(members)
    for _, role in ipairs(members) do
        role.isonline = 1
        roles[role.name] = role
        game.turns[#game.turns+1] = role.name
    end
    game.random_value = math.random(1, 100)
    broadcast(("房间:%d 系统已经随机一个数字"):format(skynet.self()))
    local rv = math.random(1, 1500)
    if rv <= 500 then
        game.user_turn = 1
    elseif rv <= 1000 then
        game.user_turn = 2
    else
        game.user_turn = 3
    end
    local name = game.turns[game.user_turn]
    broadcast(("请玩家%s开始猜数字"):format(name))
end

function CMD.offline(name)
    if roles[name] then
        roles[name].isonline = 0
        broadcast(("%s 玩家已经掉线,请求呼叫他上线"):format(name))
    end
    skynet.retpack()
end

function CMD.online(client)
    local name = client.name
    if roles[name] then
        roles[name] = client
        roles[name].isonline = 1
        broadcast(("%s 玩家已经上线"):format(name))
        sendto(client.fd, ("范围变为 [%d - %d], 接下来由 %s 来操作"):format(game.down_limit, game.up_limit, game.turns[game.user_turn]))
    end
    skynet.retpack()
end

local function game_over()
    for _, role in pairs(roles) do
        if role.isonline == 0 then
            skynet.call(redisd, "hset", "role:"..role.name, "isgame", 0)
        else
            skynet.send(role.agent, "lua", "game_over")
            sendto(role.fd, "离开房间")
        end
    end
    skynet.fork(skynet.exit)
end

function CMD.guess(name, val)
    local role = assert(roles[name])
    if game.turns[game.user_turn] ~= name then
        sendto(role.fd, ("错误:还没轮到你操作,现在由 %s 来操作"):format(game.turns[game.user_turn]))
        return
    end
    if not val or val < game.down_limit or val > game.up_limit then
        sendto(role.fd, ("错误:请输入[%d - %d]之间的数字"):format(game.down_limit, game.up_limit))
        return
    end
    game.user_turn = game.user_turn % 3+1
    local next = game.turns[game.user_turn]
    if val == game.random_value then
        broadcast(("游戏结束,%s猜中了数字%d,输了"):format(name, val))
        game_over()
        return
    end
    if val < game.random_value then
        game.down_limit = val+1
        if game.down_limit == game.up_limit then
            broadcast(("游戏结束,只剩下一个数字%d %s输了"):format(val+1, next))
            game_over()
            return
        end
        broadcast(("%s输入的数字太小,范围变为 [%d - %d], 接下来由 %s 来操作"):format(name, game.down_limit, game.up_limit, next))
        return
    end
    if val > game.random_value then
        game.up_limit = val-1
        if game.down_limit == game.up_limit then
            broadcast(("游戏结束,只剩下一个数字%d %s输了"):format(val-1, next))
            game_over()
            return
        end
        broadcast(("%s输入的数字太大,范围变为 [%d - %d], 接下来由 %s 来操作"):format(name, game.down_limit, game.up_limit, next))
        return
    end
end

skynet.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 then
            return
        end
        func(...)
    end)
end)


hall服务接口

hall服务类似《斗地主》的大厅。具有如下功能:

  1. ready:加入匹配队列。
  2. 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")
end

function CMD.ready(client)
    if not client or not client.name then
        return skynet.retpack(false, "准备:非法操作")
    end
    if resps[client.name] then
        return skynet.retpack(false, "重复准备")
    end
    tinsert(queues, 1, client)
    resps[client.name] = skynet.response()
    if #queues >= 3 then
        local roomd = skynet.newservice("room") 
        local members = {tremove(queues), tremove(queues), tremove(queues)}
        for i=1, 3 do
            local cli = members[i]
            resps[cli.name](true, roomd)
            resps[cli.name] = nil
        end
        skynet.send(roomd, "lua", "start", members)
        return
    end
    sendto(client.fd, "等待其他玩家加入")
end

function CMD.offline(name)
    for pos, client in ipairs(queues) do
        if client.name == name then
            tremove(queues, pos)
            break
        end
    end
    if resps[name] then
        resps[name](true, false, "退出")
        resps[name] = nil
    end
    skynet.retpack()
end

skynet.start(function ()
    --  消息路由
    skynet.dispatch("lua", function(session, address, cmd, ...)
        local func = CMD[cmd]
        if not func then
            skynet.retpack({ok = false, msg = "非法操作"})
            return
        end
        cs(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服务是网关,主要处理网络连接,也是游戏的入口函数文件。具有如下功能:

  1. 绑定网络连接,推送信息到agent服务。
  2. 进入redis服务。

实现:
main.lua

local skynet = require "skynet" 
local socket = require "skynet.socket"

local function accept(clientfd,addr)
    skynet.newservice("agent",clientfd,addr)--创建一个agent服务(lua虚拟机)
end

skynet.start(function()
    -- body
    local 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

guess_game

总结

  1. 这些服务接口还可以进一步进行优化,比如 agent 服务可以不要实时创建而是采用预先创建、如果某个服务相对简单,可以创建固定数量。
  2. 如果是万人同时在线游戏,agent、room 需要预先分配,长时间运行会让服务内存膨胀,同时也会造成 luagc 负担会加重。

后言

后言
本专栏知识点是通过<零声教育>的系统学习,进行梳理总结写下文章,对c/c++linux系统提升感兴趣的读者,可以点击链接查看详细的服务:C/C++服务器开发 。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/104257.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

1.初识Node.js

由于浏览器中有Javascript解析引擎&#xff0c;所以写的javascript可以在浏览器中执行&#xff0c;不同的浏览器有不同的JS解析引擎。由于浏览器内置了DOM,BOM,AJAX这种API&#xff0c;所以JS才能使用他们。 Node.js和浏览器都可以为JS提供运行环境&#xff0c;可以使用Node.j…

【Python机器学习】神经网络中误差反向传播(BP)算法详解及代码示例(图文解释 附源码)

需要全部代码请点赞关注收藏后评论留言私信~~~ 误差反向传播学习算法 用神经网络来完成机器学习任务&#xff0c;先要设计好网络结构S&#xff0c;然后用训练样本去学习网络中的连接系数和阈值系数&#xff0c;即网络参数S&#xff0c;最后才能用来对测试样本进行预测。 在研…

智能驾驶进入新周期:从「定点量产」到「做大做强」

智能驾驶赛道已经进入一个全新的周期。 过去三年时间&#xff0c;中国本土供应商陆续进入乘用车前装量产赛道&#xff0c;部分企业实现了从0到1的量产突围&#xff0c;而一些领跑的本土供应商已率先进入规模化上车的新阶段。 从最初的技术突破、产品落地&#xff0c;到定点量…

干货 | IC模拟版图设计学习笔记,一文教你快速入门

模拟版图设计处于IC设计流程的后端&#xff0c;属于模拟IC设计岗位的一种。随着我国半导体行业的发展&#xff0c;IC模拟版图岗位的人才需求也越来越大。而每个芯片最终能够付诸于生产都离不开集成电路版图设计师的功劳&#xff0c;所以IC模拟版图工程师在芯片产业的发展过程中…

Nginx-反向代理

什么是反向代理 用户直接访问反向代理服务器就可以获得目标服务器的资源。这一过程叫反向代理 如何配置反向代理 修改nginx配置文件 1.切换到nginx的conf路径下操作nginx的配置文件 cd /usr/local/openresty/nginx/conf 1.1防止修改错误可以先备份一下配置文件 cp nginx.…

IO流2.0 缓冲流 序列化 字符打印 printf格式 压缩流

04 缓冲流Buffered 4.1字节缓冲流 4.2 字符缓冲流 newLine();在底层自动识别操作系统类别和换行符 其实就是输出换行符; 4.2.1 BufferedReader&#xff08;字符为单位 尽量用于文本文件&#xff09; 不要去操作 二进制文件[声音&#xff0c;视频 ], 可能造成文件损坏 throw…

三个月转行SLAM,他的心路历程

SLAM入门心路历程 来源&#xff1a;知乎作者—莫慢待 读了三年985的计算机硕士&#xff0c;做了很多项目&#xff0c;发现自己是一个菜鸡。c/object-c/python/java/matlab甚至还写过R&#xff0c;也就是项目需要什么写什么。自学能力算是很不错了&#xff0c;第一次创业自己写…

juc-2-锁应用/线程通信

目录 1 线程安全(库存超卖) 2 锁用法 2.1 同步方法 2.2.同步代码块 2.3 synchronized 作用于静态方法 总结 案例 静态成员变量 (synchronized锁非静态方法) 2.4ReentrantLock类是可重入、互斥、实现了Lock接口的锁 3 死锁产生与排查 4 线程间的&#xff08;等待与通知…

Hadoop实训任务3:HDFS和MapReduce综合操作

目录 1、启动Hadoop服务 2、创建文本文件 3、上传文本文件 4、显示文件内容 5、完成排序任务 6、计算最大利润和平均利润 7、统计学生总成绩和平均成绩 8、总结 1、启动Hadoop服务 在master虚拟机上执行命令&#xff1a; start-all.sh 启动hadoop服务进程 ​ ​ ​…

【嵌入式】构建嵌入式Linux系统(uboot、内核、文件系统)

知识架构及层次 Linux内核由三部分构成&#xff1a; Bootloader&#xff1a;启动引导系统&#xff08;可执行文件&#xff09;Kernel&#xff1a;内核&#xff08;可执行文件&#xff09;Root File System&#xff1a;根文件系统嵌入式Linux系统构成及启动略析 嵌入式 Linux …

Java八股文,YYDS!字节最新秋招面试题,“死记”这些你也可以斩获字节offer

Java 一面基本上都是基础题&#xff0c;同样是 CURD 的活&#xff0c;谁更熟练要谁&#xff0c;比如下面这些面试题&#xff0c;八股文越熟练越容易通过 Java 面试。 下面会给大家详解的介绍一下每个技术点的必问问题&#xff01; 一、Java 基础 1.JDK动态代理和CGLIB动态代…

C++图像的形态学操作

什么是图像的形态学操作&#xff1f; 使用数学形态学的基本运算&#xff0c;由计算机对图像进行分析&#xff0c;以达到所需结果的一种技术。 形态学&#xff0c;morphology, 形态学最初是生物学中研究动物和植物结构的一个分支&#xff0c;被引入图像处理领域后&#xff0c;图…

C语言 期末合集 pta

6-1 求最大的偶数和最小的奇数的差 分数 10 全屏浏览题目 切换布局 作者 王跃萍 单位 东北石油大学 求10个数中的最大的偶数和最小的奇数的差。 函数接口定义&#xff1a; int fun(int a[],int n); 函数fun中a、n是用户传入的参数&#xff0c;函数fun的功能是求a数组中的…

【LVGL学习笔记】(五)使用SquareLine Studio设计UI

LVGL全程LittleVGL&#xff0c;是一个轻量化的&#xff0c;开源的&#xff0c;用于嵌入式GUI设计的图形库。并且配合LVGL模拟器&#xff0c;可以在电脑对界面进行编辑显示&#xff0c;测试通过后再移植进嵌入式设备中&#xff0c;实现高效的项目开发。 LVGL中文教程手册&#…

点云地图构建及定位

点云地图构建及定位1. 回环检测1.1 基于Scan Context1.2 基于直方图2. 后端优化2.1 后端优化基本原理2.2 李群、李代数基本知识2.3 李群、李代数基本知识2.3.1 SO(3)\mathrm{SO}(3)SO(3) 对应的 BCH\mathrm{BCH}BCH 公式2.3.2 SE(3)\mathrm{SE}(3)SE(3) 对应的 BCH\mathrm{BCH}…

链式存储之:链表的引出及其简介

上篇博客&#xff0c;笔者讲解了一下顺序表ArrayList&#xff0c;对于ArrayList有想法的各位老铁可以看一下&#xff1a;值得思索的&#xff1a;ArrayList和线性表&#xff0c;你确定错过这次机会_念君思宁的博客-CSDN博客值得思索的&#xff1a;ArrayList和线性表&#xff0c;…

Html5 canvas创意特效合集

Canvas就像一块画布&#xff0c;我们可以通过调用脚本在Canvas上绘制任意形状&#xff0c;甚至是制作动画。本文就是收集了很多非常富有创意的一些canvas动画特效例子&#xff0c;这些例子都非常适合大家学习。 1.3D篝火动画特效 这款篝火特效是基于 three.js 和 canvas 制作的…

博实结将在创业板上会:计划募资25亿元,周小强为实际控制人

近日&#xff0c;深圳市博实结科技股份有限公司&#xff08;下称“博实结”&#xff09;在深圳证券交易所递交招股书&#xff08;上会稿&#xff09;&#xff0c;准备在创业板上市。据贝多财经了解&#xff0c;博实结将于2022年12月23日接受创业板上市委审议&#xff0c;一同上…

Vue3----props和emit的使用

作用&#xff1a;父组件通过 props 向下传递数据给子组件&#xff1b; 用途&#xff1a;当有一种类型的组件需要被使用多次&#xff0c;每一次的调用都只是特定的地方不同&#xff0c;就好像一张个人简介表&#xff0c;每次填的人的信息都不同&#xff0c;但是结构都是一样的。…

Nacos学习笔记 (2)Nacos基础核心特性

内容&#xff1a; Nacos架构及其核心特性&#xff0c;包括服务注册、服务发现、发布与获取配置特性&#xff0c;以及Nacos Spring关键特性。 本文内容&#xff0c;参考 Nacos 官网与 《Nacos架构&原理》一书。 1. Nacos 架构 详情推荐参考&#xff1a; &#xff08;1&a…