开发简记-多人联机游戏demo(socket实现)

本文最后更新于:2 年前


目的

做一个仿MC的简单场景,多名玩家可以联机在其中移动,破坏和放置方块

要求是尽量简单、可拓展性强,能作为联机模板使用

本篇是一个开发简记(乱记)

主要逻辑

工程文件已上传Github,这里就不贴了

服务端保存所有在线玩家的昵称和位置信息,以及世界中每一个方块的信息(当然,世界要是大的话这并不科学…)

然后处理的请求包括用户登录,登出,修改方块,修改玩家位置;另外需要支持向所有玩家广播消息,需要广播的有所有玩家的位置信息,和玩家登录登出消息。

客户端处理所有游戏本身的东西,向服务端发送登录登出,位置信息,修改方块请求。

连接时,双方建立Connect之后,服务端即为这一客户端开启一个子线程专门用来接收,客户端也开启监听;随后客户端可以发送登录请求,由服务端检测用户名重复等问题后,回应一个允许登录或错误信息,若允许登录,服务端就把这一用户名和Socket等信息录入,客户端接收到允许登录后,请求世界信息(第一次要求发送全部世界的信息,文本量较大容易失败),若请求成功则登入,若请求失败则发送登出信息,同时关闭客户端的监听,服务端收到登出消息也会删除其用户信息和关闭相应监听。


一个坑:

发现对Socket的理解不够准确,这里明确一些东西:

  • Socket无需频繁地建立,一次Connect和Accept即可让双方都获得对方Socket,将这个Socket保存起来即可不断地进行Send和Rreceive;在服务端中,每监听到一个连接就为其单独开一个子线程来循环接收其消息。这里不需要担心端口占用的问题,我的理解是:Socket当中会存储两组IP+端口信息,分别是自身的和对方的,在服务端同时执行多个Socket的Receive方法时,消息到达端口后还会根据其存储的源IP+端口信息,再次分配给指定的Socket接收。简言之,一个进程中的多个Socket监听同一个端口,来自这个端口的消息还是会根据来源地址信息分流。
  • 规范而言,一次Send后需要Receive一次来确定消息是否到达(因为Send并不是阻塞方法,发出去就不管了,而网络通信由于网络波动,中间出错并不少见),但出于性能考虑我们应该灵活变通,对于重要消息(如登录等)需要确认,对于频繁调用的不重要消息(玩家更新位置)等可以不必确认到达。

一个坑:

之前为了避免连接问题影响本地运行,把客户端的监听放到子线程里了,但是现在发现——unity不允许子线程修改任何跟unity有关的东西,包括场景切换,修改场景物体等;

百度了一波发现,untiy其实是一个单线程引擎,多个脚本间和协程其实是通过Time slicing(时间分片)完成的,这样做的话能有固定的运行顺序,不容易引发异步导致的各种冲突。故不允许子线程修改游戏本身内容其实很好理解;

绕过去的方法,最粗暴的就是搞一个bool,在子线程中设为1,主线程中监控这个bool。。。

显然这样的做法过于不雅,但也没找到特别优雅的解决方案。我目前的做法是搞一个todoList队列,子进程只负责监听,收到消息后将消息扔进队列,主线程中发现队列中有内容就去完成它。

这个思路同样可以用于服务端,在服务端为多个客户开多个监听线程时,这样避免异步导致的冲突

为了进一步避免线程间的资源竞争,需要引入封锁机制,这里也只需要对toDoList的存取做封锁就可以了


遇到了一个很奇怪的bug:

向服务器发送消息成功了,返回消息也成功了,但客户端没有任何反应

反复查找,最后发现客户端在刚连上时接收到了下面这条消息:

{"type":"UpdateAllUser","info":"[{\"playername\":\"tyin\",\"px\":20.0,\"py\":20.0,\"pz\":20.0,\"rx\":0.0,\"ry\":0.0,\"rz\":0.0}]"}{"type":"UpdateAllUser","info":"[{\"playername\":\"tyin\",\"px\":20.0,\"py\":20.0,\"pz\":20.0,\"rx\":0.0,\"ry\":0.0,\"rz\":0.0}]"}{"type":"UpdateAllUser","info":"[{\"playername\":\"tyin\",\"px\":20.0,\"py\":20.0,\"pz\":20.0,\"rx\":0.0,\"ry\":0.0,\"rz\":0.0}]"}

接收到的三条正常的信息合并到一条了,导致Json转义失败

这是由于服务端处理用户登录请求后,就把用户信息登记上了,此时登录成功的消息还没有发给客户,客户尚未开始监听,而服务端已经开始向客户推送消息;这些消息就堆叠了起来。。在客户开始监听时一次性接收。。

解决方案:

让服务端接受登录后延迟推送显然不可行(就变成来自客户端的消息堆叠了)

所以就在每条消息的后面加上了一个’\0’,接收后先根据此标识拆分成多个字符串。


开发简记-多人联机游戏demo(socket实现)
http://www.lxtyin.ac.cn/2022/01/19/2022-01-19-开发简记-多人联机游戏demo(socket实现)/
作者
lx_tyin
发布于
2022年1月19日
许可协议