# IMDT_GameDev **Repository Path**: YicongYuan/IMDT_GameDev ## Basic Information - **Project Name**: IMDT_GameDev - **Description**: IMDT项目23秋季游戏开发基础课程工程 - **Primary Language**: Lua - **License**: MIT - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2023-09-27 - **Last Updated**: 2023-10-05 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # IMDT_GameDev - 袁逸聪 2023214436 ## 快捷运行指南 使用bash脚本启动服务器、客户端、杀死监听特定端口的服务器 启动服务器 > bash server.sh 启动客户端 > bash client.sh 杀掉监听某端口的进程 > bash killserver.sh {port} > e.g: bash killserver.sh 8960 ### 运行流程 1. \> git clone 2. \> cd IMDT_GameDev.git 3. \> bash server.sh 4. 另打开一窗口 1. \> bash client.sh 5. 根据程序提示进行输入 ## 功能说明 ### 登录 启动程序后,会看到如下字样 ```terminal 请输入角色ID ``` 服务器预生成了id为1~10000的角色 1. 输入1~10000的数字,可登录到对应角色 2. 输入其他数字,提示角色不存在 3. 输入非数字,提示输入数字 ### 指令 登录后,会看到如下字样 ```terminal 输入指令进行操作,1:根据ID删除角色,2:输入数字n查询攻击力前n的角色,quit:退出 ``` 1. 输入1 1. 再输入角色id以删除角色 1. 输入非数字,提示输入数字,并立刻重新输入 2. 输入没有对应角色的数字,提示没有对应角色,并退回到选择指令状态 3. 输入存在的角色,执行删除,告知结果,并退回到选择指令状态 1. 可删除当前登录的角色,不影响使用 2. 删除后无法再次登录 2. 输入2 1. 再输入要查询的角色个数 1. 输入非数字,提示输入数字,并立刻重新输入 2. 输入<=0的数字,提示查询的个数需要>0,并退回到选择指令状态 3. 输入>0的数字n,执行查询 1. n<=服务器中已有角色数量,按照角色atk属性从大到小排序返回前n名角色信息 2. n>服务器中已有的角色数量,按照角色atk属性从大到小排序返回服务器所有角色信息 3. 输入quit 1. 退出客户端 ## 部分实现思路 ### 通用Class定义 > /src/class.lua 在此定义了类的通用特性 1. 实例对象链接到类 2. 类链接到父类 3. 用于判断所属&继承关系的InstanceOf方法 ### 非阻塞读写模拟堵塞读写 > /src/util.lua skynet包所提供的读取服务器、终端输入的方法都是非阻塞的 原版工程中,需要不断用循环扫描缓冲区,直到读到内容 在需要连续多次输入时,代码非常难以维护,因此使用非阻塞的方法包装了阻塞的版本,可以简化很多 ```lua -- 阻塞地读取终端输入 local input = readStdinNumber() socket.send(fd, "system-login|" .. input) -- 阻塞地读取服务器返回 receive() local res = executeRes() ``` ### 配置式指令协议 在现有api下,客户端与服务端交流都是收发string 但是双方并不知道对方的状态,需要一些元信息,在指令的内容以外,表示“这是什么指令” 工程中使用了一个通用的指令分发函数,以客户端向服务器发送的指令为例说明 client: ```lua -- 客户端登录时,|左边表示该指令是登录,|右边表示登录所用的id参数,支持多参数,都用|分割即可 socket.send(fd, "system-login|" .. input) ``` server: ```lua -- 与客户端约定的指令表,key为前缀(的正则搜索表达式,-前需要%转义符),value为参数 local commands = {} -- 通用的指令分发方法 local function exec_message(strin, id) -- 逐个对照指令表执行 for k, v in pairs(commands) do local head, tail = string.find(strin, k) if tail then -- 成功匹配 local pams = string.sub(strin, tail + 1) -- 切割参数 pams = string_split(pams, "|") -- 执行对应函数 v(id, pams) end end end -- 通过system-login|前缀分发到对应函数 commands['system%-login|'] = function(id, pams) local roleId = tonumber(pams[1]) local role = roleMgr:get_rol_by_id(roleId) if type(role) == 'string' then -- 登录失败,发回原因 print(role) send(id, 0, role) else -- 登录成功,发回role信息 print(role) send(id, 1, role.name) end end ``` 所有指令使用相同的约定发送、处理,要新增、减少一种指令都相对方便 指令数量增加后,应抽象出服务器指令类,每一条指令继承该类,通过配置决定使用哪些指令,并在服务器启动时注册 ### 如何解决大请求中缓冲区限制带来的问题 一个前置问题是,skynet.socket服务器向客服端连续发送多条消息时,消息之间仅仅用空格分隔 实际发送时,先存入缓冲区,再一次性发送 因此,客户端可能一次就接收到多条信息 如上所述,每条消息都是一条指令,用空格分割会导致消息不完整,因此手动使用换行符作为分割,并在客户端设置缓存队列,将接收指令和处理指令分离 ```lua -- server:连续发送多条指令时,在结尾添加换行符 send(id, 1, tonumber(#res) .. "\n") -- client local receiveQueue = Queue:new() -- 接受消息,并存入本地缓存用的消息队列 local function receive() local res = readServer(1, fd) if res == nil then return end res = string_split(res, "\n") for i, v in ipairs(res) do if v ~= nil then receiveQueue:Enqueue(v) end end end -- 客服端处理消息与接受消息分离 local function executeRes() -- 从队列中取出指令处理 local res = receiveQueue:Dequeue() ... end ``` 但是,这还不能解决单次请求数据量超出缓冲区(并非上述Queue,而是socket层面的缓冲区)时引发的问题 如果缓冲区只能放下120.5条角色信息,但服务器一次发送了130条,就有9.5条角色信息被丢弃了 对此,由两种解决思路 #### 力大砖飞:扩大缓冲区 只要让缓冲区永远大于服务器要发送的数据量就可以 肯定能解决问题,但是早晚会遇到解决不了的时候 #### 控制发送速度 让服务端和客户端交流,缓冲区将满时停止发送,缓冲区富余时继续发送,直到发完 由于显式的交流做起来比较麻烦,工程中通过基于经验的休眠实现不可靠但做起来比较方便的速度控制 > 根据测试,每秒发50个角色的信息不会产生丢包或客户端处理异常 ```lua -- server:每发50条休眠1秒,等待client读取缓冲区内容 for i, v in ipairs(res) do send(id, 1, res[i]:__tostring() .. "\n") if i % 50 == 0 then skynet.sleep(100) end end -- client:每秒读取1次缓冲区 if type(num) == 'number' then for i = 1, num, 1 do -- 读取在此进行,每显示50行读取一次 if i % 50 == 0 then receive() end local info while info == nil do info = executeRes() end print(i, info) -- 显示间隔为1/50秒,用于使显示过程现得流畅,实际数据每秒更新一次 socket.usleep(20000) end end ``` ## 额外功能 ### Splatoon-like名字生成 为了便于区分角色,需要给角色不同的名字 出于个人趣味,模仿一下Splatoon > /src/NameGenerator.lua 名字由形容词【前缀】和名词【后缀】加上连接词"的"组合而成 生成角色时,随机匹配一个名字 如【忧郁的武装直升机】,【聪明绝顶的生鱼片】等