携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第23天,点击查看活动详情
自定义NetworkDiscovery
Mirror提供的NetworkDiscovery组件只提供了基础功能,特别是对于服务器信息,只有Uri这一项。而实际开发游戏时,往往需要知道服务器的一些附加信息,比如游戏模式,当前人数等等,另外NetworkDiscovery客户端发送广播包时也可以加入一些参数来过滤服务器,比如游戏模式。所有这些都可以通过自定义NetworkDiscovery实现。
通过脚本模板创建自定义NetworkDiscovery代码框架
Mirror插件安装后,在Assets目录下面会有一个ScriptTemplates目录,这个目录是放置Unity代码模板的地方:
安装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包进行阻拦。