Unity Mirror插件WebGL端多人联机实现

server/2024/12/26 13:45:21/

unity-mirror-webgl-test" rel="nofollow" title="Demo地址">Demo地址[这里是图片001]https://gitee.com/njiyue/unity-mirror-webgl-test

使用Mirror插件及其开源的SimpleWebTransport实现,简单记录下遇到的问题。详细原理就不多介绍了哈~ Unity版本:2022.3.48f1c1


1. Unity导入unity.com/packages/tools/network/mirror-129321" rel="nofollow" title="mirror插件">mirror插件、SimpleWebTransport包


2. 报错:

导入插件和SimpleWebTransportSimpleWebTransport包后,会报这个错:

原因:程序集关联有问题

解决方法:黑色的那个SimpleWebTransport脚本文件放到SimpleWeb目录下


3. 报错:

原因:由于SimpleWebTransport包太久没被维护了,导致与最新版本的Mirror插件API对不上。

解决方法:

更改SimpleWebTransport 脚本的内容

更改为:

using System;
using System.Net;
using System.Net.Sockets;
using System.Security.Authentication;
using UnityEngine;
using UnityEngine.Serialization;namespace Mirror.SimpleWeb
{public class SimpleWebTransport : Transport{public const string NormalScheme = "ws";public const string SecureScheme = "wss";[Tooltip("Port to use for server and client")]public ushort port = 7778;[Tooltip("Protect against allocation attacks by keeping the max message size small. Otherwise an attacker might send multiple fake packets with 2GB headers, causing the server to run out of memory after allocating multiple large packets.")]public int maxMessageSize = 16 * 1024;[Tooltip("Max size for http header send as handshake for websockets")]public int handshakeMaxSize = 3000;[Tooltip("disables nagle algorithm. lowers CPU% and latency but increases bandwidth")]public bool noDelay = true;[Tooltip("Send would stall forever if the network is cut off during a send, so we need a timeout (in milliseconds)")]public int sendTimeout = 5000;[Tooltip("How long without a message before disconnecting (in milliseconds)")]public int receiveTimeout = 20000;[Tooltip("Caps the number of messages the server will process per tick. Allows LateUpdate to finish to let the reset of unity contiue incase more messages arrive before they are processed")]public int serverMaxMessagesPerTick = 10000;[Tooltip("Caps the number of messages the client will process per tick. Allows LateUpdate to finish to let the reset of unity contiue incase more messages arrive before they are processed")]public int clientMaxMessagesPerTick = 1000;[Header("Server settings")][Tooltip("Groups messages in queue before calling Stream.Send")]public bool batchSend = true;[Tooltip("Waits for 1ms before grouping and sending messages. " +"This gives time for mirror to finish adding message to queue so that less groups need to be made. " +"If WaitBeforeSend is true then BatchSend Will also be set to true")]public bool waitBeforeSend = false;[Header("Ssl Settings")][Tooltip("Sets connect scheme to wss. Useful when client needs to connect using wss when TLS is outside of transport, NOTE: if sslEnabled is true clientUseWss is also true")]public bool clientUseWss;public bool sslEnabled;[Tooltip("Path to json file that contains path to cert and its passwordUse Json file so that cert password is not included in client buildsSee cert.example.Json")]public string sslCertJson = "./cert.json";public SslProtocols sslProtocols = SslProtocols.Tls12;[Header("Debug")][Tooltip("Log functions uses ConditionalAttribute which will effect which log methods are allowed. DEBUG allows warn/error, SIMPLEWEB_LOG_ENABLED allows all")][FormerlySerializedAs("logLevels")][SerializeField] Log.Levels _logLevels = Log.Levels.none;/// <summary>/// <para>Gets _logLevels field</para>/// <para>Sets _logLevels and Log.level fields</para>/// </summary>public Log.Levels LogLevels{get => _logLevels;set{_logLevels = value;Log.level = _logLevels;}}void OnValidate(){if (maxMessageSize > ushort.MaxValue){Debug.LogWarning($"max supported value for maxMessageSize is {ushort.MaxValue}");maxMessageSize = ushort.MaxValue;}Log.level = _logLevels;}SimpleWebClient client;SimpleWebServer server;TcpConfig TcpConfig => new TcpConfig(noDelay, sendTimeout, receiveTimeout);public override bool Available(){return true;}public override int GetMaxPacketSize(int channelId = 0){return maxMessageSize;}void Awake(){Log.level = _logLevels;}public override void Shutdown(){client?.Disconnect();client = null;server?.Stop();server = null;}void LateUpdate(){ProcessMessages();}/// <summary>/// Processes message in server and client queues/// <para>Invokes OnData events allowing mirror to handle messages (Cmd/SyncVar/etc)</para>/// <para>Called within LateUpdate, Can be called by user to process message before important logic</para>/// </summary>public void ProcessMessages(){server?.ProcessMessageQueue(this);client?.ProcessMessageQueue(this);}#region Clientstring GetClientScheme() => (sslEnabled || clientUseWss) ? SecureScheme : NormalScheme;string GetServerScheme() => sslEnabled ? SecureScheme : NormalScheme;public override bool ClientConnected(){// not null and not NotConnected (we want to return true if connecting or disconnecting)return client != null && client.ConnectionState != ClientState.NotConnected;}public override void ClientConnect(string hostname){// connecting or connectedif (ClientConnected()){Debug.LogError("Already Connected");return;}UriBuilder builder = new UriBuilder{Scheme = GetClientScheme(),Host = hostname,Port = port};client = SimpleWebClient.Create(maxMessageSize, clientMaxMessagesPerTick, TcpConfig);if (client == null) { return; }client.onConnect += OnClientConnected.Invoke;client.onDisconnect += () =>{OnClientDisconnected.Invoke();// clear client here after disconnect event has been sent// there should be no more messages after disconnectclient = null;};client.onData += (ArraySegment<byte> data) => OnClientDataReceived.Invoke(data, Channels.Reliable);//——————————————————修改client.onError += (Exception e) =>{OnClientError.Invoke(TransportError.Unexpected, e.ToString());//——————————————————修改ClientDisconnect();};client.Connect(builder.Uri);}public override void ClientDisconnect(){// dont set client null here of messages wont be processedclient?.Disconnect();}//#if MIRROR_26_0_OR_NEWER //——————————————————注释掉public override void ClientSend(ArraySegment<byte> segment, int channelId = Channels.Reliable)//——————————————————修改{if (!ClientConnected()){Debug.LogError("Not Connected");return;}if (segment.Count > maxMessageSize){Log.Error("Message greater than max size");return;}if (segment.Count == 0){Log.Error("Message count was zero");return;}client.Send(segment);}//#else//——————————————————注释掉 Start//public override bool ClientSend(int channelId, ArraySegment<byte> segment)//{//    if (!ClientConnected())//    {//        Debug.LogError("Not Connected");//        return false;//    }//    if (segment.Count > maxMessageSize)//    {//        Log.Error("Message greater than max size");//        return false;//    }//    if (segment.Count == 0)//    {//        Log.Error("Message count was zero");//        return false;//    }//    client.Send(segment);//    return true;//}//#endif//——————————————————End#endregion#region Serverpublic override bool ServerActive(){return server != null && server.Active;}public override void ServerStart(){if (ServerActive()){Debug.LogError("SimpleWebServer Already Started");}SslConfig config = SslConfigLoader.Load(this);server = new SimpleWebServer(serverMaxMessagesPerTick, TcpConfig, maxMessageSize, handshakeMaxSize, config);server.onConnect += OnServerConnected.Invoke;server.onDisconnect += OnServerDisconnected.Invoke;server.onData += (int connId, ArraySegment<byte> data) => OnServerDataReceived.Invoke(connId, data, Channels.Reliable);server.onError += OnServerError.Invoke;SendLoopConfig.batchSend = batchSend || waitBeforeSend;SendLoopConfig.sleepBeforeSend = waitBeforeSend;server.Start(port);}public override void ServerStop(){if (!ServerActive()){Debug.LogError("SimpleWebServer Not Active");}server.Stop();server = null;}public override void ServerDisconnect(int connectionId)//——————————————————bool 修改为 void{if (!ServerActive()){Debug.LogError("SimpleWebServer Not Active");//return false;//——————————————————注释掉}//return server.KickClient(connectionId);//——————————————————注释掉}//#if MIRROR_26_0_OR_NEWER//——————————————————注释掉public override void ServerSend(int connectionId, ArraySegment<byte> segment, int channelId)//——————————————————修改{if (!ServerActive()){Debug.LogError("SimpleWebServer Not Active");return;}if (segment.Count > maxMessageSize){Log.Error("Message greater than max size");return;}if (segment.Count == 0){Log.Error("Message count was zero");return;}server.SendOne(connectionId, segment);return;}//#else//——————————————————注释掉 Start//public override bool ServerSend(System.Collections.Generic.List<int> connectionIds, int channelId, ArraySegment<byte> segment)//{//    if (!ServerActive())//    {//        Debug.LogError("SimpleWebServer Not Active");//        return false;//    }//    if (segment.Count > maxMessageSize)//    {//        Log.Error("Message greater than max size");//        return false;//    }//    if (segment.Count == 0)//    {//        Log.Error("Message count was zero");//        return false;//    }//    server.SendAll(connectionIds, segment);//    return true;//}//#endif//——————————————————Endpublic override string ServerGetClientAddress(int connectionId){return server.GetClientAddress(connectionId);}public override Uri ServerUri(){UriBuilder builder = new UriBuilder{Scheme = GetServerScheme(),Host = Dns.GetHostName(),Port = port};return builder.Uri;}#endregion}
}

更改SimpleWebServer脚本内容

更改为:

using System;
using System.Collections.Generic;
using UnityEngine;namespace Mirror.SimpleWeb
{public class SimpleWebServer{readonly int maxMessagesPerTick;readonly WebSocketServer server;readonly BufferPool bufferPool;public SimpleWebServer(int maxMessagesPerTick, TcpConfig tcpConfig, int maxMessageSize, int handshakeMaxSize, SslConfig sslConfig){this.maxMessagesPerTick = maxMessagesPerTick;// use max because bufferpool is used for both messages and handshakeint max = Math.Max(maxMessageSize, handshakeMaxSize);bufferPool = new BufferPool(5, 20, max);server = new WebSocketServer(tcpConfig, maxMessageSize, handshakeMaxSize, sslConfig, bufferPool);}public bool Active { get; private set; }public event Action<int> onConnect;public event Action<int> onDisconnect;public event Action<int, ArraySegment<byte>> onData;public event Action<int, TransportError, string> onError;//——————————————————修改public void Start(ushort port){server.Listen(port);Active = true;}public void Stop(){server.Stop();Active = false;}public void SendAll(List<int> connectionIds, ArraySegment<byte> source){ArrayBuffer buffer = bufferPool.Take(source.Count);buffer.CopyFrom(source);buffer.SetReleasesRequired(connectionIds.Count);// make copy of array before for each, data sent to each client is the sameforeach (int id in connectionIds){server.Send(id, buffer);}}public void SendOne(int connectionId, ArraySegment<byte> source){ArrayBuffer buffer = bufferPool.Take(source.Count);buffer.CopyFrom(source);server.Send(connectionId, buffer);}public bool KickClient(int connectionId){return server.CloseConnection(connectionId);}public string GetClientAddress(int connectionId){return server.GetClientAddress(connectionId);}public void ProcessMessageQueue(MonoBehaviour behaviour){int processedCount = 0;// check enabled every time incase behaviour was disabled after datawhile (behaviour.enabled &&processedCount < maxMessagesPerTick &&// Dequeue lastserver.receiveQueue.TryDequeue(out Message next)){processedCount++;switch (next.type){case EventType.Connected:onConnect?.Invoke(next.connId);break;case EventType.Data:onData?.Invoke(next.connId, next.data.ToSegment());next.data.Release();break;case EventType.Disconnected:onDisconnect?.Invoke(next.connId);break;case EventType.Error:onError?.Invoke(next.connId, TransportError.Unexpected, next.exception.ToString());//——————————————————修改break;}}}}
}

4. 更改 SimpleWeb.jslib 内容

更改为:

// this will create a global object
const SimpleWeb = {webSockets: [],next: 1,GetWebSocket: function (index) {return SimpleWeb.webSockets[index]},AddNextSocket: function (webSocket) {var index = SimpleWeb.next;SimpleWeb.next++;SimpleWeb.webSockets[index] = webSocket;return index;},RemoveSocket: function (index) {SimpleWeb.webSockets[index] = undefined;},
};function IsConnected(index) {var webSocket = SimpleWeb.GetWebSocket(index);if (webSocket) {return webSocket.readyState === webSocket.OPEN;}else {return false;}
}function Connect(addressPtr) {const address = Pointer_stringify(addressPtr);console.log("Connecting to " + address);// Create webSocket connection.webSocket = new WebSocket(address);webSocket.binaryType = 'arraybuffer';const index = SimpleWeb.AddNextSocket(webSocket);const reObj = 'JSInfoReceiver';//Unity接收信息的游戏对象// Connection openedwebSocket.addEventListener('open', function (event) {console.log("Connected to " + address);SendMessage(reObj,'OpenCallback', index);//Module.dynCall('vi', openCallbackPtr, [index]);});webSocket.addEventListener('close', function (event) {console.log("Disconnected from " + address);SendMessage(reObj,'CloseCallBack', index);//Module.dynCall('vi', closeCallBackPtr, [index]);});// Listen for messageswebSocket.addEventListener('message', function (event) {if (event.data instanceof ArrayBuffer) {// TODO dont alloc each timevar array = new Uint8Array(event.data);var arrayLength = array.length;var bufferPtr = _malloc(arrayLength);var dataBuffer = new Uint8Array(HEAPU8.buffer, bufferPtr, arrayLength);dataBuffer.set(array);SendMessage(reObj,'MessageCallback1', index);SendMessage(reObj,'MessageCallback2',  bufferPtr);SendMessage(reObj,'MessageCallback3',  arrayLength);//Module.dynCall('viii', messageCallbackPtr, [index, bufferPtr, arrayLength]);_free(bufferPtr);}else {console.error("message type not supported")}});webSocket.addEventListener('error', function (event) {console.error('Socket Error', event);SendMessage(reObj,'ErrorCallback', index);//Module.dynCall('vi', errorCallbackPtr, [index]);});return index;
}function Disconnect(index) {var webSocket = SimpleWeb.GetWebSocket(index);if (webSocket) {webSocket.close(1000, "Disconnect Called by Mirror");}SimpleWeb.RemoveSocket(index);
}function Send(index, arrayPtr, offset, length) {var webSocket = SimpleWeb.GetWebSocket(index);if (webSocket) {const start = arrayPtr + offset;const end = start + length;const data = HEAPU8.buffer.slice(start, end);webSocket.send(data);return true;}return false;
}const SimpleWebLib = {$SimpleWeb: SimpleWeb,IsConnected,Connect,Disconnect,Send
};
autoAddDeps(SimpleWebLib, '$SimpleWeb');
mergeInto(LibraryManager.library, SimpleWebLib);

更改原因:jslib中使用了Unity已弃用的API:Runtime.dynCall


5. 更改“WebSocketClientWebGl” 脚本内容

更改为:

using System;
using System.Collections.Generic;
using AOT;namespace Mirror.SimpleWeb
{public class WebSocketClientWebGl : SimpleWebClient{static readonly Dictionary<int, WebSocketClientWebGl> instances = new Dictionary<int, WebSocketClientWebGl>();/// <summary>/// key for instances sent between c# and js/// </summary>int index;internal WebSocketClientWebGl(int maxMessageSize, int maxMessagesPerTick) : base(maxMessageSize, maxMessagesPerTick){
#if !UNITY_WEBGL || UNITY_EDITORthrow new NotSupportedException();
#endif}public bool CheckJsConnected() => SimpleWebJSLib.IsConnected(index);public override void Connect(Uri serverAddress){
#if UNITY_WEBGLindex = SimpleWebJSLib.Connect(serverAddress.ToString()/*, OpenCallback, CloseCallBack, MessageCallback, ErrorCallback*/);
#elseindex = SimpleWebJSLib.Connect(serverAddress.ToString(), OpenCallback, CloseCallBack, MessageCallback, ErrorCallback);
#endifinstances.Add(index, this);state = ClientState.Connecting;}public override void Disconnect(){state = ClientState.Disconnecting;// disconnect should cause closeCallback and OnDisconnect to be calledSimpleWebJSLib.Disconnect(index);}public override void Send(ArraySegment<byte> segment){if (segment.Count > maxMessageSize){Log.Error($"Cant send message with length {segment.Count} because it is over the max size of {maxMessageSize}");return;}SimpleWebJSLib.Send(index, segment.Array, 0, segment.Count);}void onOpen(){receiveQueue.Enqueue(new Message(EventType.Connected));state = ClientState.Connected;}void onClose(){// this code should be last in this classreceiveQueue.Enqueue(new Message(EventType.Disconnected));state = ClientState.NotConnected;instances.Remove(index);}void onMessage(IntPtr bufferPtr, int count){try{ArrayBuffer buffer = bufferPool.Take(count);buffer.CopyFrom(bufferPtr, count);receiveQueue.Enqueue(new Message(buffer));}catch (Exception e){Log.Error($"onData {e.GetType()}: {e.Message}
{e.StackTrace}");receiveQueue.Enqueue(new Message(e));}}void onErr(){receiveQueue.Enqueue(new Message(new Exception("Javascript Websocket error")));Disconnect();}[MonoPInvokeCallback(typeof(Action<int>))]public static void OpenCallback(int index) => instances[index].onOpen();[MonoPInvokeCallback(typeof(Action<int>))]public static void CloseCallBack(int index) => instances[index].onClose();[MonoPInvokeCallback(typeof(Action<int, IntPtr, int>))]public static void MessageCallback(int index, IntPtr bufferPtr, int count) => instances[index].onMessage(bufferPtr, count);[MonoPInvokeCallback(typeof(Action<int>))]public static void ErrorCallback(int index) => instances[index].onErr();}
}

更改原因:公开静态方法供接下来的JSInfoReceiver对象 调用


6. 更改“SimpleWebJSLib” 脚本内容

更改为:

using System;
#if UNITY_WEBGL
using System.Runtime.InteropServices;
#endifnamespace Mirror.SimpleWeb
{internal static class SimpleWebJSLib{
#if UNITY_WEBGL[DllImport("__Internal")]internal static extern bool IsConnected(int index);#pragma warning disable CA2101 // Specify marshaling for P/Invoke string arguments[DllImport("__Internal")]
#pragma warning restore CA2101 // Specify marshaling for P/Invoke string argumentsinternal static extern int Connect(string address/*, Action<int> openCallback, Action<int> closeCallBack, Action<int, IntPtr, int> messageCallback, Action<int> errorCallback*/);[DllImport("__Internal")]internal static extern void Disconnect(int index);[DllImport("__Internal")]internal static extern bool Send(int index, byte[] array, int offset, int length);
#elseinternal static bool IsConnected(int index) => throw new NotSupportedException();internal static int Connect(string address, Action<int> openCallback, Action<int> closeCallBack, Action<int, IntPtr, int> messageCallback, Action<int> errorCallback) => throw new NotSupportedException();internal static void Disconnect(int index) => throw new NotSupportedException();internal static bool Send(int index, byte[] array, int offset, int length) => throw new NotSupportedException();
#endif}
}

7. Unity新建一个脚本,命名为 “JSInfoReceiver”

脚本内容:

using Mirror.SimpleWeb;
using System;
using UnityEngine;public class JSInfoReceiver : MonoBehaviour
{private int num1;private int num2;public void OpenCallback(int index) => WebSocketClientWebGl.OpenCallback(index);public void CloseCallBack(int index) => WebSocketClientWebGl.CloseCallBack(index);public void MessageCallback1(int index) => this.num1 = index;public void MessageCallback2(int index) => this.num2 = index;public void MessageCallback3(int index) => WebSocketClientWebGl.MessageCallback(num1, new IntPtr(num2), index);public void ErrorCallback(int index) => WebSocketClientWebGl.CloseCallBack(index);
}

8. Unity创建一个空物体,命名为 “JSInfoReceiver” ,挂载上JSInfoReceiver脚本组件

至此,就可以使用Mirror插件进行WebGL端的开发了,Transport改用SimpleWebTransport即可。


简单实现了一个demo(其实就是Mirror插件的案例简单改了一下)链接放文章开头了。开一个Windows程序做服务端,开两个WebGL做客户端加入,没得问题。

如果使用https,需要指定一个ssl证书,在程序根目录下创建一个cert.json文件,写入一个如图所示的Cert对象的json字符串,path和password分别指向ssl证书的路径和密码应该就可以了(暂时没有测试)。

参考:UnityWebGL使用Mirror进行多人在线遇到的问题


http://www.ppmy.cn/server/153331.html

相关文章

InnoDB存储引擎【MySQL从放弃到入门】

文章目录 InnoDB存储引擎【MySQL从放弃到入门】1.逻辑架构1.1 一条SQL语句是怎么执行的呢&#xff1f;1.2 MySQL存储引擎有哪些&#xff1f; 2.MySQL一行记录是怎么存储的&#xff1f;2.1 NULL值是如何存储的&#xff1f; 3.char和varchar的区别&#xff1f;4.数据页4.1 聚簇索…

《Vue进阶教程》(12)ref的实现详细教程

1 为什么需要ref 由于proxy只能代理引用类型数据(如: 对象, 数组, Set, Map...), 需要一种方式代理普通类型数据(String, Number, Boolean...) 设计ref主要是为了处理普通类型数据, 使普通类型数据也具有响应式 除此之外, 通过reactive代理的对象可能会出现响应丢失的情况. …

阿里巴巴2017实习生笔试题(二)

阿里巴巴2017实习生笔试题&#xff08;二&#xff09; 2024/12/25 1.下面哪一个不是动态链接库的优点&#xff1f; B A.共享 B.装载速度快 C.开发模式好 D.减少页面交换 解析 1 静态链接库的优点 (1) 代码装载速度快&#xff0c;执行速度略比动态链接库快&#xff1b;…

网站服务器被攻击了怎么办?

当网站服务器被攻击时&#xff0c;可能会出现各种问题&#xff0c;如服务中断、数据泄露、恶意软件感染等。如果不及时采取措施&#xff0c;可能会给企业带来严重的损失。因此&#xff0c;当网站服务器被攻击时&#xff0c;企业需要采取以下措施来应对&#xff1a; 一、快速定…

MYSQL 架构

MySQL 架构设计灵活&#xff0c;采用模块化的分层架构&#xff0c;分为三大层次&#xff1a;连接层、服务层 和 存储引擎层。这种设计让 MySQL 能够适应不同的使用场景并支持多种存储引擎&#xff0c;以下是对其架构的详细解析&#xff1a; 1. 连接层&#xff08;Connection La…

Vue3中使用Router进行路由配置(图文详情)

Vue3中使用Router进行路由配置 Vue Router 简介 Vue Router 是 Vue.js 官方的路由管理器&#xff0c;它允许您在单页面应用程序&#xff08;SPA, Single Page Application&#xff09;中实现导航和页面切换&#xff0c;而无需重新加载整个页面。通过 Vue Router&#xff0c;您…

undefined reference to `vtable for错误

QT构建报错&#xff1a; D:\code\QGraphicsScaleTest\main.cpp:-1: error: undefined reference to vtable for ResizableSvgViewVS编译报错&#xff1a; 1>main.obj : error LNK2001: 无法解析的外部符号 "public: virtual struct QMetaObject const * __cdecl Resi…

大模型讲师叶梓分享前沿论文:ChatDoctor——基于大模型的医疗聊天机器人

人工智能咨询培训老师叶梓 转载标明出处 人工智能讲师培训咨询老师叶梓分享前沿技术&#xff1a;基于大模型的医疗聊天机器人 大模型在医疗领域的应用仍相对有限&#xff0c;通用领域模型在提供医疗建议时常常出现错误。为了解决这一问题&#xff0c;Li等人提出了一个名为ChatD…