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进行多人在线遇到的问题