Unity Mirror 之NetworkDiscovery局域网服务器查找详解(3)

834 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第23天,点击查看活动详情

自定义NetworkDiscovery

Mirror提供的NetworkDiscovery组件只提供了基础功能,特别是对于服务器信息,只有Uri这一项。而实际开发游戏时,往往需要知道服务器的一些附加信息,比如游戏模式,当前人数等等,另外NetworkDiscovery客户端发送广播包时也可以加入一些参数来过滤服务器,比如游戏模式。所有这些都可以通过自定义NetworkDiscovery实现。

通过脚本模板创建自定义NetworkDiscovery代码框架

Mirror插件安装后,在Assets目录下面会有一个ScriptTemplates目录,这个目录是放置Unity代码模板的地方:

image.png

安装Mirror的代码模板后,就可以在Assets中通过右键Create > Mirror > Network Discovery来创建一个自定义的NetworkDiscovery.cs文件,其中包含了继承自NetworkDiscoveryBase的自定义类,以及自定义的消息,相关虚函数也提供了实现的框架。

自定义DiscoveryRequest消息

该消息是Discovery客户端通过广播在局域网中发送的寻找服务器的请求消息。默认情况这个消息不包含任意成员:

    public class DiscoveryRequest : NetworkMessage
    {
        // Add properties for whatever information you want sent by clients
        // in their broadcast messages that servers will consume.
    }

需要注意的是,DiscoveryRequest这个类没有成员并不是最终发送的UDP包没有payload。实际上,Mirror会在Network Discovery相关的UDP包的payload上添加一个shake hand secrect,比如在发送广播包时:

public void BroadcastDiscoveryRequest()
{
    //省略部分代码
    
    using (NetworkWriterPooled writer = NetworkWriterPool.Get())
    {
        writer.WriteLong(secretHandshake);//UDP负载中先写入握手密码

        try
        {
            Request request = GetRequest();

            writer.Write(request);//写入DiscoveryRequest

            ArraySegment<byte> data = writer.ToArraySegment();

            clientUdpClient.SendAsync(data.Array, data.Count, endPoint);            
        }
        catch (Exception)
        {
            // It is ok if we can't broadcast to one of the addresses                    
        }
    }
}

如果在DiscoveryRequest中加入服务器过滤条件,则可以在ProcessRequest方法中处理,同时这个方法也是创建服务返回消息的地方。

自定义 DiscoveryResponse消息

该消息是服务器收到客户端的广播消息之后,返回给客户端的消息,包含了服务器uri和其他信息。

    public class DiscoveryResponse : NetworkMessage
    {
        // Add properties for whatever information you want the server to return to
        // clients for them to display or consume for establishing a connection.
        
        // Prevent duplicate server appearance when a connection can be made via LAN on multiple NICs
        public long serverId;
        public Uri uri;
        public int playersCount;
    }

在这个例子里面,我主要添加了 playersCount 信息,表示当前连接到服务器上的玩家人数。另外,serverId是为了区别多NIC,这个上一篇说了。uri包含了服务器地址,上一篇也说了直接从transport获取到的地址的Host可能是主机名,因此还需要再处理。

自定义Serfver Discovery Unity事件

[Serializable]
public class ServerDiscoveryUnityEvent : UnityEvent<DiscoveryResponse> {};

该事件是客户端接收到服务器握手包之后,调用的UnityEvent。通过这个Event可以注册逻辑代码来刷新UI。

MyNetworkDiscovery类

下面具体分析MyNetworkDiscovery这个自定义的NetworkDiscovery类

成员和初始化

public class MyNetworkDiscovery : NetworkDiscoveryBase<DiscoveryRequest, DiscoveryResponse>
{
        
        public long ServerId { get; private set; }

        [Tooltip("Transport to be advertised during discovery")]
        public Transport transport;

        [Tooltip("Invoked when a server is found")]
        public ServerDiscoveryUnityEvent OnServerFound;

        public override void Start()
        {
            ServerId = RandomLong();

            // active transport gets initialized in awake
            // so make sure we set it here in Start()  (after awakes)
            // Or just let the user assign it in the inspector
            if (transport == null)
                transport = Transport.activeTransport;

            base.Start();
        }
}
  • 首先,该类继承自泛型类NetworkDiscoveryBase,两个泛型参数分别是自定义的Request和Response类。
  • 成员包含 ServerId,这个之前已经说了,在Start()方法中,使用RandomLong()给它赋值。这个是来自于NetworkDiscovery.cs中的做法
  • 成员要包含 transport,并在Start()中获取,这个主要是为了获取服务器的Uri。
  • 成员中包含了一个ServerDiscoveryUnityEvent事件变量OnServerFound。

服务器接收到客户端广播请求的处理

protected override DiscoveryResponse ProcessRequest(DiscoveryRequest request, IPEndPoint endpoint) 
{
    return new DiscoveryResponse()
    {
        serverId = ServerId,
        uri = transport.ServerUri(),
        playersCount = NetworkServer.connections.Count                
    };
}

这个方法是实现了基类中的虚方法,该方法在ProcessClientRequest中被调用,通过该方法创建Response对象。在这个例子中,我们没有在request中添加内容,因此没有处理他。如果你的request中有一些过滤条件,则可以在这儿进行判断,比如人数不大于几人,和服务器上的人数进行比较,如果不满足,就不发送response。这个方法最主要的功能是返回一个自定义的DiscoveryResponse对象,这儿我们初始化了该对象的所有成员,比如playersCount直接从服务器连接数获取。

客户端接收到服务器response包的处理

protected override void ProcessResponse(DiscoveryResponse response, IPEndPoint endpoint) 
{         

    // although we got a supposedly valid url, we may not be able to resolve
    // the provided host
    // However we know the real ip address of the server because we just
    // received a packet from it,  so use that as host.
    UriBuilder realUri = new UriBuilder(response.uri)
    {
        Host = endpoint.Address.ToString()
    };
    response.uri = realUri.Uri;

    OnServerFound.Invoke(response);
}

这也是一个虚函数的实现,该函数是在ReceiveGameBroadcastAsync中被调用,该函数的参数是接收到的response包以及发送方(即服务器)的IPEndPoint。通过IPEndPoint我们可以获取到服务器的IP,这样最终才可以使用该IP连接上游戏服务器。这个方法主要做两件事。一是对uri的Host重新计算,这个之前已经说过了。实际上,如果服务器能提供正确的IP是不需要在这儿处理的。比如服务器获取自己的真实IP然后填充到Host上再发送过来。我们还是遵循了NetwrokDiscovery默认的实现方法,在这儿处理了。另外一件事就是调用OnServerFound事件刷新UI。

总结

这个系列是我在使用Mirror开发游戏大厅时的经验总结。因为我们的游戏是一个随时可以加入的游戏,所以没有严格意义的房间等待的过程。在游戏大厅只要刷新出服务器,就可以直接去连接。整个过程都是使用自定义的NetworkDiscovery去实现的,还是比较简单的。如果需要实现游戏房间,Mirror也提供了NetworkRoomManager。这其实相当于起了一个房间服务器,但发现房间本身还是要靠NetworkDiscovery。当然了,我这儿说的都是局域网游戏,由于没有中心大厅服务器或匹配服务器的存在,其实是比较麻烦的,而且不稳定。因为UDP包在玩家的局域网内有可能有问题,比如路由器固件可能会对WIFI设备上的UDP包进行阻拦。